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',
'@azure/arm-subscriptions': 'commonjs @azure/arm-subscriptions',
'@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

View File

@@ -124,6 +124,19 @@
"Verbose",
"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-subscriptions": "^3.0.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",
"msal": "^1.4.16",
"node-fetch": "^2.6.7",
"qs": "^6.9.1",
"universalify": "^0.1.2",

View File

@@ -31,6 +31,7 @@
"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.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 azdata from 'azdata';
import * as nls from 'vscode-nls';
import {
@@ -15,51 +14,55 @@ import {
Resource,
Tenant
} from 'azurecore';
import { Deferred } from '../interfaces';
import * as url from 'url';
import * as Constants from '../../constants';
import { SimpleTokenCache } from '../simpleTokenCache';
import { MemoryDatabase } from '../utils/memoryDatabase';
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { Logger } from '../../utils/Logger';
import * as qs from 'qs';
import { AzureAuthError } from './azureAuthError';
import { AccountInfo, AuthenticationResult, InteractionRequiredAuthError, PublicClientApplication } from '@azure/msal-node';
const localize = nls.loadMessageBundle();
export abstract class AzureAuth implements vscode.Disposable {
public static ACCOUNT_VERSION = '2.0';
protected readonly memdb = new MemoryDatabase<string>();
protected readonly WorkSchoolAccountType: string = 'work_school';
protected readonly MicrosoftAccountType: string = 'microsoft';
protected readonly loginEndpointUrl: string;
public readonly commonTenant: Tenant;
public readonly organizationTenant: Tenant;
protected readonly redirectUri: string;
protected readonly scopes: string[];
protected readonly scopesString: string;
protected readonly clientId: string;
protected readonly resources: Resource[];
private _authLibrary: string | undefined;
constructor(
protected readonly metadata: AzureAccountProviderMetadata,
protected readonly tokenCache: SimpleTokenCache,
protected readonly context: vscode.ExtensionContext,
protected clientApplication: PublicClientApplication,
protected readonly uriEventEmitter: vscode.EventEmitter<vscode.Uri>,
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.commonTenant = {
id: 'common',
displayName: 'common',
};
this.organizationTenant = {
id: 'organizations',
displayName: 'organizations',
};
this.redirectUri = this.metadata.settings.redirectUri;
this.clientId = this.metadata.settings.clientId;
this.resources = [
this.metadata.settings.armResource,
this.metadata.settings.graphResource,
@@ -100,19 +103,39 @@ export abstract class AzureAuth implements vscode.Disposable {
if (!this.metadata.settings.microsoftResource) {
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);
loginComplete = result.authComplete;
if (!result?.response) {
Logger.error('Authentication failed');
return {
canceled: false
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;
if (!result?.response) {
Logger.error('Authentication failed - no response');
return {
canceled: false
};
}
const account = await this.hydrateAccount(result.response.accessToken, result.response.tokenClaims);
loginComplete?.resolve();
return account;
}
const account = await this.hydrateAccount(result.response.accessToken, result.response.tokenClaims);
loginComplete?.resolve();
return account;
} catch (ex) {
Logger.error('Login failed');
Logger.error(`Login failed: ${ex}`);
if (ex instanceof AzureAuthError) {
if (loginComplete) {
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.
if (account.key.accountVersion !== AzureAuth.ACCOUNT_VERSION) {
if (account.key.accountVersion !== Constants.AccountVersion) {
account.delete = true;
return account;
}
@@ -144,7 +167,7 @@ export abstract class AzureAuth implements vscode.Disposable {
// We want to return the one that owns the Azure account.
// Not doing so can result in token being issued for the wrong tenant
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) {
account.isStale = true;
return account;
@@ -154,23 +177,28 @@ export abstract class AzureAuth implements vscode.Disposable {
} catch (ex) {
if (ex instanceof AzureAuthError) {
void vscode.window.showErrorMessage(ex.message);
Logger.error(ex.originalMessageAndException);
Logger.error(`Error refreshing access for account ${account.displayInfo.displayName}`, ex.originalMessageAndException);
} else {
Logger.error(ex);
}
Logger.error(ex);
account.isStale = true;
return account;
}
}
public async hydrateAccount(token: Token | AccessToken, tokenClaims: TokenClaims): Promise<AzureAccount> {
const tenants = await this.getTenants({ ...token });
const account = this.createAccount(tokenClaims, token.key, tenants);
let account: azdata.Account;
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;
}
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) {
Logger.error('Account was stale. No tokens being fetched.');
return undefined;
@@ -178,8 +206,7 @@ export abstract class AzureAuth implements vscode.Disposable {
const resource = this.resources.find(s => s.azureResourceId === azureResource);
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;
}
@@ -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);
}
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
if (cachedTokens?.accessToken) {
@@ -213,7 +240,7 @@ export abstract class AzureAuth implements vscode.Disposable {
const maxTolerance = 2 * 60; // two minutes
if (remainingTime < maxTolerance) {
const result = await this.refreshToken(tenant, resource, cachedTokens.refreshToken);
const result = await this.refreshTokenAdal(tenant, resource, cachedTokens.refreshToken);
if (result) {
accessToken = result.accessToken;
expiresOn = Number(result.expiresOn);
@@ -224,7 +251,7 @@ export abstract class AzureAuth implements vscode.Disposable {
return {
...accessToken,
expiresOn: expiresOn,
tokenType: 'Bearer'
tokenType: Constants.Bearer
};
}
}
@@ -234,7 +261,7 @@ export abstract class AzureAuth implements vscode.Disposable {
if (!this.metadata.settings.microsoftResource) {
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) {
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.');
@@ -242,12 +269,12 @@ export abstract class AzureAuth implements vscode.Disposable {
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.
const result = await this.refreshToken(tenant, resource, baseTokens.refreshToken);
const result = await this.refreshTokenAdal(tenant, resource, baseTokens.refreshToken);
if (result?.accessToken) {
return {
...result.accessToken,
expiresOn: Number(result.expiresOn),
tokenType: 'Bearer'
tokenType: Constants.Bearer
};
}
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.
@@ -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
* 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 }], []);
if (refreshToken) {
const postData: RefreshTokenPostData = {
@@ -275,36 +304,96 @@ export abstract class AzureAuth implements vscode.Disposable {
tenant: tenant.id,
resource: resource.endpoint
};
return this.getToken(tenant, resource, postData);
return this.getTokenAdal(tenant, resource, postData);
}
return this.handleInteractionRequired(tenant, resource);
return this.handleInteractionRequiredAdal(tenant, resource);
}
public async getToken(tenant: Tenant, resource: Resource, postData: AuthorizationCodePostData | TokenPostData | RefreshTokenPostData): Promise<OAuthTokenResponse | undefined> {
Logger.verbose('Fetching token');
/**
* 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`];
}
// construct request
// 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 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) {
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);
}
const accessTokenString = response.data.access_token;
const refreshTokenString = response.data.refresh_token;
const expiresOnString = response.data.expires_on;
return this.getTokenHelper(tenant, resource, accessTokenString, refreshTokenString, expiresOnString);
return this.getTokenHelperAdal(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) {
const msg = localize('azure.accessTokenEmpty', 'No access token returned from Microsoft OAuth');
throw new AzureAuthError(msg, 'Access token was empty', undefined);
@@ -349,7 +438,8 @@ export abstract class AzureAuth implements vscode.Disposable {
const accountKey: azdata.AccountKey = {
providerId: this.metadata.id,
accountId: userKey
accountId: userKey,
authLibrary: this._authLibrary
};
await this.saveToken(tenant, resource, accountKey, result);
@@ -358,19 +448,47 @@ export abstract class AzureAuth implements vscode.Disposable {
}
//#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
}
public async getTenantsMsal(token: string): Promise<Tenant[]> {
const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2019-11-01');
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);
if (tenantResponse.status !== 200) {
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');
}
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 {
id: tenantInfo.tenantId,
displayName: tenantInfo.displayName ? tenantInfo.displayName : localize('azureWorkAccountDisplayName', "Work or school account"),
displayName: tenantInfo.displayName ? tenantInfo.displayName : tenantInfo.tenantId,
userId: token.key,
tenantCategory: tenantInfo.tenantCategory
} as Tenant;
});
const homeTenantIndex = tenants.findIndex(tenant => tenant.tenantCategory === 'Home');
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]);
@@ -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 parseMsg = localize('azure.cacheErrorParse', "Error when parsing your account from the cache");
@@ -464,12 +588,22 @@ export abstract class AzureAuth implements vscode.Disposable {
}
//#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);
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();
return result?.response;
}
@@ -487,13 +621,14 @@ export abstract class AzureAuth implements vscode.Disposable {
}
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') ?? [];
return new Set<string>(values);
};
// The user wants to ignore this tenant.
if (getTenantConfigurationSet().has(tenant.id)) {
Logger.info(`Tenant ${tenant.id} found in the ignore list, authentication will not be attempted.`);
return false;
}
@@ -544,22 +679,21 @@ export abstract class AzureAuth implements vscode.Disposable {
public createAccount(tokenClaims: TokenClaims, key: string, tenants: Tenant[]): AzureAccount {
Logger.verbose(`Token Claims: ${tokenClaims.name}`);
tenants.forEach((tenant) => {
Logger.verbose(
`Tenant ID: ${tenant.id}
Tenant Name: ${tenant.displayName}`);
Logger.verbose(`Tenant ID: ${tenant.id}, Tenant Name: ${tenant.displayName}`);
});
// Determine if this is a microsoft account
let accountIssuer = 'unknown';
if (tokenClaims.iss === 'https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/') {
accountIssuer = 'corp';
if (tokenClaims.iss === 'https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/' ||
tokenClaims.iss === 'https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/v2.0') {
accountIssuer = Constants.AccountIssuer.Corp;
}
if (tokenClaims?.idp === 'live.com') {
accountIssuer = 'msft';
accountIssuer = Constants.AccountIssuer.Msft;
}
const name = tokenClaims.name ?? tokenClaims.email ?? tokenClaims.unique_name;
const email = tokenClaims.email ?? tokenClaims.unique_name;
const name = tokenClaims.name ?? tokenClaims.email ?? tokenClaims.unique_name ?? tokenClaims.preferred_username;
const email = tokenClaims.email ?? tokenClaims.unique_name ?? tokenClaims.preferred_username;
// Read more about tid > https://learn.microsoft.com/azure/active-directory/develop/id-tokens
const owningTenant = tenants.find(t => t.id === tokenClaims.tid)
@@ -572,25 +706,26 @@ export abstract class AzureAuth implements vscode.Disposable {
let contextualDisplayName: string;
switch (accountIssuer) {
case 'corp':
case Constants.AccountIssuer.Corp:
contextualDisplayName = localize('azure.microsoftCorpAccount', "Microsoft Corp");
break;
case 'msft':
case Constants.AccountIssuer.Msft:
contextualDisplayName = localize('azure.microsoftAccountDisplayName', 'Microsoft Account');
break;
default:
contextualDisplayName = displayName;
}
let accountType = accountIssuer === 'msft'
? this.MicrosoftAccountType
: this.WorkSchoolAccountType;
let accountType = accountIssuer === Constants.AccountIssuer.Msft
? Constants.AccountType.Microsoft
: Constants.AccountType.WorkSchool;
const account = {
key: {
providerId: this.metadata.id,
accountId: key,
accountVersion: AzureAuth.ACCOUNT_VERSION,
accountVersion: Constants.AccountVersion,
authLibrary: this._authLibrary
},
name: displayName,
displayInfo: {
@@ -603,7 +738,7 @@ export abstract class AzureAuth implements vscode.Disposable {
},
properties: {
providerSettings: this.metadata,
isMsAccount: accountIssuer === 'msft',
isMsAccount: accountIssuer === Constants.AccountIssuer.Msft,
owningTenant: owningTenant,
tenants,
azureAuthType: this.authType
@@ -660,8 +795,10 @@ export abstract class AzureAuth implements vscode.Disposable {
protected toBase64UrlEncoding(base64string: string): string {
return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding
}
public async deleteAllCache(): Promise<void> {
public async deleteAllCacheMsal(): Promise<void> {
this.clientApplication.clearCache();
}
public async deleteAllCacheAdal(): Promise<void> {
const results = await this.tokenCache.findCredentials('');
for (let { account } of results) {
@@ -671,7 +808,13 @@ export abstract class AzureAuth implements vscode.Disposable {
public async clearCredentials(account: azdata.AccountKey): Promise<void> {
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) {
const msg = localize('azure.cacheErrrorRemove', "Error when removing your account from the cache.");
void vscode.window.showErrorMessage(msg);
@@ -679,9 +822,27 @@ export abstract class AzureAuth implements vscode.Disposable {
}
}
public async deleteAccountCache(account: azdata.AccountKey): Promise<void> {
const results = await this.tokenCache.findCredentials(account.accountId);
public async deleteAccountCacheMsal(account: azdata.AccountKey): Promise<void> {
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) {
await this.tokenCache.clearCredential(account);
}
@@ -722,6 +883,13 @@ export interface RefreshToken extends AccountKey {
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 {
[tenantId: string]: Token | undefined;
}

View File

@@ -12,11 +12,13 @@ import { SimpleTokenCache } from '../simpleTokenCache';
import { SimpleWebServer } from '../utils/simpleWebServer';
import { AzureAuthError } from './azureAuthError';
import { Logger } from '../../utils/Logger';
import * as Constants from '../../constants';
import * as nls from 'vscode-nls';
import * as path from 'path';
import * as http from 'http';
import * as qs from 'qs';
import { promises as fs } from 'fs';
import { PublicClientApplication, CryptoProvider, AuthorizationUrlRequest, AuthorizationCodeRequest, AuthenticationResult } from '@azure/msal-node';
const localize = nls.loadMessageBundle();
@@ -28,33 +30,43 @@ interface AuthCodeResponse {
interface CryptoValues {
nonce: string;
challengeMethod: string;
codeVerifier: string;
codeChallenge: string;
}
export class AzureAuthCodeGrant extends AzureAuth {
private static readonly USER_FRIENDLY_NAME: string = localize('azure.azureAuthCodeGrantName', 'Azure Auth Code Grant');
private cryptoProvider: CryptoProvider;
private pkceCodes: CryptoValues;
constructor(
metadata: AzureAccountProviderMetadata,
tokenCache: SimpleTokenCache,
context: vscode.ExtensionContext,
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 login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred<void, Error> }> {
protected async loginAdal(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred<void, Error> }> {
let authCompleteDeferred: Deferred<void, Error>;
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
let authResponse: AuthCodeResponse;
if (vscode.env.uiKind === vscode.UIKind.Web) {
authResponse = await this.loginWeb(tenant, resource);
authResponse = await this.loginWebAdal(tenant, resource);
} else {
authResponse = await this.loginDesktop(tenant, resource, authCompletePromise);
authResponse = await this.loginDesktopAdal(tenant, resource, authCompletePromise);
}
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
*
@@ -79,12 +115,47 @@ export class AzureAuthCodeGrant extends AzureAuth {
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 { 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 state = `${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`;
@@ -94,8 +165,8 @@ export class AzureAuthCodeGrant extends AzureAuth {
client_id: this.clientId,
redirect_uri: this.redirectUri,
state,
prompt: 'select_account',
code_challenge_method: 'S256',
prompt: Constants.SELECT_ACCOUNT,
code_challenge_method: Constants.S256_CODE_CHALLENGE_METHOD,
code_challenge: codeChallenge,
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();
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.');
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 loginQuery = {
response_type: 'code',
@@ -159,8 +279,8 @@ export class AzureAuthCodeGrant extends AzureAuth {
client_id: this.clientId,
redirect_uri: `${this.redirectUri}:${serverPort}/redirect`,
state,
prompt: 'select_account',
code_challenge_method: 'S256',
prompt: Constants.SELECT_ACCOUNT,
code_challenge_method: Constants.S256_CODE_CHALLENGE_METHOD,
code_challenge: codeChallenge,
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 codeVerifier = this.toBase64UrlEncoding(crypto.randomBytes(32).toString('base64'));
const codeChallenge = this.toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64'));
const challengeMethod = '';
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,
} from './azureAuth';
import {
AzureAccountProviderMetadata,
AzureAuthType,
@@ -21,8 +20,10 @@ import {
Resource
} from 'azurecore';
import { Deferred } from '../interfaces';
import { AuthenticationResult, DeviceCodeRequest, PublicClientApplication } from '@azure/msal-node';
import { SimpleTokenCache } from '../simpleTokenCache';
import { Logger } from '../../utils/Logger';
const localize = nls.loadMessageBundle();
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,
context: vscode.ExtensionContext,
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);
}
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 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 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);
return {
@@ -93,7 +116,6 @@ export class AzureDeviceCode extends AzureAuth {
azdata.accounts.endAutoOAuthDeviceCode();
}
private setupPolling(info: DeviceCodeLogin): Promise<DeviceCodeLoginResult> {
const timeoutMessage = localize('azure.timeoutDeviceCode', 'Timed out when waiting for device code login.');
const fiveMinutes = 5 * 60 * 1000;
@@ -130,18 +152,15 @@ export class AzureDeviceCode extends AzureAuth {
};
const postResult = await this.makePostRequest(uri, postData);
const result: DeviceCodeLoginResult = postResult.data;
return result;
} catch (ex) {
console.log(ex);
console.log('Unexpected error making Azure auth request', 'azureCore.checkForResult', JSON.stringify(ex?.response?.data, undefined, 2));
Logger.error('Unexpected error making Azure auth request', 'azureCore.checkForResult', JSON.stringify(ex?.response?.data, undefined, 2));
throw new Error(msg);
}
}
public override async autoOAuthCancelled(): Promise<void> {
return azdata.accounts.endAutoOAuthDeviceCode();
}

View File

@@ -13,31 +13,37 @@ import {
AzureAccount
} from 'azurecore';
import { Deferred } from './interfaces';
import { PublicClientApplication } from '@azure/msal-node';
import { SimpleTokenCache } from './simpleTokenCache';
import { Logger } from '../utils/Logger';
import { MultiTenantTokenResponse, Token, AzureAuth } from './auths/azureAuth';
import { AzureAuthCodeGrant } from './auths/azureAuthCodeGrant';
import { AzureDeviceCode } from './auths/azureDeviceCode';
import { filterAccounts } from '../azureResource/utils';
import * as Constants from '../constants';
const localize = nls.loadMessageBundle();
export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disposable {
private static readonly CONFIGURATION_SECTION = 'accounts.azure.auth';
private readonly authMappings = new Map<AzureAuthType, AzureAuth>();
private initComplete!: Deferred<void, Error>;
private initCompletePromise: Promise<void> = new Promise<void>((resolve, reject) => this.initComplete = { resolve, reject });
public clientApplication: PublicClientApplication;
constructor(
metadata: AzureAccountProviderMetadata,
tokenCache: SimpleTokenCache,
context: vscode.ExtensionContext,
clientApplication: PublicClientApplication,
uriEventHandler: vscode.EventEmitter<vscode.Uri>,
private readonly authLibrary: string,
private readonly forceDeviceCode: boolean = false
) {
this.clientApplication = clientApplication;
vscode.workspace.onDidChangeConfiguration((changeEvent) => {
const impact = changeEvent.affectsConfiguration(AzureAccountProvider.CONFIGURATION_SECTION);
if (impact === true) {
const impactProvider = changeEvent.affectsConfiguration(Constants.AccountsAzureAuthSection);
if (impactProvider === true) {
this.handleAuthMapping(metadata, tokenCache, context, uriEventHandler);
}
});
@@ -50,25 +56,28 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
}
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>) {
this.authMappings.forEach(m => m.dispose());
this.authMappings.clear();
const configuration = vscode.workspace.getConfiguration(AzureAccountProvider.CONFIGURATION_SECTION);
const codeGrantMethod: boolean = configuration.get<boolean>('codeGrant', false);
const deviceCodeMethod: boolean = configuration.get<boolean>('deviceCode', false);
const configuration = vscode.workspace.getConfiguration(Constants.AccountsAzureAuthSection);
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) {
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) {
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) {
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[]> {
const accounts: AzureAccount[] = [];
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);
if (!azureAuth) {
account.isStale = true;
accounts.push(account);
} 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();
@@ -123,7 +138,23 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
await this.initCompletePromise;
const azureAuth = this.getAuthMethod(account);
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> {
@@ -178,7 +209,6 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
return pick.azureAuth.startLogin();
}
refresh(account: AzureAccount): Thenable<AzureAccount | azdata.PromptFailedResult> {
return this._refresh(account);
}

View File

@@ -7,12 +7,18 @@ import * as azdata from 'azdata';
import * as events from 'events';
import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import * as os from 'os';
import { SimpleTokenCache } from './simpleTokenCache';
import providerSettings from './providerSettings';
import { AzureAccountProvider as AzureAccountProvider } from './azureAccountProvider';
import { AzureAccountProviderMetadata } from 'azurecore';
import { ProviderSettings } from './interfaces';
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();
@@ -23,11 +29,6 @@ class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.
}
export class AzureAccountProviderService implements vscode.Disposable {
// CONSTANTS ///////////////////////////////////////////////////////////////
private static CommandClearTokenCache = 'accounts.clearTokenCache';
private static ConfigurationSection = 'accounts.azure.cloud';
private static CredentialNamespace = 'azureAccountProviderCredentials';
// MEMBER VARIABLES ////////////////////////////////////////////////////////
private _disposables: 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 _event: events.EventEmitter = new events.EventEmitter();
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));
}
@@ -47,17 +52,16 @@ export class AzureAccountProviderService implements vscode.Disposable {
let self = this;
// Register commands
this._context.subscriptions.push(vscode.commands.registerCommand(
AzureAccountProviderService.CommandClearTokenCache,
() => { self._event.emit(AzureAccountProviderService.CommandClearTokenCache); }
this._context.subscriptions.push(vscode.commands.registerCommand(Constants.AccountsClearTokenCacheCommand,
() => { self._event.emit(Constants.AccountsClearTokenCacheCommand); }
));
this._event.on(AzureAccountProviderService.CommandClearTokenCache, () => { void self.onClearTokenCache(); });
this._event.on(Constants.AccountsClearTokenCacheCommand, () => { void self.onClearTokenCache(); });
// 1) Get a credential provider
// 2a) Store the credential provider for use later
// 2b) Register the configuration change handler
// 2c) Perform an initial config change handling
return azdata.credentials.getProvider(AzureAccountProviderService.CredentialNamespace)
return azdata.credentials.getProvider(Constants.AzureAccountProviderCredentials)
.then(credProvider => {
this._credentialProvider = credProvider;
@@ -103,7 +107,7 @@ export class AzureAccountProviderService implements vscode.Disposable {
// Add a new change processing onto the existing promise change
await this._configChangePromiseChain;
// 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;
this._currentConfig = newConfig;
@@ -138,22 +142,58 @@ export class AzureAccountProviderService implements vscode.Disposable {
}
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 {
const noSystemKeychain = vscode.workspace.getConfiguration('azure').get<boolean>('noSystemKeychain');
let tokenCacheKey = `azureTokenCache-${provider.metadata.id}`;
if (!this._credentialProvider) {
throw new Error('Credential provider not registered');
}
let simpleTokenCache = new SimpleTokenCache(tokenCacheKey, this._userStoragePath, noSystemKeychain, this._credentialProvider);
await simpleTokenCache.init();
const cachePath = path.join(this._userStoragePath, Constants.ConfigFilePath);
const isSaw: boolean = vscode.env.appName.toLowerCase().indexOf('saw') > 0;
let accountProvider = new AzureAccountProvider(provider.metadata as AzureAccountProviderMetadata, simpleTokenCache, this._context, this._uriEventHandler, isSaw);
switch (platform) {
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._accountDisposals[provider.metadata.id] = azdata.accounts.registerAccountProvider(provider.metadata, accountProvider);
} 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: {
id: SettingIds.arm,
endpoint: 'https://management.azure.com',
endpoint: 'https://management.azure.com/',
azureResourceId: AzureResource.ResourceManagement
},
sqlResource: {

View File

@@ -24,13 +24,15 @@ const typesClause = [
].map(type => `type == "${type}"`).join(' or ');
export class AzureDataGridProvider implements azdata.DataGridProvider {
constructor(private _appContext: AppContext) { }
constructor(private _appContext: AppContext,
private readonly authLibrary: string) { }
public providerId = constants.dataGridProviderId;
public title = loc.azureResourcesGridTitle;
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[] = [];
await Promise.all(accounts.map(async (account) => {
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 { 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];
vscode.commands.registerCommand('azure.resource.startterminal', async (node?: TreeNode) => {
try {

View File

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

View File

@@ -66,23 +66,25 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
if (subscriptions.length === 0) {
return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.noSubscriptionsLabel, this)];
} else {
// Filter out everything that we can't authenticate to.
const hasTokenResults = await Promise.all(subscriptions.map(async s => {
let token: azdata.accounts.AccountSecurityToken | undefined = undefined;
let errMsg = '';
try {
token = await azdata.accounts.getAccountSecurityToken(this.account, s.tenant!, azdata.AzureResource.ResourceManagement);
} catch (err) {
errMsg = AzureResourceErrorMessageUtil.getErrorMessage(err);
}
if (!token) {
void vscode.window.showWarningMessage(localize('azure.unableToAccessSubscription', "Unable to access subscription {0} ({1}). Please [refresh the account](command:azure.resource.signin) to try again. {2}", s.name, s.id, errMsg));
return false;
}
return true;
}));
subscriptions = subscriptions.filter((_s, i) => hasTokenResults[i]);
const authLibrary = vscode.workspace.getConfiguration('azure').get('authenticationLibrary');
if (authLibrary === 'ADAL') {
// Filter out everything that we can't authenticate to.
const hasTokenResults = await Promise.all(subscriptions.map(async s => {
let token: azdata.accounts.AccountSecurityToken | undefined = undefined;
let errMsg = '';
try {
token = await azdata.accounts.getAccountSecurityToken(this.account, s.tenant!, azdata.AzureResource.ResourceManagement);
} catch (err) {
errMsg = AzureResourceErrorMessageUtil.getErrorMessage(err);
}
if (!token) {
void vscode.window.showWarningMessage(localize('azure.unableToAccessSubscription', "Unable to access subscription {0} ({1}). Please [refresh the account](command:azure.resource.signin) to try again. {2}", s.name, s.id, errMsg));
return false;
}
return true;
}));
subscriptions = subscriptions.filter((_s, i) => hasTokenResults[i]);
}
let subTreeNodes = await Promise.all(subscriptions.map(async (subscription) => {
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 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 { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { AzureResourceErrorMessageUtil, equals } from '../utils';
import { AzureResourceErrorMessageUtil, equals, filterAccounts } from '../utils';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { FlatAccountTreeNode } from './flatAccountTreeNode';
import { Logger } from '../../utils/Logger';
@@ -26,10 +26,11 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider<Tre
private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode | 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) => {
// 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'));
// 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
@@ -55,10 +56,11 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider<Tre
}
if (this.accounts && this.accounts.length > 0) {
let accounts = filterAccounts(this.accounts, this.authLibrary);
const accountNodes: FlatAccountTreeNode[] = [];
const errorMessages: string[] = [];
// We are doing sequential account loading to avoid the Azure request throttling
for (const account of this.accounts) {
for (const account of accounts) {
try {
const accountNode = new FlatAccountTreeNode(account, this.appContext, this);
await accountNode.updateLabel();
@@ -85,7 +87,7 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider<Tre
private async loadAccounts(): Promise<void> {
try {
this.accounts = await azdata.accounts.getAllAccounts();
this.accounts = filterAccounts(await azdata.accounts.getAllAccounts(), this.authLibrary);
// System has been initialized
this.setSystemInitialized();
this._onDidChangeTreeData.fire(undefined);

View File

@@ -12,12 +12,12 @@ const localize = nls.loadMessageBundle();
import { TreeNode } from '../treeNode';
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { AzureResourceErrorMessageUtil } from '../utils';
import { AzureResourceErrorMessageUtil, filterAccounts } from '../utils';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { IAzureResourceNodeWithProviderId, IAzureResourceSubscriptionService } from '../interfaces';
import { AzureResourceServiceNames } from '../constants';
import { AzureResourceService } from '../resourceService';
import { Logger } from '../../utils/Logger';
export class FlatAzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNode>, IAzureResourceTreeChangeHandler {
public isSystemInitialized: boolean = false;
@@ -26,7 +26,8 @@ export class FlatAzureResourceTreeProvider implements vscode.TreeDataProvider<Tr
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[]> {
@@ -35,7 +36,7 @@ export class FlatAzureResourceTreeProvider implements vscode.TreeDataProvider<Tr
}
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));
}
@@ -87,7 +88,8 @@ class ResourceLoader {
private readonly _onDidAddNewResource = new vscode.EventEmitter<TreeNode | undefined>();
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.resourceService = appContext.getService<AzureResourceService>(AzureResourceServiceNames.resourceService);
}
@@ -118,7 +120,7 @@ class ResourceLoader {
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 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);
@@ -208,5 +210,4 @@ class AzureResourceResourceTreeNode extends TreeNode {
public get nodePathValue(): string {
return this.resourceNodeWithProviderId.resourceNode.treeItem.id || '';
}
}

View File

@@ -14,11 +14,10 @@ import { AzureResourceAccountTreeNode } from './accountTreeNode';
import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode';
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { AzureResourceErrorMessageUtil, equals } from '../utils';
import { AzureResourceErrorMessageUtil, equals, filterAccounts } from '../utils';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { AzureAccount } from 'azurecore';
export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNode>, IAzureResourceTreeChangeHandler {
public isSystemInitialized: boolean = false;
@@ -26,10 +25,11 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode | 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) => {
// 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'));
// 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
@@ -56,6 +56,7 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
try {
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));
} else {
return [new AzureResourceAccountNotSignedInTreeNode()];
@@ -67,7 +68,7 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
private async loadAccounts(): Promise<void> {
try {
this.accounts = await azdata.accounts.getAllAccounts();
this.accounts = filterAccounts(await azdata.accounts.getAllAccounts(), this.authLibrary);
// System has been initialized
this.setSystemInitialized();
this._onDidChangeTreeData.fire(undefined);
@@ -96,7 +97,6 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
node.clearCache();
}
}
this._onDidChangeTreeData.fire(node);
}

View File

@@ -17,6 +17,7 @@ import { IAzureResourceSubscriptionFilterService, IAzureResourceSubscriptionServ
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
import { BlobServiceClient, StorageSharedKeyCredential } from '@azure/storage-blob';
import providerSettings from '../account-provider/providerSettings';
import * as Constants from '../constants';
const localize = nls.loadMessageBundle();
@@ -541,3 +542,18 @@ export function getProviderMetadataForAccount(account: AzureAccount): AzureAccou
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.
*--------------------------------------------------------------------------------------------*/
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 HomeCategory = 'Home';
export const dataGridProviderId = 'azure-resources';
export const AzureTokenFolderName = 'Azure Accounts';
export const DefaultAuthLibrary = 'ADAL';
export enum BuiltInCommands {
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 utils from './utils';
import * as loc from './localizedConstants';
import * as constants from './constants';
import * as Constants from './constants';
import { AzureResourceGroupService } from './azureResource/providers/resourceGroup/resourceGroupService';
import { Logger } from './utils/Logger';
import { ConnectionDialogTreeProvider } from './azureResource/tree/connectionDialogTreeProvider';
@@ -58,15 +58,15 @@ let extensionContext: vscode.ExtensionContext;
function getAppDataPath() {
let platform = process.platform;
switch (platform) {
case 'win32': return process.env['APPDATA'] || path.join(process.env['USERPROFILE']!, 'AppData', 'Roaming');
case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support');
case 'linux': return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
case Constants.Platform.Windows: return process.env['APPDATA'] || path.join(process.env['USERPROFILE']!, 'AppData', 'Roaming');
case Constants.Platform.Mac: return path.join(os.homedir(), 'Library', 'Application Support');
case Constants.Platform.Linux: return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
default: throw new Error('Platform not supported');
}
}
function getDefaultLogLocation() {
return path.join(getAppDataPath(), 'azuredatastudio');
return path.join(getAppDataPath(), Constants.ServiceName);
}
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.
const config = vscode.workspace.getConfiguration('accounts.azure.auth');
const config = vscode.workspace.getConfiguration(Constants.AccountsAzureAuthSection);
if (vscode.env.uiKind === vscode.UIKind.Web) {
await config.update('deviceCode', true, vscode.ConfigurationTarget.Global);
}
const authLibrary: string = vscode.workspace.getConfiguration(Constants.AzureSection).get(Constants.AuthenticationLibrarySection)
?? Constants.DefaultAuthLibrary;
updatePiiLoggingLevel();
// 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);
const azureResourceTree = new AzureResourceTreeProvider(appContext);
const connectionDialogTree = new ConnectionDialogTreeProvider(appContext);
const azureResourceTree = new AzureResourceTreeProvider(appContext, authLibrary);
const connectionDialogTree = new ConnectionDialogTreeProvider(appContext, authLibrary);
pushDisposable(vscode.window.registerTreeDataProvider('azureResourceExplorer', azureResourceTree));
pushDisposable(vscode.window.registerTreeDataProvider('connectionDialog/azureResourceExplorer', connectionDialogTree));
pushDisposable(vscode.workspace.onDidChangeConfiguration(e => onDidChangeConfiguration(e)));
registerAzureResourceCommands(appContext, azureResourceTree, connectionDialogTree);
azdata.dataprotocol.registerDataGridProvider(new AzureDataGridProvider(appContext));
registerAzureResourceCommands(appContext, azureResourceTree, connectionDialogTree, authLibrary);
azdata.dataprotocol.registerDataGridProvider(new AzureDataGridProvider(appContext, authLibrary));
vscode.commands.registerCommand('azure.dataGrid.openInAzurePortal', async (item: azdata.DataGridItem) => {
const portalEndpoint = item.portalEndpoint;
const subscriptionId = item.subscriptionId;
@@ -130,7 +133,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
return azureResourceUtils.getLocations(appContext, account, subscription, ignoreErrors);
},
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[] = [
new KustoProvider(new KustoResourceService(), 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
async function findOrMakeStoragePath() {
let defaultLogLocation = getDefaultLogLocation();
let storagePath = path.join(defaultLogLocation, constants.AzureTokenFolderName);
let storagePath = path.join(defaultLogLocation, Constants.AzureTokenFolderName);
try {
await fs.mkdir(defaultLogLocation, { recursive: true });
@@ -258,9 +261,9 @@ async function findOrMakeStoragePath() {
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 {
const accountProviderService = new AzureAccountProviderService(extensionContext, storagePath);
const accountProviderService = new AzureAccountProviderService(extensionContext, storagePath, authLibrary);
extensionContext.subscriptions.push(accountProviderService);
await accountProviderService.activate();
} catch (err) {
@@ -281,10 +284,27 @@ async function onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent): Pro
if (e.affectsConfiguration('azure.piiLogging')) {
updatePiiLoggingLevel();
}
if (e.affectsConfiguration('azure.authenticationLibrary')) {
await displayReloadAds();
}
}
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;
}
// 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 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
export const sqlServer = localize('azurecore.sqlServer', "SQL server");
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 { AxiosResponse } from 'axios';
let azureAuthCodeGrant: TypeMoq.IMock<AzureAuthCodeGrant>;
// let azureDeviceCode: TypeMoq.IMock<AzureDeviceCode>;
@@ -52,9 +51,21 @@ describe('Azure Authentication', function () {
mockAccount = {
isStale: false,
displayInfo: {
contextualDisplayName: 'test',
accountType: 'test',
displayName: 'test',
userId: 'test'
},
key: {
providerId: 'test',
accountId: 'test'
},
properties: {
owningTenant: mockTenant,
tenants: [mockTenant]
tenants: [mockTenant],
providerSettings: provider,
isMsAccount: true
}
} as AzureAccount;
@@ -68,7 +79,7 @@ describe('Azure Authentication', 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([
mockTenant
]);
@@ -83,30 +94,30 @@ describe('Azure Authentication', function () {
describe('getAccountSecurityToken', function () {
it('should be undefined on stale account', async function () {
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();
});
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();
});
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 () {
azureAuthCodeGrant.setup(x => x.getTenants(mockToken)).returns(() => {
azureAuthCodeGrant.setup(x => x.getTenantsAdal(mockToken)).returns(() => {
return Promise.resolve([
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({
accessToken: mockAccessToken
} 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));
delete (mockToken as any).invalidData;
return Promise.resolve({
@@ -114,7 +125,7 @@ describe('Azure Authentication', function () {
} 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({
accessToken: mockAccessToken,
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');
});
it('saved token exists and can be reused', async function () {
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({
accessToken: mockAccessToken,
refreshToken: mockRefreshToken,
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');
});
@@ -145,47 +156,47 @@ describe('Azure Authentication', function () {
it('saved token had invalid expiration', async function () {
delete (mockAccessToken as any).tokenType;
(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({
accessToken: mockAccessToken,
refreshToken: mockRefreshToken,
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));
delete (mockToken as any).invalidData;
return Promise.resolve({
accessToken: mockToken
} 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?.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 () {
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);
});
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);
});
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 () {
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);
});
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({
accessToken: mockAccessToken,
refreshToken: mockRefreshToken,
@@ -194,13 +205,13 @@ describe('Azure Authentication', function () {
});
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({
accessToken: mockAccessToken
} 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');
});
});
@@ -218,16 +229,16 @@ describe('Azure Authentication', function () {
} 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({
accessToken: mockAccessToken
} 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);
});
@@ -241,7 +252,7 @@ describe('Azure Authentication', function () {
} 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 () {
@@ -255,16 +266,16 @@ describe('Azure Authentication', function () {
} 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({
accessToken: mockAccessToken
} 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);
});

View File

@@ -28,9 +28,11 @@ import allSettings from '../../../account-provider/providerSettings';
// Mock services
let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
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 mockAppContext: AppContext;
let mockAppContextADAL: AppContext;
let mockAppContextMSAL: AppContext;
let mockTreeChangeHandler: TypeMoq.IMock<IAzureResourceTreeChangeHandler>;
// Mock test data
@@ -95,18 +97,25 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
beforeEach(() => {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, undefined)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionServiceADAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
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>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockSubscriptionCache = [];
mockAppContext = new AppContext(mockExtensionContext.object);
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
mockAppContextADAL = new AppContext(mockExtensionContext.object);
mockAppContextADAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContextADAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.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.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
@@ -120,8 +129,8 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
sinon.restore();
});
it('Should be correct when created.', async function (): Promise<void> {
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
it('Should be correct when created for ADAL.', async function (): Promise<void> {
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
const accountTreeNodeId = `account_${mockAccount.key.accountId}`;
@@ -140,14 +149,34 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
should(nodeInfo.iconType).equal(AzureResourceItemType.account);
});
it('Should be correct when there are subscriptions listed.', async function (): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
it('Should be correct when created for MSAL.', async function (): Promise<void> {
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([]));
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
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();
@@ -161,13 +190,34 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
should(nodeInfo.label).equal(accountTreeNodeLabel);
});
it('Should only show subscriptions with valid tokens.', async function (): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
it('Should be correct when there are subscriptions listed for MSAL.', async function (): Promise<void> {
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));
sinon.stub(azdata.accounts, 'getAccountSecurityToken').onFirstCall().resolves(mockToken);
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();
@@ -181,13 +231,53 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
should(nodeInfo.label).equal(accountTreeNodeLabel);
});
it('Should be correct when there are subscriptions filtered.', async function (): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
it('Should only show subscriptions with valid tokens 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').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));
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
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();
@@ -206,17 +296,23 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
beforeEach(() => {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
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>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockSubscriptionCache = [];
mockAppContext = new AppContext(mockExtensionContext.object);
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
mockAppContextADAL = new AppContext(mockExtensionContext.object);
mockAppContextADAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContextADAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.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);
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
@@ -231,15 +327,15 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
sinon.restore();
});
it('Should load subscriptions from scratch and update cache when it is clearing cache.', async function (): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
it('Should load subscriptions from scratch and update cache when it is clearing cache 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([]));
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
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.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), 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> {
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([]));
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
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.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> {
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();
@@ -302,10 +398,10 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): 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));
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
const children = await accountTreeNode.getChildren();
@@ -320,16 +416,16 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): 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';
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();
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());
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());
@@ -346,17 +442,17 @@ describe('AzureResourceAccountTreeNode.clearCache', function (): void {
beforeEach(() => {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
mockSubscriptionServiceADAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockSubscriptionCache = [];
mockAppContext = new AppContext(mockExtensionContext.object);
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
mockAppContextADAL = new AppContext(mockExtensionContext.object);
mockAppContextADAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContextADAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object);
mockAppContextADAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
sinon.stub(azdata.accounts, 'getAccountSecurityToken').returns(Promise.resolve(mockToken));
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> {
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
accountTreeNode.clearCache();
should(accountTreeNode.isClearingCache).true();
});

View File

@@ -26,10 +26,11 @@ let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>;
// Mock test data
const mockAccount1: AzureAccount = {
const mockAccountAdal1: AzureAccount = {
key: {
accountId: 'mock_account_1',
providerId: 'mock_provider'
providerId: 'mock_provider',
authLibrary: 'ADAL'
},
displayInfo: {
displayName: 'mock_account_1@test.com',
@@ -40,7 +41,7 @@ const mockAccount1: AzureAccount = {
properties: TypeMoq.Mock.ofType<AzureAccountProperties>().object,
isStale: false
};
const mockAccount2: AzureAccount = {
const mockAccountAdal2: AzureAccount = {
key: {
accountId: 'mock_account_2',
providerId: 'mock_provider'
@@ -54,7 +55,39 @@ const mockAccount2: AzureAccount = {
properties: TypeMoq.Mock.ofType<AzureAccountProperties>().object,
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 {
beforeEach(() => {
@@ -68,35 +101,69 @@ describe('AzureResourceTreeProvider.getChildren', function (): void {
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
});
afterEach(function(): void {
afterEach(function (): void {
sinon.restore();
});
it('Should load accounts.', async function (): Promise<void> {
const getAllAccountsStub = sinon.stub(azdata.accounts, 'getAllAccounts').returns(Promise.resolve(mockAccounts));
it('Should load accounts for ADAL', async function (): Promise<void> {
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
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(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 account = mockAccounts[ix];
const account = mockAccountsADAL[ix];
should(child).instanceof(AzureResourceAccountTreeNode);
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([]));
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;
const children = await treeProvider.getChildren(undefined);

View File

@@ -127,6 +127,29 @@
uuid "^3.3.2"
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":
version "12.6.0"
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"
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:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
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:
version "1.1.11"
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"
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:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@@ -621,6 +671,11 @@ chokidar@3.3.0:
optionalDependencies:
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:
version "0.3.3"
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"
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:
version "3.0.0"
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"
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:
version "3.5.0"
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"
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:
version "7.0.3"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
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:
version "1.20.1"
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"
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:
version "7.0.1"
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"
mime-types "^2.1.12"
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -926,6 +1022,11 @@ get-symbol-description@^1.0.0:
call-bind "^1.0.2"
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:
version "5.1.2"
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"
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:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -1042,11 +1148,16 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2:
inherits@2, inherits@^2.0.3, inherits@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@~1.3.0:
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
internal-slot@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
@@ -1268,11 +1379,52 @@ json5@^2.1.2:
dependencies:
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:
version "4.1.0"
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4"
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:
version "3.0.0"
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"
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:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
@@ -1298,6 +1485,13 @@ log-symbols@3.0.0:
dependencies:
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:
version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
@@ -1334,6 +1528,11 @@ mime-types@^2.1.12:
dependencies:
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:
version "3.0.4"
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:
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:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
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:
version "0.5.5"
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"
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:
version "4.0.4"
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"
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:
version "1.0.6"
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"
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"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies:
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"
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:
version "0.11.10"
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"
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:
version "2.1.1"
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"
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:
version "3.2.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839"
@@ -1610,6 +1893,11 @@ rimraf@^2.6.3:
dependencies:
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:
version "5.1.2"
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"
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:
version "2.0.0"
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"
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:
version "9.0.2"
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"
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:
version "4.0.0"
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"
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"
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:
version "6.0.0"
@@ -1796,6 +2112,27 @@ supports-color@^7.1.0:
dependencies:
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:
version "2.0.0"
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"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
tslib@^1.10.0:
tslib@^1.10.0, tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
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"
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:
version "0.0.6"
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"
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:
version "3.4.0"
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"
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:
version "13.1.2"
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",
"asar": "^3.0.3",
"chromium-pickle-js": "^0.2.0",
"cookie": "^0.4.0",
"concurrently": "^5.2.0",
"cookie": "^0.4.0",
"copy-webpack-plugin": "^6.0.3",
"cson-parser": "^1.3.3",
"css-loader": "^3.2.0",

View File

@@ -502,6 +502,13 @@ declare module 'azdata' {
type?: ExtensionNodeType;
}
export interface AccountKey {
/**
* Auth Library used to add the account
*/
authLibrary?: string;
}
export namespace workspace {
/**
* 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';
export const VIEWLET_ID = 'workbench.view.accountpanel';
export type AuthLibrary = 'ADAL' | 'MSAL';
export class AccountPaneContainer extends ViewPaneContainer {
@@ -376,9 +377,14 @@ export class AccountDialog extends Modal {
this._splitView!.layout(DOM.getContentHeight(this._container!));
// 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();
}
@@ -413,7 +419,12 @@ export class AccountDialog extends Modal {
if (!providerMapping || !providerMapping.view) {
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) {
this.showSplitView();
@@ -480,3 +491,27 @@ export class AccountDialog extends Modal {
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 { INotificationService, Severity, INotification } from 'vs/platform/notification/common/notification';
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 {
// CONSTANTS ///////////////////////////////////////////////////////////
@@ -36,6 +39,8 @@ export class AccountManagementService implements IAccountManagementService {
private _accountDialogController?: AccountDialogController;
private _autoOAuthDialogController?: AutoOAuthDialogController;
private _mementoContext?: Memento;
protected readonly disposables = new DisposableStore();
private readonly configurationService: IConfigurationService;
// EVENT EMITTERS //////////////////////////////////////////////////////
private _addAccountProviderEmitter: Emitter<AccountProviderAddedEventParams>;
@@ -54,7 +59,8 @@ export class AccountManagementService implements IAccountManagementService {
@IClipboardService private _clipboardService: IClipboardService,
@IOpenerService private _openerService: IOpenerService,
@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);
const mementoObj = this._mementoContext.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE);
@@ -64,8 +70,10 @@ export class AccountManagementService implements IAccountManagementService {
this._addAccountProviderEmitter = new Emitter<AccountProviderAddedEventParams>();
this._removeAccountProviderEmitter = new Emitter<azdata.AccountProviderMetadata>();
this._updateAccountListEmitter = new Emitter<UpdateAccountListEventParams>();
this.configurationService = configurationService;
_storageService.onWillSaveState(() => this.shutdown());
this.registerListeners();
}
private get autoOAuthDialogController(): AutoOAuthDialogController {
@@ -136,6 +144,10 @@ export class AccountManagementService implements IAccountManagementService {
}
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) {
// Add the account to the list
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
let eventArg: UpdateAccountListEventParams = {
providerId: provider.metadata.id,
accountList: provider.accounts
accountList: updatedAccounts ?? provider.accounts
};
this._updateAccountListEmitter.fire(eventArg);
}
@@ -475,6 +492,39 @@ export class AccountManagementService implements IAccountManagementService {
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 { AccountDialog } from 'sql/workbench/services/accountManagement/browser/accountDialog';
import { Emitter } from 'vs/base/common/event';
import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService';
// SUITE CONSTANTS /////////////////////////////////////////////////////////
const hasAccountProvider: azdata.AccountProviderMetadata = {
@@ -530,9 +531,10 @@ function getTestState(): AccountManagementState {
.returns(() => mockAccountStore.object);
const testNotificationService = new TestNotificationService();
const testConfigurationService = new TestConfigurationService();
// 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
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 { ILogService } from 'vs/platform/log/common/log';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
/**
* Connection Widget clas for CMS Connections
@@ -47,8 +48,9 @@ export class CmsConnectionWidget extends ConnectionWidget {
@IAccountManagementService _accountManagementService: IAccountManagementService,
@ILogService _logService: ILogService,
@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];
if (authTypeOption) {
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 * as azdata from 'azdata';
import * as nls from 'vs/nls';
import * as errors from 'vs/base/common/errors';
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 { isFalsyOrWhitespace } from 'vs/base/common/strings';
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");
@@ -107,6 +109,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
color: undefined,
description: undefined,
};
private readonly configurationService: IConfigurationService;
constructor(options: azdata.ConnectionOption[],
callbacks: IConnectionComponentCallbacks,
providerName: string,
@@ -115,7 +118,8 @@ export class ConnectionWidget extends lifecycle.Disposable {
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IAccountManagementService private _accountManagementService: IAccountManagementService,
@ILogService protected _logService: ILogService,
@IErrorMessageService private _errorMessageService: IErrorMessageService
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IConfigurationService configurationService: IConfigurationService
) {
super();
this._callbacks = callbacks;
@@ -135,6 +139,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
}
this._providerName = providerName;
this._connectionStringOptions = this._connectionManagementService.getProviderProperties(this._providerName).connectionStringOptions;
this.configurationService = configurationService;
}
protected getAuthTypeDefault(option: azdata.ConnectionOption, os: OperatingSystem): string {
@@ -591,7 +596,12 @@ export class ConnectionWidget extends lifecycle.Disposable {
private async fillInAzureAccountOptions(): Promise<void> {
let oldSelection = this._azureAccountDropdown.value;
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 => {
return {
text: account.displayInfo.displayName,