MSAL Authentication support + code cleanup (#17562)
This commit is contained in:
Родитель
a7df781375
Коммит
99b91117c3
|
@ -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">
|
||||
|
|
55
package.json
55
package.json
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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": ".",
|
||||
|
|
|
@ -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
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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче