mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
Add MSAL Authentication Library support (#21024)
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user