diff --git a/extensions/azurecore/extension.webpack.config.js b/extensions/azurecore/extension.webpack.config.js index 0d7c1df575..b09bc26b63 100644 --- a/extensions/azurecore/extension.webpack.config.js +++ b/extensions/azurecore/extension.webpack.config.js @@ -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 diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json index 9bf6c58ef0..1d55c697d5 100644 --- a/extensions/azurecore/package.json +++ b/extensions/azurecore/package.json @@ -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", diff --git a/extensions/azurecore/package.nls.json b/extensions/azurecore/package.nls.json index aeb622023c..1fcc595df4 100644 --- a/extensions/azurecore/package.nls.json +++ b/extensions/azurecore/package.nls.json @@ -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." } diff --git a/extensions/azurecore/src/account-provider/auths/azureAuth.ts b/extensions/azurecore/src/account-provider/auths/azureAuth.ts index 28ea046a85..eebee216a5 100644 --- a/extensions/azurecore/src/account-provider/auths/azureAuth.ts +++ b/extensions/azurecore/src/account-provider/auths/azureAuth.ts @@ -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(); - - 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, 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 = 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 { + public async refreshAccessAdal(account: AzureAccount): Promise { // 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 { - 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 { + public async getAccountSecurityTokenAdal(account: AzureAccount, tenantId: string, azureResource: azdata.AzureResource): Promise { 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 }>; + protected abstract loginAdal(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred }>; + + protected abstract loginMsal(tenant: Tenant, resource: Resource): Promise<{ response: AuthenticationResult | null, authComplete: Deferred }>; /** * 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 { + public async refreshTokenAdal(tenant: Tenant, resource: Resource, refreshToken: RefreshToken | undefined): Promise { 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 { - 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 { + 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 { + 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 { + public async getTokenHelperAdal(tenant: Tenant, resource: Resource, accessTokenString: string, refreshTokenString: string, expiresOnString: string): Promise { 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 { - 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 { 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 { + 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 { + //#region interaction handling + public async handleInteractionRequiredMsal(tenant: Tenant, resource: Resource): Promise { 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 { + 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 => { - const configuration = vscode.workspace.getConfiguration('azure.tenant.config'); + const configuration = vscode.workspace.getConfiguration(Constants.AzureTenantConfigSection); let values: string[] = configuration.get('filter') ?? []; return new Set(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 { + public async deleteAllCacheMsal(): Promise { + this.clientApplication.clearCache(); + } + public async deleteAllCacheAdal(): Promise { 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 { 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 { - const results = await this.tokenCache.findCredentials(account.accountId); + public async deleteAccountCacheMsal(account: azdata.AccountKey): Promise { + 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 { + 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; } diff --git a/extensions/azurecore/src/account-provider/auths/azureAuthCodeGrant.ts b/extensions/azurecore/src/account-provider/auths/azureAuthCodeGrant.ts index 6961dc25e9..625393c6ba 100644 --- a/extensions/azurecore/src/account-provider/auths/azureAuthCodeGrant.ts +++ b/extensions/azurecore/src/account-provider/auths/azureAuthCodeGrant.ts @@ -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, + 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 }> { + protected async loginAdal(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred }> { let authCompleteDeferred: Deferred; let authCompletePromise = new Promise((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 }> { + let authCompleteDeferred: Deferred; + let authCompletePromise = new Promise((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 { + private async loginWebMsal(tenant: Tenant, resource: Resource): Promise { 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 { + 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): Promise { + private async loginDesktopMsal(tenant: Tenant, resource: Resource, authCompletePromise: Promise): Promise { 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): Promise { + 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 { + this.pkceCodes.nonce = this.cryptoProvider.createNewGuid(); + const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes(); + this.pkceCodes.codeVerifier = verifier; + this.pkceCodes.codeChallenge = challenge; + } } diff --git a/extensions/azurecore/src/account-provider/auths/azureDeviceCode.ts b/extensions/azurecore/src/account-provider/auths/azureDeviceCode.ts index 92da34ded1..2ed130e509 100644 --- a/extensions/azurecore/src/account-provider/auths/azureDeviceCode.ts +++ b/extensions/azurecore/src/account-provider/auths/azureDeviceCode.ts @@ -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, + 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 }> { + + protected async loginMsal(tenant: Tenant, resource: Resource): Promise<{ response: AuthenticationResult | null, authComplete: Deferred }> { + let authCompleteDeferred: Deferred; + let authCompletePromise = new Promise((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 }> { let authCompleteDeferred: Deferred; let authCompletePromise = new Promise((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 { 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 { return azdata.accounts.endAutoOAuthDeviceCode(); } diff --git a/extensions/azurecore/src/account-provider/azureAccountProvider.ts b/extensions/azurecore/src/account-provider/azureAccountProvider.ts index 59b446b457..a454c50b90 100644 --- a/extensions/azurecore/src/account-provider/azureAccountProvider.ts +++ b/extensions/azurecore/src/account-provider/azureAccountProvider.ts @@ -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(); private initComplete!: Deferred; private initCompletePromise: Promise = new Promise((resolve, reject) => this.initComplete = { resolve, reject }); + public clientApplication: PublicClientApplication; constructor( metadata: AzureAccountProviderMetadata, tokenCache: SimpleTokenCache, context: vscode.ExtensionContext, + clientApplication: PublicClientApplication, uriEventHandler: vscode.EventEmitter, + 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 { - 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) { this.authMappings.forEach(m => m.dispose()); this.authMappings.clear(); - const configuration = vscode.workspace.getConfiguration(AzureAccountProvider.CONFIGURATION_SECTION); - const codeGrantMethod: boolean = configuration.get('codeGrant', false); - const deviceCodeMethod: boolean = configuration.get('deviceCode', false); + const configuration = vscode.workspace.getConfiguration(Constants.AccountsAzureAuthSection); + const codeGrantMethod: boolean = configuration.get(Constants.AuthType.CodeGrant, false); + const deviceCodeMethod: boolean = configuration.get(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 { 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 { @@ -178,7 +209,6 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp return pick.azureAuth.startLogin(); } - refresh(account: AzureAccount): Thenable { return this._refresh(account); } diff --git a/extensions/azurecore/src/account-provider/azureAccountProviderService.ts b/extensions/azurecore/src/account-provider/azureAccountProviderService.ts index 4db21af239..0b0e91300d 100644 --- a/extensions/azurecore/src/account-provider/azureAccountProviderService.ts +++ b/extensions/azurecore/src/account-provider/azureAccountProviderService.ts @@ -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 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 { + const isSaw: boolean = vscode.env.appName.toLowerCase().indexOf(Constants.Saw) > 0; + const noSystemKeychain = vscode.workspace.getConfiguration(Constants.AzureSection).get(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('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}`); } } diff --git a/extensions/azurecore/src/account-provider/providerSettings.ts b/extensions/azurecore/src/account-provider/providerSettings.ts index 2ced26345f..7beb3411e4 100644 --- a/extensions/azurecore/src/account-provider/providerSettings.ts +++ b/extensions/azurecore/src/account-provider/providerSettings.ts @@ -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: { diff --git a/extensions/azurecore/src/azureDataGridProvider.ts b/extensions/azurecore/src/azureDataGridProvider.ts index 67a47086a3..1e99bbeeed 100644 --- a/extensions/azurecore/src/azureDataGridProvider.ts +++ b/extensions/azurecore/src/azureDataGridProvider.ts @@ -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; }) => { diff --git a/extensions/azurecore/src/azureResource/commands.ts b/extensions/azurecore/src/azureResource/commands.ts index 151b03238f..854e5ef2fb 100644 --- a/extensions/azurecore/src/azureResource/commands.ts +++ b/extensions/azurecore/src/azureResource/commands.ts @@ -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 { diff --git a/extensions/azurecore/src/azureResource/services/subscriptionService.ts b/extensions/azurecore/src/azureResource/services/subscriptionService.ts index 2d957447d7..7627661db7 100644 --- a/extensions/azurecore/src/azureResource/services/subscriptionService.ts +++ b/extensions/azurecore/src/azureResource/services/subscriptionService.ts @@ -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) { diff --git a/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts b/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts index f8561bade0..587bda2ba4 100644 --- a/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts +++ b/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts @@ -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)); + } + } diff --git a/extensions/azurecore/src/azureResource/tree/connectionDialogTreeProvider.ts b/extensions/azurecore/src/azureResource/tree/connectionDialogTreeProvider.ts index 243859cec2..bf69812a6b 100644 --- a/extensions/azurecore/src/azureResource/tree/connectionDialogTreeProvider.ts +++ b/extensions/azurecore/src/azureResource/tree/connectionDialogTreeProvider.ts @@ -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(); private loadingAccountsPromise: Promise | 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 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 { 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); diff --git a/extensions/azurecore/src/azureResource/tree/flatTreeProvider.ts b/extensions/azurecore/src/azureResource/tree/flatTreeProvider.ts index e6559f7684..e7a26e5380 100644 --- a/extensions/azurecore/src/azureResource/tree/flatTreeProvider.ts +++ b/extensions/azurecore/src/azureResource/tree/flatTreeProvider.ts @@ -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, IAzureResourceTreeChangeHandler { public isSystemInitialized: boolean = false; @@ -26,7 +26,8 @@ export class FlatAzureResourceTreeProvider implements vscode.TreeDataProvider { @@ -35,7 +36,7 @@ export class FlatAzureResourceTreeProvider implements vscode.TreeDataProvider this._onDidChangeTreeData.fire(e)); } @@ -87,7 +88,8 @@ class ResourceLoader { private readonly _onDidAddNewResource = new vscode.EventEmitter(); public readonly onDidAddNewResource = this._onDidAddNewResource.event; - constructor(private readonly appContext: AppContext) { + constructor(private readonly appContext: AppContext, + private readonly authLibrary: string) { this.subscriptionService = appContext.getService(AzureResourceServiceNames.subscriptionService); this.resourceService = appContext.getService(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 || ''; } - } diff --git a/extensions/azurecore/src/azureResource/tree/treeProvider.ts b/extensions/azurecore/src/azureResource/tree/treeProvider.ts index 14c08b8e4d..a9c51e469d 100644 --- a/extensions/azurecore/src/azureResource/tree/treeProvider.ts +++ b/extensions/azurecore/src/azureResource/tree/treeProvider.ts @@ -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, IAzureResourceTreeChangeHandler { public isSystemInitialized: boolean = false; @@ -26,10 +25,11 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider(); private loadingAccountsPromise: Promise | 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 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 { 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 { + if (account.key.authLibrary) { + return account.key.authLibrary === authLibrary; + } else { + return authLibrary === Constants.AuthLibrary.ADAL; + } + }); + return filteredAccounts; +} diff --git a/extensions/azurecore/src/constants.ts b/extensions/azurecore/src/constants.ts index 1c3250d250..4f7d10eb40 100644 --- a/extensions/azurecore/src/constants.ts +++ b/extensions/azurecore/src/constants.ts @@ -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' +} diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts index 64c16f14ea..cb2100ca30 100644 --- a/extensions/azurecore/src/extension.ts +++ b/extensions/azurecore/src/extension.ts @@ -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 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 { +async function initAzureAccountProvider(extensionContext: vscode.ExtensionContext, storagePath: string, authLibrary: string): Promise { 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 { + 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; + } + +} + diff --git a/extensions/azurecore/src/localizedConstants.ts b/extensions/azurecore/src/localizedConstants.ts index 6cfb13cea0..78fe264bb2 100644 --- a/extensions/azurecore/src/localizedConstants.ts +++ b/extensions/azurecore/src/localizedConstants.ts @@ -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"); diff --git a/extensions/azurecore/src/test/account-provider/auths/azureAuth.test.ts b/extensions/azurecore/src/test/account-provider/auths/azureAuth.test.ts index b8ead5cae0..be07eeb68c 100644 --- a/extensions/azurecore/src/test/account-provider/auths/azureAuth.test.ts +++ b/extensions/azurecore/src/test/account-provider/auths/azureAuth.test.ts @@ -13,7 +13,6 @@ import providerSettings from '../../../account-provider/providerSettings'; import { AzureResource } from 'azdata'; import { AxiosResponse } from 'axios'; - let azureAuthCodeGrant: TypeMoq.IMock; // let azureDeviceCode: TypeMoq.IMock; @@ -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 => { + azureAuthCodeGrant.setup(x => x.getTenantsAdal(mockToken)).returns((): Promise => { 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 => { + azureAuthCodeGrant.setup(x => x.refreshTokenAdal(mockTenant, provider.settings.ossRdbmsResource!, mockRefreshToken)).returns((): Promise => { 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 => { + azureAuthCodeGrant.setup(x => x.refreshTokenAdal(mockTenant, provider.settings.microsoftResource!, mockRefreshToken)).returns((): Promise => { 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); }); - 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); }); - 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); }); - 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); }); diff --git a/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts index ecba49937b..1b49556584 100644 --- a/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts +++ b/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts @@ -28,9 +28,11 @@ import allSettings from '../../../account-provider/providerSettings'; // Mock services let mockExtensionContext: TypeMoq.IMock; let mockCacheService: TypeMoq.IMock; -let mockSubscriptionService: TypeMoq.IMock; +let mockSubscriptionServiceADAL: TypeMoq.IMock; +let mockSubscriptionServiceMSAL: TypeMoq.IMock; let mockSubscriptionFilterService: TypeMoq.IMock; -let mockAppContext: AppContext; +let mockAppContextADAL: AppContext; +let mockAppContextMSAL: AppContext; let mockTreeChangeHandler: TypeMoq.IMock; // Mock test data @@ -95,18 +97,25 @@ describe('AzureResourceAccountTreeNode.info', function (): void { beforeEach(() => { mockExtensionContext = TypeMoq.Mock.ofType(); mockCacheService = TypeMoq.Mock.ofType(); - mockSubscriptionService = TypeMoq.Mock.ofType(); - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, undefined)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionServiceADAL = TypeMoq.Mock.ofType(); + mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionServiceMSAL = TypeMoq.Mock.ofType(); + mockSubscriptionServiceMSAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions)); mockSubscriptionFilterService = TypeMoq.Mock.ofType(); mockTreeChangeHandler = TypeMoq.Mock.ofType(); mockSubscriptionCache = []; - mockAppContext = new AppContext(mockExtensionContext.object); - mockAppContext.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object); - mockAppContext.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object); - mockAppContext.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object); + mockAppContextADAL = new AppContext(mockExtensionContext.object); + mockAppContextADAL.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object); + mockAppContextADAL.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object); + mockAppContextADAL.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object); + + mockAppContextMSAL = new AppContext(mockExtensionContext.object); + mockAppContextMSAL.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object); + mockAppContextMSAL.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceMSAL.object); + mockAppContextMSAL.registerService(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 { - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); + it('Should be correct when created for ADAL.', async function (): Promise { + 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 { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions)); + it('Should be correct when created for MSAL.', async function (): Promise { + 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 { + 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 { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions)); + it('Should be correct when there are subscriptions listed for MSAL.', async function (): Promise { + 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 { + 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 { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions)); + it('Should only show subscriptions with valid tokens for MSAL.', async function (): Promise { + 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 { + 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 { + 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(); mockCacheService = TypeMoq.Mock.ofType(); - mockSubscriptionService = TypeMoq.Mock.ofType(); + mockSubscriptionServiceADAL = TypeMoq.Mock.ofType(); + mockSubscriptionServiceMSAL = TypeMoq.Mock.ofType(); mockSubscriptionFilterService = TypeMoq.Mock.ofType(); mockTreeChangeHandler = TypeMoq.Mock.ofType(); mockSubscriptionCache = []; - mockAppContext = new AppContext(mockExtensionContext.object); - mockAppContext.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object); - mockAppContext.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object); - mockAppContext.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object); + mockAppContextADAL = new AppContext(mockExtensionContext.object); + mockAppContextADAL.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object); + mockAppContextADAL.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object); + mockAppContextADAL.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object); + + mockAppContextMSAL = new AppContext(mockExtensionContext.object); + mockAppContextMSAL.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object); + mockAppContextMSAL.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceMSAL.object); + mockAppContextMSAL.registerService(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 { - 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 { + 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 { - 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 { - 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 { - 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 { - 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(); mockCacheService = TypeMoq.Mock.ofType(); - mockSubscriptionService = TypeMoq.Mock.ofType(); + mockSubscriptionServiceADAL = TypeMoq.Mock.ofType(); mockSubscriptionFilterService = TypeMoq.Mock.ofType(); mockTreeChangeHandler = TypeMoq.Mock.ofType(); mockSubscriptionCache = []; - mockAppContext = new AppContext(mockExtensionContext.object); - mockAppContext.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object); - mockAppContext.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object); - mockAppContext.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object); + mockAppContextADAL = new AppContext(mockExtensionContext.object); + mockAppContextADAL.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object); + mockAppContextADAL.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object); + mockAppContextADAL.registerService(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 { - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object); + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object); accountTreeNode.clearCache(); should(accountTreeNode.isClearingCache).true(); }); diff --git a/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts b/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts index 1e932c307d..351945ddc1 100644 --- a/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts +++ b/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts @@ -26,10 +26,11 @@ let mockExtensionContext: TypeMoq.IMock; let mockCacheService: TypeMoq.IMock; // 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().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().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().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().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 { - const getAllAccountsStub = sinon.stub(azdata.accounts, 'getAllAccounts').returns(Promise.resolve(mockAccounts)); + it('Should load accounts for ADAL', async function (): Promise { + 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 { + it('Should load accounts for MSAL', async function (): Promise { + 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 { 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 { + sinon.stub(azdata.accounts, 'getAllAccounts').returns(Promise.resolve([])); + + const treeProvider = new AzureResourceTreeProvider(mockAppContext, 'MSAL'); treeProvider.isSystemInitialized = true; const children = await treeProvider.getChildren(undefined); diff --git a/extensions/azurecore/yarn.lock b/extensions/azurecore/yarn.lock index bd59a88da1..d89520cb11 100644 --- a/extensions/azurecore/yarn.lock +++ b/extensions/azurecore/yarn.lock @@ -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" diff --git a/package.json b/package.json index fdd61e8e58..75caf90642 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 2b203d6d59..053f934384 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -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 diff --git a/src/sql/workbench/services/accountManagement/browser/accountDialog.ts b/src/sql/workbench/services/accountManagement/browser/accountDialog.ts index c509ade7f7..f0b87d6df8 100644 --- a/src/sql/workbench/services/accountManagement/browser/accountDialog.ts +++ b/src/sql/workbench/services/accountManagement/browser/accountDialog.ts @@ -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; +} diff --git a/src/sql/workbench/services/accountManagement/browser/accountManagementService.ts b/src/sql/workbench/services/accountManagement/browser/accountManagementService.ts index 954ec84387..1de52112b3 100644 --- a/src/sql/workbench/services/accountManagement/browser/accountManagementService.ts +++ b/src/sql/workbench/services/accountManagement/browser/accountManagementService.ts @@ -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; @@ -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(); this._removeAccountProviderEmitter = new Emitter(); this._updateAccountListEmitter = new Emitter(); + 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); + } + } + } + })); + } } /** diff --git a/src/sql/workbench/services/accountManagement/test/browser/accountManagementService.test.ts b/src/sql/workbench/services/accountManagement/test/browser/accountManagementService.test.ts index 97f0843bcd..a9155ebba8 100644 --- a/src/sql/workbench/services/accountManagement/test/browser/accountManagementService.test.ts +++ b/src/sql/workbench/services/accountManagement/test/browser/accountManagementService.test.ts @@ -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(); diff --git a/src/sql/workbench/services/connection/browser/cmsConnectionWidget.ts b/src/sql/workbench/services/connection/browser/cmsConnectionWidget.ts index 248ddb59be..e0de861e26 100644 --- a/src/sql/workbench/services/connection/browser/cmsConnectionWidget.ts +++ b/src/sql/workbench/services/connection/browser/cmsConnectionWidget.ts @@ -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); diff --git a/src/sql/workbench/services/connection/browser/connectionManagementService.ts b/src/sql/workbench/services/connection/browser/connectionManagementService.ts index 07296b4910..dffbc894ca 100644 --- a/src/sql/workbench/services/connection/browser/connectionManagementService.ts +++ b/src/sql/workbench/services/connection/browser/connectionManagementService.ts @@ -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'; diff --git a/src/sql/workbench/services/connection/browser/connectionWidget.ts b/src/sql/workbench/services/connection/browser/connectionWidget.ts index faa790eae3..f7d386cf81 100644 --- a/src/sql/workbench/services/connection/browser/connectionWidget.ts +++ b/src/sql/workbench/services/connection/browser/connectionWidget.ts @@ -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 { 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,