Add MSAL Authentication Library support (#21024)

This commit is contained in:
Christopher Suh
2022-11-23 17:06:44 -05:00
committed by GitHub
parent fba47815e2
commit 86c3f315f2
32 changed files with 1502 additions and 320 deletions

View File

@@ -19,7 +19,10 @@ const externals = {
'universalify': 'commonjs universalify', 'universalify': 'commonjs universalify',
'@azure/arm-subscriptions': 'commonjs @azure/arm-subscriptions', '@azure/arm-subscriptions': 'commonjs @azure/arm-subscriptions',
'@azure/arm-resourcegraph': 'commonjs @azure/arm-resourcegraph', '@azure/arm-resourcegraph': 'commonjs @azure/arm-resourcegraph',
'@azure/storage-blob': 'commonjs @azure/storage-blob' '@azure/storage-blob': 'commonjs @azure/storage-blob',
'@azure/msal-node': 'commonjs @azure/msal-node',
'@azure/msal-node-extensions': 'commonjs @azure/msal-node-extensions',
'msal': 'commonjs msal'
}; };
// conditionally add ws if we are going to be running in a node environment // conditionally add ws if we are going to be running in a node environment

View File

@@ -124,6 +124,19 @@
"Verbose", "Verbose",
"All" "All"
] ]
},
"azure.authenticationLibrary": {
"type": "string",
"description": "%config.authenticationLibrary%",
"default": "ADAL",
"enum": [
"ADAL",
"MSAL"
],
"enumDescriptions": [
"Azure Active Directory Authentication Library",
"Microsoft Authentication Library"
]
} }
} }
} }
@@ -348,7 +361,10 @@
"@azure/arm-resourcegraph": "^4.0.0", "@azure/arm-resourcegraph": "^4.0.0",
"@azure/arm-subscriptions": "^3.0.0", "@azure/arm-subscriptions": "^3.0.0",
"@azure/storage-blob": "^12.6.0", "@azure/storage-blob": "^12.6.0",
"@azure/msal-node": "^1.9.0",
"@azure/msal-node-extensions": "^1.0.0-alpha.25",
"axios": "^0.27.2", "axios": "^0.27.2",
"msal": "^1.4.16",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"qs": "^6.9.1", "qs": "^6.9.1",
"universalify": "^0.1.2", "universalify": "^0.1.2",

View File

@@ -31,6 +31,7 @@
"config.azureDeviceCodeMethod": "Device Code Method", "config.azureDeviceCodeMethod": "Device Code Method",
"config.noSystemKeychain": "Disable system keychain integration. Credentials will be stored in a flat file in the user's home directory.", "config.noSystemKeychain": "Disable system keychain integration. Credentials will be stored in a flat file in the user's home directory.",
"config.piiLogging": "Should Personally Identifiable Information (PII) be logged in the Azure Accounts output channel and the output channel log file.", "config.piiLogging": "Should Personally Identifiable Information (PII) be logged in the Azure Accounts output channel and the output channel log file.",
"config.loggingLevel": "[Optional] The verbosity of logging for the Azure Accounts extension." "config.loggingLevel": "[Optional] The verbosity of logging for the Azure Accounts extension.",
"config.authenticationLibrary": "The library used for the AAD auth flow. Please restart ADS after changing this option."
} }

View File

