mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Remove ADAL Code (#23360)
* initial commit, removed all adal code * remove all authLibrary references in extension/azurecore * removed authLibrary references from src/sql * remove MSAL/ADAL setting option * wip fixing tests and removing Msal from method names * fixed tests * create accountInfo mock * fix tests * fix clientApplication mock * remove clientapplication * fix compile * add typing * wip * wip * wip * fix tree provider * remove SimpleTokenCache, FileDatabase & tests * remove remaining adal / authentication library references: * remove comma from package.nls.json * fix error fetching subscriptions * fix tests * remove getAzureAuthenticationLibraryConfig * remove adal check * fix build * remove test * undo remove customProviderSettings * fix bracket
This commit is contained in:
@@ -115,20 +115,6 @@
|
||||
"All"
|
||||
]
|
||||
},
|
||||
"azure.authenticationLibrary": {
|
||||
"type": "string",
|
||||
"description": "%config.authenticationLibrary%",
|
||||
"default": "MSAL",
|
||||
"enum": [
|
||||
"ADAL",
|
||||
"MSAL"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Azure Active Directory Authentication Library",
|
||||
"Microsoft Authentication Library"
|
||||
],
|
||||
"deprecationMessage": "Warning: ADAL has been deprecated, and is scheduled to be removed in a future release. Please use MSAL (default option) instead."
|
||||
},
|
||||
"azure.customProviderSettings": {
|
||||
"type": "array",
|
||||
"description": "%config.customProviderSettings%",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"config.noSystemKeychain": "Disable system keychain integration. Credentials will be stored in a flat file in the user's home directory.",
|
||||
"config.piiLogging": "Should Personally Identifiable Information (PII) be logged in the Azure Accounts output channel and the output channel log file.",
|
||||
"config.loggingLevel": "[Optional] The verbosity of logging for the Azure Accounts extension.",
|
||||
"config.authenticationLibrary": "The library used for the AAD auth flow. Please restart ADS after changing this option.",
|
||||
"config.customProviderSettings": "Setting containing custom Azure authentication endpoints. Changes to this setting require a restart to take effect.",
|
||||
"config.providerSettingsTitle": "Provider Settings",
|
||||
"config.providerSettingsName": "Cloud Name",
|
||||
|
||||
@@ -18,11 +18,8 @@ import {
|
||||
import { Deferred } from '../interfaces';
|
||||
import * as url from 'url';
|
||||
import * as Constants from '../../constants';
|
||||
import { SimpleTokenCache } from '../utils/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, AuthError, AuthenticationResult, InteractionRequiredAuthError, PublicClientApplication } from '@azure/msal-node';
|
||||
import { HttpClient } from './httpClient';
|
||||
@@ -43,20 +40,16 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
protected readonly clientId: string;
|
||||
protected readonly resources: Resource[];
|
||||
protected readonly httpClient: HttpClient;
|
||||
private _authLibrary: string | undefined;
|
||||
|
||||
constructor(
|
||||
protected readonly metadata: AzureAccountProviderMetadata,
|
||||
protected readonly tokenCache: SimpleTokenCache,
|
||||
protected readonly msalCacheProvider: MsalCachePluginProvider,
|
||||
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 authLibrary: string
|
||||
public readonly userFriendlyName: string
|
||||
) {
|
||||
this._authLibrary = authLibrary;
|
||||
|
||||
this.loginEndpointUrl = this.metadata.settings.host;
|
||||
this.commonTenant = {
|
||||
@@ -111,38 +104,24 @@ 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));
|
||||
}
|
||||
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,
|
||||
expiresOn: result.response.expiresOn!.getTime() / 1000
|
||||
const result = await this.login(this.organizationTenant, this.metadata.settings.microsoftResource);
|
||||
loginComplete = result.authComplete;
|
||||
if (!result?.response || !result.response?.account) {
|
||||
Logger.error(`Authentication failed: ${loginComplete}`);
|
||||
return {
|
||||
canceled: false
|
||||
};
|
||||
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 token: Token = {
|
||||
token: result.response.accessToken,
|
||||
key: result.response.account.homeAccountId,
|
||||
tokenType: result.response.tokenType,
|
||||
expiresOn: result.response.expiresOn!.getTime() / 1000
|
||||
};
|
||||
const tokenClaims = <TokenClaims>result.response.idTokenClaims;
|
||||
const account = await this.hydrateAccount(token, tokenClaims);
|
||||
loginComplete?.resolve();
|
||||
return account;
|
||||
} catch (ex) {
|
||||
Logger.error(`Login failed: ${ex}`);
|
||||
if (ex instanceof AzureAuthError) {
|
||||
@@ -162,158 +141,14 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
public async refreshAccessAdal(account: AzureAccount): Promise<AzureAccount> {
|
||||
// Deprecated account - delete it.
|
||||
if (account.key.accountVersion !== Constants.AccountVersion) {
|
||||
account.delete = true;
|
||||
return account;
|
||||
}
|
||||
try {
|
||||
// There can be multiple home tenants
|
||||
// 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.getAccountSecurityTokenAdal(account, tenant.id, azdata.AzureResource.MicrosoftResourceManagement);
|
||||
if (!tokenResult) {
|
||||
account.isStale = true;
|
||||
return account;
|
||||
}
|
||||
|
||||
return await this.hydrateAccount(tokenResult, this.getTokenClaims(tokenResult.token));
|
||||
} catch (ex) {
|
||||
if (ex instanceof AzureAuthError) {
|
||||
void vscode.window.showErrorMessage(ex.message);
|
||||
Logger.error(`Error refreshing access for account ${account.displayInfo.displayName}`, ex.originalMessageAndException);
|
||||
} else {
|
||||
Logger.error(ex);
|
||||
}
|
||||
account.isStale = true;
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
public async hydrateAccount(token: Token | AccessToken, tokenClaims: TokenClaims): Promise<AzureAccount> {
|
||||
let account: azdata.Account;
|
||||
if (this._authLibrary === Constants.AuthLibrary.MSAL) {
|
||||
const tenants = await this.getTenantsMsal(token.token, tokenClaims);
|
||||
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);
|
||||
}
|
||||
const tenants = await this.getTenants(token.token, tokenClaims);
|
||||
account = this.createAccount(tokenClaims, token.key, tenants);
|
||||
return account;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const resource = this.resources.find(s => s.azureResourceId === azureResource);
|
||||
|
||||
if (!resource) {
|
||||
Logger.error(`Unable to find Azure resource ${azureResource}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!account.properties.owningTenant) {
|
||||
// Should never happen
|
||||
throw new AzureAuthError(localize('azure.owningTenantNotFound', "Owning Tenant information not found for account."), 'Owning tenant not found.', undefined);
|
||||
}
|
||||
|
||||
const tenant = account.properties.owningTenant?.id === tenantId
|
||||
? account.properties.owningTenant
|
||||
: account.properties.tenants.find(t => t.id === tenantId);
|
||||
|
||||
if (!tenant) {
|
||||
throw new AzureAuthError(localize('azure.tenantNotFound', "Specified tenant with ID '{0}' not found.", tenantId), `Tenant ${tenantId} not found.`, undefined);
|
||||
}
|
||||
|
||||
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) {
|
||||
let expiry = Number(cachedTokens.expiresOn);
|
||||
if (Number.isNaN(expiry)) {
|
||||
Logger.error('Expiration time was not defined. This is expected on first launch');
|
||||
expiry = 0;
|
||||
}
|
||||
const currentTime = new Date().getTime() / 1000;
|
||||
|
||||
let accessToken = cachedTokens.accessToken;
|
||||
let expiresOn = Number(cachedTokens.expiresOn);
|
||||
const remainingTime = expiry - currentTime;
|
||||
const maxTolerance = 2 * 60; // two minutes
|
||||
|
||||
if (remainingTime < maxTolerance) {
|
||||
const result = await this.refreshTokenAdal(tenant, resource, cachedTokens.refreshToken);
|
||||
if (result) {
|
||||
accessToken = result.accessToken;
|
||||
expiresOn = Number(result.expiresOn);
|
||||
}
|
||||
}
|
||||
// Let's just return here.
|
||||
if (accessToken) {
|
||||
return {
|
||||
...accessToken,
|
||||
expiresOn: expiresOn,
|
||||
tokenType: Constants.Bearer
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// User didn't have any cached tokens, or the cached tokens weren't useful.
|
||||
// For most users we can use the refresh token from the general microsoft resource to an access token of basically any type of resource we want.
|
||||
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.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.');
|
||||
account.isStale = true;
|
||||
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.refreshTokenAdal(tenant, resource, baseTokens.refreshToken);
|
||||
if (result?.accessToken) {
|
||||
return {
|
||||
...result.accessToken,
|
||||
expiresOn: Number(result.expiresOn),
|
||||
tokenType: Constants.Bearer
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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.
|
||||
* @param tenant
|
||||
* @param resource
|
||||
* @param refreshToken
|
||||
* @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 refreshTokenAdal(tenant: Tenant, resource: Resource, refreshToken: RefreshToken | undefined): Promise<OAuthTokenResponse | undefined> {
|
||||
Logger.piiSanitized('Refreshing token', [{ name: 'token', objOrArray: refreshToken }], []);
|
||||
if (refreshToken) {
|
||||
const postData: RefreshTokenPostData = {
|
||||
grant_type: 'refresh_token',
|
||||
client_id: this.clientId,
|
||||
refresh_token: refreshToken.token,
|
||||
tenant: tenant.id,
|
||||
resource: resource.endpoint
|
||||
};
|
||||
return this.getTokenAdal(tenant, resource, postData);
|
||||
}
|
||||
return this.handleInteractionRequiredAdal(tenant, resource);
|
||||
}
|
||||
|
||||
protected abstract login(tenant: Tenant, resource: Resource): Promise<{ response: AuthenticationResult | null, authComplete: Deferred<void, Error> }>;
|
||||
|
||||
/**
|
||||
* Gets the access token for the correct account and scope from the token cache, if the correct token doesn't exist in the token cache
|
||||
@@ -323,7 +158,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
* @returns The authentication result, including the access token.
|
||||
* This function returns 'null' instead of 'undefined' by design as the same is returned by MSAL APIs in the flow (e.g. acquireTokenSilent).
|
||||
*/
|
||||
public async getTokenMsal(accountId: string, azureResource: azdata.AzureResource, tenantId: string): Promise<AuthenticationResult | azdata.PromptFailedResult | null> {
|
||||
public async getToken(accountId: string, azureResource: azdata.AzureResource, tenantId: string): Promise<AuthenticationResult | azdata.PromptFailedResult | null> {
|
||||
const resource = this.resources.find(s => s.azureResourceId === azureResource);
|
||||
|
||||
if (!resource) {
|
||||
@@ -368,7 +203,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
id: tenantId,
|
||||
displayName: ''
|
||||
};
|
||||
return this.handleInteractionRequiredMsal(tenant, resource);
|
||||
return this.handleInteractionRequired(tenant, resource);
|
||||
} else {
|
||||
if (e.name === 'ClientAuthError') {
|
||||
Logger.verbose('[ClientAuthError] Failed to silently acquire token');
|
||||
@@ -399,87 +234,10 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
return account;
|
||||
}
|
||||
|
||||
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);
|
||||
//#region tenant calls
|
||||
|
||||
// ADAL is being deprecated so just ignoring these for now
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
Logger.piiSanitized('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 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);
|
||||
}
|
||||
// ADAL is being deprecated so just ignoring these for now
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const accessTokenString = response.data.access_token;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const refreshTokenString = response.data.refresh_token;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const expiresOnString = response.data.expires_on;
|
||||
return this.getTokenHelperAdal(tenant, resource, accessTokenString, refreshTokenString, expiresOnString);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const tokenClaims: TokenClaims = this.getTokenClaims(accessTokenString);
|
||||
let userKey: string;
|
||||
|
||||
// Personal accounts don't have an oid when logging into the `common` tenant, but when logging into their home tenant they end up having an oid.
|
||||
// This makes the key for the same account be different.
|
||||
// We need to special case personal accounts.
|
||||
if (tokenClaims.idp === 'live.com') { // Personal account
|
||||
userKey = tokenClaims.unique_name ?? tokenClaims.email ?? tokenClaims.sub;
|
||||
} else {
|
||||
userKey = tokenClaims.home_oid ?? tokenClaims.oid ?? tokenClaims.unique_name ?? tokenClaims.email ?? tokenClaims.sub;
|
||||
}
|
||||
|
||||
if (!userKey) {
|
||||
const msg = localize('azure.noUniqueIdentifier', "The user had no unique identifier within AAD");
|
||||
throw new AzureAuthError(msg, 'No unique identifier', undefined);
|
||||
}
|
||||
|
||||
const accessToken: AccessToken = {
|
||||
token: accessTokenString,
|
||||
key: userKey
|
||||
};
|
||||
let refreshToken: RefreshToken | undefined = undefined;
|
||||
|
||||
if (refreshTokenString) {
|
||||
refreshToken = {
|
||||
token: refreshTokenString,
|
||||
key: userKey
|
||||
};
|
||||
}
|
||||
|
||||
const result: OAuthTokenResponse = {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenClaims,
|
||||
expiresOn: expiresOnString
|
||||
};
|
||||
|
||||
const accountKey: azdata.AccountKey = {
|
||||
providerId: this.metadata.id,
|
||||
accountId: userKey,
|
||||
authLibrary: this._authLibrary
|
||||
};
|
||||
|
||||
await this.saveTokenAdal(tenant, resource, accountKey, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getTenantsMsal(token: string, tokenClaims: TokenClaims): Promise<Tenant[]> {
|
||||
const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2020-01-01');
|
||||
public async getTenants(token: string, tokenClaims: TokenClaims): Promise<Tenant[]> {
|
||||
const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2019-11-01');
|
||||
try {
|
||||
Logger.verbose(`Fetching tenants with uri: ${tenantUri}`);
|
||||
let tenantList: string[] = [];
|
||||
@@ -526,135 +284,19 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//#region tenant calls
|
||||
public async getTenantsAdal(token: AccessToken): Promise<Tenant[]> {
|
||||
const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2020-01-01');
|
||||
try {
|
||||
Logger.verbose(`Fetching tenants with uri: ${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}`);
|
||||
Logger.error(`Headers: ${JSON.stringify(tenantResponse.headers)}`);
|
||||
throw new Error('Error with tenant response');
|
||||
}
|
||||
// ADAL is being deprecated so just ignoring these for now
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
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.key,
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region token management
|
||||
private async saveTokenAdal(tenant: Tenant, resource: Resource, accountKey: azdata.AccountKey, { accessToken, refreshToken, expiresOn }: OAuthTokenResponse) {
|
||||
const msg = localize('azure.cacheErrorAdd', "Error when adding your account to the cache.");
|
||||
if (!tenant.id || !resource.id) {
|
||||
Logger.piiSanitized('Tenant ID or resource ID was undefined', [], [], tenant, resource);
|
||||
throw new AzureAuthError(msg, 'Adding account to cache failed', undefined);
|
||||
}
|
||||
try {
|
||||
Logger.piiSanitized(`Saving access token`, [{ name: 'access_token', objOrArray: accessToken }], []);
|
||||
await this.tokenCache.saveCredential(`${accountKey.accountId}_access_${resource.id}_${tenant.id}`, JSON.stringify(accessToken));
|
||||
Logger.piiSanitized(`Saving refresh token`, [{ name: 'refresh_token', objOrArray: refreshToken }], []);
|
||||
await this.tokenCache.saveCredential(`${accountKey.accountId}_refresh_${resource.id}_${tenant.id}`, JSON.stringify(refreshToken));
|
||||
this.memdb.set(`${accountKey.accountId}_${tenant.id}_${resource.id}`, expiresOn);
|
||||
} catch (ex) {
|
||||
Logger.error(ex);
|
||||
throw new AzureAuthError(msg, 'Adding account to cache failed', ex);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
if (!tenant.id || !resource.id) {
|
||||
Logger.piiSanitized('Tenant ID or resource ID was undefined', [], [], tenant, resource);
|
||||
throw new AzureAuthError(getMsg, 'Getting account from cache failed', undefined);
|
||||
}
|
||||
|
||||
let accessTokenString: string | undefined = undefined;
|
||||
let refreshTokenString: string | undefined = undefined;
|
||||
let expiresOn: string;
|
||||
try {
|
||||
Logger.info('Fetching saved token');
|
||||
accessTokenString = await this.tokenCache.getCredential(`${accountKey.accountId}_access_${resource.id}_${tenant.id}`);
|
||||
refreshTokenString = await this.tokenCache.getCredential(`${accountKey.accountId}_refresh_${resource.id}_${tenant.id}`);
|
||||
expiresOn = this.memdb.get(`${accountKey.accountId}_${tenant.id}_${resource.id}`);
|
||||
} catch (ex) {
|
||||
Logger.error(ex);
|
||||
throw new AzureAuthError(getMsg, 'Getting account from cache failed', ex);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!accessTokenString) {
|
||||
Logger.error('No access token found');
|
||||
return undefined;
|
||||
}
|
||||
const accessToken: AccessToken = JSON.parse(accessTokenString) as AccessToken;
|
||||
let refreshToken: RefreshToken | undefined = undefined;
|
||||
if (refreshTokenString) {
|
||||
refreshToken = JSON.parse(refreshTokenString) as RefreshToken;
|
||||
}
|
||||
Logger.piiSanitized('GetSavedToken ', [{ name: 'access', objOrArray: accessToken }, { name: 'refresh', objOrArray: refreshToken }], [], `expiresOn=${expiresOn}`);
|
||||
return {
|
||||
accessToken, refreshToken, expiresOn
|
||||
};
|
||||
} catch (ex) {
|
||||
Logger.error(ex);
|
||||
throw new AzureAuthError(parseMsg, 'Parsing account from cache failed', ex);
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region interaction handling
|
||||
public async handleInteractionRequiredMsal(tenant: Tenant, resource: Resource): Promise<AuthenticationResult | null> {
|
||||
public async handleInteractionRequired(tenant: Tenant, resource: Resource): Promise<AuthenticationResult | null> {
|
||||
const shouldOpen = await this.askUserForInteraction(tenant, resource);
|
||||
if (shouldOpen) {
|
||||
const result = await this.loginMsal(tenant, resource);
|
||||
const result = await this.login(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;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the account needs to be refreshed based on received error instance
|
||||
* and STS error codes from errorMessage.
|
||||
@@ -802,8 +444,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
key: {
|
||||
providerId: this.metadata.id,
|
||||
accountId: key,
|
||||
accountVersion: Constants.AccountVersion,
|
||||
authLibrary: this._authLibrary
|
||||
accountVersion: Constants.AccountVersion
|
||||
},
|
||||
name: displayName,
|
||||
displayInfo: {
|
||||
@@ -830,37 +471,6 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
//#endregion
|
||||
|
||||
//#region network functions
|
||||
public async makePostRequest(url: string, postData: AuthorizationCodePostData | TokenPostData | DeviceCodeStartPostData | DeviceCodeCheckPostData): Promise<AxiosResponse<any>> {
|
||||
const config: AxiosRequestConfig = {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
validateStatus: () => true // Never throw
|
||||
};
|
||||
|
||||
// Intercept response and print out the response for future debugging
|
||||
const response = await axios.post(url, qs.stringify(postData), config);
|
||||
// ADAL is being deprecated so just ignoring these for now
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
Logger.piiSanitized('POST request ', [{ name: 'data', objOrArray: postData }, { name: 'response', objOrArray: response.data }], [], url);
|
||||
return response;
|
||||
}
|
||||
|
||||
private async makeGetRequest(url: string, token: string): Promise<AxiosResponse<any>> {
|
||||
const config: AxiosRequestConfig = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
validateStatus: () => true // Never throw
|
||||
};
|
||||
|
||||
const response = await axios.get(url, config);
|
||||
// ADAL is being deprecated so just ignoring these for now
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
Logger.piiSanitized('GET request ', [{ name: 'response', objOrArray: response.data.value ?? response.data }], [], url,);
|
||||
return response;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -877,8 +487,7 @@ 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 deleteAllCacheMsal(): Promise<void> {
|
||||
public async deleteAllCache(): Promise<void> {
|
||||
this.clientApplication.clearCache();
|
||||
|
||||
// unlink both cache files
|
||||
@@ -889,30 +498,16 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
await this.msalCacheProvider.clearCacheEncryptionKeys();
|
||||
}
|
||||
|
||||
public async deleteAllCacheAdal(): Promise<void> {
|
||||
const results = await this.tokenCache.findCredentials('');
|
||||
|
||||
for (let { account } of results) {
|
||||
await this.tokenCache.clearCredential(account);
|
||||
}
|
||||
}
|
||||
|
||||
public async clearCredentials(account: azdata.AccountKey): Promise<void> {
|
||||
try {
|
||||
// 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 await this.deleteAccountCacheMsal(account);
|
||||
} else { // fallback to ADAL by default
|
||||
return await this.deleteAccountCacheAdal(account);
|
||||
}
|
||||
return await this.deleteAccountCache(account);
|
||||
} catch (ex) {
|
||||
// We need not prompt user for error if token could not be removed from cache.
|
||||
Logger.error('Error when removing token from cache: ', ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteAccountCacheMsal(accountKey: azdata.AccountKey): Promise<void> {
|
||||
private async deleteAccountCache(accountKey: azdata.AccountKey): Promise<void> {
|
||||
const tokenCache = this.clientApplication.getTokenCache();
|
||||
try {
|
||||
let msalAccount: AccountInfo | null = await this.getAccountFromMsalCache(accountKey.accountId);
|
||||
@@ -927,16 +522,6 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
await this.msalCacheProvider.clearAccountFromLocalCache(accountKey.accountId);
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
}
|
||||
|
||||
public async dispose() { }
|
||||
|
||||
public async autoOAuthCancelled(): Promise<void> { }
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AuthorizationCodePostData, AzureAuth, OAuthTokenResponse } from './azureAuth';
|
||||
import { AzureAuth } from './azureAuth';
|
||||
import { AzureAccountProviderMetadata, AzureAuthType, Resource, Tenant } from 'azurecore';
|
||||
import { Deferred } from '../interfaces';
|
||||
import * as vscode from 'vscode';
|
||||
import * as crypto from 'crypto';
|
||||
import { SimpleTokenCache } from '../utils/simpleTokenCache';
|
||||
import { SimpleWebServer } from '../utils/simpleWebServer';
|
||||
import { AzureAuthError } from './azureAuthError';
|
||||
import { Logger } from '../../utils/Logger';
|
||||
@@ -16,19 +14,12 @@ 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';
|
||||
import { MsalCachePluginProvider } from '../utils/msalCachePlugin';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
interface AuthCodeResponse {
|
||||
authCode: string;
|
||||
codeVerifier: string;
|
||||
redirectUri: string;
|
||||
}
|
||||
|
||||
interface CryptoValues {
|
||||
nonce: string;
|
||||
challengeMethod: string;
|
||||
@@ -43,14 +34,12 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
||||
|
||||
constructor(
|
||||
metadata: AzureAccountProviderMetadata,
|
||||
tokenCache: SimpleTokenCache,
|
||||
msalCacheProvider: MsalCachePluginProvider,
|
||||
context: vscode.ExtensionContext,
|
||||
uriEventEmitter: vscode.EventEmitter<vscode.Uri>,
|
||||
clientApplication: PublicClientApplication,
|
||||
authLibrary: string
|
||||
clientApplication: PublicClientApplication
|
||||
) {
|
||||
super(metadata, tokenCache, msalCacheProvider, context, clientApplication, uriEventEmitter, AzureAuthType.AuthCodeGrant, AzureAuthCodeGrant.USER_FRIENDLY_NAME, authLibrary);
|
||||
super(metadata, msalCacheProvider, context, clientApplication, uriEventEmitter, AzureAuthType.AuthCodeGrant, AzureAuthCodeGrant.USER_FRIENDLY_NAME);
|
||||
this.cryptoProvider = new CryptoProvider();
|
||||
this.pkceCodes = {
|
||||
nonce: '',
|
||||
@@ -60,30 +49,13 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
||||
};
|
||||
}
|
||||
|
||||
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.loginWebAdal(tenant, resource);
|
||||
} else {
|
||||
authResponse = await this.loginDesktopAdal(tenant, resource, authCompletePromise);
|
||||
}
|
||||
|
||||
return {
|
||||
response: await this.getTokenWithAuthorizationCode(tenant, resource, authResponse),
|
||||
authComplete: authCompleteDeferred!
|
||||
};
|
||||
}
|
||||
|
||||
protected async loginMsal(tenant: Tenant, resource: Resource): Promise<{ response: AuthenticationResult | null, authComplete: Deferred<void, Error> }> {
|
||||
protected async login(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);
|
||||
authCodeRequest = await this.loginWeb(tenant, resource);
|
||||
} else {
|
||||
authCodeRequest = await this.loginDesktopMsal(tenant, resource, authCompletePromise);
|
||||
}
|
||||
@@ -101,26 +73,7 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an OAuthTokenResponse from Microsoft OAuth
|
||||
*
|
||||
* @param tenant
|
||||
* @param resource
|
||||
*/
|
||||
private async getTokenWithAuthorizationCode(tenant: Tenant, resource: Resource, { authCode, redirectUri, codeVerifier }: AuthCodeResponse): Promise<OAuthTokenResponse | undefined> {
|
||||
const postData: AuthorizationCodePostData = {
|
||||
grant_type: 'authorization_code',
|
||||
code: authCode,
|
||||
client_id: this.clientId,
|
||||
code_verifier: codeVerifier,
|
||||
redirect_uri: redirectUri,
|
||||
resource: resource.endpoint
|
||||
};
|
||||
|
||||
return this.getTokenAdal(tenant, resource, postData);
|
||||
}
|
||||
|
||||
private async loginWebMsal(tenant: Tenant, resource: Resource): Promise<AuthorizationCodeRequest> {
|
||||
private async loginWeb(tenant: Tenant, resource: Resource): Promise<AuthorizationCodeRequest> {
|
||||
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://microsoft.azurecore`));
|
||||
await this.createCryptoValuesMsal();
|
||||
const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
|
||||
@@ -156,36 +109,6 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
||||
}
|
||||
}
|
||||
|
||||
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)}`;
|
||||
|
||||
const loginQuery = {
|
||||
response_type: 'code',
|
||||
response_mode: 'query',
|
||||
client_id: this.clientId,
|
||||
redirect_uri: this.redirectUri,
|
||||
state,
|
||||
prompt: Constants.SELECT_ACCOUNT,
|
||||
code_challenge_method: Constants.S256_CODE_CHALLENGE_METHOD,
|
||||
code_challenge: codeChallenge,
|
||||
resource: resource.id
|
||||
};
|
||||
|
||||
const signInUrl = `${this.loginEndpointUrl}${tenant.id}/oauth2/authorize?${qs.stringify(loginQuery)}`;
|
||||
await vscode.env.openExternal(vscode.Uri.parse(signInUrl));
|
||||
|
||||
const authCode = await this.handleWebResponse(state);
|
||||
|
||||
return {
|
||||
authCode,
|
||||
codeVerifier,
|
||||
redirectUri: this.redirectUri
|
||||
};
|
||||
}
|
||||
|
||||
private async handleWebResponse(state: string): Promise<string> {
|
||||
let uriEventListener: vscode.Disposable;
|
||||
return new Promise((resolve: (value: any) => void, reject) => {
|
||||
@@ -262,40 +185,6 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
response_mode: 'query',
|
||||
client_id: this.clientId,
|
||||
redirect_uri: `${this.redirectUri}:${serverPort}/redirect`,
|
||||
state,
|
||||
prompt: Constants.SELECT_ACCOUNT,
|
||||
code_challenge_method: Constants.S256_CODE_CHALLENGE_METHOD,
|
||||
code_challenge: codeChallenge,
|
||||
resource: resource.endpoint
|
||||
};
|
||||
const loginUrl = `${this.loginEndpointUrl}${tenant.id}/oauth2/authorize?${qs.stringify(loginQuery)}`;
|
||||
await vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${serverPort}/signin?nonce=${encodeURIComponent(nonce)}`));
|
||||
const authCode = await this.addServerListeners(server, nonce, loginUrl, authCompletePromise);
|
||||
return {
|
||||
authCode,
|
||||
codeVerifier,
|
||||
redirectUri: `${this.redirectUri}:${serverPort}/redirect`
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
private async addServerListeners(server: SimpleWebServer, nonce: string, loginUrl: string, authComplete: Promise<void>): Promise<string> {
|
||||
const mediaPath = path.join(this.context.extensionPath, 'media');
|
||||
|
||||
@@ -392,18 +281,6 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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, challengeMethod, codeVerifier, codeChallenge
|
||||
};
|
||||
}
|
||||
|
||||
private async createCryptoValuesMsal(): Promise<void> {
|
||||
this.pkceCodes.nonce = this.cryptoProvider.createNewGuid();
|
||||
const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();
|
||||
|
||||
@@ -7,11 +7,7 @@ import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import {
|
||||
AzureAuth,
|
||||
OAuthTokenResponse,
|
||||
DeviceCodeStartPostData,
|
||||
DeviceCodeCheckPostData,
|
||||
|
||||
AzureAuth
|
||||
} from './azureAuth';
|
||||
import {
|
||||
AzureAccountProviderMetadata,
|
||||
@@ -21,46 +17,26 @@ import {
|
||||
} from 'azurecore';
|
||||
import { Deferred } from '../interfaces';
|
||||
import { AuthenticationResult, DeviceCodeRequest, PublicClientApplication } from '@azure/msal-node';
|
||||
import { SimpleTokenCache } from '../utils/simpleTokenCache';
|
||||
import { Logger } from '../../utils/Logger';
|
||||
import { MsalCachePluginProvider } from '../utils/msalCachePlugin';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
interface DeviceCodeLogin { // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code
|
||||
device_code: string,
|
||||
expires_in: number;
|
||||
interval: number;
|
||||
message: string;
|
||||
user_code: string;
|
||||
verification_url: string
|
||||
}
|
||||
|
||||
interface DeviceCodeLoginResult {
|
||||
token_type: string,
|
||||
scope: string,
|
||||
expires_in: number,
|
||||
access_token: string,
|
||||
refresh_token: string,
|
||||
}
|
||||
|
||||
export class AzureDeviceCode extends AzureAuth {
|
||||
private static readonly USER_FRIENDLY_NAME: string = localize('azure.azureDeviceCodeAuth', "Azure Device Code");
|
||||
private readonly pageTitle: string;
|
||||
constructor(
|
||||
metadata: AzureAccountProviderMetadata,
|
||||
tokenCache: SimpleTokenCache,
|
||||
msalCacheProvider: MsalCachePluginProvider,
|
||||
context: vscode.ExtensionContext,
|
||||
uriEventEmitter: vscode.EventEmitter<vscode.Uri>,
|
||||
clientApplication: PublicClientApplication,
|
||||
authLibrary: string
|
||||
clientApplication: PublicClientApplication
|
||||
) {
|
||||
super(metadata, tokenCache, msalCacheProvider, context, clientApplication, uriEventEmitter, AzureAuthType.DeviceCode, AzureDeviceCode.USER_FRIENDLY_NAME, authLibrary);
|
||||
super(metadata, msalCacheProvider, context, clientApplication, uriEventEmitter, AzureAuthType.DeviceCode, AzureDeviceCode.USER_FRIENDLY_NAME);
|
||||
this.pageTitle = localize('addAccount', "Add {0} account", this.metadata.displayName);
|
||||
}
|
||||
|
||||
protected async loginMsal(tenant: Tenant, resource: Resource): Promise<{ response: AuthenticationResult | null, authComplete: Deferred<void, Error> }> {
|
||||
protected async login(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 });
|
||||
|
||||
@@ -80,89 +56,11 @@ export class AzureDeviceCode extends AzureAuth {
|
||||
};
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
const uri = `${this.loginEndpointUrl}/${this.commonTenant.id}/oauth2/devicecode`;
|
||||
const postData: DeviceCodeStartPostData = {
|
||||
client_id: this.clientId,
|
||||
resource: resource.endpoint
|
||||
};
|
||||
|
||||
const postResult = await this.makePostRequest(uri, postData);
|
||||
|
||||
const initialDeviceLogin: DeviceCodeLogin = postResult.data as DeviceCodeLogin;
|
||||
|
||||
await azdata.accounts.beginAutoOAuthDeviceCode(this.metadata.id, this.pageTitle, initialDeviceLogin.message, initialDeviceLogin.user_code, initialDeviceLogin.verification_url);
|
||||
|
||||
const finalDeviceLogin = await this.setupPolling(initialDeviceLogin);
|
||||
|
||||
const accessTokenString = finalDeviceLogin.access_token;
|
||||
const refreshTokenString = finalDeviceLogin.refresh_token;
|
||||
|
||||
const currentTime = new Date().getTime() / 1000;
|
||||
const expiresOn = `${currentTime + finalDeviceLogin.expires_in}`;
|
||||
|
||||
const result = await this.getTokenHelperAdal(tenant, resource, accessTokenString, refreshTokenString, expiresOn);
|
||||
this.closeOnceComplete(authCompletePromise).catch(Logger.error);
|
||||
|
||||
return {
|
||||
response: result,
|
||||
authComplete: authCompleteDeferred!
|
||||
};
|
||||
}
|
||||
|
||||
private async closeOnceComplete(promise: Promise<void>): Promise<void> {
|
||||
await promise;
|
||||
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;
|
||||
|
||||
return new Promise<DeviceCodeLoginResult>((resolve, reject) => {
|
||||
let timeout: NodeJS.Timer;
|
||||
|
||||
const timer = setInterval(async () => {
|
||||
const x = await this.checkForResult(info);
|
||||
if (!x.access_token) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
clearInterval(timer);
|
||||
resolve(x);
|
||||
}, info.interval * 1000);
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
clearInterval(timer);
|
||||
reject(new Error(timeoutMessage));
|
||||
}, fiveMinutes);
|
||||
});
|
||||
}
|
||||
|
||||
private async checkForResult(info: DeviceCodeLogin): Promise<DeviceCodeLoginResult> {
|
||||
const msg = localize('azure.deviceCodeCheckFail', "Error encountered when trying to check for login results");
|
||||
try {
|
||||
const uri = `${this.loginEndpointUrl}/${this.commonTenant}/oauth2/token`;
|
||||
const postData: DeviceCodeCheckPostData = {
|
||||
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
||||
client_id: this.clientId,
|
||||
tenant: this.commonTenant.id,
|
||||
code: info.device_code
|
||||
};
|
||||
|
||||
const postResult = await this.makePostRequest(uri, postData);
|
||||
const result: DeviceCodeLoginResult = postResult.data as DeviceCodeLoginResult;
|
||||
|
||||
return result;
|
||||
} catch (ex) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -14,12 +14,10 @@ import {
|
||||
} from 'azurecore';
|
||||
import { Deferred } from './interfaces';
|
||||
import { AuthenticationResult, PublicClientApplication } from '@azure/msal-node';
|
||||
import { SimpleTokenCache } from './utils/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';
|
||||
import { MsalCachePluginProvider } from './utils/msalCachePlugin';
|
||||
import { getTenantIgnoreList } from '../utils';
|
||||
@@ -35,12 +33,10 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
|
||||
|
||||
constructor(
|
||||
metadata: AzureAccountProviderMetadata,
|
||||
tokenCache: SimpleTokenCache,
|
||||
context: vscode.ExtensionContext,
|
||||
clientApplication: PublicClientApplication,
|
||||
private readonly msalCacheProvider: MsalCachePluginProvider,
|
||||
uriEventHandler: vscode.EventEmitter<vscode.Uri>,
|
||||
private readonly authLibrary: string,
|
||||
private readonly forceDeviceCode: boolean = false
|
||||
) {
|
||||
this.clientApplication = clientApplication;
|
||||
@@ -48,11 +44,11 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
|
||||
vscode.workspace.onDidChangeConfiguration((changeEvent) => {
|
||||
const impactProvider = changeEvent.affectsConfiguration(Constants.AccountsAzureAuthSection);
|
||||
if (impactProvider === true) {
|
||||
this.handleAuthMapping(metadata, tokenCache, context, uriEventHandler);
|
||||
this.handleAuthMapping(metadata, context, uriEventHandler);
|
||||
}
|
||||
});
|
||||
|
||||
this.handleAuthMapping(metadata, tokenCache, context, uriEventHandler);
|
||||
this.handleAuthMapping(metadata, context, uriEventHandler);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
@@ -60,13 +56,10 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
|
||||
}
|
||||
|
||||
clearTokenCache(): Thenable<void> {
|
||||
return this.authLibrary === Constants.AuthLibrary.MSAL
|
||||
? this.getAuthMethod().deleteAllCacheMsal()
|
||||
// fallback to ADAL as default
|
||||
: this.getAuthMethod().deleteAllCacheAdal();
|
||||
return this.getAuthMethod().deleteAllCache();
|
||||
}
|
||||
|
||||
private handleAuthMapping(metadata: AzureAccountProviderMetadata, tokenCache: SimpleTokenCache, context: vscode.ExtensionContext, uriEventHandler: vscode.EventEmitter<vscode.Uri>) {
|
||||
private handleAuthMapping(metadata: AzureAccountProviderMetadata, context: vscode.ExtensionContext, uriEventHandler: vscode.EventEmitter<vscode.Uri>) {
|
||||
this.authMappings.forEach(m => m.dispose());
|
||||
this.authMappings.clear();
|
||||
|
||||
@@ -75,10 +68,10 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
|
||||
const deviceCodeMethod: boolean = configuration.get<boolean>(Constants.AuthType.DeviceCode, false);
|
||||
|
||||
if (codeGrantMethod === true && !this.forceDeviceCode) {
|
||||
this.authMappings.set(AzureAuthType.AuthCodeGrant, new AzureAuthCodeGrant(metadata, tokenCache, this.msalCacheProvider, context, uriEventHandler, this.clientApplication, this.authLibrary));
|
||||
this.authMappings.set(AzureAuthType.AuthCodeGrant, new AzureAuthCodeGrant(metadata, this.msalCacheProvider, context, uriEventHandler, this.clientApplication));
|
||||
}
|
||||
if (deviceCodeMethod === true || this.forceDeviceCode) {
|
||||
this.authMappings.set(AzureAuthType.DeviceCode, new AzureDeviceCode(metadata, tokenCache, this.msalCacheProvider, context, uriEventHandler, this.clientApplication, this.authLibrary));
|
||||
this.authMappings.set(AzureAuthType.DeviceCode, new AzureDeviceCode(metadata, this.msalCacheProvider, context, uriEventHandler, this.clientApplication));
|
||||
}
|
||||
if (codeGrantMethod === false && deviceCodeMethod === false && !this.forceDeviceCode) {
|
||||
console.error('No authentication methods selected');
|
||||
@@ -110,25 +103,20 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
|
||||
private async _initialize(storedAccounts: AzureAccount[]): Promise<AzureAccount[]> {
|
||||
const accounts: AzureAccount[] = [];
|
||||
Logger.verbose(`Initializing stored accounts ${JSON.stringify(accounts)}`);
|
||||
const updatedAccounts = filterAccounts(storedAccounts, this.authLibrary);
|
||||
for (let account of updatedAccounts) {
|
||||
for (let account of storedAccounts) {
|
||||
const azureAuth = this.getAuthMethod(account);
|
||||
if (!azureAuth) {
|
||||
account.isStale = true;
|
||||
accounts.push(account);
|
||||
} else {
|
||||
account.isStale = false;
|
||||
if (this.authLibrary === Constants.AuthLibrary.MSAL) {
|
||||
// Check MSAL Cache before adding account, to mark it as stale if it is not present in cache
|
||||
const accountInCache = await azureAuth.getAccountFromMsalCache(account.key.accountId);
|
||||
if (!accountInCache) {
|
||||
account.isStale = true;
|
||||
}
|
||||
accounts.push(account);
|
||||
|
||||
} else { // fallback to ADAL as default
|
||||
accounts.push(await azureAuth.refreshAccessAdal(account));
|
||||
// Check MSAL Cache before adding account, to mark it as stale if it is not present in cache
|
||||
const accountInCache = await azureAuth.getAccountFromMsalCache(account.key.accountId);
|
||||
if (!accountInCache) {
|
||||
account.isStale = true;
|
||||
}
|
||||
accounts.push(account);
|
||||
|
||||
}
|
||||
}
|
||||
this.initComplete.resolve();
|
||||
@@ -148,53 +136,49 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
|
||||
const azureAuth = this.getAuthMethod(account);
|
||||
if (azureAuth) {
|
||||
Logger.piiSanitized(`Getting account security token for ${JSON.stringify(account.key)} (tenant ${tenantId}). Auth Method = ${azureAuth.userFriendlyName}`, [], []);
|
||||
if (this.authLibrary === Constants.AuthLibrary.MSAL) {
|
||||
try {
|
||||
// Fetch cached token from local cache if token is available and valid.
|
||||
let accessToken = await this.msalCacheProvider.getTokenFromLocalCache(account.key.accountId, tenantId, resource);
|
||||
if (this.isValidToken(accessToken) &&
|
||||
// Ensure MSAL Cache contains user account
|
||||
(await this.clientApplication.getAllAccounts()).find((accountInfo) => accountInfo.homeAccountId === account.key.accountId)) {
|
||||
return accessToken;
|
||||
} // else fallback to fetching a new token.
|
||||
} catch (e) {
|
||||
// Log any error and move on to fetching fresh access token.
|
||||
Logger.info(`Could not fetch access token from cache: ${e}, fetching new access token instead.`);
|
||||
}
|
||||
tenantId = tenantId || account.properties.owningTenant.id;
|
||||
if (getTenantIgnoreList().includes(tenantId)) {
|
||||
// Tenant found in ignore list, don't fetch access token.
|
||||
Logger.info(`Tenant ${tenantId} found in the ignore list, authentication will not be attempted. Please remove tenant from setting: '${Constants.AzureTenantConfigFilterSetting}' if you want to re-enable tenant for authentication.`);
|
||||
throw new TenantIgnoredError(localize('tenantIgnoredError', 'Tenant found in ignore list, authentication not attempted. You can remove tenant {0} from ignore list in settings.json file: {1} if you wish to access resources from this tenant.', tenantId, Constants.AzureTenantConfigFilterSetting));
|
||||
try {
|
||||
// Fetch cached token from local cache if token is available and valid.
|
||||
let accessToken = await this.msalCacheProvider.getTokenFromLocalCache(account.key.accountId, tenantId, resource);
|
||||
if (this.isValidToken(accessToken) &&
|
||||
// Ensure MSAL Cache contains user account
|
||||
(await this.clientApplication.getAllAccounts()).find((accountInfo) => accountInfo.homeAccountId === account.key.accountId)) {
|
||||
return accessToken;
|
||||
} // else fallback to fetching a new token.
|
||||
} catch (e) {
|
||||
// Log any error and move on to fetching fresh access token.
|
||||
Logger.info(`Could not fetch access token from cache: ${e}, fetching new access token instead.`);
|
||||
}
|
||||
tenantId = tenantId || account.properties.owningTenant.id;
|
||||
if (getTenantIgnoreList().includes(tenantId)) {
|
||||
// Tenant found in ignore list, don't fetch access token.
|
||||
Logger.info(`Tenant ${tenantId} found in the ignore list, authentication will not be attempted. Please remove tenant from setting: '${Constants.AzureTenantConfigFilterSetting}' if you want to re-enable tenant for authentication.`);
|
||||
throw new TenantIgnoredError(localize('tenantIgnoredError', 'Tenant found in ignore list, authentication not attempted. You can remove tenant {0} from ignore list in settings.json file: {1} if you wish to access resources from this tenant.', tenantId, Constants.AzureTenantConfigFilterSetting));
|
||||
} else {
|
||||
let authResult = await azureAuth.getToken(account.key.accountId, resource, tenantId);
|
||||
if (this.isAuthenticationResult(authResult) && authResult.account && authResult.account.idTokenClaims) {
|
||||
const token: Token = {
|
||||
key: authResult.account.homeAccountId,
|
||||
token: authResult.accessToken,
|
||||
tokenType: authResult.tokenType,
|
||||
expiresOn: authResult.account.idTokenClaims.exp!,
|
||||
tenantId: tenantId,
|
||||
resource: resource
|
||||
};
|
||||
try {
|
||||
await this.msalCacheProvider.writeTokenToLocalCache(token);
|
||||
} catch (e) {
|
||||
Logger.error(`Could not save access token to local cache: ${e}, this might cause throttling of AAD requests.`);
|
||||
}
|
||||
return token;
|
||||
} else {
|
||||
let authResult = await azureAuth.getTokenMsal(account.key.accountId, resource, tenantId);
|
||||
if (this.isAuthenticationResult(authResult) && authResult.account && authResult.account.idTokenClaims) {
|
||||
const token: Token = {
|
||||
key: authResult.account.homeAccountId,
|
||||
token: authResult.accessToken,
|
||||
tokenType: authResult.tokenType,
|
||||
expiresOn: authResult.account.idTokenClaims.exp!,
|
||||
tenantId: tenantId,
|
||||
resource: resource
|
||||
};
|
||||
try {
|
||||
await this.msalCacheProvider.writeTokenToLocalCache(token);
|
||||
} catch (e) {
|
||||
Logger.error(`Could not save access token to local cache: ${e}, this might cause throttling of AAD requests.`);
|
||||
}
|
||||
return token;
|
||||
Logger.error(`MSAL: getToken call failed: ${authResult}`);
|
||||
// Throw error with MSAL-specific code/message, else throw generic error message
|
||||
if (this.isProviderError(authResult)) {
|
||||
throw new Error(localize('msalTokenError', `{0} occurred when acquiring token. \n{1}`, authResult.errorCode, authResult.errorMessage));
|
||||
} else {
|
||||
Logger.error(`MSAL: getToken call failed: ${authResult}`);
|
||||
// Throw error with MSAL-specific code/message, else throw generic error message
|
||||
if (this.isProviderError(authResult)) {
|
||||
throw new Error(localize('msalTokenError', `{0} occurred when acquiring token. \n{1}`, authResult.errorCode, authResult.errorMessage));
|
||||
} else {
|
||||
throw new Error(localize('genericTokenError', 'Failed to get token'));
|
||||
}
|
||||
throw new Error(localize('genericTokenError', 'Failed to get token'));
|
||||
}
|
||||
}
|
||||
} else { // fallback to ADAL as default
|
||||
return azureAuth.getAccountSecurityTokenAdal(account, tenantId, resource);
|
||||
}
|
||||
} else {
|
||||
account.isStale = true;
|
||||
|
||||
@@ -9,7 +9,6 @@ import * as events from 'events';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as vscode from 'vscode';
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import { SimpleTokenCache } from './utils/simpleTokenCache';
|
||||
import providerSettings from './providerSettings';
|
||||
import { AzureAccountProvider as AzureAccountProvider } from './azureAccountProvider';
|
||||
import { AzureAccountProviderMetadata, CacheEncryptionKeys } from 'azurecore';
|
||||
@@ -46,8 +45,7 @@ export class AzureAccountProviderService implements vscode.Disposable {
|
||||
private _onEncryptionKeysUpdated: vscode.EventEmitter<CacheEncryptionKeys>;
|
||||
|
||||
constructor(private _context: vscode.ExtensionContext,
|
||||
private _userStoragePath: string,
|
||||
private _authLibrary: string) {
|
||||
private _userStoragePath: string) {
|
||||
this._onEncryptionKeysUpdated = new vscode.EventEmitter<CacheEncryptionKeys>();
|
||||
this._disposables.push(vscode.window.registerUriHandler(this._uriEventHandler));
|
||||
}
|
||||
@@ -163,8 +161,6 @@ 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 tokenCacheKey = `azureTokenCache-${provider.metadata.id}`;
|
||||
const tokenCacheKeyMsal = Constants.MSALCacheName;
|
||||
await this.clearOldCacheIfExists();
|
||||
try {
|
||||
@@ -172,18 +168,10 @@ export class AzureAccountProviderService implements vscode.Disposable {
|
||||
throw new Error('Credential provider not registered');
|
||||
}
|
||||
|
||||
// ADAL Token Cache
|
||||
let simpleTokenCache = new SimpleTokenCache(tokenCacheKey, this._userStoragePath, noSystemKeychain, this._credentialProvider);
|
||||
if (this._authLibrary === Constants.AuthLibrary.ADAL) {
|
||||
await simpleTokenCache.init();
|
||||
}
|
||||
|
||||
// MSAL Cache Plugin
|
||||
this._cachePluginProvider = new MsalCachePluginProvider(tokenCacheKeyMsal, this._userStoragePath, this._credentialProvider, this._onEncryptionKeysUpdated);
|
||||
if (this._authLibrary === Constants.AuthLibrary.MSAL) {
|
||||
// Initialize cache provider and encryption keys
|
||||
await this._cachePluginProvider.init();
|
||||
}
|
||||
// Initialize cache provider and encryption keys
|
||||
await this._cachePluginProvider.init();
|
||||
|
||||
const msalConfiguration: Configuration = {
|
||||
auth: {
|
||||
@@ -204,8 +192,8 @@ export class AzureAccountProviderService implements vscode.Disposable {
|
||||
|
||||
this.clientApplication = new PublicClientApplication(msalConfiguration);
|
||||
let accountProvider = new AzureAccountProvider(provider.metadata as AzureAccountProviderMetadata,
|
||||
simpleTokenCache, this._context, this.clientApplication, this._cachePluginProvider,
|
||||
this._uriEventHandler, this._authLibrary, isSaw);
|
||||
this._context, this.clientApplication, this._cachePluginProvider,
|
||||
this._uriEventHandler, isSaw);
|
||||
this._accountProviders[provider.metadata.id] = accountProvider;
|
||||
this._accountDisposals[provider.metadata.id] = azdata.accounts.registerAccountProvider(provider.metadata, accountProvider);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { promises as fs, constants as fsConstants } from 'fs';
|
||||
import { Logger } from '../../utils/Logger';
|
||||
|
||||
export type ReadWriteHook = (contents: string, resetOnError?: boolean) => Promise<string>;
|
||||
const noOpHook: ReadWriteHook = async (contents): Promise<string> => {
|
||||
return contents;
|
||||
};
|
||||
|
||||
export class AlreadyInitializedError extends Error {
|
||||
|
||||
}
|
||||
|
||||
type DbMap = { [key: string]: string };
|
||||
export class FileDatabase {
|
||||
private db: DbMap = {};
|
||||
private isDirty = false;
|
||||
private isSaving = false;
|
||||
private isInitialized = false;
|
||||
private saveInterval: NodeJS.Timer | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly dbPath: string,
|
||||
private readHook: ReadWriteHook = noOpHook,
|
||||
private writeHook: ReadWriteHook = noOpHook
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new read hook. Throws AlreadyInitializedError if the database has already started.
|
||||
* @param hook
|
||||
*/
|
||||
public setReadHook(hook: ReadWriteHook): void {
|
||||
if (this.isInitialized) {
|
||||
throw new AlreadyInitializedError();
|
||||
}
|
||||
this.readHook = hook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new write hook.
|
||||
* @param hook
|
||||
*/
|
||||
public setWriteHook(hook: ReadWriteHook): void {
|
||||
this.writeHook = hook;
|
||||
}
|
||||
|
||||
public async set(key: string, value: string): Promise<void> {
|
||||
await this.waitForFileSave();
|
||||
this.db[key] = value;
|
||||
this.isDirty = true;
|
||||
}
|
||||
|
||||
public get(key: string): string {
|
||||
return this.db[key];
|
||||
}
|
||||
|
||||
public async delete(key: string): Promise<void> {
|
||||
await this.waitForFileSave();
|
||||
delete this.db[key];
|
||||
this.isDirty = true;
|
||||
}
|
||||
|
||||
public async clear(): Promise<void> {
|
||||
await this.waitForFileSave();
|
||||
this.db = {};
|
||||
this.isDirty = true;
|
||||
}
|
||||
|
||||
public getPrefix(keyPrefix: string): { key: string, value: string }[] {
|
||||
return Object.entries(this.db).filter(([key]) => {
|
||||
return key.startsWith(keyPrefix);
|
||||
}).map(([key, value]) => {
|
||||
return { key, value };
|
||||
});
|
||||
}
|
||||
|
||||
public async deletePrefix(keyPrefix: string): Promise<void> {
|
||||
await this.waitForFileSave();
|
||||
Object.keys(this.db).forEach(s => {
|
||||
if (s.startsWith(keyPrefix)) {
|
||||
delete this.db[s];
|
||||
}
|
||||
});
|
||||
this.isDirty = true;
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
this.isInitialized = true;
|
||||
this.saveInterval = setInterval(() => this.save(), 20 * 1000);
|
||||
let fileContents: string;
|
||||
try {
|
||||
await fs.access(this.dbPath, fsConstants.R_OK | fsConstants.R_OK);
|
||||
fileContents = await fs.readFile(this.dbPath, { encoding: 'utf8' });
|
||||
fileContents = await this.readHook(fileContents, true);
|
||||
} catch (ex) {
|
||||
Logger.error(`Error occurred when initializing File Database from file system cache, ADAL cache will be reset: ${ex}`);
|
||||
await this.createFile();
|
||||
this.db = {};
|
||||
this.isDirty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.db = JSON.parse(fileContents) as DbMap;
|
||||
} catch (ex) {
|
||||
Logger.error(`Error occurred when reading file database contents as JSON, ADAL cache will be reset: ${ex}`);
|
||||
await this.createFile();
|
||||
this.db = {};
|
||||
}
|
||||
}
|
||||
|
||||
public async shutdown(): Promise<void> {
|
||||
await this.waitForFileSave();
|
||||
if (this.saveInterval) {
|
||||
clearInterval(this.saveInterval);
|
||||
}
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* This doesn't need to be called as a timer will automatically call it.
|
||||
*/
|
||||
public async save(): Promise<void> {
|
||||
try {
|
||||
await this.waitForFileSave();
|
||||
if (this.isDirty === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSaving = true;
|
||||
let contents = JSON.stringify(this.db);
|
||||
contents = await this.writeHook(contents);
|
||||
|
||||
await fs.writeFile(this.dbPath, contents, { encoding: 'utf8' });
|
||||
|
||||
this.isDirty = false;
|
||||
} catch (ex) {
|
||||
Logger.error(`Error occurred while saving cache contents to file storage, this may cause issues with ADAL cache persistence: ${ex}`);
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForFileSave(): Promise<void> {
|
||||
const cleanupCrew: NodeJS.Timer[] = [];
|
||||
|
||||
const sleepToFail = (time: number): Promise<void> => {
|
||||
return new Promise((_, reject) => {
|
||||
const timeout = setTimeout(reject, time);
|
||||
cleanupCrew.push(timeout);
|
||||
});
|
||||
};
|
||||
|
||||
const poll = (func: () => boolean): Promise<void> => {
|
||||
return new Promise(resolve => {
|
||||
const interval = setInterval(() => {
|
||||
if (func() === true) {
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
cleanupCrew.push(interval);
|
||||
});
|
||||
};
|
||||
|
||||
if (this.isSaving) {
|
||||
const timeout = sleepToFail(5 * 1000);
|
||||
const check = poll(() => !this.isSaving);
|
||||
|
||||
try {
|
||||
return await Promise.race([timeout, check]);
|
||||
} catch (ex) {
|
||||
throw new Error('Save timed out');
|
||||
} finally {
|
||||
cleanupCrew.forEach(clearInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async createFile(): Promise<void> {
|
||||
return fs.writeFile(this.dbPath, '', { encoding: 'utf8' });
|
||||
}
|
||||
}
|
||||
@@ -6,21 +6,19 @@ import * as azdata from 'azdata';
|
||||
import * as os from 'os';
|
||||
import * as crypto from 'crypto';
|
||||
import * as vscode from 'vscode';
|
||||
import { AuthLibrary } from '../../constants';
|
||||
import * as LocalizedConstants from '../../localizedConstants';
|
||||
import { Logger } from '../../utils/Logger';
|
||||
import { CacheEncryptionKeys } from 'azurecore';
|
||||
|
||||
export class FileEncryptionHelper {
|
||||
constructor(
|
||||
private readonly _authLibrary: AuthLibrary,
|
||||
private readonly _credentialService: azdata.CredentialProvider,
|
||||
protected readonly _fileName: string,
|
||||
private readonly _onEncryptionKeysUpdated?: vscode.EventEmitter<CacheEncryptionKeys>
|
||||
) {
|
||||
this._algorithm = this._authLibrary === AuthLibrary.MSAL ? 'aes-256-cbc' : 'aes-256-gcm';
|
||||
this._bufferEncoding = this._authLibrary === AuthLibrary.MSAL ? 'utf16le' : 'hex';
|
||||
this._binaryEncoding = this._authLibrary === AuthLibrary.MSAL ? 'base64' : 'hex';
|
||||
this._algorithm = 'aes-256-cbc';
|
||||
this._bufferEncoding = 'utf16le';
|
||||
this._binaryEncoding = 'base64';
|
||||
}
|
||||
|
||||
private _algorithm: string;
|
||||
@@ -51,7 +49,7 @@ export class FileEncryptionHelper {
|
||||
}
|
||||
|
||||
// Emit event with cache encryption keys to send notification to provider services.
|
||||
if (this._authLibrary === AuthLibrary.MSAL && this._onEncryptionKeysUpdated) {
|
||||
if (this._onEncryptionKeysUpdated) {
|
||||
this._onEncryptionKeysUpdated.fire(this.getEncryptionKeys());
|
||||
Logger.verbose('FileEncryptionHelper: Fired encryption keys updated event.');
|
||||
}
|
||||
@@ -73,9 +71,6 @@ export class FileEncryptionHelper {
|
||||
}
|
||||
const cipherIv = crypto.createCipheriv(this._algorithm, this._keyBuffer!, this._ivBuffer!);
|
||||
let cipherText = `${cipherIv.update(content, 'utf8', this._binaryEncoding)}${cipherIv.final(this._binaryEncoding)}`;
|
||||
if (this._authLibrary === AuthLibrary.ADAL) {
|
||||
cipherText += `%${(cipherIv as crypto.CipherGCM).getAuthTag().toString(this._binaryEncoding)}`;
|
||||
}
|
||||
return cipherText;
|
||||
}
|
||||
|
||||
@@ -86,14 +81,6 @@ export class FileEncryptionHelper {
|
||||
}
|
||||
let plaintext = content;
|
||||
const decipherIv = crypto.createDecipheriv(this._algorithm, this._keyBuffer!, this._ivBuffer!);
|
||||
if (this._authLibrary === AuthLibrary.ADAL) {
|
||||
const split = content.split('%');
|
||||
if (split.length !== 2) {
|
||||
throw new Error('File didn\'t contain the auth tag.');
|
||||
}
|
||||
(decipherIv as crypto.DecipherGCM).setAuthTag(Buffer.from(split[1], this._binaryEncoding));
|
||||
plaintext = split[0];
|
||||
}
|
||||
return `${decipherIv.update(plaintext, this._binaryEncoding, 'utf8')}${decipherIv.final('utf8')}`;
|
||||
} catch (ex) {
|
||||
Logger.error(`FileEncryptionHelper: Error occurred when decrypting data, IV/KEY will be reset: ${ex}`);
|
||||
@@ -130,7 +117,7 @@ export class FileEncryptionHelper {
|
||||
.then((result) => {
|
||||
status = result;
|
||||
if (result) {
|
||||
Logger.info(`FileEncryptionHelper: Successfully saved encryption key ${credentialId} for ${this._authLibrary} persistent cache encryption in system credential store.`);
|
||||
Logger.info(`FileEncryptionHelper: Successfully saved encryption key ${credentialId} persistent cache encryption in system credential store.`);
|
||||
}
|
||||
}, (e => {
|
||||
throw Error(`FileEncryptionHelper: Could not save encryption key: ${credentialId}: ${e}`);
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as lockFile from 'lockfile';
|
||||
import * as path from 'path';
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { AccountsClearTokenCacheCommand, AuthLibrary, LocalCacheSuffix, LockFileSuffix } from '../../constants';
|
||||
import { AccountsClearTokenCacheCommand, LocalCacheSuffix, LockFileSuffix } from '../../constants';
|
||||
import { Logger } from '../../utils/Logger';
|
||||
import { FileEncryptionHelper } from './fileEncryptionHelper';
|
||||
import { CacheEncryptionKeys } from 'azurecore';
|
||||
@@ -34,7 +34,7 @@ export class MsalCachePluginProvider {
|
||||
private readonly _credentialService: azdata.CredentialProvider,
|
||||
private readonly _onEncryptionKeysUpdated: vscode.EventEmitter<CacheEncryptionKeys>
|
||||
) {
|
||||
this._fileEncryptionHelper = new FileEncryptionHelper(AuthLibrary.MSAL, this._credentialService, this._serviceName, this._onEncryptionKeysUpdated);
|
||||
this._fileEncryptionHelper = new FileEncryptionHelper(this._credentialService, this._serviceName, this._onEncryptionKeysUpdated);
|
||||
this._msalCacheConfiguration = {
|
||||
name: 'MSAL',
|
||||
cacheFilePath: path.join(msalFilePath, this._serviceName),
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as keytarType from 'keytar';
|
||||
import { join, parse } from 'path';
|
||||
import { FileDatabase } from './fileDatabase';
|
||||
import * as azdata from 'azdata';
|
||||
import { FileEncryptionHelper } from './fileEncryptionHelper';
|
||||
import { AuthLibrary } from '../../constants';
|
||||
|
||||
function getSystemKeytar(): Keytar | undefined {
|
||||
try {
|
||||
return require('keytar');
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export type MultipleAccountsResponse = { account: string, password: string }[];
|
||||
|
||||
// allow-any-unicode-next-line
|
||||
const separator = '§';
|
||||
|
||||
async function getFileKeytar(filePath: string, credentialService: azdata.CredentialProvider): Promise<Keytar | undefined> {
|
||||
const fileName = parse(filePath).base;
|
||||
const fileEncryptionHelper: FileEncryptionHelper = new FileEncryptionHelper(AuthLibrary.ADAL, credentialService, fileName);
|
||||
const db = new FileDatabase(filePath, fileEncryptionHelper.fileOpener, fileEncryptionHelper.fileSaver);
|
||||
await db.initialize();
|
||||
|
||||
const fileKeytar: Keytar = {
|
||||
async getPassword(service: string, account: string): Promise<string> {
|
||||
return db.get(`${service}${separator}${account}`);
|
||||
},
|
||||
|
||||
async setPassword(service: string, account: string, password: string): Promise<void> {
|
||||
await db.set(`${service}${separator}${account}`, password);
|
||||
},
|
||||
|
||||
async deletePassword(service: string, account: string): Promise<boolean> {
|
||||
await db.delete(`${service}${separator}${account}`);
|
||||
return true;
|
||||
},
|
||||
|
||||
async getPasswords(service: string): Promise<MultipleAccountsResponse> {
|
||||
const result = db.getPrefix(`${service}`);
|
||||
if (!result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.map(({ key, value }) => {
|
||||
return {
|
||||
account: key.split(separator)[1],
|
||||
password: value
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
return fileKeytar;
|
||||
}
|
||||
|
||||
|
||||
export type Keytar = {
|
||||
getPassword: typeof keytarType['getPassword'];
|
||||
setPassword: typeof keytarType['setPassword'];
|
||||
deletePassword: typeof keytarType['deletePassword'];
|
||||
getPasswords: (service: string) => Promise<MultipleAccountsResponse>;
|
||||
findCredentials?: typeof keytarType['findCredentials'];
|
||||
};
|
||||
|
||||
export class SimpleTokenCache {
|
||||
private keytar: Keytar | undefined;
|
||||
|
||||
constructor(
|
||||
private serviceName: string,
|
||||
private readonly userStoragePath: string,
|
||||
private readonly forceFileStorage: boolean = false,
|
||||
private readonly credentialService: azdata.CredentialProvider,
|
||||
) { }
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.serviceName = this.serviceName.replace(/-/g, '_');
|
||||
let keytar: Keytar | undefined;
|
||||
if (this.forceFileStorage === false) {
|
||||
keytar = getSystemKeytar();
|
||||
|
||||
// Add new method to keytar
|
||||
if (keytar) {
|
||||
keytar.getPasswords = async (service: string): Promise<MultipleAccountsResponse> => {
|
||||
const [serviceName, accountPrefix] = service.split(separator);
|
||||
if (serviceName === undefined || accountPrefix === undefined) {
|
||||
throw new Error('Service did not have separator: ' + service);
|
||||
}
|
||||
|
||||
const results = await keytar!.findCredentials!(serviceName);
|
||||
return results.filter(({ account }) => {
|
||||
return account.startsWith(accountPrefix);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!keytar) {
|
||||
keytar = await getFileKeytar(join(this.userStoragePath, this.serviceName), this.credentialService);
|
||||
}
|
||||
this.keytar = keytar;
|
||||
}
|
||||
|
||||
async saveCredential(id: string, key: string): Promise<void> {
|
||||
if (!this.forceFileStorage && key.length > 2500) { // Windows limitation
|
||||
throw new Error('Key length is longer than 2500 chars');
|
||||
}
|
||||
|
||||
if (id.includes(separator)) {
|
||||
throw new Error('Separator included in ID');
|
||||
}
|
||||
|
||||
try {
|
||||
const keytar = this.getKeytar();
|
||||
return await keytar.setPassword(this.serviceName, id, key);
|
||||
} catch (ex) {
|
||||
console.warn(`Adding key failed: ${ex}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getCredential(id: string): Promise<string | undefined> {
|
||||
try {
|
||||
const keytar = this.getKeytar();
|
||||
const result = await keytar.getPassword(this.serviceName, id);
|
||||
|
||||
if (result === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (ex) {
|
||||
console.warn(`Getting key failed: ${ex}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async clearCredential(id: string): Promise<boolean> {
|
||||
try {
|
||||
const keytar = this.getKeytar();
|
||||
return await keytar.deletePassword(this.serviceName, id);
|
||||
} catch (ex) {
|
||||
console.warn(`Clearing key failed: ${ex}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async findCredentials(prefix: string): Promise<{ account: string, password: string }[]> {
|
||||
try {
|
||||
const keytar = this.getKeytar();
|
||||
return await keytar.getPasswords(`${this.serviceName}${separator}${prefix}`);
|
||||
} catch (ex) {
|
||||
console.warn(`Finding credentials failed: ${ex}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private getKeytar(): Keytar {
|
||||
if (!this.keytar) {
|
||||
throw new Error('Keytar not initialized');
|
||||
}
|
||||
return this.keytar;
|
||||
}
|
||||
}
|
||||
@@ -28,15 +28,14 @@ const typesClause = [
|
||||
].map(type => `type == "${type}"`).join(' or ');
|
||||
|
||||
export class AzureDataGridProvider implements azdata.DataGridProvider {
|
||||
constructor(private _appContext: AppContext,
|
||||
private readonly authLibrary: string) { }
|
||||
constructor(private _appContext: AppContext) { }
|
||||
|
||||
public providerId = constants.dataGridProviderId;
|
||||
public title = loc.azureResourcesGridTitle;
|
||||
|
||||
public async getDataGridItems() {
|
||||
let accounts: azdata.Account[];
|
||||
accounts = azureResourceUtils.filterAccounts(await azdata.accounts.getAllAccounts(), this.authLibrary);
|
||||
accounts = await azdata.accounts.getAllAccounts();
|
||||
const items: any[] = [];
|
||||
await Promise.all(accounts.map(async (account) => {
|
||||
await Promise.all(account.properties.tenants.map(async (tenant: { id: string; }) => {
|
||||
|
||||
@@ -17,11 +17,11 @@ import { AzureResourceServiceNames } from './constants';
|
||||
import { AzureAccount, Tenant, azureResource } from 'azurecore';
|
||||
import { FlatTenantTreeNode } from './tree/flatTenantTreeNode';
|
||||
import { ConnectionDialogTreeProvider } from './tree/connectionDialogTreeProvider';
|
||||
import { AzureResourceErrorMessageUtil, filterAccounts } from './utils';
|
||||
import { AzureResourceErrorMessageUtil } from './utils';
|
||||
import { AzureResourceTenantTreeNode } from './tree/tenantTreeNode';
|
||||
import { FlatAccountTreeNode } from './tree/flatAccountTreeNode';
|
||||
|
||||
export function registerAzureResourceCommands(appContext: AppContext, azureViewTree: AzureResourceTreeProvider, connectionDialogTree: ConnectionDialogTreeProvider, authLibrary: string): void {
|
||||
export function registerAzureResourceCommands(appContext: AppContext, azureViewTree: AzureResourceTreeProvider, connectionDialogTree: ConnectionDialogTreeProvider): void {
|
||||
const trees = [azureViewTree, connectionDialogTree];
|
||||
vscode.commands.registerCommand('azure.resource.startterminal', async (node?: TreeNode) => {
|
||||
try {
|
||||
@@ -35,7 +35,7 @@ export function registerAzureResourceCommands(appContext: AppContext, azureViewT
|
||||
if (node instanceof AzureResourceAccountTreeNode) {
|
||||
azureAccount = node.account;
|
||||
} else {
|
||||
let accounts = filterAccounts(await azdata.accounts.getAllAccounts(), authLibrary);
|
||||
let accounts = await azdata.accounts.getAllAccounts();
|
||||
accounts = accounts.filter(a => a.key.providerId.startsWith('azure'));
|
||||
if (accounts.length === 0) {
|
||||
const signin = localize('azure.signIn', "Sign in");
|
||||
|
||||
@@ -13,7 +13,7 @@ import { TreeNode } from '../treeNode';
|
||||
import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode';
|
||||
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
|
||||
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
|
||||
import { AzureResourceErrorMessageUtil, equals, filterAccounts } from '../utils';
|
||||
import { AzureResourceErrorMessageUtil, equals } from '../utils';
|
||||
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
|
||||
import { Logger } from '../../utils/Logger';
|
||||
import { AzureAccount } from 'azurecore';
|
||||
@@ -26,11 +26,10 @@ 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,
|
||||
private readonly authLibrary: string) {
|
||||
public constructor(private readonly appContext: AppContext) {
|
||||
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 = filterAccounts(await azdata.accounts.getAllAccounts(), authLibrary);
|
||||
let accounts = await azdata.accounts.getAllAccounts();
|
||||
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,11 +55,10 @@ 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 accounts) {
|
||||
for (const account of this.accounts) {
|
||||
try {
|
||||
const accountNode = new FlatAccountTreeNode(account, this.appContext, this);
|
||||
accountNode.refreshLabel();
|
||||
@@ -87,7 +85,7 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider<Tre
|
||||
|
||||
private async loadAccounts(): Promise<void> {
|
||||
try {
|
||||
this.accounts = filterAccounts(await azdata.accounts.getAllAccounts(), this.authLibrary);
|
||||
this.accounts = await azdata.accounts.getAllAccounts();
|
||||
// System has been initialized
|
||||
this.setSystemInitialized();
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { AzureResourceAccountTreeNode } from './accountTreeNode';
|
||||
import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode';
|
||||
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
|
||||
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
|
||||
import { AzureResourceErrorMessageUtil, equals, filterAccounts } from '../utils';
|
||||
import { AzureResourceErrorMessageUtil, equals } from '../utils';
|
||||
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
|
||||
import { AzureAccount } from 'azurecore';
|
||||
|
||||
@@ -25,11 +25,10 @@ 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,
|
||||
private readonly authLibrary: string) {
|
||||
public constructor(private readonly appContext: AppContext) {
|
||||
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 = filterAccounts(await azdata.accounts.getAllAccounts(), authLibrary);
|
||||
let accounts = await azdata.accounts.getAllAccounts();
|
||||
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
|
||||
@@ -59,7 +58,6 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
|
||||
if (this.accounts.length === 0) {
|
||||
return [new AzureResourceAccountNotSignedInTreeNode()];
|
||||
} else {
|
||||
this.accounts = filterAccounts(this.accounts, this.authLibrary);
|
||||
return this.accounts.map((account) => new AzureResourceAccountTreeNode(account, this.appContext, this));
|
||||
}
|
||||
} else {
|
||||
@@ -72,10 +70,7 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
|
||||
|
||||
private async loadAccounts(): Promise<void> {
|
||||
try {
|
||||
let accounts = await azdata.accounts.getAllAccounts();
|
||||
if (accounts) {
|
||||
this.accounts = filterAccounts(accounts, this.authLibrary);
|
||||
}
|
||||
this.accounts = await azdata.accounts.getAllAccounts();
|
||||
// System has been initialized
|
||||
this.setSystemInitialized();
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
|
||||
@@ -674,18 +674,3 @@ 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;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ export const AccountsSection = 'accounts';
|
||||
|
||||
export const AuthSection = 'auth';
|
||||
|
||||
export const AuthenticationLibrarySection = 'authenticationLibrary';
|
||||
|
||||
export const AzureSection = 'azure';
|
||||
|
||||
export const AzureAccountProviderCredentials = 'azureAccountProviderCredentials';
|
||||
@@ -27,8 +25,6 @@ export const AccountsAzureAuthSection = AccountsSection + '.' + AzureSection + '
|
||||
|
||||
export const AccountsAzureCloudSection = AccountsSection + '.' + AzureSection + '.' + CloudSection;
|
||||
|
||||
export const AzureAuthenticationLibrarySection = AzureSection + '.' + AuthenticationLibrarySection;
|
||||
|
||||
export const EnableArcFeaturesSection = 'enableArcFeatures';
|
||||
|
||||
export const ServiceName = 'azuredatastudio';
|
||||
@@ -78,8 +74,6 @@ export const AzureTokenFolderName = 'Azure Accounts';
|
||||
|
||||
export const MSALCacheName = 'accessTokenCache';
|
||||
|
||||
export const DefaultAuthLibrary = 'MSAL';
|
||||
|
||||
export const LocalCacheSuffix = '.local';
|
||||
|
||||
export const LockFileSuffix = '.lockfile';
|
||||
@@ -109,14 +103,6 @@ export enum BuiltInCommands {
|
||||
SetContext = 'setContext'
|
||||
}
|
||||
|
||||
/**
|
||||
* AAD Auth library as selected.
|
||||
*/
|
||||
export enum AuthLibrary {
|
||||
MSAL = 'MSAL',
|
||||
ADAL = 'ADAL'
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication type as selected.
|
||||
*/
|
||||
|
||||
@@ -73,16 +73,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
|
||||
await config.update('deviceCode', true, vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
|
||||
const authLibrary: string = vscode.workspace.getConfiguration(Constants.AzureSection).get(Constants.AuthenticationLibrarySection)
|
||||
?? Constants.DefaultAuthLibrary;
|
||||
|
||||
if (authLibrary !== Constants.DefaultAuthLibrary) {
|
||||
void vscode.window.showWarningMessage(loc.deprecatedOption, loc.switchMsal, loc.dismiss).then(async (value) => {
|
||||
if (value === loc.switchMsal) {
|
||||
await vscode.workspace.getConfiguration(Constants.AzureSection).update(Constants.AuthenticationLibrarySection, Constants.DefaultAuthLibrary, vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
});
|
||||
}
|
||||
const piiLogging = vscode.workspace.getConfiguration(Constants.AzureSection).get(Constants.piiLogging, false)
|
||||
if (piiLogging) {
|
||||
void vscode.window.showWarningMessage(loc.piiWarning, loc.disable, loc.dismiss).then(async (value) => {
|
||||
@@ -96,18 +86,18 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
|
||||
|
||||
let eventEmitter: vscode.EventEmitter<azurecore.CacheEncryptionKeys>;
|
||||
// Create the provider service and activate
|
||||
let providerService = await initAzureAccountProvider(extensionContext, storagePath, authLibrary!).catch((err) => Logger.error(err));
|
||||
let providerService = await initAzureAccountProvider(extensionContext, storagePath).catch((err) => Logger.error(err));
|
||||
if (providerService) {
|
||||
eventEmitter = providerService.getEncryptionKeysEmitter();
|
||||
|
||||
registerAzureServices(appContext);
|
||||
const azureResourceTree = new AzureResourceTreeProvider(appContext, authLibrary);
|
||||
const connectionDialogTree = new ConnectionDialogTreeProvider(appContext, authLibrary);
|
||||
const azureResourceTree = new AzureResourceTreeProvider(appContext);
|
||||
const connectionDialogTree = new ConnectionDialogTreeProvider(appContext);
|
||||
pushDisposable(vscode.window.registerTreeDataProvider('azureResourceExplorer', azureResourceTree));
|
||||
pushDisposable(vscode.window.registerTreeDataProvider('connectionDialog/azureResourceExplorer', connectionDialogTree));
|
||||
pushDisposable(vscode.workspace.onDidChangeConfiguration(e => onDidChangeConfiguration(e)));
|
||||
registerAzureResourceCommands(appContext, azureResourceTree, connectionDialogTree, authLibrary);
|
||||
azdata.dataprotocol.registerDataGridProvider(new AzureDataGridProvider(appContext, authLibrary));
|
||||
registerAzureResourceCommands(appContext, azureResourceTree, connectionDialogTree);
|
||||
azdata.dataprotocol.registerDataGridProvider(new AzureDataGridProvider(appContext));
|
||||
vscode.commands.registerCommand('azure.dataGrid.openInAzurePortal', async (item: azdata.DataGridItem) => {
|
||||
const portalEndpoint = item.portalEndpoint;
|
||||
const subscriptionId = item.subscriptionId;
|
||||
@@ -263,9 +253,9 @@ async function findOrMakeStoragePath() {
|
||||
return storagePath;
|
||||
}
|
||||
|
||||
async function initAzureAccountProvider(extensionContext: vscode.ExtensionContext, storagePath: string, authLibrary: string): Promise<AzureAccountProviderService | undefined> {
|
||||
async function initAzureAccountProvider(extensionContext: vscode.ExtensionContext, storagePath: string): Promise<AzureAccountProviderService | undefined> {
|
||||
try {
|
||||
const accountProviderService = new AzureAccountProviderService(extensionContext, storagePath, authLibrary);
|
||||
const accountProviderService = new AzureAccountProviderService(extensionContext, storagePath);
|
||||
extensionContext.subscriptions.push(accountProviderService);
|
||||
await accountProviderService.activate();
|
||||
return accountProviderService;
|
||||
@@ -289,12 +279,6 @@ async function onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent): Pro
|
||||
if (e.affectsConfiguration('azure.piiLogging')) {
|
||||
updatePiiLoggingLevel();
|
||||
}
|
||||
if (e.affectsConfiguration('azure.authenticationLibrary')) {
|
||||
if (vscode.workspace.getConfiguration(Constants.AzureSection).get('authenticationLibrary') === 'ADAL') {
|
||||
void vscode.window.showInformationMessage(loc.deprecatedOption);
|
||||
}
|
||||
await utils.displayReloadAds('authenticationLibrary');
|
||||
}
|
||||
}
|
||||
|
||||
function updatePiiLoggingLevel(): void {
|
||||
|
||||
@@ -69,7 +69,6 @@ export function reloadPrompt(sectionName: string): string {
|
||||
export const reloadPromptCacheClear = localize('azurecore.reloadPromptCacheClear', "Token cache has been cleared successfully, please reload Azure Data Studio.");
|
||||
export const reloadChoice = localize('azurecore.reloadChoice', "Reload Azure Data Studio");
|
||||
|
||||
export const deprecatedOption = localize('azurecore.deprecated', "Warning: ADAL has been deprecated, and is scheduled to be removed in the next release. Please use MSAL instead.");
|
||||
export const piiWarning = localize('azurecore.piiLogging.warning', "Warning: Azure PII Logging is enabled. Enabling this option allows personally identifiable information to be logged and should only be used for debugging purposes.");
|
||||
export const disable = localize('azurecore.disable', 'Disable');
|
||||
export const dismiss = localize('azurecore.dismiss', 'Dismiss');
|
||||
|
||||
@@ -7,11 +7,11 @@ import * as should from 'should';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import 'mocha';
|
||||
import { AzureAuthCodeGrant } from '../../../account-provider/auths/azureAuthCodeGrant';
|
||||
import { Token, TokenClaims, AccessToken, RefreshToken, OAuthTokenResponse, TokenPostData } from '../../../account-provider/auths/azureAuth';
|
||||
import { Token, TokenClaims, AccessToken, RefreshToken } from '../../../account-provider/auths/azureAuth';
|
||||
import { Tenant, AzureAccount } from 'azurecore';
|
||||
import providerSettings from '../../../account-provider/providerSettings';
|
||||
import { AzureResource } from 'azdata';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { AuthenticationResult } from '@azure/msal-common';
|
||||
|
||||
let azureAuthCodeGrant: TypeMoq.IMock<AzureAuthCodeGrant>;
|
||||
// let azureDeviceCode: TypeMoq.IMock<AzureDeviceCode>;
|
||||
@@ -78,208 +78,30 @@ describe('Azure Authentication', function () {
|
||||
};
|
||||
});
|
||||
|
||||
it('accountHydration should yield a valid account', async function () {
|
||||
|
||||
azureAuthCodeGrant.setup(x => x.getTenantsAdal(mockToken)).returns((): Promise<Tenant[]> => {
|
||||
return Promise.resolve([
|
||||
mockTenant
|
||||
]);
|
||||
});
|
||||
|
||||
const response = await azureAuthCodeGrant.object.hydrateAccount(mockToken, mockClaims);
|
||||
should(response.displayInfo.displayName).be.equal(`${mockClaims.name} - ${mockClaims.email}`, 'Account name should match');
|
||||
should(response.displayInfo.userId).be.equal(mockClaims.sub, 'Account ID should match');
|
||||
should(response.properties.tenants).be.deepEqual([mockTenant], 'Tenants should match');
|
||||
});
|
||||
|
||||
describe('getAccountSecurityToken', function () {
|
||||
it('should be undefined on stale account', async function () {
|
||||
mockAccount.isStale = true;
|
||||
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.getAccountSecurityTokenAdal(mockAccount, TypeMoq.It.isAny(), <any>-1);
|
||||
should(securityToken).be.undefined();
|
||||
});
|
||||
it('incorrect tenant', async function () {
|
||||
await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, 'invalid_tenant', AzureResource.MicrosoftResourceManagement).should.be.rejected();
|
||||
});
|
||||
|
||||
it('token recieved for ossRdbmns resource', async function () {
|
||||
azureAuthCodeGrant.setup(x => x.getTenantsAdal(mockToken)).returns(() => {
|
||||
return Promise.resolve([
|
||||
mockTenant
|
||||
]);
|
||||
});
|
||||
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.refreshTokenAdal(mockTenant, provider.settings.ossRdbmsResource!, mockRefreshToken)).returns((): Promise<OAuthTokenResponse> => {
|
||||
const mockToken: AccessToken = JSON.parse(JSON.stringify(mockAccessToken)) as AccessToken;
|
||||
delete (mockToken as any).invalidData;
|
||||
return Promise.resolve({
|
||||
accessToken: mockToken
|
||||
} as OAuthTokenResponse);
|
||||
});
|
||||
|
||||
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,
|
||||
expiresOn: `${(new Date().getTime() / 1000) + (10 * 60)}`
|
||||
});
|
||||
});
|
||||
|
||||
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.getSavedTokenAdal(mockTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||
azureAuthCodeGrant.setup(x => x.getToken(mockAccount.key.accountId, AzureResource.MicrosoftResourceManagement, mockTenant.id)).returns((): Promise<AuthenticationResult> => {
|
||||
return Promise.resolve({
|
||||
accessToken: mockAccessToken,
|
||||
refreshToken: mockRefreshToken,
|
||||
expiresOn: `${(new Date().getTime() / 1000) + (10 * 60)}`
|
||||
authority: 'test',
|
||||
uniqueId: 'test',
|
||||
tenantId: 'test',
|
||||
scopes: ['test'],
|
||||
account: null,
|
||||
idToken: 'test',
|
||||
idTokenClaims: mockClaims,
|
||||
fromCache: false,
|
||||
tokenType: 'Bearer',
|
||||
correlationId: 'test',
|
||||
accessToken: mockAccessToken.token,
|
||||
refreshToken: mockRefreshToken.token,
|
||||
expiresOn: new Date(Date.now())
|
||||
});
|
||||
});
|
||||
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityTokenAdal(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
|
||||
const securityToken = await azureAuthCodeGrant.object.getToken(mockAccount.key.accountId, AzureResource.MicrosoftResourceManagement, mockTenant.id) as AuthenticationResult;
|
||||
|
||||
should(securityToken?.tokenType).be.equal('Bearer', 'tokenType should be bearer on a successful getSecurityToken from cache');
|
||||
});
|
||||
|
||||
|
||||
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.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.refreshTokenAdal(mockTenant, provider.settings.microsoftResource!, mockRefreshToken)).returns((): Promise<OAuthTokenResponse> => {
|
||||
const mockToken: AccessToken = JSON.parse(JSON.stringify(mockAccessToken)) as AccessToken;
|
||||
delete (mockToken as any).invalidData;
|
||||
return Promise.resolve({
|
||||
accessToken: mockToken
|
||||
} as OAuthTokenResponse);
|
||||
});
|
||||
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.refreshTokenAdal(mockTenant, provider.settings.microsoftResource!, mockRefreshToken), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
describe('no saved token', function () {
|
||||
it('no base token', async function () {
|
||||
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.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.getAccountSecurityTokenAdal(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement).should.be.rejected();
|
||||
});
|
||||
|
||||
it('base token exists', async function () {
|
||||
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.getSavedTokenAdal(azureAuthCodeGrant.object.commonTenant, provider.settings.microsoftResource!, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||
return Promise.resolve({
|
||||
accessToken: mockAccessToken,
|
||||
refreshToken: mockRefreshToken,
|
||||
expiresOn: ''
|
||||
});
|
||||
});
|
||||
delete (mockAccessToken as any).tokenType;
|
||||
|
||||
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.getAccountSecurityTokenAdal(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
|
||||
should(securityToken?.tokenType).be.equal('Bearer', 'tokenType should be bearer on a successful getSecurityToken from cache');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('getToken', function () {
|
||||
|
||||
it('calls handle interaction required', async function () {
|
||||
azureAuthCodeGrant.setup(x => x.makePostRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
error: 'interaction_required'
|
||||
}
|
||||
} as AxiosResponse<any>);
|
||||
});
|
||||
|
||||
azureAuthCodeGrant.setup(x => x.handleInteractionRequiredAdal(mockTenant, provider.settings.microsoftResource!)).returns(() => {
|
||||
return Promise.resolve({
|
||||
accessToken: mockAccessToken
|
||||
} as OAuthTokenResponse);
|
||||
});
|
||||
|
||||
|
||||
const result = await azureAuthCodeGrant.object.getTokenAdal(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData);
|
||||
|
||||
azureAuthCodeGrant.verify(x => x.handleInteractionRequiredAdal(mockTenant, provider.settings.microsoftResource!), TypeMoq.Times.once());
|
||||
|
||||
should(result?.accessToken).be.deepEqual(mockAccessToken);
|
||||
});
|
||||
|
||||
it('unknown error should throw error', async function () {
|
||||
azureAuthCodeGrant.setup(x => x.makePostRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
error: 'unknown error'
|
||||
}
|
||||
} as AxiosResponse<any>);
|
||||
});
|
||||
|
||||
await azureAuthCodeGrant.object.getTokenAdal(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData).should.be.rejected();
|
||||
});
|
||||
|
||||
it('calls getTokenHelper', async function () {
|
||||
azureAuthCodeGrant.setup(x => x.makePostRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
access_token: mockAccessToken.token,
|
||||
refresh_token: mockRefreshToken.token,
|
||||
expires_on: `0`
|
||||
}
|
||||
} as AxiosResponse<any>);
|
||||
});
|
||||
|
||||
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.getTokenAdal(mockTenant, provider.settings.microsoftResource!, {} as TokenPostData);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as should from 'should';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
import 'mocha';
|
||||
|
||||
import { FileDatabase } from '../../../account-provider/utils/fileDatabase';
|
||||
|
||||
let fileDatabase: FileDatabase;
|
||||
let fileName: string;
|
||||
|
||||
const k1 = 'k1';
|
||||
const v1 = 'v1';
|
||||
|
||||
const k2 = 'k2';
|
||||
const v2 = 'v2';
|
||||
|
||||
const fakeDB = {
|
||||
k1: v1,
|
||||
k2: v2
|
||||
};
|
||||
|
||||
// These tests don't work on Linux systems because gnome-keyring doesn't like running on headless machines.
|
||||
describe('AccountProvider.FileDatabase', function (): void {
|
||||
beforeEach(async function (): Promise<void> {
|
||||
fileName = crypto.randomBytes(4).toString('hex');
|
||||
fileDatabase = new FileDatabase(path.join(os.tmpdir(), fileName));
|
||||
});
|
||||
|
||||
it('set, get, and clear', async function (): Promise<void> {
|
||||
await fileDatabase.initialize();
|
||||
await fileDatabase.set(k1, v1);
|
||||
|
||||
let x = fileDatabase.get(k1);
|
||||
should(x).be.equal(v1);
|
||||
|
||||
await fileDatabase.clear();
|
||||
|
||||
x = fileDatabase.get(k1);
|
||||
should(x).be.undefined();
|
||||
});
|
||||
|
||||
it('read the file contents', async function (): Promise<void> {
|
||||
await fileDatabase.initialize();
|
||||
await fileDatabase.set(k1, v1);
|
||||
|
||||
let x = fileDatabase.get(k1);
|
||||
should(x).be.equal(v1);
|
||||
|
||||
await fileDatabase.shutdown();
|
||||
const data = await fs.readFile(path.join(os.tmpdir(), fileName));
|
||||
|
||||
should(data.toString()).be.equal(JSON.stringify({ k1: v1 }));
|
||||
});
|
||||
|
||||
it('delete prefix', async function (): Promise<void> {
|
||||
await fileDatabase.initialize();
|
||||
await Promise.all([fileDatabase.set(k1, v1), fileDatabase.set(k2, v2)]);
|
||||
|
||||
let x = fileDatabase.get(k1);
|
||||
should(x).be.equal(v1);
|
||||
|
||||
x = fileDatabase.get(k2);
|
||||
should(x).be.equal(v2);
|
||||
|
||||
await fileDatabase.deletePrefix('k');
|
||||
|
||||
x = fileDatabase.get(k1);
|
||||
should(x).be.undefined();
|
||||
});
|
||||
|
||||
it('Test write hook', async function (): Promise<void> {
|
||||
fileDatabase.setWriteHook(async (contents): Promise<string> => {
|
||||
should(contents).be.equal(JSON.stringify(fakeDB));
|
||||
return contents;
|
||||
});
|
||||
|
||||
await fileDatabase.initialize();
|
||||
await fileDatabase.set(k1, v1);
|
||||
await fileDatabase.set(k2, v2);
|
||||
await fileDatabase.save();
|
||||
});
|
||||
|
||||
it('Test read hook', async function (): Promise<void> {
|
||||
fileDatabase.setReadHook(async (contents): Promise<string> => {
|
||||
should(contents).be.equal(JSON.stringify(fakeDB));
|
||||
return contents;
|
||||
});
|
||||
await fs.writeFile(path.join(os.tmpdir(), fileName), JSON.stringify(fakeDB));
|
||||
await fileDatabase.initialize();
|
||||
});
|
||||
});
|
||||
@@ -26,42 +26,10 @@ let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
|
||||
let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>;
|
||||
|
||||
// Mock test data
|
||||
const mockAccountAdal1: AzureAccount = {
|
||||
key: {
|
||||
accountId: 'mock_account_1',
|
||||
providerId: 'mock_provider',
|
||||
authLibrary: 'ADAL'
|
||||
},
|
||||
displayInfo: {
|
||||
displayName: 'mock_account_1@test.com',
|
||||
accountType: 'Microsoft',
|
||||
contextualDisplayName: 'test',
|
||||
userId: 'test@email.com'
|
||||
},
|
||||
properties: TypeMoq.Mock.ofType<AzureAccountProperties>().object,
|
||||
isStale: false
|
||||
};
|
||||
const mockAccountAdal2: AzureAccount = {
|
||||
key: {
|
||||
accountId: 'mock_account_2',
|
||||
providerId: 'mock_provider'
|
||||
},
|
||||
displayInfo: {
|
||||
displayName: 'mock_account_2@test.com',
|
||||
accountType: 'Microsoft',
|
||||
contextualDisplayName: 'test',
|
||||
userId: 'test@email.com'
|
||||
},
|
||||
properties: TypeMoq.Mock.ofType<AzureAccountProperties>().object,
|
||||
isStale: false
|
||||
};
|
||||
const mockAccountsADAL = [mockAccountAdal1, mockAccountAdal2];
|
||||
|
||||
const mockAccountMsal1: AzureAccount = {
|
||||
key: {
|
||||
accountId: 'mock_account_1',
|
||||
providerId: 'mock_provider',
|
||||
authLibrary: 'MSAL'
|
||||
providerId: 'mock_provider'
|
||||
},
|
||||
displayInfo: {
|
||||
displayName: 'mock_account_1@test.com',
|
||||
@@ -75,8 +43,7 @@ const mockAccountMsal1: AzureAccount = {
|
||||
const mockAccountMsal2: AzureAccount = {
|
||||
key: {
|
||||
accountId: 'mock_account_2',
|
||||
providerId: 'mock_provider',
|
||||
authLibrary: 'MSAL'
|
||||
providerId: 'mock_provider'
|
||||
},
|
||||
displayInfo: {
|
||||
displayName: 'mock_account_2@test.com',
|
||||
@@ -105,31 +72,10 @@ describe('AzureResourceTreeProvider.getChildren', function (): void {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
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, '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(mockAccountsADAL.length);
|
||||
|
||||
for (let ix = 0; ix < mockAccountsADAL.length; ix++) {
|
||||
const child = children[ix];
|
||||
const account = mockAccountsADAL[ix];
|
||||
|
||||
should(child).instanceof(AzureResourceAccountTreeNode);
|
||||
should(child.nodePathValue).equal(`account_${account.key.accountId}`);
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
const treeProvider = new AzureResourceTreeProvider(mockAppContext);
|
||||
|
||||
await treeProvider.getChildren(undefined); // Load account promise
|
||||
const children = await treeProvider.getChildren(undefined); // Actual accounts
|
||||
@@ -147,23 +93,10 @@ describe('AzureResourceTreeProvider.getChildren', function (): void {
|
||||
}
|
||||
});
|
||||
|
||||
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, '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');
|
||||
const treeProvider = new AzureResourceTreeProvider(mockAppContext);
|
||||
treeProvider.isSystemInitialized = true;
|
||||
|
||||
const children = await treeProvider.getChildren(undefined);
|
||||
|
||||
@@ -8,35 +8,17 @@ import * as vscode from 'vscode';
|
||||
const mssqlExtensionConfigName = 'mssql';
|
||||
const enableSqlAuthenticationProviderConfig = 'enableSqlAuthenticationProvider';
|
||||
|
||||
const azureExtensionConfigName = 'azure';
|
||||
const azureAuthenticationLibraryConfig = 'authenticationLibrary';
|
||||
const MSAL = 'MSAL';
|
||||
|
||||
/**
|
||||
* @returns 'True' if MSAL auth library is in use and SQL Auth provider is enabled.
|
||||
* @returns 'True' if SQL Auth provider is enabled.
|
||||
*/
|
||||
export function isMssqlAuthProviderEnabled(): boolean {
|
||||
return getAzureAuthenticationLibraryConfig() === MSAL && getEnableSqlAuthenticationProviderConfig();
|
||||
return getEnableSqlAuthenticationProviderConfig();
|
||||
}
|
||||
|
||||
export function getConfiguration(config: string): vscode.WorkspaceConfiguration {
|
||||
return vscode.workspace.getConfiguration(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads setting 'azure.AuthenticationLibrary' and returns the library name enabled.
|
||||
* @returns MSAL | ADAL
|
||||
*/
|
||||
export function getAzureAuthenticationLibraryConfig(): string {
|
||||
const config = getConfiguration(azureExtensionConfigName);
|
||||
if (config) {
|
||||
return config.get<string>(azureAuthenticationLibraryConfig, MSAL); // default Auth library
|
||||
}
|
||||
else {
|
||||
return MSAL; // default Auth library
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads setting 'mssql.enableSqlAuthenticationProvider' and returns true if it's enabled.
|
||||
* @returns True Sql Auth provider is enabled for MSSQL provider.
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as path from 'path';
|
||||
import * as azurecore from 'azurecore';
|
||||
import { getAzureAuthenticationLibraryConfig, getCommonLaunchArgsAndCleanupOldLogFiles, getConfigTracingLevel, getEnableConnectionPoolingConfig, getEnableSqlAuthenticationProviderConfig, getOrDownloadServer, getParallelMessageProcessingConfig, logDebug, TracingLevel } from './utils';
|
||||
import { getCommonLaunchArgsAndCleanupOldLogFiles, getConfigTracingLevel, getEnableConnectionPoolingConfig, getEnableSqlAuthenticationProviderConfig, getOrDownloadServer, getParallelMessageProcessingConfig, logDebug, TracingLevel } from './utils';
|
||||
import { TelemetryReporter, LanguageClientErrorHandler } from './telemetry';
|
||||
import { SqlOpsDataClient, ClientOptions } from 'dataprotocol-client';
|
||||
import { TelemetryFeature, AgentServicesFeature, SerializationFeature, AccountFeature, SqlAssessmentServicesFeature, ProfilerFeature, TableDesignerFeature, ExecutionPlanServiceFeature } from './features';
|
||||
@@ -99,7 +99,7 @@ export class SqlToolsServer {
|
||||
* @param client SqlOpsDataClient instance
|
||||
*/
|
||||
private async handleEncryptionKeyEventNotification(client: SqlOpsDataClient) {
|
||||
if (getAzureAuthenticationLibraryConfig() === 'MSAL' && getEnableSqlAuthenticationProviderConfig()) {
|
||||
if (getEnableSqlAuthenticationProviderConfig()) {
|
||||
let azureCoreApi = await this.getAzureCoreAPI();
|
||||
let onDidEncryptionKeysChanged = azureCoreApi.onEncryptionKeysUpdated;
|
||||
// Register event listener from Azure Core extension and
|
||||
@@ -163,8 +163,7 @@ function generateServerOptions(logPath: string, executablePath: string): ServerO
|
||||
launchArgs.push('--parallel-message-processing');
|
||||
}
|
||||
const enableSqlAuthenticationProvider = getEnableSqlAuthenticationProviderConfig();
|
||||
const azureAuthLibrary = getAzureAuthenticationLibraryConfig();
|
||||
if (azureAuthLibrary === 'MSAL' && enableSqlAuthenticationProvider === true) {
|
||||
if (enableSqlAuthenticationProvider === true) {
|
||||
launchArgs.push('--enable-sql-authentication-provider');
|
||||
}
|
||||
const enableConnectionPooling = getEnableConnectionPoolingConfig()
|
||||
|
||||
@@ -23,8 +23,6 @@ const enableSqlAuthenticationProviderConfig = 'enableSqlAuthenticationProvider';
|
||||
const enableConnectionPoolingConfig = 'enableConnectionPooling';
|
||||
const tableDesignerPreloadConfig = 'tableDesigner.preloadDatabaseModel';
|
||||
|
||||
const azureExtensionConfigName = 'azure';
|
||||
const azureAuthenticationLibraryConfig = 'authenticationLibrary';
|
||||
/**
|
||||
*
|
||||
* @returns Whether the current OS is linux or not
|
||||
@@ -68,16 +66,6 @@ export function removeOldLogFiles(logPath: string, prefix: string): JSON {
|
||||
export function getConfiguration(config: string = extensionConfigSectionName): vscode.WorkspaceConfiguration {
|
||||
return vscode.workspace.getConfiguration(config);
|
||||
}
|
||||
/**
|
||||
* We need Azure core extension configuration for fetching Authentication Library setting in use.
|
||||
* This is required for 'enableSqlAuthenticationProvider' to be enabled (as it applies to MSAL only).
|
||||
* This can be removed in future when ADAL support is dropped.
|
||||
* @param config Azure core extension configuration section name
|
||||
* @returns Azure core extension config section
|
||||
*/
|
||||
export function getAzureCoreExtConfiguration(config: string = azureExtensionConfigName): vscode.WorkspaceConfiguration {
|
||||
return vscode.workspace.getConfiguration(config);
|
||||
}
|
||||
|
||||
export function getConfigLogFilesRemovalLimit(): number | undefined {
|
||||
let config = getConfiguration();
|
||||
@@ -157,16 +145,6 @@ export function getParallelMessageProcessingConfig(): boolean {
|
||||
return (azdata.env.quality === azdata.env.AppQuality.dev && setting?.globalValue === undefined && setting?.workspaceValue === undefined) ? true : config[parallelMessageProcessingConfig];
|
||||
}
|
||||
|
||||
export function getAzureAuthenticationLibraryConfig(): string {
|
||||
const config = getAzureCoreExtConfiguration();
|
||||
if (config) {
|
||||
return config.get<string>(azureAuthenticationLibraryConfig, 'MSAL'); // default Auth library
|
||||
}
|
||||
else {
|
||||
return 'MSAL';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves configuration `mssql:enableSqlAuthenticationProvider` from settings file.
|
||||
* @returns true if setting is enabled in ADS, false otherwise.
|
||||
|
||||
5
extensions/types/vscode-mssql.d.ts
vendored
5
extensions/types/vscode-mssql.d.ts
vendored
@@ -801,11 +801,6 @@ declare module 'vscode-mssql' {
|
||||
accountVersion?: any;
|
||||
}
|
||||
|
||||
export enum AuthLibrary {
|
||||
ADAL = 'ADAL',
|
||||
MSAL = 'MSAL'
|
||||
}
|
||||
|
||||
export enum AzureAuthType {
|
||||
AuthCodeGrant = 0,
|
||||
DeviceCode = 1
|
||||
|
||||
7
src/sql/azdata.proposed.d.ts
vendored
7
src/sql/azdata.proposed.d.ts
vendored
@@ -661,13 +661,6 @@ declare module 'azdata' {
|
||||
type?: ExtensionNodeType;
|
||||
}
|
||||
|
||||
export interface AccountKey {
|
||||
/**
|
||||
* Auth Library used to add the account
|
||||
*/
|
||||
authLibrary?: string;
|
||||
}
|
||||
|
||||
export namespace workspace {
|
||||
/**
|
||||
* Creates and enters a workspace at the specified location
|
||||
|
||||
@@ -26,8 +26,6 @@ export const passwordChars = '***************';
|
||||
|
||||
export const enableSqlAuthenticationProviderConfig = 'mssql.enableSqlAuthenticationProvider';
|
||||
|
||||
/** Configuration for Azure Authentication Library */
|
||||
export const azureAuthenticationLibraryConfig = 'azure.authenticationLibrary';
|
||||
|
||||
/* default authentication type setting name*/
|
||||
export const defaultAuthenticationType = 'defaultAuthenticationType';
|
||||
|
||||
@@ -143,6 +143,5 @@ export const enum NbTelemetryAction {
|
||||
export const enum TelemetryPropertyName {
|
||||
ChartMaxRowCountExceeded = 'chartMaxRowCountExceeded',
|
||||
ConnectionSource = 'connectionSource',
|
||||
AuthLibrary = 'AuthLibrary'
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ import { Iterable } from 'vs/base/common/iterator';
|
||||
import { LoadingSpinner } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner';
|
||||
import { Tenant, TenantListDelegate, TenantListRenderer } from 'sql/workbench/services/accountManagement/browser/tenantListRenderer';
|
||||
import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces';
|
||||
import { ADAL_AUTH_LIBRARY, AuthLibrary, getAuthLibrary } from 'sql/workbench/services/accountManagement/common/utils';
|
||||
import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles';
|
||||
|
||||
export const VIEWLET_ID = 'workbench.view.accountpanel';
|
||||
@@ -389,14 +388,9 @@ export class AccountDialog extends Modal {
|
||||
this._splitView!.layout(DOM.getContentHeight(this._container!));
|
||||
|
||||
// Set the initial items of the list
|
||||
const authLibrary: AuthLibrary = getAuthLibrary(this._configurationService);
|
||||
let updatedAccounts: azdata.Account[];
|
||||
if (authLibrary) {
|
||||
updatedAccounts = filterAccounts(newProvider.initialAccounts, authLibrary);
|
||||
}
|
||||
providerView.updateAccounts(updatedAccounts);
|
||||
providerView.updateAccounts(newProvider.initialAccounts);
|
||||
|
||||
if ((updatedAccounts.length > 0 && this._splitViewContainer!.hidden) || this._providerViewsMap.size > 1) {
|
||||
if ((newProvider.initialAccounts.length > 0 && this._splitViewContainer!.hidden) || this._providerViewsMap.size > 1) {
|
||||
this.showSplitView();
|
||||
}
|
||||
|
||||
@@ -440,12 +434,7 @@ export class AccountDialog extends Modal {
|
||||
if (!providerMapping || !providerMapping.view) {
|
||||
return;
|
||||
}
|
||||
const authLibrary: AuthLibrary = getAuthLibrary(this._configurationService);
|
||||
let updatedAccounts: azdata.Account[];
|
||||
if (authLibrary) {
|
||||
updatedAccounts = filterAccounts(args.accountList, authLibrary);
|
||||
}
|
||||
providerMapping.view.updateAccounts(updatedAccounts);
|
||||
providerMapping.view.updateAccounts(args.accountList);
|
||||
|
||||
if ((args.accountList.length > 0 && this._splitViewContainer!.hidden) || this._providerViewsMap.size > 1) {
|
||||
this.showSplitView();
|
||||
@@ -518,18 +507,3 @@ export class AccountDialog extends Modal {
|
||||
v.addAccountAction.run();
|
||||
}
|
||||
}
|
||||
|
||||
// Filter accounts based on currently selected Auth Library:
|
||||
// if the account key is present, filter based on current auth library
|
||||
// if there is no account key (pre-MSAL account), then it is an ADAL account and
|
||||
// should be displayed as long as ADAL is the currently selected auth library
|
||||
export function filterAccounts(accounts: azdata.Account[], authLibrary: AuthLibrary): azdata.Account[] {
|
||||
let filteredAccounts = accounts.filter(account => {
|
||||
if (account.key.authLibrary) {
|
||||
return account.key.authLibrary === authLibrary;
|
||||
} else {
|
||||
return authLibrary === ADAL_AUTH_LIBRARY;
|
||||
}
|
||||
});
|
||||
return filteredAccounts;
|
||||
}
|
||||
|
||||
@@ -25,11 +25,8 @@ import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { INotificationService, Severity, INotification } from 'vs/platform/notification/common/notification';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { filterAccounts } from 'sql/workbench/services/accountManagement/browser/accountDialog';
|
||||
import { ADAL_AUTH_LIBRARY, MSAL_AUTH_LIBRARY, AuthLibrary, AZURE_AUTH_LIBRARY_CONFIG, getAuthLibrary } from 'sql/workbench/services/accountManagement/common/utils';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
import { TelemetryAction, TelemetryError, TelemetryPropertyName, TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
import { TelemetryAction, TelemetryError, TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
|
||||
export class AccountManagementService implements IAccountManagementService {
|
||||
// CONSTANTS ///////////////////////////////////////////////////////////
|
||||
@@ -39,7 +36,6 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
public _providers: { [id: string]: AccountProviderWithMetadata } = {};
|
||||
public _serviceBrand: undefined;
|
||||
private _accountStore: AccountStore;
|
||||
private _authLibrary: AuthLibrary;
|
||||
private _accountDialogController?: AccountDialogController;
|
||||
private _autoOAuthDialogController?: AutoOAuthDialogController;
|
||||
private _mementoContext?: Memento;
|
||||
@@ -63,7 +59,6 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
@IOpenerService private _openerService: IOpenerService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IConfigurationService private _configurationService: IConfigurationService,
|
||||
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService
|
||||
) {
|
||||
this._mementoContext = new Memento(AccountManagementService.ACCOUNT_MEMENTO, this._storageService);
|
||||
@@ -75,12 +70,7 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
this._removeAccountProviderEmitter = new Emitter<azdata.AccountProviderMetadata>();
|
||||
this._updateAccountListEmitter = new Emitter<UpdateAccountListEventParams>();
|
||||
|
||||
// Determine authentication library in use, to support filtering accounts respectively.
|
||||
// When this value is changed a restart is required so there isn't a need to dynamically update this value at runtime.
|
||||
this._authLibrary = getAuthLibrary(this._configurationService);
|
||||
|
||||
_storageService.onWillSaveState(() => this.shutdown());
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private get autoOAuthDialogController(): AutoOAuthDialogController {
|
||||
@@ -152,9 +142,6 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
} else {
|
||||
this._telemetryService.createErrorEvent(TelemetryView.LinkedAccounts, TelemetryError.AddAzureAccountError, accountResult.errorCode,
|
||||
this.getErrorType(accountResult.errorMessage))
|
||||
.withAdditionalProperties({
|
||||
[TelemetryPropertyName.AuthLibrary]: this._authLibrary
|
||||
})
|
||||
.send();
|
||||
if (accountResult.errorCode && accountResult.errorMessage) {
|
||||
throw new Error(localize('addAccountFailedCodeMessage', `{0} \nError Message: {1}`, accountResult.errorCode, accountResult.errorMessage));
|
||||
@@ -168,9 +155,6 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
this._logService.error('Adding account failed, no result received.');
|
||||
this._telemetryService.createErrorEvent(TelemetryView.LinkedAccounts, TelemetryError.AddAzureAccountErrorNoResult, '-1',
|
||||
this.getErrorType())
|
||||
.withAdditionalProperties({
|
||||
[TelemetryPropertyName.AuthLibrary]: this._authLibrary
|
||||
})
|
||||
.send();
|
||||
throw new Error(genericAccountErrorMessage);
|
||||
}
|
||||
@@ -182,9 +166,6 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
this.spliceModifiedAccount(provider, result.changedAccount);
|
||||
}
|
||||
this._telemetryService.createActionEvent(TelemetryView.LinkedAccounts, TelemetryAction.AddAzureAccount)
|
||||
.withAdditionalProperties({
|
||||
[TelemetryPropertyName.AuthLibrary]: this._authLibrary
|
||||
})
|
||||
.send();
|
||||
this.fireAccountListUpdate(provider, result.accountAdded);
|
||||
} finally {
|
||||
@@ -235,9 +216,6 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
} else {
|
||||
this._telemetryService.createErrorEvent(TelemetryView.LinkedAccounts, TelemetryError.RefreshAzureAccountError, refreshedAccount.errorCode,
|
||||
this.getErrorType(refreshedAccount.errorMessage))
|
||||
.withAdditionalProperties({
|
||||
[TelemetryPropertyName.AuthLibrary]: this._authLibrary
|
||||
})
|
||||
.send();
|
||||
if (refreshedAccount.errorCode && refreshedAccount.errorMessage) {
|
||||
throw new Error(localize('refreshFailed', `{0} \nError Message: {1}`, refreshedAccount.errorCode, refreshedAccount.errorMessage));
|
||||
@@ -254,9 +232,6 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
this._logService.error('Refreshing account failed, no result received.');
|
||||
this._telemetryService.createErrorEvent(TelemetryView.LinkedAccounts, TelemetryError.RefreshAzureAccountErrorNoResult, '-1',
|
||||
this.getErrorType())
|
||||
.withAdditionalProperties({
|
||||
[TelemetryPropertyName.AuthLibrary]: this._authLibrary
|
||||
})
|
||||
.send();
|
||||
throw new Error(genericAccountErrorMessage);
|
||||
}
|
||||
@@ -281,9 +256,6 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
}
|
||||
|
||||
this._telemetryService.createActionEvent(TelemetryView.LinkedAccounts, TelemetryAction.RefreshAzureAccount)
|
||||
.withAdditionalProperties({
|
||||
[TelemetryPropertyName.AuthLibrary]: this._authLibrary
|
||||
})
|
||||
.send();
|
||||
this.fireAccountListUpdate(provider, result.accountAdded);
|
||||
return result.changedAccount!;
|
||||
@@ -325,7 +297,6 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
// 3) Update our local cache of accounts
|
||||
return this.doWithProvider(providerId, provider => {
|
||||
return self._accountStore.getAccountsByProvider(provider.metadata.id)
|
||||
.then(accounts => this._authLibrary ? filterAccounts(accounts, this._authLibrary) : accounts)
|
||||
.then(accounts => {
|
||||
self._providers[providerId].accounts = accounts;
|
||||
return accounts;
|
||||
@@ -337,8 +308,7 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
* Retrieves all the accounts registered with ADS based on auth library in use.
|
||||
*/
|
||||
public getAccounts(): Promise<azdata.Account[]> {
|
||||
return this._accountStore.getAllAccounts()
|
||||
.then(accounts => this._authLibrary ? filterAccounts(accounts, this._authLibrary) : accounts);
|
||||
return this._accountStore.getAllAccounts();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -575,15 +545,10 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
});
|
||||
}
|
||||
|
||||
const authLibrary: AuthLibrary = getAuthLibrary(this._configurationService)
|
||||
let updatedAccounts: azdata.Account[]
|
||||
if (authLibrary) {
|
||||
updatedAccounts = filterAccounts(provider.accounts, authLibrary);
|
||||
}
|
||||
// Step 2) Fire the event
|
||||
let eventArg: UpdateAccountListEventParams = {
|
||||
providerId: provider.metadata.id,
|
||||
accountList: updatedAccounts ?? provider.accounts
|
||||
accountList: provider.accounts
|
||||
};
|
||||
this._updateAccountListEmitter.fire(eventArg);
|
||||
}
|
||||
@@ -598,62 +563,6 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this.disposables.add(this._configurationService.onDidChangeConfiguration(async e => {
|
||||
if (e.affectsConfiguration(AZURE_AUTH_LIBRARY_CONFIG)) {
|
||||
const authLibrary: AuthLibrary = getAuthLibrary(this._configurationService);
|
||||
let accounts = await this._accountStore.getAllAccounts();
|
||||
if (accounts) {
|
||||
let updatedAccounts = await this.filterAndMergeAccounts(accounts, authLibrary);
|
||||
let eventArg: UpdateAccountListEventParams;
|
||||
if (updatedAccounts.length > 0) {
|
||||
updatedAccounts.forEach(account => {
|
||||
if (account.key.authLibrary === MSAL_AUTH_LIBRARY) {
|
||||
account.isStale = false;
|
||||
}
|
||||
});
|
||||
eventArg = {
|
||||
providerId: updatedAccounts[0].key.providerId,
|
||||
accountList: updatedAccounts
|
||||
};
|
||||
} else { // default to public cloud if no accounts
|
||||
eventArg = {
|
||||
providerId: 'azure_publicCloud',
|
||||
accountList: updatedAccounts
|
||||
};
|
||||
}
|
||||
this._updateAccountListEmitter.fire(eventArg);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Filters and merges accounts from both authentication libraries
|
||||
private async filterAndMergeAccounts(accounts: azdata.Account[], currentAuthLibrary: AuthLibrary): Promise<azdata.Account[]> {
|
||||
// Fetch accounts for alternate authenticationLibrary
|
||||
const altLibrary = currentAuthLibrary === MSAL_AUTH_LIBRARY ? ADAL_AUTH_LIBRARY : MSAL_AUTH_LIBRARY;
|
||||
const altLibraryAccounts = filterAccounts(accounts, altLibrary);
|
||||
|
||||
// Fetch accounts for current authenticationLibrary
|
||||
const currentLibraryAccounts = filterAccounts(accounts, currentAuthLibrary);
|
||||
|
||||
// In the list of alternate accounts, check if the accounts are present in the current library cache,
|
||||
// if not, add the account and mark it stale. The original account is marked as taken so its not picked again.
|
||||
for (let account of altLibraryAccounts) {
|
||||
await this.removeAccount(account.key);
|
||||
if (this.findAccountIndex(currentLibraryAccounts, account) >= 0) {
|
||||
continue;
|
||||
} else {
|
||||
// TODO: Refresh access token for the account if feasible.
|
||||
account.isStale = true;
|
||||
account.key.authLibrary = currentAuthLibrary;
|
||||
currentLibraryAccounts.push(account);
|
||||
await this.addAccountWithoutPrompt(account);
|
||||
}
|
||||
}
|
||||
return currentLibraryAccounts;
|
||||
}
|
||||
|
||||
public findAccountIndex(accounts: azdata.Account[], accountToFind: azdata.Account): number {
|
||||
let indexToRemove: number = accounts.findIndex(account => {
|
||||
// corner case handling for personal accounts
|
||||
@@ -664,10 +573,6 @@ export class AccountManagementService implements IAccountManagementService {
|
||||
if (accountToFind.key.accountId.includes('.')) {
|
||||
return account.key.accountId === accountToFind!.key.accountId.split('.')[0];
|
||||
}
|
||||
// ADAL account added
|
||||
if (account.key.accountId.includes('.')) {
|
||||
return account.key.accountId.split('.')[0] === accountToFind!.key.accountId;
|
||||
}
|
||||
return account.key.accountId === accountToFind!.key.accountId;
|
||||
});
|
||||
return indexToRemove;
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
export const AZURE_AUTH_LIBRARY_CONFIG = 'azure.authenticationLibrary';
|
||||
|
||||
export type AuthLibrary = 'ADAL' | 'MSAL';
|
||||
export const MSAL_AUTH_LIBRARY: AuthLibrary = 'MSAL';
|
||||
export const ADAL_AUTH_LIBRARY: AuthLibrary = 'ADAL';
|
||||
|
||||
export const DEFAULT_AUTH_LIBRARY: AuthLibrary = MSAL_AUTH_LIBRARY;
|
||||
|
||||
export function getAuthLibrary(configurationService: IConfigurationService): AuthLibrary {
|
||||
return configurationService.getValue(AZURE_AUTH_LIBRARY_CONFIG) || DEFAULT_AUTH_LIBRARY;
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import { EventVerifierSingle } from 'sql/base/test/common/event';
|
||||
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
|
||||
import { AccountDialog } from 'sql/workbench/services/accountManagement/browser/accountDialog';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService';
|
||||
import { NullAdsTelemetryService } from 'sql/platform/telemetry/common/adsTelemetryService';
|
||||
|
||||
// SUITE CONSTANTS /////////////////////////////////////////////////////////
|
||||
@@ -34,8 +33,7 @@ const noAccountProvider: azdata.AccountProviderMetadata = {
|
||||
const account: azdata.Account = {
|
||||
key: {
|
||||
providerId: hasAccountProvider.id,
|
||||
accountId: 'testAccount1',
|
||||
authLibrary: 'MSAL'
|
||||
accountId: 'testAccount1'
|
||||
},
|
||||
displayInfo: {
|
||||
displayName: 'Test Account 1',
|
||||
@@ -538,12 +536,11 @@ function getTestState(): AccountManagementState {
|
||||
.returns(() => <any>mockAccountStore.object);
|
||||
|
||||
const testNotificationService = new TestNotificationService();
|
||||
const testConfigurationService = new TestConfigurationService();
|
||||
const mockTelemetryService = new NullAdsTelemetryService();
|
||||
|
||||
// Create the account management service
|
||||
let ams = new AccountManagementService(mockInstantiationService.object, new TestStorageService(),
|
||||
undefined, undefined, undefined, testNotificationService, testConfigurationService, mockTelemetryService);
|
||||
undefined, undefined, undefined, testNotificationService, mockTelemetryService);
|
||||
|
||||
// Wire up event handlers
|
||||
let evUpdate = new EventVerifierSingle<UpdateAccountListEventParams>();
|
||||
|
||||
@@ -1062,9 +1062,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
||||
const azureAccounts = accounts.filter(a => a.key.providerId.startsWith('azure'));
|
||||
if (azureAccounts && azureAccounts.length > 0) {
|
||||
let accountId = (connection.authenticationType === Constants.AuthenticationType.AzureMFA || connection.authenticationType === Constants.AuthenticationType.AzureMFAAndUser) ? connection.azureAccount : connection.userName;
|
||||
// For backwards compatibility with ADAL, we need to check if the account ID matches with tenant Id or just the account ID
|
||||
// The OR case can be removed once we no longer support ADAL
|
||||
let account = azureAccounts.find(account => account.key.accountId === accountId || account.key.accountId.split('.')[0] === accountId);
|
||||
let account = azureAccounts.find(account => account.key.accountId === accountId);
|
||||
if (account) {
|
||||
this._logService.debug(`Getting security token for Azure account ${account.key.accountId}`);
|
||||
if (account.isStale) {
|
||||
|
||||
@@ -35,11 +35,9 @@ import Severity from 'vs/base/common/severity';
|
||||
import { ConnectionStringOptions } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { filterAccounts } from 'sql/workbench/services/accountManagement/browser/accountDialog';
|
||||
import { AuthenticationType, Actions, mssqlApplicationNameOption, applicationName, mssqlProviderName, mssqlCmsProviderName } from 'sql/platform/connection/common/constants';
|
||||
import { AdsWidget } from 'sql/base/browser/ui/adsWidget';
|
||||
import { createCSSRule } from 'vs/base/browser/dom';
|
||||
import { AuthLibrary, getAuthLibrary } from 'sql/workbench/services/accountManagement/common/utils';
|
||||
import { adjustForMssqlAppName } from 'sql/platform/connection/common/utils';
|
||||
import { isMssqlAuthProviderEnabled } from 'sql/workbench/services/connection/browser/utils';
|
||||
import { RequiredIndicatorClassName } from 'sql/base/browser/ui/label/label';
|
||||
@@ -711,11 +709,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
||||
private async fillInAzureAccountOptions(): Promise<void> {
|
||||
let oldSelection = this._azureAccountDropdown.value;
|
||||
const accounts = await this._accountManagementService.getAccounts();
|
||||
const updatedAccounts = accounts.filter(a => a.key.providerId.startsWith('azure'));
|
||||
const authLibrary: AuthLibrary = getAuthLibrary(this._configurationService);
|
||||
if (authLibrary) {
|
||||
this._azureAccountList = filterAccounts(updatedAccounts, authLibrary);
|
||||
}
|
||||
this._azureAccountList = accounts.filter(a => a.key.providerId.startsWith('azure'));
|
||||
|
||||
let accountDropdownOptions: SelectOptionItemSQL[] = this._azureAccountList.map(account => {
|
||||
return {
|
||||
@@ -734,10 +728,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
||||
}
|
||||
|
||||
private updateRefreshCredentialsLink(): void {
|
||||
// For backwards compatibility with ADAL, we need to check if the account ID matches with tenant Id or just the account ID
|
||||
// The OR case can be removed once we no longer support ADAL
|
||||
let chosenAccount = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value
|
||||
|| account.key.accountId.split('.')[0] === this._azureAccountDropdown.value);
|
||||
let chosenAccount = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value);
|
||||
if (chosenAccount && chosenAccount.isStale) {
|
||||
this._tableContainer.classList.remove('hide-refresh-link');
|
||||
} else {
|
||||
@@ -758,10 +749,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
||||
await this.fillInAzureAccountOptions();
|
||||
|
||||
// If a new account was added find it and select it, otherwise select the first account
|
||||
// For backwards compatibility with ADAL, we need to check if the account ID matches with tenant Id or just the account ID
|
||||
// The OR case can be removed once we no longer support ADAL
|
||||
let newAccount = this._azureAccountList.find(option => !oldAccountIds.some(oldId => oldId === option.key.accountId
|
||||
|| oldId.split('.')[0] === option.key.accountId));
|
||||
let newAccount = this._azureAccountList.find(option => !oldAccountIds.some(oldId => oldId === option.key.accountId));
|
||||
if (newAccount) {
|
||||
this._azureAccountDropdown.selectWithOptionName(newAccount.key.accountId);
|
||||
} else {
|
||||
@@ -773,10 +761,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
||||
|
||||
// Display the tenant select box if needed
|
||||
const hideTenantsClassName = 'hide-azure-tenants';
|
||||
// For backwards compatibility with ADAL, we need to check if the account ID matches with tenant Id or just the account ID
|
||||
// The OR case can be removed once we no longer support ADAL
|
||||
let selectedAccount = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value
|
||||
|| account.key.accountId.split('.')[0] === this._azureAccountDropdown.value);
|
||||
let selectedAccount = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value);
|
||||
if (!selectedAccount && selectFirstByDefault && this._azureAccountList.length > 0) {
|
||||
selectedAccount = this._azureAccountList[0];
|
||||
}
|
||||
@@ -967,10 +952,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
||||
? connectionInfo.azureAccount : connectionInfo.userName;
|
||||
let account: azdata.Account;
|
||||
if (accountName) {
|
||||
// For backwards compatibility with ADAL, we need to check if the account ID matches with tenant Id or just the account ID
|
||||
// The OR case can be removed once we no longer support ADAL
|
||||
account = this._azureAccountList?.find(account => account.key.accountId === this.getModelValue(accountName)
|
||||
|| account.key.accountId.split('.')[0] === this.getModelValue(accountName));
|
||||
account = this._azureAccountList?.find(account => account.key.accountId === this.getModelValue(accountName));
|
||||
if (account) {
|
||||
if (!account.properties.tenants?.find(tenant => tenant.id === this._azureTenantId)) {
|
||||
this._azureTenantId = account.properties.tenants[0].id;
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { azureAuthenticationLibraryConfig, enableSqlAuthenticationProviderConfig, mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||
import { MSAL_AUTH_LIBRARY } from 'sql/workbench/services/accountManagement/common/utils';
|
||||
import { enableSqlAuthenticationProviderConfig, mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||
|
||||
/**
|
||||
* Reads setting 'mssql.enableSqlAuthenticationProvider' returns true if it's enabled.
|
||||
@@ -15,16 +14,5 @@ import { MSAL_AUTH_LIBRARY } from 'sql/workbench/services/accountManagement/comm
|
||||
* @returns True if provider is MSSQL and Sql Auth provider is enabled.
|
||||
*/
|
||||
export function isMssqlAuthProviderEnabled(provider: string, configService: IConfigurationService | undefined): boolean {
|
||||
return provider === mssqlProviderName && isMSALAuthLibraryEnabled(configService) && (configService?.getValue(enableSqlAuthenticationProviderConfig) ?? true);
|
||||
}
|
||||
|
||||
/**
|
||||
* We need Azure core extension configuration for fetching Authentication Library setting in use.
|
||||
* This is required for 'enableSqlAuthenticationProvider' to be enabled (as it applies to MSAL only).
|
||||
* This can be removed in future when ADAL support is dropped.
|
||||
* @param configService Configuration Service to use.
|
||||
* @returns true if MSAL_AUTH_LIBRARY is enabled.
|
||||
*/
|
||||
export function isMSALAuthLibraryEnabled(configService: IConfigurationService | undefined): boolean {
|
||||
return configService?.getValue(azureAuthenticationLibraryConfig) === MSAL_AUTH_LIBRARY /*default*/ ?? true;
|
||||
return provider === mssqlProviderName && (configService?.getValue(enableSqlAuthenticationProviderConfig) ?? true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user