mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Add MSAL Authentication Library support (#21024)
This commit is contained in:
@@ -19,7 +19,10 @@ const externals = {
|
||||
'universalify': 'commonjs universalify',
|
||||
'@azure/arm-subscriptions': 'commonjs @azure/arm-subscriptions',
|
||||
'@azure/arm-resourcegraph': 'commonjs @azure/arm-resourcegraph',
|
||||
'@azure/storage-blob': 'commonjs @azure/storage-blob'
|
||||
'@azure/storage-blob': 'commonjs @azure/storage-blob',
|
||||
'@azure/msal-node': 'commonjs @azure/msal-node',
|
||||
'@azure/msal-node-extensions': 'commonjs @azure/msal-node-extensions',
|
||||
'msal': 'commonjs msal'
|
||||
};
|
||||
|
||||
// conditionally add ws if we are going to be running in a node environment
|
||||
|
||||
@@ -124,6 +124,19 @@
|
||||
"Verbose",
|
||||
"All"
|
||||
]
|
||||
},
|
||||
"azure.authenticationLibrary": {
|
||||
"type": "string",
|
||||
"description": "%config.authenticationLibrary%",
|
||||
"default": "ADAL",
|
||||
"enum": [
|
||||
"ADAL",
|
||||
"MSAL"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Azure Active Directory Authentication Library",
|
||||
"Microsoft Authentication Library"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,7 +361,10 @@
|
||||
"@azure/arm-resourcegraph": "^4.0.0",
|
||||
"@azure/arm-subscriptions": "^3.0.0",
|
||||
"@azure/storage-blob": "^12.6.0",
|
||||
"@azure/msal-node": "^1.9.0",
|
||||
"@azure/msal-node-extensions": "^1.0.0-alpha.25",
|
||||
"axios": "^0.27.2",
|
||||
"msal": "^1.4.16",
|
||||
"node-fetch": "^2.6.7",
|
||||
"qs": "^6.9.1",
|
||||
"universalify": "^0.1.2",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"config.azureDeviceCodeMethod": "Device Code Method",
|
||||
"config.noSystemKeychain": "Disable system keychain integration. Credentials will be stored in a flat file in the user's home directory.",
|
||||
"config.piiLogging": "Should Personally Identifiable Information (PII) be logged in the Azure Accounts output channel and the output channel log file.",
|
||||
"config.loggingLevel": "[Optional] The verbosity of logging for the Azure Accounts extension."
|
||||
"config.loggingLevel": "[Optional] The verbosity of logging for the Azure Accounts extension.",
|
||||
"config.authenticationLibrary": "The library used for the AAD auth flow. Please restart ADS after changing this option."
|
||||
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
import {
|
||||
@@ -15,51 +14,55 @@ import {
|
||||
Resource,
|
||||
Tenant
|
||||
} from 'azurecore';
|
||||
|
||||
import { Deferred } from '../interfaces';
|
||||
import * as url from 'url';
|
||||
|
||||
import * as Constants from '../../constants';
|
||||
import { SimpleTokenCache } from '../simpleTokenCache';
|
||||
import { MemoryDatabase } from '../utils/memoryDatabase';
|
||||
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { Logger } from '../../utils/Logger';
|
||||
import * as qs from 'qs';
|
||||
import { AzureAuthError } from './azureAuthError';
|
||||
import { AccountInfo, AuthenticationResult, InteractionRequiredAuthError, PublicClientApplication } from '@azure/msal-node';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
|
||||
export abstract class AzureAuth implements vscode.Disposable {
|
||||
public static ACCOUNT_VERSION = '2.0';
|
||||
protected readonly memdb = new MemoryDatabase<string>();
|
||||
|
||||
protected readonly WorkSchoolAccountType: string = 'work_school';
|
||||
protected readonly MicrosoftAccountType: string = 'microsoft';
|
||||
|
||||
protected readonly loginEndpointUrl: string;
|
||||
public readonly commonTenant: Tenant;
|
||||
public readonly organizationTenant: Tenant;
|
||||
protected readonly redirectUri: string;
|
||||
protected readonly scopes: string[];
|
||||
protected readonly scopesString: string;
|
||||
protected readonly clientId: string;
|
||||
protected readonly resources: Resource[];
|
||||
|
||||
private _authLibrary: string | undefined;
|
||||
|
||||
constructor(
|
||||
protected readonly metadata: AzureAccountProviderMetadata,
|
||||
protected readonly tokenCache: SimpleTokenCache,
|
||||
protected readonly context: vscode.ExtensionContext,
|
||||
protected clientApplication: PublicClientApplication,
|
||||
protected readonly uriEventEmitter: vscode.EventEmitter<vscode.Uri>,
|
||||
protected readonly authType: AzureAuthType,
|
||||
public readonly userFriendlyName: string
|
||||
public readonly userFriendlyName: string,
|
||||
public readonly authLibrary: string
|
||||
) {
|
||||
this._authLibrary = authLibrary;
|
||||
|
||||
this.loginEndpointUrl = this.metadata.settings.host;
|
||||
this.commonTenant = {
|
||||
id: 'common',
|
||||
displayName: 'common',
|
||||
};
|
||||
this.organizationTenant = {
|
||||
id: 'organizations',
|
||||
displayName: 'organizations',
|
||||
};
|
||||
this.redirectUri = this.metadata.settings.redirectUri;
|
||||
this.clientId = this.metadata.settings.clientId;
|
||||
|
||||
this.resources = [
|
||||
this.metadata.settings.armResource,
|
||||
this.metadata.settings.graphResource,
|
||||
@@ -100,19 +103,39 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
if (!this.metadata.settings.microsoftResource) {
|
||||
throw new Error(localize('noMicrosoftResource', "Provider '{0}' does not have a Microsoft resource endpoint defined.", this.metadata.displayName));
|
||||
}
|
||||
const result = await this.login(this.commonTenant, this.metadata.settings.microsoftResource);
|
||||
loginComplete = result.authComplete;
|
||||
if (!result?.response) {
|
||||
Logger.error('Authentication failed');
|
||||
return {
|
||||
canceled: false
|
||||
if (this._authLibrary === Constants.AuthLibrary.MSAL) {
|
||||
const result = await this.loginMsal(this.organizationTenant, this.metadata.settings.microsoftResource);
|
||||
loginComplete = result.authComplete;
|
||||
if (!result?.response || !result.response?.account) {
|
||||
Logger.error(`Authentication failed: ${loginComplete}`);
|
||||
return {
|
||||
canceled: false
|
||||
};
|
||||
}
|
||||
const token: Token = {
|
||||
token: result.response.accessToken,
|
||||
key: result.response.account.homeAccountId,
|
||||
tokenType: result.response.tokenType
|
||||
};
|
||||
const tokenClaims = <TokenClaims>result.response.idTokenClaims;
|
||||
const account = await this.hydrateAccount(token, tokenClaims);
|
||||
loginComplete?.resolve();
|
||||
return account;
|
||||
} else {// fallback to ADAL as default
|
||||
const result = await this.loginAdal(this.commonTenant, this.metadata.settings.microsoftResource);
|
||||
loginComplete = result.authComplete;
|
||||
if (!result?.response) {
|
||||
Logger.error('Authentication failed - no response');
|
||||
return {
|
||||
canceled: false
|
||||
};
|
||||
}
|
||||
const account = await this.hydrateAccount(result.response.accessToken, result.response.tokenClaims);
|
||||
loginComplete?.resolve();
|
||||
return account;
|
||||
}
|
||||
const account = await this.hydrateAccount(result.response.accessToken, result.response.tokenClaims);
|
||||
loginComplete?.resolve();
|
||||
return account;
|
||||
} catch (ex) {
|
||||
Logger.error('Login failed');
|
||||
Logger.error(`Login failed: ${ex}`);
|
||||
if (ex instanceof AzureAuthError) {
|
||||
if (loginComplete) {
|
||||
loginComplete.reject(ex);
|
||||
@@ -133,9 +156,9 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
public async refreshAccess(account: AzureAccount): Promise<AzureAccount> {
|
||||
public async refreshAccessAdal(account: AzureAccount): Promise<AzureAccount> {
|
||||
// Deprecated account - delete it.
|
||||
if (account.key.accountVersion !== AzureAuth.ACCOUNT_VERSION) {
|
||||
if (account.key.accountVersion !== Constants.AccountVersion) {
|
||||
account.delete = true;
|
||||
return account;
|
||||
}
|
||||
@@ -144,7 +167,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
// We want to return the one that owns the Azure account.
|
||||
// Not doing so can result in token being issued for the wrong tenant
|
||||
const tenant = account.properties.owningTenant;
|
||||
const tokenResult = await this.getAccountSecurityToken(account, tenant.id, azdata.AzureResource.MicrosoftResourceManagement);
|
||||
const tokenResult = await this.getAccountSecurityTokenAdal(account, tenant.id, azdata.AzureResource.MicrosoftResourceManagement);
|
||||
if (!tokenResult) {
|
||||
account.isStale = true;
|
||||
return account;
|
||||
@@ -154,23 +177,28 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
} catch (ex) {
|
||||
if (ex instanceof AzureAuthError) {
|
||||
void vscode.window.showErrorMessage(ex.message);
|
||||
Logger.error(ex.originalMessageAndException);
|
||||
Logger.error(`Error refreshing access for account ${account.displayInfo.displayName}`, ex.originalMessageAndException);
|
||||
} else {
|
||||
Logger.error(ex);
|
||||
}
|
||||
Logger.error(ex);
|
||||
account.isStale = true;
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
public async hydrateAccount(token: Token | AccessToken, tokenClaims: TokenClaims): Promise<AzureAccount> {
|
||||
const tenants = await this.getTenants({ ...token });
|
||||
const account = this.createAccount(tokenClaims, token.key, tenants);
|
||||
let account: azdata.Account;
|
||||
if (this._authLibrary === Constants.AuthLibrary.MSAL) {
|
||||
const tenants = await this.getTenantsMsal(token.token);
|
||||
account = this.createAccount(tokenClaims, token.key, tenants);
|
||||
} else { // fallback to ADAL as default
|
||||
const tenants = await this.getTenantsAdal({ ...token });
|
||||
account = this.createAccount(tokenClaims, token.key, tenants);
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
public async getAccountSecurityToken(account: AzureAccount, tenantId: string, azureResource: azdata.AzureResource): Promise<Token | undefined> {
|
||||
public async getAccountSecurityTokenAdal(account: AzureAccount, tenantId: string, azureResource: azdata.AzureResource): Promise<Token | undefined> {
|
||||
if (account.isStale === true) {
|
||||
Logger.error('Account was stale. No tokens being fetched.');
|
||||
return undefined;
|
||||
@@ -178,8 +206,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
|
||||
const resource = this.resources.find(s => s.azureResourceId === azureResource);
|
||||
if (!resource) {
|
||||
Logger.error('Invalid resource, not fetching', azureResource);
|
||||
|
||||
Logger.error(`Unable to find Azure resource ${azureResource} for account ${account.displayInfo.userId} and tenant ${tenantId}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -196,7 +223,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
throw new AzureAuthError(localize('azure.tenantNotFound', "Specified tenant with ID '{0}' not found.", tenantId), `Tenant ${tenantId} not found.`, undefined);
|
||||
}
|
||||
|
||||
const cachedTokens = await this.getSavedToken(tenant, resource, account.key);
|
||||
const cachedTokens = await this.getSavedTokenAdal(tenant, resource, account.key);
|
||||
|
||||
// Let's check to see if we can just use the cached tokens to return to the user
|
||||
if (cachedTokens?.accessToken) {
|
||||
@@ -213,7 +240,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
const maxTolerance = 2 * 60; // two minutes
|
||||
|
||||
if (remainingTime < maxTolerance) {
|
||||
const result = await this.refreshToken(tenant, resource, cachedTokens.refreshToken);
|
||||
const result = await this.refreshTokenAdal(tenant, resource, cachedTokens.refreshToken);
|
||||
if (result) {
|
||||
accessToken = result.accessToken;
|
||||
expiresOn = Number(result.expiresOn);
|
||||
@@ -224,7 +251,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
return {
|
||||
...accessToken,
|
||||
expiresOn: expiresOn,
|
||||
tokenType: 'Bearer'
|
||||
tokenType: Constants.Bearer
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -234,7 +261,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
if (!this.metadata.settings.microsoftResource) {
|
||||
throw new Error(localize('noMicrosoftResource', "Provider '{0}' does not have a Microsoft resource endpoint defined.", this.metadata.displayName));
|
||||
}
|
||||
const baseTokens = await this.getSavedToken(this.commonTenant, this.metadata.settings.microsoftResource, account.key);
|
||||
const baseTokens = await this.getSavedTokenAdal(this.commonTenant, this.metadata.settings.microsoftResource, account.key);
|
||||
if (!baseTokens) {
|
||||
Logger.error('User had no base tokens for the basic resource registered. This should not happen and indicates something went wrong with the authentication cycle');
|
||||
const msg = localize('azure.noBaseToken', 'Something failed with the authentication, or your tokens have been deleted from the system. Please try adding your account to Azure Data Studio again.');
|
||||
@@ -242,12 +269,12 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
throw new AzureAuthError(msg, 'No base token found', undefined);
|
||||
}
|
||||
// Let's try to convert the access token type, worst case we'll have to prompt the user to do an interactive authentication.
|
||||
const result = await this.refreshToken(tenant, resource, baseTokens.refreshToken);
|
||||
const result = await this.refreshTokenAdal(tenant, resource, baseTokens.refreshToken);
|
||||
if (result?.accessToken) {
|
||||
return {
|
||||
...result.accessToken,
|
||||
expiresOn: Number(result.expiresOn),
|
||||
tokenType: 'Bearer'
|
||||
tokenType: Constants.Bearer
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
@@ -255,7 +282,9 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
|
||||
|
||||
|
||||
protected abstract login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred<void, Error> }>;
|
||||
protected abstract loginAdal(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred<void, Error> }>;
|
||||
|
||||
protected abstract loginMsal(tenant: Tenant, resource: Resource): Promise<{ response: AuthenticationResult | null, authComplete: Deferred<void, Error> }>;
|
||||
|
||||
/**
|
||||
* Refreshes a token, if a refreshToken is passed in then we use that. If it is not passed in then we will prompt the user for consent.
|
||||
@@ -265,7 +294,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
* @returns The oauth token response or undefined. Undefined is returned when the user wants to ignore a tenant or chooses not to start the
|
||||
* re-authentication process for their tenant.
|
||||
*/
|
||||
public async refreshToken(tenant: Tenant, resource: Resource, refreshToken: RefreshToken | undefined): Promise<OAuthTokenResponse | undefined> {
|
||||
public async refreshTokenAdal(tenant: Tenant, resource: Resource, refreshToken: RefreshToken | undefined): Promise<OAuthTokenResponse | undefined> {
|
||||
Logger.pii('Refreshing token', [{ name: 'token', objOrArray: refreshToken }], []);
|
||||
if (refreshToken) {
|
||||
const postData: RefreshTokenPostData = {
|
||||
@@ -275,36 +304,96 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
tenant: tenant.id,
|
||||
resource: resource.endpoint
|
||||
};
|
||||
|
||||
return this.getToken(tenant, resource, postData);
|
||||
return this.getTokenAdal(tenant, resource, postData);
|
||||
}
|
||||
|
||||
return this.handleInteractionRequired(tenant, resource);
|
||||
return this.handleInteractionRequiredAdal(tenant, resource);
|
||||
}
|
||||
|
||||
public async getToken(tenant: Tenant, resource: Resource, postData: AuthorizationCodePostData | TokenPostData | RefreshTokenPostData): Promise<OAuthTokenResponse | undefined> {
|
||||
Logger.verbose('Fetching token');
|
||||
|
||||
/**
|
||||
* Gets the access token for the correct account and scope from the token cache, if the correct token doesn't exist in the token cache
|
||||
* (i.e. expired token, wrong scope, etc.), sends a request for a new token using the refresh token
|
||||
* @param accountId
|
||||
* @param azureResource
|
||||
* @returns The authentication result, including the access token
|
||||
*/
|
||||
public async getTokenMsal(accountId: string, azureResource: azdata.AzureResource, tenantId: string): Promise<AuthenticationResult | null> {
|
||||
const cache = this.clientApplication.getTokenCache();
|
||||
if (!cache) {
|
||||
Logger.error('Error: Could not fetch token cache.');
|
||||
return null;
|
||||
}
|
||||
const resource = this.resources.find(s => s.azureResourceId === azureResource);
|
||||
if (!resource) {
|
||||
Logger.error(`Error: Could not fetch the azure resource ${azureResource} `);
|
||||
return null;
|
||||
}
|
||||
let account: AccountInfo | null;
|
||||
// if the accountId is a home ID, it will include a "." character
|
||||
if (accountId.includes(".")) {
|
||||
account = await cache.getAccountByHomeId(accountId);
|
||||
} else {
|
||||
account = await cache.getAccountByLocalId(accountId);
|
||||
}
|
||||
if (!account) {
|
||||
Logger.error('Error: Could not fetch account when acquiring token');
|
||||
return null;
|
||||
}
|
||||
let newScope;
|
||||
if (resource.azureResourceId === azdata.AzureResource.ResourceManagement) {
|
||||
newScope = [`${resource?.endpoint}user_impersonation`];
|
||||
} else {
|
||||
newScope = [`${resource?.endpoint}.default`];
|
||||
}
|
||||
|
||||
// construct request
|
||||
// forceRefresh needs to be set true here in order to fetch the correct token, due to this issue
|
||||
// https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/3687
|
||||
const tokenRequest = {
|
||||
account: account,
|
||||
authority: `https://login.microsoftonline.com/${tenantId}`,
|
||||
scopes: newScope,
|
||||
forceRefresh: true
|
||||
};
|
||||
try {
|
||||
return await this.clientApplication.acquireTokenSilent(tokenRequest);
|
||||
} catch (e) {
|
||||
Logger.error('Failed to acquireTokenSilent', e);
|
||||
if (e instanceof InteractionRequiredAuthError) {
|
||||
// build refresh token request
|
||||
const tenant: Tenant = {
|
||||
id: tenantId,
|
||||
displayName: ''
|
||||
};
|
||||
return this.handleInteractionRequiredMsal(tenant, resource);
|
||||
} else if (e.name === 'ClientAuthError') {
|
||||
Logger.error(e.message);
|
||||
}
|
||||
Logger.error('Failed to silently acquire token, not InteractionRequiredAuthError');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async getTokenAdal(tenant: Tenant, resource: Resource, postData: AuthorizationCodePostData | TokenPostData | RefreshTokenPostData): Promise<OAuthTokenResponse | undefined> {
|
||||
Logger.verbose('Fetching token for tenant {0}', tenant.id);
|
||||
const tokenUrl = `${this.loginEndpointUrl}${tenant.id}/oauth2/token`;
|
||||
const response = await this.makePostRequest(tokenUrl, postData);
|
||||
Logger.pii('Token: ', [{ name: 'access token', objOrArray: response.data }, { name: 'refresh token', objOrArray: response.data }],
|
||||
[{ name: 'access token', value: response.data.access_token }, { name: 'refresh token', value: response.data.refresh_token }]);
|
||||
if (response.data.error === 'interaction_required') {
|
||||
return this.handleInteractionRequired(tenant, resource);
|
||||
}
|
||||
|
||||
Logger.pii('Token: ', [{ name: 'access token', objOrArray: response.data }, { name: 'refresh token', objOrArray: response.data }], []);
|
||||
if (response.data.error === 'interaction_required') {
|
||||
return this.handleInteractionRequiredAdal(tenant, resource);
|
||||
}
|
||||
if (response.data.error) {
|
||||
Logger.error('Response error!', response.data);
|
||||
Logger.error(`Response returned error : ${response.data}`);
|
||||
throw new AzureAuthError(localize('azure.responseError', "Token retrieval failed with an error. [Open developer tools]({0}) for more details.", 'command:workbench.action.toggleDevTools'), 'Token retrieval failed', undefined);
|
||||
}
|
||||
|
||||
const accessTokenString = response.data.access_token;
|
||||
const refreshTokenString = response.data.refresh_token;
|
||||
const expiresOnString = response.data.expires_on;
|
||||
|
||||
return this.getTokenHelper(tenant, resource, accessTokenString, refreshTokenString, expiresOnString);
|
||||
return this.getTokenHelperAdal(tenant, resource, accessTokenString, refreshTokenString, expiresOnString);
|
||||
}
|
||||
|
||||
public async getTokenHelper(tenant: Tenant, resource: Resource, accessTokenString: string, refreshTokenString: string, expiresOnString: string): Promise<OAuthTokenResponse> {
|
||||
public async getTokenHelperAdal(tenant: Tenant, resource: Resource, accessTokenString: string, refreshTokenString: string, expiresOnString: string): Promise<OAuthTokenResponse> {
|
||||
if (!accessTokenString) {
|
||||
const msg = localize('azure.accessTokenEmpty', 'No access token returned from Microsoft OAuth');
|
||||
throw new AzureAuthError(msg, 'Access token was empty', undefined);
|
||||
@@ -349,7 +438,8 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
|
||||
const accountKey: azdata.AccountKey = {
|
||||
providerId: this.metadata.id,
|
||||
accountId: userKey
|
||||
accountId: userKey,
|
||||
authLibrary: this._authLibrary
|
||||
};
|
||||
|
||||
await this.saveToken(tenant, resource, accountKey, result);
|
||||
@@ -358,19 +448,47 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
}
|
||||
|
||||
|
||||
|
||||
//#region tenant calls
|
||||
public async getTenants(token: AccessToken): Promise<Tenant[]> {
|
||||
interface TenantResponse { // https://docs.microsoft.com/en-us/rest/api/resources/tenants/list
|
||||
id: string
|
||||
tenantId: string
|
||||
displayName?: string
|
||||
tenantCategory?: string
|
||||
}
|
||||
|
||||
public async getTenantsMsal(token: string): Promise<Tenant[]> {
|
||||
const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2019-11-01');
|
||||
try {
|
||||
Logger.verbose('Fetching tenants', tenantUri);
|
||||
Logger.verbose('Fetching tenants with uri {0}', tenantUri);
|
||||
let tenantList: string[] = [];
|
||||
const tenantResponse = await this.makeGetRequest(tenantUri, token);
|
||||
const tenants: Tenant[] = tenantResponse.data.value.map((tenantInfo: TenantResponse) => {
|
||||
if (tenantInfo.displayName) {
|
||||
tenantList.push(tenantInfo.displayName);
|
||||
} else {
|
||||
tenantList.push(tenantInfo.tenantId);
|
||||
Logger.info('Tenant display name found empty: {0}', tenantInfo.tenantId);
|
||||
}
|
||||
return {
|
||||
id: tenantInfo.tenantId,
|
||||
displayName: tenantInfo.displayName ? tenantInfo.displayName : tenantInfo.tenantId,
|
||||
userId: token,
|
||||
tenantCategory: tenantInfo.tenantCategory
|
||||
} as Tenant;
|
||||
});
|
||||
Logger.verbose(`Tenants: ${tenantList}`);
|
||||
const homeTenantIndex = tenants.findIndex(tenant => tenant.tenantCategory === Constants.HomeCategory);
|
||||
// remove home tenant from list of tenants
|
||||
if (homeTenantIndex >= 0) {
|
||||
const homeTenant = tenants.splice(homeTenantIndex, 1);
|
||||
tenants.unshift(homeTenant[0]);
|
||||
}
|
||||
return tenants;
|
||||
} catch (ex) {
|
||||
Logger.error(`Error fetching tenants :${ex}`);
|
||||
throw new Error('Error retrieving tenant information');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//#region tenant calls
|
||||
public async getTenantsAdal(token: AccessToken): Promise<Tenant[]> {
|
||||
const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2019-11-01');
|
||||
try {
|
||||
Logger.verbose('Fetching tenants with URI: {0}', tenantUri);
|
||||
let tenantList: string[] = [];
|
||||
const tenantResponse = await this.makeGetRequest(tenantUri, token.token);
|
||||
if (tenantResponse.status !== 200) {
|
||||
Logger.error(`Error with tenant response, status: ${tenantResponse.status} | status text: ${tenantResponse.statusText}`);
|
||||
@@ -378,16 +496,22 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
throw new Error('Error with tenant response');
|
||||
}
|
||||
const tenants: Tenant[] = tenantResponse.data.value.map((tenantInfo: TenantResponse) => {
|
||||
Logger.verbose(`Tenant: ${tenantInfo.displayName}`);
|
||||
if (tenantInfo.displayName) {
|
||||
tenantList.push(tenantInfo.displayName);
|
||||
} else {
|
||||
tenantList.push(tenantInfo.tenantId);
|
||||
Logger.info('Tenant display name found empty: {0}', tenantInfo.tenantId);
|
||||
}
|
||||
return {
|
||||
id: tenantInfo.tenantId,
|
||||
displayName: tenantInfo.displayName ? tenantInfo.displayName : localize('azureWorkAccountDisplayName', "Work or school account"),
|
||||
displayName: tenantInfo.displayName ? tenantInfo.displayName : tenantInfo.tenantId,
|
||||
userId: token.key,
|
||||
tenantCategory: tenantInfo.tenantCategory
|
||||
} as Tenant;
|
||||
});
|
||||
|
||||
const homeTenantIndex = tenants.findIndex(tenant => tenant.tenantCategory === 'Home');
|
||||
Logger.verbose(`Tenants: ${tenantList}`);
|
||||
const homeTenantIndex = tenants.findIndex(tenant => tenant.tenantCategory === Constants.HomeCategory);
|
||||
// remove home tenant from list of tenants
|
||||
if (homeTenantIndex >= 0) {
|
||||
const homeTenant = tenants.splice(homeTenantIndex, 1);
|
||||
tenants.unshift(homeTenant[0]);
|
||||
@@ -421,7 +545,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
public async getSavedToken(tenant: Tenant, resource: Resource, accountKey: azdata.AccountKey): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | undefined, expiresOn: string } | undefined> {
|
||||
public async getSavedTokenAdal(tenant: Tenant, resource: Resource, accountKey: azdata.AccountKey): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | undefined, expiresOn: string } | undefined> {
|
||||
const getMsg = localize('azure.cacheErrorGet', "Error when getting your account from the cache");
|
||||
const parseMsg = localize('azure.cacheErrorParse', "Error when parsing your account from the cache");
|
||||
|
||||
@@ -464,12 +588,22 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region interaction handling
|
||||
|
||||
public async handleInteractionRequired(tenant: Tenant, resource: Resource): Promise<OAuthTokenResponse | undefined> {
|
||||
//#region interaction handling
|
||||
public async handleInteractionRequiredMsal(tenant: Tenant, resource: Resource): Promise<AuthenticationResult | null> {
|
||||
const shouldOpen = await this.askUserForInteraction(tenant, resource);
|
||||
if (shouldOpen) {
|
||||
const result = await this.login(tenant, resource);
|
||||
const result = await this.loginMsal(tenant, resource);
|
||||
result?.authComplete?.resolve();
|
||||
return result?.response;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async handleInteractionRequiredAdal(tenant: Tenant, resource: Resource): Promise<OAuthTokenResponse | undefined> {
|
||||
const shouldOpen = await this.askUserForInteraction(tenant, resource);
|
||||
if (shouldOpen) {
|
||||
const result = await this.loginAdal(tenant, resource);
|
||||
result?.authComplete?.resolve();
|
||||
return result?.response;
|
||||
}
|
||||
@@ -487,13 +621,14 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
}
|
||||
|
||||
const getTenantConfigurationSet = (): Set<string> => {
|
||||
const configuration = vscode.workspace.getConfiguration('azure.tenant.config');
|
||||
const configuration = vscode.workspace.getConfiguration(Constants.AzureTenantConfigSection);
|
||||
let values: string[] = configuration.get('filter') ?? [];
|
||||
return new Set<string>(values);
|
||||
};
|
||||
|
||||
// The user wants to ignore this tenant.
|
||||
if (getTenantConfigurationSet().has(tenant.id)) {
|
||||
Logger.info(`Tenant ${tenant.id} found in the ignore list, authentication will not be attempted.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -544,22 +679,21 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
public createAccount(tokenClaims: TokenClaims, key: string, tenants: Tenant[]): AzureAccount {
|
||||
Logger.verbose(`Token Claims: ${tokenClaims.name}`);
|
||||
tenants.forEach((tenant) => {
|
||||
Logger.verbose(
|
||||
`Tenant ID: ${tenant.id}
|
||||
Tenant Name: ${tenant.displayName}`);
|
||||
Logger.verbose(`Tenant ID: ${tenant.id}, Tenant Name: ${tenant.displayName}`);
|
||||
});
|
||||
// Determine if this is a microsoft account
|
||||
let accountIssuer = 'unknown';
|
||||
|
||||
if (tokenClaims.iss === 'https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/') {
|
||||
accountIssuer = 'corp';
|
||||
if (tokenClaims.iss === 'https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/' ||
|
||||
tokenClaims.iss === 'https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/v2.0') {
|
||||
accountIssuer = Constants.AccountIssuer.Corp;
|
||||
}
|
||||
if (tokenClaims?.idp === 'live.com') {
|
||||
accountIssuer = 'msft';
|
||||
accountIssuer = Constants.AccountIssuer.Msft;
|
||||
}
|
||||
|
||||
const name = tokenClaims.name ?? tokenClaims.email ?? tokenClaims.unique_name;
|
||||
const email = tokenClaims.email ?? tokenClaims.unique_name;
|
||||
const name = tokenClaims.name ?? tokenClaims.email ?? tokenClaims.unique_name ?? tokenClaims.preferred_username;
|
||||
const email = tokenClaims.email ?? tokenClaims.unique_name ?? tokenClaims.preferred_username;
|
||||
|
||||
// Read more about tid > https://learn.microsoft.com/azure/active-directory/develop/id-tokens
|
||||
const owningTenant = tenants.find(t => t.id === tokenClaims.tid)
|
||||
@@ -572,25 +706,26 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
|
||||
let contextualDisplayName: string;
|
||||
switch (accountIssuer) {
|
||||
case 'corp':
|
||||
case Constants.AccountIssuer.Corp:
|
||||
contextualDisplayName = localize('azure.microsoftCorpAccount', "Microsoft Corp");
|
||||
break;
|
||||
case 'msft':
|
||||
case Constants.AccountIssuer.Msft:
|
||||
contextualDisplayName = localize('azure.microsoftAccountDisplayName', 'Microsoft Account');
|
||||
break;
|
||||
default:
|
||||
contextualDisplayName = displayName;
|
||||
}
|
||||
|
||||
let accountType = accountIssuer === 'msft'
|
||||
? this.MicrosoftAccountType
|
||||
: this.WorkSchoolAccountType;
|
||||
let accountType = accountIssuer === Constants.AccountIssuer.Msft
|
||||
? Constants.AccountType.Microsoft
|
||||
: Constants.AccountType.WorkSchool;
|
||||
|
||||
const account = {
|
||||
key: {
|
||||
providerId: this.metadata.id,
|
||||
accountId: key,
|
||||
accountVersion: AzureAuth.ACCOUNT_VERSION,
|
||||
accountVersion: Constants.AccountVersion,
|
||||
authLibrary: this._authLibrary
|
||||
},
|
||||
name: displayName,
|
||||
displayInfo: {
|
||||
@@ -603,7 +738,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
},
|
||||
properties: {
|
||||
providerSettings: this.metadata,
|
||||
isMsAccount: accountIssuer === 'msft',
|
||||
isMsAccount: accountIssuer === Constants.AccountIssuer.Msft,
|
||||
owningTenant: owningTenant,
|
||||
tenants,
|
||||
azureAuthType: this.authType
|
||||
@@ -660,8 +795,10 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
protected toBase64UrlEncoding(base64string: string): string {
|
||||
return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding
|
||||
}
|
||||
|
||||
public async deleteAllCache(): Promise<void> {
|
||||
public async deleteAllCacheMsal(): Promise<void> {
|
||||
this.clientApplication.clearCache();
|
||||
}
|
||||
public async deleteAllCacheAdal(): Promise<void> {
|
||||
const results = await this.tokenCache.findCredentials('');
|
||||
|
||||
for (let { account } of results) {
|
||||
@@ -671,7 +808,13 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
|
||||
public async clearCredentials(account: azdata.AccountKey): Promise<void> {
|
||||
try {
|
||||
return this.deleteAccountCache(account);
|
||||
// remove account based on authLibrary field, accounts added before this field was present will default to
|
||||
// ADAL method of account removal
|
||||
if (account.authLibrary === Constants.AuthLibrary.MSAL) {
|
||||
return this.deleteAccountCacheMsal(account);
|
||||
} else { // fallback to ADAL by default
|
||||
return this.deleteAccountCacheAdal(account);
|
||||
}
|
||||
} catch (ex) {
|
||||
const msg = localize('azure.cacheErrrorRemove', "Error when removing your account from the cache.");
|
||||
void vscode.window.showErrorMessage(msg);
|
||||
@@ -679,9 +822,27 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteAccountCache(account: azdata.AccountKey): Promise<void> {
|
||||
const results = await this.tokenCache.findCredentials(account.accountId);
|
||||
public async deleteAccountCacheMsal(account: azdata.AccountKey): Promise<void> {
|
||||
const tokenCache = this.clientApplication.getTokenCache();
|
||||
let msalAccount: AccountInfo | null;
|
||||
// if the accountId is a home ID, it will include a "." character
|
||||
if (account.accountId.includes(".")) {
|
||||
msalAccount = await tokenCache.getAccountByHomeId(account.accountId);
|
||||
} else {
|
||||
msalAccount = await tokenCache.getAccountByLocalId(account.accountId);
|
||||
}
|
||||
if (!msalAccount) {
|
||||
Logger.error(`MSAL: Unable to find account ${account.accountId} for removal`);
|
||||
throw Error(`Unable to find account ${account.accountId}`);
|
||||
}
|
||||
await tokenCache.removeAccount(msalAccount);
|
||||
}
|
||||
|
||||
public async deleteAccountCacheAdal(account: azdata.AccountKey): Promise<void> {
|
||||
const results = await this.tokenCache.findCredentials(account.accountId);
|
||||
if (!results) {
|
||||
Logger.error('ADAL: Unable to find account for removal');
|
||||
}
|
||||
for (let { account } of results) {
|
||||
await this.tokenCache.clearCredential(account);
|
||||
}
|
||||
@@ -722,6 +883,13 @@ export interface RefreshToken extends AccountKey {
|
||||
key: string
|
||||
}
|
||||
|
||||
export interface TenantResponse { // https://docs.microsoft.com/en-us/rest/api/resources/tenants/list
|
||||
id: string
|
||||
tenantId: string
|
||||
displayName?: string
|
||||
tenantCategory?: string
|
||||
}
|
||||
|
||||
export interface MultiTenantTokenResponse {
|
||||
[tenantId: string]: Token | undefined;
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@ import { SimpleTokenCache } from '../simpleTokenCache';
|
||||
import { SimpleWebServer } from '../utils/simpleWebServer';
|
||||
import { AzureAuthError } from './azureAuthError';
|
||||
import { Logger } from '../../utils/Logger';
|
||||
import * as Constants from '../../constants';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as path from 'path';
|
||||
import * as http from 'http';
|
||||
import * as qs from 'qs';
|
||||
import { promises as fs } from 'fs';
|
||||
import { PublicClientApplication, CryptoProvider, AuthorizationUrlRequest, AuthorizationCodeRequest, AuthenticationResult } from '@azure/msal-node';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -28,33 +30,43 @@ interface AuthCodeResponse {
|
||||
|
||||
interface CryptoValues {
|
||||
nonce: string;
|
||||
challengeMethod: string;
|
||||
codeVerifier: string;
|
||||
codeChallenge: string;
|
||||
}
|
||||
|
||||
|
||||
export class AzureAuthCodeGrant extends AzureAuth {
|
||||
private static readonly USER_FRIENDLY_NAME: string = localize('azure.azureAuthCodeGrantName', 'Azure Auth Code Grant');
|
||||
private cryptoProvider: CryptoProvider;
|
||||
private pkceCodes: CryptoValues;
|
||||
|
||||
constructor(
|
||||
metadata: AzureAccountProviderMetadata,
|
||||
tokenCache: SimpleTokenCache,
|
||||
context: vscode.ExtensionContext,
|
||||
uriEventEmitter: vscode.EventEmitter<vscode.Uri>,
|
||||
clientApplication: PublicClientApplication,
|
||||
authLibrary: string
|
||||
) {
|
||||
super(metadata, tokenCache, context, uriEventEmitter, AzureAuthType.AuthCodeGrant, AzureAuthCodeGrant.USER_FRIENDLY_NAME);
|
||||
super(metadata, tokenCache, context, clientApplication, uriEventEmitter, AzureAuthType.AuthCodeGrant, AzureAuthCodeGrant.USER_FRIENDLY_NAME, authLibrary);
|
||||
this.cryptoProvider = new CryptoProvider();
|
||||
this.pkceCodes = {
|
||||
nonce: '',
|
||||
challengeMethod: Constants.S256_CODE_CHALLENGE_METHOD, // Use SHA256 as the challenge method
|
||||
codeVerifier: '', // Generate a code verifier for the Auth Code Request first
|
||||
codeChallenge: '', // Generate a code challenge from the previously generated code verifier
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
protected async login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred<void, Error> }> {
|
||||
protected async loginAdal(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred<void, Error> }> {
|
||||
let authCompleteDeferred: Deferred<void, Error>;
|
||||
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
|
||||
let authResponse: AuthCodeResponse;
|
||||
|
||||
if (vscode.env.uiKind === vscode.UIKind.Web) {
|
||||
authResponse = await this.loginWeb(tenant, resource);
|
||||
authResponse = await this.loginWebAdal(tenant, resource);
|
||||
} else {
|
||||
authResponse = await this.loginDesktop(tenant, resource, authCompletePromise);
|
||||
authResponse = await this.loginDesktopAdal(tenant, resource, authCompletePromise);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -63,6 +75,30 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
||||
};
|
||||
}
|
||||
|
||||
protected async loginMsal(tenant: Tenant, resource: Resource): Promise<{ response: AuthenticationResult | null, authComplete: Deferred<void, Error> }> {
|
||||
let authCompleteDeferred: Deferred<void, Error>;
|
||||
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
|
||||
let authCodeRequest: AuthorizationCodeRequest;
|
||||
|
||||
if (vscode.env.uiKind === vscode.UIKind.Web) {
|
||||
authCodeRequest = await this.loginWebMsal(tenant, resource);
|
||||
} else {
|
||||
authCodeRequest = await this.loginDesktopMsal(tenant, resource, authCompletePromise);
|
||||
}
|
||||
|
||||
let result = await this.clientApplication.acquireTokenByCode(authCodeRequest);
|
||||
if (!result) {
|
||||
Logger.error('Failed to acquireTokenByCode');
|
||||
Logger.error(`Auth Code Request: ${JSON.stringify(authCodeRequest)}`)
|
||||
throw Error('Failed to fetch token using auth code');
|
||||
} else {
|
||||
return {
|
||||
response: result,
|
||||
authComplete: authCompleteDeferred!
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an OAuthTokenResponse from Microsoft OAuth
|
||||
*
|
||||
@@ -79,12 +115,47 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
||||
resource: resource.endpoint
|
||||
};
|
||||
|
||||
return this.getToken(tenant, resource, postData);
|
||||
return this.getTokenAdal(tenant, resource, postData);
|
||||
}
|
||||
|
||||
private async loginWeb(tenant: Tenant, resource: Resource): Promise<AuthCodeResponse> {
|
||||
private async loginWebMsal(tenant: Tenant, resource: Resource): Promise<AuthorizationCodeRequest> {
|
||||
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://microsoft.azurecore`));
|
||||
const { nonce, codeVerifier, codeChallenge } = this.createCryptoValues();
|
||||
await this.createCryptoValuesMsal();
|
||||
const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
|
||||
const state = `${port},${encodeURIComponent(this.pkceCodes.nonce)},${encodeURIComponent(callbackUri.query)}`;
|
||||
|
||||
try {
|
||||
let authUrlRequest: AuthorizationUrlRequest;
|
||||
authUrlRequest = {
|
||||
scopes: this.scopes,
|
||||
redirectUri: this.redirectUri,
|
||||
codeChallenge: this.pkceCodes.codeChallenge,
|
||||
codeChallengeMethod: this.pkceCodes.challengeMethod,
|
||||
prompt: Constants.SELECT_ACCOUNT,
|
||||
state: state
|
||||
};
|
||||
let authCodeRequest: AuthorizationCodeRequest;
|
||||
authCodeRequest = {
|
||||
scopes: this.scopes,
|
||||
redirectUri: this.redirectUri,
|
||||
codeVerifier: this.pkceCodes.codeVerifier,
|
||||
code: ''
|
||||
};
|
||||
let authCodeUrl = await this.clientApplication.getAuthCodeUrl(authUrlRequest);
|
||||
await vscode.env.openExternal(vscode.Uri.parse(authCodeUrl));
|
||||
const authCode = await this.handleWebResponse(state);
|
||||
authCodeRequest.code = authCode;
|
||||
|
||||
return authCodeRequest;
|
||||
} catch (e) {
|
||||
Logger.error('MSAL: Error requesting auth code', e);
|
||||
throw new AzureAuthError('error', 'Error requesting auth code', e);
|
||||
}
|
||||
}
|
||||
|
||||
private async loginWebAdal(tenant: Tenant, resource: Resource): Promise<AuthCodeResponse> {
|
||||
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://microsoft.azurecore`));
|
||||
const { nonce, codeVerifier, codeChallenge } = this.createCryptoValuesAdal();
|
||||
const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
|
||||
const state = `${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`;
|
||||
|
||||
@@ -94,8 +165,8 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
||||
client_id: this.clientId,
|
||||
redirect_uri: this.redirectUri,
|
||||
state,
|
||||
prompt: 'select_account',
|
||||
code_challenge_method: 'S256',
|
||||
prompt: Constants.SELECT_ACCOUNT,
|
||||
code_challenge_method: Constants.S256_CODE_CHALLENGE_METHOD,
|
||||
code_challenge: codeChallenge,
|
||||
resource: resource.id
|
||||
};
|
||||
@@ -141,7 +212,7 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
||||
}, {});
|
||||
}
|
||||
|
||||
private async loginDesktop(tenant: Tenant, resource: Resource, authCompletePromise: Promise<void>): Promise<AuthCodeResponse> {
|
||||
private async loginDesktopMsal(tenant: Tenant, resource: Resource, authCompletePromise: Promise<void>): Promise<AuthorizationCodeRequest> {
|
||||
const server = new SimpleWebServer();
|
||||
let serverPort: string;
|
||||
|
||||
@@ -151,7 +222,56 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
||||
const msg = localize('azure.serverCouldNotStart', 'Server could not start. This could be a permissions error or an incompatibility on your system. You can try enabling device code authentication from settings.');
|
||||
throw new AzureAuthError(msg, 'Server could not start', ex);
|
||||
}
|
||||
const { nonce, codeVerifier, codeChallenge } = this.createCryptoValues();
|
||||
await this.createCryptoValuesMsal();
|
||||
const state = `${serverPort},${this.pkceCodes.nonce}`;
|
||||
|
||||
try {
|
||||
let authUrlRequest: AuthorizationUrlRequest;
|
||||
authUrlRequest = {
|
||||
scopes: this.scopes,
|
||||
redirectUri: `${this.redirectUri}:${serverPort}/redirect`,
|
||||
codeChallenge: this.pkceCodes.codeChallenge,
|
||||
codeChallengeMethod: this.pkceCodes.challengeMethod,
|
||||
prompt: Constants.SELECT_ACCOUNT,
|
||||
authority: `https://login.microsoftonline.com/${tenant.id}`,
|
||||
state: state
|
||||
};
|
||||
let authCodeRequest: AuthorizationCodeRequest;
|
||||
authCodeRequest = {
|
||||
scopes: this.scopes,
|
||||
redirectUri: `${this.redirectUri}:${serverPort}/redirect`,
|
||||
codeVerifier: this.pkceCodes.codeVerifier,
|
||||
authority: `https://login.microsoftonline.com/${tenant.id}`,
|
||||
code: ''
|
||||
};
|
||||
let authCodeUrl = await this.clientApplication.getAuthCodeUrl(authUrlRequest);
|
||||
|
||||
|
||||
await vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${serverPort}/signin?nonce=${encodeURIComponent(this.pkceCodes.nonce)}`));
|
||||
const authCode = await this.addServerListeners(server, this.pkceCodes.nonce, authCodeUrl, authCompletePromise);
|
||||
|
||||
authCodeRequest.code = authCode;
|
||||
|
||||
return authCodeRequest;
|
||||
}
|
||||
|
||||
catch (e) {
|
||||
Logger.error('MSAL: Error requesting auth code', e);
|
||||
throw new AzureAuthError('error', 'Error requesting auth code', e);
|
||||
}
|
||||
}
|
||||
|
||||
private async loginDesktopAdal(tenant: Tenant, resource: Resource, authCompletePromise: Promise<void>): Promise<AuthCodeResponse> {
|
||||
const server = new SimpleWebServer();
|
||||
let serverPort: string;
|
||||
|
||||
try {
|
||||
serverPort = await server.startup();
|
||||
} catch (ex) {
|
||||
const msg = localize('azure.serverCouldNotStart', 'Server could not start. This could be a permissions error or an incompatibility on your system. You can try enabling device code authentication from settings.');
|
||||
throw new AzureAuthError(msg, 'Server could not start', ex);
|
||||
}
|
||||
const { nonce, codeVerifier, codeChallenge } = this.createCryptoValuesAdal();
|
||||
const state = `${serverPort},${encodeURIComponent(nonce)}`;
|
||||
const loginQuery = {
|
||||
response_type: 'code',
|
||||
@@ -159,8 +279,8 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
||||
client_id: this.clientId,
|
||||
redirect_uri: `${this.redirectUri}:${serverPort}/redirect`,
|
||||
state,
|
||||
prompt: 'select_account',
|
||||
code_challenge_method: 'S256',
|
||||
prompt: Constants.SELECT_ACCOUNT,
|
||||
code_challenge_method: Constants.S256_CODE_CHALLENGE_METHOD,
|
||||
code_challenge: codeChallenge,
|
||||
resource: resource.endpoint
|
||||
};
|
||||
@@ -272,13 +392,21 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
||||
}
|
||||
|
||||
|
||||
private createCryptoValues(): CryptoValues {
|
||||
private createCryptoValuesAdal(): CryptoValues {
|
||||
const nonce = crypto.randomBytes(16).toString('base64');
|
||||
const codeVerifier = this.toBase64UrlEncoding(crypto.randomBytes(32).toString('base64'));
|
||||
const codeChallenge = this.toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64'));
|
||||
const challengeMethod = '';
|
||||
|
||||
return {
|
||||
nonce, codeVerifier, codeChallenge
|
||||
nonce, challengeMethod, codeVerifier, codeChallenge
|
||||
};
|
||||
}
|
||||
|
||||
private async createCryptoValuesMsal(): Promise<void> {
|
||||
this.pkceCodes.nonce = this.cryptoProvider.createNewGuid();
|
||||
const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();
|
||||
this.pkceCodes.codeVerifier = verifier;
|
||||
this.pkceCodes.codeChallenge = challenge;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
DeviceCodeCheckPostData,
|
||||
|
||||
} from './azureAuth';
|
||||
|
||||
import {
|
||||
AzureAccountProviderMetadata,
|
||||
AzureAuthType,
|
||||
@@ -21,8 +20,10 @@ import {
|
||||
Resource
|
||||
} from 'azurecore';
|
||||
import { Deferred } from '../interfaces';
|
||||
import { AuthenticationResult, DeviceCodeRequest, PublicClientApplication } from '@azure/msal-node';
|
||||
import { SimpleTokenCache } from '../simpleTokenCache';
|
||||
import { Logger } from '../../utils/Logger';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
interface DeviceCodeLogin { // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code
|
||||
@@ -50,12 +51,34 @@ export class AzureDeviceCode extends AzureAuth {
|
||||
tokenCache: SimpleTokenCache,
|
||||
context: vscode.ExtensionContext,
|
||||
uriEventEmitter: vscode.EventEmitter<vscode.Uri>,
|
||||
clientApplication: PublicClientApplication,
|
||||
authLibrary: string
|
||||
) {
|
||||
super(metadata, tokenCache, context, uriEventEmitter, AzureAuthType.DeviceCode, AzureDeviceCode.USER_FRIENDLY_NAME);
|
||||
super(metadata, tokenCache, context, clientApplication, uriEventEmitter, AzureAuthType.DeviceCode, AzureDeviceCode.USER_FRIENDLY_NAME, authLibrary);
|
||||
this.pageTitle = localize('addAccount', "Add {0} account", this.metadata.displayName);
|
||||
|
||||
}
|
||||
protected async login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse, authComplete: Deferred<void, Error> }> {
|
||||
|
||||
protected async loginMsal(tenant: Tenant, resource: Resource): Promise<{ response: AuthenticationResult | null, authComplete: Deferred<void, Error> }> {
|
||||
let authCompleteDeferred: Deferred<void, Error>;
|
||||
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
|
||||
|
||||
const deviceCodeRequest: DeviceCodeRequest = {
|
||||
scopes: this.scopes,
|
||||
authority: `https://login.microsoftonline.com/${tenant.id}`,
|
||||
deviceCodeCallback: async (response) => {
|
||||
await azdata.accounts.beginAutoOAuthDeviceCode(this.metadata.id, this.pageTitle, response.message, response.userCode, response.verificationUri);
|
||||
}
|
||||
};
|
||||
const authResult = await this.clientApplication.acquireTokenByDeviceCode(deviceCodeRequest);
|
||||
this.closeOnceComplete(authCompletePromise).catch(Logger.error);
|
||||
|
||||
return {
|
||||
response: authResult,
|
||||
authComplete: authCompleteDeferred!
|
||||
};
|
||||
}
|
||||
|
||||
protected async loginAdal(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse, authComplete: Deferred<void, Error> }> {
|
||||
let authCompleteDeferred: Deferred<void, Error>;
|
||||
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
|
||||
|
||||
@@ -79,7 +102,7 @@ export class AzureDeviceCode extends AzureAuth {
|
||||
const currentTime = new Date().getTime() / 1000;
|
||||
const expiresOn = `${currentTime + finalDeviceLogin.expires_in}`;
|
||||
|
||||
const result = await this.getTokenHelper(tenant, resource, accessTokenString, refreshTokenString, expiresOn);
|
||||
const result = await this.getTokenHelperAdal(tenant, resource, accessTokenString, refreshTokenString, expiresOn);
|
||||
this.closeOnceComplete(authCompletePromise).catch(Logger.error);
|
||||
|
||||
return {
|
||||
@@ -93,7 +116,6 @@ export class AzureDeviceCode extends AzureAuth {
|
||||
azdata.accounts.endAutoOAuthDeviceCode();
|
||||
}
|
||||
|
||||
|
||||
private setupPolling(info: DeviceCodeLogin): Promise<DeviceCodeLoginResult> {
|
||||
const timeoutMessage = localize('azure.timeoutDeviceCode', 'Timed out when waiting for device code login.');
|
||||
const fiveMinutes = 5 * 60 * 1000;
|
||||
@@ -130,18 +152,15 @@ export class AzureDeviceCode extends AzureAuth {
|
||||
};
|
||||
|
||||
const postResult = await this.makePostRequest(uri, postData);
|
||||
|
||||
const result: DeviceCodeLoginResult = postResult.data;
|
||||
|
||||
return result;
|
||||
} catch (ex) {
|
||||
console.log(ex);
|
||||
console.log('Unexpected error making Azure auth request', 'azureCore.checkForResult', JSON.stringify(ex?.response?.data, undefined, 2));
|
||||
Logger.error('Unexpected error making Azure auth request', 'azureCore.checkForResult', JSON.stringify(ex?.response?.data, undefined, 2));
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override async autoOAuthCancelled(): Promise<void> {
|
||||
return azdata.accounts.endAutoOAuthDeviceCode();
|
||||
}
|
||||
|
||||
@@ -13,31 +13,37 @@ import {
|
||||
AzureAccount
|
||||
} from 'azurecore';
|
||||
import { Deferred } from './interfaces';
|
||||
|
||||
import { PublicClientApplication } from '@azure/msal-node';
|
||||
import { SimpleTokenCache } from './simpleTokenCache';
|
||||
import { Logger } from '../utils/Logger';
|
||||
import { MultiTenantTokenResponse, Token, AzureAuth } from './auths/azureAuth';
|
||||
import { AzureAuthCodeGrant } from './auths/azureAuthCodeGrant';
|
||||
import { AzureDeviceCode } from './auths/azureDeviceCode';
|
||||
import { filterAccounts } from '../azureResource/utils';
|
||||
import * as Constants from '../constants';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disposable {
|
||||
private static readonly CONFIGURATION_SECTION = 'accounts.azure.auth';
|
||||
private readonly authMappings = new Map<AzureAuthType, AzureAuth>();
|
||||
private initComplete!: Deferred<void, Error>;
|
||||
private initCompletePromise: Promise<void> = new Promise<void>((resolve, reject) => this.initComplete = { resolve, reject });
|
||||
public clientApplication: PublicClientApplication;
|
||||
|
||||
constructor(
|
||||
metadata: AzureAccountProviderMetadata,
|
||||
tokenCache: SimpleTokenCache,
|
||||
context: vscode.ExtensionContext,
|
||||
clientApplication: PublicClientApplication,
|
||||
uriEventHandler: vscode.EventEmitter<vscode.Uri>,
|
||||
private readonly authLibrary: string,
|
||||
private readonly forceDeviceCode: boolean = false
|
||||
) {
|
||||
this.clientApplication = clientApplication;
|
||||
|
||||
vscode.workspace.onDidChangeConfiguration((changeEvent) => {
|
||||
const impact = changeEvent.affectsConfiguration(AzureAccountProvider.CONFIGURATION_SECTION);
|
||||
if (impact === true) {
|
||||
const impactProvider = changeEvent.affectsConfiguration(Constants.AccountsAzureAuthSection);
|
||||
if (impactProvider === true) {
|
||||
this.handleAuthMapping(metadata, tokenCache, context, uriEventHandler);
|
||||
}
|
||||
});
|
||||
@@ -50,25 +56,28 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
|
||||
}
|
||||
|
||||
clearTokenCache(): Thenable<void> {
|
||||
return this.getAuthMethod().deleteAllCache();
|
||||
return this.authLibrary === Constants.AuthLibrary.MSAL
|
||||
? this.getAuthMethod().deleteAllCacheMsal()
|
||||
// fallback to ADAL as default
|
||||
: this.getAuthMethod().deleteAllCacheAdal();
|
||||
}
|
||||
|
||||
private handleAuthMapping(metadata: AzureAccountProviderMetadata, tokenCache: SimpleTokenCache, context: vscode.ExtensionContext, uriEventHandler: vscode.EventEmitter<vscode.Uri>) {
|
||||
this.authMappings.forEach(m => m.dispose());
|
||||
this.authMappings.clear();
|
||||
const configuration = vscode.workspace.getConfiguration(AzureAccountProvider.CONFIGURATION_SECTION);
|
||||
|
||||
const codeGrantMethod: boolean = configuration.get<boolean>('codeGrant', false);
|
||||
const deviceCodeMethod: boolean = configuration.get<boolean>('deviceCode', false);
|
||||
const configuration = vscode.workspace.getConfiguration(Constants.AccountsAzureAuthSection);
|
||||
const codeGrantMethod: boolean = configuration.get<boolean>(Constants.AuthType.CodeGrant, false);
|
||||
const deviceCodeMethod: boolean = configuration.get<boolean>(Constants.AuthType.DeviceCode, false);
|
||||
|
||||
if (codeGrantMethod === true && !this.forceDeviceCode) {
|
||||
this.authMappings.set(AzureAuthType.AuthCodeGrant, new AzureAuthCodeGrant(metadata, tokenCache, context, uriEventHandler));
|
||||
this.authMappings.set(AzureAuthType.AuthCodeGrant, new AzureAuthCodeGrant(metadata, tokenCache, context, uriEventHandler, this.clientApplication, this.authLibrary));
|
||||
}
|
||||
if (deviceCodeMethod === true || this.forceDeviceCode) {
|
||||
this.authMappings.set(AzureAuthType.DeviceCode, new AzureDeviceCode(metadata, tokenCache, context, uriEventHandler));
|
||||
this.authMappings.set(AzureAuthType.DeviceCode, new AzureDeviceCode(metadata, tokenCache, context, uriEventHandler, this.clientApplication, this.authLibrary));
|
||||
}
|
||||
if (codeGrantMethod === false && deviceCodeMethod === false && !this.forceDeviceCode) {
|
||||
Logger.error('Error: No authentication methods selected');
|
||||
console.error('No authentication methods selected');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,13 +106,19 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
|
||||
private async _initialize(storedAccounts: AzureAccount[]): Promise<AzureAccount[]> {
|
||||
const accounts: AzureAccount[] = [];
|
||||
console.log(`Initializing stored accounts ${JSON.stringify(accounts)}`);
|
||||
for (let account of storedAccounts) {
|
||||
const updatedAccounts = filterAccounts(storedAccounts, this.authLibrary);
|
||||
for (let account of updatedAccounts) {
|
||||
const azureAuth = this.getAuthMethod(account);
|
||||
if (!azureAuth) {
|
||||
account.isStale = true;
|
||||
accounts.push(account);
|
||||
} else {
|
||||
accounts.push(await azureAuth.refreshAccess(account));
|
||||
account.isStale = false;
|
||||
if (this.authLibrary === Constants.AuthLibrary.MSAL) {
|
||||
accounts.push(account);
|
||||
} else { // fallback to ADAL as default
|
||||
accounts.push(await azureAuth.refreshAccessAdal(account));
|
||||
}
|
||||
}
|
||||
}
|
||||
this.initComplete.resolve();
|
||||
@@ -123,7 +138,23 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
|
||||
await this.initCompletePromise;
|
||||
const azureAuth = this.getAuthMethod(account);
|
||||
Logger.pii(`Getting account security token for ${JSON.stringify(account.key)} (tenant ${tenantId}). Auth Method = ${azureAuth.userFriendlyName}`, [], []);
|
||||
return azureAuth?.getAccountSecurityToken(account, tenantId, resource);
|
||||
if (this.authLibrary === Constants.AuthLibrary.MSAL) {
|
||||
let authResult = await azureAuth?.getTokenMsal(account.key.accountId, resource, tenantId);
|
||||
if (!authResult || !authResult.account || !authResult.account.idTokenClaims) {
|
||||
Logger.error(`MSAL: getToken call failed`);
|
||||
throw Error('Failed to get token');
|
||||
} else {
|
||||
const token: Token = {
|
||||
key: authResult.account.homeAccountId,
|
||||
token: authResult.accessToken,
|
||||
tokenType: authResult.tokenType,
|
||||
expiresOn: authResult.account.idTokenClaims.exp
|
||||
};
|
||||
return token;
|
||||
}
|
||||
} else { // fallback to ADAL as default
|
||||
return azureAuth?.getAccountSecurityTokenAdal(account, tenantId, resource);
|
||||
}
|
||||
}
|
||||
|
||||
private async _getSecurityToken(account: AzureAccount, resource: azdata.AzureResource): Promise<MultiTenantTokenResponse | undefined> {
|
||||
@@ -178,7 +209,6 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
|
||||
|
||||
return pick.azureAuth.startLogin();
|
||||
}
|
||||
|
||||
refresh(account: AzureAccount): Thenable<AzureAccount | azdata.PromptFailedResult> {
|
||||
return this._refresh(account);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,18 @@ import * as azdata from 'azdata';
|
||||
import * as events from 'events';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as vscode from 'vscode';
|
||||
import * as os from 'os';
|
||||
import { SimpleTokenCache } from './simpleTokenCache';
|
||||
import providerSettings from './providerSettings';
|
||||
import { AzureAccountProvider as AzureAccountProvider } from './azureAccountProvider';
|
||||
import { AzureAccountProviderMetadata } from 'azurecore';
|
||||
import { ProviderSettings } from './interfaces';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { PublicClientApplication } from '@azure/msal-node';
|
||||
import { DataProtectionScope, PersistenceCachePlugin, FilePersistenceWithDataProtection, KeychainPersistence, LibSecretPersistence } from '@azure/msal-node-extensions';
|
||||
import * as path from 'path';
|
||||
import { Logger } from '../utils/Logger';
|
||||
import * as Constants from '../constants';
|
||||
|
||||
let localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -23,11 +29,6 @@ class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.
|
||||
}
|
||||
|
||||
export class AzureAccountProviderService implements vscode.Disposable {
|
||||
// CONSTANTS ///////////////////////////////////////////////////////////////
|
||||
private static CommandClearTokenCache = 'accounts.clearTokenCache';
|
||||
private static ConfigurationSection = 'accounts.azure.cloud';
|
||||
private static CredentialNamespace = 'azureAccountProviderCredentials';
|
||||
|
||||
// MEMBER VARIABLES ////////////////////////////////////////////////////////
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private _accountDisposals: { [accountProviderId: string]: vscode.Disposable } = {};
|
||||
@@ -37,8 +38,12 @@ export class AzureAccountProviderService implements vscode.Disposable {
|
||||
private _currentConfig: vscode.WorkspaceConfiguration | undefined = undefined;
|
||||
private _event: events.EventEmitter = new events.EventEmitter();
|
||||
private readonly _uriEventHandler: UriEventHandler = new UriEventHandler();
|
||||
public clientApplication!: PublicClientApplication;
|
||||
public persistence: FilePersistenceWithDataProtection | KeychainPersistence | LibSecretPersistence | undefined;
|
||||
|
||||
constructor(private _context: vscode.ExtensionContext, private _userStoragePath: string) {
|
||||
constructor(private _context: vscode.ExtensionContext,
|
||||
private _userStoragePath: string,
|
||||
private _authLibrary: string) {
|
||||
this._disposables.push(vscode.window.registerUriHandler(this._uriEventHandler));
|
||||
}
|
||||
|
||||
@@ -47,17 +52,16 @@ export class AzureAccountProviderService implements vscode.Disposable {
|
||||
let self = this;
|
||||
|
||||
// Register commands
|
||||
this._context.subscriptions.push(vscode.commands.registerCommand(
|
||||
AzureAccountProviderService.CommandClearTokenCache,
|
||||
() => { self._event.emit(AzureAccountProviderService.CommandClearTokenCache); }
|
||||
this._context.subscriptions.push(vscode.commands.registerCommand(Constants.AccountsClearTokenCacheCommand,
|
||||
() => { self._event.emit(Constants.AccountsClearTokenCacheCommand); }
|
||||
));
|
||||
this._event.on(AzureAccountProviderService.CommandClearTokenCache, () => { void self.onClearTokenCache(); });
|
||||
this._event.on(Constants.AccountsClearTokenCacheCommand, () => { void self.onClearTokenCache(); });
|
||||
|
||||
// 1) Get a credential provider
|
||||
// 2a) Store the credential provider for use later
|
||||
// 2b) Register the configuration change handler
|
||||
// 2c) Perform an initial config change handling
|
||||
return azdata.credentials.getProvider(AzureAccountProviderService.CredentialNamespace)
|
||||
return azdata.credentials.getProvider(Constants.AzureAccountProviderCredentials)
|
||||
.then(credProvider => {
|
||||
this._credentialProvider = credProvider;
|
||||
|
||||
@@ -103,7 +107,7 @@ export class AzureAccountProviderService implements vscode.Disposable {
|
||||
// Add a new change processing onto the existing promise change
|
||||
await this._configChangePromiseChain;
|
||||
// Grab the stored config and the latest config
|
||||
let newConfig = vscode.workspace.getConfiguration(AzureAccountProviderService.ConfigurationSection);
|
||||
let newConfig = vscode.workspace.getConfiguration(Constants.AccountsAzureCloudSection);
|
||||
let oldConfig = this._currentConfig;
|
||||
this._currentConfig = newConfig;
|
||||
|
||||
@@ -138,22 +142,58 @@ export class AzureAccountProviderService implements vscode.Disposable {
|
||||
}
|
||||
|
||||
private async registerAccountProvider(provider: ProviderSettings): Promise<void> {
|
||||
const isSaw: boolean = vscode.env.appName.toLowerCase().indexOf(Constants.Saw) > 0;
|
||||
const noSystemKeychain = vscode.workspace.getConfiguration(Constants.AzureSection).get<boolean>(Constants.NoSystemKeyChainSection);
|
||||
const platform = os.platform();
|
||||
const tokenCacheKey = `azureTokenCache-${provider.metadata.id}`;
|
||||
const lockOptions = {
|
||||
retryNumber: 100,
|
||||
retryDelay: 50
|
||||
}
|
||||
|
||||
try {
|
||||
const noSystemKeychain = vscode.workspace.getConfiguration('azure').get<boolean>('noSystemKeychain');
|
||||
let tokenCacheKey = `azureTokenCache-${provider.metadata.id}`;
|
||||
if (!this._credentialProvider) {
|
||||
throw new Error('Credential provider not registered');
|
||||
}
|
||||
|
||||
let simpleTokenCache = new SimpleTokenCache(tokenCacheKey, this._userStoragePath, noSystemKeychain, this._credentialProvider);
|
||||
await simpleTokenCache.init();
|
||||
const cachePath = path.join(this._userStoragePath, Constants.ConfigFilePath);
|
||||
|
||||
const isSaw: boolean = vscode.env.appName.toLowerCase().indexOf('saw') > 0;
|
||||
let accountProvider = new AzureAccountProvider(provider.metadata as AzureAccountProviderMetadata, simpleTokenCache, this._context, this._uriEventHandler, isSaw);
|
||||
switch (platform) {
|
||||
case Constants.Platform.Windows:
|
||||
const dataProtectionScope = DataProtectionScope.CurrentUser;
|
||||
const optionalEntropy = "";
|
||||
this.persistence = await FilePersistenceWithDataProtection.create(cachePath, dataProtectionScope, optionalEntropy);
|
||||
break;
|
||||
case Constants.Platform.Mac:
|
||||
case Constants.Platform.Linux:
|
||||
this.persistence = await KeychainPersistence.create(cachePath, Constants.ServiceName, Constants.Account);
|
||||
break;
|
||||
}
|
||||
if (!this.persistence) {
|
||||
Logger.error('Unable to intialize persistence for access token cache. Tokens will not persist in system memory for future use.');
|
||||
throw new Error('Unable to intialize persistence for access token cache. Tokens will not persist in system memory for future use.');
|
||||
}
|
||||
|
||||
let persistenceCachePlugin: PersistenceCachePlugin = new PersistenceCachePlugin(this.persistence, lockOptions); // or any of the other ones.
|
||||
const MSAL_CONFIG = {
|
||||
auth: {
|
||||
clientId: provider.metadata.settings.clientId,
|
||||
redirect_uri: `${provider.metadata.settings.redirectUri}/redirect`
|
||||
},
|
||||
cache: {
|
||||
cachePlugin: persistenceCachePlugin
|
||||
}
|
||||
}
|
||||
|
||||
this.clientApplication = new PublicClientApplication(MSAL_CONFIG);
|
||||
let accountProvider = new AzureAccountProvider(provider.metadata as AzureAccountProviderMetadata,
|
||||
simpleTokenCache, this._context, this.clientApplication, this._uriEventHandler, this._authLibrary, isSaw);
|
||||
this._accountProviders[provider.metadata.id] = accountProvider;
|
||||
this._accountDisposals[provider.metadata.id] = azdata.accounts.registerAccountProvider(provider.metadata, accountProvider);
|
||||
} catch (e) {
|
||||
console.error(`Failed to register account provider: ${e}`);
|
||||
console.error(`Failed to register account provider, isSaw: ${isSaw}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ const publicAzureSettings: ProviderSettings = {
|
||||
},
|
||||
armResource: {
|
||||
id: SettingIds.arm,
|
||||
endpoint: 'https://management.azure.com',
|
||||
endpoint: 'https://management.azure.com/',
|
||||
azureResourceId: AzureResource.ResourceManagement
|
||||
},
|
||||
sqlResource: {
|
||||
|
||||
@@ -24,13 +24,15 @@ const typesClause = [
|
||||
].map(type => `type == "${type}"`).join(' or ');
|
||||
|
||||
export class AzureDataGridProvider implements azdata.DataGridProvider {
|
||||
constructor(private _appContext: AppContext) { }
|
||||
constructor(private _appContext: AppContext,
|
||||
private readonly authLibrary: string) { }
|
||||
|
||||
public providerId = constants.dataGridProviderId;
|
||||
public title = loc.azureResourcesGridTitle;
|
||||
|
||||
public async getDataGridItems() {
|
||||
const accounts = await azdata.accounts.getAllAccounts();
|
||||
let accounts: azdata.Account[];
|
||||
accounts = azureResourceUtils.filterAccounts(await azdata.accounts.getAllAccounts(), this.authLibrary);
|
||||
const items: any[] = [];
|
||||
await Promise.all(accounts.map(async (account) => {
|
||||
await Promise.all(account.properties.tenants.map(async (tenant: { id: string; }) => {
|
||||
|
||||
@@ -19,7 +19,7 @@ import { FlatAccountTreeNode } from './tree/flatAccountTreeNode';
|
||||
import { ConnectionDialogTreeProvider } from './tree/connectionDialogTreeProvider';
|
||||
import { AzureResourceErrorMessageUtil } from './utils';
|
||||
|
||||
export function registerAzureResourceCommands(appContext: AppContext, azureViewTree: AzureResourceTreeProvider, connectionDialogTree: ConnectionDialogTreeProvider): void {
|
||||
export function registerAzureResourceCommands(appContext: AppContext, azureViewTree: AzureResourceTreeProvider, connectionDialogTree: ConnectionDialogTreeProvider, authLibrary: string): void {
|
||||
const trees = [azureViewTree, connectionDialogTree];
|
||||
vscode.commands.registerCommand('azure.resource.startterminal', async (node?: TreeNode) => {
|
||||
try {
|
||||
|
||||
@@ -28,7 +28,6 @@ export class AzureResourceSubscriptionService implements IAzureResourceSubscript
|
||||
const subscriptions: azureResource.AzureResourceSubscription[] = [];
|
||||
let gotSubscriptions = false;
|
||||
const errors: Error[] = [];
|
||||
|
||||
for (const tenantId of tenantIds ?? account.properties.tenants.map(t => t.id)) {
|
||||
try {
|
||||
const token = await azdata.accounts.getAccountSecurityToken(account, tenantId, azdata.AzureResource.ResourceManagement);
|
||||
@@ -42,6 +41,7 @@ export class AzureResourceSubscriptionService implements IAzureResourceSubscript
|
||||
tenant: tenantId
|
||||
};
|
||||
}));
|
||||
Logger.verbose(`AzureResourceSubscriptionService.getSubscriptions: Retrieved ${newSubs.length} subscriptions for tenant ${tenantId} / account ${account.displayInfo.displayName}`);
|
||||
gotSubscriptions = true;
|
||||
}
|
||||
else if (!account.isStale) {
|
||||
|
||||
@@ -66,23 +66,25 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
|
||||
if (subscriptions.length === 0) {
|
||||
return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.noSubscriptionsLabel, this)];
|
||||
} else {
|
||||
// Filter out everything that we can't authenticate to.
|
||||
const hasTokenResults = await Promise.all(subscriptions.map(async s => {
|
||||
let token: azdata.accounts.AccountSecurityToken | undefined = undefined;
|
||||
let errMsg = '';
|
||||
try {
|
||||
token = await azdata.accounts.getAccountSecurityToken(this.account, s.tenant!, azdata.AzureResource.ResourceManagement);
|
||||
} catch (err) {
|
||||
errMsg = AzureResourceErrorMessageUtil.getErrorMessage(err);
|
||||
}
|
||||
if (!token) {
|
||||
void vscode.window.showWarningMessage(localize('azure.unableToAccessSubscription', "Unable to access subscription {0} ({1}). Please [refresh the account](command:azure.resource.signin) to try again. {2}", s.name, s.id, errMsg));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
subscriptions = subscriptions.filter((_s, i) => hasTokenResults[i]);
|
||||
|
||||
const authLibrary = vscode.workspace.getConfiguration('azure').get('authenticationLibrary');
|
||||
if (authLibrary === 'ADAL') {
|
||||
// Filter out everything that we can't authenticate to.
|
||||
const hasTokenResults = await Promise.all(subscriptions.map(async s => {
|
||||
let token: azdata.accounts.AccountSecurityToken | undefined = undefined;
|
||||
let errMsg = '';
|
||||
try {
|
||||
token = await azdata.accounts.getAccountSecurityToken(this.account, s.tenant!, azdata.AzureResource.ResourceManagement);
|
||||
} catch (err) {
|
||||
errMsg = AzureResourceErrorMessageUtil.getErrorMessage(err);
|
||||
}
|
||||
if (!token) {
|
||||
void vscode.window.showWarningMessage(localize('azure.unableToAccessSubscription', "Unable to access subscription {0} ({1}). Please [refresh the account](command:azure.resource.signin) to try again. {2}", s.name, s.id, errMsg));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
subscriptions = subscriptions.filter((_s, i) => hasTokenResults[i]);
|
||||
}
|
||||
let subTreeNodes = await Promise.all(subscriptions.map(async (subscription) => {
|
||||
return new AzureResourceSubscriptionTreeNode(this.account, subscription, subscription.tenant!, this.appContext, this.treeChangeHandler, this);
|
||||
}));
|
||||
@@ -164,4 +166,9 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
|
||||
private _selectedSubscriptionCount = 0;
|
||||
|
||||
private static readonly noSubscriptionsLabel = localize('azure.resource.tree.accountTreeNode.noSubscriptionsLabel', "No Subscriptions found.");
|
||||
|
||||
sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { TreeNode } from '../treeNode';
|
||||
import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode';
|
||||
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
|
||||
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
|
||||
import { AzureResourceErrorMessageUtil, equals } from '../utils';
|
||||
import { AzureResourceErrorMessageUtil, equals, filterAccounts } from '../utils';
|
||||
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
|
||||
import { FlatAccountTreeNode } from './flatAccountTreeNode';
|
||||
import { Logger } from '../../utils/Logger';
|
||||
@@ -26,10 +26,11 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider<Tre
|
||||
private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode | undefined>();
|
||||
private loadingAccountsPromise: Promise<void> | undefined;
|
||||
|
||||
public constructor(private readonly appContext: AppContext) {
|
||||
public constructor(private readonly appContext: AppContext,
|
||||
private readonly authLibrary: string) {
|
||||
azdata.accounts.onDidChangeAccounts(async (e: azdata.DidChangeAccountsParams) => {
|
||||
// This event sends it per provider, we need to make sure we get all the azure related accounts
|
||||
let accounts = await azdata.accounts.getAllAccounts();
|
||||
let accounts = filterAccounts(await azdata.accounts.getAllAccounts(), authLibrary);
|
||||
accounts = accounts.filter(a => a.key.providerId.startsWith('azure'));
|
||||
// the onDidChangeAccounts event will trigger in many cases where the accounts didn't actually change
|
||||
// the notifyNodeChanged event triggers a refresh which triggers a getChildren which can trigger this callback
|
||||
@@ -55,10 +56,11 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider<Tre
|
||||
}
|
||||
|
||||
if (this.accounts && this.accounts.length > 0) {
|
||||
let accounts = filterAccounts(this.accounts, this.authLibrary);
|
||||
const accountNodes: FlatAccountTreeNode[] = [];
|
||||
const errorMessages: string[] = [];
|
||||
// We are doing sequential account loading to avoid the Azure request throttling
|
||||
for (const account of this.accounts) {
|
||||
for (const account of accounts) {
|
||||
try {
|
||||
const accountNode = new FlatAccountTreeNode(account, this.appContext, this);
|
||||
await accountNode.updateLabel();
|
||||
@@ -85,7 +87,7 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider<Tre
|
||||
|
||||
private async loadAccounts(): Promise<void> {
|
||||
try {
|
||||
this.accounts = await azdata.accounts.getAllAccounts();
|
||||
this.accounts = filterAccounts(await azdata.accounts.getAllAccounts(), this.authLibrary);
|
||||
// System has been initialized
|
||||
this.setSystemInitialized();
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
|
||||
@@ -12,12 +12,12 @@ const localize = nls.loadMessageBundle();
|
||||
import { TreeNode } from '../treeNode';
|
||||
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
|
||||
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
|
||||
import { AzureResourceErrorMessageUtil } from '../utils';
|
||||
import { AzureResourceErrorMessageUtil, filterAccounts } from '../utils';
|
||||
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
|
||||
import { IAzureResourceNodeWithProviderId, IAzureResourceSubscriptionService } from '../interfaces';
|
||||
import { AzureResourceServiceNames } from '../constants';
|
||||
import { AzureResourceService } from '../resourceService';
|
||||
|
||||
import { Logger } from '../../utils/Logger';
|
||||
|
||||
export class FlatAzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNode>, IAzureResourceTreeChangeHandler {
|
||||
public isSystemInitialized: boolean = false;
|
||||
@@ -26,7 +26,8 @@ export class FlatAzureResourceTreeProvider implements vscode.TreeDataProvider<Tr
|
||||
|
||||
private resourceLoader: ResourceLoader | undefined;
|
||||
|
||||
public constructor(private readonly appContext: AppContext) {
|
||||
public constructor(private readonly appContext: AppContext,
|
||||
private readonly authLibrary: string) {
|
||||
}
|
||||
|
||||
public async getChildren(element?: TreeNode): Promise<TreeNode[]> {
|
||||
@@ -35,7 +36,7 @@ export class FlatAzureResourceTreeProvider implements vscode.TreeDataProvider<Tr
|
||||
}
|
||||
|
||||
if (!this.resourceLoader) {
|
||||
this.resourceLoader = new ResourceLoader(this.appContext);
|
||||
this.resourceLoader = new ResourceLoader(this.appContext, this.authLibrary);
|
||||
this.resourceLoader.onDidAddNewResource(e => this._onDidChangeTreeData.fire(e));
|
||||
}
|
||||
|
||||
@@ -87,7 +88,8 @@ class ResourceLoader {
|
||||
private readonly _onDidAddNewResource = new vscode.EventEmitter<TreeNode | undefined>();
|
||||
public readonly onDidAddNewResource = this._onDidAddNewResource.event;
|
||||
|
||||
constructor(private readonly appContext: AppContext) {
|
||||
constructor(private readonly appContext: AppContext,
|
||||
private readonly authLibrary: string) {
|
||||
this.subscriptionService = appContext.getService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService);
|
||||
this.resourceService = appContext.getService<AzureResourceService>(AzureResourceServiceNames.resourceService);
|
||||
}
|
||||
@@ -118,7 +120,7 @@ class ResourceLoader {
|
||||
|
||||
this._state = LoaderState.Loading;
|
||||
|
||||
const accounts = await azdata.accounts.getAllAccounts();
|
||||
const accounts = filterAccounts(await azdata.accounts.getAllAccounts(), this.authLibrary);
|
||||
|
||||
for (const account of accounts) {
|
||||
for (const tenant of account.properties.tenants) {
|
||||
@@ -141,7 +143,7 @@ class ResourceLoader {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('finished loading');
|
||||
Logger.verbose('finished loading all accounts and subscriptions');
|
||||
|
||||
clearInterval(interval);
|
||||
|
||||
@@ -208,5 +210,4 @@ class AzureResourceResourceTreeNode extends TreeNode {
|
||||
public get nodePathValue(): string {
|
||||
return this.resourceNodeWithProviderId.resourceNode.treeItem.id || '';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,11 +14,10 @@ import { AzureResourceAccountTreeNode } from './accountTreeNode';
|
||||
import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode';
|
||||
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
|
||||
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
|
||||
import { AzureResourceErrorMessageUtil, equals } from '../utils';
|
||||
import { AzureResourceErrorMessageUtil, equals, filterAccounts } from '../utils';
|
||||
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
|
||||
import { AzureAccount } from 'azurecore';
|
||||
|
||||
|
||||
export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNode>, IAzureResourceTreeChangeHandler {
|
||||
public isSystemInitialized: boolean = false;
|
||||
|
||||
@@ -26,10 +25,11 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
|
||||
private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode | undefined>();
|
||||
private loadingAccountsPromise: Promise<void> | undefined;
|
||||
|
||||
public constructor(private readonly appContext: AppContext) {
|
||||
public constructor(private readonly appContext: AppContext,
|
||||
private readonly authLibrary: string) {
|
||||
azdata.accounts.onDidChangeAccounts(async (e: azdata.DidChangeAccountsParams) => {
|
||||
// This event sends it per provider, we need to make sure we get all the azure related accounts
|
||||
let accounts = await azdata.accounts.getAllAccounts();
|
||||
let accounts = filterAccounts(await azdata.accounts.getAllAccounts(), authLibrary);
|
||||
accounts = accounts.filter(a => a.key.providerId.startsWith('azure'));
|
||||
// the onDidChangeAccounts event will trigger in many cases where the accounts didn't actually change
|
||||
// the notifyNodeChanged event triggers a refresh which triggers a getChildren which can trigger this callback
|
||||
@@ -56,6 +56,7 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
|
||||
|
||||
try {
|
||||
if (this.accounts && this.accounts.length > 0) {
|
||||
this.accounts = filterAccounts(this.accounts, this.authLibrary);
|
||||
return this.accounts.map((account) => new AzureResourceAccountTreeNode(account, this.appContext, this));
|
||||
} else {
|
||||
return [new AzureResourceAccountNotSignedInTreeNode()];
|
||||
@@ -67,7 +68,7 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
|
||||
|
||||
private async loadAccounts(): Promise<void> {
|
||||
try {
|
||||
this.accounts = await azdata.accounts.getAllAccounts();
|
||||
this.accounts = filterAccounts(await azdata.accounts.getAllAccounts(), this.authLibrary);
|
||||
// System has been initialized
|
||||
this.setSystemInitialized();
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
@@ -96,7 +97,6 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
|
||||
node.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
this._onDidChangeTreeData.fire(node);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { IAzureResourceSubscriptionFilterService, IAzureResourceSubscriptionServ
|
||||
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
|
||||
import { BlobServiceClient, StorageSharedKeyCredential } from '@azure/storage-blob';
|
||||
import providerSettings from '../account-provider/providerSettings';
|
||||
import * as Constants from '../constants';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -541,3 +542,18 @@ export function getProviderMetadataForAccount(account: AzureAccount): AzureAccou
|
||||
|
||||
return provider.metadata;
|
||||
}
|
||||
|
||||
// Filter accounts based on currently selected Auth Library:
|
||||
// if the account key is present, filter based on current auth library
|
||||
// if there is no account key (pre-MSAL account), then it is an ADAL account and
|
||||
// should be displayed as long as ADAL is the currently selected auth library
|
||||
export function filterAccounts(accounts: azdata.Account[], authLibrary: string): azdata.Account[] {
|
||||
let filteredAccounts = accounts.filter(account => {
|
||||
if (account.key.authLibrary) {
|
||||
return account.key.authLibrary === authLibrary;
|
||||
} else {
|
||||
return authLibrary === Constants.AuthLibrary.ADAL;
|
||||
}
|
||||
});
|
||||
return filteredAccounts;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,106 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const extensionConfigSectionName = 'azure';
|
||||
export const Account = 'account';
|
||||
|
||||
export const AccountsSection = 'accounts';
|
||||
|
||||
export const AuthSection = 'auth';
|
||||
|
||||
export const AuthenticationLibrarySection = 'authenticationLibrary';
|
||||
|
||||
export const AzureSection = 'azure';
|
||||
|
||||
export const AzureAccountProviderCredentials = 'azureAccountProviderCredentials';
|
||||
|
||||
export const CloudSection = 'cloud';
|
||||
|
||||
export const ClearTokenCacheCommand = 'clearTokenCache';
|
||||
|
||||
export const ConfigSection = 'config';
|
||||
|
||||
export const AccountsClearTokenCacheCommand = AccountsSection + '.' + ClearTokenCacheCommand;
|
||||
|
||||
export const AccountsAzureAuthSection = AccountsSection + '.' + AzureSection + '.' + AuthSection;
|
||||
|
||||
export const AccountsAzureCloudSection = AccountsSection + '.' + AzureSection + '.' + CloudSection;
|
||||
|
||||
export const AzureAuthenticationLibrarySection = AzureSection + '.' + AuthenticationLibrarySection;
|
||||
|
||||
export const EnableArcFeaturesSection = 'enableArcFeatures';
|
||||
|
||||
export const ServiceName = 'azuredatastudio';
|
||||
|
||||
export const TenantSection = 'tenant';
|
||||
|
||||
export const AzureTenantConfigSection = AzureSection + '.' + TenantSection + '.' + ConfigSection;
|
||||
|
||||
export const NoSystemKeyChainSection = 'noSystemKeychain';
|
||||
|
||||
/** MSAL Account version */
|
||||
export const AccountVersion = '2.0';
|
||||
|
||||
export const Bearer = 'Bearer';
|
||||
|
||||
/**
|
||||
* Use SHA-256 algorithm
|
||||
*/
|
||||
export const S256_CODE_CHALLENGE_METHOD = 'S256';
|
||||
|
||||
export const SELECT_ACCOUNT = 'select_account';
|
||||
|
||||
export const ConfigFilePath = './cache.json'
|
||||
|
||||
export const Saw = 'saw';
|
||||
|
||||
export const ViewType = 'view';
|
||||
|
||||
export const HomeCategory = 'Home';
|
||||
|
||||
export const dataGridProviderId = 'azure-resources';
|
||||
|
||||
export const AzureTokenFolderName = 'Azure Accounts';
|
||||
|
||||
export const DefaultAuthLibrary = 'ADAL';
|
||||
|
||||
export enum BuiltInCommands {
|
||||
SetContext = 'setContext'
|
||||
}
|
||||
|
||||
/**
|
||||
* AAD Auth library as selected.
|
||||
*/
|
||||
export enum AuthLibrary {
|
||||
MSAL = 'MSAL',
|
||||
ADAL = 'ADAL'
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication type as selected.
|
||||
*/
|
||||
export enum AuthType {
|
||||
DeviceCode = 'deviceCode',
|
||||
CodeGrant = 'codeGrant'
|
||||
}
|
||||
|
||||
/**
|
||||
* Account issuer as received from access token
|
||||
*/
|
||||
export enum AccountIssuer {
|
||||
Corp = 'corp',
|
||||
Msft = 'msft',
|
||||
}
|
||||
|
||||
/**
|
||||
* Azure Account type as received from access token
|
||||
*/
|
||||
export enum AccountType {
|
||||
WorkSchool = 'work_school',
|
||||
Microsoft = 'microsoft',
|
||||
}
|
||||
|
||||
export enum Platform {
|
||||
Windows = 'win32',
|
||||
Mac = 'darwin',
|
||||
Linux = 'linux'
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ import * as azurecore from 'azurecore';
|
||||
import * as azureResourceUtils from './azureResource/utils';
|
||||
import * as utils from './utils';
|
||||
import * as loc from './localizedConstants';
|
||||
import * as constants from './constants';
|
||||
import * as Constants from './constants';
|
||||
import { AzureResourceGroupService } from './azureResource/providers/resourceGroup/resourceGroupService';
|
||||
import { Logger } from './utils/Logger';
|
||||
import { ConnectionDialogTreeProvider } from './azureResource/tree/connectionDialogTreeProvider';
|
||||
@@ -58,15 +58,15 @@ let extensionContext: vscode.ExtensionContext;
|
||||
function getAppDataPath() {
|
||||
let platform = process.platform;
|
||||
switch (platform) {
|
||||
case 'win32': return process.env['APPDATA'] || path.join(process.env['USERPROFILE']!, 'AppData', 'Roaming');
|
||||
case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support');
|
||||
case 'linux': return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
|
||||
case Constants.Platform.Windows: return process.env['APPDATA'] || path.join(process.env['USERPROFILE']!, 'AppData', 'Roaming');
|
||||
case Constants.Platform.Mac: return path.join(os.homedir(), 'Library', 'Application Support');
|
||||
case Constants.Platform.Linux: return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
|
||||
default: throw new Error('Platform not supported');
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultLogLocation() {
|
||||
return path.join(getAppDataPath(), 'azuredatastudio');
|
||||
return path.join(getAppDataPath(), Constants.ServiceName);
|
||||
}
|
||||
|
||||
function pushDisposable(disposable: vscode.Disposable): void {
|
||||
@@ -85,24 +85,27 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
|
||||
}
|
||||
|
||||
// TODO: Since Code Grant auth doesnt work in web mode, enabling Device code auth by default for web mode. We can remove this once we have that working in web mode.
|
||||
const config = vscode.workspace.getConfiguration('accounts.azure.auth');
|
||||
const config = vscode.workspace.getConfiguration(Constants.AccountsAzureAuthSection);
|
||||
if (vscode.env.uiKind === vscode.UIKind.Web) {
|
||||
await config.update('deviceCode', true, vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
|
||||
const authLibrary: string = vscode.workspace.getConfiguration(Constants.AzureSection).get(Constants.AuthenticationLibrarySection)
|
||||
?? Constants.DefaultAuthLibrary;
|
||||
|
||||
updatePiiLoggingLevel();
|
||||
|
||||
// Create the provider service and activate
|
||||
initAzureAccountProvider(extensionContext, storagePath).catch((err) => console.log(err));
|
||||
initAzureAccountProvider(extensionContext, storagePath, authLibrary!).catch((err) => console.log(err));
|
||||
|
||||
registerAzureServices(appContext);
|
||||
const azureResourceTree = new AzureResourceTreeProvider(appContext);
|
||||
const connectionDialogTree = new ConnectionDialogTreeProvider(appContext);
|
||||
const azureResourceTree = new AzureResourceTreeProvider(appContext, authLibrary);
|
||||
const connectionDialogTree = new ConnectionDialogTreeProvider(appContext, authLibrary);
|
||||
pushDisposable(vscode.window.registerTreeDataProvider('azureResourceExplorer', azureResourceTree));
|
||||
pushDisposable(vscode.window.registerTreeDataProvider('connectionDialog/azureResourceExplorer', connectionDialogTree));
|
||||
pushDisposable(vscode.workspace.onDidChangeConfiguration(e => onDidChangeConfiguration(e)));
|
||||
registerAzureResourceCommands(appContext, azureResourceTree, connectionDialogTree);
|
||||
azdata.dataprotocol.registerDataGridProvider(new AzureDataGridProvider(appContext));
|
||||
registerAzureResourceCommands(appContext, azureResourceTree, connectionDialogTree, authLibrary);
|
||||
azdata.dataprotocol.registerDataGridProvider(new AzureDataGridProvider(appContext, authLibrary));
|
||||
vscode.commands.registerCommand('azure.dataGrid.openInAzurePortal', async (item: azdata.DataGridItem) => {
|
||||
const portalEndpoint = item.portalEndpoint;
|
||||
const subscriptionId = item.subscriptionId;
|
||||
@@ -130,7 +133,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
|
||||
return azureResourceUtils.getLocations(appContext, account, subscription, ignoreErrors);
|
||||
},
|
||||
provideResources(): azurecore.azureResource.IAzureResourceProvider[] {
|
||||
const arcFeaturedEnabled = vscode.workspace.getConfiguration(constants.extensionConfigSectionName).get('enableArcFeatures');
|
||||
const arcFeaturedEnabled = vscode.workspace.getConfiguration(Constants.AzureSection).get(Constants.EnableArcFeaturesSection);
|
||||
const providers: azurecore.azureResource.IAzureResourceProvider[] = [
|
||||
new KustoProvider(new KustoResourceService(), extensionContext),
|
||||
new AzureMonitorProvider(new AzureMonitorResourceService(), extensionContext),
|
||||
@@ -233,7 +236,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
|
||||
// Create the folder for storing the token caches
|
||||
async function findOrMakeStoragePath() {
|
||||
let defaultLogLocation = getDefaultLogLocation();
|
||||
let storagePath = path.join(defaultLogLocation, constants.AzureTokenFolderName);
|
||||
let storagePath = path.join(defaultLogLocation, Constants.AzureTokenFolderName);
|
||||
|
||||
try {
|
||||
await fs.mkdir(defaultLogLocation, { recursive: true });
|
||||
@@ -258,9 +261,9 @@ async function findOrMakeStoragePath() {
|
||||
return storagePath;
|
||||
}
|
||||
|
||||
async function initAzureAccountProvider(extensionContext: vscode.ExtensionContext, storagePath: string): Promise<void> {
|
||||
async function initAzureAccountProvider(extensionContext: vscode.ExtensionContext, storagePath: string, authLibrary: string): Promise<void> {
|
||||
try {
|
||||
const accountProviderService = new AzureAccountProviderService(extensionContext, storagePath);
|
||||
const accountProviderService = new AzureAccountProviderService(extensionContext, storagePath, authLibrary);
|
||||
extensionContext.subscriptions.push(accountProviderService);
|
||||
await accountProviderService.activate();
|
||||
} catch (err) {
|
||||
@@ -281,10 +284,27 @@ async function onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent): Pro
|
||||
if (e.affectsConfiguration('azure.piiLogging')) {
|
||||
updatePiiLoggingLevel();
|
||||
}
|
||||
if (e.affectsConfiguration('azure.authenticationLibrary')) {
|
||||
await displayReloadAds();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePiiLoggingLevel(): void {
|
||||
const piiLogging: boolean = vscode.workspace.getConfiguration(constants.extensionConfigSectionName).get('piiLogging', false);
|
||||
const piiLogging: boolean = vscode.workspace.getConfiguration(Constants.AzureSection).get('piiLogging', false);
|
||||
Logger.piiLogging = piiLogging;
|
||||
}
|
||||
|
||||
// Display notification with button to reload
|
||||
// return true if button clicked
|
||||
// return false if button not clicked
|
||||
async function displayReloadAds(): Promise<boolean> {
|
||||
const result = await vscode.window.showInformationMessage(loc.reloadPrompt, loc.reloadChoice);
|
||||
if (result === loc.reloadChoice) {
|
||||
await vscode.commands.executeCommand('workbench.action.reloadWindow');
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,9 @@ export const location = localize('azurecore.location', "Location");
|
||||
export const subscription = localize('azurecore.subscription', "Subscription");
|
||||
export const typeIcon = localize('azurecore.typeIcon', "Type Icon");
|
||||
|
||||
export const reloadPrompt = localize('azurecore.reloadPrompt', "Authentication Library has changed, please reload Azure Data Studio.");
|
||||
export const reloadChoice = localize('azurecore.reloadChoice', "Reload Azure Data Studio");
|
||||
|
||||
// Azure Resource Types
|
||||
export const sqlServer = localize('azurecore.sqlServer', "SQL server");
|
||||
export const sqlDatabase = localize('azurecore.sqlDatabase', "SQL database");
|
||||
|
||||
@@ -13,7 +13,6 @@ import providerSettings from '../../../account-provider/providerSettings';
|
||||
import { AzureResource } from 'azdata';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
|
||||
let azureAuthCodeGrant: TypeMoq.IMock<AzureAuthCodeGrant>;
|
||||
// let azureDeviceCode: TypeMoq.IMock<AzureDeviceCode>;
|
||||
|
||||
@@ -52,9 +51,21 @@ describe('Azure Authentication', function () {
|
||||
|
||||
mockAccount = {
|
||||
isStale: false,
|
||||
displayInfo: {
|
||||
contextualDisplayName: 'test',
|
||||
accountType: 'test',
|
||||
displayName: 'test',
|
||||
userId: 'test'
|
||||
},
|
||||
key: {
|
||||
providerId: 'test',
|
||||
accountId: 'test'
|
||||
},
|
||||
properties: {
|
||||
owningTenant: mockTenant,
|
||||
tenants: [mockTenant]
|
||||
tenants: [mockTenant],
|
||||
providerSettings: provider,
|
||||
isMsAccount: true
|
||||
}
|
||||
} as AzureAccount;
|
||||
|
||||
@@ -68,7 +79,7 @@ describe('Azure Authentication', function () {
|
||||
|
||||
it('accountHydration should yield a valid account', async function () {
|
||||
|
||||
azureAuthCodeGrant.setup(x => x.getTenants(mockToken)).returns((): Promise<Tenant[]> => {
|
||||
azureAuthCodeGrant.setup(x => x.getTenantsAdal(mockToken)).returns((): Promise<Tenant[]> => {
|
||||
return Promise.resolve([
|
||||
mockTenant
|
||||
]);
|
||||
@@ -83,30 +94,30 @@ describe('Azure Authentication', function () {
|
||||
describe('getAccountSecurityToken', function () {
|
||||
it('should be undefined on stale account', async function () {
|
||||
mockAccount.isStale = true;
|
||||
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, TypeMoq.It.isAny(), TypeMoq.It.isAny());
|
||||
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, TypeMoq.It.isAny(), TypeMoq.It.isAny());
|
||||
should(securityToken).be.undefined();
|
||||
});
|
||||
it('dont find correct resources', async function () {
|
||||
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, TypeMoq.It.isAny(), -1);
|
||||
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, TypeMoq.It.isAny(), -1);
|
||||
should(securityToken).be.undefined();
|
||||
});
|
||||
it('incorrect tenant', async function () {
|
||||
await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, 'invalid_tenant', AzureResource.MicrosoftResourceManagement).should.be.rejected();
|
||||
await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, 'invalid_tenant', AzureResource.MicrosoftResourceManagement).should.be.rejected();
|
||||
});
|
||||
|
||||
it('token recieved for ossRdbmns resource', async function () {
|
||||
azureAuthCodeGrant.setup(x => x.getTenants(mockToken)).returns(() => {
|
||||
azureAuthCodeGrant.setup(x => x.getTenantsAdal(mockToken)).returns(() => {
|
||||
return Promise.resolve([
|
||||
mockTenant
|
||||
]);
|
||||
});
|
||||
azureAuthCodeGrant.setup(x => x.getTokenHelper(mockTenant, provider.settings.ossRdbmsResource!, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
|
||||
azureAuthCodeGrant.setup(x => x.getTokenHelperAdal(mockTenant, provider.settings.ossRdbmsResource!, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
|
||||
return Promise.resolve({
|
||||
accessToken: mockAccessToken
|
||||
} as OAuthTokenResponse);
|
||||
});
|
||||
|
||||
azureAuthCodeGrant.setup(x => x.refreshToken(mockTenant, provider.settings.ossRdbmsResource!, mockRefreshToken)).returns((): Promise<OAuthTokenResponse> => {
|
||||
azureAuthCodeGrant.setup(x => x.refreshTokenAdal(mockTenant, provider.settings.ossRdbmsResource!, mockRefreshToken)).returns((): Promise<OAuthTokenResponse> => {
|
||||
const mockToken: AccessToken = JSON.parse(JSON.stringify(mockAccessToken));
|
||||
delete (mockToken as any).invalidData;
|
||||
return Promise.resolve({
|
||||
@@ -114,7 +125,7 @@ describe('Azure Authentication', function () {
|
||||
} as OAuthTokenResponse);
|
||||
});
|
||||
|
||||
azureAuthCodeGrant.setup(x => x.getSavedToken(mockTenant, provider.settings.ossRdbmsResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||
azureAuthCodeGrant.setup(x => x.getSavedTokenAdal(mockTenant, provider.settings.ossRdbmsResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||
return Promise.resolve({
|
||||
accessToken: mockAccessToken,
|
||||
refreshToken: mockRefreshToken,
|
||||
@@ -122,21 +133,21 @@ describe('Azure Authentication', function () {
|
||||
});
|
||||
});
|
||||
|
||||
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.OssRdbms);
|
||||
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, mockTenant.id, AzureResource.OssRdbms);
|
||||
should(securityToken?.token).be.equal(mockAccessToken.token, 'Token are not similar');
|
||||
|
||||
});
|
||||
|
||||
it('saved token exists and can be reused', async function () {
|
||||
delete (mockAccessToken as any).tokenType;
|
||||
azureAuthCodeGrant.setup(x => x.getSavedToken(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||
azureAuthCodeGrant.setup(x => x.getSavedTokenAdal(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||
return Promise.resolve({
|
||||
accessToken: mockAccessToken,
|
||||
refreshToken: mockRefreshToken,
|
||||
expiresOn: `${(new Date().getTime() / 1000) + (10 * 60)}`
|
||||
});
|
||||
});
|
||||
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
|
||||
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
|
||||
|
||||
should(securityToken?.tokenType).be.equal('Bearer', 'tokenType should be bearer on a successful getSecurityToken from cache');
|
||||
});
|
||||
@@ -145,47 +156,47 @@ describe('Azure Authentication', function () {
|
||||
it('saved token had invalid expiration', async function () {
|
||||
delete (mockAccessToken as any).tokenType;
|
||||
(mockAccessToken as any).invalidData = 'this should not exist on response';
|
||||
azureAuthCodeGrant.setup(x => x.getSavedToken(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||
azureAuthCodeGrant.setup(x => x.getSavedTokenAdal(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||
return Promise.resolve({
|
||||
accessToken: mockAccessToken,
|
||||
refreshToken: mockRefreshToken,
|
||||
expiresOn: 'invalid'
|
||||
});
|
||||
});
|
||||
azureAuthCodeGrant.setup(x => x.refreshToken(mockTenant, provider.settings.microsoftResource!, mockRefreshToken)).returns((): Promise<OAuthTokenResponse> => {
|
||||
azureAuthCodeGrant.setup(x => x.refreshTokenAdal(mockTenant, provider.settings.microsoftResource!, mockRefreshToken)).returns((): Promise<OAuthTokenResponse> => {
|
||||
const mockToken: AccessToken = JSON.parse(JSON.stringify(mockAccessToken));
|
||||
delete (mockToken as any).invalidData;
|
||||
return Promise.resolve({
|
||||
accessToken: mockToken
|
||||
} as OAuthTokenResponse);
|
||||
});
|
||||
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
|
||||
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
|
||||
|
||||
should((securityToken as any).invalidData).be.undefined(); // Ensure its a new one
|
||||
should(securityToken?.tokenType).be.equal('Bearer', 'tokenType should be bearer on a successful getSecurityToken from cache');
|
||||
|
||||
azureAuthCodeGrant.verify(x => x.refreshToken(mockTenant, provider.settings.microsoftResource!, mockRefreshToken), TypeMoq.Times.once());
|
||||
azureAuthCodeGrant.verify(x => x.refreshTokenAdal(mockTenant, provider.settings.microsoftResource!, mockRefreshToken), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
describe('no saved token', function () {
|
||||
it('no base token', async function () {
|
||||
azureAuthCodeGrant.setup(x => x.getSavedToken(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string } | undefined> => {
|
||||
azureAuthCodeGrant.setup(x => x.getSavedTokenAdal(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string } | undefined> => {
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
azureAuthCodeGrant.setup(x => x.getSavedToken(azureAuthCodeGrant.object.commonTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string } | undefined> => {
|
||||
azureAuthCodeGrant.setup(x => x.getSavedTokenAdal(azureAuthCodeGrant.object.commonTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string } | undefined> => {
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement).should.be.rejected();
|
||||
await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement).should.be.rejected();
|
||||
});
|
||||
|
||||
it('base token exists', async function () {
|
||||
azureAuthCodeGrant.setup(x => x.getSavedToken(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string } | undefined> => {
|
||||
azureAuthCodeGrant.setup(x => x.getSavedTokenAdal(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string } | undefined> => {
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
azureAuthCodeGrant.setup(x => x.getSavedToken(azureAuthCodeGrant.object.commonTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||
azureAuthCodeGrant.setup(x => x.getSavedTokenAdal(azureAuthCodeGrant.object.commonTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||
return Promise.resolve({
|
||||
accessToken: mockAccessToken,
|
||||
refreshToken: mockRefreshToken,
|
||||
@@ -194,13 +205,13 @@ describe('Azure Authentication', function () {
|
||||
});
|
||||
delete (mockAccessToken as any).tokenType;
|
||||
|
||||
azureAuthCodeGrant.setup(x => x.refreshToken(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
|
||||
azureAuthCodeGrant.setup(x => x.refreshTokenAdal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
|
||||
return Promise.resolve({
|
||||
accessToken: mockAccessToken
|
||||
} as OAuthTokenResponse);
|
||||
});
|
||||
|
||||
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
|
||||
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
|
||||
should(securityToken?.tokenType).be.equal('Bearer', 'tokenType should be bearer on a successful getSecurityToken from cache');
|
||||
});
|
||||
});
|
||||
@@ -218,16 +229,16 @@ describe('Azure Authentication', function () {
|
||||
} as AxiosResponse<any>);
|
||||
});
|
||||
|
||||
azureAuthCodeGrant.setup(x => x.handleInteractionRequired(mockTenant, provider.settings.microsoftResource!)).returns(() => {
|
||||
azureAuthCodeGrant.setup(x => x.handleInteractionRequiredAdal(mockTenant, provider.settings.microsoftResource!)).returns(() => {
|
||||
return Promise.resolve({
|
||||
accessToken: mockAccessToken
|
||||
} as OAuthTokenResponse);
|
||||
});
|
||||
|
||||
|
||||
const result = await azureAuthCodeGrant.object.getToken(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData);
|
||||
const result = await azureAuthCodeGrant.object.getTokenAdal(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData);
|
||||
|
||||
azureAuthCodeGrant.verify(x => x.handleInteractionRequired(mockTenant, provider.settings.microsoftResource!), TypeMoq.Times.once());
|
||||
azureAuthCodeGrant.verify(x => x.handleInteractionRequiredAdal(mockTenant, provider.settings.microsoftResource!), TypeMoq.Times.once());
|
||||
|
||||
should(result?.accessToken).be.deepEqual(mockAccessToken);
|
||||
});
|
||||
@@ -241,7 +252,7 @@ describe('Azure Authentication', function () {
|
||||
} as AxiosResponse<any>);
|
||||
});
|
||||
|
||||
await azureAuthCodeGrant.object.getToken(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData).should.be.rejected();
|
||||
await azureAuthCodeGrant.object.getTokenAdal(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData).should.be.rejected();
|
||||
});
|
||||
|
||||
it('calls getTokenHelper', async function () {
|
||||
@@ -255,16 +266,16 @@ describe('Azure Authentication', function () {
|
||||
} as AxiosResponse<any>);
|
||||
});
|
||||
|
||||
azureAuthCodeGrant.setup(x => x.getTokenHelper(mockTenant, provider.settings.microsoftResource!, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
|
||||
azureAuthCodeGrant.setup(x => x.getTokenHelperAdal(mockTenant, provider.settings.microsoftResource!, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
|
||||
return Promise.resolve({
|
||||
accessToken: mockAccessToken
|
||||
} as OAuthTokenResponse);
|
||||
});
|
||||
|
||||
|
||||
const result = await azureAuthCodeGrant.object.getToken(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData);
|
||||
const result = await azureAuthCodeGrant.object.getTokenAdal(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData);
|
||||
|
||||
azureAuthCodeGrant.verify(x => x.getTokenHelper(mockTenant, provider.settings.microsoftResource!, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
azureAuthCodeGrant.verify(x => x.getTokenHelperAdal(mockTenant, provider.settings.microsoftResource!, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
|
||||
should(result?.accessToken).be.deepEqual(mockAccessToken);
|
||||
});
|
||||
|
||||
@@ -28,9 +28,11 @@ import allSettings from '../../../account-provider/providerSettings';
|
||||
// Mock services
|
||||
let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
|
||||
let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>;
|
||||
let mockSubscriptionService: TypeMoq.IMock<IAzureResourceSubscriptionService>;
|
||||
let mockSubscriptionServiceADAL: TypeMoq.IMock<IAzureResourceSubscriptionService>;
|
||||
let mockSubscriptionServiceMSAL: TypeMoq.IMock<IAzureResourceSubscriptionService>;
|
||||
let mockSubscriptionFilterService: TypeMoq.IMock<IAzureResourceSubscriptionFilterService>;
|
||||
let mockAppContext: AppContext;
|
||||
let mockAppContextADAL: AppContext;
|
||||
let mockAppContextMSAL: AppContext;
|
||||
let mockTreeChangeHandler: TypeMoq.IMock<IAzureResourceTreeChangeHandler>;
|
||||
|
||||
// Mock test data
|
||||
@@ -95,18 +97,25 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
|
||||
beforeEach(() => {
|
||||
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
|
||||
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, undefined)).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionServiceADAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionServiceMSAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionServiceMSAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
|
||||
|
||||
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
|
||||
|
||||
mockSubscriptionCache = [];
|
||||
|
||||
mockAppContext = new AppContext(mockExtensionContext.object);
|
||||
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
|
||||
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
mockAppContextADAL = new AppContext(mockExtensionContext.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
|
||||
mockAppContextMSAL = new AppContext(mockExtensionContext.object);
|
||||
mockAppContextMSAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContextMSAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceMSAL.object);
|
||||
mockAppContextMSAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
|
||||
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
|
||||
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
|
||||
@@ -120,8 +129,8 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Should be correct when created.', async function (): Promise<void> {
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||
it('Should be correct when created for ADAL.', async function (): Promise<void> {
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
|
||||
|
||||
const accountTreeNodeId = `account_${mockAccount.key.accountId}`;
|
||||
|
||||
@@ -140,14 +149,34 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
|
||||
should(nodeInfo.iconType).equal(AzureResourceItemType.account);
|
||||
});
|
||||
|
||||
it('Should be correct when there are subscriptions listed.', async function (): Promise<void> {
|
||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
|
||||
it('Should be correct when created for MSAL.', async function (): Promise<void> {
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextMSAL, mockTreeChangeHandler.object);
|
||||
|
||||
const accountTreeNodeId = `account_${mockAccount.key.accountId}`;
|
||||
|
||||
should(accountTreeNode.nodePathValue).equal(accountTreeNodeId);
|
||||
|
||||
const treeItem = await accountTreeNode.getTreeItem();
|
||||
should(treeItem.id).equal(accountTreeNodeId);
|
||||
should(treeItem.label).equal(mockAccount.displayInfo.displayName);
|
||||
should(treeItem.contextValue).equal(AzureResourceItemType.account);
|
||||
should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed);
|
||||
|
||||
const nodeInfo = accountTreeNode.getNodeInfo();
|
||||
should(nodeInfo.label).equal(mockAccount.displayInfo.displayName);
|
||||
should(nodeInfo.isLeaf).false();
|
||||
should(nodeInfo.nodeType).equal(AzureResourceItemType.account);
|
||||
should(nodeInfo.iconType).equal(AzureResourceItemType.account);
|
||||
});
|
||||
|
||||
it('Should be correct when there are subscriptions listed for ADAL.', async function (): Promise<void> {
|
||||
mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
|
||||
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
|
||||
|
||||
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
|
||||
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
|
||||
|
||||
const subscriptionNodes = await accountTreeNode.getChildren();
|
||||
|
||||
@@ -161,13 +190,34 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
|
||||
should(nodeInfo.label).equal(accountTreeNodeLabel);
|
||||
});
|
||||
|
||||
it('Should only show subscriptions with valid tokens.', async function (): Promise<void> {
|
||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
|
||||
it('Should be correct when there are subscriptions listed for MSAL.', async function (): Promise<void> {
|
||||
mockSubscriptionServiceMSAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
|
||||
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
|
||||
|
||||
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
|
||||
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextMSAL, mockTreeChangeHandler.object);
|
||||
|
||||
const subscriptionNodes = await accountTreeNode.getChildren();
|
||||
|
||||
should(subscriptionNodes).Array();
|
||||
should(subscriptionNodes.length).equal(mockSubscriptions.length);
|
||||
|
||||
const treeItem = await accountTreeNode.getTreeItem();
|
||||
should(treeItem.label).equal(accountTreeNodeLabel);
|
||||
|
||||
const nodeInfo = accountTreeNode.getNodeInfo();
|
||||
should(nodeInfo.label).equal(accountTreeNodeLabel);
|
||||
});
|
||||
|
||||
it('Should only show subscriptions with valid tokens for ADAL.', async function (): Promise<void> {
|
||||
mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
|
||||
sinon.stub(azdata.accounts, 'getAccountSecurityToken').onFirstCall().resolves(mockToken);
|
||||
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
|
||||
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
|
||||
|
||||
const subscriptionNodes = await accountTreeNode.getChildren();
|
||||
|
||||
@@ -181,13 +231,53 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
|
||||
should(nodeInfo.label).equal(accountTreeNodeLabel);
|
||||
});
|
||||
|
||||
it('Should be correct when there are subscriptions filtered.', async function (): Promise<void> {
|
||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
|
||||
it('Should only show subscriptions with valid tokens for MSAL.', async function (): Promise<void> {
|
||||
mockSubscriptionServiceMSAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
|
||||
sinon.stub(azdata.accounts, 'getAccountSecurityToken').onFirstCall().resolves(mockToken);
|
||||
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
|
||||
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextMSAL, mockTreeChangeHandler.object);
|
||||
|
||||
const subscriptionNodes = await accountTreeNode.getChildren();
|
||||
|
||||
should(subscriptionNodes).Array();
|
||||
should(subscriptionNodes.length).equal(1);
|
||||
|
||||
const treeItem = await accountTreeNode.getTreeItem();
|
||||
should(treeItem.label).equal(accountTreeNodeLabel);
|
||||
|
||||
const nodeInfo = accountTreeNode.getNodeInfo();
|
||||
should(nodeInfo.label).equal(accountTreeNodeLabel);
|
||||
});
|
||||
|
||||
it('Should be correct when there are subscriptions filtered for ADAL.', async function (): Promise<void> {
|
||||
mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
|
||||
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
|
||||
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
|
||||
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
|
||||
|
||||
const subscriptionNodes = await accountTreeNode.getChildren();
|
||||
|
||||
should(subscriptionNodes).Array();
|
||||
should(subscriptionNodes.length).equal(mockFilteredSubscriptions.length);
|
||||
|
||||
const treeItem = await accountTreeNode.getTreeItem();
|
||||
should(treeItem.label).equal(accountTreeNodeLabel);
|
||||
|
||||
const nodeInfo = accountTreeNode.getNodeInfo();
|
||||
should(nodeInfo.label).equal(accountTreeNodeLabel);
|
||||
});
|
||||
|
||||
it('Should be correct when there are subscriptions filtered for MSAL.', async function (): Promise<void> {
|
||||
mockSubscriptionServiceMSAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
|
||||
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
|
||||
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
|
||||
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextMSAL, mockTreeChangeHandler.object);
|
||||
|
||||
const subscriptionNodes = await accountTreeNode.getChildren();
|
||||
|
||||
@@ -206,17 +296,23 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
||||
beforeEach(() => {
|
||||
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
|
||||
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionServiceADAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionServiceMSAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
|
||||
|
||||
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
|
||||
|
||||
mockSubscriptionCache = [];
|
||||
|
||||
mockAppContext = new AppContext(mockExtensionContext.object);
|
||||
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
|
||||
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
mockAppContextADAL = new AppContext(mockExtensionContext.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
|
||||
mockAppContextMSAL = new AppContext(mockExtensionContext.object);
|
||||
mockAppContextMSAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContextMSAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceMSAL.object);
|
||||
mockAppContextMSAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
|
||||
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
|
||||
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
|
||||
@@ -231,15 +327,15 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Should load subscriptions from scratch and update cache when it is clearing cache.', async function (): Promise<void> {
|
||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
|
||||
it('Should load subscriptions from scratch and update cache when it is clearing cache for ADAL.', async function (): Promise<void> {
|
||||
mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
|
||||
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
|
||||
|
||||
const children = await accountTreeNode.getChildren();
|
||||
|
||||
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockSubscriptionServiceADAL.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(0));
|
||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once());
|
||||
@@ -265,16 +361,16 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
||||
});
|
||||
|
||||
it('Should load subscriptions from cache when it is not clearing cache.', async function (): Promise<void> {
|
||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
|
||||
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
|
||||
|
||||
await accountTreeNode.getChildren();
|
||||
const children = await accountTreeNode.getChildren();
|
||||
|
||||
|
||||
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockSubscriptionServiceADAL.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
|
||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
|
||||
@@ -286,9 +382,9 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
||||
});
|
||||
|
||||
it('Should handle when there is no subscriptions.', async function (): Promise<void> {
|
||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve([]));
|
||||
mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve([]));
|
||||
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
|
||||
|
||||
const children = await accountTreeNode.getChildren();
|
||||
|
||||
@@ -302,10 +398,10 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
||||
});
|
||||
|
||||
it('Should honor subscription filtering.', async function (): Promise<void> {
|
||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
|
||||
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
|
||||
|
||||
const children = await accountTreeNode.getChildren();
|
||||
|
||||
@@ -320,16 +416,16 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
||||
});
|
||||
|
||||
it('Should handle errors.', async function (): Promise<void> {
|
||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
|
||||
|
||||
const mockError = 'Test error';
|
||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => { throw new Error(mockError); });
|
||||
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
|
||||
|
||||
const children = await accountTreeNode.getChildren();
|
||||
|
||||
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockSubscriptionServiceADAL.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once());
|
||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never());
|
||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
@@ -346,17 +442,17 @@ describe('AzureResourceAccountTreeNode.clearCache', function (): void {
|
||||
beforeEach(() => {
|
||||
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
|
||||
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionServiceADAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
|
||||
|
||||
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
|
||||
|
||||
mockSubscriptionCache = [];
|
||||
|
||||
mockAppContext = new AppContext(mockExtensionContext.object);
|
||||
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
|
||||
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
mockAppContextADAL = new AppContext(mockExtensionContext.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
|
||||
sinon.stub(azdata.accounts, 'getAccountSecurityToken').returns(Promise.resolve(mockToken));
|
||||
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
|
||||
@@ -372,7 +468,7 @@ describe('AzureResourceAccountTreeNode.clearCache', function (): void {
|
||||
});
|
||||
|
||||
it('Should clear cache.', async function (): Promise<void> {
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
|
||||
accountTreeNode.clearCache();
|
||||
should(accountTreeNode.isClearingCache).true();
|
||||
});
|
||||
|
||||
@@ -26,10 +26,11 @@ let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
|
||||
let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>;
|
||||
|
||||
// Mock test data
|
||||
const mockAccount1: AzureAccount = {
|
||||
const mockAccountAdal1: AzureAccount = {
|
||||
key: {
|
||||
accountId: 'mock_account_1',
|
||||
providerId: 'mock_provider'
|
||||
providerId: 'mock_provider',
|
||||
authLibrary: 'ADAL'
|
||||
},
|
||||
displayInfo: {
|
||||
displayName: 'mock_account_1@test.com',
|
||||
@@ -40,7 +41,7 @@ const mockAccount1: AzureAccount = {
|
||||
properties: TypeMoq.Mock.ofType<AzureAccountProperties>().object,
|
||||
isStale: false
|
||||
};
|
||||
const mockAccount2: AzureAccount = {
|
||||
const mockAccountAdal2: AzureAccount = {
|
||||
key: {
|
||||
accountId: 'mock_account_2',
|
||||
providerId: 'mock_provider'
|
||||
@@ -54,7 +55,39 @@ const mockAccount2: AzureAccount = {
|
||||
properties: TypeMoq.Mock.ofType<AzureAccountProperties>().object,
|
||||
isStale: false
|
||||
};
|
||||
const mockAccounts = [mockAccount1, mockAccount2];
|
||||
const mockAccountsADAL = [mockAccountAdal1, mockAccountAdal2];
|
||||
|
||||
const mockAccountMsal1: AzureAccount = {
|
||||
key: {
|
||||
accountId: 'mock_account_1',
|
||||
providerId: 'mock_provider',
|
||||
authLibrary: 'MSAL'
|
||||
},
|
||||
displayInfo: {
|
||||
displayName: 'mock_account_1@test.com',
|
||||
accountType: 'Microsoft',
|
||||
contextualDisplayName: 'test',
|
||||
userId: 'test@email.com'
|
||||
},
|
||||
properties: TypeMoq.Mock.ofType<AzureAccountProperties>().object,
|
||||
isStale: false
|
||||
};
|
||||
const mockAccountMsal2: AzureAccount = {
|
||||
key: {
|
||||
accountId: 'mock_account_2',
|
||||
providerId: 'mock_provider',
|
||||
authLibrary: 'MSAL'
|
||||
},
|
||||
displayInfo: {
|
||||
displayName: 'mock_account_2@test.com',
|
||||
accountType: 'Microsoft',
|
||||
contextualDisplayName: 'test',
|
||||
userId: 'test@email.com'
|
||||
},
|
||||
properties: TypeMoq.Mock.ofType<AzureAccountProperties>().object,
|
||||
isStale: false
|
||||
};
|
||||
const mockAccountsMSAL = [mockAccountMsal1, mockAccountMsal2];
|
||||
|
||||
describe('AzureResourceTreeProvider.getChildren', function (): void {
|
||||
beforeEach(() => {
|
||||
@@ -68,35 +101,69 @@ describe('AzureResourceTreeProvider.getChildren', function (): void {
|
||||
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
|
||||
});
|
||||
|
||||
afterEach(function(): void {
|
||||
afterEach(function (): void {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Should load accounts.', async function (): Promise<void> {
|
||||
const getAllAccountsStub = sinon.stub(azdata.accounts, 'getAllAccounts').returns(Promise.resolve(mockAccounts));
|
||||
it('Should load accounts for ADAL', async function (): Promise<void> {
|
||||
const getAllAccountsStub = sinon.stub(azdata.accounts, 'getAllAccounts').returns(Promise.resolve(mockAccountsADAL));
|
||||
|
||||
const treeProvider = new AzureResourceTreeProvider(mockAppContext);
|
||||
const treeProvider = new AzureResourceTreeProvider(mockAppContext, 'ADAL');
|
||||
|
||||
await treeProvider.getChildren(undefined); // Load account promise
|
||||
const children = await treeProvider.getChildren(undefined); // Actual accounts
|
||||
|
||||
should(getAllAccountsStub.calledOnce).be.true('getAllAccounts should have been called exactly once');
|
||||
should(children).Array();
|
||||
should(children.length).equal(mockAccounts.length);
|
||||
should(children.length).equal(mockAccountsADAL.length);
|
||||
|
||||
for (let ix = 0; ix < mockAccounts.length; ix++) {
|
||||
for (let ix = 0; ix < mockAccountsADAL.length; ix++) {
|
||||
const child = children[ix];
|
||||
const account = mockAccounts[ix];
|
||||
const account = mockAccountsADAL[ix];
|
||||
|
||||
should(child).instanceof(AzureResourceAccountTreeNode);
|
||||
should(child.nodePathValue).equal(`account_${account.key.accountId}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('Should handle when there is no accounts.', async function (): Promise<void> {
|
||||
it('Should load accounts for MSAL', async function (): Promise<void> {
|
||||
const getAllAccountsStub = sinon.stub(azdata.accounts, 'getAllAccounts').returns(Promise.resolve(mockAccountsMSAL));
|
||||
|
||||
const treeProvider = new AzureResourceTreeProvider(mockAppContext, 'MSAL');
|
||||
|
||||
await treeProvider.getChildren(undefined); // Load account promise
|
||||
const children = await treeProvider.getChildren(undefined); // Actual accounts
|
||||
|
||||
should(getAllAccountsStub.calledOnce).be.true('getAllAccounts should have been called exactly once');
|
||||
should(children).Array();
|
||||
should(children.length).equal(mockAccountsMSAL.length);
|
||||
|
||||
for (let ix = 0; ix < mockAccountsMSAL.length; ix++) {
|
||||
const child = children[ix];
|
||||
const account = mockAccountsMSAL[ix];
|
||||
|
||||
should(child).instanceof(AzureResourceAccountTreeNode);
|
||||
should(child.nodePathValue).equal(`account_${account.key.accountId}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('Should handle when there is no accounts for ADAL', async function (): Promise<void> {
|
||||
sinon.stub(azdata.accounts, 'getAllAccounts').returns(Promise.resolve([]));
|
||||
|
||||
const treeProvider = new AzureResourceTreeProvider(mockAppContext);
|
||||
const treeProvider = new AzureResourceTreeProvider(mockAppContext, 'ADAL');
|
||||
treeProvider.isSystemInitialized = true;
|
||||
|
||||
const children = await treeProvider.getChildren(undefined);
|
||||
|
||||
should(children).Array();
|
||||
should(children.length).equal(1);
|
||||
should(children[0]).instanceof(AzureResourceAccountNotSignedInTreeNode);
|
||||
});
|
||||
|
||||
it('Should handle when there is no accounts for MSAL', async function (): Promise<void> {
|
||||
sinon.stub(azdata.accounts, 'getAllAccounts').returns(Promise.resolve([]));
|
||||
|
||||
const treeProvider = new AzureResourceTreeProvider(mockAppContext, 'MSAL');
|
||||
treeProvider.isSystemInitialized = true;
|
||||
|
||||
const children = await treeProvider.getChildren(undefined);
|
||||
|
||||
@@ -127,6 +127,29 @@
|
||||
uuid "^3.3.2"
|
||||
xml2js "^0.4.19"
|
||||
|
||||
"@azure/msal-common@^7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-7.6.0.tgz#b52e97ef540275f72611cff57937dfa0b34cdcca"
|
||||
integrity sha512-XqfbglUTVLdkHQ8F9UQJtKseRr3sSnr9ysboxtoswvaMVaEfvyLtMoHv9XdKUfOc0qKGzNgRFd9yRjIWVepl6Q==
|
||||
|
||||
"@azure/msal-node-extensions@^1.0.0-alpha.25":
|
||||
version "1.0.0-alpha.25"
|
||||
resolved "https://registry.yarnpkg.com/@azure/msal-node-extensions/-/msal-node-extensions-1.0.0-alpha.25.tgz#3259e1fd32b6107f61a402dce22caab53e81527c"
|
||||
integrity sha512-7pOUdE2OZO2omA1DBAJhVGHJo8laqw+x5JgV/XLzyogapduAzps3lbM/G3VV+VuEb0KG1QHkpaOF/6eftPssKw==
|
||||
dependencies:
|
||||
"@azure/msal-common" "^7.6.0"
|
||||
keytar "^7.8.0"
|
||||
node-addon-api "5.0.0"
|
||||
|
||||
"@azure/msal-node@^1.9.0":
|
||||
version "1.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-1.14.2.tgz#8f236a19efa506133d6c715047393146af182e3a"
|
||||
integrity sha512-t3whVhhLdZVVeDEtUPD2Wqfa8BDi3EDMnpWp8dbuRW0GhUpikBfs4AQU0Fe6P9zS87n9LpmUTLrIcPEEuzkvfA==
|
||||
dependencies:
|
||||
"@azure/msal-common" "^7.6.0"
|
||||
jsonwebtoken "^8.5.1"
|
||||
uuid "^8.3.0"
|
||||
|
||||
"@azure/storage-blob@^12.6.0":
|
||||
version "12.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@azure/storage-blob/-/storage-blob-12.6.0.tgz#9905d80e5f908a573cc65e1cb8302abc32818844"
|
||||
@@ -549,11 +572,25 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
||||
|
||||
base64-js@^1.3.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
||||
|
||||
bl@^4.0.3:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
|
||||
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
|
||||
dependencies:
|
||||
buffer "^5.5.0"
|
||||
inherits "^2.0.4"
|
||||
readable-stream "^3.4.0"
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
@@ -574,6 +611,19 @@ browser-stdout@1.3.1:
|
||||
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
|
||||
integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
|
||||
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
|
||||
|
||||
buffer@^5.5.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
||||
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
|
||||
dependencies:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
call-bind@^1.0.0, call-bind@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
|
||||
@@ -621,6 +671,11 @@ chokidar@3.3.0:
|
||||
optionalDependencies:
|
||||
fsevents "~2.1.1"
|
||||
|
||||
chownr@^1.1.1:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
|
||||
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
|
||||
|
||||
circular-json@^0.3.1:
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
|
||||
@@ -711,6 +766,18 @@ decamelize@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
|
||||
|
||||
decompress-response@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
|
||||
integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
|
||||
dependencies:
|
||||
mimic-response "^3.1.0"
|
||||
|
||||
deep-extend@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
||||
|
||||
default-require-extensions@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.0.tgz#e03f93aac9b2b6443fc52e5e4a37b3ad9ad8df96"
|
||||
@@ -731,6 +798,11 @@ delayed-stream@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
||||
|
||||
detect-libc@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
|
||||
integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
|
||||
|
||||
diff@3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
|
||||
@@ -741,11 +813,25 @@ diff@^4.0.2:
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
|
||||
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
emoji-regex@^7.0.1:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
|
||||
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
|
||||
|
||||
end-of-stream@^1.1.0, end-of-stream@^1.4.1:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
|
||||
dependencies:
|
||||
once "^1.4.0"
|
||||
|
||||
es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.5:
|
||||
version "1.20.1"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814"
|
||||
@@ -816,6 +902,11 @@ events@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
|
||||
|
||||
expand-template@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
|
||||
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
|
||||
|
||||
fill-range@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
||||
@@ -869,6 +960,11 @@ form-data@^4.0.0:
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fs-constants@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
@@ -926,6 +1022,11 @@ get-symbol-description@^1.0.0:
|
||||
call-bind "^1.0.2"
|
||||
get-intrinsic "^1.1.1"
|
||||
|
||||
github-from-package@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
|
||||
integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==
|
||||
|
||||
glob-parent@~5.1.0:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
@@ -1034,6 +1135,11 @@ https-proxy-agent@^2.2.4:
|
||||
agent-base "^4.3.0"
|
||||
debug "^3.1.0"
|
||||
|
||||
ieee754@^1.1.13:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
@@ -1042,11 +1148,16 @@ inflight@^1.0.4:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2:
|
||||
inherits@2, inherits@^2.0.3, inherits@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
ini@~1.3.0:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
|
||||
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||
|
||||
internal-slot@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
|
||||
@@ -1268,11 +1379,52 @@ json5@^2.1.2:
|
||||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
jsonwebtoken@^8.5.1:
|
||||
version "8.5.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
|
||||
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
|
||||
dependencies:
|
||||
jws "^3.2.2"
|
||||
lodash.includes "^4.3.0"
|
||||
lodash.isboolean "^3.0.3"
|
||||
lodash.isinteger "^4.0.4"
|
||||
lodash.isnumber "^3.0.3"
|
||||
lodash.isplainobject "^4.0.6"
|
||||
lodash.isstring "^4.0.1"
|
||||
lodash.once "^4.0.0"
|
||||
ms "^2.1.1"
|
||||
semver "^5.6.0"
|
||||
|
||||
just-extend@^4.0.2:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4"
|
||||
integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==
|
||||
|
||||
jwa@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
|
||||
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
|
||||
dependencies:
|
||||
buffer-equal-constant-time "1.0.1"
|
||||
ecdsa-sig-formatter "1.0.11"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
jws@^3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
|
||||
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
|
||||
dependencies:
|
||||
jwa "^1.4.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
keytar@^7.8.0:
|
||||
version "7.9.0"
|
||||
resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.9.0.tgz#4c6225708f51b50cbf77c5aae81721964c2918cb"
|
||||
integrity sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==
|
||||
dependencies:
|
||||
node-addon-api "^4.3.0"
|
||||
prebuild-install "^7.0.1"
|
||||
|
||||
locate-path@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
|
||||
@@ -1286,6 +1438,41 @@ lodash.get@^4.4.2:
|
||||
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
|
||||
|
||||
lodash.includes@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
|
||||
integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==
|
||||
|
||||
lodash.isboolean@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
|
||||
integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==
|
||||
|
||||
lodash.isinteger@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
|
||||
integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==
|
||||
|
||||
lodash.isnumber@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
|
||||
integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==
|
||||
|
||||
lodash.isplainobject@^4.0.6:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
|
||||
integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==
|
||||
|
||||
lodash.isstring@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
|
||||
integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==
|
||||
|
||||
lodash.once@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
||||
integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
|
||||
|
||||
lodash@^4.16.4, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.4:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
@@ -1298,6 +1485,13 @@ log-symbols@3.0.0:
|
||||
dependencies:
|
||||
chalk "^2.4.2"
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
make-dir@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
|
||||
@@ -1334,6 +1528,11 @@ mime-types@^2.1.12:
|
||||
dependencies:
|
||||
mime-db "1.43.0"
|
||||
|
||||
mimic-response@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
|
||||
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
|
||||
|
||||
minimatch@3.0.4, minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
@@ -1341,11 +1540,21 @@ minimatch@3.0.4, minimatch@^3.0.4:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.3:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
|
||||
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
|
||||
|
||||
minimist@^1.2.5:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
|
||||
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
|
||||
|
||||
mkdirp@0.5.5, mkdirp@~0.5.1:
|
||||
version "0.5.5"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
|
||||
@@ -1417,6 +1626,18 @@ ms@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
msal@^1.4.16:
|
||||
version "1.4.17"
|
||||
resolved "https://registry.yarnpkg.com/msal/-/msal-1.4.17.tgz#b78171c0471ede506eeaabc86343f8f4e2d01634"
|
||||
integrity sha512-RjHwP2cCIWQ9iUIk1SziUMb9+jj5mC4OqG2w16E5yig8jySi/TwiFvKlwcjNrPsndph0HtgCtbENnk5julf3yQ==
|
||||
dependencies:
|
||||
tslib "^1.9.3"
|
||||
|
||||
napi-build-utils@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
|
||||
integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
|
||||
|
||||
nise@^4.0.1:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.4.tgz#d73dea3e5731e6561992b8f570be9e363c4512dd"
|
||||
@@ -1428,6 +1649,23 @@ nise@^4.0.1:
|
||||
just-extend "^4.0.2"
|
||||
path-to-regexp "^1.7.0"
|
||||
|
||||
node-abi@^3.3.0:
|
||||
version "3.28.0"
|
||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.28.0.tgz#b0df8b317e1c4f2f323756c5fc8ffccc5bca4718"
|
||||
integrity sha512-fRlDb4I0eLcQeUvGq7IY3xHrSb0c9ummdvDSYWfT9+LKP+3jCKw/tKoqaM7r1BAoiAC6GtwyjaGnOz6B3OtF+A==
|
||||
dependencies:
|
||||
semver "^7.3.5"
|
||||
|
||||
node-addon-api@5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.0.0.tgz#7d7e6f9ef89043befdb20c1989c905ebde18c501"
|
||||
integrity sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==
|
||||
|
||||
node-addon-api@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
|
||||
integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
|
||||
|
||||
node-environment-flags@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088"
|
||||
@@ -1487,10 +1725,10 @@ object.getownpropertydescriptors@^2.0.3:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.19.1"
|
||||
|
||||
once@^1.3.0:
|
||||
once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
@@ -1550,6 +1788,24 @@ postinstall-build@^5.0.1:
|
||||
resolved "https://registry.yarnpkg.com/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7"
|
||||
integrity sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg==
|
||||
|
||||
prebuild-install@^7.0.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45"
|
||||
integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==
|
||||
dependencies:
|
||||
detect-libc "^2.0.0"
|
||||
expand-template "^2.0.3"
|
||||
github-from-package "0.0.0"
|
||||
minimist "^1.2.3"
|
||||
mkdirp-classic "^0.5.3"
|
||||
napi-build-utils "^1.0.1"
|
||||
node-abi "^3.3.0"
|
||||
pump "^3.0.0"
|
||||
rc "^1.2.7"
|
||||
simple-get "^4.0.0"
|
||||
tar-fs "^2.0.0"
|
||||
tunnel-agent "^0.6.0"
|
||||
|
||||
process@^0.11.10:
|
||||
version "0.11.10"
|
||||
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||
@@ -1560,6 +1816,14 @@ psl@^1.1.28, psl@^1.1.33:
|
||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
|
||||
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
|
||||
|
||||
pump@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
|
||||
dependencies:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
punycode@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
@@ -1570,6 +1834,25 @@ qs@^6.9.1:
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e"
|
||||
integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==
|
||||
|
||||
rc@^1.2.7:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
|
||||
dependencies:
|
||||
deep-extend "^0.6.0"
|
||||
ini "~1.3.0"
|
||||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
readable-stream@^3.1.1, readable-stream@^3.4.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
||||
dependencies:
|
||||
inherits "^2.0.3"
|
||||
string_decoder "^1.1.1"
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
readdirp@~3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839"
|
||||
@@ -1610,6 +1893,11 @@ rimraf@^2.6.3:
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
@@ -1630,6 +1918,13 @@ semver@^6.0.0, semver@^6.3.0:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
semver@^7.3.5:
|
||||
version "7.3.8"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
|
||||
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
set-blocking@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
@@ -1688,6 +1983,20 @@ side-channel@^1.0.4:
|
||||
get-intrinsic "^1.0.2"
|
||||
object-inspect "^1.9.0"
|
||||
|
||||
simple-concat@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
|
||||
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
|
||||
|
||||
simple-get@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
|
||||
integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
|
||||
dependencies:
|
||||
decompress-response "^6.0.0"
|
||||
once "^1.3.1"
|
||||
simple-concat "^1.0.0"
|
||||
|
||||
sinon@^9.0.2:
|
||||
version "9.0.2"
|
||||
resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.0.2.tgz#b9017e24633f4b1c98dfb6e784a5f0509f5fd85d"
|
||||
@@ -1751,6 +2060,13 @@ string.prototype.trimstart@^1.0.5:
|
||||
define-properties "^1.1.4"
|
||||
es-abstract "^1.19.5"
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
|
||||
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
strip-ansi@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
|
||||
@@ -1770,10 +2086,10 @@ strip-bom@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
|
||||
integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
|
||||
|
||||
strip-json-comments@2.0.1:
|
||||
strip-json-comments@2.0.1, strip-json-comments@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
||||
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
|
||||
|
||||
supports-color@6.0.0:
|
||||
version "6.0.0"
|
||||
@@ -1796,6 +2112,27 @@ supports-color@^7.1.0:
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
tar-fs@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
|
||||
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
|
||||
dependencies:
|
||||
chownr "^1.1.1"
|
||||
mkdirp-classic "^0.5.2"
|
||||
pump "^3.0.0"
|
||||
tar-stream "^2.1.4"
|
||||
|
||||
tar-stream@^2.1.4:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
|
||||
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
|
||||
dependencies:
|
||||
bl "^4.0.3"
|
||||
end-of-stream "^1.4.1"
|
||||
fs-constants "^1.0.0"
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^3.1.1"
|
||||
|
||||
to-fast-properties@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
|
||||
@@ -1831,7 +2168,7 @@ tr46@~0.0.3:
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
|
||||
|
||||
tslib@^1.10.0:
|
||||
tslib@^1.10.0, tslib@^1.9.3:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
@@ -1846,6 +2183,13 @@ tslib@^2.2.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||
|
||||
tunnel-agent@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
|
||||
integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
tunnel@0.0.6, tunnel@^0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
|
||||
@@ -1880,6 +2224,11 @@ universalify@^0.1.2:
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||
|
||||
util-deprecate@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||
|
||||
uuid@^3.3.2:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
@@ -1980,6 +2329,11 @@ y18n@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
|
||||
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
yargs-parser@13.1.2, yargs-parser@^13.1.2:
|
||||
version "13.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
|
||||
|
||||
@@ -164,8 +164,8 @@
|
||||
"ansi-colors": "^3.2.3",
|
||||
"asar": "^3.0.3",
|
||||
"chromium-pickle-js": "^0.2.0",
|
||||
"cookie": "^0.4.0",
|
||||
"concurrently": "^5.2.0",
|
||||
"cookie": "^0.4.0",
|
||||
"copy-webpack-plugin": "^6.0.3",
|
||||
"cson-parser": "^1.3.3",
|
||||
"css-loader": "^3.2.0",
|
||||
|
||||
7
src/sql/azdata.proposed.d.ts
vendored
7
src/sql/azdata.proposed.d.ts
vendored
@@ -502,6 +502,13 @@ declare module 'azdata' {
|
||||
type?: ExtensionNodeType;
|
||||
}
|
||||
|
||||
export interface AccountKey {
|
||||
/**
|
||||
* Auth Library used to add the account
|
||||
*/
|
||||
authLibrary?: string;
|
||||
}
|
||||
|
||||
export namespace workspace {
|
||||
/**
|
||||
* Creates and enters a workspace at the specified location
|
||||
|
||||
@@ -50,6 +50,7 @@ import { Tenant, TenantListDelegate, TenantListRenderer } from 'sql/workbench/se
|
||||
import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces';
|
||||
|
||||
export const VIEWLET_ID = 'workbench.view.accountpanel';
|
||||
export type AuthLibrary = 'ADAL' | 'MSAL';
|
||||
|
||||
export class AccountPaneContainer extends ViewPaneContainer {
|
||||
|
||||
@@ -376,9 +377,14 @@ export class AccountDialog extends Modal {
|
||||
this._splitView!.layout(DOM.getContentHeight(this._container!));
|
||||
|
||||
// Set the initial items of the list
|
||||
providerView.updateAccounts(newProvider.initialAccounts);
|
||||
const authLibrary: AuthLibrary = this._configurationService.getValue('azure.authenticationLibrary');
|
||||
let updatedAccounts: azdata.Account[];
|
||||
if (authLibrary) {
|
||||
updatedAccounts = filterAccounts(newProvider.initialAccounts, authLibrary);
|
||||
}
|
||||
providerView.updateAccounts(updatedAccounts);
|
||||
|
||||
if (newProvider.initialAccounts.length > 0 && this._splitViewContainer!.hidden) {
|
||||
if (updatedAccounts.length > 0 && this._splitViewContainer!.hidden) {
|
||||
this.showSplitView();
|
||||
}
|
||||
|
||||
@@ -413,7 +419,12 @@ export class AccountDialog extends Modal {
|
||||
if (!providerMapping || !providerMapping.view) {
|
||||
return;
|
||||
}
|
||||
providerMapping.view.updateAccounts(args.accountList);
|
||||
const authLibrary: AuthLibrary = this._configurationService.getValue('azure.authenticationLibrary');
|
||||
let updatedAccounts: azdata.Account[];
|
||||
if (authLibrary) {
|
||||
updatedAccounts = filterAccounts(args.accountList, authLibrary);
|
||||
}
|
||||
providerMapping.view.updateAccounts(updatedAccounts);
|
||||
|
||||
if (args.accountList.length > 0 && this._splitViewContainer!.hidden) {
|
||||
this.showSplitView();
|
||||
@@ -480,3 +491,27 @@ export class AccountDialog extends Modal {
|
||||
v.addAccountAction.run();
|
||||
}
|
||||
}
|
||||
|
||||
// Filter accounts based on currently selected Auth Library:
|
||||
// if the account key is present, filter based on current auth library
|
||||
// if there is no account key (pre-MSAL account), then it is an ADAL account and
|
||||
// should be displayed as long as ADAL is the currently selected auth library
|
||||
export function filterAccounts(accounts: azdata.Account[], authLibrary: AuthLibrary): azdata.Account[] {
|
||||
let filteredAccounts = accounts.filter(account => {
|
||||
if (account.key.authLibrary) {
|
||||
if (account.key.authLibrary === authLibrary) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (authLibrary === 'ADAL') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return filteredAccounts;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ import { values } from 'vs/base/common/collections';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { INotificationService, Severity, INotification } from 'vs/platform/notification/common/notification';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { AuthLibrary, filterAccounts } from 'sql/workbench/services/accountManagement/browser/accountDialog';
|
||||
|
||||
export class AccountManagementService implements IAccountManagementService {
|
||||
// CONSTANTS ///////////////////////////////////////////////////////////
|
||||
@@ -36,6 +39,8 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
private _accountDialogController?: AccountDialogController;
|
||||
private _autoOAuthDialogController?: AutoOAuthDialogController;
|
||||
private _mementoContext?: Memento;
|
||||
protected readonly disposables = new DisposableStore();
|
||||
private readonly configurationService: IConfigurationService;
|
||||
|
||||
// EVENT EMITTERS //////////////////////////////////////////////////////
|
||||
private _addAccountProviderEmitter: Emitter<AccountProviderAddedEventParams>;
|
||||
@@ -54,7 +59,8 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
@IClipboardService private _clipboardService: IClipboardService,
|
||||
@IOpenerService private _openerService: IOpenerService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@INotificationService private readonly _notificationService: INotificationService
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
this._mementoContext = new Memento(AccountManagementService.ACCOUNT_MEMENTO, this._storageService);
|
||||
const mementoObj = this._mementoContext.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE);
|
||||
@@ -64,8 +70,10 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
this._addAccountProviderEmitter = new Emitter<AccountProviderAddedEventParams>();
|
||||
this._removeAccountProviderEmitter = new Emitter<azdata.AccountProviderMetadata>();
|
||||
this._updateAccountListEmitter = new Emitter<UpdateAccountListEventParams>();
|
||||
this.configurationService = configurationService;
|
||||
|
||||
_storageService.onWillSaveState(() => this.shutdown());
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private get autoOAuthDialogController(): AutoOAuthDialogController {
|
||||
@@ -136,6 +144,10 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
}
|
||||
|
||||
let result = await this._accountStore.addOrUpdate(account);
|
||||
if (!result) {
|
||||
this._logService.error('adding account failed');
|
||||
throw Error('Adding account failed, check Azure Accounts log for more info.')
|
||||
}
|
||||
if (result.accountAdded) {
|
||||
// Add the account to the list
|
||||
provider.accounts.push(result.changedAccount);
|
||||
@@ -458,10 +470,15 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
});
|
||||
}
|
||||
|
||||
const authLibrary: AuthLibrary = this.configurationService.getValue('azure.authenticationLibrary');
|
||||
let updatedAccounts: azdata.Account[]
|
||||
if (authLibrary) {
|
||||
updatedAccounts = filterAccounts(provider.accounts, authLibrary);
|
||||
}
|
||||
// Step 2) Fire the event
|
||||
let eventArg: UpdateAccountListEventParams = {
|
||||
providerId: provider.metadata.id,
|
||||
accountList: provider.accounts
|
||||
accountList: updatedAccounts ?? provider.accounts
|
||||
};
|
||||
this._updateAccountListEmitter.fire(eventArg);
|
||||
}
|
||||
@@ -475,6 +492,39 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
provider.accounts.splice(indexToRemove, 1, modifiedAccount);
|
||||
}
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this.disposables.add(this.configurationService.onDidChangeConfiguration(async e => {
|
||||
if (e.affectsConfiguration('azure.authenticationLibrary')) {
|
||||
const authLibrary: AuthLibrary = this.configurationService.getValue('azure.authenticationLibrary');
|
||||
if (authLibrary) {
|
||||
let accounts = await this._accountStore.getAllAccounts();
|
||||
if (accounts) {
|
||||
let updatedAccounts = filterAccounts(accounts, authLibrary);
|
||||
let eventArg: UpdateAccountListEventParams;
|
||||
if (updatedAccounts.length > 0) {
|
||||
updatedAccounts.forEach(account => {
|
||||
if (account.key.authLibrary === 'MSAL') {
|
||||
account.isStale = false;
|
||||
}
|
||||
});
|
||||
eventArg = {
|
||||
providerId: updatedAccounts[0].key.providerId,
|
||||
accountList: updatedAccounts
|
||||
};
|
||||
|
||||
} else { // default to public cloud if no accounts
|
||||
eventArg = {
|
||||
providerId: 'azure_publicCloud',
|
||||
accountList: updatedAccounts
|
||||
};
|
||||
}
|
||||
this._updateAccountListEmitter.fire(eventArg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ import { EventVerifierSingle } from 'sql/base/test/common/event';
|
||||
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
|
||||
import { AccountDialog } from 'sql/workbench/services/accountManagement/browser/accountDialog';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService';
|
||||
|
||||
// SUITE CONSTANTS /////////////////////////////////////////////////////////
|
||||
const hasAccountProvider: azdata.AccountProviderMetadata = {
|
||||
@@ -530,9 +531,10 @@ function getTestState(): AccountManagementState {
|
||||
.returns(() => mockAccountStore.object);
|
||||
|
||||
const testNotificationService = new TestNotificationService();
|
||||
const testConfigurationService = new TestConfigurationService();
|
||||
|
||||
// Create the account management service
|
||||
let ams = new AccountManagementService(mockInstantiationService.object, new TestStorageService(), undefined!, undefined!, undefined!, testNotificationService);
|
||||
let ams = new AccountManagementService(mockInstantiationService.object, new TestStorageService(), undefined!, undefined!, undefined!, testNotificationService, testConfigurationService);
|
||||
|
||||
// Wire up event handlers
|
||||
let evUpdate = new EventVerifierSingle<UpdateAccountListEventParams>();
|
||||
|
||||
@@ -26,6 +26,7 @@ import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
|
||||
import { ConnectionWidget } from 'sql/workbench/services/connection/browser/connectionWidget';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
/**
|
||||
* Connection Widget clas for CMS Connections
|
||||
@@ -47,8 +48,9 @@ export class CmsConnectionWidget extends ConnectionWidget {
|
||||
@IAccountManagementService _accountManagementService: IAccountManagementService,
|
||||
@ILogService _logService: ILogService,
|
||||
@IErrorMessageService _errorMessageService: IErrorMessageService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
super(options, callbacks, providerName, _themeService, _contextViewService, _connectionManagementService, _accountManagementService, _logService, _errorMessageService);
|
||||
super(options, callbacks, providerName, _themeService, _contextViewService, _connectionManagementService, _accountManagementService, _logService, _errorMessageService, configurationService);
|
||||
let authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
|
||||
if (authTypeOption) {
|
||||
let authTypeDefault = this.getAuthTypeDefault(authTypeOption, OS);
|
||||
|
||||
@@ -27,7 +27,6 @@ import { AzureResource, ConnectionOptionSpecialType } from 'sql/workbench/api/co
|
||||
import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces';
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
@@ -36,6 +36,8 @@ import Severity from 'vs/base/common/severity';
|
||||
import { ConnectionStringOptions } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
|
||||
import { AuthenticationType } from 'sql/platform/connection/common/constants';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { AuthLibrary, filterAccounts } from 'sql/workbench/services/accountManagement/browser/accountDialog';
|
||||
|
||||
const ConnectionStringText = localize('connectionWidget.connectionString', "Connection string");
|
||||
|
||||
@@ -107,6 +109,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
||||
color: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
private readonly configurationService: IConfigurationService;
|
||||
constructor(options: azdata.ConnectionOption[],
|
||||
callbacks: IConnectionComponentCallbacks,
|
||||
providerName: string,
|
||||
@@ -115,7 +118,8 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@IAccountManagementService private _accountManagementService: IAccountManagementService,
|
||||
@ILogService protected _logService: ILogService,
|
||||
@IErrorMessageService private _errorMessageService: IErrorMessageService
|
||||
@IErrorMessageService private _errorMessageService: IErrorMessageService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
this._callbacks = callbacks;
|
||||
@@ -135,6 +139,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
||||
}
|
||||
this._providerName = providerName;
|
||||
this._connectionStringOptions = this._connectionManagementService.getProviderProperties(this._providerName).connectionStringOptions;
|
||||
this.configurationService = configurationService;
|
||||
}
|
||||
|
||||
protected getAuthTypeDefault(option: azdata.ConnectionOption, os: OperatingSystem): string {
|
||||
@@ -591,7 +596,12 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
||||
private async fillInAzureAccountOptions(): Promise<void> {
|
||||
let oldSelection = this._azureAccountDropdown.value;
|
||||
const accounts = await this._accountManagementService.getAccounts();
|
||||
this._azureAccountList = accounts.filter(a => a.key.providerId.startsWith('azure'));
|
||||
const updatedAccounts = accounts.filter(a => a.key.providerId.startsWith('azure'));
|
||||
const authLibrary: AuthLibrary = this.configurationService.getValue('azure.authenticationLibrary');
|
||||
if (authLibrary) {
|
||||
this._azureAccountList = filterAccounts(updatedAccounts, authLibrary);
|
||||
}
|
||||
|
||||
let accountDropdownOptions: SelectOptionItemSQL[] = this._azureAccountList.map(account => {
|
||||
return {
|
||||
text: account.displayInfo.displayName,
|
||||
|
||||
Reference in New Issue
Block a user