@@ -5,7 +5,6 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { import {
@@ -15,51 +14,55 @@ import {
Resource, Resource,
Tenant Tenant
} from 'azurecore'; } from 'azurecore';
import { Deferred } from '../interfaces'; import { Deferred } from '../interfaces';
import * as url from 'url'; import * as url from 'url';
import * as Constants from '../../constants';
import { SimpleTokenCache } from '../simpleTokenCache'; import { SimpleTokenCache } from '../simpleTokenCache';
import { MemoryDatabase } from '../utils/memoryDatabase'; import { MemoryDatabase } from '../utils/memoryDatabase';
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { Logger } from '../../utils/Logger'; import { Logger } from '../../utils/Logger';
import * as qs from 'qs'; import * as qs from 'qs';
import { AzureAuthError } from './azureAuthError'; import { AzureAuthError } from './azureAuthError';
import { AccountInfo, AuthenticationResult, InteractionRequiredAuthError, PublicClientApplication } from '@azure/msal-node';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
export abstract class AzureAuth implements vscode.Disposable { export abstract class AzureAuth implements vscode.Disposable {
public static ACCOUNT_VERSION = '2.0';
protected readonly memdb = new MemoryDatabase<string>(); protected readonly memdb = new MemoryDatabase<string>();
protected readonly WorkSchoolAccountType: string = 'work_school';
protected readonly MicrosoftAccountType: string = 'microsoft';
protected readonly loginEndpointUrl: string; protected readonly loginEndpointUrl: string;
public readonly commonTenant: Tenant; public readonly commonTenant: Tenant;
public readonly organizationTenant: Tenant;
protected readonly redirectUri: string; protected readonly redirectUri: string;
protected readonly scopes: string[]; protected readonly scopes: string[];
protected readonly scopesString: string; protected readonly scopesString: string;
protected readonly clientId: string; protected readonly clientId: string;
protected readonly resources: Resource[]; protected readonly resources: Resource[];
private _authLibrary: string | undefined;
constructor( constructor(
protected readonly metadata: AzureAccountProviderMetadata, protected readonly metadata: AzureAccountProviderMetadata,
protected readonly tokenCache: SimpleTokenCache, protected readonly tokenCache: SimpleTokenCache,
protected readonly context: vscode.ExtensionContext, protected readonly context: vscode.ExtensionContext,
protected clientApplication: PublicClientApplication,
protected readonly uriEventEmitter: vscode.EventEmitter<vscode.Uri>, protected readonly uriEventEmitter: vscode.EventEmitter<vscode.Uri>,
protected readonly authType: AzureAuthType, protected readonly authType: AzureAuthType,
public readonly userFriendlyName: string public readonly userFriendlyName: string,
public readonly authLibrary: string
) { ) {
this._authLibrary = authLibrary;
this.loginEndpointUrl = this.metadata.settings.host; this.loginEndpointUrl = this.metadata.settings.host;
this.commonTenant = { this.commonTenant = {
id: 'common', id: 'common',
displayName: 'common', displayName: 'common',
}; };
this.organizationTenant = {
id: 'organizations',
displayName: 'organizations',
};
this.redirectUri = this.metadata.settings.redirectUri; this.redirectUri = this.metadata.settings.redirectUri;
this.clientId = this.metadata.settings.clientId; this.clientId = this.metadata.settings.clientId;
this.resources = [ this.resources = [
this.metadata.settings.armResource, this.metadata.settings.armResource,
this.metadata.settings.graphResource, this.metadata.settings.graphResource,
@@ -100,10 +103,29 @@ export abstract class AzureAuth implements vscode.Disposable {
if (!this.metadata.settings.microsoftResource) { if (!this.metadata.settings.microsoftResource) {
throw new Error(localize('noMicrosoftResource', "Provider '{0}' does not have a Microsoft resource endpoint defined.", this.metadata.displayName)); throw new Error(localize('noMicrosoftResource', "Provider '{0}' does not have a Microsoft resource endpoint defined.", this.metadata.displayName));
} }
const result = await this.login(this.commonTenant, this.metadata.settings.microsoftResource); if (this._authLibrary === Constants.AuthLibrary.MSAL) {
const result = await this.loginMsal(this.organizationTenant, this.metadata.settings.microsoftResource);
loginComplete = result.authComplete;
if (!result?.response || !result.response?.account) {
Logger.error(`Authentication failed: ${loginComplete}`);
return {
canceled: false
};
}
const token: Token = {
token: result.response.accessToken,
key: result.response.account.homeAccountId,
tokenType: result.response.tokenType
};
const tokenClaims = <TokenClaims>result.response.idTokenClaims;
const account = await this.hydrateAccount(token, tokenClaims);
loginComplete?.resolve();
return account;
} else {// fallback to ADAL as default
const result = await this.loginAdal(this.commonTenant, this.metadata.settings.microsoftResource);
loginComplete = result.authComplete; loginComplete = result.authComplete;
if (!result?.response) { if (!result?.response) {
Logger.error('Authentication failed'); Logger.error('Authentication failed - no response');
return { return {
canceled: false canceled: false
}; };
@@ -111,8 +133,9 @@ export abstract class AzureAuth implements vscode.Disposable {
const account = await this.hydrateAccount(result.response.accessToken, result.response.tokenClaims); const account = await this.hydrateAccount(result.response.accessToken, result.response.tokenClaims);
loginComplete?.resolve(); loginComplete?.resolve();
return account; return account;
}
} catch (ex) { } catch (ex) {
Logger.error('Login failed'); Logger.error(`Login failed: ${ex}`);
if (ex instanceof AzureAuthError) { if (ex instanceof AzureAuthError) {
if (loginComplete) { if (loginComplete) {
loginComplete.reject(ex); loginComplete.reject(ex);
@@ -133,9 +156,9 @@ export abstract class AzureAuth implements vscode.Disposable {
} }
} }
public async refreshAccess(account: AzureAccount): Promise<AzureAccount> { public async refreshAccessAdal(account: AzureAccount): Promise<AzureAccount> {
// Deprecated account - delete it. // Deprecated account - delete it.
if (account.key.accountVersion !== AzureAuth.ACCOUNT_VERSION) { if (account.key.accountVersion !== Constants.AccountVersion) {
account.delete = true; account.delete = true;
return account; return account;
} }
@@ -144,7 +167,7 @@ export abstract class AzureAuth implements vscode.Disposable {
// We want to return the one that owns the Azure account. // We want to return the one that owns the Azure account.
// Not doing so can result in token being issued for the wrong tenant // Not doing so can result in token being issued for the wrong tenant
const tenant = account.properties.owningTenant; const tenant = account.properties.owningTenant;
const tokenResult = await this.getAccountSecurityToken(account, tenant.id, azdata.AzureResource.MicrosoftResourceManagement); const tokenResult = await this.getAccountSecurityTokenAdal(account, tenant.id, azdata.AzureResource.MicrosoftResourceManagement);
if (!tokenResult) { if (!tokenResult) {
account.isStale = true; account.isStale = true;
return account; return account;
@@ -154,23 +177,28 @@ export abstract class AzureAuth implements vscode.Disposable {
} catch (ex) { } catch (ex) {
if (ex instanceof AzureAuthError) { if (ex instanceof AzureAuthError) {
void vscode.window.showErrorMessage(ex.message); void vscode.window.showErrorMessage(ex.message);
Logger.error(ex.originalMessageAndException); Logger.error(`Error refreshing access for account ${account.displayInfo.displayName}`, ex.originalMessageAndException);
} else { } else {
Logger.error(ex); Logger.error(ex);
} }
Logger.error(ex);
account.isStale = true; account.isStale = true;
return account; return account;
} }
} }
public async hydrateAccount(token: Token | AccessToken, tokenClaims: TokenClaims): Promise<AzureAccount> { public async hydrateAccount(token: Token | AccessToken, tokenClaims: TokenClaims): Promise<AzureAccount> {
const tenants = await this.getTenants({ ...token }); let account: azdata.Account;
const account = this.createAccount(tokenClaims, token.key, tenants); if (this._authLibrary === Constants.AuthLibrary.MSAL) {
const tenants = await this.getTenantsMsal(token.token);
account = this.createAccount(tokenClaims, token.key, tenants);
} else { // fallback to ADAL as default
const tenants = await this.getTenantsAdal({ ...token });
account = this.createAccount(tokenClaims, token.key, tenants);
}
return account; return account;
} }
public async getAccountSecurityToken(account: AzureAccount, tenantId: string, azureResource: azdata.AzureResource): Promise<Token | undefined> { public async getAccountSecurityTokenAdal(account: AzureAccount, tenantId: string, azureResource: azdata.AzureResource): Promise<Token | undefined> {
if (account.isStale === true) { if (account.isStale === true) {
Logger.error('Account was stale. No tokens being fetched.'); Logger.error('Account was stale. No tokens being fetched.');
return undefined; return undefined;
@@ -178,8 +206,7 @@ export abstract class AzureAuth implements vscode.Disposable {
const resource = this.resources.find(s => s.azureResourceId === azureResource); const resource = this.resources.find(s => s.azureResourceId === azureResource);
if (!resource) { if (!resource) {
Logger.error('Invalid resource, not fetching', azureResource); Logger.error(`Unable to find Azure resource ${azureResource} for account ${account.displayInfo.userId} and tenant ${tenantId}`);
return undefined; return undefined;
} }
@@ -196,7 +223,7 @@ export abstract class AzureAuth implements vscode.Disposable {
throw new AzureAuthError(localize('azure.tenantNotFound', "Specified tenant with ID '{0}' not found.", tenantId), `Tenant ${tenantId} not found.`, undefined); throw new AzureAuthError(localize('azure.tenantNotFound', "Specified tenant with ID '{0}' not found.", tenantId), `Tenant ${tenantId} not found.`, undefined);
} }
const cachedTokens = await this.getSavedToken(tenant, resource, account.key); const cachedTokens = await this.getSavedTokenAdal(tenant, resource, account.key);
// Let's check to see if we can just use the cached tokens to return to the user // Let's check to see if we can just use the cached tokens to return to the user
if (cachedTokens?.accessToken) { if (cachedTokens?.accessToken) {
@@ -213,7 +240,7 @@ export abstract class AzureAuth implements vscode.Disposable {
const maxTolerance = 2 * 60; // two minutes const maxTolerance = 2 * 60; // two minutes
if (remainingTime < maxTolerance) { if (remainingTime < maxTolerance) {
const result = await this.refreshToken(tenant, resource, cachedTokens.refreshToken); const result = await this.refreshTokenAdal(tenant, resource, cachedTokens.refreshToken);
if (result) { if (result) {
accessToken = result.accessToken; accessToken = result.accessToken;
expiresOn = Number(result.expiresOn); expiresOn = Number(result.expiresOn);
@@ -224,7 +251,7 @@ export abstract class AzureAuth implements vscode.Disposable {
return { return {
...accessToken, ...accessToken,
expiresOn: expiresOn, expiresOn: expiresOn,
tokenType: 'Bearer' tokenType: Constants.Bearer
}; };
} }
} }
@@ -234,7 +261,7 @@ export abstract class AzureAuth implements vscode.Disposable {
if (!this.metadata.settings.microsoftResource) { if (!this.metadata.settings.microsoftResource) {
throw new Error(localize('noMicrosoftResource', "Provider '{0}' does not have a Microsoft resource endpoint defined.", this.metadata.displayName)); throw new Error(localize('noMicrosoftResource', "Provider '{0}' does not have a Microsoft resource endpoint defined.", this.metadata.displayName));
} }
const baseTokens = await this.getSavedToken(this.commonTenant, this.metadata.settings.microsoftResource, account.key); const baseTokens = await this.getSavedTokenAdal(this.commonTenant, this.metadata.settings.microsoftResource, account.key);
if (!baseTokens) { if (!baseTokens) {
Logger.error('User had no base tokens for the basic resource registered. This should not happen and indicates something went wrong with the authentication cycle'); Logger.error('User had no base tokens for the basic resource registered. This should not happen and indicates something went wrong with the authentication cycle');
const msg = localize('azure.noBaseToken', 'Something failed with the authentication, or your tokens have been deleted from the system. Please try adding your account to Azure Data Studio again.'); const msg = localize('azure.noBaseToken', 'Something failed with the authentication, or your tokens have been deleted from the system. Please try adding your account to Azure Data Studio again.');
@@ -242,12 +269,12 @@ export abstract class AzureAuth implements vscode.Disposable {
throw new AzureAuthError(msg, 'No base token found', undefined); throw new AzureAuthError(msg, 'No base token found', undefined);
} }
// Let's try to convert the access token type, worst case we'll have to prompt the user to do an interactive authentication. // Let's try to convert the access token type, worst case we'll have to prompt the user to do an interactive authentication.
const result = await this.refreshToken(tenant, resource, baseTokens.refreshToken); const result = await this.refreshTokenAdal(tenant, resource, baseTokens.refreshToken);
if (result?.accessToken) { if (result?.accessToken) {
return { return {
...result.accessToken, ...result.accessToken,
expiresOn: Number(result.expiresOn), expiresOn: Number(result.expiresOn),
tokenType: 'Bearer' tokenType: Constants.Bearer
}; };
} }
return undefined; return undefined;
@@ -255,7 +282,9 @@ export abstract class AzureAuth implements vscode.Disposable {
protected abstract login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred<void, Error> }>; protected abstract loginAdal(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred<void, Error> }>;
protected abstract loginMsal(tenant: Tenant, resource: Resource): Promise<{ response: AuthenticationResult | null, authComplete: Deferred<void, Error> }>;
/** /**
* Refreshes a token, if a refreshToken is passed in then we use that. If it is not passed in then we will prompt the user for consent. * Refreshes a token, if a refreshToken is passed in then we use that. If it is not passed in then we will prompt the user for consent.
@@ -265,7 +294,7 @@ export abstract class AzureAuth implements vscode.Disposable {
* @returns The oauth token response or undefined. Undefined is returned when the user wants to ignore a tenant or chooses not to start the * @returns The oauth token response or undefined. Undefined is returned when the user wants to ignore a tenant or chooses not to start the
* re-authentication process for their tenant. * re-authentication process for their tenant.
*/ */
public async refreshToken(tenant: Tenant, resource: Resource, refreshToken: RefreshToken | undefined): Promise<OAuthTokenResponse | undefined> { public async refreshTokenAdal(tenant: Tenant, resource: Resource, refreshToken: RefreshToken | undefined): Promise<OAuthTokenResponse | undefined> {
Logger.pii('Refreshing token', [{ name: 'token', objOrArray: refreshToken }], []); Logger.pii('Refreshing token', [{ name: 'token', objOrArray: refreshToken }], []);
if (refreshToken) { if (refreshToken) {
const postData: RefreshTokenPostData = { const postData: RefreshTokenPostData = {
@@ -275,36 +304,96 @@ export abstract class AzureAuth implements vscode.Disposable {
tenant: tenant.id, tenant: tenant.id,
resource: resource.endpoint resource: resource.endpoint
}; };
return this.getTokenAdal(tenant, resource, postData);
return this.getToken(tenant, resource, postData); }
return this.handleInteractionRequiredAdal(tenant, resource);
} }
return this.handleInteractionRequired(tenant, resource);
/**
* Gets the access token for the correct account and scope from the token cache, if the correct token doesn't exist in the token cache
* (i.e. expired token, wrong scope, etc.), sends a request for a new token using the refresh token
* @param accountId
* @param azureResource
* @returns The authentication result, including the access token
*/
public async getTokenMsal(accountId: string, azureResource: azdata.AzureResource, tenantId: string): Promise<AuthenticationResult | null> {
const cache = this.clientApplication.getTokenCache();
if (!cache) {
Logger.error('Error: Could not fetch token cache.');
return null;
}
const resource = this.resources.find(s => s.azureResourceId === azureResource);
if (!resource) {
Logger.error(`Error: Could not fetch the azure resource ${azureResource} `);
return null;
}
let account: AccountInfo | null;
// if the accountId is a home ID, it will include a "." character
if (accountId.includes(".")) {
account = await cache.getAccountByHomeId(accountId);
} else {
account = await cache.getAccountByLocalId(accountId);
}
if (!account) {
Logger.error('Error: Could not fetch account when acquiring token');
return null;
}
let newScope;
if (resource.azureResourceId === azdata.AzureResource.ResourceManagement) {
newScope = [`${resource?.endpoint}user_impersonation`];
} else {
newScope = [`${resource?.endpoint}.default`];
} }
public async getToken(tenant: Tenant, resource: Resource, postData: AuthorizationCodePostData | TokenPostData | RefreshTokenPostData): Promise<OAuthTokenResponse | undefined> { // construct request
Logger.verbose('Fetching token'); // forceRefresh needs to be set true here in order to fetch the correct token, due to this issue
// https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/3687
const tokenRequest = {
account: account,
authority: `https://login.microsoftonline.com/${tenantId}`,
scopes: newScope,
forceRefresh: true
};
try {
return await this.clientApplication.acquireTokenSilent(tokenRequest);
} catch (e) {
Logger.error('Failed to acquireTokenSilent', e);
if (e instanceof InteractionRequiredAuthError) {
// build refresh token request
const tenant: Tenant = {
id: tenantId,
displayName: ''
};
return this.handleInteractionRequiredMsal(tenant, resource);
} else if (e.name === 'ClientAuthError') {
Logger.error(e.message);
}
Logger.error('Failed to silently acquire token, not InteractionRequiredAuthError');
return null;
}
}
public async getTokenAdal(tenant: Tenant, resource: Resource, postData: AuthorizationCodePostData | TokenPostData | RefreshTokenPostData): Promise<OAuthTokenResponse | undefined> {
Logger.verbose('Fetching token for tenant {0}', tenant.id);
const tokenUrl = `${this.loginEndpointUrl}${tenant.id}/oauth2/token`; const tokenUrl = `${this.loginEndpointUrl}${tenant.id}/oauth2/token`;
const response = await this.makePostRequest(tokenUrl, postData); const response = await this.makePostRequest(tokenUrl, postData);
Logger.pii('Token: ', [{ name: 'access token', objOrArray: response.data }, { name: 'refresh token', objOrArray: response.data }],
[{ name: 'access token', value: response.data.access_token }, { name: 'refresh token', value: response.data.refresh_token }]);
if (response.data.error === 'interaction_required') {
return this.handleInteractionRequired(tenant, resource);
}
Logger.pii('Token: ', [{ name: 'access token', objOrArray: response.data }, { name: 'refresh token', objOrArray: response.data }], []);
if (response.data.error === 'interaction_required') {
return this.handleInteractionRequiredAdal(tenant, resource);
}
if (response.data.error) { if (response.data.error) {
Logger.error('Response error!', response.data); Logger.error(`Response returned error : ${response.data}`);
throw new AzureAuthError(localize('azure.responseError', "Token retrieval failed with an error. [Open developer tools]({0}) for more details.", 'command:workbench.action.toggleDevTools'), 'Token retrieval failed', undefined); throw new AzureAuthError(localize('azure.responseError', "Token retrieval failed with an error. [Open developer tools]({0}) for more details.", 'command:workbench.action.toggleDevTools'), 'Token retrieval failed', undefined);
} }
const accessTokenString = response.data.access_token; const accessTokenString = response.data.access_token;
const refreshTokenString = response.data.refresh_token; const refreshTokenString = response.data.refresh_token;
const expiresOnString = response.data.expires_on; const expiresOnString = response.data.expires_on;
return this.getTokenHelperAdal(tenant, resource, accessTokenString, refreshTokenString, expiresOnString);
return this.getTokenHelper(tenant, resource, accessTokenString, refreshTokenString, expiresOnString);
} }
public async getTokenHelper(tenant: Tenant, resource: Resource, accessTokenString: string, refreshTokenString: string, expiresOnString: string): Promise<OAuthTokenResponse> { public async getTokenHelperAdal(tenant: Tenant, resource: Resource, accessTokenString: string, refreshTokenString: string, expiresOnString: string): Promise<OAuthTokenResponse> {
if (!accessTokenString) { if (!accessTokenString) {
const msg = localize('azure.accessTokenEmpty', 'No access token returned from Microsoft OAuth'); const msg = localize('azure.accessTokenEmpty', 'No access token returned from Microsoft OAuth');
throw new AzureAuthError(msg, 'Access token was empty', undefined); throw new AzureAuthError(msg, 'Access token was empty', undefined);
@@ -349,7 +438,8 @@ export abstract class AzureAuth implements vscode.Disposable {
const accountKey: azdata.AccountKey = { const accountKey: azdata.AccountKey = {
providerId: this.metadata.id, providerId: this.metadata.id,
accountId: userKey accountId: userKey,
authLibrary: this._authLibrary
}; };
await this.saveToken(tenant, resource, accountKey, result); await this.saveToken(tenant, resource, accountKey, result);
@@ -358,19 +448,47 @@ export abstract class AzureAuth implements vscode.Disposable {
} }
public async getTenantsMsal(token: string): Promise<Tenant[]> {
//#region tenant calls
public 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'); const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2019-11-01');
try { try {
Logger.verbose('Fetching tenants', tenantUri); Logger.verbose('Fetching tenants with uri {0}', tenantUri);
let tenantList: string[] = [];
const tenantResponse = await this.makeGetRequest(tenantUri, token);
const tenants: Tenant[] = tenantResponse.data.value.map((tenantInfo: TenantResponse) => {
if (tenantInfo.displayName) {
tenantList.push(tenantInfo.displayName);
} else {
tenantList.push(tenantInfo.tenantId);
Logger.info('Tenant display name found empty: {0}', tenantInfo.tenantId);
}
return {
id: tenantInfo.tenantId,
displayName: tenantInfo.displayName ? tenantInfo.displayName : tenantInfo.tenantId,
userId: token,
tenantCategory: tenantInfo.tenantCategory
} as Tenant;
});
Logger.verbose(`Tenants: ${tenantList}`);
const homeTenantIndex = tenants.findIndex(tenant => tenant.tenantCategory === Constants.HomeCategory);
// remove home tenant from list of tenants
if (homeTenantIndex >= 0) {
const homeTenant = tenants.splice(homeTenantIndex, 1);
tenants.unshift(homeTenant[0]);
}
return tenants;
} catch (ex) {
Logger.error(`Error fetching tenants :${ex}`);
throw new Error('Error retrieving tenant information');
}
}
//#region tenant calls
public async getTenantsAdal(token: AccessToken): Promise<Tenant[]> {
const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2019-11-01');
try {
Logger.verbose('Fetching tenants with URI: {0}', tenantUri);
let tenantList: string[] = [];
const tenantResponse = await this.makeGetRequest(tenantUri, token.token); const tenantResponse = await this.makeGetRequest(tenantUri, token.token);
if (tenantResponse.status !== 200) { if (tenantResponse.status !== 200) {
Logger.error(`Error with tenant response, status: ${tenantResponse.status} | status text: ${tenantResponse.statusText}`); Logger.error(`Error with tenant response, status: ${tenantResponse.status} | status text: ${tenantResponse.statusText}`);
@@ -378,16 +496,22 @@ export abstract class AzureAuth implements vscode.Disposable {
throw new Error('Error with tenant response'); throw new Error('Error with tenant response');
} }
const tenants: Tenant[] = tenantResponse.data.value.map((tenantInfo: TenantResponse) => { const tenants: Tenant[] = tenantResponse.data.value.map((tenantInfo: TenantResponse) => {
Logger.verbose(`Tenant: ${tenantInfo.displayName}`); if (tenantInfo.displayName) {
tenantList.push(tenantInfo.displayName);
} else {
tenantList.push(tenantInfo.tenantId);
Logger.info('Tenant display name found empty: {0}', tenantInfo.tenantId);
}
return { return {
id: tenantInfo.tenantId, id: tenantInfo.tenantId,
displayName: tenantInfo.displayName ? tenantInfo.displayName : localize('azureWorkAccountDisplayName', "Work or school account"), displayName: tenantInfo.displayName ? tenantInfo.displayName : tenantInfo.tenantId,
userId: token.key, userId: token.key,
tenantCategory: tenantInfo.tenantCategory tenantCategory: tenantInfo.tenantCategory
} as Tenant; } as Tenant;
}); });
Logger.verbose(`Tenants: ${tenantList}`);
const homeTenantIndex = tenants.findIndex(tenant => tenant.tenantCategory === 'Home'); const homeTenantIndex = tenants.findIndex(tenant => tenant.tenantCategory === Constants.HomeCategory);
// remove home tenant from list of tenants
if (homeTenantIndex >= 0) { if (homeTenantIndex >= 0) {
const homeTenant = tenants.splice(homeTenantIndex, 1); const homeTenant = tenants.splice(homeTenantIndex, 1);
tenants.unshift(homeTenant[0]); tenants.unshift(homeTenant[0]);
@@ -421,7 +545,7 @@ export abstract class AzureAuth implements vscode.Disposable {
} }
} }
public async getSavedToken(tenant: Tenant, resource: Resource, accountKey: azdata.AccountKey): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | undefined, expiresOn: string } | undefined> { public async getSavedTokenAdal(tenant: Tenant, resource: Resource, accountKey: azdata.AccountKey): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | undefined, expiresOn: string } | undefined> {
const getMsg = localize('azure.cacheErrorGet', "Error when getting your account from the cache"); const getMsg = localize('azure.cacheErrorGet', "Error when getting your account from the cache");
const parseMsg = localize('azure.cacheErrorParse', "Error when parsing your account from the cache"); const parseMsg = localize('azure.cacheErrorParse', "Error when parsing your account from the cache");
@@ -464,12 +588,22 @@ export abstract class AzureAuth implements vscode.Disposable {
} }
//#endregion //#endregion
//#region interaction handling
public async handleInteractionRequired(tenant: Tenant, resource: Resource): Promise<OAuthTokenResponse | undefined> { //#region interaction handling
public async handleInteractionRequiredMsal(tenant: Tenant, resource: Resource): Promise<AuthenticationResult | null> {
const shouldOpen = await this.askUserForInteraction(tenant, resource); const shouldOpen = await this.askUserForInteraction(tenant, resource);
if (shouldOpen) { if (shouldOpen) {
const result = await this.login(tenant, resource); const result = await this.loginMsal(tenant, resource);
result?.authComplete?.resolve();
return result?.response;
}
return null;
}
public async handleInteractionRequiredAdal(tenant: Tenant, resource: Resource): Promise<OAuthTokenResponse | undefined> {
const shouldOpen = await this.askUserForInteraction(tenant, resource);
if (shouldOpen) {
const result = await this.loginAdal(tenant, resource);
result?.authComplete?.resolve(); result?.authComplete?.resolve();
return result?.response; return result?.response;
} }
@@ -487,13 +621,14 @@ export abstract class AzureAuth implements vscode.Disposable {
} }
const getTenantConfigurationSet = (): Set<string> => { const getTenantConfigurationSet = (): Set<string> => {
const configuration = vscode.workspace.getConfiguration('azure.tenant.config'); const configuration = vscode.workspace.getConfiguration(Constants.AzureTenantConfigSection);
let values: string[] = configuration.get('filter') ?? []; let values: string[] = configuration.get('filter') ?? [];
return new Set<string>(values); return new Set<string>(values);
}; };
// The user wants to ignore this tenant. // The user wants to ignore this tenant.
if (getTenantConfigurationSet().has(tenant.id)) { if (getTenantConfigurationSet().has(tenant.id)) {
Logger.info(`Tenant ${tenant.id} found in the ignore list, authentication will not be attempted.`);
return false; return false;
} }
@@ -544,22 +679,21 @@ export abstract class AzureAuth implements vscode.Disposable {
public createAccount(tokenClaims: TokenClaims, key: string, tenants: Tenant[]): AzureAccount { public createAccount(tokenClaims: TokenClaims, key: string, tenants: Tenant[]): AzureAccount {
Logger.verbose(`Token Claims: ${tokenClaims.name}`); Logger.verbose(`Token Claims: ${tokenClaims.name}`);
tenants.forEach((tenant) => { tenants.forEach((tenant) => {
Logger.verbose( Logger.verbose(`Tenant ID: ${tenant.id}, Tenant Name: ${tenant.displayName}`);
`Tenant ID: ${tenant.id}
Tenant Name: ${tenant.displayName}`);
}); });
// Determine if this is a microsoft account // Determine if this is a microsoft account
let accountIssuer = 'unknown'; let accountIssuer = 'unknown';
if (tokenClaims.iss === 'https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/') { if (tokenClaims.iss === 'https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/' ||
accountIssuer = 'corp'; tokenClaims.iss === 'https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/v2.0') {
accountIssuer = Constants.AccountIssuer.Corp;
} }
if (tokenClaims?.idp === 'live.com') { if (tokenClaims?.idp === 'live.com') {
accountIssuer = 'msft'; accountIssuer = Constants.AccountIssuer.Msft;
} }
const name = tokenClaims.name ?? tokenClaims.email ?? tokenClaims.unique_name; const name = tokenClaims.name ?? tokenClaims.email ?? tokenClaims.unique_name ?? tokenClaims.preferred_username;
const email = tokenClaims.email ?? tokenClaims.unique_name; const email = tokenClaims.email ?? tokenClaims.unique_name ?? tokenClaims.preferred_username;
// Read more about tid > https://learn.microsoft.com/azure/active-directory/develop/id-tokens // Read more about tid > https://learn.microsoft.com/azure/active-directory/develop/id-tokens
const owningTenant = tenants.find(t => t.id === tokenClaims.tid) const owningTenant = tenants.find(t => t.id === tokenClaims.tid)
@@ -572,25 +706,26 @@ export abstract class AzureAuth implements vscode.Disposable {
let contextualDisplayName: string; let contextualDisplayName: string;
switch (accountIssuer) { switch (accountIssuer) {
case 'corp': case Constants.AccountIssuer.Corp:
contextualDisplayName = localize('azure.microsoftCorpAccount', "Microsoft Corp"); contextualDisplayName = localize('azure.microsoftCorpAccount', "Microsoft Corp");
break; break;
case 'msft': case Constants.AccountIssuer.Msft:
contextualDisplayName = localize('azure.microsoftAccountDisplayName', 'Microsoft Account'); contextualDisplayName = localize('azure.microsoftAccountDisplayName', 'Microsoft Account');
break; break;
default: default:
contextualDisplayName = displayName; contextualDisplayName = displayName;
} }
let accountType = accountIssuer === 'msft' let accountType = accountIssuer === Constants.AccountIssuer.Msft
? this.MicrosoftAccountType ? Constants.AccountType.Microsoft
: this.WorkSchoolAccountType; : Constants.AccountType.WorkSchool;
const account = { const account = {
key: { key: {
providerId: this.metadata.id, providerId: this.metadata.id,
accountId: key, accountId: key,
accountVersion: AzureAuth.ACCOUNT_VERSION, accountVersion: Constants.AccountVersion,
authLibrary: this._authLibrary
}, },
name: displayName, name: displayName,
displayInfo: { displayInfo: {
@@ -603,7 +738,7 @@ export abstract class AzureAuth implements vscode.Disposable {
}, },
properties: { properties: {
providerSettings: this.metadata, providerSettings: this.metadata,
isMsAccount: accountIssuer === 'msft', isMsAccount: accountIssuer === Constants.AccountIssuer.Msft,
owningTenant: owningTenant, owningTenant: owningTenant,
tenants, tenants,
azureAuthType: this.authType azureAuthType: this.authType
@@ -660,8 +795,10 @@ export abstract class AzureAuth implements vscode.Disposable {
protected toBase64UrlEncoding(base64string: string): string { protected toBase64UrlEncoding(base64string: string): string {
return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding
} }
public async deleteAllCacheMsal(): Promise<void> {
public async deleteAllCache(): Promise<void> { this.clientApplication.clearCache();
}
public async deleteAllCacheAdal(): Promise<void> {
const results = await this.tokenCache.findCredentials(''); const results = await this.tokenCache.findCredentials('');
for (let { account } of results) { for (let { account } of results) {
@@ -671,7 +808,13 @@ export abstract class AzureAuth implements vscode.Disposable {
public async clearCredentials(account: azdata.AccountKey): Promise<void> { public async clearCredentials(account: azdata.AccountKey): Promise<void> {
try { try {
return this.deleteAccountCache(account); // remove account based on authLibrary field, accounts added before this field was present will default to
// ADAL method of account removal
if (account.authLibrary === Constants.AuthLibrary.MSAL) {
return this.deleteAccountCacheMsal(account);
} else { // fallback to ADAL by default
return this.deleteAccountCacheAdal(account);
}
} catch (ex) { } catch (ex) {
const msg = localize('azure.cacheErrrorRemove', "Error when removing your account from the cache."); const msg = localize('azure.cacheErrrorRemove', "Error when removing your account from the cache.");
void vscode.window.showErrorMessage(msg); void vscode.window.showErrorMessage(msg);
@@ -679,9 +822,27 @@ export abstract class AzureAuth implements vscode.Disposable {
} }
} }
public async deleteAccountCache(account: azdata.AccountKey): Promise<void> { public async deleteAccountCacheMsal(account: azdata.AccountKey): Promise<void> {
const results = await this.tokenCache.findCredentials(account.accountId); const tokenCache = this.clientApplication.getTokenCache();
let msalAccount: AccountInfo | null;
// if the accountId is a home ID, it will include a "." character
if (account.accountId.includes(".")) {
msalAccount = await tokenCache.getAccountByHomeId(account.accountId);
} else {
msalAccount = await tokenCache.getAccountByLocalId(account.accountId);
}
if (!msalAccount) {
Logger.error(`MSAL: Unable to find account ${account.accountId} for removal`);
throw Error(`Unable to find account ${account.accountId}`);
}
await tokenCache.removeAccount(msalAccount);
}
public async deleteAccountCacheAdal(account: azdata.AccountKey): Promise<void> {
const results = await this.tokenCache.findCredentials(account.accountId);
if (!results) {
Logger.error('ADAL: Unable to find account for removal');
}
for (let { account } of results) { for (let { account } of results) {
await this.tokenCache.clearCredential(account); await this.tokenCache.clearCredential(account);
} }
@@ -722,6 +883,13 @@ export interface RefreshToken extends AccountKey {
key: string key: string
} }
export interface TenantResponse { // https://docs.microsoft.com/en-us/rest/api/resources/tenants/list
id: string
tenantId: string
displayName?: string
tenantCategory?: string
}
export interface MultiTenantTokenResponse { export interface MultiTenantTokenResponse {
[tenantId: string]: Token | undefined; [tenantId: string]: Token | undefined;
} }

View File

@@ -12,11 +12,13 @@ import { SimpleTokenCache } from '../simpleTokenCache';
import { SimpleWebServer } from '../utils/simpleWebServer'; import { SimpleWebServer } from '../utils/simpleWebServer';
import { AzureAuthError } from './azureAuthError'; import { AzureAuthError } from './azureAuthError';
import { Logger } from '../../utils/Logger'; import { Logger } from '../../utils/Logger';
import * as Constants from '../../constants';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import * as path from 'path'; import * as path from 'path';
import * as http from 'http'; import * as http from 'http';
import * as qs from 'qs'; import * as qs from 'qs';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { PublicClientApplication, CryptoProvider, AuthorizationUrlRequest, AuthorizationCodeRequest, AuthenticationResult } from '@azure/msal-node';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
@@ -28,33 +30,43 @@ interface AuthCodeResponse {
interface CryptoValues { interface CryptoValues {
nonce: string; nonce: string;
challengeMethod: string;
codeVerifier: string; codeVerifier: string;
codeChallenge: string; codeChallenge: string;
} }
export class AzureAuthCodeGrant extends AzureAuth { export class AzureAuthCodeGrant extends AzureAuth {
private static readonly USER_FRIENDLY_NAME: string = localize('azure.azureAuthCodeGrantName', 'Azure Auth Code Grant'); private static readonly USER_FRIENDLY_NAME: string = localize('azure.azureAuthCodeGrantName', 'Azure Auth Code Grant');
private cryptoProvider: CryptoProvider;
private pkceCodes: CryptoValues;
constructor( constructor(
metadata: AzureAccountProviderMetadata, metadata: AzureAccountProviderMetadata,
tokenCache: SimpleTokenCache, tokenCache: SimpleTokenCache,
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
uriEventEmitter: vscode.EventEmitter<vscode.Uri>, uriEventEmitter: vscode.EventEmitter<vscode.Uri>,
clientApplication: PublicClientApplication,
authLibrary: string
) { ) {
super(metadata, tokenCache, context, uriEventEmitter, AzureAuthType.AuthCodeGrant, AzureAuthCodeGrant.USER_FRIENDLY_NAME); super(metadata, tokenCache, context, clientApplication, uriEventEmitter, AzureAuthType.AuthCodeGrant, AzureAuthCodeGrant.USER_FRIENDLY_NAME, authLibrary);
this.cryptoProvider = new CryptoProvider();
this.pkceCodes = {
nonce: '',
challengeMethod: Constants.S256_CODE_CHALLENGE_METHOD, // Use SHA256 as the challenge method
codeVerifier: '', // Generate a code verifier for the Auth Code Request first
codeChallenge: '', // Generate a code challenge from the previously generated code verifier
};
} }
protected async loginAdal(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred<void, Error> }> {
protected async login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred<void, Error> }> {
let authCompleteDeferred: Deferred<void, Error>; let authCompleteDeferred: Deferred<void, Error>;
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject }); let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
let authResponse: AuthCodeResponse; let authResponse: AuthCodeResponse;
if (vscode.env.uiKind === vscode.UIKind.Web) { if (vscode.env.uiKind === vscode.UIKind.Web) {
authResponse = await this.loginWeb(tenant, resource); authResponse = await this.loginWebAdal(tenant, resource);
} else { } else {
authResponse = await this.loginDesktop(tenant, resource, authCompletePromise); authResponse = await this.loginDesktopAdal(tenant, resource, authCompletePromise);
} }
return { return {
@@ -63,6 +75,30 @@ export class AzureAuthCodeGrant extends AzureAuth {
}; };
} }
protected async loginMsal(tenant: Tenant, resource: Resource): Promise<{ response: AuthenticationResult | null, authComplete: Deferred<void, Error> }> {
let authCompleteDeferred: Deferred<void, Error>;
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
let authCodeRequest: AuthorizationCodeRequest;
if (vscode.env.uiKind === vscode.UIKind.Web) {
authCodeRequest = await this.loginWebMsal(tenant, resource);
} else {
authCodeRequest = await this.loginDesktopMsal(tenant, resource, authCompletePromise);
}
let result = await this.clientApplication.acquireTokenByCode(authCodeRequest);
if (!result) {
Logger.error('Failed to acquireTokenByCode');
Logger.error(`Auth Code Request: ${JSON.stringify(authCodeRequest)}`)
throw Error('Failed to fetch token using auth code');
} else {
return {
response: result,
authComplete: authCompleteDeferred!
};
}
}
/** /**
* Requests an OAuthTokenResponse from Microsoft OAuth * Requests an OAuthTokenResponse from Microsoft OAuth
* *
@@ -79,12 +115,47 @@ export class AzureAuthCodeGrant extends AzureAuth {
resource: resource.endpoint resource: resource.endpoint
}; };
return this.getToken(tenant, resource, postData); return this.getTokenAdal(tenant, resource, postData);
} }
private async loginWeb(tenant: Tenant, resource: Resource): Promise<AuthCodeResponse> { private async loginWebMsal(tenant: Tenant, resource: Resource): Promise<AuthorizationCodeRequest> {
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://microsoft.azurecore`)); const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://microsoft.azurecore`));
const { nonce, codeVerifier, codeChallenge } = this.createCryptoValues(); await this.createCryptoValuesMsal();
const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
const state = `${port},${encodeURIComponent(this.pkceCodes.nonce)},${encodeURIComponent(callbackUri.query)}`;
try {
let authUrlRequest: AuthorizationUrlRequest;
authUrlRequest = {
scopes: this.scopes,
redirectUri: this.redirectUri,
codeChallenge: this.pkceCodes.codeChallenge,
codeChallengeMethod: this.pkceCodes.challengeMethod,
prompt: Constants.SELECT_ACCOUNT,
state: state
};
let authCodeRequest: AuthorizationCodeRequest;
authCodeRequest = {
scopes: this.scopes,
redirectUri: this.redirectUri,
codeVerifier: this.pkceCodes.codeVerifier,
code: ''
};
let authCodeUrl = await this.clientApplication.getAuthCodeUrl(authUrlRequest);
await vscode.env.openExternal(vscode.Uri.parse(authCodeUrl));
const authCode = await this.handleWebResponse(state);
authCodeRequest.code = authCode;
return authCodeRequest;
} catch (e) {
Logger.error('MSAL: Error requesting auth code', e);
throw new AzureAuthError('error', 'Error requesting auth code', e);
}
}
private async loginWebAdal(tenant: Tenant, resource: Resource): Promise<AuthCodeResponse> {
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://microsoft.azurecore`));
const { nonce, codeVerifier, codeChallenge } = this.createCryptoValuesAdal();
const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80); const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
const state = `${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`; const state = `${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`;
@@ -94,8 +165,8 @@ export class AzureAuthCodeGrant extends AzureAuth {
client_id: this.clientId, client_id: this.clientId,
redirect_uri: this.redirectUri, redirect_uri: this.redirectUri,
state, state,
prompt: 'select_account', prompt: Constants.SELECT_ACCOUNT,
code_challenge_method: 'S256', code_challenge_method: Constants.S256_CODE_CHALLENGE_METHOD,
code_challenge: codeChallenge, code_challenge: codeChallenge,
resource: resource.id resource: resource.id
}; };
@@ -141,7 +212,7 @@ export class AzureAuthCodeGrant extends AzureAuth {
}, {}); }, {});
} }
private async loginDesktop(tenant: Tenant, resource: Resource, authCompletePromise: Promise<void>): Promise<AuthCodeResponse> { private async loginDesktopMsal(tenant: Tenant, resource: Resource, authCompletePromise: Promise<void>): Promise<AuthorizationCodeRequest> {
const server = new SimpleWebServer(); const server = new SimpleWebServer();
let serverPort: string; let serverPort: string;
@@ -151,7 +222,56 @@ export class AzureAuthCodeGrant extends AzureAuth {
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.'); 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.');
throw new AzureAuthError(msg, 'Server could not start', ex); throw new AzureAuthError(msg, 'Server could not start', ex);
} }
const { nonce, codeVerifier, codeChallenge } = this.createCryptoValues(); await this.createCryptoValuesMsal();
const state = `${serverPort},${this.pkceCodes.nonce}`;
try {
let authUrlRequest: AuthorizationUrlRequest;
authUrlRequest = {
scopes: this.scopes,
redirectUri: `${this.redirectUri}:${serverPort}/redirect`,
codeChallenge: this.pkceCodes.codeChallenge,
codeChallengeMethod: this.pkceCodes.challengeMethod,
prompt: Constants.SELECT_ACCOUNT,
authority: `https://login.microsoftonline.com/${tenant.id}`,
state: state
};
let authCodeRequest: AuthorizationCodeRequest;
authCodeRequest = {
scopes: this.scopes,
redirectUri: `${this.redirectUri}:${serverPort}/redirect`,
codeVerifier: this.pkceCodes.codeVerifier,
authority: `https://login.microsoftonline.com/${tenant.id}`,
code: ''
};
let authCodeUrl = await this.clientApplication.getAuthCodeUrl(authUrlRequest);
await vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${serverPort}/signin?nonce=${encodeURIComponent(this.pkceCodes.nonce)}`));
const authCode = await this.addServerListeners(server, this.pkceCodes.nonce, authCodeUrl, authCompletePromise);
authCodeRequest.code = authCode;
return authCodeRequest;
}
catch (e) {
Logger.error('MSAL: Error requesting auth code', e);
throw new AzureAuthError('error', 'Error requesting auth code', e);
}
}
private async loginDesktopAdal(tenant: Tenant, resource: Resource, authCompletePromise: Promise<void>): Promise<AuthCodeResponse> {
const server = new SimpleWebServer();
let serverPort: string;
try {
serverPort = await server.startup();
} catch (ex) {
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.');
throw new AzureAuthError(msg, 'Server could not start', ex);
}
const { nonce, codeVerifier, codeChallenge } = this.createCryptoValuesAdal();
const state = `${serverPort},${encodeURIComponent(nonce)}`; const state = `${serverPort},${encodeURIComponent(nonce)}`;
const loginQuery = { const loginQuery = {
response_type: 'code', response_type: 'code',
@@ -159,8 +279,8 @@ export class AzureAuthCodeGrant extends AzureAuth {
client_id: this.clientId, client_id: this.clientId,
redirect_uri: `${this.redirectUri}:${serverPort}/redirect`, redirect_uri: `${this.redirectUri}:${serverPort}/redirect`,
state, state,
prompt: 'select_account', prompt: Constants.SELECT_ACCOUNT,
code_challenge_method: 'S256', code_challenge_method: Constants.S256_CODE_CHALLENGE_METHOD,
code_challenge: codeChallenge, code_challenge: codeChallenge,
resource: resource.endpoint resource: resource.endpoint
}; };
@@ -272,13 +392,21 @@ export class AzureAuthCodeGrant extends AzureAuth {
} }
private createCryptoValues(): CryptoValues { private createCryptoValuesAdal(): CryptoValues {
const nonce = crypto.randomBytes(16).toString('base64'); const nonce = crypto.randomBytes(16).toString('base64');
const codeVerifier = this.toBase64UrlEncoding(crypto.randomBytes(32).toString('base64')); const codeVerifier = this.toBase64UrlEncoding(crypto.randomBytes(32).toString('base64'));
const codeChallenge = this.toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64')); const codeChallenge = this.toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64'));
const challengeMethod = '';
return { return {
nonce, codeVerifier, codeChallenge nonce, challengeMethod, codeVerifier, codeChallenge
}; };
} }
private async createCryptoValuesMsal(): Promise<void> {
this.pkceCodes.nonce = this.cryptoProvider.createNewGuid();
const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();
this.pkceCodes.codeVerifier = verifier;
this.pkceCodes.codeChallenge = challenge;
}
} }

View File

@@ -13,7 +13,6 @@ import {
DeviceCodeCheckPostData, DeviceCodeCheckPostData,
} from './azureAuth'; } from './azureAuth';
import { import {
AzureAccountProviderMetadata, AzureAccountProviderMetadata,
AzureAuthType, AzureAuthType,
@@ -21,8 +20,10 @@ import {
Resource Resource
} from 'azurecore'; } from 'azurecore';
import { Deferred } from '../interfaces'; import { Deferred } from '../interfaces';
import { AuthenticationResult, DeviceCodeRequest, PublicClientApplication } from '@azure/msal-node';
import { SimpleTokenCache } from '../simpleTokenCache'; import { SimpleTokenCache } from '../simpleTokenCache';
import { Logger } from '../../utils/Logger'; import { Logger } from '../../utils/Logger';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
interface DeviceCodeLogin { // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code interface DeviceCodeLogin { // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code
@@ -50,12 +51,34 @@ export class AzureDeviceCode extends AzureAuth {
tokenCache: SimpleTokenCache, tokenCache: SimpleTokenCache,
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
uriEventEmitter: vscode.EventEmitter<vscode.Uri>, uriEventEmitter: vscode.EventEmitter<vscode.Uri>,
clientApplication: PublicClientApplication,
authLibrary: string
) { ) {
super(metadata, tokenCache, context, uriEventEmitter, AzureAuthType.DeviceCode, AzureDeviceCode.USER_FRIENDLY_NAME); super(metadata, tokenCache, context, clientApplication, uriEventEmitter, AzureAuthType.DeviceCode, AzureDeviceCode.USER_FRIENDLY_NAME, authLibrary);
this.pageTitle = localize('addAccount', "Add {0} account", this.metadata.displayName); this.pageTitle = localize('addAccount', "Add {0} account", this.metadata.displayName);
} }
protected async login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse, authComplete: Deferred<void, Error> }> {
protected async loginMsal(tenant: Tenant, resource: Resource): Promise<{ response: AuthenticationResult | null, authComplete: Deferred<void, Error> }> {
let authCompleteDeferred: Deferred<void, Error>;
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
const deviceCodeRequest: DeviceCodeRequest = {
scopes: this.scopes,
authority: `https://login.microsoftonline.com/${tenant.id}`,
deviceCodeCallback: async (response) => {
await azdata.accounts.beginAutoOAuthDeviceCode(this.metadata.id, this.pageTitle, response.message, response.userCode, response.verificationUri);
}
};
const authResult = await this.clientApplication.acquireTokenByDeviceCode(deviceCodeRequest);
this.closeOnceComplete(authCompletePromise).catch(Logger.error);
return {
response: authResult,
authComplete: authCompleteDeferred!
};
}
protected async loginAdal(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse, authComplete: Deferred<void, Error> }> {
let authCompleteDeferred: Deferred<void, Error>; let authCompleteDeferred: Deferred<void, Error>;
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject }); let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
@@ -79,7 +102,7 @@ export class AzureDeviceCode extends AzureAuth {
const currentTime = new Date().getTime() / 1000; const currentTime = new Date().getTime() / 1000;
const expiresOn = `${currentTime + finalDeviceLogin.expires_in}`; const expiresOn = `${currentTime + finalDeviceLogin.expires_in}`;
const result = await this.getTokenHelper(tenant, resource, accessTokenString, refreshTokenString, expiresOn); const result = await this.getTokenHelperAdal(tenant, resource, accessTokenString, refreshTokenString, expiresOn);
this.closeOnceComplete(authCompletePromise).catch(Logger.error); this.closeOnceComplete(authCompletePromise).catch(Logger.error);
return { return {
@@ -93,7 +116,6 @@ export class AzureDeviceCode extends AzureAuth {
azdata.accounts.endAutoOAuthDeviceCode(); azdata.accounts.endAutoOAuthDeviceCode();
} }
private setupPolling(info: DeviceCodeLogin): Promise<DeviceCodeLoginResult> { private setupPolling(info: DeviceCodeLogin): Promise<DeviceCodeLoginResult> {
const timeoutMessage = localize('azure.timeoutDeviceCode', 'Timed out when waiting for device code login.'); const timeoutMessage = localize('azure.timeoutDeviceCode', 'Timed out when waiting for device code login.');
const fiveMinutes = 5 * 60 * 1000; const fiveMinutes = 5 * 60 * 1000;
@@ -130,18 +152,15 @@ export class AzureDeviceCode extends AzureAuth {
}; };
const postResult = await this.makePostRequest(uri, postData); const postResult = await this.makePostRequest(uri, postData);
const result: DeviceCodeLoginResult = postResult.data; const result: DeviceCodeLoginResult = postResult.data;
return result; return result;
} catch (ex) { } catch (ex) {
console.log(ex); Logger.error('Unexpected error making Azure auth request', 'azureCore.checkForResult', JSON.stringify(ex?.response?.data, undefined, 2));
console.log('Unexpected error making Azure auth request', 'azureCore.checkForResult', JSON.stringify(ex?.response?.data, undefined, 2));
throw new Error(msg); throw new Error(msg);
} }
} }
public override async autoOAuthCancelled(): Promise<void> { public override async autoOAuthCancelled(): Promise<void> {
return azdata.accounts.endAutoOAuthDeviceCode(); return azdata.accounts.endAutoOAuthDeviceCode();
} }

View File

@@ -13,31 +13,37 @@ import {
AzureAccount AzureAccount
} from 'azurecore'; } from 'azurecore';
import { Deferred } from './interfaces'; import { Deferred } from './interfaces';
import { PublicClientApplication } from '@azure/msal-node';
import { SimpleTokenCache } from './simpleTokenCache'; import { SimpleTokenCache } from './simpleTokenCache';
import { Logger } from '../utils/Logger'; import { Logger } from '../utils/Logger';
import { MultiTenantTokenResponse, Token, AzureAuth } from './auths/azureAuth'; import { MultiTenantTokenResponse, Token, AzureAuth } from './auths/azureAuth';
import { AzureAuthCodeGrant } from './auths/azureAuthCodeGrant'; import { AzureAuthCodeGrant } from './auths/azureAuthCodeGrant';
import { AzureDeviceCode } from './auths/azureDeviceCode'; import { AzureDeviceCode } from './auths/azureDeviceCode';
import { filterAccounts } from '../azureResource/utils';
import * as Constants from '../constants';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disposable { export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disposable {
private static readonly CONFIGURATION_SECTION = 'accounts.azure.auth';
private readonly authMappings = new Map<AzureAuthType, AzureAuth>(); private readonly authMappings = new Map<AzureAuthType, AzureAuth>();
private initComplete!: Deferred<void, Error>; private initComplete!: Deferred<void, Error>;
private initCompletePromise: Promise<void> = new Promise<void>((resolve, reject) => this.initComplete = { resolve, reject }); private initCompletePromise: Promise<void> = new Promise<void>((resolve, reject) => this.initComplete = { resolve, reject });
public clientApplication: PublicClientApplication;
constructor( constructor(
metadata: AzureAccountProviderMetadata, metadata: AzureAccountProviderMetadata,
tokenCache: SimpleTokenCache, tokenCache: SimpleTokenCache,
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
clientApplication: PublicClientApplication,
uriEventHandler: vscode.EventEmitter<vscode.Uri>, uriEventHandler: vscode.EventEmitter<vscode.Uri>,
private readonly authLibrary: string,
private readonly forceDeviceCode: boolean = false private readonly forceDeviceCode: boolean = false
) { ) {
this.clientApplication = clientApplication;
vscode.workspace.onDidChangeConfiguration((changeEvent) => { vscode.workspace.onDidChangeConfiguration((changeEvent) => {
const impact = changeEvent.affectsConfiguration(AzureAccountProvider.CONFIGURATION_SECTION); const impactProvider = changeEvent.affectsConfiguration(Constants.AccountsAzureAuthSection);
if (impact === true) { if (impactProvider === true) {
this.handleAuthMapping(metadata, tokenCache, context, uriEventHandler); this.handleAuthMapping(metadata, tokenCache, context, uriEventHandler);
} }
}); });
@@ -50,25 +56,28 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
} }
clearTokenCache(): Thenable<void> { clearTokenCache(): Thenable<void> {
return this.getAuthMethod().deleteAllCache(); return this.authLibrary === Constants.AuthLibrary.MSAL
? this.getAuthMethod().deleteAllCacheMsal()
// fallback to ADAL as default
: this.getAuthMethod().deleteAllCacheAdal();
} }
private handleAuthMapping(metadata: AzureAccountProviderMetadata, tokenCache: SimpleTokenCache, context: vscode.ExtensionContext, uriEventHandler: vscode.EventEmitter<vscode.Uri>) { private handleAuthMapping(metadata: AzureAccountProviderMetadata, tokenCache: SimpleTokenCache, context: vscode.ExtensionContext, uriEventHandler: vscode.EventEmitter<vscode.Uri>) {
this.authMappings.forEach(m => m.dispose()); this.authMappings.forEach(m => m.dispose());
this.authMappings.clear(); this.authMappings.clear();
const configuration = vscode.workspace.getConfiguration(AzureAccountProvider.CONFIGURATION_SECTION);
const codeGrantMethod: boolean = configuration.get<boolean>('codeGrant', false); const configuration = vscode.workspace.getConfiguration(Constants.AccountsAzureAuthSection);
const deviceCodeMethod: boolean = configuration.get<boolean>('deviceCode', false); const codeGrantMethod: boolean = configuration.get<boolean>(Constants.AuthType.CodeGrant, false);
const deviceCodeMethod: boolean = configuration.get<boolean>(Constants.AuthType.DeviceCode, false);
if (codeGrantMethod === true && !this.forceDeviceCode) { if (codeGrantMethod === true && !this.forceDeviceCode) {
this.authMappings.set(AzureAuthType.AuthCodeGrant, new AzureAuthCodeGrant(metadata, tokenCache, context, uriEventHandler)); this.authMappings.set(AzureAuthType.AuthCodeGrant, new AzureAuthCodeGrant(metadata, tokenCache, context, uriEventHandler, this.clientApplication, this.authLibrary));
} }
if (deviceCodeMethod === true || this.forceDeviceCode) { if (deviceCodeMethod === true || this.forceDeviceCode) {
this.authMappings.set(AzureAuthType.DeviceCode, new AzureDeviceCode(metadata, tokenCache, context, uriEventHandler)); this.authMappings.set(AzureAuthType.DeviceCode, new AzureDeviceCode(metadata, tokenCache, context, uriEventHandler, this.clientApplication, this.authLibrary));
} }
if (codeGrantMethod === false && deviceCodeMethod === false && !this.forceDeviceCode) { if (codeGrantMethod === false && deviceCodeMethod === false && !this.forceDeviceCode) {
Logger.error('Error: No authentication methods selected'); console.error('No authentication methods selected');
} }
} }
@@ -97,13 +106,19 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
private async _initialize(storedAccounts: AzureAccount[]): Promise<AzureAccount[]> { private async _initialize(storedAccounts: AzureAccount[]): Promise<AzureAccount[]> {
const accounts: AzureAccount[] = []; const accounts: AzureAccount[] = [];
console.log(`Initializing stored accounts ${JSON.stringify(accounts)}`); console.log(`Initializing stored accounts ${JSON.stringify(accounts)}`);
for (let account of storedAccounts) { const updatedAccounts = filterAccounts(storedAccounts, this.authLibrary);
for (let account of updatedAccounts) {
const azureAuth = this.getAuthMethod(account); const azureAuth = this.getAuthMethod(account);
if (!azureAuth) { if (!azureAuth) {
account.isStale = true; account.isStale = true;
accounts.push(account); accounts.push(account);
} else { } else {
accounts.push(await azureAuth.refreshAccess(account)); account.isStale = false;
if (this.authLibrary === Constants.AuthLibrary.MSAL) {
accounts.push(account);
} else { // fallback to ADAL as default
accounts.push(await azureAuth.refreshAccessAdal(account));
}
} }
} }
this.initComplete.resolve(); this.initComplete.resolve();
@@ -123,7 +138,23 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
await this.initCompletePromise; await this.initCompletePromise;
const azureAuth = this.getAuthMethod(account); const azureAuth = this.getAuthMethod(account);
Logger.pii(`Getting account security token for ${JSON.stringify(account.key)} (tenant ${tenantId}). Auth Method = ${azureAuth.userFriendlyName}`, [], []); Logger.pii(`Getting account security token for ${JSON.stringify(account.key)} (tenant ${tenantId}). Auth Method = ${azureAuth.userFriendlyName}`, [], []);
return azureAuth?.getAccountSecurityToken(account, tenantId, resource); if (this.authLibrary === Constants.AuthLibrary.MSAL) {
let authResult = await azureAuth?.getTokenMsal(account.key.accountId, resource, tenantId);
if (!authResult || !authResult.account || !authResult.account.idTokenClaims) {
Logger.error(`MSAL: getToken call failed`);
throw Error('Failed to get token');
} else {
const token: Token = {
key: authResult.account.homeAccountId,
token: authResult.accessToken,
tokenType: authResult.tokenType,
expiresOn: authResult.account.idTokenClaims.exp
};
return token;
}
} else { // fallback to ADAL as default
return azureAuth?.getAccountSecurityTokenAdal(account, tenantId, resource);
}
} }
private async _getSecurityToken(account: AzureAccount, resource: azdata.AzureResource): Promise<MultiTenantTokenResponse | undefined> { private async _getSecurityToken(account: AzureAccount, resource: azdata.AzureResource): Promise<MultiTenantTokenResponse | undefined> {
@@ -178,7 +209,6 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
return pick.azureAuth.startLogin(); return pick.azureAuth.startLogin();
} }
refresh(account: AzureAccount): Thenable<AzureAccount | azdata.PromptFailedResult> { refresh(account: AzureAccount): Thenable<AzureAccount | azdata.PromptFailedResult> {
return this._refresh(account); return this._refresh(account);
} }

View File

@@ -7,12 +7,18 @@ import * as azdata from 'azdata';
import * as events from 'events'; import * as events from 'events';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as os from 'os';
import { SimpleTokenCache } from './simpleTokenCache'; import { SimpleTokenCache } from './simpleTokenCache';
import providerSettings from './providerSettings'; import providerSettings from './providerSettings';
import { AzureAccountProvider as AzureAccountProvider } from './azureAccountProvider'; import { AzureAccountProvider as AzureAccountProvider } from './azureAccountProvider';
import { AzureAccountProviderMetadata } from 'azurecore'; import { AzureAccountProviderMetadata } from 'azurecore';
import { ProviderSettings } from './interfaces'; import { ProviderSettings } from './interfaces';
import * as loc from '../localizedConstants'; import * as loc from '../localizedConstants';
import { PublicClientApplication } from '@azure/msal-node';
import { DataProtectionScope, PersistenceCachePlugin, FilePersistenceWithDataProtection, KeychainPersistence, LibSecretPersistence } from '@azure/msal-node-extensions';
import * as path from 'path';
import { Logger } from '../utils/Logger';
import * as Constants from '../constants';
let localize = nls.loadMessageBundle(); let localize = nls.loadMessageBundle();
@@ -23,11 +29,6 @@ class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.
} }
export class AzureAccountProviderService implements vscode.Disposable { export class AzureAccountProviderService implements vscode.Disposable {
// CONSTANTS ///////////////////////////////////////////////////////////////
private static CommandClearTokenCache = 'accounts.clearTokenCache';
private static ConfigurationSection = 'accounts.azure.cloud';
private static CredentialNamespace = 'azureAccountProviderCredentials';
// MEMBER VARIABLES //////////////////////////////////////////////////////// // MEMBER VARIABLES ////////////////////////////////////////////////////////
private _disposables: vscode.Disposable[] = []; private _disposables: vscode.Disposable[] = [];
private _accountDisposals: { [accountProviderId: string]: vscode.Disposable } = {}; private _accountDisposals: { [accountProviderId: string]: vscode.Disposable } = {};
@@ -37,8 +38,12 @@ export class AzureAccountProviderService implements vscode.Disposable {
private _currentConfig: vscode.WorkspaceConfiguration | undefined = undefined; private _currentConfig: vscode.WorkspaceConfiguration | undefined = undefined;
private _event: events.EventEmitter = new events.EventEmitter(); private _event: events.EventEmitter = new events.EventEmitter();
private readonly _uriEventHandler: UriEventHandler = new UriEventHandler(); private readonly _uriEventHandler: UriEventHandler = new UriEventHandler();
public clientApplication!: PublicClientApplication;
public persistence: FilePersistenceWithDataProtection | KeychainPersistence | LibSecretPersistence | undefined;
constructor(private _context: vscode.ExtensionContext, private _userStoragePath: string) { constructor(private _context: vscode.ExtensionContext,
private _userStoragePath: string,
private _authLibrary: string) {
this._disposables.push(vscode.window.registerUriHandler(this._uriEventHandler)); this._disposables.push(vscode.window.registerUriHandler(this._uriEventHandler));
} }
@@ -47,17 +52,16 @@ export class AzureAccountProviderService implements vscode.Disposable {
let self = this; let self = this;
// Register commands // Register commands
this._context.subscriptions.push(vscode.commands.registerCommand( this._context.subscriptions.push(vscode.commands.registerCommand(Constants.AccountsClearTokenCacheCommand,
AzureAccountProviderService.CommandClearTokenCache, () => { self._event.emit(Constants.AccountsClearTokenCacheCommand); }
() => { self._event.emit(AzureAccountProviderService.CommandClearTokenCache); }
)); ));
this._event.on(AzureAccountProviderService.CommandClearTokenCache, () => { void self.onClearTokenCache(); }); this._event.on(Constants.AccountsClearTokenCacheCommand, () => { void self.onClearTokenCache(); });
// 1) Get a credential provider // 1) Get a credential provider
// 2a) Store the credential provider for use later // 2a) Store the credential provider for use later
// 2b) Register the configuration change handler // 2b) Register the configuration change handler
// 2c) Perform an initial config change handling // 2c) Perform an initial config change handling
return azdata.credentials.getProvider(AzureAccountProviderService.CredentialNamespace) return azdata.credentials.getProvider(Constants.AzureAccountProviderCredentials)
.then(credProvider => { .then(credProvider => {
this._credentialProvider = credProvider; this._credentialProvider = credProvider;
@@ -103,7 +107,7 @@ export class AzureAccountProviderService implements vscode.Disposable {
// Add a new change processing onto the existing promise change // Add a new change processing onto the existing promise change
await this._configChangePromiseChain; await this._configChangePromiseChain;
// Grab the stored config and the latest config // Grab the stored config and the latest config
let newConfig = vscode.workspace.getConfiguration(AzureAccountProviderService.ConfigurationSection); let newConfig = vscode.workspace.getConfiguration(Constants.AccountsAzureCloudSection);
let oldConfig = this._currentConfig; let oldConfig = this._currentConfig;
this._currentConfig = newConfig; this._currentConfig = newConfig;
@@ -138,22 +142,58 @@ export class AzureAccountProviderService implements vscode.Disposable {
} }
private async registerAccountProvider(provider: ProviderSettings): Promise<void> { private async registerAccountProvider(provider: ProviderSettings): Promise<void> {
const isSaw: boolean = vscode.env.appName.toLowerCase().indexOf(Constants.Saw) > 0;
const noSystemKeychain = vscode.workspace.getConfiguration(Constants.AzureSection).get<boolean>(Constants.NoSystemKeyChainSection);
const platform = os.platform();
const tokenCacheKey = `azureTokenCache-${provider.metadata.id}`;
const lockOptions = {
retryNumber: 100,
retryDelay: 50
}
try { try {
const noSystemKeychain = vscode.workspace.getConfiguration('azure').get<boolean>('noSystemKeychain');
let tokenCacheKey = `azureTokenCache-${provider.metadata.id}`;
if (!this._credentialProvider) { if (!this._credentialProvider) {
throw new Error('Credential provider not registered'); throw new Error('Credential provider not registered');
} }
let simpleTokenCache = new SimpleTokenCache(tokenCacheKey, this._userStoragePath, noSystemKeychain, this._credentialProvider); let simpleTokenCache = new SimpleTokenCache(tokenCacheKey, this._userStoragePath, noSystemKeychain, this._credentialProvider);
await simpleTokenCache.init(); await simpleTokenCache.init();
const cachePath = path.join(this._userStoragePath, Constants.ConfigFilePath);
const isSaw: boolean = vscode.env.appName.toLowerCase().indexOf('saw') > 0; switch (platform) {
let accountProvider = new AzureAccountProvider(provider.metadata as AzureAccountProviderMetadata, simpleTokenCache, this._context, this._uriEventHandler, isSaw); case Constants.Platform.Windows:
const dataProtectionScope = DataProtectionScope.CurrentUser;
const optionalEntropy = "";
this.persistence = await FilePersistenceWithDataProtection.create(cachePath, dataProtectionScope, optionalEntropy);
break;
case Constants.Platform.Mac:
case Constants.Platform.Linux:
this.persistence = await KeychainPersistence.create(cachePath, Constants.ServiceName, Constants.Account);
break;
}
if (!this.persistence) {
Logger.error('Unable to intialize persistence for access token cache. Tokens will not persist in system memory for future use.');
throw new Error('Unable to intialize persistence for access token cache. Tokens will not persist in system memory for future use.');
}
let persistenceCachePlugin: PersistenceCachePlugin = new PersistenceCachePlugin(this.persistence, lockOptions); // or any of the other ones.
const MSAL_CONFIG = {
auth: {
clientId: provider.metadata.settings.clientId,
redirect_uri: `${provider.metadata.settings.redirectUri}/redirect`
},
cache: {
cachePlugin: persistenceCachePlugin
}
}
this.clientApplication = new PublicClientApplication(MSAL_CONFIG);
let accountProvider = new AzureAccountProvider(provider.metadata as AzureAccountProviderMetadata,
simpleTokenCache, this._context, this.clientApplication, this._uriEventHandler, this._authLibrary, isSaw);
this._accountProviders[provider.metadata.id] = accountProvider; this._accountProviders[provider.metadata.id] = accountProvider;
this._accountDisposals[provider.metadata.id] = azdata.accounts.registerAccountProvider(provider.metadata, accountProvider); this._accountDisposals[provider.metadata.id] = azdata.accounts.registerAccountProvider(provider.metadata, accountProvider);
} catch (e) { } catch (e) {
console.error(`Failed to register account provider: ${e}`); console.error(`Failed to register account provider, isSaw: ${isSaw}: ${e}`);
} }
} }

View File

@@ -49,7 +49,7 @@ const publicAzureSettings: ProviderSettings = {
}, },
armResource: { armResource: {
id: SettingIds.arm, id: SettingIds.arm,
endpoint: 'https://management.azure.com', endpoint: 'https://management.azure.com/',
azureResourceId: AzureResource.ResourceManagement azureResourceId: AzureResource.ResourceManagement
}, },
sqlResource: { sqlResource: {

View File

@@ -24,13 +24,15 @@ const typesClause = [
].map(type => `type == "${type}"`).join(' or '); ].map(type => `type == "${type}"`).join(' or ');
export class AzureDataGridProvider implements azdata.DataGridProvider { export class AzureDataGridProvider implements azdata.DataGridProvider {
constructor(private _appContext: AppContext) { } constructor(private _appContext: AppContext,
private readonly authLibrary: string) { }
public providerId = constants.dataGridProviderId; public providerId = constants.dataGridProviderId;
public title = loc.azureResourcesGridTitle; public title = loc.azureResourcesGridTitle;
public async getDataGridItems() { public async getDataGridItems() {
const accounts = await azdata.accounts.getAllAccounts(); let accounts: azdata.Account[];
accounts = azureResourceUtils.filterAccounts(await azdata.accounts.getAllAccounts(), this.authLibrary);
const items: any[] = []; const items: any[] = [];
await Promise.all(accounts.map(async (account) => { await Promise.all(accounts.map(async (account) => {
await Promise.all(account.properties.tenants.map(async (tenant: { id: string; }) => { await Promise.all(account.properties.tenants.map(async (tenant: { id: string; }) => {

View File

@@ -19,7 +19,7 @@ import { FlatAccountTreeNode } from './tree/flatAccountTreeNode';
import { ConnectionDialogTreeProvider } from './tree/connectionDialogTreeProvider'; import { ConnectionDialogTreeProvider } from './tree/connectionDialogTreeProvider';
import { AzureResourceErrorMessageUtil } from './utils'; import { AzureResourceErrorMessageUtil } from './utils';
export function registerAzureResourceCommands(appContext: AppContext, azureViewTree: AzureResourceTreeProvider, connectionDialogTree: ConnectionDialogTreeProvider): void { export function registerAzureResourceCommands(appContext: AppContext, azureViewTree: AzureResourceTreeProvider, connectionDialogTree: ConnectionDialogTreeProvider, authLibrary: string): void {
const trees = [azureViewTree, connectionDialogTree]; const trees = [azureViewTree, connectionDialogTree];
vscode.commands.registerCommand('azure.resource.startterminal', async (node?: TreeNode) => { vscode.commands.registerCommand('azure.resource.startterminal', async (node?: TreeNode) => {
try { try {

View File

@@ -28,7 +28,6 @@ export class AzureResourceSubscriptionService implements IAzureResourceSubscript
const subscriptions: azureResource.AzureResourceSubscription[] = []; const subscriptions: azureResource.AzureResourceSubscription[] = [];
let gotSubscriptions = false; let gotSubscriptions = false;
const errors: Error[] = []; const errors: Error[] = [];
for (const tenantId of tenantIds ?? account.properties.tenants.map(t => t.id)) { for (const tenantId of tenantIds ?? account.properties.tenants.map(t => t.id)) {
try { try {
const token = await azdata.accounts.getAccountSecurityToken(account, tenantId, azdata.AzureResource.ResourceManagement); const token = await azdata.accounts.getAccountSecurityToken(account, tenantId, azdata.AzureResource.ResourceManagement);
@@ -42,6 +41,7 @@ export class AzureResourceSubscriptionService implements IAzureResourceSubscript
tenant: tenantId tenant: tenantId
}; };
})); }));
Logger.verbose(`AzureResourceSubscriptionService.getSubscriptions: Retrieved ${newSubs.length} subscriptions for tenant ${tenantId} / account ${account.displayInfo.displayName}`);
gotSubscriptions = true; gotSubscriptions = true;
} }
else if (!account.isStale) { else if (!account.isStale) {

View File

@@ -66,6 +66,8 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
if (subscriptions.length === 0) { if (subscriptions.length === 0) {
return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.noSubscriptionsLabel, this)]; return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.noSubscriptionsLabel, this)];
} else { } else {
const authLibrary = vscode.workspace.getConfiguration('azure').get('authenticationLibrary');
if (authLibrary === 'ADAL') {
// Filter out everything that we can't authenticate to. // Filter out everything that we can't authenticate to.
const hasTokenResults = await Promise.all(subscriptions.map(async s => { const hasTokenResults = await Promise.all(subscriptions.map(async s => {
let token: azdata.accounts.AccountSecurityToken | undefined = undefined; let token: azdata.accounts.AccountSecurityToken | undefined = undefined;
@@ -82,7 +84,7 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
return true; return true;
})); }));
subscriptions = subscriptions.filter((_s, i) => hasTokenResults[i]); subscriptions = subscriptions.filter((_s, i) => hasTokenResults[i]);
}
let subTreeNodes = await Promise.all(subscriptions.map(async (subscription) => { let subTreeNodes = await Promise.all(subscriptions.map(async (subscription) => {
return new AzureResourceSubscriptionTreeNode(this.account, subscription, subscription.tenant!, this.appContext, this.treeChangeHandler, this); return new AzureResourceSubscriptionTreeNode(this.account, subscription, subscription.tenant!, this.appContext, this.treeChangeHandler, this);
})); }));
@@ -164,4 +166,9 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
private _selectedSubscriptionCount = 0; private _selectedSubscriptionCount = 0;
private static readonly noSubscriptionsLabel = localize('azure.resource.tree.accountTreeNode.noSubscriptionsLabel', "No Subscriptions found."); private static readonly noSubscriptionsLabel = localize('azure.resource.tree.accountTreeNode.noSubscriptionsLabel', "No Subscriptions found.");
sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
} }

View File

@@ -13,7 +13,7 @@ import { TreeNode } from '../treeNode';
import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode'; import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode';
import { AzureResourceMessageTreeNode } from '../messageTreeNode'; import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { AzureResourceErrorMessageUtil, equals } from '../utils'; import { AzureResourceErrorMessageUtil, equals, filterAccounts } from '../utils';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { FlatAccountTreeNode } from './flatAccountTreeNode'; import { FlatAccountTreeNode } from './flatAccountTreeNode';
import { Logger } from '../../utils/Logger'; import { Logger } from '../../utils/Logger';
@@ -26,10 +26,11 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider<Tre
private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode | undefined>(); private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode | undefined>();
private loadingAccountsPromise: Promise<void> | undefined; private loadingAccountsPromise: Promise<void> | undefined;
public constructor(private readonly appContext: AppContext) { public constructor(private readonly appContext: AppContext,
private readonly authLibrary: string) {
azdata.accounts.onDidChangeAccounts(async (e: azdata.DidChangeAccountsParams) => { azdata.accounts.onDidChangeAccounts(async (e: azdata.DidChangeAccountsParams) => {
// This event sends it per provider, we need to make sure we get all the azure related accounts // This event sends it per provider, we need to make sure we get all the azure related accounts
let accounts = await azdata.accounts.getAllAccounts(); let accounts = filterAccounts(await azdata.accounts.getAllAccounts(), authLibrary);
accounts = accounts.filter(a => a.key.providerId.startsWith('azure')); accounts = accounts.filter(a => a.key.providerId.startsWith('azure'));
// the onDidChangeAccounts event will trigger in many cases where the accounts didn't actually change // the onDidChangeAccounts event will trigger in many cases where the accounts didn't actually change
// the notifyNodeChanged event triggers a refresh which triggers a getChildren which can trigger this callback // the notifyNodeChanged event triggers a refresh which triggers a getChildren which can trigger this callback
@@ -55,10 +56,11 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider<Tre
} }
if (this.accounts && this.accounts.length > 0) { if (this.accounts && this.accounts.length > 0) {
let accounts = filterAccounts(this.accounts, this.authLibrary);
const accountNodes: FlatAccountTreeNode[] = []; const accountNodes: FlatAccountTreeNode[] = [];
const errorMessages: string[] = []; const errorMessages: string[] = [];
// We are doing sequential account loading to avoid the Azure request throttling // We are doing sequential account loading to avoid the Azure request throttling
for (const account of this.accounts) { for (const account of accounts) {
try { try {
const accountNode = new FlatAccountTreeNode(account, this.appContext, this); const accountNode = new FlatAccountTreeNode(account, this.appContext, this);
await accountNode.updateLabel(); await accountNode.updateLabel();
@@ -85,7 +87,7 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider<Tre
private async loadAccounts(): Promise<void> { private async loadAccounts(): Promise<void> {
try { try {
this.accounts = await azdata.accounts.getAllAccounts(); this.accounts = filterAccounts(await azdata.accounts.getAllAccounts(), this.authLibrary);
// System has been initialized // System has been initialized
this.setSystemInitialized(); this.setSystemInitialized();
this._onDidChangeTreeData.fire(undefined); this._onDidChangeTreeData.fire(undefined);

View File

@@ -12,12 +12,12 @@ const localize = nls.loadMessageBundle();
import { TreeNode } from '../treeNode'; import { TreeNode } from '../treeNode';
import { AzureResourceMessageTreeNode } from '../messageTreeNode'; import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { AzureResourceErrorMessageUtil } from '../utils'; import { AzureResourceErrorMessageUtil, filterAccounts } from '../utils';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { IAzureResourceNodeWithProviderId, IAzureResourceSubscriptionService } from '../interfaces'; import { IAzureResourceNodeWithProviderId, IAzureResourceSubscriptionService } from '../interfaces';
import { AzureResourceServiceNames } from '../constants'; import { AzureResourceServiceNames } from '../constants';
import { AzureResourceService } from '../resourceService'; import { AzureResourceService } from '../resourceService';
import { Logger } from '../../utils/Logger';
export class FlatAzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNode>, IAzureResourceTreeChangeHandler { export class FlatAzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNode>, IAzureResourceTreeChangeHandler {
public isSystemInitialized: boolean = false; public isSystemInitialized: boolean = false;
@@ -26,7 +26,8 @@ export class FlatAzureResourceTreeProvider implements vscode.TreeDataProvider<Tr
private resourceLoader: ResourceLoader | undefined; private resourceLoader: ResourceLoader | undefined;
public constructor(private readonly appContext: AppContext) { public constructor(private readonly appContext: AppContext,
private readonly authLibrary: string) {
} }
public async getChildren(element?: TreeNode): Promise<TreeNode[]> { public async getChildren(element?: TreeNode): Promise<TreeNode[]> {
@@ -35,7 +36,7 @@ export class FlatAzureResourceTreeProvider implements vscode.TreeDataProvider<Tr
} }
if (!this.resourceLoader) { if (!this.resourceLoader) {
this.resourceLoader = new ResourceLoader(this.appContext); this.resourceLoader = new ResourceLoader(this.appContext, this.authLibrary);
this.resourceLoader.onDidAddNewResource(e => this._onDidChangeTreeData.fire(e)); this.resourceLoader.onDidAddNewResource(e => this._onDidChangeTreeData.fire(e));
} }
@@ -87,7 +88,8 @@ class ResourceLoader {
private readonly _onDidAddNewResource = new vscode.EventEmitter<TreeNode | undefined>(); private readonly _onDidAddNewResource = new vscode.EventEmitter<TreeNode | undefined>();
public readonly onDidAddNewResource = this._onDidAddNewResource.event; public readonly onDidAddNewResource = this._onDidAddNewResource.event;
constructor(private readonly appContext: AppContext) { constructor(private readonly appContext: AppContext,
private readonly authLibrary: string) {
this.subscriptionService = appContext.getService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService); this.subscriptionService = appContext.getService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService);
this.resourceService = appContext.getService<AzureResourceService>(AzureResourceServiceNames.resourceService); this.resourceService = appContext.getService<AzureResourceService>(AzureResourceServiceNames.resourceService);
} }
@@ -118,7 +120,7 @@ class ResourceLoader {
this._state = LoaderState.Loading; this._state = LoaderState.Loading;
const accounts = await azdata.accounts.getAllAccounts(); const accounts = filterAccounts(await azdata.accounts.getAllAccounts(), this.authLibrary);
for (const account of accounts) { for (const account of accounts) {
for (const tenant of account.properties.tenants) { for (const tenant of account.properties.tenants) {
@@ -141,7 +143,7 @@ class ResourceLoader {
} }
} }
console.log('finished loading'); Logger.verbose('finished loading all accounts and subscriptions');
clearInterval(interval); clearInterval(interval);
@@ -208,5 +210,4 @@ class AzureResourceResourceTreeNode extends TreeNode {
public get nodePathValue(): string { public get nodePathValue(): string {
return this.resourceNodeWithProviderId.resourceNode.treeItem.id || ''; return this.resourceNodeWithProviderId.resourceNode.treeItem.id || '';
} }
} }

View File

@@ -14,11 +14,10 @@ import { AzureResourceAccountTreeNode } from './accountTreeNode';
import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode'; import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode';
import { AzureResourceMessageTreeNode } from '../messageTreeNode'; import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { AzureResourceErrorMessageUtil, equals } from '../utils'; import { AzureResourceErrorMessageUtil, equals, filterAccounts } from '../utils';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { AzureAccount } from 'azurecore'; import { AzureAccount } from 'azurecore';
export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNode>, IAzureResourceTreeChangeHandler { export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNode>, IAzureResourceTreeChangeHandler {
public isSystemInitialized: boolean = false; public isSystemInitialized: boolean = false;
@@ -26,10 +25,11 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode | undefined>(); private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode | undefined>();
private loadingAccountsPromise: Promise<void> | undefined; private loadingAccountsPromise: Promise<void> | undefined;
public constructor(private readonly appContext: AppContext) { public constructor(private readonly appContext: AppContext,
private readonly authLibrary: string) {
azdata.accounts.onDidChangeAccounts(async (e: azdata.DidChangeAccountsParams) => { azdata.accounts.onDidChangeAccounts(async (e: azdata.DidChangeAccountsParams) => {
// This event sends it per provider, we need to make sure we get all the azure related accounts // This event sends it per provider, we need to make sure we get all the azure related accounts
let accounts = await azdata.accounts.getAllAccounts(); let accounts = filterAccounts(await azdata.accounts.getAllAccounts(), authLibrary);
accounts = accounts.filter(a => a.key.providerId.startsWith('azure')); accounts = accounts.filter(a => a.key.providerId.startsWith('azure'));
// the onDidChangeAccounts event will trigger in many cases where the accounts didn't actually change // the onDidChangeAccounts event will trigger in many cases where the accounts didn't actually change
// the notifyNodeChanged event triggers a refresh which triggers a getChildren which can trigger this callback // the notifyNodeChanged event triggers a refresh which triggers a getChildren which can trigger this callback
@@ -56,6 +56,7 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
try { try {
if (this.accounts && this.accounts.length > 0) { if (this.accounts && this.accounts.length > 0) {
this.accounts = filterAccounts(this.accounts, this.authLibrary);
return this.accounts.map((account) => new AzureResourceAccountTreeNode(account, this.appContext, this)); return this.accounts.map((account) => new AzureResourceAccountTreeNode(account, this.appContext, this));
} else { } else {
return [new AzureResourceAccountNotSignedInTreeNode()]; return [new AzureResourceAccountNotSignedInTreeNode()];
@@ -67,7 +68,7 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
private async loadAccounts(): Promise<void> { private async loadAccounts(): Promise<void> {
try { try {
this.accounts = await azdata.accounts.getAllAccounts(); this.accounts = filterAccounts(await azdata.accounts.getAllAccounts(), this.authLibrary);
// System has been initialized // System has been initialized
this.setSystemInitialized(); this.setSystemInitialized();
this._onDidChangeTreeData.fire(undefined); this._onDidChangeTreeData.fire(undefined);
@@ -96,7 +97,6 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
node.clearCache(); node.clearCache();
} }
} }
this._onDidChangeTreeData.fire(node); this._onDidChangeTreeData.fire(node);
} }

View File

@@ -17,6 +17,7 @@ import { IAzureResourceSubscriptionFilterService, IAzureResourceSubscriptionServ
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService'; import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
import { BlobServiceClient, StorageSharedKeyCredential } from '@azure/storage-blob'; import { BlobServiceClient, StorageSharedKeyCredential } from '@azure/storage-blob';
import providerSettings from '../account-provider/providerSettings'; import providerSettings from '../account-provider/providerSettings';
import * as Constants from '../constants';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
@@ -541,3 +542,18 @@ export function getProviderMetadataForAccount(account: AzureAccount): AzureAccou
return provider.metadata; return provider.metadata;
} }
// Filter accounts based on currently selected Auth Library:
// if the account key is present, filter based on current auth library
// if there is no account key (pre-MSAL account), then it is an ADAL account and
// should be displayed as long as ADAL is the currently selected auth library
export function filterAccounts(accounts: azdata.Account[], authLibrary: string): azdata.Account[] {
let filteredAccounts = accounts.filter(account => {
if (account.key.authLibrary) {
return account.key.authLibrary === authLibrary;
} else {
return authLibrary === Constants.AuthLibrary.ADAL;
}
});
return filteredAccounts;
}

View File

@@ -3,13 +3,106 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
export const extensionConfigSectionName = 'azure'; export const Account = 'account';
export const AccountsSection = 'accounts';
export const AuthSection = 'auth';
export const AuthenticationLibrarySection = 'authenticationLibrary';
export const AzureSection = 'azure';
export const AzureAccountProviderCredentials = 'azureAccountProviderCredentials';
export const CloudSection = 'cloud';
export const ClearTokenCacheCommand = 'clearTokenCache';
export const ConfigSection = 'config';
export const AccountsClearTokenCacheCommand = AccountsSection + '.' + ClearTokenCacheCommand;
export const AccountsAzureAuthSection = AccountsSection + '.' + AzureSection + '.' + AuthSection;
export const AccountsAzureCloudSection = AccountsSection + '.' + AzureSection + '.' + CloudSection;
export const AzureAuthenticationLibrarySection = AzureSection + '.' + AuthenticationLibrarySection;
export const EnableArcFeaturesSection = 'enableArcFeatures';
export const ServiceName = 'azuredatastudio';
export const TenantSection = 'tenant';
export const AzureTenantConfigSection = AzureSection + '.' + TenantSection + '.' + ConfigSection;
export const NoSystemKeyChainSection = 'noSystemKeychain';
/** MSAL Account version */
export const AccountVersion = '2.0';
export const Bearer = 'Bearer';
/**
* Use SHA-256 algorithm
*/
export const S256_CODE_CHALLENGE_METHOD = 'S256';
export const SELECT_ACCOUNT = 'select_account';
export const ConfigFilePath = './cache.json'
export const Saw = 'saw';
export const ViewType = 'view'; export const ViewType = 'view';
export const HomeCategory = 'Home';
export const dataGridProviderId = 'azure-resources'; export const dataGridProviderId = 'azure-resources';
export const AzureTokenFolderName = 'Azure Accounts'; export const AzureTokenFolderName = 'Azure Accounts';
export const DefaultAuthLibrary = 'ADAL';
export enum BuiltInCommands { export enum BuiltInCommands {
SetContext = 'setContext' SetContext = 'setContext'
} }
/**
* AAD Auth library as selected.
*/
export enum AuthLibrary {
MSAL = 'MSAL',
ADAL = 'ADAL'
}
/**
* Authentication type as selected.
*/
export enum AuthType {
DeviceCode = 'deviceCode',
CodeGrant = 'codeGrant'
}
/**
* Account issuer as received from access token
*/
export enum AccountIssuer {
Corp = 'corp',
Msft = 'msft',
}
/**
* Azure Account type as received from access token
*/
export enum AccountType {
WorkSchool = 'work_school',
Microsoft = 'microsoft',
}
export enum Platform {
Windows = 'win32',
Mac = 'darwin',
Linux = 'linux'
}

View File

@@ -45,7 +45,7 @@ import * as azurecore from 'azurecore';
import * as azureResourceUtils from './azureResource/utils'; import * as azureResourceUtils from './azureResource/utils';
import * as utils from './utils'; import * as utils from './utils';
import * as loc from './localizedConstants'; import * as loc from './localizedConstants';
import * as constants from './constants'; import * as Constants from './constants';
import { AzureResourceGroupService } from './azureResource/providers/resourceGroup/resourceGroupService'; import { AzureResourceGroupService } from './azureResource/providers/resourceGroup/resourceGroupService';
import { Logger } from './utils/Logger'; import { Logger } from './utils/Logger';
import { ConnectionDialogTreeProvider } from './azureResource/tree/connectionDialogTreeProvider'; import { ConnectionDialogTreeProvider } from './azureResource/tree/connectionDialogTreeProvider';
@@ -58,15 +58,15 @@ let extensionContext: vscode.ExtensionContext;
function getAppDataPath() { function getAppDataPath() {
let platform = process.platform; let platform = process.platform;
switch (platform) { switch (platform) {
case 'win32': return process.env['APPDATA'] || path.join(process.env['USERPROFILE']!, 'AppData', 'Roaming'); case Constants.Platform.Windows: return process.env['APPDATA'] || path.join(process.env['USERPROFILE']!, 'AppData', 'Roaming');
case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support'); case Constants.Platform.Mac: return path.join(os.homedir(), 'Library', 'Application Support');
case 'linux': return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config'); case Constants.Platform.Linux: return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
default: throw new Error('Platform not supported'); default: throw new Error('Platform not supported');
} }
} }
function getDefaultLogLocation() { function getDefaultLogLocation() {
return path.join(getAppDataPath(), 'azuredatastudio'); return path.join(getAppDataPath(), Constants.ServiceName);
} }
function pushDisposable(disposable: vscode.Disposable): void { function pushDisposable(disposable: vscode.Disposable): void {
@@ -85,24 +85,27 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
} }
// TODO: Since Code Grant auth doesnt work in web mode, enabling Device code auth by default for web mode. We can remove this once we have that working in web mode. // TODO: Since Code Grant auth doesnt work in web mode, enabling Device code auth by default for web mode. We can remove this once we have that working in web mode.
const config = vscode.workspace.getConfiguration('accounts.azure.auth'); const config = vscode.workspace.getConfiguration(Constants.AccountsAzureAuthSection);
if (vscode.env.uiKind === vscode.UIKind.Web) { if (vscode.env.uiKind === vscode.UIKind.Web) {
await config.update('deviceCode', true, vscode.ConfigurationTarget.Global); await config.update('deviceCode', true, vscode.ConfigurationTarget.Global);
} }
const authLibrary: string = vscode.workspace.getConfiguration(Constants.AzureSection).get(Constants.AuthenticationLibrarySection)
?? Constants.DefaultAuthLibrary;
updatePiiLoggingLevel(); updatePiiLoggingLevel();
// Create the provider service and activate // Create the provider service and activate
initAzureAccountProvider(extensionContext, storagePath).catch((err) => console.log(err)); initAzureAccountProvider(extensionContext, storagePath, authLibrary!).catch((err) => console.log(err));
registerAzureServices(appContext); registerAzureServices(appContext);
const azureResourceTree = new AzureResourceTreeProvider(appContext); const azureResourceTree = new AzureResourceTreeProvider(appContext, authLibrary);
const connectionDialogTree = new ConnectionDialogTreeProvider(appContext); const connectionDialogTree = new ConnectionDialogTreeProvider(appContext, authLibrary);
pushDisposable(vscode.window.registerTreeDataProvider('azureResourceExplorer', azureResourceTree)); pushDisposable(vscode.window.registerTreeDataProvider('azureResourceExplorer', azureResourceTree));
pushDisposable(vscode.window.registerTreeDataProvider('connectionDialog/azureResourceExplorer', connectionDialogTree)); pushDisposable(vscode.window.registerTreeDataProvider('connectionDialog/azureResourceExplorer', connectionDialogTree));
pushDisposable(vscode.workspace.onDidChangeConfiguration(e => onDidChangeConfiguration(e))); pushDisposable(vscode.workspace.onDidChangeConfiguration(e => onDidChangeConfiguration(e)));
registerAzureResourceCommands(appContext, azureResourceTree, connectionDialogTree); registerAzureResourceCommands(appContext, azureResourceTree, connectionDialogTree, authLibrary);
azdata.dataprotocol.registerDataGridProvider(new AzureDataGridProvider(appContext)); azdata.dataprotocol.registerDataGridProvider(new AzureDataGridProvider(appContext, authLibrary));
vscode.commands.registerCommand('azure.dataGrid.openInAzurePortal', async (item: azdata.DataGridItem) => { vscode.commands.registerCommand('azure.dataGrid.openInAzurePortal', async (item: azdata.DataGridItem) => {
const portalEndpoint = item.portalEndpoint; const portalEndpoint = item.portalEndpoint;
const subscriptionId = item.subscriptionId; const subscriptionId = item.subscriptionId;
@@ -130,7 +133,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
return azureResourceUtils.getLocations(appContext, account, subscription, ignoreErrors); return azureResourceUtils.getLocations(appContext, account, subscription, ignoreErrors);
}, },
provideResources(): azurecore.azureResource.IAzureResourceProvider[] { provideResources(): azurecore.azureResource.IAzureResourceProvider[] {
const arcFeaturedEnabled = vscode.workspace.getConfiguration(constants.extensionConfigSectionName).get('enableArcFeatures'); const arcFeaturedEnabled = vscode.workspace.getConfiguration(Constants.AzureSection).get(Constants.EnableArcFeaturesSection);
const providers: azurecore.azureResource.IAzureResourceProvider[] = [ const providers: azurecore.azureResource.IAzureResourceProvider[] = [
new KustoProvider(new KustoResourceService(), extensionContext), new KustoProvider(new KustoResourceService(), extensionContext),
new AzureMonitorProvider(new AzureMonitorResourceService(), extensionContext), new AzureMonitorProvider(new AzureMonitorResourceService(), extensionContext),
@@ -233,7 +236,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
// Create the folder for storing the token caches // Create the folder for storing the token caches
async function findOrMakeStoragePath() { async function findOrMakeStoragePath() {
let defaultLogLocation = getDefaultLogLocation(); let defaultLogLocation = getDefaultLogLocation();
let storagePath = path.join(defaultLogLocation, constants.AzureTokenFolderName); let storagePath = path.join(defaultLogLocation, Constants.AzureTokenFolderName);
try { try {
await fs.mkdir(defaultLogLocation, { recursive: true }); await fs.mkdir(defaultLogLocation, { recursive: true });
@@ -258,9 +261,9 @@ async function findOrMakeStoragePath() {
return storagePath; return storagePath;
} }
async function initAzureAccountProvider(extensionContext: vscode.ExtensionContext, storagePath: string): Promise<void> { async function initAzureAccountProvider(extensionContext: vscode.ExtensionContext, storagePath: string, authLibrary: string): Promise<void> {
try { try {
const accountProviderService = new AzureAccountProviderService(extensionContext, storagePath); const accountProviderService = new AzureAccountProviderService(extensionContext, storagePath, authLibrary);
extensionContext.subscriptions.push(accountProviderService); extensionContext.subscriptions.push(accountProviderService);
await accountProviderService.activate(); await accountProviderService.activate();
} catch (err) { } catch (err) {
@@ -281,10 +284,27 @@ async function onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent): Pro
if (e.affectsConfiguration('azure.piiLogging')) { if (e.affectsConfiguration('azure.piiLogging')) {
updatePiiLoggingLevel(); updatePiiLoggingLevel();
} }
if (e.affectsConfiguration('azure.authenticationLibrary')) {
await displayReloadAds();
}
} }
function updatePiiLoggingLevel(): void { function updatePiiLoggingLevel(): void {
const piiLogging: boolean = vscode.workspace.getConfiguration(constants.extensionConfigSectionName).get('piiLogging', false); const piiLogging: boolean = vscode.workspace.getConfiguration(Constants.AzureSection).get('piiLogging', false);
Logger.piiLogging = piiLogging; Logger.piiLogging = piiLogging;
} }
// Display notification with button to reload
// return true if button clicked
// return false if button not clicked
async function displayReloadAds(): Promise<boolean> {
const result = await vscode.window.showInformationMessage(loc.reloadPrompt, loc.reloadChoice);
if (result === loc.reloadChoice) {
await vscode.commands.executeCommand('workbench.action.reloadWindow');
return true;
} else {
return false;
}
}

View File

@@ -62,6 +62,9 @@ export const location = localize('azurecore.location', "Location");
export const subscription = localize('azurecore.subscription', "Subscription"); export const subscription = localize('azurecore.subscription', "Subscription");
export const typeIcon = localize('azurecore.typeIcon', "Type Icon"); export const typeIcon = localize('azurecore.typeIcon', "Type Icon");
export const reloadPrompt = localize('azurecore.reloadPrompt', "Authentication Library has changed, please reload Azure Data Studio.");
export const reloadChoice = localize('azurecore.reloadChoice', "Reload Azure Data Studio");
// Azure Resource Types // Azure Resource Types
export const sqlServer = localize('azurecore.sqlServer', "SQL server"); export const sqlServer = localize('azurecore.sqlServer', "SQL server");
export const sqlDatabase = localize('azurecore.sqlDatabase', "SQL database"); export const sqlDatabase = localize('azurecore.sqlDatabase', "SQL database");

View File

@@ -13,7 +13,6 @@ import providerSettings from '../../../account-provider/providerSettings';
import { AzureResource } from 'azdata'; import { AzureResource } from 'azdata';
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
let azureAuthCodeGrant: TypeMoq.IMock<AzureAuthCodeGrant>; let azureAuthCodeGrant: TypeMoq.IMock<AzureAuthCodeGrant>;
// let azureDeviceCode: TypeMoq.IMock<AzureDeviceCode>; // let azureDeviceCode: TypeMoq.IMock<AzureDeviceCode>;
@@ -52,9 +51,21 @@ describe('Azure Authentication', function () {
mockAccount = { mockAccount = {
isStale: false, isStale: false,
displayInfo: {
contextualDisplayName: 'test',
accountType: 'test',
displayName: 'test',
userId: 'test'
},
key: {
providerId: 'test',
accountId: 'test'
},
properties: { properties: {
owningTenant: mockTenant, owningTenant: mockTenant,
tenants: [mockTenant] tenants: [mockTenant],
providerSettings: provider,
isMsAccount: true
} }
} as AzureAccount; } as AzureAccount;
@@ -68,7 +79,7 @@ describe('Azure Authentication', function () {
it('accountHydration should yield a valid account', async function () { it('accountHydration should yield a valid account', async function () {
azureAuthCodeGrant.setup(x => x.getTenants(mockToken)).returns((): Promise<Tenant[]> => { azureAuthCodeGrant.setup(x => x.getTenantsAdal(mockToken)).returns((): Promise<Tenant[]> => {
return Promise.resolve([ return Promise.resolve([
mockTenant mockTenant
]); ]);
@@ -83,30 +94,30 @@ describe('Azure Authentication', function () {
describe('getAccountSecurityToken', function () { describe('getAccountSecurityToken', function () {
it('should be undefined on stale account', async function () { it('should be undefined on stale account', async function () {
mockAccount.isStale = true; mockAccount.isStale = true;
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, TypeMoq.It.isAny(), TypeMoq.It.isAny()); const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, TypeMoq.It.isAny(), TypeMoq.It.isAny());
should(securityToken).be.undefined(); should(securityToken).be.undefined();
}); });
it('dont find correct resources', async function () { it('dont find correct resources', async function () {
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, TypeMoq.It.isAny(), -1); const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, TypeMoq.It.isAny(), -1);
should(securityToken).be.undefined(); should(securityToken).be.undefined();
}); });
it('incorrect tenant', async function () { it('incorrect tenant', async function () {
await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, 'invalid_tenant', AzureResource.MicrosoftResourceManagement).should.be.rejected(); await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, 'invalid_tenant', AzureResource.MicrosoftResourceManagement).should.be.rejected();
}); });
it('token recieved for ossRdbmns resource', async function () { it('token recieved for ossRdbmns resource', async function () {
azureAuthCodeGrant.setup(x => x.getTenants(mockToken)).returns(() => { azureAuthCodeGrant.setup(x => x.getTenantsAdal(mockToken)).returns(() => {
return Promise.resolve([ return Promise.resolve([
mockTenant mockTenant
]); ]);
}); });
azureAuthCodeGrant.setup(x => x.getTokenHelper(mockTenant, provider.settings.ossRdbmsResource!, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { azureAuthCodeGrant.setup(x => x.getTokenHelperAdal(mockTenant, provider.settings.ossRdbmsResource!, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
return Promise.resolve({ return Promise.resolve({
accessToken: mockAccessToken accessToken: mockAccessToken
} as OAuthTokenResponse); } as OAuthTokenResponse);
}); });
azureAuthCodeGrant.setup(x => x.refreshToken(mockTenant, provider.settings.ossRdbmsResource!, mockRefreshToken)).returns((): Promise<OAuthTokenResponse> => { azureAuthCodeGrant.setup(x => x.refreshTokenAdal(mockTenant, provider.settings.ossRdbmsResource!, mockRefreshToken)).returns((): Promise<OAuthTokenResponse> => {
const mockToken: AccessToken = JSON.parse(JSON.stringify(mockAccessToken)); const mockToken: AccessToken = JSON.parse(JSON.stringify(mockAccessToken));
delete (mockToken as any).invalidData; delete (mockToken as any).invalidData;
return Promise.resolve({ return Promise.resolve({
@@ -114,7 +125,7 @@ describe('Azure Authentication', function () {
} as OAuthTokenResponse); } as OAuthTokenResponse);
}); });
azureAuthCodeGrant.setup(x => x.getSavedToken(mockTenant, provider.settings.ossRdbmsResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => { azureAuthCodeGrant.setup(x => x.getSavedTokenAdal(mockTenant, provider.settings.ossRdbmsResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
return Promise.resolve({ return Promise.resolve({
accessToken: mockAccessToken, accessToken: mockAccessToken,
refreshToken: mockRefreshToken, refreshToken: mockRefreshToken,
@@ -122,21 +133,21 @@ describe('Azure Authentication', function () {
}); });
}); });
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.OssRdbms); const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, mockTenant.id, AzureResource.OssRdbms);
should(securityToken?.token).be.equal(mockAccessToken.token, 'Token are not similar'); should(securityToken?.token).be.equal(mockAccessToken.token, 'Token are not similar');
}); });
it('saved token exists and can be reused', async function () { it('saved token exists and can be reused', async function () {
delete (mockAccessToken as any).tokenType; delete (mockAccessToken as any).tokenType;
azureAuthCodeGrant.setup(x => x.getSavedToken(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => { azureAuthCodeGrant.setup(x => x.getSavedTokenAdal(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
return Promise.resolve({ return Promise.resolve({
accessToken: mockAccessToken, accessToken: mockAccessToken,
refreshToken: mockRefreshToken, refreshToken: mockRefreshToken,
expiresOn: `${(new Date().getTime() / 1000) + (10 * 60)}` expiresOn: `${(new Date().getTime() / 1000) + (10 * 60)}`
}); });
}); });
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement); const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
should(securityToken?.tokenType).be.equal('Bearer', 'tokenType should be bearer on a successful getSecurityToken from cache'); should(securityToken?.tokenType).be.equal('Bearer', 'tokenType should be bearer on a successful getSecurityToken from cache');
}); });
@@ -145,47 +156,47 @@ describe('Azure Authentication', function () {
it('saved token had invalid expiration', async function () { it('saved token had invalid expiration', async function () {
delete (mockAccessToken as any).tokenType; delete (mockAccessToken as any).tokenType;
(mockAccessToken as any).invalidData = 'this should not exist on response'; (mockAccessToken as any).invalidData = 'this should not exist on response';
azureAuthCodeGrant.setup(x => x.getSavedToken(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => { azureAuthCodeGrant.setup(x => x.getSavedTokenAdal(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
return Promise.resolve({ return Promise.resolve({
accessToken: mockAccessToken, accessToken: mockAccessToken,
refreshToken: mockRefreshToken, refreshToken: mockRefreshToken,
expiresOn: 'invalid' expiresOn: 'invalid'
}); });
}); });
azureAuthCodeGrant.setup(x => x.refreshToken(mockTenant, provider.settings.microsoftResource!, mockRefreshToken)).returns((): Promise<OAuthTokenResponse> => { azureAuthCodeGrant.setup(x => x.refreshTokenAdal(mockTenant, provider.settings.microsoftResource!, mockRefreshToken)).returns((): Promise<OAuthTokenResponse> => {
const mockToken: AccessToken = JSON.parse(JSON.stringify(mockAccessToken)); const mockToken: AccessToken = JSON.parse(JSON.stringify(mockAccessToken));
delete (mockToken as any).invalidData; delete (mockToken as any).invalidData;
return Promise.resolve({ return Promise.resolve({
accessToken: mockToken accessToken: mockToken
} as OAuthTokenResponse); } as OAuthTokenResponse);
}); });
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement); const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
should((securityToken as any).invalidData).be.undefined(); // Ensure its a new one should((securityToken as any).invalidData).be.undefined(); // Ensure its a new one
should(securityToken?.tokenType).be.equal('Bearer', 'tokenType should be bearer on a successful getSecurityToken from cache'); should(securityToken?.tokenType).be.equal('Bearer', 'tokenType should be bearer on a successful getSecurityToken from cache');
azureAuthCodeGrant.verify(x => x.refreshToken(mockTenant, provider.settings.microsoftResource!, mockRefreshToken), TypeMoq.Times.once()); azureAuthCodeGrant.verify(x => x.refreshTokenAdal(mockTenant, provider.settings.microsoftResource!, mockRefreshToken), TypeMoq.Times.once());
}); });
describe('no saved token', function () { describe('no saved token', function () {
it('no base token', async function () { it('no base token', async function () {
azureAuthCodeGrant.setup(x => x.getSavedToken(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string } | undefined> => { azureAuthCodeGrant.setup(x => x.getSavedTokenAdal(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string } | undefined> => {
return Promise.resolve(undefined); return Promise.resolve(undefined);
}); });
azureAuthCodeGrant.setup(x => x.getSavedToken(azureAuthCodeGrant.object.commonTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string } | undefined> => { azureAuthCodeGrant.setup(x => x.getSavedTokenAdal(azureAuthCodeGrant.object.commonTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string } | undefined> => {
return Promise.resolve(undefined); return Promise.resolve(undefined);
}); });
await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement).should.be.rejected(); await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement).should.be.rejected();
}); });
it('base token exists', async function () { it('base token exists', async function () {
azureAuthCodeGrant.setup(x => x.getSavedToken(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string } | undefined> => { azureAuthCodeGrant.setup(x => x.getSavedTokenAdal(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string } | undefined> => {
return Promise.resolve(undefined); return Promise.resolve(undefined);
}); });
azureAuthCodeGrant.setup(x => x.getSavedToken(azureAuthCodeGrant.object.commonTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => { azureAuthCodeGrant.setup(x => x.getSavedTokenAdal(azureAuthCodeGrant.object.commonTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
return Promise.resolve({ return Promise.resolve({
accessToken: mockAccessToken, accessToken: mockAccessToken,
refreshToken: mockRefreshToken, refreshToken: mockRefreshToken,
@@ -194,13 +205,13 @@ describe('Azure Authentication', function () {
}); });
delete (mockAccessToken as any).tokenType; delete (mockAccessToken as any).tokenType;
azureAuthCodeGrant.setup(x => x.refreshToken(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { azureAuthCodeGrant.setup(x => x.refreshTokenAdal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
return Promise.resolve({ return Promise.resolve({
accessToken: mockAccessToken accessToken: mockAccessToken
} as OAuthTokenResponse); } as OAuthTokenResponse);
}); });
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement); const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
should(securityToken?.tokenType).be.equal('Bearer', 'tokenType should be bearer on a successful getSecurityToken from cache'); should(securityToken?.tokenType).be.equal('Bearer', 'tokenType should be bearer on a successful getSecurityToken from cache');
}); });
}); });
@@ -218,16 +229,16 @@ describe('Azure Authentication', function () {
} as AxiosResponse<any>); } as AxiosResponse<any>);
}); });
azureAuthCodeGrant.setup(x => x.handleInteractionRequired(mockTenant, provider.settings.microsoftResource!)).returns(() => { azureAuthCodeGrant.setup(x => x.handleInteractionRequiredAdal(mockTenant, provider.settings.microsoftResource!)).returns(() => {
return Promise.resolve({ return Promise.resolve({
accessToken: mockAccessToken accessToken: mockAccessToken
} as OAuthTokenResponse); } as OAuthTokenResponse);
}); });
const result = await azureAuthCodeGrant.object.getToken(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData); const result = await azureAuthCodeGrant.object.getTokenAdal(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData);
azureAuthCodeGrant.verify(x => x.handleInteractionRequired(mockTenant, provider.settings.microsoftResource!), TypeMoq.Times.once()); azureAuthCodeGrant.verify(x => x.handleInteractionRequiredAdal(mockTenant, provider.settings.microsoftResource!), TypeMoq.Times.once());
should(result?.accessToken).be.deepEqual(mockAccessToken); should(result?.accessToken).be.deepEqual(mockAccessToken);
}); });
@@ -241,7 +252,7 @@ describe('Azure Authentication', function () {
} as AxiosResponse<any>); } as AxiosResponse<any>);
}); });
await azureAuthCodeGrant.object.getToken(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData).should.be.rejected(); await azureAuthCodeGrant.object.getTokenAdal(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData).should.be.rejected();
}); });
it('calls getTokenHelper', async function () { it('calls getTokenHelper', async function () {
@@ -255,16 +266,16 @@ describe('Azure Authentication', function () {
} as AxiosResponse<any>); } as AxiosResponse<any>);
}); });
azureAuthCodeGrant.setup(x => x.getTokenHelper(mockTenant, provider.settings.microsoftResource!, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { azureAuthCodeGrant.setup(x => x.getTokenHelperAdal(mockTenant, provider.settings.microsoftResource!, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
return Promise.resolve({ return Promise.resolve({
accessToken: mockAccessToken accessToken: mockAccessToken
} as OAuthTokenResponse); } as OAuthTokenResponse);
}); });
const result = await azureAuthCodeGrant.object.getToken(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData); const result = await azureAuthCodeGrant.object.getTokenAdal(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData);
azureAuthCodeGrant.verify(x => x.getTokenHelper(mockTenant, provider.settings.microsoftResource!, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); azureAuthCodeGrant.verify(x => x.getTokenHelperAdal(mockTenant, provider.settings.microsoftResource!, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
should(result?.accessToken).be.deepEqual(mockAccessToken); should(result?.accessToken).be.deepEqual(mockAccessToken);
}); });

View File

@@ -28,9 +28,11 @@ import allSettings from '../../../account-provider/providerSettings';
// Mock services // Mock services
let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>; let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>; let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>;
let mockSubscriptionService: TypeMoq.IMock<IAzureResourceSubscriptionService>; let mockSubscriptionServiceADAL: TypeMoq.IMock<IAzureResourceSubscriptionService>;
let mockSubscriptionServiceMSAL: TypeMoq.IMock<IAzureResourceSubscriptionService>;
let mockSubscriptionFilterService: TypeMoq.IMock<IAzureResourceSubscriptionFilterService>; let mockSubscriptionFilterService: TypeMoq.IMock<IAzureResourceSubscriptionFilterService>;
let mockAppContext: AppContext; let mockAppContextADAL: AppContext;
let mockAppContextMSAL: AppContext;
let mockTreeChangeHandler: TypeMoq.IMock<IAzureResourceTreeChangeHandler>; let mockTreeChangeHandler: TypeMoq.IMock<IAzureResourceTreeChangeHandler>;
// Mock test data // Mock test data
@@ -95,18 +97,25 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
beforeEach(() => { beforeEach(() => {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>(); mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>(); mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>(); mockSubscriptionServiceADAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, undefined)).returns(() => Promise.resolve(mockSubscriptions)); mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionServiceMSAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
mockSubscriptionServiceMSAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>(); mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>(); mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockSubscriptionCache = []; mockSubscriptionCache = [];
mockAppContext = new AppContext(mockExtensionContext.object); mockAppContextADAL = new AppContext(mockExtensionContext.object);
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object); mockAppContextADAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object); mockAppContextADAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object);
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object); mockAppContextADAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
mockAppContextMSAL = new AppContext(mockExtensionContext.object);
mockAppContextMSAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContextMSAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceMSAL.object);
mockAppContextMSAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid()); mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache); mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
@@ -120,8 +129,8 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
sinon.restore(); sinon.restore();
}); });
it('Should be correct when created.', async function (): Promise<void> { it('Should be correct when created for ADAL.', async function (): Promise<void> {
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
const accountTreeNodeId = `account_${mockAccount.key.accountId}`; const accountTreeNodeId = `account_${mockAccount.key.accountId}`;
@@ -140,14 +149,34 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
should(nodeInfo.iconType).equal(AzureResourceItemType.account); should(nodeInfo.iconType).equal(AzureResourceItemType.account);
}); });
it('Should be correct when there are subscriptions listed.', async function (): Promise<void> { it('Should be correct when created for MSAL.', async function (): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions)); const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextMSAL, mockTreeChangeHandler.object);
const accountTreeNodeId = `account_${mockAccount.key.accountId}`;
should(accountTreeNode.nodePathValue).equal(accountTreeNodeId);
const treeItem = await accountTreeNode.getTreeItem();
should(treeItem.id).equal(accountTreeNodeId);
should(treeItem.label).equal(mockAccount.displayInfo.displayName);
should(treeItem.contextValue).equal(AzureResourceItemType.account);
should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed);
const nodeInfo = accountTreeNode.getNodeInfo();
should(nodeInfo.label).equal(mockAccount.displayInfo.displayName);
should(nodeInfo.isLeaf).false();
should(nodeInfo.nodeType).equal(AzureResourceItemType.account);
should(nodeInfo.iconType).equal(AzureResourceItemType.account);
});
it('Should be correct when there are subscriptions listed for ADAL.', async function (): Promise<void> {
mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([])); mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken); sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`; const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
const subscriptionNodes = await accountTreeNode.getChildren(); const subscriptionNodes = await accountTreeNode.getChildren();
@@ -161,13 +190,34 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
should(nodeInfo.label).equal(accountTreeNodeLabel); should(nodeInfo.label).equal(accountTreeNodeLabel);
}); });
it('Should only show subscriptions with valid tokens.', async function (): Promise<void> { it('Should be correct when there are subscriptions listed for MSAL.', async function (): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions)); mockSubscriptionServiceMSAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextMSAL, mockTreeChangeHandler.object);
const subscriptionNodes = await accountTreeNode.getChildren();
should(subscriptionNodes).Array();
should(subscriptionNodes.length).equal(mockSubscriptions.length);
const treeItem = await accountTreeNode.getTreeItem();
should(treeItem.label).equal(accountTreeNodeLabel);
const nodeInfo = accountTreeNode.getNodeInfo();
should(nodeInfo.label).equal(accountTreeNodeLabel);
});
it('Should only show subscriptions with valid tokens for ADAL.', async function (): Promise<void> {
mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions)); mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
sinon.stub(azdata.accounts, 'getAccountSecurityToken').onFirstCall().resolves(mockToken); sinon.stub(azdata.accounts, 'getAccountSecurityToken').onFirstCall().resolves(mockToken);
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`; const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
const subscriptionNodes = await accountTreeNode.getChildren(); const subscriptionNodes = await accountTreeNode.getChildren();
@@ -181,13 +231,53 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
should(nodeInfo.label).equal(accountTreeNodeLabel); should(nodeInfo.label).equal(accountTreeNodeLabel);
}); });
it('Should be correct when there are subscriptions filtered.', async function (): Promise<void> { it('Should only show subscriptions with valid tokens for MSAL.', async function (): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions)); mockSubscriptionServiceMSAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
sinon.stub(azdata.accounts, 'getAccountSecurityToken').onFirstCall().resolves(mockToken);
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextMSAL, mockTreeChangeHandler.object);
const subscriptionNodes = await accountTreeNode.getChildren();
should(subscriptionNodes).Array();
should(subscriptionNodes.length).equal(1);
const treeItem = await accountTreeNode.getTreeItem();
should(treeItem.label).equal(accountTreeNodeLabel);
const nodeInfo = accountTreeNode.getNodeInfo();
should(nodeInfo.label).equal(accountTreeNodeLabel);
});
it('Should be correct when there are subscriptions filtered for ADAL.', async function (): Promise<void> {
mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions)); mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken); sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`; const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
const subscriptionNodes = await accountTreeNode.getChildren();
should(subscriptionNodes).Array();
should(subscriptionNodes.length).equal(mockFilteredSubscriptions.length);
const treeItem = await accountTreeNode.getTreeItem();
should(treeItem.label).equal(accountTreeNodeLabel);
const nodeInfo = accountTreeNode.getNodeInfo();
should(nodeInfo.label).equal(accountTreeNodeLabel);
});
it('Should be correct when there are subscriptions filtered for MSAL.', async function (): Promise<void> {
mockSubscriptionServiceMSAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextMSAL, mockTreeChangeHandler.object);
const subscriptionNodes = await accountTreeNode.getChildren(); const subscriptionNodes = await accountTreeNode.getChildren();
@@ -206,17 +296,23 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
beforeEach(() => { beforeEach(() => {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>(); mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>(); mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>(); mockSubscriptionServiceADAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
mockSubscriptionServiceMSAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>(); mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>(); mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockSubscriptionCache = []; mockSubscriptionCache = [];
mockAppContext = new AppContext(mockExtensionContext.object); mockAppContextADAL = new AppContext(mockExtensionContext.object);
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object); mockAppContextADAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object); mockAppContextADAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object);
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object); mockAppContextADAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
mockAppContextMSAL = new AppContext(mockExtensionContext.object);
mockAppContextMSAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContextMSAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceMSAL.object);
mockAppContextMSAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken); sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid()); mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
@@ -231,15 +327,15 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
sinon.restore(); sinon.restore();
}); });
it('Should load subscriptions from scratch and update cache when it is clearing cache.', async function (): Promise<void> { it('Should load subscriptions from scratch and update cache when it is clearing cache for ADAL.', async function (): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions)); mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([])); mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
const children = await accountTreeNode.getChildren(); const children = await accountTreeNode.getChildren();
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once()); mockSubscriptionServiceADAL.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(0)); mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(0));
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once()); mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once());
@@ -265,16 +361,16 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
}); });
it('Should load subscriptions from cache when it is not clearing cache.', async function (): Promise<void> { it('Should load subscriptions from cache when it is not clearing cache.', async function (): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions)); mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([])); mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
await accountTreeNode.getChildren(); await accountTreeNode.getChildren();
const children = await accountTreeNode.getChildren(); const children = await accountTreeNode.getChildren();
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once()); mockSubscriptionServiceADAL.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
@@ -286,9 +382,9 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
}); });
it('Should handle when there is no subscriptions.', async function (): Promise<void> { it('Should handle when there is no subscriptions.', async function (): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve([])); mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve([]));
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
const children = await accountTreeNode.getChildren(); const children = await accountTreeNode.getChildren();
@@ -302,10 +398,10 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
}); });
it('Should honor subscription filtering.', async function (): Promise<void> { it('Should honor subscription filtering.', async function (): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions)); mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions)); mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
const children = await accountTreeNode.getChildren(); const children = await accountTreeNode.getChildren();
@@ -320,16 +416,16 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
}); });
it('Should handle errors.', async function (): Promise<void> { it('Should handle errors.', async function (): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions)); mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
const mockError = 'Test error'; const mockError = 'Test error';
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => { throw new Error(mockError); }); mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => { throw new Error(mockError); });
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
const children = await accountTreeNode.getChildren(); const children = await accountTreeNode.getChildren();
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once()); mockSubscriptionServiceADAL.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once()); mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once());
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never()); mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never());
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
@@ -346,17 +442,17 @@ describe('AzureResourceAccountTreeNode.clearCache', function (): void {
beforeEach(() => { beforeEach(() => {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>(); mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>(); mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>(); mockSubscriptionServiceADAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>(); mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>(); mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockSubscriptionCache = []; mockSubscriptionCache = [];
mockAppContext = new AppContext(mockExtensionContext.object); mockAppContextADAL = new AppContext(mockExtensionContext.object);
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object); mockAppContextADAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object); mockAppContextADAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object);
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object); mockAppContextADAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
sinon.stub(azdata.accounts, 'getAccountSecurityToken').returns(Promise.resolve(mockToken)); sinon.stub(azdata.accounts, 'getAccountSecurityToken').returns(Promise.resolve(mockToken));
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid()); mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
@@ -372,7 +468,7 @@ describe('AzureResourceAccountTreeNode.clearCache', function (): void {
}); });
it('Should clear cache.', async function (): Promise<void> { it('Should clear cache.', async function (): Promise<void> {
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
accountTreeNode.clearCache(); accountTreeNode.clearCache();
should(accountTreeNode.isClearingCache).true(); should(accountTreeNode.isClearingCache).true();
}); });

View File

@@ -26,10 +26,11 @@ let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>; let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>;
// Mock test data // Mock test data
const mockAccount1: AzureAccount = { const mockAccountAdal1: AzureAccount = {
key: { key: {
accountId: 'mock_account_1', accountId: 'mock_account_1',
providerId: 'mock_provider' providerId: 'mock_provider',
authLibrary: 'ADAL'
}, },
displayInfo: { displayInfo: {
displayName: 'mock_account_1@test.com', displayName: 'mock_account_1@test.com',
@@ -40,7 +41,7 @@ const mockAccount1: AzureAccount = {
properties: TypeMoq.Mock.ofType<AzureAccountProperties>().object, properties: TypeMoq.Mock.ofType<AzureAccountProperties>().object,
isStale: false isStale: false
}; };
const mockAccount2: AzureAccount = { const mockAccountAdal2: AzureAccount = {
key: { key: {
accountId: 'mock_account_2', accountId: 'mock_account_2',
providerId: 'mock_provider' providerId: 'mock_provider'
@@ -54,7 +55,39 @@ const mockAccount2: AzureAccount = {
properties: TypeMoq.Mock.ofType<AzureAccountProperties>().object, properties: TypeMoq.Mock.ofType<AzureAccountProperties>().object,
isStale: false isStale: false
}; };
const mockAccounts = [mockAccount1, mockAccount2]; const mockAccountsADAL = [mockAccountAdal1, mockAccountAdal2];
const mockAccountMsal1: AzureAccount = {
key: {
accountId: 'mock_account_1',
providerId: 'mock_provider',
authLibrary: 'MSAL'
},
displayInfo: {
displayName: 'mock_account_1@test.com',
accountType: 'Microsoft',
contextualDisplayName: 'test',
userId: 'test@email.com'
},
properties: TypeMoq.Mock.ofType<AzureAccountProperties>().object,
isStale: false
};
const mockAccountMsal2: AzureAccount = {
key: {
accountId: 'mock_account_2',
providerId: 'mock_provider',
authLibrary: 'MSAL'
},
displayInfo: {
displayName: 'mock_account_2@test.com',
accountType: 'Microsoft',
contextualDisplayName: 'test',
userId: 'test@email.com'
},
properties: TypeMoq.Mock.ofType<AzureAccountProperties>().object,
isStale: false
};
const mockAccountsMSAL = [mockAccountMsal1, mockAccountMsal2];
describe('AzureResourceTreeProvider.getChildren', function (): void { describe('AzureResourceTreeProvider.getChildren', function (): void {
beforeEach(() => { beforeEach(() => {
@@ -72,31 +105,65 @@ describe('AzureResourceTreeProvider.getChildren', function (): void {
sinon.restore(); sinon.restore();
}); });
it('Should load accounts.', async function (): Promise<void> { it('Should load accounts for ADAL', async function (): Promise<void> {
const getAllAccountsStub = sinon.stub(azdata.accounts, 'getAllAccounts').returns(Promise.resolve(mockAccounts)); const getAllAccountsStub = sinon.stub(azdata.accounts, 'getAllAccounts').returns(Promise.resolve(mockAccountsADAL));
const treeProvider = new AzureResourceTreeProvider(mockAppContext); const treeProvider = new AzureResourceTreeProvider(mockAppContext, 'ADAL');
await treeProvider.getChildren(undefined); // Load account promise await treeProvider.getChildren(undefined); // Load account promise
const children = await treeProvider.getChildren(undefined); // Actual accounts const children = await treeProvider.getChildren(undefined); // Actual accounts
should(getAllAccountsStub.calledOnce).be.true('getAllAccounts should have been called exactly once'); should(getAllAccountsStub.calledOnce).be.true('getAllAccounts should have been called exactly once');
should(children).Array(); should(children).Array();
should(children.length).equal(mockAccounts.length); should(children.length).equal(mockAccountsADAL.length);
for (let ix = 0; ix < mockAccounts.length; ix++) { for (let ix = 0; ix < mockAccountsADAL.length; ix++) {
const child = children[ix]; const child = children[ix];
const account = mockAccounts[ix]; const account = mockAccountsADAL[ix];
should(child).instanceof(AzureResourceAccountTreeNode); should(child).instanceof(AzureResourceAccountTreeNode);
should(child.nodePathValue).equal(`account_${account.key.accountId}`); should(child.nodePathValue).equal(`account_${account.key.accountId}`);
} }
}); });
it('Should handle when there is no accounts.', async function (): Promise<void> { it('Should load accounts for MSAL', async function (): Promise<void> {
const getAllAccountsStub = sinon.stub(azdata.accounts, 'getAllAccounts').returns(Promise.resolve(mockAccountsMSAL));
const treeProvider = new AzureResourceTreeProvider(mockAppContext, 'MSAL');
await treeProvider.getChildren(undefined); // Load account promise
const children = await treeProvider.getChildren(undefined); // Actual accounts
should(getAllAccountsStub.calledOnce).be.true('getAllAccounts should have been called exactly once');
should(children).Array();
should(children.length).equal(mockAccountsMSAL.length);
for (let ix = 0; ix < mockAccountsMSAL.length; ix++) {
const child = children[ix];
const account = mockAccountsMSAL[ix];
should(child).instanceof(AzureResourceAccountTreeNode);
should(child.nodePathValue).equal(`account_${account.key.accountId}`);
}
});
it('Should handle when there is no accounts for ADAL', async function (): Promise<void> {
sinon.stub(azdata.accounts, 'getAllAccounts').returns(Promise.resolve([])); sinon.stub(azdata.accounts, 'getAllAccounts').returns(Promise.resolve([]));
const treeProvider = new AzureResourceTreeProvider(mockAppContext); const treeProvider = new AzureResourceTreeProvider(mockAppContext, 'ADAL');
treeProvider.isSystemInitialized = true;
const children = await treeProvider.getChildren(undefined);
should(children).Array();
should(children.length).equal(1);
should(children[0]).instanceof(AzureResourceAccountNotSignedInTreeNode);
});
it('Should handle when there is no accounts for MSAL', async function (): Promise<void> {
sinon.stub(azdata.accounts, 'getAllAccounts').returns(Promise.resolve([]));
const treeProvider = new AzureResourceTreeProvider(mockAppContext, 'MSAL');
treeProvider.isSystemInitialized = true; treeProvider.isSystemInitialized = true;
const children = await treeProvider.getChildren(undefined); const children = await treeProvider.getChildren(undefined);

View File

@@ -127,6 +127,29 @@
uuid "^3.3.2" uuid "^3.3.2"
xml2js "^0.4.19" xml2js "^0.4.19"
"@azure/msal-common@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-7.6.0.tgz#b52e97ef540275f72611cff57937dfa0b34cdcca"
integrity sha512-XqfbglUTVLdkHQ8F9UQJtKseRr3sSnr9ysboxtoswvaMVaEfvyLtMoHv9XdKUfOc0qKGzNgRFd9yRjIWVepl6Q==
"@azure/msal-node-extensions@^1.0.0-alpha.25":
version "1.0.0-alpha.25"
resolved "https://registry.yarnpkg.com/@azure/msal-node-extensions/-/msal-node-extensions-1.0.0-alpha.25.tgz#3259e1fd32b6107f61a402dce22caab53e81527c"
integrity sha512-7pOUdE2OZO2omA1DBAJhVGHJo8laqw+x5JgV/XLzyogapduAzps3lbM/G3VV+VuEb0KG1QHkpaOF/6eftPssKw==
dependencies:
"@azure/msal-common" "^7.6.0"
keytar "^7.8.0"
node-addon-api "5.0.0"
"@azure/msal-node@^1.9.0":
version "1.14.2"
resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-1.14.2.tgz#8f236a19efa506133d6c715047393146af182e3a"
integrity sha512-t3whVhhLdZVVeDEtUPD2Wqfa8BDi3EDMnpWp8dbuRW0GhUpikBfs4AQU0Fe6P9zS87n9LpmUTLrIcPEEuzkvfA==
dependencies:
"@azure/msal-common" "^7.6.0"
jsonwebtoken "^8.5.1"
uuid "^8.3.0"
"@azure/storage-blob@^12.6.0": "@azure/storage-blob@^12.6.0":
version "12.6.0" version "12.6.0"
resolved "https://registry.yarnpkg.com/@azure/storage-blob/-/storage-blob-12.6.0.tgz#9905d80e5f908a573cc65e1cb8302abc32818844" resolved "https://registry.yarnpkg.com/@azure/storage-blob/-/storage-blob-12.6.0.tgz#9905d80e5f908a573cc65e1cb8302abc32818844"
@@ -549,11 +572,25 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
binary-extensions@^2.0.0: binary-extensions@^2.0.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
bl@^4.0.3:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
dependencies:
buffer "^5.5.0"
inherits "^2.0.4"
readable-stream "^3.4.0"
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.11" version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -574,6 +611,19 @@ browser-stdout@1.3.1:
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
buffer@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.1.13"
call-bind@^1.0.0, call-bind@^1.0.2: call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@@ -621,6 +671,11 @@ chokidar@3.3.0:
optionalDependencies: optionalDependencies:
fsevents "~2.1.1" fsevents "~2.1.1"
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: circular-json@^0.3.1:
version "0.3.3" version "0.3.3"
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
@@ -711,6 +766,18 @@ decamelize@^1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
decompress-response@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
dependencies:
mimic-response "^3.1.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==
default-require-extensions@^3.0.0: default-require-extensions@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.0.tgz#e03f93aac9b2b6443fc52e5e4a37b3ad9ad8df96" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.0.tgz#e03f93aac9b2b6443fc52e5e4a37b3ad9ad8df96"
@@ -731,6 +798,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
detect-libc@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
diff@3.5.0: diff@3.5.0:
version "3.5.0" version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
@@ -741,11 +813,25 @@ diff@^4.0.2:
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
ecdsa-sig-formatter@1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"
emoji-regex@^7.0.1: emoji-regex@^7.0.1:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
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.19.0, es-abstract@^1.19.1, es-abstract@^1.19.5: es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.5:
version "1.20.1" version "1.20.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814"
@@ -816,6 +902,11 @@ events@^3.0.0:
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
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==
fill-range@^7.0.1: fill-range@^7.0.1:
version "7.0.1" version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -869,6 +960,11 @@ form-data@^4.0.0:
combined-stream "^1.0.8" combined-stream "^1.0.8"
mime-types "^2.1.12" 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: fs.realpath@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -926,6 +1022,11 @@ get-symbol-description@^1.0.0:
call-bind "^1.0.2" call-bind "^1.0.2"
get-intrinsic "^1.1.1" get-intrinsic "^1.1.1"
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 sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==
glob-parent@~5.1.0: glob-parent@~5.1.0:
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@@ -1034,6 +1135,11 @@ https-proxy-agent@^2.2.4:
agent-base "^4.3.0" agent-base "^4.3.0"
debug "^3.1.0" debug "^3.1.0"
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
inflight@^1.0.4: inflight@^1.0.4:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -1042,11 +1148,16 @@ inflight@^1.0.4:
once "^1.3.0" once "^1.3.0"
wrappy "1" wrappy "1"
inherits@2: inherits@2, inherits@^2.0.3, inherits@^2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@~1.3.0:
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
internal-slot@^1.0.3: internal-slot@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
@@ -1268,11 +1379,52 @@ json5@^2.1.2:
dependencies: dependencies:
minimist "^1.2.5" minimist "^1.2.5"
jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
dependencies:
jws "^3.2.2"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
lodash.isnumber "^3.0.3"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.once "^4.0.0"
ms "^2.1.1"
semver "^5.6.0"
just-extend@^4.0.2: just-extend@^4.0.2:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4"
integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA== integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies:
jwa "^1.4.1"
safe-buffer "^5.0.1"
keytar@^7.8.0:
version "7.9.0"
resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.9.0.tgz#4c6225708f51b50cbf77c5aae81721964c2918cb"
integrity sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==
dependencies:
node-addon-api "^4.3.0"
prebuild-install "^7.0.1"
locate-path@^3.0.0: locate-path@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
@@ -1286,6 +1438,41 @@ lodash.get@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==
lodash.isstring@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
lodash@^4.16.4, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.4: lodash@^4.16.4, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.4:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
@@ -1298,6 +1485,13 @@ log-symbols@3.0.0:
dependencies: dependencies:
chalk "^2.4.2" chalk "^2.4.2"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
make-dir@^2.1.0: make-dir@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
@@ -1334,6 +1528,11 @@ mime-types@^2.1.12:
dependencies: dependencies:
mime-db "1.43.0" mime-db "1.43.0"
mimic-response@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@3.0.4, minimatch@^3.0.4: minimatch@3.0.4, minimatch@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@@ -1341,11 +1540,21 @@ minimatch@3.0.4, minimatch@^3.0.4:
dependencies: dependencies:
brace-expansion "^1.1.7" brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.3:
version "1.2.7"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
minimist@^1.2.5: minimist@^1.2.5:
version "1.2.6" version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
mkdirp@0.5.5, mkdirp@~0.5.1: mkdirp@0.5.5, mkdirp@~0.5.1:
version "0.5.5" version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
@@ -1417,6 +1626,18 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
msal@^1.4.16:
version "1.4.17"
resolved "https://registry.yarnpkg.com/msal/-/msal-1.4.17.tgz#b78171c0471ede506eeaabc86343f8f4e2d01634"
integrity sha512-RjHwP2cCIWQ9iUIk1SziUMb9+jj5mC4OqG2w16E5yig8jySi/TwiFvKlwcjNrPsndph0HtgCtbENnk5julf3yQ==
dependencies:
tslib "^1.9.3"
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==
nise@^4.0.1: nise@^4.0.1:
version "4.0.4" version "4.0.4"
resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.4.tgz#d73dea3e5731e6561992b8f570be9e363c4512dd" resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.4.tgz#d73dea3e5731e6561992b8f570be9e363c4512dd"
@@ -1428,6 +1649,23 @@ nise@^4.0.1:
just-extend "^4.0.2" just-extend "^4.0.2"
path-to-regexp "^1.7.0" path-to-regexp "^1.7.0"
node-abi@^3.3.0:
version "3.28.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.28.0.tgz#b0df8b317e1c4f2f323756c5fc8ffccc5bca4718"
integrity sha512-fRlDb4I0eLcQeUvGq7IY3xHrSb0c9ummdvDSYWfT9+LKP+3jCKw/tKoqaM7r1BAoiAC6GtwyjaGnOz6B3OtF+A==
dependencies:
semver "^7.3.5"
node-addon-api@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.0.0.tgz#7d7e6f9ef89043befdb20c1989c905ebde18c501"
integrity sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==
node-addon-api@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
node-environment-flags@1.0.6: node-environment-flags@1.0.6:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088" resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088"
@@ -1487,10 +1725,10 @@ object.getownpropertydescriptors@^2.0.3:
define-properties "^1.1.3" define-properties "^1.1.3"
es-abstract "^1.19.1" es-abstract "^1.19.1"
once@^1.3.0: once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies: dependencies:
wrappy "1" wrappy "1"
@@ -1550,6 +1788,24 @@ postinstall-build@^5.0.1:
resolved "https://registry.yarnpkg.com/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7" resolved "https://registry.yarnpkg.com/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7"
integrity sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg== integrity sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg==
prebuild-install@^7.0.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45"
integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==
dependencies:
detect-libc "^2.0.0"
expand-template "^2.0.3"
github-from-package "0.0.0"
minimist "^1.2.3"
mkdirp-classic "^0.5.3"
napi-build-utils "^1.0.1"
node-abi "^3.3.0"
pump "^3.0.0"
rc "^1.2.7"
simple-get "^4.0.0"
tar-fs "^2.0.0"
tunnel-agent "^0.6.0"
process@^0.11.10: process@^0.11.10:
version "0.11.10" version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
@@ -1560,6 +1816,14 @@ psl@^1.1.28, psl@^1.1.33:
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
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@^2.1.1: punycode@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
@@ -1570,6 +1834,25 @@ qs@^6.9.1:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e"
integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==
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@^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"
readdirp@~3.2.0: readdirp@~3.2.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839"
@@ -1610,6 +1893,11 @@ rimraf@^2.6.3:
dependencies: dependencies:
glob "^7.1.3" glob "^7.1.3"
safe-buffer@^5.0.1, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safe-buffer@~5.1.1: safe-buffer@~5.1.1:
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@@ -1630,6 +1918,13 @@ semver@^6.0.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.3.5:
version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
dependencies:
lru-cache "^6.0.0"
set-blocking@^2.0.0: set-blocking@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@@ -1688,6 +1983,20 @@ side-channel@^1.0.4:
get-intrinsic "^1.0.2" get-intrinsic "^1.0.2"
object-inspect "^1.9.0" object-inspect "^1.9.0"
simple-concat@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
simple-get@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
dependencies:
decompress-response "^6.0.0"
once "^1.3.1"
simple-concat "^1.0.0"
sinon@^9.0.2: sinon@^9.0.2:
version "9.0.2" version "9.0.2"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.0.2.tgz#b9017e24633f4b1c98dfb6e784a5f0509f5fd85d" resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.0.2.tgz#b9017e24633f4b1c98dfb6e784a5f0509f5fd85d"
@@ -1751,6 +2060,13 @@ string.prototype.trimstart@^1.0.5:
define-properties "^1.1.4" define-properties "^1.1.4"
es-abstract "^1.19.5" es-abstract "^1.19.5"
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"
strip-ansi@^4.0.0: strip-ansi@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
@@ -1770,10 +2086,10 @@ strip-bom@^4.0.0:
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
strip-json-comments@2.0.1: strip-json-comments@2.0.1, strip-json-comments@~2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
supports-color@6.0.0: supports-color@6.0.0:
version "6.0.0" version "6.0.0"
@@ -1796,6 +2112,27 @@ supports-color@^7.1.0:
dependencies: dependencies:
has-flag "^4.0.0" has-flag "^4.0.0"
tar-fs@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
dependencies:
chownr "^1.1.1"
mkdirp-classic "^0.5.2"
pump "^3.0.0"
tar-stream "^2.1.4"
tar-stream@^2.1.4:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
dependencies:
bl "^4.0.3"
end-of-stream "^1.4.1"
fs-constants "^1.0.0"
inherits "^2.0.3"
readable-stream "^3.1.1"
to-fast-properties@^2.0.0: to-fast-properties@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
@@ -1831,7 +2168,7 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
tslib@^1.10.0: tslib@^1.10.0, tslib@^1.9.3:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
@@ -1846,6 +2183,13 @@ tslib@^2.2.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
dependencies:
safe-buffer "^5.0.1"
tunnel@0.0.6, tunnel@^0.0.6: tunnel@0.0.6, tunnel@^0.0.6:
version "0.0.6" version "0.0.6"
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
@@ -1880,6 +2224,11 @@ universalify@^0.1.2:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
uuid@^3.3.2: uuid@^3.3.2:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
@@ -1980,6 +2329,11 @@ y18n@^4.0.0:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yargs-parser@13.1.2, yargs-parser@^13.1.2: yargs-parser@13.1.2, yargs-parser@^13.1.2:
version "13.1.2" version "13.1.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"

View File

@@ -164,8 +164,8 @@
"ansi-colors": "^3.2.3", "ansi-colors": "^3.2.3",
"asar": "^3.0.3", "asar": "^3.0.3",
"chromium-pickle-js": "^0.2.0", "chromium-pickle-js": "^0.2.0",
"cookie": "^0.4.0",
"concurrently": "^5.2.0", "concurrently": "^5.2.0",
"cookie": "^0.4.0",
"copy-webpack-plugin": "^6.0.3", "copy-webpack-plugin": "^6.0.3",
"cson-parser": "^1.3.3", "cson-parser": "^1.3.3",
"css-loader": "^3.2.0", "css-loader": "^3.2.0",

View File

@@ -502,6 +502,13 @@ declare module 'azdata' {
type?: ExtensionNodeType; type?: ExtensionNodeType;
} }
export interface AccountKey {
/**
* Auth Library used to add the account
*/
authLibrary?: string;
}
export namespace workspace { export namespace workspace {
/** /**
* Creates and enters a workspace at the specified location * Creates and enters a workspace at the specified location

View File

@@ -50,6 +50,7 @@ import { Tenant, TenantListDelegate, TenantListRenderer } from 'sql/workbench/se
import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces'; import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces';
export const VIEWLET_ID = 'workbench.view.accountpanel'; export const VIEWLET_ID = 'workbench.view.accountpanel';
export type AuthLibrary = 'ADAL' | 'MSAL';
export class AccountPaneContainer extends ViewPaneContainer { export class AccountPaneContainer extends ViewPaneContainer {
@@ -376,9 +377,14 @@ export class AccountDialog extends Modal {
this._splitView!.layout(DOM.getContentHeight(this._container!)); this._splitView!.layout(DOM.getContentHeight(this._container!));
// Set the initial items of the list // Set the initial items of the list
providerView.updateAccounts(newProvider.initialAccounts); const authLibrary: AuthLibrary = this._configurationService.getValue('azure.authenticationLibrary');
let updatedAccounts: azdata.Account[];
if (authLibrary) {
updatedAccounts = filterAccounts(newProvider.initialAccounts, authLibrary);
}
providerView.updateAccounts(updatedAccounts);
if (newProvider.initialAccounts.length > 0 && this._splitViewContainer!.hidden) { if (updatedAccounts.length > 0 && this._splitViewContainer!.hidden) {
this.showSplitView(); this.showSplitView();
} }
@@ -413,7 +419,12 @@ export class AccountDialog extends Modal {
if (!providerMapping || !providerMapping.view) { if (!providerMapping || !providerMapping.view) {
return; return;
} }
providerMapping.view.updateAccounts(args.accountList); const authLibrary: AuthLibrary = this._configurationService.getValue('azure.authenticationLibrary');
let updatedAccounts: azdata.Account[];
if (authLibrary) {
updatedAccounts = filterAccounts(args.accountList, authLibrary);
}
providerMapping.view.updateAccounts(updatedAccounts);
if (args.accountList.length > 0 && this._splitViewContainer!.hidden) { if (args.accountList.length > 0 && this._splitViewContainer!.hidden) {
this.showSplitView(); this.showSplitView();
@@ -480,3 +491,27 @@ export class AccountDialog extends Modal {
v.addAccountAction.run(); v.addAccountAction.run();
} }
} }
// Filter accounts based on currently selected Auth Library:
// if the account key is present, filter based on current auth library
// if there is no account key (pre-MSAL account), then it is an ADAL account and
// should be displayed as long as ADAL is the currently selected auth library
export function filterAccounts(accounts: azdata.Account[], authLibrary: AuthLibrary): azdata.Account[] {
let filteredAccounts = accounts.filter(account => {
if (account.key.authLibrary) {
if (account.key.authLibrary === authLibrary) {
return true;
} else {
return false;
}
} else {
if (authLibrary === 'ADAL') {
return true;
} else {
return false;
}
}
});
return filteredAccounts;
}

View File

@@ -24,6 +24,9 @@ import { values } from 'vs/base/common/collections';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService, Severity, INotification } from 'vs/platform/notification/common/notification'; import { INotificationService, Severity, INotification } from 'vs/platform/notification/common/notification';
import { Action } from 'vs/base/common/actions'; import { Action } from 'vs/base/common/actions';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { AuthLibrary, filterAccounts } from 'sql/workbench/services/accountManagement/browser/accountDialog';
export class AccountManagementService implements IAccountManagementService { export class AccountManagementService implements IAccountManagementService {
// CONSTANTS /////////////////////////////////////////////////////////// // CONSTANTS ///////////////////////////////////////////////////////////
@@ -36,6 +39,8 @@ export class AccountManagementService implements IAccountManagementService {
private _accountDialogController?: AccountDialogController; private _accountDialogController?: AccountDialogController;
private _autoOAuthDialogController?: AutoOAuthDialogController; private _autoOAuthDialogController?: AutoOAuthDialogController;
private _mementoContext?: Memento; private _mementoContext?: Memento;
protected readonly disposables = new DisposableStore();
private readonly configurationService: IConfigurationService;
// EVENT EMITTERS ////////////////////////////////////////////////////// // EVENT EMITTERS //////////////////////////////////////////////////////
private _addAccountProviderEmitter: Emitter<AccountProviderAddedEventParams>; private _addAccountProviderEmitter: Emitter<AccountProviderAddedEventParams>;
@@ -54,7 +59,8 @@ export class AccountManagementService implements IAccountManagementService {
@IClipboardService private _clipboardService: IClipboardService, @IClipboardService private _clipboardService: IClipboardService,
@IOpenerService private _openerService: IOpenerService, @IOpenerService private _openerService: IOpenerService,
@ILogService private readonly _logService: ILogService, @ILogService private readonly _logService: ILogService,
@INotificationService private readonly _notificationService: INotificationService @INotificationService private readonly _notificationService: INotificationService,
@IConfigurationService configurationService: IConfigurationService
) { ) {
this._mementoContext = new Memento(AccountManagementService.ACCOUNT_MEMENTO, this._storageService); this._mementoContext = new Memento(AccountManagementService.ACCOUNT_MEMENTO, this._storageService);
const mementoObj = this._mementoContext.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); const mementoObj = this._mementoContext.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE);
@@ -64,8 +70,10 @@ export class AccountManagementService implements IAccountManagementService {
this._addAccountProviderEmitter = new Emitter<AccountProviderAddedEventParams>(); this._addAccountProviderEmitter = new Emitter<AccountProviderAddedEventParams>();
this._removeAccountProviderEmitter = new Emitter<azdata.AccountProviderMetadata>(); this._removeAccountProviderEmitter = new Emitter<azdata.AccountProviderMetadata>();
this._updateAccountListEmitter = new Emitter<UpdateAccountListEventParams>(); this._updateAccountListEmitter = new Emitter<UpdateAccountListEventParams>();
this.configurationService = configurationService;
_storageService.onWillSaveState(() => this.shutdown()); _storageService.onWillSaveState(() => this.shutdown());
this.registerListeners();
} }
private get autoOAuthDialogController(): AutoOAuthDialogController { private get autoOAuthDialogController(): AutoOAuthDialogController {
@@ -136,6 +144,10 @@ export class AccountManagementService implements IAccountManagementService {
} }
let result = await this._accountStore.addOrUpdate(account); let result = await this._accountStore.addOrUpdate(account);
if (!result) {
this._logService.error('adding account failed');
throw Error('Adding account failed, check Azure Accounts log for more info.')
}
if (result.accountAdded) { if (result.accountAdded) {
// Add the account to the list // Add the account to the list
provider.accounts.push(result.changedAccount); provider.accounts.push(result.changedAccount);
@@ -458,10 +470,15 @@ export class AccountManagementService implements IAccountManagementService {
}); });
} }
const authLibrary: AuthLibrary = this.configurationService.getValue('azure.authenticationLibrary');
let updatedAccounts: azdata.Account[]
if (authLibrary) {
updatedAccounts = filterAccounts(provider.accounts, authLibrary);
}
// Step 2) Fire the event // Step 2) Fire the event
let eventArg: UpdateAccountListEventParams = { let eventArg: UpdateAccountListEventParams = {
providerId: provider.metadata.id, providerId: provider.metadata.id,
accountList: provider.accounts accountList: updatedAccounts ?? provider.accounts
}; };
this._updateAccountListEmitter.fire(eventArg); this._updateAccountListEmitter.fire(eventArg);
} }
@@ -475,6 +492,39 @@ export class AccountManagementService implements IAccountManagementService {
provider.accounts.splice(indexToRemove, 1, modifiedAccount); provider.accounts.splice(indexToRemove, 1, modifiedAccount);
} }
} }
private registerListeners(): void {
this.disposables.add(this.configurationService.onDidChangeConfiguration(async e => {
if (e.affectsConfiguration('azure.authenticationLibrary')) {
const authLibrary: AuthLibrary = this.configurationService.getValue('azure.authenticationLibrary');
if (authLibrary) {
let accounts = await this._accountStore.getAllAccounts();
if (accounts) {
let updatedAccounts = filterAccounts(accounts, authLibrary);
let eventArg: UpdateAccountListEventParams;
if (updatedAccounts.length > 0) {
updatedAccounts.forEach(account => {
if (account.key.authLibrary === 'MSAL') {
account.isStale = false;
}
});
eventArg = {
providerId: updatedAccounts[0].key.providerId,
accountList: updatedAccounts
};
} else { // default to public cloud if no accounts
eventArg = {
providerId: 'azure_publicCloud',
accountList: updatedAccounts
};
}
this._updateAccountListEmitter.fire(eventArg);
}
}
}
}));
}
} }
/** /**

View File

@@ -18,6 +18,7 @@ import { EventVerifierSingle } from 'sql/base/test/common/event';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { AccountDialog } from 'sql/workbench/services/accountManagement/browser/accountDialog'; import { AccountDialog } from 'sql/workbench/services/accountManagement/browser/accountDialog';
import { Emitter } from 'vs/base/common/event'; import { Emitter } from 'vs/base/common/event';
import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService';
// SUITE CONSTANTS ///////////////////////////////////////////////////////// // SUITE CONSTANTS /////////////////////////////////////////////////////////
const hasAccountProvider: azdata.AccountProviderMetadata = { const hasAccountProvider: azdata.AccountProviderMetadata = {
@@ -530,9 +531,10 @@ function getTestState(): AccountManagementState {
.returns(() => mockAccountStore.object); .returns(() => mockAccountStore.object);
const testNotificationService = new TestNotificationService(); const testNotificationService = new TestNotificationService();
const testConfigurationService = new TestConfigurationService();
// Create the account management service // Create the account management service
let ams = new AccountManagementService(mockInstantiationService.object, new TestStorageService(), undefined!, undefined!, undefined!, testNotificationService); let ams = new AccountManagementService(mockInstantiationService.object, new TestStorageService(), undefined!, undefined!, undefined!, testNotificationService, testConfigurationService);
// Wire up event handlers // Wire up event handlers
let evUpdate = new EventVerifierSingle<UpdateAccountListEventParams>(); let evUpdate = new EventVerifierSingle<UpdateAccountListEventParams>();

View File

@@ -26,6 +26,7 @@ import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { ConnectionWidget } from 'sql/workbench/services/connection/browser/connectionWidget'; import { ConnectionWidget } from 'sql/workbench/services/connection/browser/connectionWidget';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService'; import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
/** /**
* Connection Widget clas for CMS Connections * Connection Widget clas for CMS Connections
@@ -47,8 +48,9 @@ export class CmsConnectionWidget extends ConnectionWidget {
@IAccountManagementService _accountManagementService: IAccountManagementService, @IAccountManagementService _accountManagementService: IAccountManagementService,
@ILogService _logService: ILogService, @ILogService _logService: ILogService,
@IErrorMessageService _errorMessageService: IErrorMessageService, @IErrorMessageService _errorMessageService: IErrorMessageService,
@IConfigurationService configurationService: IConfigurationService
) { ) {
super(options, callbacks, providerName, _themeService, _contextViewService, _connectionManagementService, _accountManagementService, _logService, _errorMessageService); super(options, callbacks, providerName, _themeService, _contextViewService, _connectionManagementService, _accountManagementService, _logService, _errorMessageService, configurationService);
let authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType]; let authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
if (authTypeOption) { if (authTypeOption) {
let authTypeDefault = this.getAuthTypeDefault(authTypeOption, OS); let authTypeDefault = this.getAuthTypeDefault(authTypeOption, OS);

View File

@@ -27,7 +27,6 @@ import { AzureResource, ConnectionOptionSpecialType } from 'sql/workbench/api/co
import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces'; import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as nls from 'vs/nls'; import * as nls from 'vs/nls';
import * as errors from 'vs/base/common/errors'; import * as errors from 'vs/base/common/errors';
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle';

View File

@@ -36,6 +36,8 @@ import Severity from 'vs/base/common/severity';
import { ConnectionStringOptions } from 'sql/platform/capabilities/common/capabilitiesService'; import { ConnectionStringOptions } from 'sql/platform/capabilities/common/capabilitiesService';
import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { isFalsyOrWhitespace } from 'vs/base/common/strings';
import { AuthenticationType } from 'sql/platform/connection/common/constants'; import { AuthenticationType } from 'sql/platform/connection/common/constants';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { AuthLibrary, filterAccounts } from 'sql/workbench/services/accountManagement/browser/accountDialog';
const ConnectionStringText = localize('connectionWidget.connectionString', "Connection string"); const ConnectionStringText = localize('connectionWidget.connectionString', "Connection string");
@@ -107,6 +109,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
color: undefined, color: undefined,
description: undefined, description: undefined,
}; };
private readonly configurationService: IConfigurationService;
constructor(options: azdata.ConnectionOption[], constructor(options: azdata.ConnectionOption[],
callbacks: IConnectionComponentCallbacks, callbacks: IConnectionComponentCallbacks,
providerName: string, providerName: string,
@@ -115,7 +118,8 @@ export class ConnectionWidget extends lifecycle.Disposable {
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService, @IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IAccountManagementService private _accountManagementService: IAccountManagementService, @IAccountManagementService private _accountManagementService: IAccountManagementService,
@ILogService protected _logService: ILogService, @ILogService protected _logService: ILogService,
@IErrorMessageService private _errorMessageService: IErrorMessageService @IErrorMessageService private _errorMessageService: IErrorMessageService,
@IConfigurationService configurationService: IConfigurationService
) { ) {
super(); super();
this._callbacks = callbacks; this._callbacks = callbacks;
@@ -135,6 +139,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
} }
this._providerName = providerName; this._providerName = providerName;
this._connectionStringOptions = this._connectionManagementService.getProviderProperties(this._providerName).connectionStringOptions; this._connectionStringOptions = this._connectionManagementService.getProviderProperties(this._providerName).connectionStringOptions;
this.configurationService = configurationService;
} }
protected getAuthTypeDefault(option: azdata.ConnectionOption, os: OperatingSystem): string { protected getAuthTypeDefault(option: azdata.ConnectionOption, os: OperatingSystem): string {
@@ -591,7 +596,12 @@ export class ConnectionWidget extends lifecycle.Disposable {
private async fillInAzureAccountOptions(): Promise<void> { private async fillInAzureAccountOptions(): Promise<void> {
let oldSelection = this._azureAccountDropdown.value; let oldSelection = this._azureAccountDropdown.value;
const accounts = await this._accountManagementService.getAccounts(); const accounts = await this._accountManagementService.getAccounts();
this._azureAccountList = accounts.filter(a => a.key.providerId.startsWith('azure')); const updatedAccounts = accounts.filter(a => a.key.providerId.startsWith('azure'));
const authLibrary: AuthLibrary = this.configurationService.getValue('azure.authenticationLibrary');
if (authLibrary) {
this._azureAccountList = filterAccounts(updatedAccounts, authLibrary);
}
let accountDropdownOptions: SelectOptionItemSQL[] = this._azureAccountList.map(account => { let accountDropdownOptions: SelectOptionItemSQL[] = this._azureAccountList.map(account => {
return { return {
text: account.displayInfo.displayName, text: account.displayInfo.displayName,