diff --git a/extensions/azurecore/src/account-provider/azureAccountProviderService.ts b/extensions/azurecore/src/account-provider/azureAccountProviderService.ts index 0443d1de21..2a239794df 100644 --- a/extensions/azurecore/src/account-provider/azureAccountProviderService.ts +++ b/extensions/azurecore/src/account-provider/azureAccountProviderService.ts @@ -12,7 +12,7 @@ import { promises as fsPromises } from 'fs'; import { SimpleTokenCache } from './utils/simpleTokenCache'; import providerSettings from './providerSettings'; import { AzureAccountProvider as AzureAccountProvider } from './azureAccountProvider'; -import { AzureAccountProviderMetadata } from 'azurecore'; +import { AzureAccountProviderMetadata, CacheEncryptionKeys } from 'azurecore'; import { ProviderSettings } from './interfaces'; import { MsalCachePluginProvider } from './utils/msalCachePlugin'; import * as loc from '../localizedConstants'; @@ -41,10 +41,12 @@ export class AzureAccountProviderService implements vscode.Disposable { private _event: events.EventEmitter = new events.EventEmitter(); private readonly _uriEventHandler: UriEventHandler = new UriEventHandler(); public clientApplication!: PublicClientApplication; + private _onEncryptionKeysUpdated: vscode.EventEmitter; constructor(private _context: vscode.ExtensionContext, private _userStoragePath: string, private _authLibrary: string) { + this._onEncryptionKeysUpdated = new vscode.EventEmitter(); this._disposables.push(vscode.window.registerUriHandler(this._uriEventHandler)); } @@ -75,6 +77,10 @@ export class AzureAccountProviderService implements vscode.Disposable { }); } + public getEncryptionKeysEmitter(): vscode.EventEmitter { + return this._onEncryptionKeysUpdated; + } + public dispose() { while (this._disposables.length) { const item = this._disposables.pop(); @@ -155,10 +161,12 @@ export class AzureAccountProviderService implements vscode.Disposable { // ADAL Token Cache let simpleTokenCache = new SimpleTokenCache(tokenCacheKey, this._userStoragePath, noSystemKeychain, this._credentialProvider); - await simpleTokenCache.init(); + if (this._authLibrary === Constants.AuthLibrary.ADAL) { + await simpleTokenCache.init(); + } // MSAL Cache Plugin - this._cachePluginProvider = new MsalCachePluginProvider(tokenCacheKeyMsal, this._userStoragePath, this._credentialProvider); + this._cachePluginProvider = new MsalCachePluginProvider(tokenCacheKeyMsal, this._userStoragePath, this._credentialProvider, this._onEncryptionKeysUpdated); const msalConfiguration: Configuration = { auth: { diff --git a/extensions/azurecore/src/account-provider/utils/fileEncryptionHelper.ts b/extensions/azurecore/src/account-provider/utils/fileEncryptionHelper.ts index 881afb4ea2..95554e675d 100644 --- a/extensions/azurecore/src/account-provider/utils/fileEncryptionHelper.ts +++ b/extensions/azurecore/src/account-provider/utils/fileEncryptionHelper.ts @@ -9,12 +9,14 @@ import * as vscode from 'vscode'; import { AuthLibrary } from '../../constants'; import * as LocalizedConstants from '../../localizedConstants'; import { Logger } from '../../utils/Logger'; +import { CacheEncryptionKeys } from 'azurecore'; export class FileEncryptionHelper { constructor( private readonly _authLibrary: AuthLibrary, private readonly _credentialService: azdata.CredentialProvider, - protected readonly _fileName: string + protected readonly _fileName: string, + private readonly _onEncryptionKeysUpdated?: vscode.EventEmitter ) { this._algorithm = this._authLibrary === AuthLibrary.MSAL ? 'aes-256-cbc' : 'aes-256-gcm'; this._bufferEncoding = this._authLibrary === AuthLibrary.MSAL ? 'utf16le' : 'hex'; @@ -48,6 +50,14 @@ export class FileEncryptionHelper { this._ivBuffer = Buffer.from(iv, this._bufferEncoding); this._keyBuffer = Buffer.from(key, this._bufferEncoding); } + + // Emit event with cache encryption keys to send notification to provider services. + if (this._authLibrary === AuthLibrary.MSAL && this._onEncryptionKeysUpdated) { + this._onEncryptionKeysUpdated.fire({ + iv: this._ivBuffer.toString(this._bufferEncoding), + key: this._keyBuffer.toString(this._bufferEncoding) + }); + } } fileSaver = async (content: string): Promise => { diff --git a/extensions/azurecore/src/account-provider/utils/msalCachePlugin.ts b/extensions/azurecore/src/account-provider/utils/msalCachePlugin.ts index 13f5c212e8..1e1642a6d0 100644 --- a/extensions/azurecore/src/account-provider/utils/msalCachePlugin.ts +++ b/extensions/azurecore/src/account-provider/utils/msalCachePlugin.ts @@ -9,18 +9,21 @@ import { promises as fsPromises } from 'fs'; import * as lockFile from 'lockfile'; import * as path from 'path'; import * as azdata from 'azdata'; +import * as vscode from 'vscode'; import { AccountsClearTokenCacheCommand, AuthLibrary } from '../../constants'; import { Logger } from '../../utils/Logger'; import { FileEncryptionHelper } from './fileEncryptionHelper'; +import { CacheEncryptionKeys } from 'azurecore'; export class MsalCachePluginProvider { constructor( private readonly _serviceName: string, private readonly _msalFilePath: string, - private readonly _credentialService: azdata.CredentialProvider + private readonly _credentialService: azdata.CredentialProvider, + private readonly _onEncryptionKeysUpdated: vscode.EventEmitter ) { this._msalFilePath = path.join(this._msalFilePath, this._serviceName); - this._fileEncryptionHelper = new FileEncryptionHelper(AuthLibrary.MSAL, this._credentialService, this._serviceName); + this._fileEncryptionHelper = new FileEncryptionHelper(AuthLibrary.MSAL, this._credentialService, this._serviceName, this._onEncryptionKeysUpdated); } private _lockTaken: boolean = false; diff --git a/extensions/azurecore/src/azurecore.d.ts b/extensions/azurecore/src/azurecore.d.ts index e3e4565be5..3abc1dab9f 100644 --- a/extensions/azurecore/src/azurecore.d.ts +++ b/extensions/azurecore/src/azurecore.d.ts @@ -5,7 +5,7 @@ declare module 'azurecore' { import * as azdata from 'azdata'; - import { TreeDataProvider } from 'vscode'; + import * as vscode from 'vscode'; import { BlobItem } from '@azure/storage-blob'; /** @@ -314,8 +314,13 @@ declare module 'azurecore' { getRegionDisplayName(region?: string): string; getProviderMetadataForAccount(account: AzureAccount): AzureAccountProviderMetadata; provideResources(): azureResource.IAzureResourceProvider[]; - runGraphQuery(account: AzureAccount, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors: boolean, query: string): Promise>; + /** + * Event emitted when MSAL cache encryption keys are updated in credential store. + * Returns encryption keys used for encryption/decryption of MSAL cache that can be used + * by connection providers to read/write to the same access token cache for stable connectivity. + */ + onEncryptionKeysUpdated: vscode.Event; } export type GetSubscriptionsResult = { subscriptions: azureResource.AzureResourceSubscription[], errors: Error[] }; @@ -333,6 +338,7 @@ declare module 'azurecore' { export type AzureRestResponse = { response: any, errors: Error[] }; export type GetBlobsResult = { blobs: azureResource.Blob[], errors: Error[] }; export type GetStorageAccountAccessKeyResult = { keyName1: string, keyName2: string, errors: Error[] }; + export type CacheEncryptionKeys = { key: string; iv: string; } export namespace azureResource { diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts index c3f24584e7..171288290e 100644 --- a/extensions/azurecore/src/extension.ts +++ b/extensions/azurecore/src/extension.ts @@ -99,38 +99,43 @@ export async function activate(context: vscode.ExtensionContext): Promise; // Create the provider service and activate - initAzureAccountProvider(extensionContext, storagePath, authLibrary!).catch((err) => Logger.error(err)); - - registerAzureServices(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, 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; - const resourceGroup = item.resourceGroup; - const type = item.type; - const name = item.name; - if (portalEndpoint && subscriptionId && resourceGroup && type && name) { - await vscode.env.openExternal(vscode.Uri.parse(`${portalEndpoint}/#resource/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${type}/${name}`)); - } else { - Logger.error(`Missing required values - subscriptionId : ${subscriptionId} resourceGroup : ${resourceGroup} type: ${type} name: ${name}`); - void vscode.window.showErrorMessage(loc.unableToOpenAzureLink); - } - }); + let providerService = await initAzureAccountProvider(extensionContext, storagePath, authLibrary!).catch((err) => Logger.error(err)); + if (providerService) { + eventEmitter = providerService.getEncryptionKeysEmitter(); + registerAzureServices(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, 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; + const resourceGroup = item.resourceGroup; + const type = item.type; + const name = item.name; + if (portalEndpoint && subscriptionId && resourceGroup && type && name) { + await vscode.env.openExternal(vscode.Uri.parse(`${portalEndpoint}/#resource/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${type}/${name}`)); + } else { + Logger.error(`Missing required values - subscriptionId : ${subscriptionId} resourceGroup : ${resourceGroup} type: ${type} name: ${name}`); + void vscode.window.showErrorMessage(loc.unableToOpenAzureLink); + } + }); + } return { getSubscriptions(account?: azurecore.AzureAccount, ignoreErrors?: boolean, selectedOnly: boolean = false): Promise { return selectedOnly ? azureResourceUtils.getSelectedSubscriptions(appContext, account, ignoreErrors) : azureResourceUtils.getSubscriptions(appContext, account, ignoreErrors); }, - getResourceGroups(account?: azurecore.AzureAccount, subscription?: azurecore.azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); }, + getResourceGroups(account?: azurecore.AzureAccount, subscription?: azurecore.azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise { + return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); + }, getLocations(account?: azurecore.AzureAccount, subscription?: azurecore.azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise { @@ -235,7 +240,8 @@ export async function activate(context: vscode.ExtensionContext): Promise> { return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, query); - } + }, + onEncryptionKeysUpdated: eventEmitter!.event }; } @@ -267,13 +273,15 @@ async function findOrMakeStoragePath() { return storagePath; } -async function initAzureAccountProvider(extensionContext: vscode.ExtensionContext, storagePath: string, authLibrary: string): Promise { +async function initAzureAccountProvider(extensionContext: vscode.ExtensionContext, storagePath: string, authLibrary: string): Promise { try { const accountProviderService = new AzureAccountProviderService(extensionContext, storagePath, authLibrary); extensionContext.subscriptions.push(accountProviderService); await accountProviderService.activate(); + return accountProviderService; } catch (err) { Logger.error('Unexpected error starting account provider: ' + err.message); + return undefined; } } diff --git a/extensions/machine-learning/src/test/stubs.ts b/extensions/machine-learning/src/test/stubs.ts index cea74d97d7..8e7d3b4a7c 100644 --- a/extensions/machine-learning/src/test/stubs.ts +++ b/extensions/machine-learning/src/test/stubs.ts @@ -61,5 +61,6 @@ export class AzurecoreApiStub implements azurecore.IExtension { provideResources(): azurecore.azureResource.IAzureResourceProvider[] { throw new Error('Method not implemented.'); } + onEncryptionKeysUpdated: any } diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 5299d17357..9b60ec5fd0 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -1604,3 +1604,25 @@ export namespace DropObjectRequest { export const type = new RequestType('objectManagement/drop'); } // ------------------------------- < Object Management > ------------------------------------ + +// ------------------------------- < Encryption IV/KEY updation Event > ------------------------------------ +/** + * Parameters for the MSAL cache encryption key notification + */ +export class DidChangeEncryptionIVKeyParams { + /** + * Buffer encoded IV string for MSAL cache encryption + */ + public iv: string; + /** + * Buffer encoded Key string for MSAL cache encryption + */ + public key: string; +} + +/** + * Notification sent when the encryption keys are changed. + */ +export namespace EncryptionKeysChangedNotification { + export const type = new NotificationType('connection/encryptionKeysChanged'); +} diff --git a/extensions/mssql/src/sqlToolsServer.ts b/extensions/mssql/src/sqlToolsServer.ts index 1b83127969..8683fbd1c0 100644 --- a/extensions/mssql/src/sqlToolsServer.ts +++ b/extensions/mssql/src/sqlToolsServer.ts @@ -9,6 +9,7 @@ import * as Constants from './constants'; import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as path from 'path'; +import * as azurecore from 'azurecore'; import { getAzureAuthenticationLibraryConfig, getCommonLaunchArgsAndCleanupOldLogFiles, getConfigTracingLevel, getEnableSqlAuthenticationProviderConfig, getOrDownloadServer, getParallelMessageProcessingConfig, TracingLevel } from './utils'; import { TelemetryReporter, LanguageClientErrorHandler } from './telemetry'; import { SqlOpsDataClient, ClientOptions } from 'dataprotocol-client'; @@ -19,7 +20,7 @@ import { SchemaCompareService } from './schemaCompare/schemaCompareService'; import { AppContext } from './appContext'; import { DacFxService } from './dacfx/dacFxService'; import { CmsService } from './cms/cmsService'; -import { CompletionExtensionParams, CompletionExtLoadRequest } from './contracts'; +import { CompletionExtensionParams, CompletionExtLoadRequest, DidChangeEncryptionIVKeyParams, EncryptionKeysChangedNotification } from './contracts'; import { promises as fs } from 'fs'; import * as nls from 'vscode-nls'; import { LanguageExtensionService } from './languageExtension/languageExtensionService'; @@ -82,6 +83,7 @@ export class SqlToolsServer { statusView.text = localize('startingServiceStatusMsg', "Starting {0}", Constants.serviceName); this.client.start(); await Promise.all([this.activateFeatures(context), clientReadyPromise]); + await this.handleEncryptionKeyEventNotification(this.client); return this.client; } catch (e) { TelemetryReporter.sendTelemetryEvent('ServiceInitializingFailed'); @@ -90,6 +92,35 @@ export class SqlToolsServer { } } + /** + * This is a hop notification handler to send Encryption Key and Iv information from Azure Core extension to backend + * SqlToolsService. This notification is needed for Azure authentication flows to be able to read/write into + * shared MSAL cache. + * @param client SqlOpsDataClient instance + */ + private async handleEncryptionKeyEventNotification(client: SqlOpsDataClient) { + if (getAzureAuthenticationLibraryConfig() === 'MSAL' && getEnableSqlAuthenticationProviderConfig()) { + let onDidEncryptionKeysChanged = (await this.getAzureCoreAPI()).onEncryptionKeysUpdated; + // Register event listener from Azure Core extension + onDidEncryptionKeysChanged((keys: azurecore.CacheEncryptionKeys) => { + // Send client notification for updated encryption keys + client.sendNotification(EncryptionKeysChangedNotification.type, + { + key: keys.key, + iv: keys.iv + }); + }); + } + } + + private async getAzureCoreAPI(): Promise { + const api = (await vscode.extensions.getExtension(azurecore.extension.name)?.activate()) as azurecore.IExtension; + if (!api) { + throw new Error('Azure core extension could not be activated.'); + } + return api; + } + private async download(context: AppContext): Promise { const configDir = context.extensionContext.extensionPath; const rawConfig = await fs.readFile(path.join(configDir, 'config.json'));