MSAL Authentication support + code cleanup (#17562)

This commit is contained in:
Cheena Malhotra 2023-03-02 17:53:36 -08:00 коммит произвёл GitHub
Родитель a7df781375
Коммит 99b91117c3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
66 изменённых файлов: 3447 добавлений и 974 удалений

Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "@microsoft/ads-adal-library",
"version": "1.0.16",
"version": "1.0.17",
"description": "ADAL NodeJS authentication library",
"main": "dist/index.js",
"repository": "git://github.com/microsoft/vscode-mssql.git",

Просмотреть файл

@ -2,13 +2,13 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import { ProviderSettings, SecureStorageProvider, Tenant, AADResource, LoginResponse, Deferred, AzureAccount, Logger, MessageDisplayer, ErrorLookup, CachingProvider, RefreshTokenPostData, AuthorizationCodePostData, TokenPostData, AccountKey, StringLookup, DeviceCodeStartPostData, DeviceCodeCheckPostData, AzureAuthType, AccountType, UserInteraction } from '../models';
import { AzureAuthError } from '../errors/AzureAuthError';
import { ErrorCodes } from '../errors/errors';
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { AccessToken, Token, TokenClaims, RefreshToken, OAuthTokenResponse } from '../models/auth';
import * as qs from 'qs';
import * as url from 'url';
import { AzureAuthError } from '../errors/AzureAuthError';
import { ErrorCodes } from '../errors/errors';
import { AADResource, AccountKey, AccountType, AuthorizationCodePostData, AzureAccount, AzureAuthType, CachingProvider, Deferred, DeviceCodeCheckPostData, DeviceCodeStartPostData, ErrorLookup, Logger, LoginResponse, MessageDisplayer, ProviderSettings, RefreshTokenPostData, SecureStorageProvider, StringLookup, Tenant, TokenPostData, UserInteraction } from '../models';
import { AccessToken, OAuthTokenResponse, RefreshToken, Token, TokenClaims } from '../models/auth';
export abstract class AzureAuth {
public static readonly ACCOUNT_VERSION = '2.0';
@ -60,15 +60,11 @@ export abstract class AzureAuth {
}
public getHomeTenant(account: AzureAccount): Tenant {
// Home is defined by the API
// Lets pick the home tenant - and fall back to commonTenant if they don't exist
return account.properties.tenants.find(t => t.tenantCategory === 'Home') ?? account.properties.tenants[0] ?? this.commonTenant;
return account.properties.owningTenant ?? this.commonTenant;
}
public async refreshAccess(account: AzureAccount): Promise<AzureAccount> {
// Deprecated account - delete it.
if (account.key.accountVersion !== AzureAuth.ACCOUNT_VERSION) {
account.delete = true;
return account;
}
try {
@ -339,13 +335,13 @@ export abstract class AzureAuth {
throw new AzureAuthError(ErrorCodes.GetAccount, this.errorLookup.getSimpleError(ErrorCodes.GetAccount));
}
let accessTokenString: string;
let refreshTokenString: string;
let accessTokenString: string | undefined | null;
let refreshTokenString: string | undefined | null;
let expiresOn: string;
try {
accessTokenString = await this.cachingProvider.get(`${accountKey.id}_access_${resource.id}_${tenant.id}`);
refreshTokenString = await this.cachingProvider.get(`${accountKey.id}_refresh_${resource.id}_${tenant.id}`);
expiresOn = await this.cachingProvider.get(`${accountKey.id}_${tenant.id}_${resource.id}`);
expiresOn = await this.cachingProvider.get(`${accountKey.id}_${tenant.id}_${resource.id}`) ?? '';
} catch (ex) {
this.logger.error(ex);
// Error when getting your account from the cache
@ -377,7 +373,7 @@ export abstract class AzureAuth {
public async deleteAccountCache(accountKey: AccountKey): Promise<void> {
const results = await this.cachingProvider.findCredentials(accountKey.id);
for (let { account } of results) {
for (let { account } of results!) {
await this.cachingProvider.remove(account);
}
}
@ -420,6 +416,7 @@ export abstract class AzureAuth {
const name = tokenClaims.name ?? tokenClaims.email ?? tokenClaims.unique_name;
const email = tokenClaims.email ?? tokenClaims.unique_name;
const owningTenant = tenants.find(t => t.id === tokenClaims.tid) ?? { 'id': tokenClaims.tid, 'displayName': 'Microsoft Account' };
let displayName = name;
if (email) {
@ -443,6 +440,7 @@ export abstract class AzureAuth {
providerSettings: this.providerSettings,
isMsAccount: accountType === AccountType.Microsoft,
tenants,
owningTenant: owningTenant,
azureAuthType: this.azureAuthType
},
isStale: false

Просмотреть файл

@ -25,7 +25,7 @@ export class AzureDeviceCode extends AzureAuth {
protected async login(tenant: Tenant, resource: AADResource): Promise<LoginResponse> {
let authCompleteDeferred: Deferred<void>;
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
const uri = `${this.loginEndpointUrl}/${this.commonTenant.id}/oauth2/devicecode`;
const uri = `${this.loginEndpointUrl}/${tenant.id}/oauth2/devicecode`;
const postData: DeviceCodeStartPostData = {
client_id: this.clientId,

Просмотреть файл

@ -10,7 +10,6 @@ export interface AzureAccount {
properties: AzureAccountProperties;
isStale: boolean;
isSignedIn?: boolean;
delete?: boolean;
}
export interface AccountKey {
@ -56,6 +55,7 @@ interface AzureAccountProperties {
*/
isMsAccount: boolean;
owningTenant: Tenant;
/**
* A list of tenants (aka directories) that the account belongs to
*/

Просмотреть файл

@ -4,17 +4,22 @@
* ------------------------------------------------------------------------------------------ */
interface KVProvider {
set(key: string, value: string): Promise<void>;
get(key: string): Promise<string>;
get(key: string): Promise<string | undefined | null>;
clear(): Promise<void>;
remove(key: string): Promise<boolean>;
}
// used for token storage
export interface SecureStorageProvider extends KVProvider {
}
// used for various caching
export interface CachingProvider extends KVProvider {
findCredentials(key: string): Promise<{ account: string; password: string; }[]>;
}
findCredentials(key: string): Promise<{
account: string;
password: string;
}[] | undefined>;
}
export { };

Просмотреть файл

@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
export interface ProviderSettings {
scopes: string[];
displayName: string;
id: string;
clientId: string;

Просмотреть файл

@ -128,6 +128,36 @@
<trans-unit id="azureLogChannelName">
<source xml:lang="en">Azure Logs</source>
</trans-unit>
<trans-unit id="azureConsentDialogOpen">
<source xml:lang="en">Open</source>
</trans-unit>
<trans-unit id="azureConsentDialogCancel">
<source xml:lang="en">Cancel</source>
</trans-unit>
<trans-unit id="azureConsentDialogIgnore">
<source xml:lang="en">Ignore Tenant</source>
</trans-unit>
<trans-unit id="azureConsentDialogBody">
<source xml:lang="en">Your tenant '{0} ({1})' requires you to re-authenticate again to access {2} resources. Press Open to start the authentication process.</source>
</trans-unit>
<trans-unit id="azureMicrosoftCorpAccount">
<source xml:lang="en">Microsoft Corp</source>
</trans-unit>
<trans-unit id="azureMicrosoftAccount">
<source xml:lang="en">Microsoft Account</source>
</trans-unit>
<trans-unit id="azureNoMicrosoftResource">
<source xml:lang="en">Provider '{0}' does not have a Microsoft resource endpoint defined.</source>
</trans-unit>
<trans-unit id="azureServerCouldNotStart">
<source xml:lang="en">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.</source>
</trans-unit>
<trans-unit id="azureAuthNonceError">
<source xml:lang="en">Authentication failed due to a nonce mismatch, please close Azure Data Studio and try again.</source>
</trans-unit>
<trans-unit id="azureAuthStateError">
<source xml:lang="en">Authentication failed due to a state mismatch, please close ADS and try again.</source>
</trans-unit>
<trans-unit id="encryptPrompt">
<source xml:lang="en">Encrypt</source>
</trans-unit>
@ -161,6 +191,21 @@
<trans-unit id="azureAddAccount">
<source xml:lang="en">Add an Account...</source>
</trans-unit>
<trans-unit id="accountAddedSuccessfully">
<source xml:lang="en">Azure account {0} successfully added.</source>
</trans-unit>
<trans-unit id="accountCouldNotBeAdded">
<source xml:lang="en">New Azure account could not be added.</source>
</trans-unit>
<trans-unit id="accountRemovedSuccessfully">
<source xml:lang="en">Selected Azure Account removed successfully.</source>
</trans-unit>
<trans-unit id="accountRemovalFailed">
<source xml:lang="en">An error occurred while removing user account: {0}</source>
</trans-unit>
<trans-unit id="noAzureAccountForRemoval">
<source xml:land="en">No Azure Account can be found for removal.</source>
</trans-unit>
<trans-unit id="cannotConnect">
<source xml:lang="en">Cannot connect due to expired tokens. Please re-authenticate and try again.</source>
</trans-unit>
@ -309,7 +354,7 @@
<source xml:lang="en">Firewall rule successfully added. Retry profile creation? </source>
</trans-unit>
<trans-unit id="msgAccountRefreshFailed">
<source xml:lang="en">Credential Error: Account credentials have expired. Please re-authenticate.</source>
<source xml:lang="en">Credential Error: An error occurred while attempting to refresh account credentials. Please re-authenticate.</source>
</trans-unit>
<trans-unit id="msgPromptProfileUpdateFailed">
<source xml:lang="en">Connection Profile could not be updated. Please modify the connection details manually in settings.json and try again.</source>
@ -596,6 +641,12 @@
<trans-unit id="showOutputChannelActionButtonText">
<source xml:lang="en">Show MSSQL output</source>
</trans-unit>
<trans-unit id="reloadPrompt">
<source xml:lang="en">Authentication Library has changed, please reload Visual Studio Code.</source>
</trans-unit>
<trans-unit id="reloadChoice">
<source xml:lang="en">Reload Visual Studio Code</source>
</trans-unit>
</body>
</file>
</xliff>

Просмотреть файл

@ -95,6 +95,9 @@
<trans-unit id="mssql.copyObjectName">
<source xml:lang="en">Copy Object Name</source>
</trans-unit>
<trans-unit id="mssql.addAadAccount">
<source xml:lang="en">Add Azure Account</source>
</trans-unit>
<trans-unit id="mssql.removeAadAccount">
<source xml:lang="en">Remove Azure Account</source>
</trans-unit>
@ -209,6 +212,12 @@
<trans-unit id="mssql.connection.emptyPasswordInput">
<source xml:lang="en">[Optional] Indicates whether this profile has an empty password explicitly set</source>
</trans-unit>
<trans-unit id="mssql.azureAuthenticationLibrary">
<source xml:lang="en">The library used for the Azure Active Directory authentication flow. Please restart Visual Studio Code after changing this option.</source>
</trans-unit>
<trans-unit id="mssql.enableSqlAuthenticationProvider">
<source xml:lang="en">Enables use of the Sql Authentication Provider for 'Active Directory Interactive' authentication mode when user selects 'AzureMFA' authentication. This enables Server-side resource endpoint integration when fetching access tokens. This option is only supported for 'MSAL' Azure Authentication Library. Please restart Visual Studio Code after changing this option.</source>
</trans-unit>
<trans-unit id="mssql.shortcuts">
<source xml:lang="en">Shortcuts related to the results window</source>
</trans-unit>
@ -302,7 +311,7 @@
<trans-unit id="mssql.tracingLevel">
<source xml:lang="en">[Optional] Log level for backend services. Azure Data Studio generates a file name every time it starts and if the file already exists the logs entries are appended to that file. For cleanup of old log files see logRetentionMinutes and logFilesRemovalLimit settings. The default tracingLevel does not log much. Changing verbosity could lead to extensive logging and disk space requirements for the logs. Error includes Critical, Warning includes Error, Information includes Warning and Verbose includes Information</source>
</trans-unit>
<trans-unit id="mssql.piiLogging">
<trans-unit id="mssql.piiLogging">
<source xml:lang="en">Should Personally Identifiable Information (PII) be logged in the Azure Logs output channel and the output channel log file.</source>
</trans-unit>
<trans-unit id="mssql.logRetentionMinutes">

Просмотреть файл

@ -36,23 +36,7 @@
"multi-root ready"
],
"activationEvents": [
"onView:objectExplorer",
"onLanguage:sql",
"onCommand:mssql.connect",
"onCommand:mssql.runQuery",
"onCommand:mssql.runCurrentStatement",
"onCommand:mssql.disconnect",
"onCommand:mssql.manageProfiles",
"onCommand:mssql.chooseDatabase",
"onCommand:mssql.cancelQuery",
"onCommand:mssql.showGettingStarted",
"onCommand:mssql.newQuery",
"onCommand:mssql.rebuildIntelliSenseCache",
"onCommand:mssql.addObjectExplorer",
"onCommand:mssql.objectExplorerNewQuery",
"onCommand:mssql.toggleSqlCmd",
"onCommand:mssql.loadCompletionExtension",
"onCommand:mssql.removeAadAccount"
"onCommand:mssql.loadCompletionExtension"
],
"main": "./out/src/extension",
"extensionDependencies": [
@ -81,8 +65,10 @@
"@types/jquery": "^3.3.31",
"@types/jqueryui": "^1.12.7",
"@types/keytar": "^4.4.2",
"@types/lockfile": "^1.0.2",
"@types/mocha": "^5.2.7",
"@types/node": "^14.14.16",
"@types/node": "^14.17.0",
"@types/node-fetch": "^2.6.1",
"@types/sinon": "^10.0.12",
"@types/tmp": "0.0.28",
"@types/underscore": "1.8.3",
@ -127,7 +113,11 @@
"@azure/arm-resources": "^5.0.0",
"@azure/arm-sql": "^9.0.0",
"@azure/arm-subscriptions": "^5.0.0",
"@azure/msal-common": "^10.0.0",
"@azure/msal-node": "^1.15.0",
"@microsoft/ads-adal-library": "1.0.16",
"http-proxy-agent": "5.0.0",
"https-proxy-agent": "5.0.0",
"core-js": "^2.4.1",
"decompress-zip": "^0.2.2",
"ejs": "^3.1.7",
@ -135,12 +125,14 @@
"figures": "^1.4.0",
"find-remove": "1.2.1",
"getmac": "1.2.1",
"http-proxy-agent": "^2.1.0",
"https-proxy-agent": "^2.2.1",
"jquery": "^3.4.1",
"lockfile": "1.0.4",
"msal": "^1.4.17",
"node-fetch": "^2.6.1",
"opener": "1.4.2",
"plist": "^3.0.5",
"pretty-data": "^0.40.0",
"qs": "^6.9.1",
"rangy": "^1.3.0",
"reflect-metadata": "0.1.12",
"semver": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz",
@ -556,6 +548,11 @@
"title": "%mssql.copyObjectName%",
"category": "MS SQL"
},
{
"command": "mssql.addAadAccount",
"title": "%mssql.addAadAccount%",
"category": "MS SQL"
},
{
"command": "mssql.removeAadAccount",
"title": "%mssql.removeAadAccount%",
@ -905,6 +902,24 @@
],
"scope": "resource"
},
"mssql.azureAuthenticationLibrary": {
"type": "string",
"description": "%mssql.azureAuthenticationLibrary%",
"default": "MSAL",
"enum": [
"ADAL",
"MSAL"
],
"enumDescriptions": [
"(deprecated) Azure Active Directory Authentication Library",
"Microsoft Authentication Library"
]
},
"mssql.enableSqlAuthenticationProvider": {
"type": "boolean",
"description": "%mssql.enableSqlAuthenticationProvider%",
"default": true
},
"mssql.format.alignColumnDefinitionsInColumns": {
"type": "boolean",
"description": "%mssql.format.alignColumnDefinitionsInColumns%",

Просмотреть файл

@ -30,6 +30,7 @@
"mssql.objectExplorerNewQuery":"New Query",
"mssql.toggleSqlCmd":"Toggle SQLCMD Mode",
"mssql.copyObjectName":"Copy Object Name",
"mssql.addAadAccount":"Add Azure Account",
"mssql.removeAadAccount":"Remove Azure Account",
"mssql.rebuildIntelliSenseCache":"Refresh IntelliSense Cache",
"mssql.logDebugInfo":"[Optional] Log debug output to the VS Code console (Help -> Toggle Developer Tools)",
@ -68,6 +69,8 @@
"mssql.connection.profileName":"[Optional] Specify a custom name for this connection profile to easily browse and search in the command palette of Visual Studio Code.",
"mssql.connection.savePassword":"[Optional] When set to 'true', the password for SQL Server authentication is saved in the secure store of your operating system such as KeyChain in MacOS or Secure Store in Windows.",
"mssql.connection.emptyPasswordInput":"[Optional] Indicates whether this profile has an empty password explicitly set",
"mssql.azureAuthenticationLibrary":"The library used for the Azure Active Directory authentication flow. Please restart Visual Studio Code after changing this option.",
"mssql.enableSqlAuthenticationProvider":"Enables use of the Sql Authentication Provider for 'Active Directory Interactive' authentication mode when user selects 'AzureMFA' authentication. This enables Server-side resource endpoint integration when fetching access tokens. This option is only supported for 'MSAL' Azure Authentication Library. Please restart Visual Studio Code after changing this option.",
"mssql.shortcuts":"Shortcuts related to the results window",
"mssql.messagesDefaultOpen":"True for the messages pane to be open by default; false for closed",
"mssql.resultsFontFamily":"Set the font family for the results grid; set to blank to use the editor font",

Просмотреть файл

@ -3,20 +3,19 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import { IAccount, IAccountKey } from 'vscode-mssql';
import SqlToolsServiceClient from '../languageservice/serviceclient';
import { IAzureSession } from '../models/interfaces';
import * as Constants from '../constants/constants';
import { AzureController } from './azureController';
import { AccountStore } from './accountStore';
import providerSettings from '../azure/providerSettings';
import { Tenant, Token } from '@microsoft/ads-adal-library';
import { AzureAuthType, IAccount, IAccountKey, ITenant, IToken } from '../models/contracts/azure';
export class AccountService {
private _account: IAccount = undefined;
private _isStale: boolean;
protected readonly commonTenant: Tenant = {
protected readonly commonTenant: ITenant = {
id: 'common',
displayName: 'common'
};
@ -58,7 +57,11 @@ export class AccountService {
name: undefined
},
properties: {
tenants: [tenant]
tenants: [tenant],
owningTenant: tenant,
azureAuthType: AzureAuthType.AuthCodeGrant,
providerSettings: providerSettings,
isMsAccount: false
},
isStale: this._isStale,
isSignedIn: false
@ -75,15 +78,13 @@ export class AccountService {
return mapping;
}
public async refreshToken(account): Promise<Token> {
return await this._azureController.refreshToken(account, this._accountStore, providerSettings.resources.azureManagementResource);
public async refreshToken(account: IAccount): Promise<IToken> {
return await this._azureController.refreshAccessToken(account, this._accountStore, undefined, providerSettings.resources.azureManagementResource);
}
public getHomeTenant(account: IAccount): Tenant {
public getHomeTenant(account: IAccount): ITenant {
// Home is defined by the API
// Lets pick the home tenant - and fall back to commonTenant if they don't exist
return account.properties.tenants.find(t => t.tenantCategory === 'Home') ?? account.properties.tenants[0] ?? this.commonTenant;
}
}

Просмотреть файл

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { IAccount } from 'vscode-mssql';
import { IAccount } from '../models/contracts/azure';
import * as Constants from '../constants/constants';
import { Logger } from '../models/logger';
@ -21,21 +21,18 @@ export class AccountStore {
}
public getAccount(key: string): IAccount | undefined {
let account: IAccount;
let account: IAccount | undefined;
let configValues = this._context.globalState.get<IAccount[]>(Constants.configAzureAccount);
if (!configValues) {
throw new Error('No Azure accounts stored');
}
for (let value of configValues) {
if (value.key.id === key) {
// Compare account IDs considering multi-tenant account ID format with MSAL.
if (value.key.id === key || value.key.id.startsWith(key) || key.startsWith(value.key.id)) {
account = value;
break;
}
}
if (!account) {
// Throw error message saying the account was not found
return undefined;
}
return account;
}

Просмотреть файл

@ -0,0 +1,250 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as Constants from '../../constants/constants';
import * as LocalizedConstants from '../../constants/localizedConstants';
import * as azureUtils from '../utils';
import { Subscription } from '@azure/arm-subscriptions';
import { AzureAuth, AzureCodeGrant, AzureDeviceCode, CachingProvider } from '@microsoft/ads-adal-library';
import { IAzureAccountSession } from 'vscode-mssql';
import providerSettings from '../../azure/providerSettings';
import { ConnectionProfile } from '../../models/connectionProfile';
import { AzureAuthType, IAADResource, IAccount, ITenant, IToken } from '../../models/contracts/azure';
import { AccountStore } from '../accountStore';
import { AzureController } from '../azureController';
import { getAzureActiveDirectoryConfig } from '../utils';
import { SimpleTokenCache } from './adalCacheService';
import { AzureAuthRequest } from './azureAuthRequest';
import { AzureErrorLookup } from './azureErrorLookup';
import { AzureMessageDisplayer } from './azureMessageDisplayer';
import { AzureStringLookup } from './azureStringLookup';
import { AzureUserInteraction } from './azureUserInteraction';
import { StorageService } from './storageService';
export class AdalAzureController extends AzureController {
private _authMappings = new Map<AzureAuthType, AzureAuth>();
private cacheProvider: SimpleTokenCache;
private storageService: StorageService;
private authRequest: AzureAuthRequest;
private azureStringLookup: AzureStringLookup;
private azureUserInteraction: AzureUserInteraction;
private azureErrorLookup: AzureErrorLookup;
private azureMessageDisplayer: AzureMessageDisplayer;
public init(): void {
this.azureStringLookup = new AzureStringLookup();
this.azureErrorLookup = new AzureErrorLookup();
this.azureMessageDisplayer = new AzureMessageDisplayer();
}
public async login(authType: AzureAuthType): Promise<IAccount | undefined> {
let azureAuth = await this.getAzureAuthInstance(authType);
let response = await azureAuth!.startLogin();
return response ? response as IAccount : undefined;
}
public async getAccountSecurityToken(account: IAccount, tenantId: string, settings: IAADResource): Promise<IToken | undefined> {
let token: IToken | undefined;
let azureAuth = await this.getAzureAuthInstance(getAzureActiveDirectoryConfig());
tenantId = tenantId ? tenantId : azureAuth!.getHomeTenant(account).id;
token = await azureAuth!.getAccountSecurityToken(
account, tenantId, settings
);
return token;
}
public async refreshAccessToken(account: IAccount, accountStore: AccountStore, tenantId: string | undefined, settings: IAADResource)
: Promise<IToken | undefined> {
try {
let token: IToken | undefined;
let azureAuth = await this.getAzureAuthInstance(getAzureActiveDirectoryConfig());
let newAccount = await azureAuth!.refreshAccess(account);
if (newAccount.isStale === true) {
return undefined;
}
await accountStore.addAccount(newAccount as IAccount);
token = await this.getAccountSecurityToken(
account, tenantId!, settings
);
return token;
} catch (ex) {
let errorMsg = this.azureErrorLookup.getSimpleError(ex.errorCode);
this._vscodeWrapper.showErrorMessage(errorMsg);
}
}
/**
* Gets the token for given account and updates the connection profile with token information needed for AAD authentication
*/
public async populateAccountProperties(profile: ConnectionProfile, accountStore: AccountStore, settings: IAADResource): Promise<ConnectionProfile> {
let account = await this.addAccount(accountStore);
profile.user = account!.displayInfo.displayName;
profile.email = account!.displayInfo.email;
profile.accountId = account!.key.id;
if (!profile.tenantId) {
await this.promptForTenantChoice(account!, profile);
}
const token = await this.getAccountSecurityToken(
account!, profile.tenantId, settings
);
if (!token) {
let errorMessage = LocalizedConstants.msgGetTokenFail;
this.logger.error(errorMessage);
this._vscodeWrapper.showErrorMessage(errorMessage);
} else {
profile.azureAccountToken = token.token;
profile.expiresOn = token.expiresOn;
}
return profile;
}
public async refreshTokenWrapper(profile, accountStore: AccountStore, accountAnswer, settings: IAADResource): Promise<ConnectionProfile | undefined> {
let account = accountStore.getAccount(accountAnswer.key.id);
if (!account) {
await this._vscodeWrapper.showErrorMessage(LocalizedConstants.msgAccountNotFound);
throw new Error(LocalizedConstants.msgAccountNotFound);
}
let azureAccountToken = await this.refreshToken(account, accountStore, settings, profile.tenantId);
if (!azureAccountToken) {
let errorMessage = LocalizedConstants.msgAccountRefreshFailed;
return this._vscodeWrapper.showErrorMessage(errorMessage, LocalizedConstants.refreshTokenLabel).then(async result => {
if (result === LocalizedConstants.refreshTokenLabel) {
let refreshedProfile = await this.populateAccountProperties(profile, accountStore, settings);
return refreshedProfile;
} else {
return undefined;
}
});
}
profile.azureAccountToken = azureAccountToken.token;
profile.expiresOn = azureAccountToken.expiresOn;
profile.user = account.displayInfo.displayName;
profile.email = account.displayInfo.email;
profile.accountId = account.key.id;
return profile;
}
public async refreshToken(account: IAccount, accountStore: AccountStore, settings: IAADResource, tenantId: string | undefined): Promise<IToken | undefined> {
try {
let token: IToken | undefined;
let azureAuth = await this.getAzureAuthInstance(getAzureActiveDirectoryConfig());
let newAccount = await azureAuth!.refreshAccess(account);
if (newAccount.isStale === true) {
return undefined;
}
await accountStore.addAccount(newAccount as IAccount);
token = await this.getAccountSecurityToken(
account, tenantId!, settings
);
return token;
} catch (ex) {
let errorMsg = this.azureErrorLookup.getSimpleError(ex.errorCode);
this._vscodeWrapper.showErrorMessage(errorMsg);
}
}
/**
* Returns Azure sessions with subscriptions, tenant and token for each given account
*/
public async getAccountSessions(account: IAccount): Promise<IAzureAccountSession[]> {
let sessions: IAzureAccountSession[] = [];
const tenants = <ITenant[]>account.properties.tenants;
for (const tenantId of tenants.map(t => t.id)) {
const token = await this.getAccountSecurityToken(account, tenantId, providerSettings.resources.azureManagementResource);
const subClient = this._subscriptionClientFactory(token!);
const newSubPages = subClient.subscriptions.list();
const array = await azureUtils.getAllValues<Subscription, IAzureAccountSession>(newSubPages, (nextSub) => {
return {
subscription: nextSub,
tenantId: tenantId,
account: account,
token: token
};
});
sessions = sessions.concat(array);
}
return sessions.sort((a, b) => (a.subscription.displayName || '').localeCompare(b.subscription.displayName || ''));
}
public async handleAuthMapping(): Promise<void> {
if (!this._credentialStoreInitialized) {
let storagePath = await this.findOrMakeStoragePath();
// ADAL Cache Service
this.cacheProvider = new SimpleTokenCache(Constants.adalCacheFileName, storagePath!);
await this.cacheProvider.init();
this.storageService = this.cacheProvider.db;
// MSAL Cache Provider
this._credentialStoreInitialized = true;
this.logger.verbose(`Credential store initialized.`);
this.authRequest = new AzureAuthRequest(this.context, this.logger);
await this.authRequest.startServer();
this.azureUserInteraction = new AzureUserInteraction(this.authRequest.getState());
}
this._authMappings.clear();
const configuration = getAzureActiveDirectoryConfig();
if (configuration === AzureAuthType.AuthCodeGrant) {
this._authMappings.set(AzureAuthType.AuthCodeGrant, new AzureCodeGrant(
providerSettings, this.storageService, this.cacheProvider as CachingProvider, this.logger,
this.azureMessageDisplayer, this.azureErrorLookup, this.azureUserInteraction,
this.azureStringLookup, this.authRequest
));
} else if (configuration === AzureAuthType.DeviceCode) {
this._authMappings.set(AzureAuthType.DeviceCode, new AzureDeviceCode(
providerSettings, this.storageService, this.cacheProvider as CachingProvider, this.logger,
this.azureMessageDisplayer, this.azureErrorLookup, this.azureUserInteraction,
this.azureStringLookup, this.authRequest
));
}
}
private async getAzureAuthInstance(authType: AzureAuthType): Promise<AzureAuth | undefined> {
if (!this._authMappings.has(authType)) {
await this.handleAuthMapping();
}
return this._authMappings.get(authType);
}
public async removeAccount(account: IAccount): Promise<void> {
let azureAuth = await this.getAzureAuthInstance(getAzureActiveDirectoryConfig());
await azureAuth!.deleteAccountCache(account.key);
this.logger.verbose(`Account deleted from cache successfully: ${account.key.id}`);
}
/**
* Returns true if token is invalid or expired
* @param token Token
* @param token expiry
*/
public static isTokenInValid(token: string, expiresOn?: number): boolean {
return (!token || this.isTokenExpired(expiresOn));
}
/**
* Returns true if token is expired
* @param token expiry
*/
public static isTokenExpired(expiresOn?: number): boolean {
if (!expiresOn) {
return true;
}
const currentTime = new Date().getTime() / 1000;
const maxTolerance = 2 * 60; // two minutes
return (expiresOn - currentTime < maxTolerance);
}
}

Просмотреть файл

@ -0,0 +1,133 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CachingProvider } from '@microsoft/ads-adal-library';
import * as keytarType from 'keytar';
import { join } from 'path';
import { StorageService } from './storageService';
export type MultipleAccountsResponse = { account: string, password: string }[];
// allow-any-unicode-next-line
const separator = '§';
async function getFileKeytar(db: StorageService): Promise<Keytar | undefined> {
const fileKeytar: Keytar = {
async getPassword(service: string, account: string): Promise<string> {
return db.get(`${service}${separator}${account}`);
},
async setPassword(service: string, account: string, password: string): Promise<void> {
await db.set(`${service}${separator}${account}`, password);
},
async deletePassword(service: string, account: string): Promise<boolean> {
await db.remove(`${service}${separator}${account}`);
return true;
},
async getPasswords(service: string): Promise<MultipleAccountsResponse> {
const result = db.getPrefix(`${service}`);
if (!result) {
return [];
}
return result.map(({ key, value }) => {
return {
account: key.split(separator)[1],
password: value
};
});
}
};
return fileKeytar;
}
export type Keytar = {
getPassword: typeof keytarType['getPassword'];
setPassword: typeof keytarType['setPassword'];
deletePassword: typeof keytarType['deletePassword'];
getPasswords: (service: string) => Promise<MultipleAccountsResponse>;
findCredentials?: typeof keytarType['findCredentials'];
};
export class SimpleTokenCache implements CachingProvider {
private keytar: Keytar | undefined;
public db: StorageService;
constructor(
private serviceName: string,
private readonly userStoragePath: string
) { }
// tslint:disable:no-empty
async clear(): Promise<void> { }
async init(): Promise<void> {
this.serviceName = this.serviceName.replace(/-/g, '_');
let filePath = join(this.userStoragePath, this.serviceName);
this.db = new StorageService(filePath);
await this.db.initialize();
this.keytar = await getFileKeytar(this.db);
}
async set(id: string, key: string): Promise<void> {
if (id.includes(separator)) {
throw new Error('Separator included in ID');
}
try {
const keytar = this.getKeytar();
return await keytar.setPassword(this.serviceName, id, key);
} catch (ex) {
console.warn(`Adding key failed: ${ex}`);
}
}
async get(id: string): Promise<string | undefined> {
try {
const keytar = this.getKeytar();
const result = await keytar.getPassword(this.serviceName, id);
if (result === null) {
return undefined;
}
return result;
} catch (ex) {
console.warn(`Getting key failed: ${ex}`);
return undefined;
}
}
async remove(id: string): Promise<boolean> {
try {
const keytar = this.getKeytar();
return await keytar.deletePassword(this.serviceName, id);
} catch (ex) {
console.warn(`Clearing key failed: ${ex}`);
return false;
}
}
async findCredentials(prefix: string): Promise<{ account: string, password: string }[]> {
try {
const keytar = this.getKeytar();
return await keytar.getPasswords(`${this.serviceName}${separator}${prefix}`);
} catch (ex) {
console.warn(`Finding credentials failed: ${ex}`);
return [];
}
}
private getKeytar(): Keytar {
if (!this.keytar) {
throw new Error('Keytar not initialized');
}
return this.keytar;
}
}

Просмотреть файл

@ -3,16 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as LocalizedConstants from '../constants/localizedConstants';
import * as LocalizedConstants from '../../constants/localizedConstants';
import { AuthRequest, AzureAuthError } from '@microsoft/ads-adal-library';
import { SimpleWebServer } from './simpleWebServer';
import { SimpleWebServer } from '../simpleWebServer';
import * as crypto from 'crypto';
import * as http from 'http';
import * as path from 'path';
import { promises as fs } from 'fs';
import * as vscode from 'vscode';
import VscodeWrapper from '../controllers/vscodeWrapper';
import { Logger } from '../models/logger';
import VscodeWrapper from '../../controllers/vscodeWrapper';
import { Logger } from '../../models/logger';
export class AzureAuthRequest implements AuthRequest {
simpleWebServer: SimpleWebServer;

Просмотреть файл

@ -1,3 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ErrorLookup, ErrorCodes, Error1Context } from '@microsoft/ads-adal-library';

Просмотреть файл

@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MessageDisplayer } from '@microsoft/ads-adal-library';
export class AzureMessageDisplayer implements MessageDisplayer {
async displayInfoMessage(msg: string): Promise<void> {
return;
}
async displayErrorMessage(msg: string): Promise<void> {
return;
}
}

Просмотреть файл

@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { StringLookup, InteractionRequiredContext } from '@microsoft/ads-adal-library';
export class AzureStringLookup implements StringLookup {
getSimpleString: (code: number) => string;
getInteractionRequiredString: (context: InteractionRequiredContext) => string;
}

Просмотреть файл

@ -1,3 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { UserInteraction } from '@microsoft/ads-adal-library';
import * as vscode from 'vscode';

Просмотреть файл

@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as crypto from 'crypto';
import { CredentialStore } from '../../credentialstore/credentialstore';
export class FileEncryptionHelper {
constructor(
private _credentialStore: CredentialStore,
private _fileName: string
) { }
private _ivBuffer: Buffer | undefined;
private _keyBuffer: Buffer | undefined;
async init(): Promise<void> {
const iv = await this._credentialStore.readCredential(`${this._fileName}-iv`);
const key = await this._credentialStore.readCredential(`${this._fileName}-key`);
if (!iv?.password || !key?.password) {
this._ivBuffer = crypto.randomBytes(16);
this._keyBuffer = crypto.randomBytes(32);
try {
await this._credentialStore.saveCredential(`${this._fileName}-iv`, this._ivBuffer.toString('hex'));
await this._credentialStore.saveCredential(`${this._fileName}-key`, this._keyBuffer.toString('hex'));
} catch (ex) {
console.log(ex);
}
} else {
this._ivBuffer = Buffer.from(iv.password, 'hex');
this._keyBuffer = Buffer.from(key.password, 'hex');
}
}
fileSaver = async (content: string): Promise<string> => {
if (!this._keyBuffer || !this._ivBuffer) {
await this.init();
}
const cipherIv = crypto.createCipheriv('aes-256-gcm', this._keyBuffer!, this._ivBuffer!);
return `${cipherIv.update(content, 'utf8', 'hex')}${cipherIv.final('hex')}%${cipherIv.getAuthTag().toString('hex')}`;
}
fileOpener = async (content: string): Promise<string> => {
if (!this._keyBuffer || !this._ivBuffer) {
await this.init();
}
const decipherIv = crypto.createDecipheriv('aes-256-gcm', this._keyBuffer!, this._ivBuffer!);
const split = content.split('%');
if (split.length !== 2) {
throw new Error('File didn\'t contain the auth tag.');
}
decipherIv.setAuthTag(Buffer.from(split[1], 'hex'));
return `${decipherIv.update(split[0], 'hex', 'utf8')}${decipherIv.final('utf8')}`;
}
}

Просмотреть файл

@ -2,13 +2,9 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises as fs, constants as fsConstants } from 'fs';
import { SecureStorageProvider } from '@microsoft/ads-adal-library';
export type ReadWriteHook = (contents: string) => Promise<string>;
const noOpHook: ReadWriteHook = async (contents): Promise<string> => {
return contents;
};
import { SecureStorageProvider } from '@microsoft/ads-adal-library';
import { constants as fsConstants, promises as fs } from 'fs';
export class AlreadyInitializedError extends Error {
}
@ -18,35 +14,13 @@ export class StorageService implements SecureStorageProvider {
private db: { [key: string]: string };
private isSaving = false;
private isDirty = false;
private isInitialized = false;
private saveInterval: NodeJS.Timer;
constructor(
private readonly dbPath: string,
private readHook: ReadWriteHook = noOpHook,
private writeHook: ReadWriteHook = noOpHook
private readonly dbPath: string
) {
}
/**
* Sets a new read hook. Throws AlreadyInitializedError if the database has already started.
* @param hook
*/
public setReadHook(hook: ReadWriteHook): void {
if (this.isInitialized) {
throw new AlreadyInitializedError();
}
this.readHook = hook;
}
/**
* Sets a new write hook.
* @param hook
*/
public setWriteHook(hook: ReadWriteHook): void {
this.writeHook = hook;
}
public async set(key: string, value: string): Promise<void> {
await this.waitForFileSave();
this.db[key] = value;
@ -89,13 +63,11 @@ export class StorageService implements SecureStorageProvider {
}
public async initialize(): Promise<void> {
this.isInitialized = true;
this.setupSaveTask();
let fileContents: string;
try {
await fs.access(this.dbPath, fsConstants.R_OK);
fileContents = await fs.readFile(this.dbPath, { encoding: 'utf8' });
fileContents = await this.readHook(fileContents);
fileContents = await fs.readFile(this.dbPath, { encoding: 'utf-8' });
} catch (ex) {
console.log(`file db does not exist ${ex}`);
await this.createFile();
@ -132,10 +104,7 @@ export class StorageService implements SecureStorageProvider {
this.isSaving = true;
let contents = JSON.stringify(this.db);
contents = await this.writeHook(contents);
await fs.writeFile(this.dbPath, contents, { encoding: 'utf8' });
await fs.writeFile(this.dbPath, contents, { encoding: 'utf-8' });
this.isDirty = false;
} catch (ex) {
console.log(`File saving is erroring! ${ex}`);
@ -144,7 +113,6 @@ export class StorageService implements SecureStorageProvider {
}
}
private async waitForFileSave(): Promise<void> {
const cleanupCrew: NodeJS.Timer[] = [];

Просмотреть файл

@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class AzureAuthError extends Error {
constructor(localizedMessage: string,
public readonly originalMessage: string,
private readonly originalException: any) {
super(localizedMessage);
}
/**
* The original message and exception for displaying extra information
*/
public get originalMessageAndException(): string {
return JSON.stringify({
originalMessage: this.originalMessage,
originalException: this.originalException
}, undefined, 2);
}
}

Просмотреть файл

@ -3,234 +3,126 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import * as LocalizedConstants from '../constants/localizedConstants';
import { AzureStringLookup } from '../azure/azureStringLookup';
import { AzureUserInteraction } from '../azure/azureUserInteraction';
import { AzureErrorLookup } from '../azure/azureErrorLookup';
import { AzureMessageDisplayer } from './azureMessageDisplayer';
import { AzureAuthRequest } from './azureAuthRequest';
import { SimpleTokenCache } from './cacheService';
import * as path from 'path';
import * as os from 'os';
import { promises as fs } from 'fs';
import { CredentialStore } from '../credentialstore/credentialstore';
import { StorageService } from './storageService';
import * as utils from '../models/utils';
import { IAccount } from 'vscode-mssql';
import { AADResource, AzureAuthType, AzureCodeGrant, AzureDeviceCode, Token } from '@microsoft/ads-adal-library';
import { ConnectionProfile } from '../models/connectionProfile';
import { AccountStore } from './accountStore';
import * as AzureConstants from './constants';
import * as azureUtils from './utils';
import { Subscription } from '@azure/arm-subscriptions';
import { promises as fs } from 'fs';
import { IAzureAccountSession } from 'vscode-mssql';
import providerSettings from '../azure/providerSettings';
import VscodeWrapper from '../controllers/vscodeWrapper';
import { QuestionTypes, IQuestion, IPrompter, INameValueChoice } from '../prompts/question';
import { Tenant } from '@microsoft/ads-adal-library';
import { AzureAccount } from '../../lib/ads-adal-library/src';
import { Subscription } from '@azure/arm-subscriptions';
import * as mssql from 'vscode-mssql';
import * as azureUtils from './utils';
import * as Constants from '../constants/constants';
import { ConnectionProfile } from '../models/connectionProfile';
import { AuthLibrary, AzureAuthType, IAADResource, IAccount, IProviderSettings, ITenant, IToken } from '../models/contracts/azure';
import { Logger, LogLevel } from '../models/logger';
import { INameValueChoice, IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
import { AccountStore } from './accountStore';
export class AzureController {
private authRequest: AzureAuthRequest;
private azureStringLookup: AzureStringLookup;
private azureUserInteraction: AzureUserInteraction;
private azureErrorLookup: AzureErrorLookup;
private azureMessageDisplayer: AzureMessageDisplayer;
private cacheService: SimpleTokenCache;
private storageService: StorageService;
private context: vscode.ExtensionContext;
private logger: Logger;
private prompter: IPrompter;
private _vscodeWrapper: VscodeWrapper;
private credentialStoreInitialized = false;
export abstract class AzureController {
protected _providerSettings: IProviderSettings;
protected _authLibrary: AuthLibrary;
protected _vscodeWrapper: VscodeWrapper;
protected _credentialStoreInitialized = false;
protected logger: Logger;
protected _isSqlAuthProviderEnabled: boolean = false;
constructor(
context: vscode.ExtensionContext,
prompter: IPrompter,
logger?: Logger,
private _subscriptionClientFactory: azureUtils.SubscriptionClientFactory = azureUtils.defaultSubscriptionClientFactory) {
this.context = context;
this.prompter = prompter;
if (!logger) {
let logLevel: LogLevel = LogLevel[utils.getConfigTracingLevel() as keyof typeof LogLevel];
let pii = utils.getConfigPiiLogging();
let _channel = vscode.window.createOutputChannel(LocalizedConstants.azureLogChannelName);
this.logger = new Logger(text => _channel.append(text), logLevel, pii);
} else {
this.logger = logger;
}
protected context: vscode.ExtensionContext,
protected prompter: IPrompter,
protected _subscriptionClientFactory: azureUtils.SubscriptionClientFactory = azureUtils.defaultSubscriptionClientFactory) {
if (!this._vscodeWrapper) {
this._vscodeWrapper = new VscodeWrapper();
}
}
public async init(): Promise<void> {
this.authRequest = new AzureAuthRequest(this.context, this.logger);
await this.authRequest.startServer();
this.azureStringLookup = new AzureStringLookup();
this.azureUserInteraction = new AzureUserInteraction(this.authRequest.getState());
this.azureErrorLookup = new AzureErrorLookup();
this.azureMessageDisplayer = new AzureMessageDisplayer();
}
// Setup Logger
let logLevel: LogLevel = LogLevel[utils.getConfigTracingLevel() as keyof typeof LogLevel];
let pii = utils.getConfigPiiLogging();
let _channel = this._vscodeWrapper.createOutputChannel(LocalizedConstants.azureLogChannelName);
this.logger = new Logger(text => _channel.append(text), logLevel, pii);
private async promptForTenantChoice(account: AzureAccount, profile: ConnectionProfile): Promise<void> {
let tenantChoices: INameValueChoice[] = account.properties.tenants?.map(t => ({ name: t.displayName, value: t }));
if (tenantChoices && tenantChoices.length === 1) {
profile.tenantId = tenantChoices[0].value.id;
return;
}
let tenantQuestion: IQuestion = {
type: QuestionTypes.expand,
name: LocalizedConstants.tenant,
message: LocalizedConstants.azureChooseTenant,
choices: tenantChoices,
shouldPrompt: (answers) => profile.isAzureActiveDirectory() && tenantChoices.length > 1,
onAnswered: (value: Tenant) => {
profile.tenantId = value.id;
this._authLibrary = azureUtils.getAzureAuthLibraryConfig();
this._providerSettings = providerSettings;
vscode.workspace.onDidChangeConfiguration((changeEvent) => {
const impactsProvider = changeEvent.affectsConfiguration(AzureConstants.accountsAzureAuthSection);
if (impactsProvider === true) {
this.handleAuthMapping();
}
};
await this.prompter.promptSingle(tenantQuestion, true);
});
}
public async addAccount(accountStore: AccountStore): Promise<IAccount> {
let account: IAccount;
let config = azureUtils.getAzureActiveDirectoryConfig();
if (config === utils.azureAuthTypeToString(AzureAuthType.AuthCodeGrant)) {
let azureCodeGrant = await this.createAuthCodeGrant();
account = await azureCodeGrant.startLogin();
await accountStore.addAccount(account);
} else if (config === utils.azureAuthTypeToString(AzureAuthType.DeviceCode)) {
let azureDeviceCode = await this.createDeviceCode();
account = await azureDeviceCode.startLogin();
await accountStore.addAccount(account);
}
public abstract init(): void;
public abstract login(authType: AzureAuthType): Promise<IAccount | undefined>;
public abstract populateAccountProperties(profile: ConnectionProfile, accountStore: AccountStore, settings: IAADResource): Promise<ConnectionProfile>;
public abstract getAccountSecurityToken(account: IAccount, tenantId: string | undefined, settings: IAADResource): Promise<IToken | undefined>;
public abstract refreshAccessToken(account: IAccount, accountStore: AccountStore,
tenantId: string | undefined, settings: IAADResource): Promise<IToken | undefined>;
public abstract removeAccount(account: IAccount): Promise<void>;
public abstract handleAuthMapping(): void;
public isSqlAuthProviderEnabled(): boolean {
return this._authLibrary === AuthLibrary.MSAL && this._isSqlAuthProviderEnabled;
}
public async addAccount(accountStore: AccountStore): Promise<IAccount | undefined> {
let config = azureUtils.getAzureActiveDirectoryConfig();
let account = await this.login(config!);
await accountStore.addAccount(account!);
this.logger.verbose('Account added successfully.');
return account;
}
public async getAccountSecurityToken(account: IAccount, tenantId: string | undefined, settings: AADResource): Promise<Token> {
let token: Token;
let config = azureUtils.getAzureActiveDirectoryConfig();
if (config === utils.azureAuthTypeToString(AzureAuthType.AuthCodeGrant)) {
let azureCodeGrant = await this.createAuthCodeGrant();
tenantId = tenantId ? tenantId : azureCodeGrant.getHomeTenant(account).id;
token = await azureCodeGrant.getAccountSecurityToken(
account, tenantId, settings
);
} else if (config === utils.azureAuthTypeToString(AzureAuthType.DeviceCode)) {
let azureDeviceCode = await this.createDeviceCode();
tenantId = tenantId ? tenantId : azureDeviceCode.getHomeTenant(account).id;
token = await azureDeviceCode.getAccountSecurityToken(
account, tenantId, settings
);
}
this.logger.verbose('Access token retreived successfully.');
return token;
}
/**
* Gets the token for given account and updates the connection profile with token information needed for AAD authentication
*/
public async populateAccountProperties(profile: ConnectionProfile, accountStore: AccountStore, settings: AADResource): Promise<ConnectionProfile> {
let account = await this.addAccount(accountStore);
if (!profile.tenantId) {
await this.promptForTenantChoice(account, profile);
}
const token = await this.getAccountSecurityToken(
account, profile.tenantId, settings
);
if (!token) {
let errorMessage = LocalizedConstants.msgGetTokenFail;
this.logger.error(errorMessage);
this._vscodeWrapper.showErrorMessage(errorMessage);
} else {
profile.azureAccountToken = token.token;
profile.expiresOn = token.expiresOn;
profile.email = account.displayInfo.email;
profile.accountId = account.key.id;
}
return profile;
}
public async refreshTokenWrapper(profile, accountStore, accountAnswer, settings: AADResource): Promise<ConnectionProfile> {
public async refreshTokenWrapper(profile, accountStore, accountAnswer, settings: IAADResource): Promise<ConnectionProfile | undefined> {
let account = accountStore.getAccount(accountAnswer.key.id);
if (!account) {
await this._vscodeWrapper.showErrorMessage(LocalizedConstants.msgAccountNotFound);
throw new Error(LocalizedConstants.msgAccountNotFound);
}
let azureAccountToken = await this.refreshToken(account, accountStore, settings, profile.tenantId);
if (!azureAccountToken) {
let errorMessage = LocalizedConstants.msgAccountRefreshFailed;
return this._vscodeWrapper.showErrorMessage(errorMessage, LocalizedConstants.refreshTokenLabel).then(async result => {
if (result === LocalizedConstants.refreshTokenLabel) {
let refreshedProfile = await this.populateAccountProperties(profile, accountStore, settings);
return refreshedProfile;
} else {
return undefined;
}
});
}
profile.azureAccountToken = azureAccountToken.token;
profile.expiresOn = azureAccountToken.expiresOn;
profile.email = account.displayInfo.email;
profile.accountId = account.key.id;
return profile;
}
public async refreshToken(account: IAccount, accountStore: AccountStore, settings: AADResource, tenantId: string = undefined): Promise<Token | undefined> {
try {
let token: Token;
if (account.properties.azureAuthType === 0) {
// Auth Code Grant
let azureCodeGrant = await this.createAuthCodeGrant();
let newAccount = await azureCodeGrant.refreshAccess(account);
if (newAccount.isStale === true) {
return undefined;
}
await accountStore.addAccount(newAccount);
token = await this.getAccountSecurityToken(
account, tenantId, settings
);
} else if (account.properties.azureAuthType === 1) {
// Auth Device Code
let azureDeviceCode = await this.createDeviceCode();
let newAccount = await azureDeviceCode.refreshAccess(account);
await accountStore.addAccount(newAccount);
if (newAccount.isStale === true) {
return undefined;
}
token = await this.getAccountSecurityToken(
account, tenantId, settings
);
if (this._authLibrary === AuthLibrary.MSAL && !this._isSqlAuthProviderEnabled) {
this.logger.verbose(`Account found, refreshing access token for tenant ${profile.tenantId}`);
let azureAccountToken = await this.refreshAccessToken(account, accountStore, profile.tenantId, settings);
if (!azureAccountToken) {
let errorMessage = LocalizedConstants.msgAccountRefreshFailed;
return this._vscodeWrapper.showErrorMessage(errorMessage, LocalizedConstants.refreshTokenLabel).then(async result => {
if (result === LocalizedConstants.refreshTokenLabel) {
let refreshedProfile = await this.populateAccountProperties(profile, accountStore, settings);
return refreshedProfile;
} else {
return undefined;
}
});
}
return token;
} catch (ex) {
let errorMsg = this.azureErrorLookup.getSimpleError(ex.errorCode);
this._vscodeWrapper.showErrorMessage(errorMsg);
profile.azureAccountToken = azureAccountToken.token;
profile.expiresOn = azureAccountToken.expiresOn;
profile.email = account.displayInfo.email;
profile.accountId = account.key.id;
} else {
this.logger.verbose('Account found and SQL Authentication Provider is enabled, access token will not be refreshed by extension.');
}
return profile;
}
/**
* Returns Azure sessions with subscriptions, tenant and token for each given account
*/
public async getAccountSessions(account: IAccount): Promise<mssql.IAzureAccountSession[]> {
let sessions: mssql.IAzureAccountSession[] = [];
const tenants = <Tenant[]>account.properties.tenants;
public async getAccountSessions(account: IAccount): Promise<IAzureAccountSession[]> {
let sessions: IAzureAccountSession[] = [];
const tenants = <ITenant[]>account.properties.tenants;
for (const tenantId of tenants.map(t => t.id)) {
const token = await this.getAccountSecurityToken(account, tenantId, providerSettings.resources.azureManagementResource);
const subClient = this._subscriptionClientFactory(token);
const subClient = this._subscriptionClientFactory(token!);
const newSubPages = await subClient.subscriptions.list();
const array = await azureUtils.getAllValues<Subscription, mssql.IAzureAccountSession>(newSubPages, (nextSub) => {
const array = await azureUtils.getAllValues<Subscription, IAzureAccountSession>(newSubPages, (nextSub) => {
return {
subscription: nextSub,
tenantId: tenantId,
@ -244,99 +136,17 @@ export class AzureController {
return sessions.sort((a, b) => (a.subscription.displayName || '').localeCompare(b.subscription.displayName || ''));
}
private async createAuthCodeGrant(): Promise<AzureCodeGrant> {
await this.initializeCredentialStore();
return new AzureCodeGrant(
providerSettings, this.storageService, this.cacheService, this.logger,
this.azureMessageDisplayer, this.azureErrorLookup, this.azureUserInteraction,
this.azureStringLookup, this.authRequest
);
}
private async createDeviceCode(): Promise<AzureDeviceCode> {
await this.initializeCredentialStore();
return new AzureDeviceCode(
providerSettings, this.storageService, this.cacheService, this.logger,
this.azureMessageDisplayer, this.azureErrorLookup, this.azureUserInteraction,
this.azureStringLookup, this.authRequest
);
}
public async removeToken(account: AzureAccount): Promise<void> {
let azureAuth = await this.createAuthCodeGrant();
await azureAuth.deleteAccountCache(account.key);
this.logger.verbose(`Account deleted from cache successfully: ${account.key.id}`);
return;
}
/**
* Checks if this.init() has already been called, initializes the credential store (should only be called once)
*/
private async initializeCredentialStore(): Promise<void> {
if (!this.credentialStoreInitialized) {
let storagePath = await this.findOrMakeStoragePath();
let credentialStore = new CredentialStore(this.context);
this.cacheService = new SimpleTokenCache(Constants.adalCacheFileName, storagePath, true, credentialStore);
await this.cacheService.init();
this.storageService = this.cacheService.db;
this.credentialStoreInitialized = true;
this.logger.verbose(`Credential store initialized.`);
}
}
private getAppDataPath(): string {
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');
default: throw new Error('Platform not supported');
}
}
private getDefaultOutputLocation(): string {
return path.join(this.getAppDataPath(), Constants.vscodeAppName);
}
// Generates storage path for Azure Account cache, e.g C:\users\<>\AppData\Roaming\Code\Azure Accounts\
private async findOrMakeStoragePath(): Promise<string | undefined> {
let defaultOutputLocation = this.getDefaultOutputLocation();
let storagePath = path.join(defaultOutputLocation, Constants.azureAccountDirectory);
try {
await fs.mkdir(defaultOutputLocation, { recursive: true });
} catch (e) {
if (e.code !== 'EEXIST') {
this.logger.error(`Creating the base directory failed... ${e}`);
return undefined;
}
}
try {
await fs.mkdir(storagePath, { recursive: true });
} catch (e) {
if (e.code !== 'EEXIST') {
this.logger.error(`Initialization of vscode-mssql storage failed: ${e}`);
this.logger.error('Azure accounts will not be available');
return undefined;
}
}
this.logger.log('Initialized vscode-mssql storage.');
return storagePath;
}
/**
* Verifies if the token still valid, refreshes the token for given account
* @param session
*/
public async checkAndRefreshToken(
session: mssql.IAzureAccountSession,
session: IAzureAccountSession,
accountStore: AccountStore): Promise<void> {
if (session?.account && AzureController.isTokenInValid(session.token?.token, session.token.expiresOn)) {
const token = await this.refreshToken(session.account, accountStore,
if (session?.account && AzureController.isTokenInValid(session.token!.token, session.token!.expiresOn)) {
const token = await this.refreshAccessToken(session.account, accountStore, undefined,
providerSettings.resources.azureManagementResource);
session.token = token;
session.token = token!;
this.logger.verbose(`Access Token refreshed for account: ${session?.account?.key.id}`);
} else {
this.logger.verbose(`Access Token not refreshed for account: ${session?.account?.key.id}`);
@ -364,4 +174,65 @@ export class AzureController {
const maxTolerance = 2 * 60; // two minutes
return (expiresOn - currentTime < maxTolerance);
}
protected async promptForTenantChoice(account: IAccount, profile: ConnectionProfile): Promise<void> {
let tenantChoices: INameValueChoice[] = account.properties.tenants?.map(t => ({ name: t.displayName, value: t }));
if (tenantChoices && tenantChoices.length === 1) {
profile.tenantId = tenantChoices[0].value.id;
return;
}
let tenantQuestion: IQuestion = {
type: QuestionTypes.expand,
name: LocalizedConstants.tenant,
message: LocalizedConstants.azureChooseTenant,
choices: tenantChoices,
shouldPrompt: (answers) => profile.isAzureActiveDirectory() && tenantChoices.length > 1,
onAnswered: (value: ITenant) => {
profile.tenantId = value.id;
}
};
await this.prompter.promptSingle(tenantQuestion, true);
}
// Generates storage path for Azure Account cache, e.g C:\users\<>\AppData\Roaming\Code\Azure Accounts\
protected async findOrMakeStoragePath(): Promise<string | undefined> {
let defaultOutputLocation = this.getDefaultOutputLocation();
let storagePath = path.join(defaultOutputLocation, AzureConstants.azureAccountDirectory);
try {
await fs.mkdir(defaultOutputLocation, { recursive: true });
} catch (e) {
if (e.code !== 'EEXIST') {
this.logger.error(`Creating the base directory failed... ${e}`);
return undefined;
}
}
try {
await fs.mkdir(storagePath, { recursive: true });
} catch (e) {
if (e.code !== 'EEXIST') {
this.logger.error(`Initialization of vscode-mssql storage failed: ${e}`);
this.logger.error('Azure accounts will not be available');
return undefined;
}
}
this.logger.log('Initialized vscode-mssql storage.');
return storagePath;
}
private getAppDataPath(): string {
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');
default: throw new Error('Platform not supported');
}
}
private getDefaultOutputLocation(): string {
return path.join(this.getAppDataPath(), AzureConstants.serviceName);
}
}

Просмотреть файл

@ -1,10 +0,0 @@
import { MessageDisplayer } from '@microsoft/ads-adal-library';
export class AzureMessageDisplayer implements MessageDisplayer {
async displayInfoMessage(msg: string): Promise<void> {
return;
}
async displayErrorMessage(msg: string): Promise<void> {
return;
}
}

Просмотреть файл

@ -3,9 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Location } from '@azure/arm-subscriptions';
import { ResourceGroup } from '@azure/arm-resources';
import { Server } from '@azure/arm-sql';
import { Location } from '@azure/arm-subscriptions';
import * as mssql from 'vscode-mssql';
import * as azureUtils from './utils';
@ -23,7 +23,7 @@ export class AzureResourceController {
* @returns List of locations
*/
public async getLocations(session: mssql.IAzureAccountSession): Promise<Location[]> {
const subClient = this._subscriptionClientFactory(session.token);
const subClient = this._subscriptionClientFactory(session.token!);
if (session.subscription?.subscriptionId) {
const locationsPages = await subClient.subscriptions.listLocations(session.subscription.subscriptionId);
let locations = await azureUtils.getAllValues(locationsPages, (v) => v);
@ -46,7 +46,7 @@ export class AzureResourceController {
resourceGroupName: string,
serverName: string,
parameters: Server,
token: mssql.Token): Promise<string | undefined> {
token: mssql.IToken): Promise<string | undefined> {
if (subscriptionId && resourceGroupName) {
const sqlClient = this._sqlManagementClientFactory(token, subscriptionId);
if (sqlClient) {
@ -66,7 +66,7 @@ export class AzureResourceController {
*/
public async getResourceGroups(session: mssql.IAzureAccountSession): Promise<ResourceGroup[]> {
if (session.subscription?.subscriptionId) {
const resourceGroupClient = this._resourceManagementClientFactory(session.token, session.subscription.subscriptionId);
const resourceGroupClient = this._resourceManagementClientFactory(session.token!, session.subscription.subscriptionId);
const newGroupsPages = await resourceGroupClient.resourceGroups.list();
let groups = await azureUtils.getAllValues(newGroupsPages, (v) => v);
return groups.sort((a, b) => (a.name || '').localeCompare(b.name || ''));

Просмотреть файл

@ -1,6 +0,0 @@
import { StringLookup, InteractionRequiredContext } from '@microsoft/ads-adal-library';
export class AzureStringLookup implements StringLookup {
getSimpleString: (code: number) => string;
getInteractionRequiredString: (context: InteractionRequiredContext) => string;
}

Просмотреть файл

@ -1,203 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as keytarType from 'keytar';
import { join, parse } from 'path';
import { StorageService } from './storageService';
import * as crypto from 'crypto';
import { ICredentialStore } from '../credentialstore/icredentialstore';
import { CachingProvider } from '@microsoft/ads-adal-library';
function getSystemKeytar(): Keytar | undefined | null {
try {
// tslint:disable-next-line:no-require-imports
return require('keytar');
} catch (err) {
console.log(err);
}
return undefined;
}
export type MultipleAccountsResponse = { account: string, password: string }[];
const separator = '§';
export type Keytar = {
getPassword: typeof keytarType['getPassword'];
setPassword: typeof keytarType['setPassword'];
deletePassword: typeof keytarType['deletePassword'];
getPasswords: (service: string) => Promise<MultipleAccountsResponse>;
findCredentials?: typeof keytarType['findCredentials'];
};
export class SimpleTokenCache implements CachingProvider {
private keytar: Keytar;
public db: StorageService;
constructor(
private serviceName: string,
private readonly userStoragePath: string,
private readonly forceFileStorage: boolean = false,
private readonly credentialService: ICredentialStore
) {
}
async getFileKeytar(filePath: string, credentialService: ICredentialStore): Promise<Keytar | undefined> {
const fileName = parse(filePath).base;
const iv = await credentialService.readCredential(`${fileName}-iv`);
const credentialKey = await credentialService.readCredential(`${fileName}-key`);
let ivBuffer: Buffer;
let keyBuffer: Buffer;
if (!iv?.password || !credentialKey?.password) {
ivBuffer = crypto.randomBytes(16);
keyBuffer = crypto.randomBytes(32);
try {
await credentialService.saveCredential(`${fileName}-iv`, ivBuffer.toString('hex'));
await credentialService.saveCredential(`${fileName}-key`, keyBuffer.toString('hex'));
} catch (ex) {
console.log(ex);
}
} else {
ivBuffer = Buffer.from(iv.password, 'hex');
keyBuffer = Buffer.from(credentialKey.password, 'hex');
}
const fileSaver = async (content: string): Promise<string> => {
const cipherIv = crypto.createCipheriv('aes-256-gcm', keyBuffer, ivBuffer);
return `${cipherIv.update(content, 'utf8', 'hex')}${cipherIv.final('hex')}%${cipherIv.getAuthTag().toString('hex')}`;
};
const fileOpener = async (content: string): Promise<string> => {
const decipherIv = crypto.createDecipheriv('aes-256-gcm', keyBuffer, ivBuffer);
const split = content.split('%');
if (split.length !== 2) {
throw new Error('File didn\'t contain the auth tag.');
}
decipherIv.setAuthTag(Buffer.from(split[1], 'hex'));
return `${decipherIv.update(split[0], 'hex', 'utf8')}${decipherIv.final('utf8')}`;
};
this.db = new StorageService(filePath, fileOpener, fileSaver);
await this.db.initialize();
const self = this;
const fileKeytar: Keytar = {
async getPassword(service: string, account: string): Promise<string> {
return self.db.get(`${service}${separator}${account}`);
},
async setPassword(service: string, account: string, password: string): Promise<void> {
await self.db.set(`${service}${separator}${account}`, password);
},
async deletePassword(service: string, account: string): Promise<boolean> {
await self.db.remove(`${service}${separator}${account}`);
return true;
},
async getPasswords(service: string): Promise<MultipleAccountsResponse> {
const result = self.db.getPrefix(`${service}`);
if (!result) {
return [];
}
return result.map(({ key, value }) => {
return {
account: key.split(separator)[1],
password: value
};
});
}
};
return fileKeytar;
}
async init(): Promise<void> {
this.serviceName = this.serviceName.replace(/-/g, '_');
let keytar: Keytar;
if (this.forceFileStorage === false) {
keytar = getSystemKeytar();
// Add new method to keytar
if (keytar) {
keytar.getPasswords = async (service: string): Promise<MultipleAccountsResponse> => {
const [serviceName, accountPrefix] = service.split(separator);
if (serviceName === undefined || accountPrefix === undefined) {
throw new Error('Service did not have seperator: ' + service);
}
const results = await keytar.findCredentials(serviceName);
return results.filter(({ account }) => {
return account.startsWith(accountPrefix);
});
};
}
}
if (!keytar) {
keytar = await this.getFileKeytar(join(this.userStoragePath, this.serviceName), this.credentialService);
}
this.keytar = keytar;
}
async set(id: string, key: string): Promise<void> {
if (!this.forceFileStorage && key.length > 2500) { // Windows limitation
throw new Error('Key length is longer than 2500 chars');
}
if (id.includes(separator)) {
throw new Error('Separator included in ID');
}
try {
return await this.keytar.setPassword(this.serviceName, id, key);
} catch (ex) {
console.log(`Adding key failed: ${ex}`);
}
}
async get(id: string): Promise<string | undefined> {
try {
const result = await this.keytar.getPassword(this.serviceName, id);
if (result === null) {
return undefined;
}
return result;
} catch (ex) {
console.log(`Getting key failed: ${ex}`);
return undefined;
}
}
async remove(key: string): Promise<boolean> {
try {
return await this.keytar.deletePassword(this.serviceName, key);
} catch (ex) {
console.log(`Clearing key failed: ${ex}`);
return false;
}
}
async clear(): Promise<void> {
try {
this.keytar = getSystemKeytar();
} catch (ex) {
console.log(`clear keytar failed ${ex}`);
}
}
async findCredentials(prefix: string): Promise<{ account: string, password: string }[]> {
try {
return await this.keytar.getPasswords(`${this.serviceName}${separator}${prefix}`);
} catch (ex) {
console.log(`Finding credentials failed: ${ex}`);
return undefined;
}
}
}

100
src/azure/constants.ts Normal file
Просмотреть файл

@ -0,0 +1,100 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const serviceName = 'Code';
export const httpConfigSectionName = 'http';
export const extensionConfigSectionName = 'mssql';
export const azureAccountDirectory = 'Azure Accounts';
export const homeCategory = 'Home';
export const account = 'account';
export const accountsSection = 'accounts';
export const authSection = 'auth';
export const azureSection = 'azure';
export const azureAccountProviderCredentials = 'azureAccountProviderCredentials';
export const cloudSection = 'cloud';
export const clearTokenCacheCommand = 'clearTokenCache';
export const configSection = 'config';
export const mssqlSection = 'mssql';
export const tenantSection = 'tenant';
export const sqlAuthProviderSection = 'enableSqlAuthenticationProvider';
export const mssqlAuthenticationProviderConfig = mssqlSection + '.' + sqlAuthProviderSection;
export const accountsClearTokenCacheCommand = accountsSection + '.' + clearTokenCacheCommand;
export const accountsAzureAuthSection = accountsSection + '.' + azureSection + '.' + authSection;
export const accountsAzureCloudSection = accountsSection + '.' + azureSection + '.' + cloudSection;
export const azureTenantConfigSection = azureSection + '.' + tenantSection + '.' + configSection;
/** MSAL Account version */
export const accountVersion = '2.0';
export const bearer = 'Bearer';
/**
* Use SHA-256 algorithm
*/
export const s256CodeChallengeMethod = 'S256';
export const selectAccount = 'select_account';
/**
* Account issuer as received from access token
*/
export enum AccountIssuer {
Corp = 'corp',
Msft = 'msft'
}
/**
* http methods
*/
export enum HttpMethod {
GET = 'get',
POST = 'post'
}
export enum HttpStatus {
SUCCESS_RANGE_START = 200,
SUCCESS_RANGE_END = 299,
REDIRECT = 302,
CLIENT_ERROR_RANGE_START = 400,
CLIENT_ERROR_RANGE_END = 499,
SERVER_ERROR_RANGE_START = 500,
SERVER_ERROR_RANGE_END = 599
}
export enum ProxyStatus {
SUCCESS_RANGE_START = 200,
SUCCESS_RANGE_END = 299,
SERVER_ERROR = 500
}
/**
* Constants
*/
export const constants = {
MSAL_SKU: 'msal.js.node',
JWT_BEARER_ASSERTION_TYPE: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
AUTHORIZATION_PENDING: 'authorization_pending',
HTTP_PROTOCOL: 'http://',
LOCALHOST: 'localhost'
};

Просмотреть файл

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as coreAuth from '@azure/core-auth';
import * as mssql from 'vscode-mssql';
import { IToken } from '../models/contracts/azure';
/**
* TokenCredential wrapper to only return the given token.
@ -14,7 +14,7 @@ import * as mssql from 'vscode-mssql';
*/
export class TokenCredentialWrapper implements coreAuth.TokenCredential {
constructor(private _token: mssql.Token) {
constructor(private _token: IToken) {
}
public getToken(_: string | string[], __?: coreAuth.GetTokenOptions): Promise<coreAuth.AccessToken | null> {

Просмотреть файл

@ -0,0 +1,351 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { INetworkModule, NetworkRequestOptions, NetworkResponse } from '@azure/msal-common';
import * as http from 'http';
import * as https from 'https';
import { constants, HttpMethod, HttpStatus, ProxyStatus } from '../constants';
import { NetworkUtils } from './NetworkUtils';
/**
* This class implements the API for network requests.
*/
export class HttpClient implements INetworkModule {
private proxyUrl: string;
private customAgentOptions: http.AgentOptions | https.AgentOptions;
constructor(
proxyUrl?: string,
customAgentOptions?: http.AgentOptions | https.AgentOptions
) {
this.proxyUrl = proxyUrl || '';
this.customAgentOptions = customAgentOptions || {};
}
/**
* Http Get request
* @param url
* @param options
*/
async sendGetRequestAsync<T>(
url: string,
options?: NetworkRequestOptions
): Promise<NetworkResponse<T>> {
if (this.proxyUrl) {
return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.GET, options, this.customAgentOptions as http.AgentOptions);
} else {
return networkRequestViaHttps(url, HttpMethod.GET, options, this.customAgentOptions as https.AgentOptions);
}
}
/**
* Http Post request
* @param url
* @param options
*/
async sendPostRequestAsync<T>(
url: string,
options?: NetworkRequestOptions,
cancellationToken?: number
): Promise<NetworkResponse<T>> {
if (this.proxyUrl) {
return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.POST, options, this.customAgentOptions as http.AgentOptions, cancellationToken);
} else {
return networkRequestViaHttps(url, HttpMethod.POST, options, this.customAgentOptions as https.AgentOptions, cancellationToken);
}
}
}
const networkRequestViaProxy = <T>(
destinationUrlString: string,
proxyUrlString: string,
httpMethod: string,
options?: NetworkRequestOptions,
agentOptions?: http.AgentOptions,
timeout?: number
): Promise<NetworkResponse<T>> => {
const destinationUrl = new URL(destinationUrlString);
const proxyUrl = new URL(proxyUrlString);
// 'method: connect' must be used to establish a connection to the proxy
const headers = options?.headers || {} as Record<string, string>;
const tunnelRequestOptions: https.RequestOptions = {
host: proxyUrl.hostname,
port: proxyUrl.port,
method: 'CONNECT',
path: destinationUrl.hostname,
headers: headers
};
if (destinationUrl.searchParams) {
tunnelRequestOptions.path += `?${destinationUrl.searchParams}`;
}
if (timeout) {
tunnelRequestOptions.timeout = timeout;
}
if (agentOptions && Object.keys(agentOptions).length) {
tunnelRequestOptions.agent = new http.Agent(agentOptions);
}
// compose a request string for the socket
let postRequestStringContent: string = '';
if (httpMethod === HttpMethod.POST) {
const body = options?.body || '';
postRequestStringContent =
'Content-Type: application/x-www-form-urlencoded\r\n' +
`Content-Length: ${body.length}\r\n` +
`\r\n${body}`;
}
const outgoingRequestString = `${httpMethod.toUpperCase()} ${destinationUrl.href} HTTP/1.1\r\n` +
`Host: ${destinationUrl.host}\r\n` +
'Connection: close\r\n' +
postRequestStringContent +
'\r\n';
return new Promise<NetworkResponse<T>>(((resolve, reject) => {
const request = http.request(tunnelRequestOptions);
if (tunnelRequestOptions.timeout) {
request.on('timeout', () => {
request.destroy();
reject(new Error('Request time out'));
});
}
request.end();
// establish connection to the proxy
request.on('connect', (response, socket) => {
const proxyStatusCode = response?.statusCode || ProxyStatus.SERVER_ERROR;
if ((proxyStatusCode < ProxyStatus.SUCCESS_RANGE_START) || (proxyStatusCode > ProxyStatus.SUCCESS_RANGE_END)) {
request.destroy();
socket.destroy();
reject(new Error(`Error connecting to proxy. Http status code: ${response.statusCode}. Http status message: ${response?.statusMessage || 'Unknown'}`));
}
if (tunnelRequestOptions.timeout) {
socket.setTimeout(tunnelRequestOptions.timeout);
socket.on('timeout', () => {
request.destroy();
socket.destroy();
reject(new Error('Request time out'));
});
}
// make a request over an HTTP tunnel
socket.write(outgoingRequestString);
const data: Buffer[] = [];
socket.on('data', (chunk) => {
data.push(chunk);
});
socket.on('end', () => {
// combine all received buffer streams into one buffer, and then into a string
const dataString = Buffer.concat([...data]).toString();
// separate each line into it's own entry in an arry
const dataStringArray = dataString.split('\r\n');
// the first entry will contain the statusCode and statusMessage
const httpStatusCode = parseInt(dataStringArray[0].split(' ')[1], undefined);
// remove 'HTTP/1.1' and the status code to get the status message
const statusMessage = dataStringArray[0].split(' ').slice(2).join(' ');
// the last entry will contain the body
const body = dataStringArray[dataStringArray.length - 1];
// everything in between the first and last entries are the headers
const headersArray = dataStringArray.slice(1, dataStringArray.length - 2);
// build an object out of all the headers
const entries = new Map();
headersArray.forEach((header) => {
/**
* the header might look like 'Content-Length: 1531', but that is just a string
* it needs to be converted to a key/value pair
* split the string at the first instance of ':'
* there may be more than one ':' if the value of the header is supposed to be a JSON object
*/
const headerKeyValue = header.split(new RegExp(/:\s(.*)/s));
const headerKey = headerKeyValue[0];
let headerValue = headerKeyValue[1];
// check if the value of the header is supposed to be a JSON object
try {
const object = JSON.parse(headerValue);
// if it is, then convert it from a string to a JSON object
if (object && (typeof object === 'object')) {
headerValue = object;
}
} catch (e) {
// otherwise, leave it as a string
}
entries.set(headerKey, headerValue);
});
const parsedHeaders = Object.fromEntries(entries) as Record<string, string>;
const networkResponse = NetworkUtils.getNetworkResponse(
parsedHeaders,
parseBody(httpStatusCode, statusMessage, parsedHeaders, body) as T,
httpStatusCode
);
if (((httpStatusCode < HttpStatus.SUCCESS_RANGE_START) || (httpStatusCode > HttpStatus.SUCCESS_RANGE_END)) &&
// do not destroy the request for the device code flow
networkResponse.body['error'] !== constants.AUTHORIZATION_PENDING) {
request.destroy();
}
resolve(networkResponse);
});
socket.on('error', (chunk) => {
request.destroy();
socket.destroy();
reject(new Error(chunk.toString()));
});
});
request.on('error', (chunk) => {
request.destroy();
reject(new Error(chunk.toString()));
});
}));
};
const networkRequestViaHttps = <T>(
urlString: string,
httpMethod: string,
options?: NetworkRequestOptions,
agentOptions?: https.AgentOptions,
timeout?: number
): Promise<NetworkResponse<T>> => {
const isPostRequest = httpMethod === HttpMethod.POST;
const body: string = options?.body || '';
const url = new URL(urlString);
const emptyHeaders: Record<string, string> = {};
const customOptions: https.RequestOptions = {
hostname: url.hostname,
path: url.pathname,
method: httpMethod,
headers: options?.headers || emptyHeaders
};
if (url.searchParams) {
customOptions.path += `?${url.searchParams}`;
}
if (timeout) {
customOptions.timeout = timeout;
}
if (agentOptions && Object.keys(agentOptions).length) {
customOptions.agent = new https.Agent(agentOptions);
}
if (isPostRequest) {
// needed for post request to work
customOptions.headers = {
...customOptions.headers,
'Content-Length': body.length
};
}
return new Promise<NetworkResponse<T>>((resolve, reject) => {
const request = https.request(customOptions);
if (timeout) {
request.on('timeout', () => {
request.destroy();
reject(new Error('Request time out'));
});
}
if (isPostRequest) {
request.write(body);
}
request.end();
request.on('response', (response) => {
const headers = response.headers;
const statusCode = response.statusCode as number;
const statusMessage = response.statusMessage;
const data: Buffer[] = [];
response.on('data', (chunk) => {
data.push(chunk);
});
response.on('end', () => {
// combine all received buffer streams into one buffer, and then into a string
const dataBody = Buffer.concat([...data]).toString();
const parsedHeaders = headers as Record<string, string>;
const networkResponse = NetworkUtils.getNetworkResponse(
parsedHeaders,
parseBody(statusCode, statusMessage, parsedHeaders, dataBody) as T,
statusCode
);
if (((statusCode < HttpStatus.SUCCESS_RANGE_START) || (statusCode > HttpStatus.SUCCESS_RANGE_END)) &&
// do not destroy the request for the device code flow
networkResponse.body['error'] !== constants.AUTHORIZATION_PENDING) {
request.destroy();
}
resolve(networkResponse);
});
});
request.on('error', (chunk) => {
request.destroy();
reject(new Error(chunk.toString()));
});
});
};
/**
* Check if extra parsing is needed on the repsonse from the server
* @param statusCode {number} the status code of the response from the server
* @param statusMessage {string | undefined} the status message of the response from the server
* @param headers {Record<string, string>} the headers of the response from the server
* @param body {string} the body from the response of the server
* @returns {Object} JSON parsed body or error object
*/
const parseBody = (statusCode: number, statusMessage: string | undefined, headers: Record<string, string>, body: string) => {
/*
* Informational responses (100 199)
* Successful responses (200 299)
* Redirection messages (300 399)
* Client error responses (400 499)
* Server error responses (500 599)
*/
let parsedBody;
try {
parsedBody = JSON.parse(body);
} catch (error) {
let errorType;
let errorDescriptionHelper;
if ((statusCode >= HttpStatus.CLIENT_ERROR_RANGE_START) && (statusCode <= HttpStatus.CLIENT_ERROR_RANGE_END)) {
errorType = 'client_error';
errorDescriptionHelper = 'A client';
} else if ((statusCode >= HttpStatus.SERVER_ERROR_RANGE_START) && (statusCode <= HttpStatus.SERVER_ERROR_RANGE_END)) {
errorType = 'server_error';
errorDescriptionHelper = 'A server';
} else {
errorType = 'unknown_error';
errorDescriptionHelper = 'An unknown';
}
parsedBody = {
error: errorType,
error_description: `${errorDescriptionHelper} error occured.\nHttp status code: ${statusCode}\nHttp status message: ${statusMessage || 'Unknown'}\nHeaders: ${JSON.stringify(headers)}`
};
}
return parsedBody;
};

Просмотреть файл

@ -0,0 +1,670 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Resource } from '@azure/arm-resources';
import { AccountInfo, AuthenticationResult, InteractionRequiredAuthError, PublicClientApplication, SilentFlowRequest } from '@azure/msal-node';
import * as url from 'url';
import * as vscode from 'vscode';
import * as LocalizedConstants from '../../constants/localizedConstants';
import VscodeWrapper from '../../controllers/vscodeWrapper';
import { AccountType, AzureAuthType, IAADResource, IAccount, IPromptFailedResult, IProviderSettings, ITenant } from '../../models/contracts/azure';
import { IDeferred } from '../../models/interfaces';
import { Logger } from '../../models/logger';
import * as Utils from '../../models/utils';
import { AzureAuthError } from '../azureAuthError';
import * as Constants from '../constants';
import * as azureUtils from '../utils';
import { HttpClient } from './httpClient';
// tslint:disable:no-null-keyword
export abstract class MsalAzureAuth {
protected readonly loginEndpointUrl: string;
public readonly commonTenant: ITenant;
public readonly organizationTenant: ITenant;
protected readonly redirectUri: string;
protected readonly scopes: string[];
protected readonly scopesString: string;
protected readonly clientId: string;
protected readonly resources: Resource[];
protected readonly httpClient: HttpClient;
constructor(
protected readonly providerSettings: IProviderSettings,
protected readonly context: vscode.ExtensionContext,
protected clientApplication: PublicClientApplication,
protected readonly authType: AzureAuthType,
protected readonly vscodeWrapper: VscodeWrapper,
protected readonly logger: Logger
) {
this.loginEndpointUrl = this.providerSettings.loginEndpoint ?? 'https://login.microsoftonline.com/';
this.commonTenant = {
id: 'common',
displayName: 'common'
};
this.organizationTenant = {
id: 'organizations',
displayName: 'organizations'
};
// Use localhost for MSAL instead of this.providerSettings.redirectUri (kept as-is for ADAL only);
this.redirectUri = 'http://localhost';
this.clientId = this.providerSettings.clientId;
this.scopes = [...this.providerSettings.scopes];
this.scopesString = this.scopes.join(' ');
this.httpClient = azureUtils.getProxyEnabledHttpClient();
}
public async startLogin(): Promise<IAccount | IPromptFailedResult> {
let loginComplete: IDeferred<void, Error> | undefined = undefined;
try {
this.logger.verbose('Starting login');
if (!this.providerSettings.resources.windowsManagementResource) {
throw new Error(Utils.formatString(LocalizedConstants.azureNoMicrosoftResource, this.providerSettings.displayName));
}
const result = await this.login(this.organizationTenant);
loginComplete = result.authComplete;
if (!result?.response || !result.response?.account) {
this.logger.error(`Authentication failed: ${loginComplete}`);
return {
canceled: false
};
}
const token: IToken = {
token: result.response.accessToken,
key: result.response.account.homeAccountId,
tokenType: result.response.tokenType
};
const tokenClaims = <ITokenClaims>result.response.idTokenClaims;
const account = await this.hydrateAccount(token, tokenClaims);
loginComplete?.resolve();
return account;
} catch (ex) {
this.logger.error(`Login failed: ${ex}`);
if (ex instanceof AzureAuthError) {
if (loginComplete) {
loginComplete.reject(ex);
this.logger.error(ex);
} else {
void vscode.window.showErrorMessage(ex.message);
this.logger.error(ex.originalMessageAndException);
}
} else {
this.logger.error(ex);
}
return {
canceled: false
};
}
}
public async hydrateAccount(token: IToken | IAccessToken, tokenClaims: ITokenClaims): Promise<IAccount> {
const tenants = await this.getTenants(token.token);
let account = this.createAccount(tokenClaims, token.key, tenants);
return account;
}
protected abstract login(tenant: ITenant): Promise<{ response: AuthenticationResult | null, authComplete: IDeferred<void, Error> }>;
/**
* 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 account
* @param azureResource
* @returns The authentication result, including the access token
*/
public async getToken(account: IAccount, tenantId: string, settings: IAADResource): Promise<AuthenticationResult | null> {
let accountInfo: AccountInfo | null = await this.getAccountFromMsalCache(account.key.id);
// Resource endpoint must end with '/' to form a valid scope for MSAL token request.
const endpoint = settings.endpoint.endsWith('/') ? settings.endpoint : settings.endpoint + '/';
if (!account) {
this.logger.error('Error: Account not received.');
return null;
}
if (!tenantId) {
tenantId = account.properties.owningTenant.id;
}
let newScope: string[];
if (settings.id === this.providerSettings.resources.windowsManagementResource.id) {
newScope = [`${endpoint}user_impersonation`];
} else {
newScope = [`${endpoint}.default`];
}
let authority = this.loginEndpointUrl + tenantId;
this.logger.info(`Authority URL set to: ${authority}`);
// 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: SilentFlowRequest = {
account: accountInfo!,
authority: authority,
scopes: newScope,
forceRefresh: true
};
try {
return await this.clientApplication.acquireTokenSilent(tokenRequest);
} catch (e) {
this.logger.error('Failed to acquireTokenSilent', e);
if (e instanceof InteractionRequiredAuthError) {
// build refresh token request
const tenant: ITenant = {
id: tenantId,
displayName: ''
};
return this.handleInteractionRequired(tenant, settings);
} else if (e.name === 'ClientAuthError') {
this.logger.error(e.message);
}
this.logger.error(`Failed to silently acquire token, not InteractionRequiredAuthError: ${e.message}`);
throw e;
}
}
public async refreshAccessToken(account: IAccount, tenantId: string, settings: IAADResource): Promise<IAccount | undefined> {
try {
const tokenResult = await this.getToken(account, tenantId, settings);
if (!tokenResult) {
account.isStale = true;
return account;
}
const tokenClaims = this.getTokenClaims(tokenResult.accessToken);
if (!tokenClaims) {
account.isStale = true;
return account;
}
const token: IToken = {
key: tokenResult.account!.homeAccountId,
token: tokenResult.accessToken,
tokenType: tokenResult.tokenType,
expiresOn: tokenResult.account!.idTokenClaims!.exp
};
return await this.hydrateAccount(token, tokenClaims);
} catch (ex) {
account.isStale = true;
throw ex;
}
}
public async getAccountFromMsalCache(accountId: string): Promise<AccountInfo | null> {
const cache = this.clientApplication.getTokenCache();
if (!cache) {
this.logger.error('Error: Could not fetch token cache.');
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);
}
return account;
}
public async getTenants(token: string): Promise<ITenant[]> {
const tenantUri = url.resolve(this.providerSettings.resources.azureManagementResource.endpoint, 'tenants?api-version=2019-11-01');
try {
this.logger.verbose('Fetching tenants with uri {0}', tenantUri);
let tenantList: string[] = [];
const tenantResponse = await this.httpClient.sendGetRequestAsync<any>(tenantUri, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
const data = tenantResponse.body;
if (data.error) {
this.logger.error(`Error fetching tenants :${data.error.code} - ${data.error.message}`);
throw new Error(`${data.error.code} - ${data.error.message}`);
}
const tenants: ITenant[] = data.value.map((tenantInfo: ITenantResponse) => {
if (tenantInfo.displayName) {
tenantList.push(tenantInfo.displayName);
} else {
tenantList.push(tenantInfo.tenantId);
this.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 ITenant;
});
this.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]);
}
this.logger.verbose(`Filtered Tenants: ${tenantList}`);
return tenants;
} catch (ex) {
this.logger.error(`Error fetching tenants :${ex}`);
throw ex;
}
}
//#region interaction handling
public async handleInteractionRequired(tenant: ITenant, settings: IAADResource): Promise<AuthenticationResult | null> {
const shouldOpen = await this.askUserForInteraction(tenant, settings);
if (shouldOpen) {
const result = await this.login(tenant);
result?.authComplete?.resolve();
return result?.response;
}
return null;
}
/**
* Asks the user if they would like to do the interaction based authentication as required by OAuth2
* @param tenant
* @param resource
*/
private async askUserForInteraction(tenant: ITenant, settings: IAADResource): Promise<boolean> {
if (!tenant.displayName && !tenant.id) {
throw new Error('Tenant did not have display name or id');
}
const getTenantConfigurationSet = (): Set<string> => {
const configuration = vscode.workspace.getConfiguration(Constants.azureTenantConfigSection);
let values: string[] = configuration.get('filter') ?? [];
return new Set<string>(values);
};
// The user wants to ignore this tenant.
if (getTenantConfigurationSet().has(tenant.id)) {
this.logger.info(`Tenant ${tenant.id} found in the ignore list, authentication will not be attempted.`);
return false;
}
const updateTenantConfigurationSet = async (set: Set<string>): Promise<void> => {
const configuration = vscode.workspace.getConfiguration('azure.tenant.config');
await configuration.update('filter', Array.from(set), vscode.ConfigurationTarget.Global);
};
interface IConsentMessageItem extends vscode.MessageItem {
booleanResult: boolean;
action?: (tenantId: string) => Promise<void>;
}
const openItem: IConsentMessageItem = {
title: LocalizedConstants.azureConsentDialogOpen,
booleanResult: true
};
const closeItem: IConsentMessageItem = {
title: LocalizedConstants.azureConsentDialogCancel,
isCloseAffordance: true,
booleanResult: false
};
const dontAskAgainItem: IConsentMessageItem = {
title: LocalizedConstants.azureConsentDialogIgnore,
booleanResult: false,
action: async (tenantId: string) => {
let set = getTenantConfigurationSet();
set.add(tenantId);
await updateTenantConfigurationSet(set);
}
};
const messageBody = Utils.formatString(LocalizedConstants.azureConsentDialogBody, tenant.displayName, tenant.id, settings.id);
const result = await vscode.window.showInformationMessage(messageBody, { modal: true }, openItem, closeItem, dontAskAgainItem);
if (result?.action) {
await result.action(tenant.id);
}
return result?.booleanResult || false;
}
//#endregion
//#region data modeling
public createAccount(tokenClaims: ITokenClaims, key: string, tenants: ITenant[]): IAccount {
this.logger.verbose(`Token Claims acccount: ${tokenClaims.name}, TID: ${tokenClaims.tid}`);
tenants.forEach((tenant) => {
this.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/' ||
tokenClaims.iss === `${this.loginEndpointUrl}72f988bf-86f1-41af-91ab-2d7cd011db47/v2.0`) {
accountIssuer = Constants.AccountIssuer.Corp;
}
if (tokenClaims?.idp === 'live.com') {
accountIssuer = Constants.AccountIssuer.Msft;
}
const name = tokenClaims.name ?? tokenClaims.email ?? tokenClaims.unique_name ?? tokenClaims.preferred_username;
const email = tokenClaims.email ?? tokenClaims.unique_name ?? tokenClaims.preferred_username;
let owningTenant: ITenant = this.commonTenant; // default to common tenant
// Read more about tid > https://learn.microsoft.com/azure/active-directory/develop/id-tokens
if (tokenClaims.tid) {
owningTenant = tenants.find(t => t.id === tokenClaims.tid) ?? { 'id': tokenClaims.tid, 'displayName': 'Microsoft Account' };
} else {
this.logger.info('Could not find tenant information from tokenClaims, falling back to common Tenant.');
}
let displayName = name;
if (email) {
displayName = `${displayName} - ${email}`;
}
let contextualDisplayName: string;
switch (accountIssuer) {
case Constants.AccountIssuer.Corp:
contextualDisplayName = LocalizedConstants.azureMicrosoftCorpAccount;
break;
case Constants.AccountIssuer.Msft:
contextualDisplayName = LocalizedConstants.azureMicrosoftAccount;
break;
default:
contextualDisplayName = displayName;
}
let accountType = accountIssuer === Constants.AccountIssuer.Msft
? AccountType.Microsoft
: AccountType.WorkSchool;
const account: IAccount = {
key: {
providerId: this.providerSettings.id,
id: key,
accountVersion: Constants.accountVersion
},
name: displayName,
displayInfo: {
accountType: accountType,
userId: key,
contextualDisplayName: contextualDisplayName,
displayName,
email,
name
},
properties: {
providerSettings: this.providerSettings,
isMsAccount: accountIssuer === Constants.AccountIssuer.Msft,
owningTenant: owningTenant,
tenants,
azureAuthType: this.authType
},
isStale: false
} as IAccount;
return account;
}
//#endregion
//#region inconsequential
protected getTokenClaims(accessToken: string): ITokenClaims {
try {
const split = accessToken.split('.');
return JSON.parse(Buffer.from(split[1], 'base64').toString('utf8'));
} catch (ex) {
throw new Error('Unable to read token claims: ' + JSON.stringify(ex));
}
}
protected toBase64UrlEncoding(base64string: string): string {
return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding
}
public async deleteAllCache(): Promise<void> {
this.clientApplication.clearCache();
}
public async clearCredentials(account: IAccount): Promise<void> {
try {
const tokenCache = this.clientApplication.getTokenCache();
let accountInfo: AccountInfo | null = await this.getAccountFromMsalCache(account.key.id);
await tokenCache.removeAccount(accountInfo!);
} catch (ex) {
// We need not prompt user for error if token could not be removed from cache.
this.logger.error('Error when removing token from cache: ', ex);
}
}
// tslint:disable:no-empty
public async autoOAuthCancelled(): Promise<void> { }
//#endregion
}
//#region models
export interface IAccountKey {
/**
* Account Key - uniquely identifies an account
*/
key: string;
}
export interface IAccessToken extends IAccountKey {
/**
* Access Token
*/
token: string;
}
export interface IRefreshToken extends IAccountKey {
/**
* Refresh Token
*/
token: string;
}
export interface ITenantResponse { // https://docs.microsoft.com/en-us/rest/api/resources/tenants/list
id: string;
tenantId: string;
displayName?: string;
tenantCategory?: string;
}
export interface IMultiTenantTokenResponse {
[tenantId: string]: IToken | undefined;
}
export interface IToken extends IAccountKey {
/**
* Access token
*/
token: string;
/**
* Access token expiry timestamp
*/
expiresOn?: number;
/**
* TokenType
*/
tokenType: string;
}
export interface ITokenClaims { // https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens
/**
* Identifies the intended recipient of the token. In id_tokens, the audience
* is your app's Application ID, assigned to your app in the Azure portal.
* This value should be validated. The token should be rejected if it fails
* to match your app's Application ID.
*/
aud: string;
/**
* Identifies the issuer, or 'authorization server' that constructs and
* returns the token. It also identifies the Azure AD tenant for which
* the user was authenticated. If the token was issued by the v2.0 endpoint,
* the URI will end in /v2.0. The GUID that indicates that the user is a consumer
* user from a Microsoft account is 9188040d-6c67-4c5b-b112-36a304b66dad.
* Your app should use the GUID portion of the claim to restrict the set of
* tenants that can sign in to the app, if applicable.
*/
iss: string;
/**
* 'Issued At' indicates when the authentication for this token occurred.
*/
iat: number;
/**
* Records the identity provider that authenticated the subject of the token.
* This value is identical to the value of the Issuer claim unless the user
* account not in the same tenant as the issuer - guests, for instance.
* If the claim isn't present, it means that the value of iss can be used instead.
* For personal accounts being used in an organizational context (for instance,
* a personal account invited to an Azure AD tenant), the idp claim may be
* 'live.com' or an STS URI containing the Microsoft account tenant
* 9188040d-6c67-4c5b-b112-36a304b66dad.
*/
idp: string;
/**
* The 'nbf' (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing.
*/
nbf: number;
/**
* The 'exp' (expiration time) claim identifies the expiration time on or
* after which the JWT must not be accepted for processing. It's important
* to note that in certain circumstances, a resource may reject the token
* before this time. For example, if a change in authentication is required
* or a token revocation has been detected.
*/
exp: number;
home_oid?: string;
/**
* The code hash is included in ID tokens only when the ID token is issued with an
* OAuth 2.0 authorization code. It can be used to validate the authenticity of an
* authorization code. To understand how to do this validation, see the OpenID
* Connect specification.
*/
c_hash: string;
/**
* The access token hash is included in ID tokens only when the ID token is issued
* from the /authorize endpoint with an OAuth 2.0 access token. It can be used to
* validate the authenticity of an access token. To understand how to do this validation,
* see the OpenID Connect specification. This is not returned on ID tokens from the /token endpoint.
*/
at_hash: string;
/**
* An internal claim used by Azure AD to record data for token reuse. Should be ignored.
*/
aio: string;
/**
* The primary username that represents the user. It could be an email address, phone number,
* or a generic username without a specified format. Its value is mutable and might change
* over time. Since it is mutable, this value must not be used to make authorization decisions.
* It can be used for username hints, however, and in human-readable UI as a username. The profile
* scope is required in order to receive this claim. Present only in v2.0 tokens.
*/
preferred_username: string;
/**
* The email claim is present by default for guest accounts that have an email address.
* Your app can request the email claim for managed users (those from the same tenant as the resource)
* using the email optional claim. On the v2.0 endpoint, your app can also request the email OpenID
* Connect scope - you don't need to request both the optional claim and the scope to get the claim.
*/
email: string;
/**
* The name claim provides a human-readable value that identifies the subject of the token. The value
* isn't guaranteed to be unique, it can be changed, and it's designed to be used only for display purposes.
* The profile scope is required to receive this claim.
*/
name: string;
/**
* The nonce matches the parameter included in the original /authorize request to the IDP. If it does not
* match, your application should reject the token.
*/
nonce: string;
/**
* The immutable identifier for an object in the Microsoft identity system, in this case, a user account.
* This ID uniquely identifies the user across applications - two different applications signing in the
* same user will receive the same value in the oid claim. The Microsoft Graph will return this ID as
* the id property for a given user account. Because the oid allows multiple apps to correlate users,
* the profile scope is required to receive this claim. Note that if a single user exists in multiple
* tenants, the user will contain a different object ID in each tenant - they're considered different
* accounts, even though the user logs into each account with the same credentials. The oid claim is a
* GUID and cannot be reused.
*/
oid: string;
/**
* The set of roles that were assigned to the user who is logging in.
*/
roles: string[];
/**
* An internal claim used by Azure to revalidate tokens. Should be ignored.
*/
rh: string;
/**
* The principal about which the token asserts information, such as the user
* of an app. This value is immutable and cannot be reassigned or reused.
* The subject is a pairwise identifier - it is unique to a particular application ID.
* If a single user signs into two different apps using two different client IDs,
* those apps will receive two different values for the subject claim.
* This may or may not be wanted depending on your architecture and privacy requirements.
*/
sub: string;
/**
* Represents the tenant that the user is signing in to. For work and school accounts,
* the GUID is the immutable tenant ID of the organization that the user is signing in to.
* For sign-ins to the personal Microsoft account tenant (services like Xbox, Teams for Life, or Outlook),
* the value is 9188040d-6c67-4c5b-b112-36a304b66dad.
*/
tid: string;
/**
* Only present in v1.0 tokens. Provides a human readable value that identifies the subject of the token.
* This value is not guaranteed to be unique within a tenant and should be used only for display purposes.
*/
unique_name: string;
/**
* Token identifier claim, equivalent to jti in the JWT specification. Unique, per-token identifier that is case-sensitive.
*/
uti: string;
/**
* Indicates the version of the id_token.
*/
ver: string;
}
export type OAuthTokenResponse = { accessToken: IAccessToken, refreshToken: IRefreshToken | undefined, tokenClaims: ITokenClaims, expiresOn: string };
export interface ITokenPostData {
grant_type: 'refresh_token' | 'authorization_code' | 'urn:ietf:params:oauth:grant-type:device_code';
client_id: string;
resource: string;
}
export interface IRefreshTokenPostData extends ITokenPostData {
grant_type: 'refresh_token';
refresh_token: string;
client_id: string;
tenant: string;
}
export interface IAuthorizationCodePostData extends ITokenPostData {
grant_type: 'authorization_code';
code: string;
code_verifier: string;
redirect_uri: string;
}
export interface IDeviceCodeStartPostData extends Omit<ITokenPostData, 'grant_type'> {
}
export interface IDeviceCodeCheckPostData extends Omit<ITokenPostData, 'resource'> {
grant_type: 'urn:ietf:params:oauth:grant-type:device_code';
tenant: string;
code: string;
}
//#endregion

Просмотреть файл

@ -0,0 +1,210 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { AuthenticationResult, AuthorizationCodeRequest, AuthorizationUrlRequest, CryptoProvider, PublicClientApplication } from '@azure/msal-node';
import { ITenant, AzureAuthType, IProviderSettings } from '../../models/contracts/azure';
import { IDeferred } from '../../models/interfaces';
import { Logger } from '../../models/logger';
import { MsalAzureAuth } from './msalAzureAuth';
import { SimpleWebServer } from '../simpleWebServer';
import { AzureAuthError } from '../azureAuthError';
import * as Constants from '../constants';
import * as LocalizedConstants from '../../constants/localizedConstants';
import * as path from 'path';
import * as http from 'http';
import { promises as fs } from 'fs';
import VscodeWrapper from '../../controllers/vscodeWrapper';
interface ICryptoValues {
nonce: string;
challengeMethod: string;
codeVerifier: string;
codeChallenge: string;
}
export class MsalAzureCodeGrant extends MsalAzureAuth {
private pkceCodes: ICryptoValues;
private cryptoProvider: CryptoProvider;
constructor(
protected readonly providerSettings: IProviderSettings,
protected readonly context: vscode.ExtensionContext,
protected clientApplication: PublicClientApplication,
protected readonly vscodeWrapper: VscodeWrapper,
protected readonly logger: Logger) {
super(providerSettings, context, clientApplication, AzureAuthType.AuthCodeGrant, vscodeWrapper, logger);
this.cryptoProvider = new CryptoProvider();
this.pkceCodes = {
nonce: '',
challengeMethod: Constants.s256CodeChallengeMethod, // 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: ITenant): Promise<{ response: AuthenticationResult; authComplete: IDeferred<void, Error>; }> {
let authCompleteDeferred: IDeferred<void, Error>;
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
let serverPort: string;
const server = new SimpleWebServer();
try {
serverPort = await server.startup();
} catch (ex) {
const msg = LocalizedConstants.azureServerCouldNotStart;
throw new AzureAuthError(msg, 'Server could not start', ex);
}
await this.createCryptoValuesMsal();
const state = `${serverPort},${this.pkceCodes.nonce}`;
let authCodeRequest: AuthorizationCodeRequest;
let authority = this.loginEndpointUrl + tenant.id;
this.logger.info(`Authority URL set to: ${authority}`);
try {
let authUrlRequest: AuthorizationUrlRequest;
authUrlRequest = {
scopes: this.scopes,
redirectUri: `${this.redirectUri}:${serverPort}/redirect`,
codeChallenge: this.pkceCodes.codeChallenge,
codeChallengeMethod: this.pkceCodes.challengeMethod,
prompt: Constants.selectAccount,
authority: authority,
state: state
};
authCodeRequest = {
scopes: this.scopes,
redirectUri: `${this.redirectUri}:${serverPort}/redirect`,
codeVerifier: this.pkceCodes.codeVerifier,
authority: authority,
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;
} catch (e) {
this.logger.error('MSAL: Error requesting auth code', e);
throw new AzureAuthError('error', 'Error requesting auth code', e);
}
let result = await this.clientApplication.acquireTokenByCode(authCodeRequest);
if (!result) {
this.logger.error('Failed to acquireTokenByCode');
this.logger.error(`Auth Code Request: ${JSON.stringify(authCodeRequest)}`);
throw Error('Failed to fetch token using auth code');
} else {
return {
response: result,
authComplete: authCompleteDeferred!
};
}
}
private async addServerListeners(server: SimpleWebServer, nonce: string, loginUrl: string, authComplete: Promise<void>): Promise<string> {
const mediaPath = path.join(this.context.extensionPath, 'media');
// Utility function
const sendFile = async (res: http.ServerResponse, filePath: string, contentType: string): Promise<void> => {
let fileContents;
try {
fileContents = await fs.readFile(filePath);
} catch (ex) {
this.logger.error(ex);
res.writeHead(400);
res.end();
return;
}
res.writeHead(200, {
'Content-Length': fileContents.length,
'Content-Type': contentType
});
res.end(fileContents);
};
server.on('/landing.css', (req, reqUrl, res) => {
sendFile(res, path.join(mediaPath, 'landing.css'), 'text/css; charset=utf-8').catch(this.logger.error);
});
server.on('/SignIn.svg', (req, reqUrl, res) => {
sendFile(res, path.join(mediaPath, 'SignIn.svg'), 'image/svg+xml').catch(this.logger.error);
});
server.on('/signin', (req, reqUrl, res) => {
let receivedNonce: string = reqUrl.query.nonce as string;
receivedNonce = receivedNonce.replace(/ /g, '+');
if (receivedNonce !== nonce) {
res.writeHead(400, { 'content-type': 'text/html' });
res.write(LocalizedConstants.azureAuthNonceError);
res.end();
this.logger.error('nonce no match', receivedNonce, nonce);
return;
}
res.writeHead(302, { Location: loginUrl });
res.end();
});
return new Promise<string>((resolve, reject) => {
server.on('/redirect', (req, reqUrl, res) => {
const state = reqUrl.query.state as string ?? '';
const split = state.split(',');
if (split.length !== 2) {
res.writeHead(400, { 'content-type': 'text/html' });
res.write(LocalizedConstants.azureAuthStateError);
res.end();
reject(new Error('State mismatch'));
return;
}
const port = split[0];
res.writeHead(302, { Location: `http://127.0.0.1:${port}/callback${reqUrl.search}` });
res.end();
});
server.on('/callback', (req, reqUrl, res) => {
const state = reqUrl.query.state as string ?? '';
const code = reqUrl.query.code as string ?? '';
const stateSplit = state.split(',');
if (stateSplit.length !== 2) {
res.writeHead(400, { 'content-type': 'text/html' });
res.write(LocalizedConstants.azureAuthStateError);
res.end();
reject(new Error('State mismatch'));
return;
}
if (stateSplit[1] !== encodeURIComponent(nonce)) {
res.writeHead(400, { 'content-type': 'text/html' });
res.write(LocalizedConstants.azureAuthNonceError);
res.end();
reject(new Error('Nonce mismatch'));
return;
}
resolve(code);
authComplete.then(() => {
sendFile(res, path.join(mediaPath, 'landing.html'), 'text/html; charset=utf-8').catch(console.error);
}, (ex: Error) => {
res.writeHead(400, { 'content-type': 'text/html' });
res.write(ex.message);
res.end();
});
});
});
}
private async createCryptoValuesMsal(): Promise<void> {
this.pkceCodes.nonce = this.cryptoProvider.createNewGuid();
const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();
this.pkceCodes.codeVerifier = verifier;
this.pkceCodes.codeChallenge = challenge;
}
}

Просмотреть файл

@ -0,0 +1,186 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ILoggerCallback, LogLevel as MsalLogLevel } from '@azure/msal-common';
import { Configuration, PublicClientApplication } from '@azure/msal-node';
import * as Constants from '../../constants/constants';
import * as LocalizedConstants from '../../constants/localizedConstants';
import { ConnectionProfile } from '../../models/connectionProfile';
import { AzureAuthType, IAADResource, IAccount, IToken } from '../../models/contracts/azure';
import { AccountStore } from '../accountStore';
import { AzureController } from '../azureController';
import { getAzureActiveDirectoryConfig, getEnableSqlAuthenticationProviderConfig } from '../utils';
import { HttpClient } from './httpClient';
import { MsalAzureAuth } from './msalAzureAuth';
import { MsalAzureCodeGrant } from './msalAzureCodeGrant';
import { MsalAzureDeviceCode } from './msalAzureDeviceCode';
import { MsalCachePluginProvider } from './msalCachePlugin';
export class MsalAzureController extends AzureController {
private _authMappings = new Map<AzureAuthType, MsalAzureAuth>();
private _cachePluginProvider: MsalCachePluginProvider | undefined = undefined;
protected clientApplication: PublicClientApplication;
private getLoggerCallback(): ILoggerCallback {
return (level: number, message: string, containsPii: boolean) => {
if (!containsPii) {
switch (level) {
case MsalLogLevel.Error:
this.logger.error(message);
break;
case MsalLogLevel.Info:
this.logger.info(message);
break;
case MsalLogLevel.Verbose:
default:
this.logger.verbose(message);
break;
}
} else {
this.logger.pii(message);
}
};
}
public init(): void {
// Since this setting is only applicable to MSAL, we can enable it safely only for MSAL Controller
if (getEnableSqlAuthenticationProviderConfig()) {
this._isSqlAuthProviderEnabled = true;
}
}
public async login(authType: AzureAuthType): Promise<IAccount | undefined> {
let azureAuth = await this.getAzureAuthInstance(authType);
let response = await azureAuth!.startLogin();
return response ? response as IAccount : undefined;
}
private async getAzureAuthInstance(authType: AzureAuthType): Promise<MsalAzureAuth | undefined> {
if (!this._authMappings.has(authType)) {
await this.handleAuthMapping();
}
return this._authMappings!.get(authType);
}
public async getAccountSecurityToken(account: IAccount, tenantId: string, settings: IAADResource): Promise<IToken | undefined> {
let azureAuth = await this.getAzureAuthInstance(getAzureActiveDirectoryConfig());
if (azureAuth) {
this.logger.piiSantized(`Getting account security token for ${JSON.stringify(account?.key)} (tenant ${tenantId}). Auth Method = ${AzureAuthType[account?.properties.azureAuthType]}`, [], []);
tenantId = tenantId || account.properties.owningTenant.id;
let result = await azureAuth.getToken(account, tenantId, settings);
if (!result || !result.account || !result.account.idTokenClaims) {
this.logger.error(`MSAL: getToken call failed`);
throw Error('Failed to get token');
} else {
const token: IToken = {
key: result.account.homeAccountId,
token: result.accessToken,
tokenType: result.tokenType,
expiresOn: result.account.idTokenClaims.exp
};
return token;
}
} else {
account.isStale = true;
this.logger.error(`_getAccountSecurityToken: Authentication method not found for account ${account.displayInfo.displayName}`);
throw Error('Failed to get authentication method, please remove and re-add the account');
}
}
public async refreshAccessToken(account: IAccount, accountStore: AccountStore, tenantId: string | undefined,
settings: IAADResource): Promise<IToken | undefined> {
try {
let azureAuth = await this.getAzureAuthInstance(getAzureActiveDirectoryConfig());
let newAccount = await azureAuth!.refreshAccessToken(account, 'organizations',
this._providerSettings.resources.windowsManagementResource);
if (newAccount!.isStale === true) {
return undefined;
}
await accountStore.addAccount(newAccount!);
return await this.getAccountSecurityToken(
account, tenantId!, settings
);
} catch (ex) {
this._vscodeWrapper.showErrorMessage(ex);
}
}
/**
* Gets the token for given account and updates the connection profile with token information needed for AAD authentication
*/
public async populateAccountProperties(profile: ConnectionProfile, accountStore: AccountStore, settings: IAADResource): Promise<ConnectionProfile> {
let account = await this.addAccount(accountStore);
profile.user = account!.displayInfo.displayName;
profile.email = account!.displayInfo.email;
profile.accountId = account!.key.id;
// Skip fetching access token for profile if Sql Authentication Provider is enabled.
if (!this.isSqlAuthProviderEnabled()) {
if (!profile.tenantId) {
await this.promptForTenantChoice(account!, profile);
}
const token = await this.getAccountSecurityToken(
account!, profile.tenantId, settings
);
if (!token) {
let errorMessage = LocalizedConstants.msgGetTokenFail;
this.logger.error(errorMessage);
this._vscodeWrapper.showErrorMessage(errorMessage);
} else {
profile.azureAccountToken = token.token;
profile.expiresOn = token.expiresOn;
}
} else {
this.logger.verbose('SQL Authentication Provider is enabled, access token will not be acquired by extension.');
}
return profile;
}
public async removeAccount(account: IAccount): Promise<void> {
let azureAuth = await this.getAzureAuthInstance(getAzureActiveDirectoryConfig());
await azureAuth!.clearCredentials(account);
}
public async handleAuthMapping(): Promise<void> {
if (!this.clientApplication) {
let storagePath = await this.findOrMakeStoragePath();
this._cachePluginProvider = new MsalCachePluginProvider(Constants.msalCacheFileName, storagePath!, this.logger);
const msalConfiguration: Configuration = {
auth: {
clientId: this._providerSettings.clientId,
authority: 'https://login.windows.net/common'
},
system: {
loggerOptions: {
loggerCallback: this.getLoggerCallback(),
logLevel: MsalLogLevel.Trace,
piiLoggingEnabled: true
},
networkClient: new HttpClient()
},
cache: {
cachePlugin: this._cachePluginProvider?.getCachePlugin()
}
};
this.clientApplication = new PublicClientApplication(msalConfiguration);
}
this._authMappings.clear();
const configuration = getAzureActiveDirectoryConfig();
if (configuration === AzureAuthType.AuthCodeGrant) {
this._authMappings.set(AzureAuthType.AuthCodeGrant, new MsalAzureCodeGrant(
this._providerSettings, this.context, this.clientApplication, this._vscodeWrapper, this.logger));
} else if (configuration === AzureAuthType.DeviceCode) {
this._authMappings.set(AzureAuthType.DeviceCode, new MsalAzureDeviceCode(
this._providerSettings, this.context, this.clientApplication, this._vscodeWrapper, this.logger));
}
}
}

Просмотреть файл

@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AuthenticationResult, DeviceCodeRequest, PublicClientApplication } from '@azure/msal-node';
import * as vscode from 'vscode';
import * as LocalizedConstants from '../../constants/localizedConstants';
import VscodeWrapper from '../../controllers/vscodeWrapper';
import { AzureAuthType, IProviderSettings, ITenant } from '../../models/contracts/azure';
import { IDeferred } from '../../models/interfaces';
import { Logger } from '../../models/logger';
import { MsalAzureAuth } from './msalAzureAuth';
export class MsalAzureDeviceCode extends MsalAzureAuth {
constructor(
protected readonly providerSettings: IProviderSettings,
protected readonly context: vscode.ExtensionContext,
protected clientApplication: PublicClientApplication,
protected readonly vscodeWrapper: VscodeWrapper,
protected readonly logger: Logger) {
super(providerSettings, context, clientApplication, AzureAuthType.DeviceCode, vscodeWrapper, logger);
}
protected async login(tenant: ITenant): Promise<{ response: AuthenticationResult; authComplete: IDeferred<void, Error>; }> {
let authCompleteDeferred: IDeferred<void, Error>;
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
let authority = this.loginEndpointUrl + tenant.id;
this.logger.info(`Authority URL set to: ${authority}`);
const deviceCodeRequest: DeviceCodeRequest = {
scopes: this.scopes,
authority: authority,
deviceCodeCallback: async (response) => {
await this.displayDeviceCodeScreen(response.message, response.userCode, response.verificationUri);
}
};
const authResult = await this.clientApplication.acquireTokenByDeviceCode(deviceCodeRequest);
this.logger.pii(`Authentication completed for account: ${authResult?.account!.name}, tenant: ${authResult?.tenantId}`);
this.closeOnceComplete(authCompletePromise).catch(this.logger.error);
return {
response: authResult!,
authComplete: authCompleteDeferred!
};
}
private async closeOnceComplete(promise: Promise<void>): Promise<void> {
await promise;
}
public async displayDeviceCodeScreen(msg: string, userCode: string, verificationUrl: string): Promise<void> {
// create a notification with the device code message, usercode, and verificationurl
const selection = await this.vscodeWrapper.showInformationMessage(msg, LocalizedConstants.msgCopyAndOpenWebpage);
if (selection === LocalizedConstants.msgCopyAndOpenWebpage) {
this.vscodeWrapper.clipboardWriteText(userCode);
await vscode.env.openExternal(vscode.Uri.parse(verificationUrl));
console.log(msg);
console.log(userCode);
console.log(verificationUrl);
}
return;
}
}

Просмотреть файл

@ -0,0 +1,120 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICachePlugin, TokenCacheContext } from '@azure/msal-node';
import { promises as fsPromises } from 'fs';
import * as lockFile from 'lockfile';
import * as path from 'path';
import { Logger } from '../../models/logger';
export class MsalCachePluginProvider {
constructor(
private readonly _serviceName: string,
private readonly _msalFilePath: string,
private readonly _logger: Logger
) {
this._msalFilePath = path.join(this._msalFilePath, this._serviceName);
this._serviceName = this._serviceName.replace(/-/, '_');
this._logger.verbose(`MsalCachePluginProvider: Using cache path ${_msalFilePath} and serviceName ${_serviceName}`);
}
private _lockTaken: boolean = false;
private getLockfilePath(): string {
return this._msalFilePath + '.lockfile';
}
public getCachePlugin(): ICachePlugin {
const lockFilePath = this.getLockfilePath();
const beforeCacheAccess = async (cacheContext: TokenCacheContext): Promise<void> => {
await this.waitAndLock(lockFilePath);
try {
const cache = await fsPromises.readFile(this._msalFilePath, { encoding: 'utf8' });
try {
cacheContext.tokenCache.deserialize(cache);
} catch (e) {
// Handle deserialization error in cache file in case file gets corrupted.
// Clearing cache here will ensure account is marked stale so re-authentication can be triggered.
this._logger.verbose(`MsalCachePlugin: Error occurred when trying to read cache file, file contents will be cleared: ${e.message}`);
await fsPromises.writeFile(this._msalFilePath, '', { encoding: 'utf8' });
}
this._logger.verbose(`MsalCachePlugin: Token read from cache successfully.`);
} catch (e) {
if (e.code === 'ENOENT') {
// File doesn't exist, log and continue
this._logger.verbose(`MsalCachePlugin: Cache file not found on disk: ${e.code}`);
} else {
this._logger.error(`MsalCachePlugin: Failed to read from cache file: ${e}`);
throw e;
}
} finally {
lockFile.unlockSync(lockFilePath);
this._lockTaken = false;
}
};
const afterCacheAccess = async (cacheContext: TokenCacheContext): Promise<void> => {
if (cacheContext.cacheHasChanged) {
await this.waitAndLock(lockFilePath);
try {
const data = cacheContext.tokenCache.serialize();
await fsPromises.writeFile(this._msalFilePath, data, { encoding: 'utf8' });
this._logger.verbose(`MsalCachePlugin: Token written to cache successfully.`);
} catch (e) {
this._logger.error(`MsalCachePlugin: Failed to write to cache file. ${e}`);
throw e;
} finally {
lockFile.unlockSync(lockFilePath);
this._lockTaken = false;
}
}
};
// This is an implementation of ICachePlugin that uses the beforeCacheAccess and afterCacheAccess callbacks to read and write to a file
// Ref https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-node-migration#enable-token-caching
// In future we should use msal-node-extensions to provide a secure storage of tokens, instead of implementing our own
// However - as of now this library does not come with pre-compiled native libraries that causes runtime issues
// Ref https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/3332
return {
beforeCacheAccess,
afterCacheAccess
};
}
private async waitAndLock(lockFilePath: string): Promise<void> {
// Make 500 retry attempts with 100ms wait time between each attempt to allow enough time for the lock to be released.
const retries = 500;
const retryWait = 100;
// We cannot rely on lockfile.lockSync() to clear stale lockfile,
// so we check if the lockfile exists and if it does, calling unlockSync() will clear it.
if (lockFile.checkSync(lockFilePath) && !this._lockTaken) {
lockFile.unlockSync(lockFilePath);
this._logger.verbose(`MsalCachePlugin: Stale lockfile found and has been removed.`);
}
let retryAttempt = 0;
while (retryAttempt <= retries) {
try {
// Use lockfile.lockSync() to ensure only one process is accessing the cache at a time.
// lockfile.lock() does not wait for async callback promise to resolve.
lockFile.lockSync(lockFilePath);
this._lockTaken = true;
break;
} catch (e) {
if (retryAttempt === retries) {
this._logger.error(`MsalCachePlugin: Failed to acquire lock on cache file after ${retries} attempts.`);
throw new Error(`Failed to acquire lock on cache file after ${retries} attempts. Please try clearing Access token cache.`);
}
retryAttempt++;
this._logger.verbose(`MsalCachePlugin: Failed to acquire lock on cache file. Retrying in ${retryWait} ms.`);
// tslint:disable:no-empty
setTimeout(() => { }, retryWait);
}
}
}
}

Просмотреть файл

@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { NetworkResponse } from '@azure/msal-common';
export class NetworkUtils {
static getNetworkResponse<T>(headers: Record<string, string>, body: T, statusCode: number): NetworkResponse<T> {
return {
headers: headers,
body: body,
status: statusCode
};
}
}

Просмотреть файл

@ -3,10 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ProviderSettings } from '@microsoft/ads-adal-library';
import { IProviderSettings } from '../models/contracts/azure';
const publicAzureSettings: ProviderSettings = {
const publicAzureSettings: IProviderSettings = {
displayName: 'publicCloudDisplayName',
id: 'azure_publicCloud',
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
@ -22,34 +21,18 @@ const publicAzureSettings: ProviderSettings = {
azureManagementResource: {
id: 'arm',
resource: 'AzureResourceManagement',
endpoint: 'https://management.azure.com'
endpoint: 'https://management.azure.com/'
},
// graphResource: {
// id: '',
// resource: '',
// endpoint: ''
// },
databaseResource: {
id: 'sql',
resource: 'Sql',
endpoint: 'https://database.windows.net/'
}
// ossRdbmsResource: {
// id: '',
// resource: '',
// endpoint: ''
// },
// azureKeyVaultResource: {
// id: '',
// resource: '',
// endpoint: ''
// },
// azureDevopsResource: {
// id: '',
// resource: '',
// endpoint: ''
// }
}
},
scopes: [
'openid', 'email', 'profile', 'offline_access',
'https://management.azure.com/user_impersonation'
]
};
const allSettings = publicAzureSettings;

Просмотреть файл

@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as http from 'http';
import * as url from 'url';
import { AddressInfo } from 'net';

Просмотреть файл

@ -7,12 +7,21 @@ import { ResourceManagementClient } from '@azure/arm-resources';
import { SqlManagementClient } from '@azure/arm-sql';
import { SubscriptionClient } from '@azure/arm-subscriptions';
import { PagedAsyncIterableIterator } from '@azure/core-paging';
import { Token } from 'vscode-mssql';
import { HttpsProxyAgentOptions } from 'https-proxy-agent';
import { parse } from 'url';
import * as vscode from 'vscode';
import * as Constants from '../constants/constants';
import { getProxyAgentOptions } from '../languageservice/proxy';
import { AuthLibrary, AzureAuthType, IToken } from '../models/contracts/azure';
import * as Constants from './constants';
import { TokenCredentialWrapper } from './credentialWrapper';
import { HttpClient } from './msal/httpClient';
const configAzureAD = 'azureActiveDirectory';
const configAzureAuthLibrary = 'azureAuthenticationLibrary';
const configProxy = 'proxy';
const configProxyStrictSSL = 'proxyStrictSSL';
const configProxyAuthorization = 'proxyAuthorization';
/**
* Helper method to convert azure results that comes as pages to an array
@ -20,41 +29,87 @@ const configAzureAD = 'azureActiveDirectory';
* @param convertor a function to convert a value in page to the expected value to add to array
* @returns array or Azure resources
*/
export async function getAllValues<T, TResult>(pages: PagedAsyncIterableIterator<T>, convertor: (input: T) => TResult): Promise<TResult[]> {
export async function getAllValues<T, TResult>(pages: PagedAsyncIterableIterator<T>, convertor: (input: T) => TResult | undefined): Promise<TResult[]> {
let values: TResult[] = [];
let newValue = await pages.next();
while (!newValue.done) {
values.push(convertor(newValue.value));
values.push(convertor(newValue.value)!);
newValue = await pages.next();
}
return values;
}
export type SubscriptionClientFactory = (token: Token) => SubscriptionClient;
export function defaultSubscriptionClientFactory(token: Token): SubscriptionClient {
export type SubscriptionClientFactory = (token: IToken) => SubscriptionClient;
export function defaultSubscriptionClientFactory(token: IToken): SubscriptionClient {
return new SubscriptionClient(new TokenCredentialWrapper(token));
}
export type ResourceManagementClientFactory = (token: Token, subscriptionId: string) => ResourceManagementClient;
export function defaultResourceManagementClientFactory(token: Token, subscriptionId: string): ResourceManagementClient {
export type ResourceManagementClientFactory = (token: IToken, subscriptionId: string) => ResourceManagementClient;
export function defaultResourceManagementClientFactory(token: IToken, subscriptionId: string): ResourceManagementClient {
return new ResourceManagementClient(new TokenCredentialWrapper(token), subscriptionId);
}
export type SqlManagementClientFactory = (token: Token, subscriptionId: string) => SqlManagementClient;
export function defaultSqlManagementClientFactory(token: Token, subscriptionId: string): SqlManagementClient {
export type SqlManagementClientFactory = (token: IToken, subscriptionId: string) => SqlManagementClient;
export function defaultSqlManagementClientFactory(token: IToken, subscriptionId: string): SqlManagementClient {
return new SqlManagementClient(new TokenCredentialWrapper(token), subscriptionId);
}
function getConfiguration(): vscode.WorkspaceConfiguration {
return vscode.workspace.getConfiguration(Constants.extensionConfigSectionName);
}
export function getAzureActiveDirectoryConfig(): string {
function getHttpConfiguration(): vscode.WorkspaceConfiguration {
return vscode.workspace.getConfiguration(Constants.httpConfigSectionName);
}
export function getAzureActiveDirectoryConfig(): AzureAuthType {
let config = getConfiguration();
if (config) {
return config.get(configAzureAD);
const val: string | undefined = config.get(configAzureAD);
if (val) {
return AzureAuthType[val];
}
} else {
return undefined;
return AzureAuthType.AuthCodeGrant;
}
}
export function getAzureAuthLibraryConfig(): AuthLibrary {
let config = getConfiguration();
if (config) {
const val: string | undefined = config.get(configAzureAuthLibrary);
if (val) {
return AuthLibrary[val];
}
}
return AuthLibrary.MSAL; // default to MSAL
}
export function getEnableSqlAuthenticationProviderConfig(): boolean {
const config = getConfiguration();
if (config) {
const val: boolean | undefined = config.get(Constants.sqlAuthProviderSection);
if (val !== undefined) {
return val;
}
}
return true; // default setting
}
export function getProxyEnabledHttpClient(): HttpClient {
const proxy = <string>getHttpConfiguration().get(configProxy);
const strictSSL = getHttpConfiguration().get(configProxyStrictSSL, true);
const authorization = getHttpConfiguration().get(configProxyAuthorization);
const url = parse(proxy);
let agentOptions = getProxyAgentOptions(url, proxy, strictSSL);
if (authorization && url.protocol === 'https:') {
let httpsAgentOptions = agentOptions as HttpsProxyAgentOptions;
httpsAgentOptions!.headers = Object.assign(httpsAgentOptions!.headers || {}, {
'Proxy-Authorization': authorization
});
agentOptions = httpsAgentOptions;
}
return new HttpClient(proxy, agentOptions);
}

Просмотреть файл

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
// Collection of Non-localizable Constants
export const vscodeAppName = 'Code';
export const vscodeAppName = 'code';
export const languageId = 'sql';
export const extensionName = 'mssql';
export const extensionConfigSectionName = 'mssql';
@ -19,7 +19,6 @@ export const connectionsArrayName = 'connections';
export const disconnectedServerLabel = 'disconnectedServer';
export const serverLabel = 'Server';
export const folderLabel = 'Folder';
export const azureAccountDirectory = 'Azure Accounts';
export const cmdRunQuery = 'mssql.runQuery';
export const cmdRunCurrentStatement = 'mssql.runCurrentStatement';
export const cmdCancelQuery = 'mssql.cancelQuery';
@ -65,8 +64,11 @@ export const cmdAzureSignIn = 'azure-account.login';
export const cmdAzureSignInWithDeviceCode = 'azure-account.loginWithDeviceCode';
export const cmdAzureSignInToCloud = 'azure-account.loginToCloud';
export const cmdAadRemoveAccount = 'mssql.removeAadAccount';
export const cmdAadAddAccount = 'mssql.addAadAccount';
export const piiLogging = 'piiLogging';
export const mssqlPiiLogging = 'mssql.piiLogging';
export const azureAuthLibrary = 'mssql.azureAuthenticationLibrary';
export const enableSqlAuthenticationProvider = 'mssql.enableSqlAuthenticationProvider';
export const sqlDbPrefix = '.database.windows.net';
export const defaultConnectionTimeout = 15;
export const azureSqlDbConnectionTimeout = 30;
@ -112,7 +114,8 @@ export const databaseString = 'Database';
export const localizedTexts = 'localizedTexts';
export const ipAddressRegex = /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/;
export const configAzureAccount = 'azureAccount';
export const adalCacheFileName = 'azureTokenCache_azure_publicCloud';
export const adalCacheFileName = 'azureTokenCache-azure_publicCloud';
export const msalCacheFileName = 'azureTokenCacheMsal-azure_publicCloud';
// Configuration Constants
export const copyIncludeHeaders = 'copyIncludeHeaders';

Просмотреть файл

@ -4,31 +4,34 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ConnectionCredentials } from '../models/connectionCredentials';
import { NotificationHandler, RequestType } from 'vscode-languageclient';
import { ConnectionDetails, IConnectionInfo, IServerInfo } from 'vscode-mssql';
import { AccountService } from '../azure/accountService';
import { AccountStore } from '../azure/accountStore';
import { AdalAzureController } from '../azure/adal/adalAzureController';
import { AzureController } from '../azure/azureController';
import { MsalAzureController } from '../azure/msal/msalAzureController';
import providerSettings from '../azure/providerSettings';
import { getAzureAuthLibraryConfig } from '../azure/utils';
import * as Constants from '../constants/constants';
import * as LocalizedConstants from '../constants/localizedConstants';
import * as ConnectionContracts from '../models/contracts/connection';
import * as LanguageServiceContracts from '../models/contracts/languageService';
import * as Utils from '../models/utils';
import { FirewallService } from '../firewall/firewallService';
import SqlToolsServerClient from '../languageservice/serviceclient';
import { ConnectionCredentials } from '../models/connectionCredentials';
import { ConnectionProfile } from '../models/connectionProfile';
import { ConnectionStore } from '../models/connectionStore';
import { AuthLibrary, IAccount } from '../models/contracts/azure';
import * as ConnectionContracts from '../models/contracts/connection';
import { ConnectionSummary } from '../models/contracts/connection';
import * as LanguageServiceContracts from '../models/contracts/languageService';
import { EncryptOptions, IConnectionProfile } from '../models/interfaces';
import { PlatformInformation, Runtime } from '../models/platform';
import * as Utils from '../models/utils';
import { IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
import { Deferred } from '../protocol';
import { ConnectionUI } from '../views/connectionUI';
import StatusView from '../views/statusView';
import SqlToolsServerClient from '../languageservice/serviceclient';
import { IPrompter } from '../prompts/question';
import VscodeWrapper from './vscodeWrapper';
import { NotificationHandler, RequestType } from 'vscode-languageclient';
import { Runtime, PlatformInformation } from '../models/platform';
import { Deferred } from '../protocol';
import { AccountService } from '../azure/accountService';
import { FirewallService } from '../firewall/firewallService';
import { EncryptOptions, IConnectionProfile } from '../models/interfaces';
import { ConnectionSummary } from '../models/contracts/connection';
import { AccountStore } from '../azure/accountStore';
import { ConnectionProfile } from '../models/connectionProfile';
import { QuestionTypes, IQuestion } from '../prompts/question';
import { AzureController } from '../azure/azureController';
import { ConnectionDetails, IConnectionInfo, IAccount, ServerInfo } from 'vscode-mssql';
import providerSettings from '../azure/providerSettings';
/**
* Information for a document's connection. Exported for testing purposes.
@ -52,7 +55,7 @@ export class ConnectionInfo {
/**
* Information about the SQL Server instance.
*/
public serverInfo: ServerInfo;
public serverInfo: IServerInfo;
/**
* Whether the connection is in the process of connecting.
@ -70,7 +73,7 @@ export class ConnectionInfo {
public errorMessage: string;
public get loginFailed(): boolean {
return this.errorNumber && this.errorNumber === Constants.errorLoginFailed;
return this.errorNumber !== undefined && this.errorNumber === Constants.errorLoginFailed;
}
}
@ -83,7 +86,7 @@ export default class ConnectionManager {
private _statusView: StatusView;
private _connections: { [fileUri: string]: ConnectionInfo };
private _connectionCredentialsToServerInfoMap:
Map<IConnectionInfo, ServerInfo>;
Map<IConnectionInfo, IServerInfo>;
private _uriToConnectionPromiseMap: Map<string, Deferred<boolean>>;
private _failedUriToFirewallIpMap: Map<string, string>;
private _failedUriToSSLMap: Map<string, string>;
@ -101,7 +104,7 @@ export default class ConnectionManager {
private _accountStore?: AccountStore) {
this._statusView = statusView;
this._connections = {};
this._connectionCredentialsToServerInfoMap = new Map<IConnectionInfo, ServerInfo>();
this._connectionCredentialsToServerInfoMap = new Map<IConnectionInfo, IServerInfo>();
this._uriToConnectionPromiseMap = new Map<string, Deferred<boolean>>();
if (!this.client) {
@ -112,7 +115,7 @@ export default class ConnectionManager {
}
if (!this._connectionStore) {
this._connectionStore = new ConnectionStore(context);
this._connectionStore = new ConnectionStore(context, this.client?.logger);
}
if (!this._accountStore) {
@ -124,7 +127,13 @@ export default class ConnectionManager {
}
if (!this.azureController) {
this.azureController = new AzureController(context, prompter);
const authLibrary = getAzureAuthLibraryConfig();
if (authLibrary === AuthLibrary.ADAL) {
this.azureController = new AdalAzureController(context, prompter);
} else {
this.azureController = new MsalAzureController(context, prompter);
}
this.azureController.init();
}
@ -145,7 +154,7 @@ export default class ConnectionManager {
* Exposed for testing purposes
*/
public get vscodeWrapper(): VscodeWrapper {
return this._vscodeWrapper;
return this._vscodeWrapper!;
}
/**
@ -159,7 +168,7 @@ export default class ConnectionManager {
* Exposed for testing purposes
*/
public get client(): SqlToolsServerClient {
return this._client;
return this._client!;
}
/**
@ -173,7 +182,7 @@ export default class ConnectionManager {
* Get the connection view.
*/
public get connectionUI(): ConnectionUI {
return this._connectionUI;
return this._connectionUI!;
}
/**
@ -194,7 +203,7 @@ export default class ConnectionManager {
* Exposed for testing purposes
*/
public get connectionStore(): ConnectionStore {
return this._connectionStore;
return this._connectionStore!;
}
/**
@ -208,7 +217,7 @@ export default class ConnectionManager {
* Exposed for testing purposes
*/
public get accountStore(): AccountStore {
return this._accountStore;
return this._accountStore!;
}
/**
@ -707,7 +716,7 @@ export default class ConnectionManager {
* Get the server info for a connection
* @param connectionCreds
*/
public getServerInfo(connectionCredentials: IConnectionInfo): ServerInfo {
public getServerInfo(connectionCredentials: IConnectionInfo): IServerInfo {
if (this._connectionCredentialsToServerInfoMap.has(connectionCredentials)) {
return this._connectionCredentialsToServerInfoMap.get(connectionCredentials);
}
@ -773,25 +782,39 @@ export default class ConnectionManager {
title: LocalizedConstants.connectProgressNoticationTitle,
cancellable: false
}, async (_progress, _token) => {
// Check if the azure account token is present before sending connect request
// Check if the azure account token is present before sending connect request (only with SQL Auth Provider is not enabled.)
if (connectionCreds.authenticationType === Constants.azureMfa) {
if (AzureController.isTokenInValid(connectionCreds.azureAccountToken, connectionCreds.expiresOn)) {
let account = this.accountStore.getAccount(connectionCreds.accountId);
let profile = new ConnectionProfile(connectionCreds);
let azureAccountToken = await this.azureController.refreshToken(account, this.accountStore, providerSettings.resources.databaseResource, profile.tenantId);
if (!azureAccountToken) {
let errorMessage = LocalizedConstants.msgAccountRefreshFailed;
let refreshResult = await this.vscodeWrapper.showErrorMessage(errorMessage, LocalizedConstants.refreshTokenLabel);
if (refreshResult === LocalizedConstants.refreshTokenLabel) {
await this.azureController.populateAccountProperties(
profile, this.accountStore, providerSettings.resources.databaseResource);
} else {
throw new Error(LocalizedConstants.cannotConnect);
}
let account: IAccount;
let profile: ConnectionProfile;
if (connectionCreds.accountId) {
account = this.accountStore.getAccount(connectionCreds.accountId);
profile = new ConnectionProfile(connectionCreds);
} else {
connectionCreds.azureAccountToken = azureAccountToken.token;
connectionCreds.expiresOn = azureAccountToken.expiresOn;
throw new Error(LocalizedConstants.cannotConnect);
}
// Always set username
connectionCreds.user = account.displayInfo.displayName;
connectionCreds.email = account.displayInfo.email;
profile.user = account.displayInfo.displayName;
profile.email = account.displayInfo.email;
if (!this.azureController.isSqlAuthProviderEnabled()) {
let azureAccountToken = await this.azureController.refreshAccessToken(account!,
this.accountStore, profile.tenantId, providerSettings.resources.databaseResource!);
if (!azureAccountToken) {
let errorMessage = LocalizedConstants.msgAccountRefreshFailed;
let refreshResult = await this.vscodeWrapper.showErrorMessage(errorMessage, LocalizedConstants.refreshTokenLabel);
if (refreshResult === LocalizedConstants.refreshTokenLabel) {
await this.azureController.populateAccountProperties(
profile, this.accountStore, providerSettings.resources.databaseResource!);
} else {
throw new Error(LocalizedConstants.cannotConnect);
}
} else {
connectionCreds.azureAccountToken = azureAccountToken.token;
connectionCreds.expiresOn = azureAccountToken.expiresOn;
}
}
}
}
@ -834,7 +857,7 @@ export default class ConnectionManager {
connectParams.connection = connectionDetails;
// send connection request message to service host
this._uriToConnectionPromiseMap.set(connectParams.ownerUri, promise);
this._uriToConnectionPromiseMap.set(connectParams.ownerUri, promise!);
try {
const result = await this.client.sendRequest(ConnectionContracts.ConnectionRequest.type, connectParams);
if (!result) {
@ -876,7 +899,7 @@ export default class ConnectionManager {
*/
public onManageProfiles(): Promise<boolean> {
// Show quick pick to create, edit, or remove profiles
return this._connectionUI.promptToManageProfiles();
return this.connectionUI.promptToManageProfiles();
}
public async onCreateProfile(): Promise<boolean> {
@ -967,26 +990,44 @@ export default class ConnectionManager {
return;
}
public async addAccount(): Promise<void> {
let account = await this.connectionUI.addNewAccount();
if (account) {
this.vscodeWrapper.showInformationMessage(Utils.formatString(LocalizedConstants.accountAddedSuccessfully, account.displayInfo.displayName));
} else {
this.vscodeWrapper.showErrorMessage(LocalizedConstants.accountCouldNotBeAdded);
}
}
public async removeAccount(prompter: IPrompter): Promise<void> {
// list options for accounts to remove
let questions: IQuestion[] = [];
let azureAccountChoices = ConnectionProfile.getAccountChoices(this._accountStore);
questions.push(
{
type: QuestionTypes.expand,
name: 'account',
message: LocalizedConstants.azureChooseAccount,
choices: azureAccountChoices
}
);
if (azureAccountChoices.length > 0) {
questions.push(
{
type: QuestionTypes.expand,
name: 'account',
message: LocalizedConstants.azureChooseAccount,
choices: azureAccountChoices
}
);
return prompter.prompt<IAccount>(questions, true).then(async answers => {
if (answers.account) {
this._accountStore.removeAccount(answers.account.key.id);
this.azureController.removeToken(answers.account);
}
});
return prompter.prompt<IAccount>(questions, true).then(async answers => {
if (answers?.account) {
try {
this._accountStore.removeAccount(answers.account.key.id);
this.azureController.removeAccount(answers.account);
this.vscodeWrapper.showInformationMessage(LocalizedConstants.accountRemovedSuccessfully);
} catch (e) {
this.vscodeWrapper.showErrorMessage(Utils.formatString(LocalizedConstants.accountRemovalFailed, e.message));
}
}
});
} else {
this.vscodeWrapper.showInformationMessage(LocalizedConstants.noAzureAccountForRemoval);
}
}
}

Просмотреть файл

@ -4,41 +4,41 @@
*--------------------------------------------------------------------------------------------*/
import * as events from 'events';
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { IConnectionInfo } from 'vscode-mssql';
import { AzureResourceController } from '../azure/azureResourceController';
import * as Constants from '../constants/constants';
import * as LocalizedConstants from '../constants/localizedConstants';
import * as Utils from '../models/utils';
import { SqlOutputContentProvider } from '../models/sqlOutputContentProvider';
import { RebuildIntelliSenseNotification, CompletionExtensionParams, CompletionExtLoadRequest } from '../models/contracts/languageService';
import StatusView from '../views/statusView';
import ConnectionManager from './connectionManager';
import * as ConnInfo from '../models/connectionInfo';
import SqlToolsServerClient from '../languageservice/serviceclient';
import { IPrompter } from '../prompts/question';
import CodeAdapter from '../prompts/adapter';
import VscodeWrapper from './vscodeWrapper';
import UntitledSqlDocumentService from './untitledSqlDocumentService';
import { ISelectionData, IConnectionProfile } from './../models/interfaces';
import * as path from 'path';
import * as fs from 'fs';
import { ObjectExplorerProvider } from '../objectExplorer/objectExplorerProvider';
import { ScriptingService } from '../scripting/scriptingService';
import { TreeNodeInfo } from '../objectExplorer/treeNodeInfo';
import { AccountSignInTreeNode } from '../objectExplorer/accountSignInTreeNode';
import { Deferred } from '../protocol';
import { ConnectTreeNode } from '../objectExplorer/connectTreeNode';
import { ObjectExplorerUtils } from '../objectExplorer/objectExplorerUtils';
import * as ConnInfo from '../models/connectionInfo';
import { CompletionExtensionParams, CompletionExtLoadRequest, RebuildIntelliSenseNotification } from '../models/contracts/languageService';
import { ScriptOperation } from '../models/contracts/scripting/scriptingRequest';
import { QueryHistoryProvider } from '../queryHistory/queryHistoryProvider';
import { SqlOutputContentProvider } from '../models/sqlOutputContentProvider';
import * as Utils from '../models/utils';
import { AccountSignInTreeNode } from '../objectExplorer/accountSignInTreeNode';
import { ConnectTreeNode } from '../objectExplorer/connectTreeNode';
import { ObjectExplorerProvider } from '../objectExplorer/objectExplorerProvider';
import { ObjectExplorerUtils } from '../objectExplorer/objectExplorerUtils';
import { TreeNodeInfo } from '../objectExplorer/treeNodeInfo';
import CodeAdapter from '../prompts/adapter';
import { IPrompter } from '../prompts/question';
import { Deferred } from '../protocol';
import { QueryHistoryNode } from '../queryHistory/queryHistoryNode';
import { DacFxService } from '../services/dacFxService';
import { SqlProjectsService } from '../services/sqlProjectsService';
import { IConnectionInfo } from 'vscode-mssql';
import { SchemaCompareService } from '../services/schemaCompareService';
import { SqlTasksService } from '../services/sqlTasksService';
import { QueryHistoryProvider } from '../queryHistory/queryHistoryProvider';
import { ScriptingService } from '../scripting/scriptingService';
import { AzureAccountService } from '../services/azureAccountService';
import { AzureResourceService } from '../services/azureResourceService';
import { AzureResourceController } from '../azure/azureResourceController';
import { DacFxService } from '../services/dacFxService';
import { SqlProjectsService } from '../services/sqlProjectsService';
import { SchemaCompareService } from '../services/schemaCompareService';
import { SqlTasksService } from '../services/sqlTasksService';
import StatusView from '../views/statusView';
import { IConnectionProfile, ISelectionData } from './../models/interfaces';
import ConnectionManager from './connectionManager';
import UntitledSqlDocumentService from './untitledSqlDocumentService';
import VscodeWrapper from './vscodeWrapper';
/**
* The main controller class that initializes the extension
@ -52,10 +52,10 @@ export default class MainController implements vscode.Disposable {
private _prompter: IPrompter;
private _vscodeWrapper: VscodeWrapper;
private _initialized: boolean = false;
private _lastSavedUri: string;
private _lastSavedTimer: Utils.Timer;
private _lastOpenedUri: string;
private _lastOpenedTimer: Utils.Timer;
private _lastSavedUri: string | undefined;
private _lastSavedTimer: Utils.Timer | undefined;
private _lastOpenedUri: string | undefined;
private _lastOpenedTimer: Utils.Timer | undefined;
private _untitledSqlDocumentService: UntitledSqlDocumentService;
private _objectExplorerProvider: ObjectExplorerProvider;
private _queryHistoryProvider: QueryHistoryProvider;
@ -72,7 +72,9 @@ export default class MainController implements vscode.Disposable {
* The main controller constructor
* @constructor
*/
constructor(context: vscode.ExtensionContext, connectionManager?: ConnectionManager, vscodeWrapper?: VscodeWrapper) {
constructor(context: vscode.ExtensionContext,
connectionManager?: ConnectionManager,
vscodeWrapper?: VscodeWrapper) {
this._context = context;
if (connectionManager) {
this._connectionMgr = connectionManager;
@ -151,6 +153,8 @@ export default class MainController implements vscode.Disposable {
this._event.on(Constants.cmdToggleSqlCmd, async () => { await this.onToggleSqlCmd(); });
this.registerCommand(Constants.cmdAadRemoveAccount);
this._event.on(Constants.cmdAadRemoveAccount, () => this.removeAadAccount(this._prompter));
this.registerCommand(Constants.cmdAadAddAccount);
this._event.on(Constants.cmdAadAddAccount, () => this.addAddAccount());
this.initializeObjectExplorer();
@ -180,8 +184,8 @@ export default class MainController implements vscode.Disposable {
this.dacFxService = new DacFxService(SqlToolsServerClient.instance);
this.schemaCompareService = new SchemaCompareService(SqlToolsServerClient.instance);
const azureResourceController = new AzureResourceController();
this.azureAccountService = new AzureAccountService(this._connectionMgr.azureController, this.connectionManager.accountStore);
this.azureResourceService = new AzureResourceService(this._connectionMgr.azureController, azureResourceController, this.connectionManager.accountStore);
this.azureAccountService = new AzureAccountService(this._connectionMgr.azureController, this._connectionMgr.accountStore);
this.azureResourceService = new AzureResourceService(this._connectionMgr.azureController, azureResourceController, this._connectionMgr.accountStore);
// Add handlers for VS Code generated commands
this._vscodeWrapper.onDidCloseTextDocument(async (params) => await this.onDidCloseTextDocument(params));
@ -1196,7 +1200,7 @@ export default class MainController implements vscode.Disposable {
this._objectExplorerProvider.refreshNode(n);
} catch (e) {
errorFoundWhileRefreshing = true;
Utils.logToOutputChannel(e.toString());
this._connectionMgr.client.logger.error(e);
}
});
if (errorFoundWhileRefreshing) {
@ -1210,6 +1214,12 @@ export default class MainController implements vscode.Disposable {
if (e.affectsConfiguration(Constants.mssqlPiiLogging)) {
this.updatePiiLoggingLevel();
}
// Prompt to reload VS Code when below settings are updated.
if (e.affectsConfiguration(Constants.azureAuthLibrary)
|| e.affectsConfiguration(Constants.enableSqlAuthenticationProvider)) {
await this.displayReloadMessage();
}
}
}
@ -1221,7 +1231,26 @@ export default class MainController implements vscode.Disposable {
SqlToolsServerClient.instance.logger.piiLogging = piiLogging;
}
/**
* Display notification with button to reload
* return true if button clicked
* return false if button not clicked
*/
private async displayReloadMessage(): Promise<boolean> {
const result = await vscode.window.showInformationMessage(LocalizedConstants.reloadPrompt, LocalizedConstants.reloadChoice);
if (result === LocalizedConstants.reloadChoice) {
await vscode.commands.executeCommand('workbench.action.reloadWindow');
return true;
} else {
return false;
}
}
public removeAadAccount(prompter: IPrompter): void {
this.connectionManager.removeAccount(prompter);
}
public addAddAccount(): void {
this.connectionManager.addAccount();
}
}

Просмотреть файл

@ -5,12 +5,12 @@
import * as vscode from 'vscode';
import { TextDocumentShowOptions } from 'vscode';
import { AzureLoginStatus } from '../models/interfaces';
import * as Constants from './../constants/constants';
export import TextEditor = vscode.TextEditor;
export import ConfigurationTarget = vscode.ConfigurationTarget;
import { TextDocumentShowOptions } from 'vscode';
export default class VscodeWrapper {
@ -32,7 +32,7 @@ export default class VscodeWrapper {
* Get the current active text editor
*/
public get activeTextEditor(): vscode.TextEditor {
return vscode.window.activeTextEditor;
return vscode.window.activeTextEditor!;
}
/**
@ -40,7 +40,7 @@ export default class VscodeWrapper {
* has changed. *Note* that the event also fires when the active editor changes
* to `undefined`.
*/
public get onDidChangeActiveTextEditor(): vscode.Event<vscode.TextEditor> {
public get onDidChangeActiveTextEditor(): vscode.Event<vscode.TextEditor | undefined> {
return vscode.window.onDidChangeActiveTextEditor;
}
@ -61,7 +61,7 @@ export default class VscodeWrapper {
/**
* Get the URI string for the current active text editor
*/
public get activeTextEditorUri(): string {
public get activeTextEditorUri(): string | undefined {
if (typeof vscode.window.activeTextEditor !== 'undefined' &&
typeof vscode.window.activeTextEditor.document !== 'undefined') {
return vscode.window.activeTextEditor.document.uri.toString(true);
@ -91,7 +91,7 @@ export default class VscodeWrapper {
* the command handler function doesn't return anything.
* @see vscode.commands.executeCommand
*/
public executeCommand<T>(command: string, ...rest: any[]): Thenable<T> {
public executeCommand<T>(command: string, ...rest: any[]): Thenable<T | undefined> {
return vscode.commands.executeCommand<T>(command, ...rest);
}
@ -226,21 +226,21 @@ export default class VscodeWrapper {
/**
* Formats and shows a vscode error message
*/
public showErrorMessage(msg: string, ...items: string[]): Thenable<string> {
public showErrorMessage(msg: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showErrorMessage(Constants.extensionName + ': ' + msg, ...items);
}
/**
* Shows an input box with given options
*/
public showInputBox(options?: vscode.InputBoxOptions): Thenable<string> {
public showInputBox(options?: vscode.InputBoxOptions): Thenable<string | undefined> {
return vscode.window.showInputBox(options);
}
/**
* Formats and shows a vscode information message
*/
public showInformationMessage(msg: string, ...items: string[]): Thenable<string> {
public showInformationMessage(msg: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showInformationMessage(Constants.extensionName + ': ' + msg, ...items);
}
@ -259,7 +259,7 @@ export default class VscodeWrapper {
* @param options Configures the behavior of the selection list.
* @return A promise that resolves to the selected item or undefined.
*/
public showQuickPick<T extends vscode.QuickPickItem>(items: T[] | Thenable<T[]>, options?: vscode.QuickPickOptions): Thenable<T> {
public showQuickPick<T extends vscode.QuickPickItem>(items: T[] | Thenable<T[]>, options?: vscode.QuickPickOptions): Thenable<T | undefined> {
return vscode.window.showQuickPick<T>(items, options);
}
@ -269,7 +269,7 @@ export default class VscodeWrapper {
* @param options Configures the behavior of the save dialog
* @return A promise that resolves to the selected resource or `undefined`.
*/
public showSaveDialog(options: vscode.SaveDialogOptions): Thenable<vscode.Uri> {
public showSaveDialog(options: vscode.SaveDialogOptions): Thenable<vscode.Uri | undefined> {
return vscode.window.showSaveDialog(options);
}
@ -291,7 +291,7 @@ export default class VscodeWrapper {
/**
* Formats and shows a vscode warning message
*/
public showWarningMessage(msg: string): Thenable<string> {
public showWarningMessage(msg: string): Thenable<string | undefined> {
return vscode.window.showWarningMessage(Constants.extensionName + ': ' + msg);
}
@ -397,7 +397,7 @@ export default class VscodeWrapper {
* but not active
*/
public get azureAccountExtensionActive(): boolean {
return this.azureAccountExtension && this.azureAccountExtension.isActive;
return this.azureAccountExtension !== undefined && this.azureAccountExtension.isActive;
}
/**
@ -405,6 +405,6 @@ export default class VscodeWrapper {
*/
public get isAccountSignedIn(): boolean {
return this.azureAccountExtensionActive &&
this.azureAccountExtension.exports.status === AzureLoginStatus.LoggedIn;
this.azureAccountExtension!.exports.status === AzureLoginStatus.LoggedIn;
}
}

Просмотреть файл

@ -3,14 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import SqlToolsServerClient from '../languageservice/serviceclient';
import * as Contracts from '../models/contracts';
import * as Utils from '../models/utils';
import { ICredentialStore } from './icredentialstore';
import SqlToolsServerClient from '../languageservice/serviceclient';
/**
* Implements a credential storage for Windows, Mac (darwin), or Linux.
*
* Allows a single credential to be stored per service (that is, one username per service);
*/
export class CredentialStore implements ICredentialStore {
@ -29,20 +28,17 @@ export class CredentialStore implements ICredentialStore {
/**
* Gets a credential saved in the credential store
*
* @param {string} credentialId the ID uniquely identifying this credential
* @returns {Promise<Credential>} Promise that resolved to the credential, or undefined if not found
*/
public async readCredential(credentialId: string): Promise<Contracts.Credential> {
let self = this;
let cred: Contracts.Credential = new Contracts.Credential();
cred.credentialId = credentialId;
const returnedCred = await self._client.sendRequest(Contracts.ReadCredentialRequest.type, cred);
const returnedCred = await this._client!.sendRequest(Contracts.ReadCredentialRequest.type, cred);
return returnedCred;
}
public async saveCredential(credentialId: string, password: any): Promise<boolean> {
let self = this;
let cred: Contracts.Credential = new Contracts.Credential();
cred.credentialId = credentialId;
cred.password = password;
@ -52,18 +48,17 @@ export class CredentialStore implements ICredentialStore {
if (Utils.isLinux) {
await this._secretStorage.store(credentialId, password);
}
const success = await self._client.sendRequest(Contracts.SaveCredentialRequest.type, cred);
const success = await this._client!.sendRequest(Contracts.SaveCredentialRequest.type, cred);
return success;
}
public async deleteCredential(credentialId: string): Promise<boolean> {
let self = this;
let cred: Contracts.Credential = new Contracts.Credential();
cred.credentialId = credentialId;
if (Utils.isLinux) {
await this._secretStorage.delete(credentialId);
}
const success = await self._client.sendRequest(Contracts.DeleteCredentialRequest.type, cred);
const success = await this._client!.sendRequest(Contracts.DeleteCredentialRequest.type, cred);
return success;
}
}

Просмотреть файл

@ -3,13 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IPackage, IStatusView, PackageError, IHttpClient } from './interfaces';
import { ILogger } from '../models/interfaces';
import { parse as parseUrl, Url } from 'url';
import * as https from 'https';
import * as http from 'http';
import { getProxyAgent, isBoolean } from './proxy';
import * as fs from 'fs';
import * as http from 'http';
import * as https from 'https';
import { parse as parseUrl, Url } from 'url';
import { ILogger } from '../models/interfaces';
import { IHttpClient, IPackage, IStatusView, PackageError } from './interfaces';
import { getProxyAgent, isBoolean } from './proxy';
/*
* Http client class to handle downloading files using http or https urls
@ -39,13 +39,13 @@ export default class HttpClient implements IHttpClient {
let request = clientRequest(options, response => {
if (response.statusCode === 301 || response.statusCode === 302) {
// Redirect - download from new location
return resolve(this.downloadFile(response.headers.location, pkg, logger, statusView, proxy, strictSSL, authorization));
return resolve(this.downloadFile(response.headers.location!, pkg, logger, statusView, proxy, strictSSL, authorization));
}
if (response.statusCode !== 200) {
// Download failed - print error message
logger.appendLine(`failed (error code '${response.statusCode}')`);
return reject(new PackageError(response.statusCode.toString(), pkg));
return reject(new PackageError(response.statusCode!.toString(), pkg));
}
// If status code is 200
@ -117,7 +117,7 @@ export default class HttpClient implements IHttpClient {
private handleSuccessfulResponse(pkg: IPackage, response: http.IncomingMessage, logger: ILogger, statusView: IStatusView): Promise<void> {
return new Promise<void>((resolve, reject) => {
let progress: IDownloadProgress = {
packageSize: parseInt(response.headers['content-length'], 10),
packageSize: parseInt(response.headers['content-length']!, 10),
dots: 0,
downloadedBytes: 0,
downloadPercentage: 0
@ -126,7 +126,7 @@ export default class HttpClient implements IHttpClient {
response.on('data', data => {
this.handleDataReceivedEvent(progress, data, logger, statusView);
});
let tmpFile = fs.createWriteStream(undefined, { fd: pkg.tmpFile.fd });
let tmpFile = fs.createWriteStream('', { fd: pkg.tmpFile.fd });
response.on('end', () => {
resolve();
});

Просмотреть файл

@ -3,11 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Url, parse as parseUrl } from 'url';
import * as httpProxyAgent from 'http-proxy-agent';
import * as httpsProxyAgent from 'https-proxy-agent';
import { HttpProxyAgent, HttpProxyAgentOptions } from 'http-proxy-agent';
import { HttpsProxyAgent, HttpsProxyAgentOptions } from 'https-proxy-agent';
import { parse as parseUrl, Url } from 'url';
function getSystemProxyURL(requestURL: Url): string {
function getSystemProxyURL(requestURL: Url): string | undefined {
if (requestURL.protocol === 'http:') {
return process.env.HTTP_PROXY || process.env.http_proxy || undefined;
} else if (requestURL.protocol === 'https:') {
@ -24,7 +24,20 @@ export function isBoolean(obj: any): obj is boolean {
/*
* Returns the proxy agent using the proxy url in the parameters or the system proxy. Returns null if no proxy found
*/
export function getProxyAgent(requestURL: Url, proxy?: string, strictSSL?: boolean): any {
export function getProxyAgent(requestURL: Url, proxy?: string, strictSSL?: boolean): HttpsProxyAgent | HttpProxyAgent {
const proxyURL = proxy || getSystemProxyURL(requestURL);
if (!proxyURL) {
return undefined;
}
const proxyEndpoint = parseUrl(proxyURL);
const opts = this.getProxyAgentOptions(requestURL, proxy, strictSSL);
return proxyEndpoint.protocol === 'https:' ? new HttpsProxyAgent(opts as HttpsProxyAgentOptions) : new HttpProxyAgent(opts as HttpProxyAgentOptions);
}
/*
* Returns the proxy agent using the proxy url in the parameters or the system proxy. Returns null if no proxy found
*/
export function getProxyAgentOptions(requestURL: Url, proxy?: string, strictSSL?: boolean): HttpsProxyAgentOptions | HttpProxyAgentOptions {
const proxyURL = proxy || getSystemProxyURL(requestURL);
if (!proxyURL) {
@ -33,16 +46,16 @@ export function getProxyAgent(requestURL: Url, proxy?: string, strictSSL?: boole
const proxyEndpoint = parseUrl(proxyURL);
if (!/^https?:$/.test(proxyEndpoint.protocol)) {
if (!/^https?:$/.test(proxyEndpoint.protocol!)) {
return undefined;
}
const opts = {
const opts: HttpsProxyAgentOptions | HttpProxyAgentOptions = {
host: proxyEndpoint.hostname,
port: Number(proxyEndpoint.port),
auth: proxyEndpoint.auth,
rejectUnauthorized: isBoolean(strictSSL) ? strictSSL : true
};
return requestURL.protocol === 'http:' ? new httpProxyAgent(opts) : new httpsProxyAgent(opts);
return opts;
}

Просмотреть файл

@ -27,6 +27,8 @@ import * as LanguageServiceContracts from '../models/contracts/languageService';
import { IConfig } from '../languageservice/interfaces';
import { exists } from '../utils/utils';
import { env } from 'process';
import { getAzureAuthLibraryConfig, getEnableSqlAuthenticationProviderConfig } from '../azure/utils';
import { AuthLibrary } from '../models/contracts/azure';
const STS_OVERRIDE_ENV_VAR = 'MSSQL_SQLTOOLSSERVICE';
@ -154,9 +156,10 @@ export default class SqlToolsServiceClient {
public static get instance(): SqlToolsServiceClient {
if (this._instance === undefined) {
let config = new ExtConfig();
let vscodeWrapper = new VscodeWrapper();
let logLevel: LogLevel = LogLevel[Utils.getConfigTracingLevel() as keyof typeof LogLevel];
let pii = Utils.getConfigPiiLogging();
_channel = vscode.window.createOutputChannel(Constants.serviceInitializingOutputChannelName);
_channel = vscodeWrapper.createOutputChannel(Constants.serviceInitializingOutputChannelName);
let logger = new Logger(text => _channel.append(text), logLevel, pii);
let serverStatusView = new ServerStatusView();
let httpClient = new HttpClient();
@ -164,7 +167,6 @@ export default class SqlToolsServiceClient {
let downloadProvider = new ServiceDownloadProvider(config, logger, serverStatusView, httpClient,
decompressProvider);
let serviceProvider = new ServerProvider(downloadProvider, config, serverStatusView);
let vscodeWrapper = new VscodeWrapper();
let statusView = new StatusView(vscodeWrapper);
this._instance = new SqlToolsServiceClient(config, serviceProvider, logger, statusView, vscodeWrapper);
}
@ -360,7 +362,7 @@ export default class SqlToolsServiceClient {
}
private generateResourceServiceServerOptions(executablePath: string): ServerOptions {
let launchArgs = Utils.getCommonLaunchArgsAndCleanupOldLogFiles(this._logPath, 'resourceprovider.log', executablePath);
let launchArgs = Utils.getCommonLaunchArgsAndCleanupOldLogFiles(executablePath, this._logPath, 'resourceprovider.log');
return { command: executablePath, args: launchArgs, transport: TransportKind.stdio };
}
@ -389,16 +391,28 @@ export default class SqlToolsServiceClient {
serverArgs = [servicePath];
serverCommand = 'dotnet';
}
// Get the extenion's configuration
let config = vscode.workspace.getConfiguration(Constants.extensionConfigSectionName);
if (config) {
// Populate common args
serverArgs = serverArgs.concat(Utils.getCommonLaunchArgsAndCleanupOldLogFiles(servicePath, this._logPath, 'sqltools.log'));
// Enable diagnostic logging in the service if it is configured
let logDebugInfo = config[Constants.configLogDebugInfo];
if (logDebugInfo) {
serverArgs.push('--enable-logging');
}
// Send application name to determine MSAL cache location
serverArgs.push('--application-name', 'code');
// Enable SQL Auth Provider registration for Azure MFA Authentication
const enableSqlAuthenticationProvider = getEnableSqlAuthenticationProviderConfig();
const azureAuthLibrary = getAzureAuthLibraryConfig();
if (azureAuthLibrary === AuthLibrary.MSAL && enableSqlAuthenticationProvider) {
serverArgs.push('--enable-sql-authentication-provider');
}
// Send Locale for sqltoolsservice localization
let applyLocalization = config[Constants.configApplyLocalization];
if (applyLocalization) {
@ -408,7 +422,6 @@ export default class SqlToolsServiceClient {
}
}
// run the service host using dotnet.exe from the path
let serverOptions: ServerOptions = { command: serverCommand, args: serverArgs, transport: TransportKind.stdio };
return serverOptions;

Просмотреть файл

@ -3,12 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConnectionInfo, IServerInfo } from 'vscode-mssql';
import * as Constants from '../constants/constants';
import * as LocalizedConstants from '../constants/localizedConstants';
import * as Interfaces from './interfaces';
import { EncryptOptions, IConnectionProfile } from '../models/interfaces';
import * as Interfaces from './interfaces';
import * as Utils from './utils';
import { IConnectionInfo, ServerInfo } from 'vscode-mssql';
/**
* Sets sensible defaults for key connection properties, especially
@ -200,7 +200,7 @@ export function getUserNameOrDomainLogin(creds: IConnectionInfo, defaultValue?:
* @param {Interfaces.IConnectionCredentials} connCreds connection
* @returns {string} tooltip
*/
export function getTooltip(connCreds: IConnectionInfo, serverInfo?: ServerInfo): string {
export function getTooltip(connCreds: IConnectionInfo, serverInfo?: IServerInfo): string {
let tooltip: string =
connCreds.connectionString ? 'Connection string: ' + connCreds.connectionString + '\r\n' :
('Server name: ' + connCreds.server + '\r\n' +

Просмотреть файл

@ -10,12 +10,10 @@ import { ConnectionCredentials } from './connectionCredentials';
import { QuestionTypes, IQuestion, IPrompter, INameValueChoice } from '../prompts/question';
import * as utils from './utils';
import { ConnectionStore } from './connectionStore';
import { AzureAuthType } from '@microsoft/ads-adal-library';
import { AzureController } from '../azure/azureController';
import { AccountStore } from '../azure/accountStore';
import { IAccount } from 'vscode-mssql';
import providerSettings from '../azure/providerSettings';
import { Tenant, AzureAccount } from '@microsoft/ads-adal-library';
import { AzureAuthType, IAccount, ITenant } from './contracts/azure';
// Concrete implementation of the IConnectionProfile interface
@ -43,6 +41,7 @@ export class ConnectionProfile extends ConnectionCredentials implements IConnect
this.expiresOn = connectionCredentials.expiresOn;
this.database = connectionCredentials.database;
this.email = connectionCredentials.email;
this.user = connectionCredentials.email;
this.password = connectionCredentials.password;
this.server = connectionCredentials.server;
}
@ -73,7 +72,6 @@ export class ConnectionProfile extends ConnectionCredentials implements IConnect
azureAccountChoices.unshift({ name: LocalizedConstants.azureAddAccount, value: 'addAccount' });
let tenantChoices: INameValueChoice[] = [];
let questions: IQuestion[] = await ConnectionCredentials.getRequiredCredentialValuesQuestions(profile, true,
false, connectionStore, defaultProfileValues);
@ -95,7 +93,8 @@ export class ConnectionProfile extends ConnectionCredentials implements IConnect
onAnswered: (value) => {
accountAnswer = value;
if (value !== 'addAccount') {
let account: AzureAccount = value;
let account: IAccount = value;
profile.accountId = account?.key.id;
tenantChoices.push(...account?.properties?.tenants.map(t => ({ name: t.displayName, value: t })));
if (tenantChoices.length === 1) {
profile.tenantId = tenantChoices[0].value.id;
@ -109,7 +108,7 @@ export class ConnectionProfile extends ConnectionCredentials implements IConnect
message: LocalizedConstants.azureChooseTenant,
choices: tenantChoices,
shouldPrompt: (answers) => profile.isAzureActiveDirectory() && tenantChoices.length > 1,
onAnswered: (value: Tenant) => {
onAnswered: (value: ITenant) => {
profile.tenantId = value.id;
}
},
@ -127,7 +126,7 @@ export class ConnectionProfile extends ConnectionCredentials implements IConnect
});
return prompter.prompt(questions, true).then(async answers => {
if (answers.authenticationType === 'AzureMFA') {
if (answers?.authenticationType === 'AzureMFA') {
if (answers.AAD === 'addAccount') {
profile = await azureController.populateAccountProperties(profile, accountStore, providerSettings.resources.databaseResource);
} else {

Просмотреть файл

@ -17,6 +17,7 @@ import { IConnectionConfig } from '../connectionconfig/iconnectionconfig';
import { ConnectionConfig } from '../connectionconfig/connectionconfig';
import VscodeWrapper from '../controllers/vscodeWrapper';
import { IConnectionInfo } from 'vscode-mssql';
import { Logger } from './logger';
/**
* Manages the connections list including saved profiles and the most recently used connections
@ -28,6 +29,7 @@ export class ConnectionStore {
constructor(
private _context: vscode.ExtensionContext,
private _logger: Logger,
private _credentialStore?: ICredentialStore,
private _connectionConfig?: IConnectionConfig,
private _vscodeWrapper?: VscodeWrapper) {
@ -288,7 +290,7 @@ export class ConnectionStore {
await this._credentialStore.deleteCredential(credentialId);
} catch (err) {
deleteCredentialSuccess = false;
Utils.logToOutputChannel(Utils.formatString(LocalizedConstants.deleteCredentialError, credentialId, err));
this._logger.log(LocalizedConstants.deleteCredentialError, credentialId, err);
}
}
// Update the MRU list to be empty

Просмотреть файл

@ -0,0 +1,218 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Represents a tenant information for an account.
*/
export interface ITenant {
id: string;
displayName: string;
userId?: string;
tenantCategory?: string;
}
/**
* Represents a key that identifies an account.
*/
export interface IAccountKey {
/**
* Identifier for the account, unique to the provider
*/
id: string;
/**
* Identifier of the provider
*/
providerId: string;
/**
* Version of the account
*/
accountVersion?: any;
}
export enum AuthLibrary {
ADAL = 'ADAL',
MSAL = 'MSAL'
}
export enum AzureAuthType {
AuthCodeGrant = 0,
DeviceCode = 1
}
export enum AccountType {
Microsoft = 'microsoft',
WorkSchool = 'work_school'
}
/**
* Represents display information for an account.
*/
export interface IAccountDisplayInfo {
/**
* account provider (eg, Work/School vs Microsoft Account)
*/
accountType: AccountType;
/**
* User id that identifies the account, such as "user@contoso.com".
*/
userId: string;
/**
* A display name that identifies the account, such as "User Name".
*/
displayName: string;
/**
* email for AAD
*/
email?: string;
/**
* name of account
*/
name: string;
}
export interface IAccount {
/**
* The key that identifies the account
*/
key: IAccountKey;
/**
* Display information for the account
*/
displayInfo: IAccountDisplayInfo;
/**
* Custom properties stored with the account
*/
properties: IAzureAccountProperties;
/**
* Indicates if the account needs refreshing
*/
isStale: boolean;
/**
* Indicates if the account is signed in
*/
isSignedIn?: boolean;
}
export interface IAzureAccountProperties {
/**
* Auth type of azure used to authenticate this account.
*/
azureAuthType: AzureAuthType;
/**
* Provider settings for Azure account.
*/
providerSettings: IProviderSettings;
/**
* Whether or not the account is a Microsoft account
*/
isMsAccount: boolean;
/**
* Represents the tenant that the user would be signing in to. For work and school accounts,
* the GUID is the immutable tenant ID of the organization that the user is signing in to.
* For sign-ins to the personal Microsoft account tenant (services like Xbox, Teams for Life, or Outlook),
* the value is 9188040d-6c67-4c5b-b112-36a304b66dad.
*/
owningTenant: ITenant;
/**
* A list of tenants (aka directories) that the account belongs to
*/
tenants: ITenant[];
}
/**
* Represents settings for an AAD account provider
*/
export interface IProviderSettings {
scopes: string[];
displayName: string;
id: string;
clientId: string;
loginEndpoint: string;
portalEndpoint: string;
redirectUri: string;
resources: IProviderResources;
}
export interface IProviderResources {
windowsManagementResource: IAADResource;
azureManagementResource: IAADResource;
graphResource?: IAADResource;
databaseResource?: IAADResource;
ossRdbmsResource?: IAADResource;
azureKeyVaultResource?: IAADResource;
azureDevopsResource?: IAADResource;
}
export interface IAADResource {
id: string;
resource: string;
endpoint: string;
}
/**
* Error to be used when the user has cancelled the prompt or refresh methods. When
* AccountProvider.refresh or AccountProvider.prompt are rejected with this error, the error
* will not be reported to the user.
*/
export interface IPromptFailedResult {
/**
* Type guard for differentiating user cancelled sign in errors from other errors
*/
canceled: boolean;
}
export interface ITokenKey {
/**
* Account Key - uniquely identifies an account
*/
key: string;
}
export interface IAccessToken extends ITokenKey {
/**
* Access Token
*/
token: string;
/**
* Access token expiry timestamp
*/
expiresOn?: number;
}
export interface IToken extends IAccessToken {
/**
* TokenType
*/
tokenType: string;
}
export interface IRefreshToken extends ITokenKey {
/**
* Refresh Token
*/
token: string;
}
export interface ITokenClaims {
aud: string;
iss: string;
iat: number;
idp: string;
nbf: number;
exp: number;
home_oid?: string;
c_hash: string;
at_hash: string;
aio: string;
preferred_username: string;
email: string;
name: string;
nonce: string;
oid?: string;
roles: string[];
rh: string;
sub: string;
tid: string;
unique_name: string;
uti: string;
ver: string;
}

Просмотреть файл

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { NotificationType, RequestType } from 'vscode-languageclient';
import { ConnectionDetails, ServerInfo } from 'vscode-mssql';
import { ConnectionDetails, IServerInfo } from 'vscode-mssql';
// ------------------------------- < Connect Request > ----------------------------------------------
@ -71,7 +71,7 @@ export class ConnectionCompleteParams {
/**
* Information about the connected server.
*/
public serverInfo: ServerInfo;
public serverInfo: IServerInfo;
/**
* information about the actual connection established

Просмотреть файл

@ -3,11 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AzureAuthType } from '@microsoft/ads-adal-library';
import * as vscode from 'vscode';
import { AccountStore } from '../azure/accountStore';
import * as Constants from '../constants/constants';
import * as vscodeMssql from 'vscode-mssql';
import { AzureAuthType } from './contracts/azure';
// interfaces
export enum ContentType {
@ -465,3 +465,8 @@ export interface ISubscriptionPolicies {
* @enum {string}
*/
export type SpendingLimit = 'On' | 'Off' | 'CurrentPeriodOff';
export interface IDeferred<T, E extends Error = Error> {
resolve: (result: T | Promise<T>) => void;
reject: (reason: E) => void;
}

Просмотреть файл

@ -13,7 +13,7 @@ import * as Constants from '../constants/constants';
import { IAzureSignInQuickPickItem, IConnectionProfile, AuthenticationTypes } from './interfaces';
import * as LocalizedConstants from '../constants/localizedConstants';
import * as fs from 'fs';
import { AzureAuthType } from '@microsoft/ads-adal-library';
import { AzureAuthType } from './contracts/azure';
import { IConnectionInfo } from 'vscode-mssql';
// CONSTANTS //////////////////////////////////////////////////////////////////////////////////////
@ -26,8 +26,6 @@ const configPiiLogging = 'piiLogging';
const configLogRetentionMinutes = 'logRetentionMinutes';
const configLogFilesRemovalLimit = 'logFilesRemovalLimit';
const outputChannel = vscode.window.createOutputChannel(Constants.outputChannelName);
// INTERFACES /////////////////////////////////////////////////////////////////////////////////////
// Interface for package.json information
@ -111,21 +109,6 @@ export function getActiveTextEditorUri(): string {
return '';
}
// Helper to log messages to "MSSQL" output channel
export function logToOutputChannel(msg: any): void {
if (msg instanceof Array) {
msg.forEach(element => {
outputChannel.appendLine(element.toString());
});
} else {
outputChannel.appendLine(msg.toString());
}
}
export function openOutputChannel(): void {
outputChannel.show();
}
// Helper to log debug messages
export function logDebug(msg: any): void {
let config = vscode.workspace.getConfiguration(Constants.extensionConfigSectionName);
@ -152,15 +135,6 @@ export function showErrorMsg(msg: string): void {
vscode.window.showErrorMessage(Constants.extensionName + ': ' + msg);
}
// Helper to show an error message with an option to open the output channel
export function showOutputChannelErrorMsg(msg: string): void {
vscode.window.showErrorMessage(msg, LocalizedConstants.showOutputChannelActionButtonText).then((result) => {
if (result === LocalizedConstants.showOutputChannelActionButtonText) {
openOutputChannel();
}
});
}
export function isEmpty(str: any): boolean {
return (!str || '' === str);
}
@ -439,17 +413,17 @@ export function removeOldLogFiles(logPath: string, prefix: string): JSON {
return findRemoveSync(logPath, { age: { seconds: getConfigLogRetentionSeconds() }, limit: getConfigLogFilesRemovalLimit() });
}
export function getCommonLaunchArgsAndCleanupOldLogFiles(logPath: string, fileName: string, executablePath: string): string[] {
export function getCommonLaunchArgsAndCleanupOldLogFiles(executablePath: string, logPath: string, fileName: string): string[] {
let launchArgs = [];
launchArgs.push('--log-file');
let logFile = path.join(logPath, fileName);
launchArgs.push(logFile);
console.log(`logFile for ${path.basename(executablePath)} is ${logFile}`);
console.log(`This process (ui Extenstion Host) is pid: ${process.pid}`);
// Delete old log files
let deletedLogFiles = removeOldLogFiles(logPath, fileName);
console.log(`Old log files deletion report: ${JSON.stringify(deletedLogFiles)}`);
console.log(`This process (ui Extenstion Host) for ${path.basename(executablePath)} is pid: ${process.pid}`);
launchArgs.push('--tracing-level');
launchArgs.push(getConfigTracingLevel());
if (getConfigPiiLogging()) {

Просмотреть файл

@ -115,7 +115,7 @@ export class ObjectExplorerService {
if (self._treeNodeToChildrenMap.has(node)) {
self._treeNodeToChildrenMap.delete(node);
}
return promise.resolve(node);
return promise?.resolve(node);
} else {
// create session failure
if (self._currentNode?.connectionInfo?.password) {
@ -459,9 +459,13 @@ export class ObjectExplorerService {
let azureController = this._connectionManager.azureController;
let account = this._connectionManager.accountStore.getAccount(connectionCredentials.accountId);
let profile = new ConnectionProfile(connectionCredentials);
if (!connectionCredentials.azureAccountToken) {
let azureAccountToken = await azureController.refreshToken(
account, this._connectionManager.accountStore, providerSettings.resources.databaseResource, connectionCredentials.tenantId);
if (azureController.isSqlAuthProviderEnabled()) {
this._client.logger.verbose('SQL Authentication provider is enabled for Azure MFA connections, skipping token acquiry in extension.');
connectionCredentials.user = account.displayInfo.displayName;
connectionCredentials.email = account.displayInfo.email;
} else if (!connectionCredentials.azureAccountToken) {
let azureAccountToken = await azureController.refreshAccessToken(
account, this._connectionManager.accountStore, connectionCredentials.tenantId, providerSettings.resources.databaseResource);
if (!azureAccountToken) {
this._client.logger.verbose('Access token could not be refreshed for connection profile.');
let errorMessage = LocalizedConstants.msgAccountRefreshFailed;

Просмотреть файл

@ -3,34 +3,35 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import * as mssql from 'vscode-mssql';
import { IAzureAccountService, IAzureAccountSession } from 'vscode-mssql';
import { AccountStore } from '../azure/accountStore';
import { AzureController } from '../azure/azureController';
import providerSettings from '../azure/providerSettings';
import { IAccount, IToken } from '../models/contracts/azure';
export class AzureAccountService implements mssql.IAzureAccountService {
export class AzureAccountService implements IAzureAccountService {
constructor(
private _azureController: AzureController,
private _accountStore: AccountStore) {
}
public async addAccount(): Promise<mssql.IAccount> {
public async addAccount(): Promise<IAccount> {
return await this._azureController.addAccount(this._accountStore);
}
public async getAccounts(): Promise<mssql.IAccount[]> {
public async getAccounts(): Promise<IAccount[]> {
return await this._accountStore.getAccounts();
}
public async getAccountSecurityToken(account: mssql.IAccount, tenantId: string | undefined): Promise<mssql.Token> {
public async getAccountSecurityToken(account: IAccount, tenantId: string | undefined): Promise<IToken> {
return await this._azureController.getAccountSecurityToken(account, tenantId, providerSettings.resources.azureManagementResource);
}
/**
* Returns Azure sessions with subscription, tenant and token for each given account
*/
public async getAccountSessions(account: mssql.IAccount): Promise<mssql.IAzureAccountSession[]> {
public async getAccountSessions(account: IAccount): Promise<IAzureAccountSession[]> {
return await this._azureController.getAccountSessions(account);
}
}

Просмотреть файл

@ -4,22 +4,22 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as constants from '../constants/constants';
import * as LocalizedConstants from '../constants/localizedConstants';
import { ConnectionCredentials } from '../models/connectionCredentials';
import ConnectionManager from '../controllers/connectionManager';
import { ConnectionStore } from '../models/connectionStore';
import { ConnectionProfile } from '../models/connectionProfile';
import { IConnectionProfile, IConnectionCredentialsQuickPickItem, CredentialsQuickPickItemType } from '../models/interfaces';
import { INameValueChoice, IQuestion, IPrompter, QuestionTypes } from '../prompts/question';
import { Timer } from '../models/utils';
import * as Utils from '../models/utils';
import VscodeWrapper from '../controllers/vscodeWrapper';
import { ObjectExplorerUtils } from '../objectExplorer/objectExplorerUtils';
import { IFirewallIpAddressRange } from '../models/contracts/firewall/firewallRequest';
import { IAccount, IConnectionInfo } from 'vscode-mssql';
import { AccountStore } from '../azure/accountStore';
import providerSettings from '../azure/providerSettings';
import { IConnectionInfo } from 'vscode-mssql';
import * as constants from '../constants/constants';
import * as LocalizedConstants from '../constants/localizedConstants';
import ConnectionManager from '../controllers/connectionManager';
import VscodeWrapper from '../controllers/vscodeWrapper';
import { ConnectionCredentials } from '../models/connectionCredentials';
import { ConnectionProfile } from '../models/connectionProfile';
import { ConnectionStore } from '../models/connectionStore';
import { IFirewallIpAddressRange } from '../models/contracts/firewall/firewallRequest';
import { CredentialsQuickPickItemType, IConnectionCredentialsQuickPickItem, IConnectionProfile } from '../models/interfaces';
import * as Utils from '../models/utils';
import { Timer } from '../models/utils';
import { ObjectExplorerUtils } from '../objectExplorer/objectExplorerUtils';
import { INameValueChoice, IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
import { CancelError } from '../utils/utils';
/**
@ -444,7 +444,7 @@ export class ConnectionUI {
* @param validate whether the profile should be connected to and validated before saving
* @returns undefined if profile creation failed
*/
public async createAndSaveProfile(validate: boolean = true): Promise<IConnectionProfile> {
public async createAndSaveProfile(validate: boolean = true): Promise<IConnectionProfile | undefined> {
let profile = await this.promptForCreateProfile();
if (profile) {
let savedProfile = validate ?
@ -463,7 +463,7 @@ export class ConnectionUI {
/**
* Validate a connection profile by connecting to it, and save it if we are successful.
*/
public async validateAndSaveProfile(profile: IConnectionProfile): Promise<IConnectionProfile> {
public async validateAndSaveProfile(profile: IConnectionProfile): Promise<IConnectionProfile | undefined> {
let uri = this.vscodeWrapper.activeTextEditorUri;
if (!uri || !this.vscodeWrapper.isEditingSqlFile) {
uri = ObjectExplorerUtils.getNodeUriFromProfile(profile);
@ -530,7 +530,7 @@ export class ConnectionUI {
providerSettings.resources.azureManagementResource);
}
let account = this._accountStore.getAccount(profile.accountId);
this.connectionManager.accountService.setAccount(account);
this.connectionManager.accountService.setAccount(account!);
}
let success = await this.createFirewallRule(profile.server, ipAddress);
@ -671,6 +671,10 @@ export class ConnectionUI {
});
}
public async addNewAccount(): Promise<IAccount> {
return this.connectionManager.azureController.addAccount(this._accountStore);
}
// Prompts the user to pick a profile for removal, then removes from the global saved state
public async removeProfile(): Promise<boolean> {
let self = this;

Просмотреть файл

@ -4,13 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { IConnectionInfo, IServerInfo } from 'vscode-mssql';
import * as Constants from '../constants/constants';
import * as LocalizedConstants from '../constants/localizedConstants';
import VscodeWrapper from '../controllers/vscodeWrapper';
import * as ConnInfo from '../models/connectionInfo';
import * as ConnectionContracts from '../models/contracts/connection';
import * as Utils from '../models/utils';
import VscodeWrapper from '../controllers/vscodeWrapper';
import { IConnectionInfo, ServerInfo } from 'vscode-mssql';
// Status bar element for each file in the editor
class FileStatusBar {
@ -156,7 +156,7 @@ export default class StatusView implements vscode.Disposable {
this.showProgress(fileUri, LocalizedConstants.connectingLabel, bar.statusConnection);
}
public connectSuccess(fileUri: string, connCreds: IConnectionInfo, serverInfo: ServerInfo): void {
public connectSuccess(fileUri: string, connCreds: IConnectionInfo, serverInfo: IServerInfo): void {
let bar = this.getStatusBar(fileUri);
bar.statusConnection.command = Constants.cmdChooseDatabase;
bar.statusConnection.text = ConnInfo.getConnectionDisplayString(connCreds);

Просмотреть файл

@ -5,13 +5,15 @@
import * as assert from 'assert';
import * as TypeMoq from 'typemoq';
import { IAccount, IAzureAccountSession } from 'vscode-mssql';
import { AzureAuthType, IAccount } from '../src/models/contracts/azure';
import { SubscriptionClient, Subscription, Subscriptions, Location } from '@azure/arm-subscriptions';
import { PagedAsyncIterableIterator } from '@azure/core-paging';
import { ResourceGroup, ResourceGroups, ResourceManagementClient } from '@azure/arm-resources';
import { AzureResourceController } from '../src/azure/azureResourceController';
import { AzureAccountService } from '../src/services/azureAccountService';
import { TokenCredentialWrapper } from '../src/azure/credentialWrapper';
import allSettings from '../src/azure/providerSettings';
import { IAzureAccountSession } from 'vscode-mssql';
export interface ITestContext {
azureAccountService: TypeMoq.IMock<AzureAccountService>;
@ -31,7 +33,14 @@ export function createContext(): ITestContext {
tenants: [{
id: '',
displayName: ''
}]
}],
azureAuthType: AzureAuthType.AuthCodeGrant,
isMsAccount: false,
owningTenant: {
id: '',
displayName: ''
},
providerSettings: allSettings
},
isStale: false,
isSignedIn: true

Просмотреть файл

@ -3,23 +3,24 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import * as vscode from 'vscode';
import * as assert from 'assert';
import * as TypeMoq from 'typemoq';
import { IConnectionProfile, AuthenticationTypes } from '../src/models/interfaces';
import { ConnectionCredentials } from '../src/models/connectionCredentials';
import { ConnectionProfile } from '../src/models/connectionProfile';
import { IQuestion, IPrompter, INameValueChoice } from '../src/prompts/question';
import { TestPrompter } from './stubs';
import { ConnectionUI } from '../src/views/connectionUI';
import { ConnectionStore } from '../src/models/connectionStore';
import * as vscode from 'vscode';
import { IConnectionInfo } from 'vscode-mssql';
import { AccountStore } from '../src/azure/accountStore';
import { AzureController } from '../src/azure/azureController';
import { MsalAzureController } from '../src/azure/msal/msalAzureController';
import * as LocalizedConstants from '../src/constants/localizedConstants';
import ConnectionManager from '../src/controllers/connectionManager';
import VscodeWrapper from '../src/controllers/vscodeWrapper';
import * as LocalizedConstants from '../src/constants/localizedConstants';
import * as assert from 'assert';
import { AccountStore } from '../src/azure/accountStore';
import { IConnectionInfo } from 'vscode-mssql';
import { AzureController } from '../src/azure/azureController';
import { ConnectionCredentials } from '../src/models/connectionCredentials';
import { ConnectionProfile } from '../src/models/connectionProfile';
import { ConnectionStore } from '../src/models/connectionStore';
import { AuthenticationTypes, IConnectionProfile } from '../src/models/interfaces';
import { Logger } from '../src/models/logger';
import { INameValueChoice, IPrompter, IQuestion } from '../src/prompts/question';
import { ConnectionUI } from '../src/views/connectionUI';
import { TestPrompter } from './stubs';
function createTestCredentials(): IConnectionInfo {
const creds: IConnectionInfo = {
@ -78,7 +79,7 @@ suite('Connection Profile tests', () => {
mockPrompter = TypeMoq.Mock.ofType<IPrompter>();
mockLogger = TypeMoq.Mock.ofType<Logger>();
mockContext.setup(c => c.globalState).returns(() => globalstate.object);
mockAzureController = new AzureController(mockContext.object, mockPrompter.object, mockLogger.object);
mockAzureController = new MsalAzureController(mockContext.object, mockPrompter.object);
mockAccountStore = new AccountStore(mockContext.object, mockLogger.object);
});

Просмотреть файл

@ -14,8 +14,9 @@ import {
import VscodeWrapper from '../src/controllers/vscodeWrapper';
import { assert } from 'chai';
import { IAzureSession, IAzureResourceFilter } from '../src/models/interfaces';
import { Tenant, Token } from '@microsoft/ads-adal-library';
import { IAccount } from 'vscode-mssql';
import { } from '@microsoft/ads-adal-library';
import { AzureAuthType, IAccount, ITenant, IToken, IAzureAccountProperties } from '../src/models/contracts/azure';
import allSettings from '../src/azure/providerSettings';
suite('Firewall Service Tests', () => {
@ -78,12 +79,19 @@ suite('Firewall Service Tests', () => {
let server = 'test_server';
let startIpAddress = '1.2.3.1';
let endIpAddress = '1.2.3.255';
let mockTenants: Tenant = {
let mockTenants: ITenant = {
id: '1',
displayName: undefined
};
let properties = {
tenants: [mockTenants]
let properties: IAzureAccountProperties = {
tenants: [mockTenants],
azureAuthType: AzureAuthType.AuthCodeGrant,
isMsAccount: false,
owningTenant: {
id: '1',
displayName: undefined
},
providerSettings: allSettings
};
let mockAccount: IAccount = {
properties: properties,
@ -91,7 +99,7 @@ suite('Firewall Service Tests', () => {
displayInfo: undefined,
isStale: undefined
};
let mockToken: Token = {
let mockToken: IToken = {
key: '',
tokenType: '',
token: '',

Просмотреть файл

@ -10,21 +10,21 @@ import { OutputChannel } from 'vscode';
import { IPrompter } from '../src/prompts/question';
import SqlToolsServiceClient from './../src/languageservice/serviceclient';
import { IConnectionInfo, IServerInfo } from 'vscode-mssql';
import * as Constants from '../src/constants/constants';
import * as LocalizedConstants from '../src/constants/localizedConstants';
import ConnectionManager from '../src/controllers/connectionManager';
import { AuthenticationTypes } from '../src/models/interfaces';
import MainController from '../src/controllers/mainController';
import VscodeWrapper from '../src/controllers/vscodeWrapper';
import { ConnectionStore } from '../src/models/connectionStore';
import * as ConnectionContracts from '../src/models/contracts/connection';
import * as LanguageServiceContracts from '../src/models/contracts/languageService';
import MainController from '../src/controllers/mainController';
import * as Interfaces from '../src/models/interfaces';
import { ConnectionStore } from '../src/models/connectionStore';
import StatusView from '../src/views/statusView';
import { AuthenticationTypes } from '../src/models/interfaces';
import * as Utils from '../src/models/utils';
import { TestExtensionContext, TestPrompter } from './stubs';
import VscodeWrapper from '../src/controllers/vscodeWrapper';
import * as LocalizedConstants from '../src/constants/localizedConstants';
import { ConnectionUI } from '../src/views/connectionUI';
import * as Constants from '../src/constants/constants';
import { IConnectionInfo, ServerInfo } from 'vscode-mssql';
import StatusView from '../src/views/statusView';
import { TestExtensionContext, TestPrompter } from './stubs';
function createTestConnectionResult(ownerUri?: string): ConnectionContracts.ConnectionCompleteParams {
let result = new ConnectionContracts.ConnectionCompleteParams();
@ -573,7 +573,7 @@ suite('Per File Connection Tests', () => {
let statusViewMock: TypeMoq.IMock<StatusView> = TypeMoq.Mock.ofType(StatusView);
let actualDbName = undefined;
statusViewMock.setup(x => x.connectSuccess(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.callback((fileUri, creds: IConnectionInfo, server: ServerInfo) => {
.callback((fileUri, creds: IConnectionInfo, server: IServerInfo) => {
actualDbName = creds.database;
});
@ -604,7 +604,7 @@ suite('Per File Connection Tests', () => {
databaseName: dbName,
userName: connectionCreds.user
};
const serverInfo: ServerInfo = {
const serverInfo: IServerInfo = {
engineEditionId: 0,
serverMajorVersion: 0,
isCloud: false,

Просмотреть файл

@ -3,14 +3,14 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import { assert } from 'chai';
import * as TypeMoq from 'typemoq';
import { IServerInfo, MetadataType, ObjectMetadata } from 'vscode-mssql';
import ConnectionManager from '../src/controllers/connectionManager';
import SqlToolsServiceClient from '../src/languageservice/serviceclient';
import { ScriptingService } from '../src/scripting/scriptingService';
import { ScriptingRequest, IScriptingObject, IScriptingResult, ScriptOperation } from '../src/models/contracts/scripting/scriptingRequest';
import { IScriptingObject, IScriptingResult, ScriptingRequest, ScriptOperation } from '../src/models/contracts/scripting/scriptingRequest';
import { TreeNodeInfo } from '../src/objectExplorer/treeNodeInfo';
import { assert } from 'chai';
import { MetadataType, ObjectMetadata, ServerInfo } from 'vscode-mssql';
import { ScriptingService } from '../src/scripting/scriptingService';
import { TestExtensionContext } from './stubs';
suite('Scripting Service Tests', () => {
@ -30,7 +30,7 @@ suite('Scripting Service Tests', () => {
client.setup(c => c.sendRequest(ScriptingRequest.type, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockScriptResult));
connectionManager.object.client = client.object;
connectionManager.setup(c => c.getServerInfo(TypeMoq.It.isAny())).returns(() => {
const serverInfo: ServerInfo = {
const serverInfo: IServerInfo = {
engineEditionId: 2,
serverMajorVersion: 1,
isCloud: true,

Просмотреть файл

@ -1,12 +1,17 @@
{
"compileOnSave": true,
"compilerOptions": {
"outDir": "./out/",
"module": "commonjs",
"outDir": "out",
"module": "CommonJS",
"target": "ES6",
"lib": [ "es6", "dom" ],
"lib": [
"es6",
"ES2020",
"dom"
],
"sourceMap": true,
"allowUnusedLabels": false,
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"rootDir": ".",

133
typings/vscode-mssql.d.ts поставляемый
Просмотреть файл

@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'vscode-mssql' {
import * as vscode from 'vscode';
@ -125,13 +126,13 @@ declare module 'vscode-mssql' {
* @param connectionInfo connection info of the connection
* @returns server information
*/
getServerInfo(connectionInfo: IConnectionInfo): ServerInfo
getServerInfo(connectionInfo: IConnectionInfo): IServerInfo
}
/**
* Information about a SQL Server instance.
*/
export interface ServerInfo {
export interface IServerInfo {
/**
* The major version of the SQL Server instance.
*/
@ -438,6 +439,14 @@ declare module 'vscode-mssql' {
validateStreamingJob(packageFilePath: string, createStreamingJobTsql: string): Thenable<ValidateStreamingJobResult>;
}
/**
* Error that connect method throws if connection fails because of a fire wall rule error.
*/
export interface IFireWallRuleError extends Error {
connectionUri: string;
}
//////////////////// Azure Types ////////////////////
/**
* Interface for working with .sqlproj files
*/
@ -682,20 +691,13 @@ declare module 'vscode-mssql' {
/**
* Represents a tenant information for an account.
*/
export interface Tenant {
export interface ITenant {
id: string;
displayName: string;
userId?: string;
tenantCategory?: string;
}
/**
* Error that connect method throws if connection fails because of a fire wall rule error.
*/
export interface IFireWallRuleError extends Error {
connectionUri: string;
}
/**
* Represents a key that identifies an account.
*/
@ -714,6 +716,15 @@ declare module 'vscode-mssql' {
accountVersion?: any;
}
export enum AuthLibrary {
ADAL = 'ADAL',
MSAL = 'MSAL'
}
export enum AzureAuthType {
AuthCodeGrant = 0,
DeviceCode = 1
}
export enum AccountType {
Microsoft = 'microsoft',
@ -758,7 +769,7 @@ declare module 'vscode-mssql' {
/**
* Custom properties stored with the account
*/
properties: any;
properties: IAzureAccountProperties;
/**
* Indicates if the account needs refreshing
*/
@ -769,20 +780,62 @@ declare module 'vscode-mssql' {
isSignedIn?: boolean;
}
export interface IAzureAccountSession {
subscription: azure.subscription.Subscription,
tenantId: string,
account: IAccount,
token: Token
export interface IAzureAccountProperties {
/**
* Auth type of azure used to authenticate this account.
*/
azureAuthType: AzureAuthType;
providerSettings: IProviderSettings;
/**
* Whether or not the account is a Microsoft account
*/
isMsAccount: boolean;
/**
* Represents the tenant that the user would be signing in to. For work and school accounts, the GUID is the immutable tenant ID of the organization that the user is signing in to.
* For sign-ins to the personal Microsoft account tenant (services like Xbox, Teams for Life, or Outlook), the value is 9188040d-6c67-4c5b-b112-36a304b66dad.
*/
owningTenant: ITenant;
/**
* A list of tenants (aka directories) that the account belongs to
*/
tenants: ITenant[];
}
export interface TokenKey {
export interface IProviderSettings {
scopes: string[];
displayName: string;
id: string;
clientId: string;
loginEndpoint: string;
portalEndpoint: string;
redirectUri: string;
resources: IProviderResources;
}
export interface IProviderResources {
windowsManagementResource: IAADResource;
azureManagementResource: IAADResource;
graphResource?: IAADResource;
databaseResource?: IAADResource;
ossRdbmsResource?: IAADResource;
azureKeyVaultResource?: IAADResource;
azureDevopsResource?: IAADResource;
}
export interface IAADResource {
id: string;
resource: string;
endpoint: string;
}
export interface ITokenKey {
/**
* Account Key - uniquely identifies an account
*/
key: string;
}
export interface AccessToken extends TokenKey {
export interface IAccessToken extends ITokenKey {
/**
* Access Token
*/
@ -792,13 +845,53 @@ declare module 'vscode-mssql' {
*/
expiresOn?: number;
}
export interface Token extends AccessToken {
export interface IToken extends IAccessToken {
/**
* TokenType
*/
tokenType: string;
}
export interface IRefreshToken extends ITokenKey {
/**
* Refresh Token
*/
token: string;
}
export interface ITokenClaims {
aud: string;
iss: string;
iat: number;
idp: string;
nbf: number;
exp: number;
home_oid?: string;
c_hash: string;
at_hash: string;
aio: string;
preferred_username: string;
email: string;
name: string;
nonce: string;
oid?: string;
roles: string[];
rh: string;
sub: string;
tid: string;
unique_name: string;
uti: string;
ver: string;
}
export interface IAzureAccountSession {
subscription: azure.subscription.Subscription,
tenantId: string,
account: IAccount,
token: IToken | undefined
}
export interface IAzureAccountService {
/**
* Prompts user to login to Azure and returns the account
@ -813,7 +906,7 @@ declare module 'vscode-mssql' {
/**
* Returns an access token for given user and tenant
*/
getAccountSecurityToken(account: IAccount, tenantId: string | undefined): Promise<Token>;
getAccountSecurityToken(account: IAccount, tenantId: string | undefined): Promise<IToken>;
/**
* Returns Azure subscriptions with tenant and token for each given account

211
yarn.lock
Просмотреть файл

@ -162,6 +162,20 @@
dependencies:
tslib "^2.2.0"
"@azure/msal-common@^10.0.0":
version "10.0.0"
resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-10.0.0.tgz#07fc39ae2a2e6f2c1da8e26657058317de52b65a"
integrity sha512-/LghpT93jsZLy55QzTsRZWMx6R1Mjc1Aktwps8sKSGE3WbrGwbSsh2uhDlpl6FMcKChYjJ0ochThWwwOodrQNg==
"@azure/msal-node@^1.15.0":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-1.15.0.tgz#0a0248c73afaa16b195c1afba4149c72ea3b42a2"
integrity sha512-fwC5M0c8pxOAzmScPbpx7j28YVTDebUaizlVF7bR0xvlU0r3VWW5OobCcr9ybqKS6wGyO7u4EhXJS9rjRWAuwA==
dependencies:
"@azure/msal-common" "^10.0.0"
jsonwebtoken "^9.0.0"
uuid "^8.3.0"
"@babel/code-frame@^7.0.0":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb"
@ -237,6 +251,11 @@
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
"@types/ejs@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.0.tgz#ab8109208106b5e764e5a6c92b2ba1c625b73020"
@ -268,15 +287,33 @@
dependencies:
keytar "*"
"@types/lockfile@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/lockfile/-/lockfile-1.0.2.tgz#3f77e84171a2b7e3198bd5717c7547a54393baf8"
integrity sha512-jD5VbvhfMhaYN4M3qPJuhMVUg3Dfc4tvPvLEAXn6GXbs/ajDFtCQahX37GIE65ipTI3I+hEvNaXS3MYAn9Ce3Q==
"@types/mocha@^5.2.7":
version "5.2.7"
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea"
integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==
"@types/node@^14.14.16":
version "14.17.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.9.tgz#b97c057e6138adb7b720df2bd0264b03c9f504fd"
integrity sha512-CMjgRNsks27IDwI785YMY0KLt3co/c0cQ5foxHYv/shC2w8oOnVwz5Ubq1QG5KzrcW+AXk6gzdnxIkDnTvzu3g==
"@types/node-fetch@^2.6.1":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da"
integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*":
version "18.14.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.0.tgz#94c47b9217bbac49d4a67a967fdcdeed89ebb7d0"
integrity sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A==
"@types/node@^14.17.0":
version "14.18.36"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.36.tgz#c414052cb9d43fab67d679d5f3c641be911f5835"
integrity sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==
"@types/sinon@^10.0.12":
version "10.0.12"
@ -345,13 +382,6 @@ acorn@4.X:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=
agent-base@4, agent-base@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
dependencies:
es6-promisify "^5.0.0"
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@ -994,6 +1024,11 @@ 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-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe"
@ -1482,13 +1517,6 @@ debug@2.X, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
dependencies:
ms "2.0.0"
debug@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debug@4:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
@ -1503,13 +1531,6 @@ debug@4.3.1:
dependencies:
ms "2.1.2"
debug@^3.1.0:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
dependencies:
ms "^2.1.1"
decache@^4.1.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/decache/-/decache-4.6.0.tgz#87026bc6e696759e82d57a3841c4e251a30356e8"
@ -1738,6 +1759,13 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
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"
editions@^2.2.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/editions/-/editions-2.3.1.tgz#3bc9962f1978e801312fbd0aebfed63b49bfe698"
@ -1805,18 +1833,6 @@ es6-iterator@^2.0.1, es6-iterator@^2.0.3, es6-iterator@~2.0.3:
es5-ext "^0.10.35"
es6-symbol "^3.1.1"
es6-promise@^4.0.3:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
es6-promisify@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
dependencies:
es6-promise "^4.0.3"
es6-symbol@^3.1.1, es6-symbol@~3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
@ -2198,6 +2214,15 @@ forever-agent@~0.6.1:
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
form-data@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
@ -2798,13 +2823,14 @@ hosted-git-info@^2.1.4:
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
http-proxy-agent@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405"
integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==
http-proxy-agent@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==
dependencies:
agent-base "4"
debug "3.1.0"
"@tootallnate/once" "2"
agent-base "6"
debug "4"
http-proxy-agent@^4.0.1:
version "4.0.1"
@ -2824,15 +2850,7 @@ http-signature@~1.2.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
https-proxy-agent@^2.2.1:
version "2.2.4"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==
dependencies:
agent-base "^4.3.0"
debug "^3.1.0"
https-proxy-agent@^5.0.0:
https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
@ -3284,6 +3302,16 @@ json5@^0.5.1:
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=
jsonwebtoken@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d"
integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==
dependencies:
jws "^3.2.2"
lodash "^4.17.21"
ms "^2.1.1"
semver "^7.3.8"
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@ -3304,6 +3332,23 @@ just-extend@^4.0.2:
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744"
integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==
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@*:
version "7.7.0"
resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.7.0.tgz#3002b106c01631aa79b1aa9ee0493b94179bbbd2"
@ -3433,6 +3478,13 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lockfile@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.4.tgz#07f819d25ae48f87e538e6578b6964a4981a5609"
integrity sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==
dependencies:
signal-exit "^3.0.2"
lodash._basecopy@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36"
@ -3557,7 +3609,7 @@ lodash@^2.4.1:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-2.4.2.tgz#fadd834b9683073da179b3eae6d9c0d15053f73e"
integrity sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=
lodash@^4.13.1, lodash@^4.16.4, lodash@^4.17.4:
lodash@^4.13.1, lodash@^4.16.4, lodash@^4.17.21, lodash@^4.17.4:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -3603,6 +3655,13 @@ lru-cache@^4.1.5:
pseudomap "^1.0.2"
yallist "^2.1.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-error-cause@^1.1.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d"
@ -3840,6 +3899,13 @@ ms@2.1.3, ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
msal@^1.4.17:
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"
multipipe@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-0.1.2.tgz#2a8f2ddf70eed564dff2d57f1e1a137d9f05078b"
@ -3917,6 +3983,13 @@ node-addon-api@^3.0.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
node-fetch@^2.6.1:
version "2.6.9"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==
dependencies:
whatwg-url "^5.0.0"
nopt@3.x, nopt@^3.0.1:
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
@ -4407,7 +4480,7 @@ q@^1.1.2:
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
qs@^6.9.7:
qs@^6.9.1, qs@^6.9.7:
version "6.11.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
@ -4786,6 +4859,13 @@ 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.8:
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"
"semver@https://registry.npmjs.org/semver/-/semver-5.0.3.tgz":
version "5.0.3"
resolved "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a"
@ -4836,6 +4916,11 @@ signal-exit@^3.0.0:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
signal-exit@^3.0.2:
version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
simple-concat@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
@ -5388,6 +5473,11 @@ tough-cookie@~2.5.0:
psl "^1.1.28"
punycode "^2.1.1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
traceur@0.0.105:
version "0.0.105"
resolved "https://registry.yarnpkg.com/traceur/-/traceur-0.0.105.tgz#5cf9dee83d6b77861c3d6c44d53859aed7ab0479"
@ -5852,6 +5942,19 @@ vscode-nls@^2.0.2:
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-2.0.2.tgz#808522380844b8ad153499af5c3b03921aea02da"
integrity sha512-xK4p7Wksahb1imTwJZeA7+OSobDlRkWYWBuz9eR6LyJRLLG4LBxvLvZF8GO1ZY1tUWHITjZn2BtA8nRufKdHSg==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
when@^3.7.5:
version "3.7.8"
resolved "https://registry.yarnpkg.com/when/-/when-3.7.8.tgz#c7130b6a7ea04693e842cdc9e7a1f2aa39a39f82"