mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
Large cleanup of AzureCore - Introduction of getAccountSecurityToken and deprecation of getSecurityToken (#11446)
* do a large cleanup of azurecore * Fix tests * Rework Device Code * Fix tests * Fix AE scenario * Fix firewall rule - clenaup logging * Shorthand syntax * Fix firewall tests * Start on tests for azureAuth * Add more tests * Address comments * Add a few more important tests * Don't throw error on old code * Fill in todo
This commit is contained in:
@@ -3,104 +3,39 @@
|
|||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as azdata from 'azdata';
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import * as azdata from 'azdata';
|
||||||
|
|
||||||
import * as nls from 'vscode-nls';
|
import * as nls from 'vscode-nls';
|
||||||
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios';
|
|
||||||
import * as qs from 'qs';
|
|
||||||
import * as url from 'url';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AzureAccountProviderMetadata,
|
|
||||||
Tenant,
|
|
||||||
AzureAccount,
|
AzureAccount,
|
||||||
Resource,
|
AzureAccountProviderMetadata,
|
||||||
AzureAuthType,
|
AzureAuthType,
|
||||||
Subscription,
|
Deferred,
|
||||||
Deferred
|
Resource,
|
||||||
|
Tenant
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
|
import * as url from 'url';
|
||||||
|
|
||||||
import { SimpleTokenCache } from '../simpleTokenCache';
|
import { SimpleTokenCache } from '../simpleTokenCache';
|
||||||
import { MemoryDatabase } from '../utils/memoryDatabase';
|
import { MemoryDatabase } from '../utils/memoryDatabase';
|
||||||
|
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
import { Logger } from '../../utils/Logger';
|
import { Logger } from '../../utils/Logger';
|
||||||
|
import * as qs from 'qs';
|
||||||
|
import { AzureAuthError } from './azureAuthError';
|
||||||
|
|
||||||
const localize = nls.loadMessageBundle();
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
export interface AccountKey {
|
|
||||||
/**
|
|
||||||
* Account Key - uniquely identifies an account
|
|
||||||
*/
|
|
||||||
key: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccessToken extends AccountKey {
|
|
||||||
/**
|
|
||||||
* Access Token
|
|
||||||
*/
|
|
||||||
token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RefreshToken extends AccountKey {
|
|
||||||
/**
|
|
||||||
* Refresh Token
|
|
||||||
*/
|
|
||||||
token: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Account Key
|
|
||||||
*/
|
|
||||||
key: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TokenResponse {
|
|
||||||
[tenantId: string]: Token
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Token extends AccountKey {
|
|
||||||
/**
|
|
||||||
* Access token
|
|
||||||
*/
|
|
||||||
token: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TokenType
|
|
||||||
*/
|
|
||||||
tokenType: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TokenClaims { // https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens
|
|
||||||
aud: string;
|
|
||||||
iss: string;
|
|
||||||
iat: number;
|
|
||||||
idp: string,
|
|
||||||
nbf: number;
|
|
||||||
exp: number;
|
|
||||||
c_hash: string;
|
|
||||||
at_hash: string;
|
|
||||||
aio: string;
|
|
||||||
preferred_username: string;
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
nonce: string;
|
|
||||||
oid: string;
|
|
||||||
roles: string[];
|
|
||||||
rh: string;
|
|
||||||
sub: string;
|
|
||||||
tid: string;
|
|
||||||
unique_name: string;
|
|
||||||
uti: string;
|
|
||||||
ver: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TokenRefreshResponse = { accessToken: AccessToken, refreshToken: RefreshToken, tokenClaims: TokenClaims, expiresOn: string };
|
|
||||||
|
|
||||||
export abstract class AzureAuth implements vscode.Disposable {
|
export abstract class AzureAuth implements vscode.Disposable {
|
||||||
protected readonly memdb = new MemoryDatabase();
|
protected readonly memdb = new MemoryDatabase<string>();
|
||||||
|
|
||||||
protected readonly WorkSchoolAccountType: string = 'work_school';
|
protected readonly WorkSchoolAccountType: string = 'work_school';
|
||||||
protected readonly MicrosoftAccountType: string = 'microsoft';
|
protected readonly MicrosoftAccountType: string = 'microsoft';
|
||||||
|
|
||||||
protected readonly loginEndpointUrl: string;
|
protected readonly loginEndpointUrl: string;
|
||||||
protected readonly commonTenant: Tenant;
|
public readonly commonTenant: Tenant;
|
||||||
protected readonly redirectUri: string;
|
protected readonly redirectUri: string;
|
||||||
protected readonly scopes: string[];
|
protected readonly scopes: string[];
|
||||||
protected readonly scopesString: string;
|
protected readonly scopesString: string;
|
||||||
@@ -141,185 +76,227 @@ export abstract class AzureAuth implements vscode.Disposable {
|
|||||||
this.scopesString = this.scopes.join(' ');
|
this.scopesString = this.scopes.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract async login(): Promise<AzureAccount | azdata.PromptFailedResult>;
|
public async startLogin(): Promise<AzureAccount | azdata.PromptFailedResult> {
|
||||||
|
let loginComplete: Deferred<void>;
|
||||||
public abstract async autoOAuthCancelled(): Promise<void>;
|
|
||||||
|
|
||||||
public abstract async promptForConsent(resourceId: string, tenant: string): Promise<{ tokenRefreshResponse: TokenRefreshResponse, authCompleteDeferred: Deferred<void> } | undefined>;
|
|
||||||
|
|
||||||
public dispose() { }
|
|
||||||
|
|
||||||
public async refreshAccess(oldAccount: azdata.Account): Promise<azdata.Account> {
|
|
||||||
const response = await this.getCachedToken(oldAccount.key);
|
|
||||||
if (!response) {
|
|
||||||
oldAccount.isStale = true;
|
|
||||||
return oldAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshToken = response.refreshToken;
|
|
||||||
if (!refreshToken || !refreshToken.key) {
|
|
||||||
oldAccount.isStale = true;
|
|
||||||
return oldAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Refresh the access token
|
const result = await this.login(this.commonTenant, this.metadata.settings.microsoftResource);
|
||||||
const tokenResponse = await this.refreshAccessToken(oldAccount.key, refreshToken);
|
loginComplete = result.authComplete;
|
||||||
const tenants = await this.getTenants(tokenResponse.accessToken);
|
if (!result?.response) {
|
||||||
|
Logger.error('Authentication failed');
|
||||||
// Recreate account object
|
return {
|
||||||
const newAccount = this.createAccount(tokenResponse.tokenClaims, tokenResponse.accessToken.key, tenants);
|
canceled: false
|
||||||
|
};
|
||||||
const subscriptions = await this.getSubscriptions(newAccount);
|
}
|
||||||
newAccount.properties.subscriptions = subscriptions;
|
const account = await this.hydrateAccount(result.response.accessToken, result.response.tokenClaims);
|
||||||
|
loginComplete?.resolve();
|
||||||
return newAccount;
|
return account;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
oldAccount.isStale = true;
|
if (ex instanceof AzureAuthError) {
|
||||||
if (ex.message) {
|
if (loginComplete) {
|
||||||
await vscode.window.showErrorMessage(ex.message);
|
loginComplete.reject(ex.getPrintableString());
|
||||||
|
} else {
|
||||||
|
vscode.window.showErrorMessage(ex.getPrintableString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Logger.error(ex);
|
Logger.error(ex);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
return oldAccount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getHomeTenant(account: AzureAccount): Tenant {
|
||||||
|
// Home is defined by the API
|
||||||
|
// Lets pick the home tenant - and fall back to commonTenant if they don't exist
|
||||||
|
return account.properties.tenants.find(t => t.tenantCategory === 'Home') ?? account.properties.tenants[0] ?? this.commonTenant;
|
||||||
|
}
|
||||||
|
|
||||||
public async getSecurityToken(account: azdata.Account, azureResource: azdata.AzureResource): Promise<TokenResponse | undefined> {
|
public async refreshAccess(account: AzureAccount): Promise<AzureAccount> {
|
||||||
|
try {
|
||||||
|
const tenant = this.getHomeTenant(account);
|
||||||
|
const tokenResult = await this.getAccountSecurityToken(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) {
|
||||||
|
vscode.window.showErrorMessage(ex.getPrintableString());
|
||||||
|
}
|
||||||
|
Logger.error(ex);
|
||||||
|
account.isStale = true;
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async hydrateAccount(token: Token | AccessToken, tokenClaims: TokenClaims): Promise<AzureAccount> {
|
||||||
|
const tenants = await this.getTenants({ ...token });
|
||||||
|
const account = this.createAccount(tokenClaims, token.key, tenants);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAccountSecurityToken(account: AzureAccount, tenantId: string, azureResource: azdata.AzureResource): Promise<Token | undefined> {
|
||||||
if (account.isStale === true) {
|
if (account.isStale === true) {
|
||||||
Logger.log('Account was stale, no tokens being fetched');
|
Logger.log('Account was stale. No tokens being fetched.');
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resource = this.resources.find(s => s.azureResourceId === azureResource);
|
const resource = this.resources.find(s => s.azureResourceId === azureResource);
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
|
Logger.log('Invalid resource, not fetching', azureResource);
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const azureAccount = account as AzureAccount;
|
const tenant = account.properties.tenants.find(t => t.id === tenantId);
|
||||||
const response: TokenResponse = {};
|
|
||||||
|
|
||||||
for (const tenant of azureAccount.properties.tenants) {
|
if (!tenant) {
|
||||||
let cachedTokens = await this.getCachedToken(account.key, resource.id, tenant.id);
|
throw new AzureAuthError(localize('azure.tenantNotFound', "Specifed tenant with ID '{0}' not found.", tenantId), `Tenant ${tenantId} not found.`, undefined);
|
||||||
// Check expiration
|
}
|
||||||
if (cachedTokens) {
|
|
||||||
const expiresOn = Number(this.memdb.get(this.createMemdbString(account.key.accountId, tenant.id, resource.id)));
|
const cachedTokens = await this.getSavedToken(tenant, resource, account.key);
|
||||||
|
|
||||||
|
// Let's check to see if we can just use the cached tokens to return to the user
|
||||||
|
if (cachedTokens?.accessToken) {
|
||||||
|
let expiry = Number(cachedTokens.expiresOn);
|
||||||
|
if (Number.isNaN(expiry)) {
|
||||||
|
Logger.log('Expiration time was not defined. This is expected on first launch');
|
||||||
|
expiry = 0;
|
||||||
|
}
|
||||||
const currentTime = new Date().getTime() / 1000;
|
const currentTime = new Date().getTime() / 1000;
|
||||||
|
|
||||||
if (!Number.isNaN(expiresOn)) {
|
let accessToken = cachedTokens.accessToken;
|
||||||
const remainingTime = expiresOn - currentTime;
|
const remainingTime = expiry - currentTime;
|
||||||
const fiveMinutes = 5 * 60;
|
const maxTolerance = 2 * 60; // two minutes
|
||||||
// If the remaining time is less than five minutes, assume the token has expired. It's too close to expiration to be meaningful.
|
|
||||||
if (remainingTime < fiveMinutes) {
|
|
||||||
cachedTokens = undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No expiration date, assume expired.
|
|
||||||
cachedTokens = undefined;
|
|
||||||
Logger.log('Assuming expired token due to no expiration date - this is expected on first launch.');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (remainingTime < maxTolerance) {
|
||||||
|
const result = await this.refreshToken(tenant, resource, cachedTokens.refreshToken);
|
||||||
|
accessToken = result.accessToken;
|
||||||
}
|
}
|
||||||
|
// Let's just return here.
|
||||||
// Refresh
|
if (accessToken) {
|
||||||
if (!cachedTokens) {
|
return {
|
||||||
|
...accessToken,
|
||||||
const baseToken = await this.getCachedToken(account.key);
|
|
||||||
if (!baseToken) {
|
|
||||||
account.isStale = true;
|
|
||||||
Logger.log('Base token was empty, account is stale.');
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.refreshAccessToken(account.key, baseToken.refreshToken, tenant, resource);
|
|
||||||
} catch (ex) {
|
|
||||||
Logger.log(`Could not refresh access token for ${JSON.stringify(tenant)} - silently removing the tenant from the user's account.`);
|
|
||||||
Logger.error(`Actual error: ${JSON.stringify(ex?.response?.data ?? ex.message ?? ex, undefined, 2)}`);
|
|
||||||
azureAccount.properties.tenants = azureAccount.properties.tenants.filter(t => t.id !== tenant.id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedTokens = await this.getCachedToken(account.key, resource.id, tenant.id);
|
|
||||||
if (!cachedTokens) {
|
|
||||||
Logger.log('Refresh access tokens didn not set cache');
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { accessToken } = cachedTokens;
|
|
||||||
response[tenant.id] = {
|
|
||||||
token: accessToken.token,
|
|
||||||
key: accessToken.key,
|
|
||||||
tokenType: 'Bearer'
|
tokenType: 'Bearer'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (azureAccount.properties.subscriptions) {
|
// User didn't have any cached tokens, or the cached tokens weren't useful.
|
||||||
azureAccount.properties.subscriptions.forEach(subscription => {
|
// 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.
|
||||||
// Make sure that tenant has information populated.
|
const baseTokens = await this.getSavedToken(this.commonTenant, this.metadata.settings.microsoftResource, account.key);
|
||||||
if (response[subscription.tenantId]) {
|
if (!baseTokens) {
|
||||||
response[subscription.id] = {
|
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');
|
||||||
...response[subscription.tenantId]
|
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.refreshToken(tenant, resource, baseTokens.refreshToken);
|
||||||
|
if (result.accessToken) {
|
||||||
|
return {
|
||||||
|
...result.accessToken,
|
||||||
|
tokenType: 'Bearer'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async clearCredentials(account: azdata.AccountKey): Promise<void> {
|
|
||||||
try {
|
|
||||||
return this.deleteAccountCache(account);
|
|
||||||
} catch (ex) {
|
|
||||||
const msg = localize('azure.cacheErrrorRemove', "Error when removing your account from the cache.");
|
|
||||||
vscode.window.showErrorMessage(msg);
|
|
||||||
Logger.error('Error when removing tokens.', ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected toBase64UrlEncoding(base64string: string): string {
|
protected abstract async login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse, authComplete: Deferred<void> }>;
|
||||||
return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async makePostRequest(uri: string, postData: { [key: string]: string }, validateStatus = false) {
|
/**
|
||||||
try {
|
* 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.
|
||||||
const config: AxiosRequestConfig = {
|
* @param tenant
|
||||||
headers: {
|
* @param resource
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
* @param refreshToken
|
||||||
}
|
*/
|
||||||
|
public async refreshToken(tenant: Tenant, resource: Resource, refreshToken: RefreshToken | undefined): Promise<OAuthTokenResponse> {
|
||||||
|
if (refreshToken) {
|
||||||
|
const postData: RefreshTokenPostData = {
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
client_id: this.clientId,
|
||||||
|
refresh_token: refreshToken.token,
|
||||||
|
tenant: tenant.id,
|
||||||
|
resource: resource.endpoint
|
||||||
};
|
};
|
||||||
|
|
||||||
if (validateStatus) {
|
return this.getToken(tenant, resource, postData);
|
||||||
config.validateStatus = () => true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await axios.post(uri, qs.stringify(postData), config);
|
return this.handleInteractionRequired(tenant, resource);
|
||||||
} catch (ex) {
|
|
||||||
Logger.log('Unexpected error making Azure auth request', 'azureCore.postRequest', JSON.stringify(ex?.response?.data, undefined, 2));
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async makeGetRequest(token: string, uri: string): Promise<AxiosResponse<any>> {
|
public async getToken(tenant: Tenant, resource: Resource, postData: AuthorizationCodePostData | TokenPostData | RefreshTokenPostData): Promise<OAuthTokenResponse> {
|
||||||
try {
|
const tokenUrl = `${this.loginEndpointUrl}${tenant.id}/oauth2/token`;
|
||||||
const config = {
|
const response = await this.makePostRequest(tokenUrl, postData);
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
if (response.data.error === 'interaction_required') {
|
||||||
'Content-Type': 'application/json',
|
return this.handleInteractionRequired(tenant, resource);
|
||||||
},
|
}
|
||||||
|
|
||||||
|
if (response.data.error) {
|
||||||
|
Logger.error('Response error!', response.data);
|
||||||
|
throw new AzureAuthError(localize('azure.responseError', "Token retrival failed with an error. Open developer tools to view the error"), 'Token retrival failed', undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessTokenString = response.data.access_token;
|
||||||
|
const refreshTokenString = response.data.refresh_token;
|
||||||
|
const expiresOnString = response.data.expires_on;
|
||||||
|
|
||||||
|
return this.getTokenHelper(tenant, resource, accessTokenString, refreshTokenString, expiresOnString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getTokenHelper(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);
|
||||||
|
|
||||||
|
const userKey = tokenClaims.sub ?? tokenClaims.oid;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (refreshTokenString) {
|
||||||
|
refreshToken = {
|
||||||
|
token: refreshTokenString,
|
||||||
|
key: userKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: OAuthTokenResponse = {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
tokenClaims,
|
||||||
|
expiresOn: expiresOnString
|
||||||
};
|
};
|
||||||
|
|
||||||
return await axios.get(uri, config);
|
const accountKey: azdata.AccountKey = {
|
||||||
} catch (ex) {
|
providerId: this.metadata.id,
|
||||||
// Intercept and print error
|
accountId: userKey
|
||||||
Logger.log('Unexpected error making Azure auth request', 'azureCore.getRequest', JSON.stringify(ex?.response?.data, undefined, 2));
|
};
|
||||||
// rethrow error
|
|
||||||
throw ex;
|
await this.saveToken(tenant, resource, accountKey, result);
|
||||||
}
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getTenants(token: AccessToken): Promise<Tenant[]> {
|
|
||||||
|
|
||||||
|
//#region tenant calls
|
||||||
|
public async getTenants(token: AccessToken): Promise<Tenant[]> {
|
||||||
interface TenantResponse { // https://docs.microsoft.com/en-us/rest/api/resources/tenants/list
|
interface TenantResponse { // https://docs.microsoft.com/en-us/rest/api/resources/tenants/list
|
||||||
id: string
|
id: string
|
||||||
tenantId: string
|
tenantId: string
|
||||||
@@ -329,7 +306,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
|||||||
|
|
||||||
const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2019-11-01');
|
const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2019-11-01');
|
||||||
try {
|
try {
|
||||||
const tenantResponse = await this.makeGetRequest(token.token, tenantUri);
|
const tenantResponse = await this.makeGetRequest(tenantUri, token.token);
|
||||||
Logger.pii('getTenants', tenantResponse.data);
|
Logger.pii('getTenants', tenantResponse.data);
|
||||||
const tenants: Tenant[] = tenantResponse.data.value.map((tenantInfo: TenantResponse) => {
|
const tenants: Tenant[] = tenantResponse.data.value.map((tenantInfo: TenantResponse) => {
|
||||||
return {
|
return {
|
||||||
@@ -353,97 +330,88 @@ export abstract class AzureAuth implements vscode.Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getSubscriptions(account: AzureAccount): Promise<Subscription[]> {
|
//#endregion
|
||||||
interface SubscriptionResponse { // https://docs.microsoft.com/en-us/rest/api/resources/subscriptions/list
|
|
||||||
subscriptionId: string
|
|
||||||
tenantId: string
|
|
||||||
displayName: string
|
|
||||||
}
|
|
||||||
const allSubs: Subscription[] = [];
|
|
||||||
const tokens = await this.getSecurityToken(account, azdata.AzureResource.ResourceManagement);
|
|
||||||
if (!tokens) {
|
|
||||||
Logger.log('There were no resource management tokens to retrieve subscriptions from. Account is stale.');
|
|
||||||
account.isStale = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const tenant of account.properties.tenants) {
|
//#region token management
|
||||||
const token = tokens[tenant.id];
|
private async saveToken(tenant: Tenant, resource: Resource, accountKey: azdata.AccountKey, { accessToken, refreshToken, expiresOn }: OAuthTokenResponse) {
|
||||||
const subscriptionUri = url.resolve(this.metadata.settings.armResource.endpoint, 'subscriptions?api-version=2019-11-01');
|
const msg = localize('azure.cacheErrorAdd', "Error when adding your account to the cache.");
|
||||||
|
if (!tenant.id || !resource.id) {
|
||||||
|
Logger.pii('Tenant ID or resource ID was undefined', tenant, resource);
|
||||||
|
throw new AzureAuthError(msg, 'Adding account to cache failed', undefined);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const subscriptionResponse = await this.makeGetRequest(token.token, subscriptionUri);
|
await this.tokenCache.saveCredential(`${accountKey.accountId}_access_${resource.id}_${tenant.id}`, JSON.stringify(accessToken));
|
||||||
Logger.pii('getSubscriptions', subscriptionResponse.data);
|
await this.tokenCache.saveCredential(`${accountKey.accountId}_refresh_${resource.id}_${tenant.id}`, JSON.stringify(refreshToken));
|
||||||
const subscriptions: Subscription[] = subscriptionResponse.data.value.map((subscriptionInfo: SubscriptionResponse) => {
|
this.memdb.set(`${accountKey.accountId}_${tenant.id}_${resource.id}`, expiresOn);
|
||||||
return {
|
|
||||||
id: subscriptionInfo.subscriptionId,
|
|
||||||
displayName: subscriptionInfo.displayName,
|
|
||||||
tenantId: subscriptionInfo.tenantId
|
|
||||||
} as Subscription;
|
|
||||||
});
|
|
||||||
allSubs.push(...subscriptions);
|
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger.error(ex);
|
Logger.error(ex);
|
||||||
throw new Error('Error retrieving subscription information');
|
throw new AzureAuthError(msg, 'Adding account to cache failed', ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return allSubs;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getToken(postData: { [key: string]: string }, tenant = this.commonTenant, resourceId: string = '', resourceEndpoint: string = ''): Promise<TokenRefreshResponse | undefined> {
|
public async getSavedToken(tenant: Tenant, resource: Resource, accountKey: azdata.AccountKey): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> {
|
||||||
|
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.pii('Tenant ID or resource ID was undefined', tenant, resource);
|
||||||
|
throw new AzureAuthError(getMsg, 'Getting account from cache failed', undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
let accessTokenString: string;
|
||||||
|
let refreshTokenString: string;
|
||||||
|
let expiresOn: string;
|
||||||
try {
|
try {
|
||||||
let refreshResponse: TokenRefreshResponse;
|
accessTokenString = await this.tokenCache.getCredential(`${accountKey.accountId}_access_${resource.id}_${tenant.id}`);
|
||||||
|
refreshTokenString = await this.tokenCache.getCredential(`${accountKey.accountId}_refresh_${resource.id}_${tenant.id}`);
|
||||||
try {
|
expiresOn = this.memdb.get(`${accountKey.accountId}_${tenant.id}_${resource.id}`);
|
||||||
const tokenUrl = `${this.loginEndpointUrl}${tenant.id}/oauth2/token`;
|
|
||||||
const tokenResponse = await this.makePostRequest(tokenUrl, postData);
|
|
||||||
Logger.pii(JSON.stringify(tokenResponse.data));
|
|
||||||
const tokenClaims = this.getTokenClaims(tokenResponse.data.access_token);
|
|
||||||
|
|
||||||
const accessToken: AccessToken = {
|
|
||||||
token: tokenResponse.data.access_token,
|
|
||||||
key: tokenClaims.oid ?? tokenClaims.email ?? tokenClaims.unique_name ?? tokenClaims.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshToken: RefreshToken = {
|
|
||||||
token: tokenResponse.data.refresh_token,
|
|
||||||
key: accessToken.key
|
|
||||||
};
|
|
||||||
const expiresOn = tokenResponse.data.expires_on;
|
|
||||||
|
|
||||||
refreshResponse = { accessToken, refreshToken, tokenClaims, expiresOn };
|
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger.pii(JSON.stringify(ex?.response?.data));
|
Logger.error(ex);
|
||||||
if (ex?.response?.data?.error === 'interaction_required') {
|
throw new AzureAuthError(getMsg, 'Getting account from cache failed', ex);
|
||||||
const shouldOpenLink = await this.openConsentDialog(tenant, resourceId);
|
|
||||||
if (shouldOpenLink === true) {
|
|
||||||
const { tokenRefreshResponse, authCompleteDeferred } = await this.promptForConsent(resourceEndpoint, tenant.id);
|
|
||||||
refreshResponse = tokenRefreshResponse;
|
|
||||||
authCompleteDeferred.resolve();
|
|
||||||
} else {
|
|
||||||
vscode.window.showInformationMessage(localize('azure.noConsentToReauth', "The authentication failed since Azure Data Studio was unable to open re-authentication page."));
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
try {
|
||||||
|
if (!accessTokenString) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
const accessToken: AccessToken = JSON.parse(accessTokenString);
|
||||||
|
let refreshToken: RefreshToken;
|
||||||
|
if (refreshTokenString) {
|
||||||
|
refreshToken = JSON.parse(refreshTokenString);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.memdb.set(this.createMemdbString(refreshResponse.accessToken.key, tenant.id, resourceId), refreshResponse.expiresOn);
|
return {
|
||||||
return refreshResponse;
|
accessToken, refreshToken, expiresOn
|
||||||
} catch (err) {
|
};
|
||||||
const msg = localize('azure.noToken', "Retrieving the Azure token failed. Please sign in again.");
|
} catch (ex) {
|
||||||
vscode.window.showErrorMessage(msg);
|
Logger.error(ex);
|
||||||
throw new Error(err);
|
throw new AzureAuthError(parseMsg, 'Parsing account from cache failed', ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region interaction handling
|
||||||
|
|
||||||
|
public async handleInteractionRequired(tenant: Tenant, resource: Resource): Promise<OAuthTokenResponse | undefined> {
|
||||||
|
const shouldOpen = await this.askUserForInteraction(tenant, resource);
|
||||||
|
if (shouldOpen) {
|
||||||
|
const result = await this.login(tenant, resource);
|
||||||
|
result?.authComplete?.resolve();
|
||||||
|
return result?.response;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
public async openConsentDialog(tenant: Tenant, resourceId: string): Promise<boolean> {
|
/**
|
||||||
|
* Asks the user if they would like to do the interaction based authentication as required by OAuth2
|
||||||
|
* @param tenant
|
||||||
|
* @param resource
|
||||||
|
*/
|
||||||
|
private async askUserForInteraction(tenant: Tenant, resource: Resource): Promise<boolean> {
|
||||||
if (!tenant.displayName && !tenant.id) {
|
if (!tenant.displayName && !tenant.id) {
|
||||||
throw new Error('Tenant did not have display name or id');
|
throw new Error('Tenant did not have display name or id');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tenant.id === 'common') {
|
|
||||||
throw new Error('Common tenant should not need consent');
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTenantConfigurationSet = (): Set<string> => {
|
const getTenantConfigurationSet = (): Set<string> => {
|
||||||
const configuration = vscode.workspace.getConfiguration('azure.tenant.config');
|
const configuration = vscode.workspace.getConfiguration('azure.tenant.config');
|
||||||
let values: string[] = configuration.get('filter') ?? [];
|
let values: string[] = configuration.get('filter') ?? [];
|
||||||
@@ -486,7 +454,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
const messageBody = localize('azurecore.consentDialog.body', "Your tenant '{0} ({1})' requires you to re-authenticate again to access {2} resources. Press Open to start the authentication process.", tenant.displayName, tenant.id, resourceId);
|
const messageBody = localize('azurecore.consentDialog.body', "Your tenant '{0} ({1})' requires you to re-authenticate again to access {2} resources. Press Open to start the authentication process.", tenant.displayName, tenant.id, resource.id);
|
||||||
const result = await vscode.window.showInformationMessage(messageBody, { modal: true }, openItem, closeItem, dontAskAgainItem);
|
const result = await vscode.window.showInformationMessage(messageBody, { modal: true }, openItem, closeItem, dontAskAgainItem);
|
||||||
|
|
||||||
if (result.action) {
|
if (result.action) {
|
||||||
@@ -495,113 +463,9 @@ export abstract class AzureAuth implements vscode.Disposable {
|
|||||||
|
|
||||||
return result.booleanResult;
|
return result.booleanResult;
|
||||||
}
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
protected getTokenClaims(accessToken: string): TokenClaims | undefined {
|
//#region data modeling
|
||||||
try {
|
|
||||||
const split = accessToken.split('.');
|
|
||||||
return JSON.parse(Buffer.from(split[1], 'base64').toString('binary'));
|
|
||||||
} catch (ex) {
|
|
||||||
throw new Error('Unable to read token claims: ' + JSON.stringify(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async refreshAccessToken(account: azdata.AccountKey, rt: RefreshToken, tenant: Tenant = this.commonTenant, resource?: Resource): Promise<TokenRefreshResponse> {
|
|
||||||
const postData: { [key: string]: string } = {
|
|
||||||
grant_type: 'refresh_token',
|
|
||||||
refresh_token: rt.token,
|
|
||||||
client_id: this.clientId,
|
|
||||||
tenant: tenant.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (resource) {
|
|
||||||
postData.resource = resource.endpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTokenResponse = await this.getToken(postData, tenant, resource?.id, resource?.endpoint);
|
|
||||||
|
|
||||||
const accessToken = getTokenResponse?.accessToken;
|
|
||||||
const refreshToken = getTokenResponse?.refreshToken;
|
|
||||||
|
|
||||||
if (!accessToken || !refreshToken) {
|
|
||||||
Logger.log('Access or refresh token were undefined');
|
|
||||||
const msg = localize('azure.refreshTokenError', "Error when refreshing your account.");
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.setCachedToken(account, accessToken, refreshToken, resource?.id, tenant?.id);
|
|
||||||
|
|
||||||
return getTokenResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async setCachedToken(account: azdata.AccountKey, accessToken: AccessToken, refreshToken: RefreshToken, resourceId?: string, tenantId?: string): Promise<void> {
|
|
||||||
const msg = localize('azure.cacheErrorAdd', "Error when adding your account to the cache.");
|
|
||||||
resourceId = resourceId ?? '';
|
|
||||||
tenantId = tenantId ?? '';
|
|
||||||
if (!accessToken || !accessToken.token || !refreshToken.token || !accessToken.key) {
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.tokenCache.saveCredential(`${account.accountId}_access_${resourceId}_${tenantId}`, JSON.stringify(accessToken));
|
|
||||||
await this.tokenCache.saveCredential(`${account.accountId}_refresh_${resourceId}_${tenantId}`, JSON.stringify(refreshToken));
|
|
||||||
} catch (ex) {
|
|
||||||
Logger.error('Error when storing tokens.', ex);
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getCachedToken(account: azdata.AccountKey, resourceId?: string, tenantId?: string): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken } | undefined> {
|
|
||||||
resourceId = resourceId ?? '';
|
|
||||||
tenantId = tenantId ?? '';
|
|
||||||
|
|
||||||
let accessToken: AccessToken;
|
|
||||||
let refreshToken: RefreshToken;
|
|
||||||
try {
|
|
||||||
accessToken = JSON.parse(await this.tokenCache.getCredential(`${account.accountId}_access_${resourceId}_${tenantId}`));
|
|
||||||
refreshToken = JSON.parse(await this.tokenCache.getCredential(`${account.accountId}_refresh_${resourceId}_${tenantId}`));
|
|
||||||
} catch (ex) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!accessToken || !refreshToken) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!refreshToken.token || !refreshToken.key) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!accessToken.token || !accessToken.key) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
accessToken,
|
|
||||||
refreshToken
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public createMemdbString(accountKey: string, tenantId: string, resourceId: string): string {
|
|
||||||
return `${accountKey}_${tenantId}_${resourceId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deleteAccountCache(account: azdata.AccountKey): Promise<void> {
|
|
||||||
const results = await this.tokenCache.findCredentials(account.accountId);
|
|
||||||
|
|
||||||
for (let { account } of results) {
|
|
||||||
await this.tokenCache.clearCredential(account);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deleteAllCache(): Promise<void> {
|
|
||||||
const results = await this.tokenCache.findCredentials('');
|
|
||||||
|
|
||||||
for (let { account } of results) {
|
|
||||||
await this.tokenCache.clearCredential(account);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public createAccount(tokenClaims: TokenClaims, key: string, tenants: Tenant[]): AzureAccount {
|
public createAccount(tokenClaims: TokenClaims, key: string, tenants: Tenant[]): AzureAccount {
|
||||||
// Determine if this is a microsoft account
|
// Determine if this is a microsoft account
|
||||||
@@ -663,4 +527,184 @@ export abstract class AzureAuth implements vscode.Disposable {
|
|||||||
|
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#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);
|
||||||
|
Logger.pii(url, postData, response.data);
|
||||||
|
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);
|
||||||
|
Logger.pii(url, response.data);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region inconsequential
|
||||||
|
protected getTokenClaims(accessToken: string): TokenClaims | undefined {
|
||||||
|
try {
|
||||||
|
const split = accessToken.split('.');
|
||||||
|
return JSON.parse(Buffer.from(split[1], 'base64').toString('binary'));
|
||||||
|
} catch (ex) {
|
||||||
|
throw new Error('Unable to read token claims: ' + JSON.stringify(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toBase64UrlEncoding(base64string: string): string {
|
||||||
|
return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteAllCache(): Promise<void> {
|
||||||
|
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 {
|
||||||
|
return this.deleteAccountCache(account);
|
||||||
|
} catch (ex) {
|
||||||
|
const msg = localize('azure.cacheErrrorRemove', "Error when removing your account from the cache.");
|
||||||
|
vscode.window.showErrorMessage(msg);
|
||||||
|
Logger.error('Error when removing tokens.', ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteAccountCache(account: azdata.AccountKey): Promise<void> {
|
||||||
|
const results = await this.tokenCache.findCredentials(account.accountId);
|
||||||
|
|
||||||
|
for (let { account } of results) {
|
||||||
|
await this.tokenCache.clearCredential(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async dispose() { }
|
||||||
|
|
||||||
|
public async autoOAuthCancelled(): Promise<void> { }
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region models
|
||||||
|
|
||||||
|
export interface AccountKey {
|
||||||
|
/**
|
||||||
|
* Account Key - uniquely identifies an account
|
||||||
|
*/
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessToken extends AccountKey {
|
||||||
|
/**
|
||||||
|
* Access Token
|
||||||
|
*/
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshToken extends AccountKey {
|
||||||
|
/**
|
||||||
|
* Refresh Token
|
||||||
|
*/
|
||||||
|
token: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account Key
|
||||||
|
*/
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MultiTenantTokenResponse {
|
||||||
|
[tenantId: string]: Token
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Token extends AccountKey {
|
||||||
|
/**
|
||||||
|
* Access token
|
||||||
|
*/
|
||||||
|
token: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TokenType
|
||||||
|
*/
|
||||||
|
tokenType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenClaims { // https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens
|
||||||
|
aud: string;
|
||||||
|
iss: string;
|
||||||
|
iat: number;
|
||||||
|
idp: string,
|
||||||
|
nbf: number;
|
||||||
|
exp: number;
|
||||||
|
c_hash: string;
|
||||||
|
at_hash: string;
|
||||||
|
aio: string;
|
||||||
|
preferred_username: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
nonce: string;
|
||||||
|
oid: string;
|
||||||
|
roles: string[];
|
||||||
|
rh: string;
|
||||||
|
sub: string;
|
||||||
|
tid: string;
|
||||||
|
unique_name: string;
|
||||||
|
uti: string;
|
||||||
|
ver: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OAuthTokenResponse = { accessToken: AccessToken, refreshToken: RefreshToken, tokenClaims: TokenClaims, expiresOn: string };
|
||||||
|
|
||||||
|
export interface TokenPostData {
|
||||||
|
grant_type: 'refresh_token' | 'authorization_code' | 'urn:ietf:params:oauth:grant-type:device_code';
|
||||||
|
client_id: string;
|
||||||
|
resource: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshTokenPostData extends TokenPostData {
|
||||||
|
grant_type: 'refresh_token';
|
||||||
|
refresh_token: string;
|
||||||
|
client_id: string;
|
||||||
|
tenant: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthorizationCodePostData extends TokenPostData {
|
||||||
|
grant_type: 'authorization_code';
|
||||||
|
code: string;
|
||||||
|
code_verifier: string;
|
||||||
|
redirect_uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceCodeStartPostData extends Omit<TokenPostData, 'grant_type'> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceCodeCheckPostData extends Omit<TokenPostData, 'resource'> {
|
||||||
|
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
||||||
|
tenant: string,
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|||||||
@@ -3,49 +3,37 @@
|
|||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as azdata from 'azdata';
|
import { AuthorizationCodePostData, AzureAuth, OAuthTokenResponse } from './azureAuth';
|
||||||
|
import { AzureAccountProviderMetadata, AzureAuthType, Deferred, Resource, Tenant } from '../interfaces';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
import { SimpleTokenCache } from '../simpleTokenCache';
|
||||||
|
import { SimpleWebServer } from '../utils/simpleWebServer';
|
||||||
|
import { AzureAuthError } from './azureAuthError';
|
||||||
|
import { Logger } from '../../utils/Logger';
|
||||||
import * as nls from 'vscode-nls';
|
import * as nls from 'vscode-nls';
|
||||||
import { promises as fs } from 'fs';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as qs from 'qs';
|
import * as qs from 'qs';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
|
||||||
import {
|
|
||||||
AzureAuth,
|
|
||||||
AccessToken,
|
|
||||||
RefreshToken,
|
|
||||||
TokenClaims,
|
|
||||||
TokenRefreshResponse,
|
|
||||||
} from './azureAuth';
|
|
||||||
|
|
||||||
import {
|
|
||||||
AzureAccountProviderMetadata,
|
|
||||||
AzureAuthType,
|
|
||||||
Deferred
|
|
||||||
} from '../interfaces';
|
|
||||||
|
|
||||||
import { SimpleWebServer } from '../utils/simpleWebServer';
|
|
||||||
import { SimpleTokenCache } from '../simpleTokenCache';
|
|
||||||
import { Logger } from '../../utils/Logger';
|
|
||||||
const localize = nls.loadMessageBundle();
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
function parseQuery(uri: vscode.Uri) {
|
interface AuthCodeResponse {
|
||||||
return uri.query.split('&').reduce((prev: any, current) => {
|
authCode: string;
|
||||||
const queryString = current.split('=');
|
codeVerifier: string;
|
||||||
prev[queryString[0]] = queryString[1];
|
redirectUri: string;
|
||||||
return prev;
|
|
||||||
}, {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthCodeResponse {
|
interface CryptoValues {
|
||||||
authCode: string,
|
nonce: string;
|
||||||
codeVerifier: string
|
codeVerifier: string;
|
||||||
|
codeChallenge: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class AzureAuthCodeGrant extends AzureAuth {
|
export class AzureAuthCodeGrant extends AzureAuth {
|
||||||
private static readonly USER_FRIENDLY_NAME: string = localize('azure.azureAuthCodeGrantName', "Azure Auth Code Grant");
|
private static readonly USER_FRIENDLY_NAME: string = localize('azure.azureAuthCodeGrantName', 'Azure Auth Code Grant');
|
||||||
private server: SimpleWebServer;
|
private server: SimpleWebServer;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -57,105 +45,52 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
|||||||
super(metadata, tokenCache, context, uriEventEmitter, AzureAuthType.AuthCodeGrant, AzureAuthCodeGrant.USER_FRIENDLY_NAME);
|
super(metadata, tokenCache, context, uriEventEmitter, AzureAuthType.AuthCodeGrant, AzureAuthCodeGrant.USER_FRIENDLY_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async promptForConsent(resourceEndpoint: string, tenant: string = this.commonTenant.id): Promise<{ tokenRefreshResponse: TokenRefreshResponse, authCompleteDeferred: Deferred<void> } | undefined> {
|
|
||||||
|
protected async login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse, authComplete: Deferred<void> }> {
|
||||||
let authCompleteDeferred: Deferred<void>;
|
let authCompleteDeferred: Deferred<void>;
|
||||||
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
|
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
|
||||||
|
|
||||||
let authResponse: AuthCodeResponse;
|
let authResponse: AuthCodeResponse;
|
||||||
|
|
||||||
if (vscode.env.uiKind === vscode.UIKind.Web) {
|
if (vscode.env.uiKind === vscode.UIKind.Web) {
|
||||||
authResponse = await this.loginWithoutLocalServer(resourceEndpoint, tenant);
|
authResponse = await this.loginWeb(tenant, resource);
|
||||||
} else {
|
} else {
|
||||||
authResponse = await this.loginWithLocalServer(authCompletePromise, resourceEndpoint, tenant);
|
authResponse = await this.loginDesktop(tenant, resource, authCompletePromise);
|
||||||
}
|
|
||||||
|
|
||||||
let tokenClaims: TokenClaims;
|
|
||||||
let accessToken: AccessToken;
|
|
||||||
let refreshToken: RefreshToken;
|
|
||||||
let expiresOn: string;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { accessToken: at, refreshToken: rt, tokenClaims: tc, expiresOn: eo } = await this.getTokenWithAuthCode(authResponse.authCode, authResponse.codeVerifier, this.redirectUri);
|
|
||||||
tokenClaims = tc;
|
|
||||||
accessToken = at;
|
|
||||||
refreshToken = rt;
|
|
||||||
expiresOn = eo;
|
|
||||||
} catch (ex) {
|
|
||||||
if (ex.msg) {
|
|
||||||
vscode.window.showErrorMessage(ex.msg);
|
|
||||||
}
|
|
||||||
Logger.error(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
const msg = localize('azure.tokenFail', "Failure when retrieving tokens.");
|
|
||||||
authCompleteDeferred.reject(new Error(msg));
|
|
||||||
throw Error('Failure when retrieving tokens');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tokenRefreshResponse: { accessToken, refreshToken, tokenClaims, expiresOn },
|
response: await this.getTokenWithAuthorizationCode(tenant, resource, authResponse),
|
||||||
authCompleteDeferred
|
authComplete: authCompleteDeferred
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async autoOAuthCancelled(): Promise<void> {
|
/**
|
||||||
return this.server.shutdown();
|
* Requests an OAuthTokenResponse from Microsoft OAuth
|
||||||
}
|
*
|
||||||
|
* @param tenant
|
||||||
public async loginWithLocalServer(authCompletePromise: Promise<void>, resourceId: string, tenant: string = this.commonTenant.id): Promise<AuthCodeResponse | undefined> {
|
* @param resource
|
||||||
this.server = new SimpleWebServer();
|
* @param authCode
|
||||||
const nonce = crypto.randomBytes(16).toString('base64');
|
* @param redirectUri
|
||||||
let serverPort: string;
|
* @param codeVerifier
|
||||||
|
*/
|
||||||
try {
|
private async getTokenWithAuthorizationCode(tenant: Tenant, resource: Resource, { authCode, redirectUri, codeVerifier }: AuthCodeResponse): Promise<OAuthTokenResponse | undefined> {
|
||||||
serverPort = await this.server.startup();
|
const postData: AuthorizationCodePostData = {
|
||||||
} catch (err) {
|
grant_type: 'authorization_code',
|
||||||
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.');
|
code: authCode,
|
||||||
vscode.window.showErrorMessage(msg);
|
|
||||||
Logger.error(JSON.stringify(err));
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The login code to use
|
|
||||||
let loginUrl: string;
|
|
||||||
let codeVerifier: string;
|
|
||||||
{
|
|
||||||
codeVerifier = this.toBase64UrlEncoding(crypto.randomBytes(32).toString('base64'));
|
|
||||||
const state = `${serverPort},${encodeURIComponent(nonce)}`;
|
|
||||||
const codeChallenge = this.toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64'));
|
|
||||||
const loginQuery = {
|
|
||||||
response_type: 'code',
|
|
||||||
response_mode: 'query',
|
|
||||||
client_id: this.clientId,
|
client_id: this.clientId,
|
||||||
redirect_uri: this.redirectUri,
|
code_verifier: codeVerifier,
|
||||||
state,
|
redirect_uri: redirectUri,
|
||||||
prompt: 'select_account',
|
resource: resource.endpoint
|
||||||
code_challenge_method: 'S256',
|
|
||||||
code_challenge: codeChallenge,
|
|
||||||
resource: resourceId
|
|
||||||
};
|
};
|
||||||
loginUrl = `${this.loginEndpointUrl}${tenant}/oauth2/authorize?${qs.stringify(loginQuery)}`;
|
|
||||||
|
return this.getToken(tenant, resource, postData);
|
||||||
}
|
}
|
||||||
|
|
||||||
await vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${serverPort}/signin?nonce=${encodeURIComponent(nonce)}`));
|
private async loginWeb(tenant: Tenant, resource: Resource): Promise<AuthCodeResponse> {
|
||||||
|
|
||||||
const authCode = await this.addServerListeners(this.server, nonce, loginUrl, authCompletePromise);
|
|
||||||
|
|
||||||
return {
|
|
||||||
authCode,
|
|
||||||
codeVerifier
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async loginWithoutLocalServer(resourceId: string, tenant: string = this.commonTenant.id): Promise<AuthCodeResponse | undefined> {
|
|
||||||
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://microsoft.azurecore`));
|
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://microsoft.azurecore`));
|
||||||
const nonce = crypto.randomBytes(16).toString('base64');
|
const { nonce, codeVerifier, codeChallenge } = this.createCryptoValues();
|
||||||
const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
|
const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
|
||||||
const state = `${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`;
|
const state = `${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`;
|
||||||
|
|
||||||
const codeVerifier = this.toBase64UrlEncoding(crypto.randomBytes(32).toString('base64'));
|
|
||||||
const codeChallenge = this.toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64'));
|
|
||||||
|
|
||||||
const loginQuery = {
|
const loginQuery = {
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
response_mode: 'query',
|
response_mode: 'query',
|
||||||
@@ -165,26 +100,27 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
|||||||
prompt: 'select_account',
|
prompt: 'select_account',
|
||||||
code_challenge_method: 'S256',
|
code_challenge_method: 'S256',
|
||||||
code_challenge: codeChallenge,
|
code_challenge: codeChallenge,
|
||||||
resource: resourceId
|
resource: resource.id
|
||||||
};
|
};
|
||||||
|
|
||||||
const signInUrl = `${this.loginEndpointUrl}${tenant}/oauth2/authorize?${qs.stringify(loginQuery)}`;
|
const signInUrl = `${this.loginEndpointUrl}${tenant}/oauth2/authorize?${qs.stringify(loginQuery)}`;
|
||||||
await vscode.env.openExternal(vscode.Uri.parse(signInUrl));
|
await vscode.env.openExternal(vscode.Uri.parse(signInUrl));
|
||||||
|
|
||||||
const authCode = await this.handleCodeResponse(state);
|
const authCode = await this.handleWebResponse(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authCode,
|
authCode,
|
||||||
codeVerifier
|
codeVerifier,
|
||||||
|
redirectUri: this.redirectUri
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleCodeResponse(state: string): Promise<string> {
|
private async handleWebResponse(state: string): Promise<string> {
|
||||||
let uriEventListener: vscode.Disposable;
|
let uriEventListener: vscode.Disposable;
|
||||||
return new Promise((resolve: (value: any) => void, reject) => {
|
return new Promise((resolve: (value: any) => void, reject) => {
|
||||||
uriEventListener = this.uriEventEmitter.event(async (uri: vscode.Uri) => {
|
uriEventListener = this.uriEventEmitter.event(async (uri: vscode.Uri) => {
|
||||||
try {
|
try {
|
||||||
const query = parseQuery(uri);
|
const query = this.parseQuery(uri);
|
||||||
const code = query.code;
|
const code = query.code;
|
||||||
if (query.state !== state && decodeURIComponent(query.state) !== state) {
|
if (query.state !== state && decodeURIComponent(query.state) !== state) {
|
||||||
reject(new Error('State mismatch'));
|
reject(new Error('State mismatch'));
|
||||||
@@ -200,33 +136,46 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async login(): Promise<azdata.Account | azdata.PromptFailedResult> {
|
private parseQuery(uri: vscode.Uri): { [key: string]: string } {
|
||||||
const { tokenRefreshResponse, authCompleteDeferred } = await this.promptForConsent(this.metadata.settings.signInResourceId);
|
return uri.query.split('&').reduce((prev: any, current) => {
|
||||||
const { accessToken, refreshToken, tokenClaims } = tokenRefreshResponse;
|
const queryString = current.split('=');
|
||||||
|
prev[queryString[0]] = queryString[1];
|
||||||
|
return prev;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
const tenants = await this.getTenants(accessToken);
|
private async loginDesktop(tenant: Tenant, resource: Resource, authCompletePromise: Promise<void>): Promise<AuthCodeResponse> {
|
||||||
|
this.server = new SimpleWebServer();
|
||||||
|
let serverPort: string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.setCachedToken({ accountId: accessToken.key, providerId: this.metadata.id }, accessToken, refreshToken);
|
serverPort = await this.server.startup();
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger.error(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.');
|
||||||
if (ex.msg) {
|
throw new AzureAuthError(msg, 'Server could not start', ex);
|
||||||
vscode.window.showErrorMessage(ex.msg);
|
|
||||||
authCompleteDeferred.reject(ex);
|
|
||||||
} else {
|
|
||||||
authCompleteDeferred.reject(new Error('There was an issue when storing the cache.'));
|
|
||||||
}
|
}
|
||||||
|
const { nonce, codeVerifier, codeChallenge } = this.createCryptoValues();
|
||||||
|
const state = `${serverPort},${encodeURIComponent(nonce)}`;
|
||||||
|
const loginQuery = {
|
||||||
|
response_type: 'code',
|
||||||
|
response_mode: 'query',
|
||||||
|
client_id: this.clientId,
|
||||||
|
redirect_uri: this.redirectUri,
|
||||||
|
state,
|
||||||
|
prompt: 'select_account',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
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(this.server, nonce, loginUrl, authCompletePromise);
|
||||||
|
return {
|
||||||
|
authCode,
|
||||||
|
codeVerifier,
|
||||||
|
redirectUri: this.redirectUri
|
||||||
|
};
|
||||||
|
|
||||||
return { canceled: false } as azdata.PromptFailedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
const account = this.createAccount(tokenClaims, accessToken.key, tenants);
|
|
||||||
|
|
||||||
const subscriptions = await this.getSubscriptions(account);
|
|
||||||
account.properties.subscriptions = subscriptions;
|
|
||||||
|
|
||||||
authCompleteDeferred.resolve();
|
|
||||||
return account;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async addServerListeners(server: SimpleWebServer, nonce: string, loginUrl: string, authComplete: Promise<void>): Promise<string> {
|
private async addServerListeners(server: SimpleWebServer, nonce: string, loginUrl: string, authComplete: Promise<void>): Promise<string> {
|
||||||
@@ -266,7 +215,7 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
|||||||
|
|
||||||
if (receivedNonce !== nonce) {
|
if (receivedNonce !== nonce) {
|
||||||
res.writeHead(400, { 'content-type': 'text/html' });
|
res.writeHead(400, { 'content-type': 'text/html' });
|
||||||
res.write(localize('azureAuth.nonceError', "Authentication failed due to a nonce mismatch, please close Azure Data Studio and try again."));
|
res.write(localize('azureAuth.nonceError', 'Authentication failed due to a nonce mismatch, please close Azure Data Studio and try again.'));
|
||||||
res.end();
|
res.end();
|
||||||
Logger.error('nonce no match', receivedNonce, nonce);
|
Logger.error('nonce no match', receivedNonce, nonce);
|
||||||
return;
|
return;
|
||||||
@@ -283,7 +232,7 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
|||||||
const stateSplit = state.split(',');
|
const stateSplit = state.split(',');
|
||||||
if (stateSplit.length !== 2) {
|
if (stateSplit.length !== 2) {
|
||||||
res.writeHead(400, { 'content-type': 'text/html' });
|
res.writeHead(400, { 'content-type': 'text/html' });
|
||||||
res.write(localize('azureAuth.stateError', "Authentication failed due to a state mismatch, please close ADS and try again."));
|
res.write(localize('azureAuth.stateError', 'Authentication failed due to a state mismatch, please close ADS and try again.'));
|
||||||
res.end();
|
res.end();
|
||||||
reject(new Error('State mismatch'));
|
reject(new Error('State mismatch'));
|
||||||
return;
|
return;
|
||||||
@@ -291,7 +240,7 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
|||||||
|
|
||||||
if (stateSplit[1] !== encodeURIComponent(nonce)) {
|
if (stateSplit[1] !== encodeURIComponent(nonce)) {
|
||||||
res.writeHead(400, { 'content-type': 'text/html' });
|
res.writeHead(400, { 'content-type': 'text/html' });
|
||||||
res.write(localize('azureAuth.nonceError', "Authentication failed due to a nonce mismatch, please close Azure Data Studio and try again."));
|
res.write(localize('azureAuth.nonceError', 'Authentication failed due to a nonce mismatch, please close Azure Data Studio and try again.'));
|
||||||
res.end();
|
res.end();
|
||||||
reject(new Error('Nonce mismatch'));
|
reject(new Error('Nonce mismatch'));
|
||||||
return;
|
return;
|
||||||
@@ -310,20 +259,14 @@ export class AzureAuthCodeGrant extends AzureAuth {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getTokenWithAuthCode(authCode: string, codeVerifier: string, redirectUri: string): Promise<TokenRefreshResponse | undefined> {
|
|
||||||
const postData = {
|
private createCryptoValues(): CryptoValues {
|
||||||
grant_type: 'authorization_code',
|
const nonce = crypto.randomBytes(16).toString('base64');
|
||||||
code: authCode,
|
const codeVerifier = this.toBase64UrlEncoding(crypto.randomBytes(32).toString('base64'));
|
||||||
client_id: this.clientId,
|
const codeChallenge = this.toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64'));
|
||||||
code_verifier: codeVerifier,
|
|
||||||
redirect_uri: redirectUri,
|
return {
|
||||||
resource: this.metadata.settings.signInResourceId
|
nonce, codeVerifier, codeChallenge
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.getToken(postData);
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose() {
|
|
||||||
this.server?.shutdown().catch(console.error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
export class AzureAuthError extends Error {
|
||||||
|
private readonly _originalMessage: string;
|
||||||
|
|
||||||
|
constructor(localizedMessage: string, _originalMessage: string, private readonly originalException: any) {
|
||||||
|
super(localizedMessage);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
get originalMessage(): string {
|
||||||
|
return this._originalMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrintableString(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
originalMessage: this.originalMessage,
|
||||||
|
originalException: this.originalException
|
||||||
|
}, undefined, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,21 +8,24 @@ import * as vscode from 'vscode';
|
|||||||
import * as nls from 'vscode-nls';
|
import * as nls from 'vscode-nls';
|
||||||
import {
|
import {
|
||||||
AzureAuth,
|
AzureAuth,
|
||||||
TokenClaims,
|
OAuthTokenResponse,
|
||||||
AccessToken,
|
DeviceCodeStartPostData,
|
||||||
RefreshToken,
|
DeviceCodeCheckPostData,
|
||||||
|
|
||||||
} from './azureAuth';
|
} from './azureAuth';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AzureAccountProviderMetadata,
|
AzureAccountProviderMetadata,
|
||||||
AzureAccount,
|
|
||||||
AzureAuthType,
|
AzureAuthType,
|
||||||
|
Tenant,
|
||||||
|
Resource,
|
||||||
|
Deferred,
|
||||||
// Tenant,
|
// Tenant,
|
||||||
// Subscription
|
// Subscription
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
|
|
||||||
import { SimpleTokenCache } from '../simpleTokenCache';
|
import { SimpleTokenCache } from '../simpleTokenCache';
|
||||||
|
import { Logger } from '../../utils/Logger';
|
||||||
const localize = nls.loadMessageBundle();
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
interface DeviceCodeLogin { // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code
|
interface DeviceCodeLogin { // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code
|
||||||
@@ -43,7 +46,6 @@ interface DeviceCodeLoginResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AzureDeviceCode extends AzureAuth {
|
export class AzureDeviceCode extends AzureAuth {
|
||||||
|
|
||||||
private static readonly USER_FRIENDLY_NAME: string = localize('azure.azureDeviceCodeAuth', "Azure Device Code");
|
private static readonly USER_FRIENDLY_NAME: string = localize('azure.azureDeviceCodeAuth', "Azure Device Code");
|
||||||
private readonly pageTitle: string;
|
private readonly pageTitle: string;
|
||||||
constructor(
|
constructor(
|
||||||
@@ -56,19 +58,17 @@ export class AzureDeviceCode extends AzureAuth {
|
|||||||
this.pageTitle = localize('addAccount', "Add {0} account", this.metadata.displayName);
|
this.pageTitle = localize('addAccount', "Add {0} account", this.metadata.displayName);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
protected async login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse, authComplete: Deferred<void> }> {
|
||||||
|
let authCompleteDeferred: Deferred<void>;
|
||||||
|
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
|
||||||
|
|
||||||
public async promptForConsent(resourceId: string, tenant: string = this.commonTenant.id): Promise<undefined> {
|
const uri = `${this.loginEndpointUrl}/${this.commonTenant.id}/oauth2/devicecode`;
|
||||||
vscode.window.showErrorMessage(localize('azure.deviceCodeDoesNotSupportConsent', "Device code authentication does not support prompting for consent. Switch the authentication method in settings to code grant."));
|
const postData: DeviceCodeStartPostData = {
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async login(): Promise<AzureAccount | azdata.PromptFailedResult> {
|
|
||||||
try {
|
|
||||||
const uri = `${this.loginEndpointUrl}/${this.commonTenant}/oauth2/devicecode`;
|
|
||||||
const postResult = await this.makePostRequest(uri, {
|
|
||||||
client_id: this.clientId,
|
client_id: this.clientId,
|
||||||
resource: this.metadata.settings.signInResourceId
|
resource: resource.endpoint
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const postResult = await this.makePostRequest(uri, postData);
|
||||||
|
|
||||||
const initialDeviceLogin: DeviceCodeLogin = postResult.data;
|
const initialDeviceLogin: DeviceCodeLogin = postResult.data;
|
||||||
|
|
||||||
@@ -76,41 +76,25 @@ export class AzureDeviceCode extends AzureAuth {
|
|||||||
|
|
||||||
const finalDeviceLogin = await this.setupPolling(initialDeviceLogin);
|
const finalDeviceLogin = await this.setupPolling(initialDeviceLogin);
|
||||||
|
|
||||||
let tokenClaims: TokenClaims;
|
const accessTokenString = finalDeviceLogin.access_token;
|
||||||
let accessToken: AccessToken;
|
const refreshTokenString = finalDeviceLogin.refresh_token;
|
||||||
let refreshToken: RefreshToken;
|
|
||||||
// let tenants: Tenant[];
|
|
||||||
// let subscriptions: Subscription[];
|
|
||||||
|
|
||||||
tokenClaims = this.getTokenClaims(finalDeviceLogin.access_token);
|
const currentTime = new Date().getTime() / 1000;
|
||||||
|
const expiresOn = `${currentTime + finalDeviceLogin.expires_in}`;
|
||||||
|
|
||||||
accessToken = {
|
const result = await this.getTokenHelper(tenant, resource, accessTokenString, refreshTokenString, expiresOn);
|
||||||
token: finalDeviceLogin.access_token,
|
this.closeOnceComplete(authCompletePromise).catch(Logger.error);
|
||||||
key: tokenClaims.email || tokenClaims.unique_name || tokenClaims.name,
|
|
||||||
|
return {
|
||||||
|
response: result,
|
||||||
|
authComplete: authCompleteDeferred
|
||||||
};
|
};
|
||||||
|
|
||||||
refreshToken = {
|
|
||||||
token: finalDeviceLogin.refresh_token,
|
|
||||||
key: accessToken.key,
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.setCachedToken({ accountId: accessToken.key, providerId: this.metadata.id }, accessToken, refreshToken);
|
|
||||||
|
|
||||||
const tenants = await this.getTenants(accessToken);
|
|
||||||
const account = this.createAccount(tokenClaims, accessToken.key, tenants);
|
|
||||||
const subscriptions = await this.getSubscriptions(account);
|
|
||||||
account.properties.subscriptions = subscriptions;
|
|
||||||
return account;
|
|
||||||
} catch (ex) {
|
|
||||||
console.log(ex);
|
|
||||||
if (ex.msg) {
|
|
||||||
vscode.window.showErrorMessage(ex.msg);
|
|
||||||
}
|
}
|
||||||
return { canceled: false };
|
|
||||||
} finally {
|
private async closeOnceComplete(promise: Promise<void>): Promise<void> {
|
||||||
|
await promise;
|
||||||
azdata.accounts.endAutoOAuthDeviceCode();
|
azdata.accounts.endAutoOAuthDeviceCode();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private setupPolling(info: DeviceCodeLogin): Promise<DeviceCodeLoginResult> {
|
private setupPolling(info: DeviceCodeLogin): Promise<DeviceCodeLoginResult> {
|
||||||
@@ -141,14 +125,14 @@ export class AzureDeviceCode extends AzureAuth {
|
|||||||
const msg = localize('azure.deviceCodeCheckFail', "Error encountered when trying to check for login results");
|
const msg = localize('azure.deviceCodeCheckFail', "Error encountered when trying to check for login results");
|
||||||
try {
|
try {
|
||||||
const uri = `${this.loginEndpointUrl}/${this.commonTenant}/oauth2/token`;
|
const uri = `${this.loginEndpointUrl}/${this.commonTenant}/oauth2/token`;
|
||||||
const postData = {
|
const postData: DeviceCodeCheckPostData = {
|
||||||
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
||||||
client_id: this.clientId,
|
client_id: this.clientId,
|
||||||
tenant: this.commonTenant.id,
|
tenant: this.commonTenant.id,
|
||||||
code: info.device_code
|
code: info.device_code
|
||||||
};
|
};
|
||||||
|
|
||||||
const postResult = await this.makePostRequest(uri, postData, true);
|
const postResult = await this.makePostRequest(uri, postData);
|
||||||
|
|
||||||
const result: DeviceCodeLoginResult = postResult.data;
|
const result: DeviceCodeLoginResult = postResult.data;
|
||||||
|
|
||||||
@@ -164,5 +148,4 @@ export class AzureDeviceCode extends AzureAuth {
|
|||||||
public async autoOAuthCancelled(): Promise<void> {
|
public async autoOAuthCancelled(): Promise<void> {
|
||||||
return azdata.accounts.endAutoOAuthDeviceCode();
|
return azdata.accounts.endAutoOAuthDeviceCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,15 @@ import * as nls from 'vscode-nls';
|
|||||||
import {
|
import {
|
||||||
AzureAccountProviderMetadata,
|
AzureAccountProviderMetadata,
|
||||||
AzureAuthType,
|
AzureAuthType,
|
||||||
Deferred
|
Deferred,
|
||||||
|
AzureAccount
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
|
|
||||||
import { SimpleTokenCache } from './simpleTokenCache';
|
import { SimpleTokenCache } from './simpleTokenCache';
|
||||||
import { AzureAuth, TokenResponse } from './auths/azureAuth';
|
import { Logger } from '../utils/Logger';
|
||||||
|
import { MultiTenantTokenResponse, Token, AzureAuth } from './auths/azureAuth';
|
||||||
import { AzureAuthCodeGrant } from './auths/azureAuthCodeGrant';
|
import { AzureAuthCodeGrant } from './auths/azureAuthCodeGrant';
|
||||||
import { AzureDeviceCode } from './auths/azureDeviceCode';
|
import { AzureDeviceCode } from './auths/azureDeviceCode';
|
||||||
import { Logger } from '../utils/Logger';
|
|
||||||
|
|
||||||
const localize = nls.loadMessageBundle();
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
@@ -101,14 +102,29 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Thenable<TokenResponse | undefined> {
|
getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Thenable<MultiTenantTokenResponse | undefined> {
|
||||||
return this._getSecurityToken(account, resource);
|
return this._getSecurityToken(account, resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Promise<TokenResponse | undefined> {
|
getAccountSecurityToken(account: azdata.Account, tenant: string, resource: azdata.AzureResource): Thenable<Token | undefined> {
|
||||||
|
return this._getAccountSecurityToken(account, tenant, resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getAccountSecurityToken(account: azdata.Account, tenant: string, resource: azdata.AzureResource): Promise<Token | undefined> {
|
||||||
await this.initCompletePromise;
|
await this.initCompletePromise;
|
||||||
const azureAuth = this.getAuthMethod(undefined);
|
const azureAuth = this.getAuthMethod(undefined);
|
||||||
return azureAuth?.getSecurityToken(account, resource);
|
return azureAuth?.getAccountSecurityToken(account, tenant, resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Promise<MultiTenantTokenResponse | undefined> {
|
||||||
|
vscode.window.showInformationMessage(localize('azure.deprecatedGetSecurityToken', "A call was made to azdata.accounts.getSecurityToken, this method is deprecated and will be removed in future releases. Please use getAccountSecurityToken instead."));
|
||||||
|
const azureAccount = account as AzureAccount;
|
||||||
|
const response: MultiTenantTokenResponse = {};
|
||||||
|
for (const tenant of azureAccount.properties.tenants) {
|
||||||
|
response[tenant.id] = await this._getAccountSecurityToken(account, tenant.id, resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt(): Thenable<azdata.Account | azdata.PromptFailedResult> {
|
prompt(): Thenable<azdata.Account | azdata.PromptFailedResult> {
|
||||||
@@ -134,7 +150,7 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.authMappings.size === 1) {
|
if (this.authMappings.size === 1) {
|
||||||
return this.getAuthMethod(undefined).login();
|
return this.getAuthMethod(undefined).startLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: Option[] = [];
|
const options: Option[] = [];
|
||||||
@@ -150,7 +166,7 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
|
|||||||
return { canceled: true };
|
return { canceled: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
return pick.azureAuth.login();
|
return pick.azureAuth.startLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh(account: azdata.Account): Thenable<azdata.Account | azdata.PromptFailedResult> {
|
refresh(account: azdata.Account): Thenable<azdata.Account | azdata.PromptFailedResult> {
|
||||||
|
|||||||
@@ -64,11 +64,6 @@ interface Settings {
|
|||||||
*/
|
*/
|
||||||
clientId?: string;
|
clientId?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Identifier of the resource to request when signing in
|
|
||||||
*/
|
|
||||||
signInResourceId?: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information that describes the Microsoft resource management resource
|
* Information that describes the Microsoft resource management resource
|
||||||
*/
|
*/
|
||||||
@@ -177,10 +172,6 @@ interface AzureAccountProperties {
|
|||||||
*/
|
*/
|
||||||
tenants: Tenant[];
|
tenants: Tenant[];
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of subscriptions the user belongs to
|
|
||||||
*/
|
|
||||||
subscriptions?: Subscription[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Subscription {
|
export interface Subscription {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const publicAzureSettings: ProviderSettings = {
|
|||||||
settings: {
|
settings: {
|
||||||
host: 'https://login.microsoftonline.com/',
|
host: 'https://login.microsoftonline.com/',
|
||||||
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
||||||
signInResourceId: 'https://management.core.windows.net/',
|
|
||||||
microsoftResource: {
|
microsoftResource: {
|
||||||
id: 'marm',
|
id: 'marm',
|
||||||
endpoint: 'https://management.core.windows.net/',
|
endpoint: 'https://management.core.windows.net/',
|
||||||
@@ -72,7 +71,6 @@ const usGovAzureSettings: ProviderSettings = {
|
|||||||
settings: {
|
settings: {
|
||||||
host: 'https://login.microsoftonline.us/',
|
host: 'https://login.microsoftonline.us/',
|
||||||
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
||||||
signInResourceId: 'https://management.core.usgovcloudapi.net/',
|
|
||||||
microsoftResource: {
|
microsoftResource: {
|
||||||
id: 'marm',
|
id: 'marm',
|
||||||
endpoint: 'https://management.core.usgovcloudapi.net/',
|
endpoint: 'https://management.core.usgovcloudapi.net/',
|
||||||
@@ -121,7 +119,6 @@ const usNatAzureSettings: ProviderSettings = {
|
|||||||
settings: {
|
settings: {
|
||||||
host: 'https://login.microsoftonline.eaglex.ic.gov/',
|
host: 'https://login.microsoftonline.eaglex.ic.gov/',
|
||||||
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
||||||
signInResourceId: 'https://management.core.eaglex.ic.gov/',
|
|
||||||
microsoftResource: {
|
microsoftResource: {
|
||||||
id: 'marm',
|
id: 'marm',
|
||||||
endpoint: 'https://management.azure.eaglex.ic.gov/',
|
endpoint: 'https://management.azure.eaglex.ic.gov/',
|
||||||
@@ -171,7 +168,6 @@ const germanyAzureSettings: ProviderSettings = {
|
|||||||
settings: {
|
settings: {
|
||||||
host: 'https://login.microsoftazure.de/',
|
host: 'https://login.microsoftazure.de/',
|
||||||
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
||||||
signInResourceId: 'https://management.core.cloudapi.de/',
|
|
||||||
graphResource: {
|
graphResource: {
|
||||||
id: 'https://graph.cloudapi.de/',
|
id: 'https://graph.cloudapi.de/',
|
||||||
endpoint: 'https://graph.cloudapi.de'
|
endpoint: 'https://graph.cloudapi.de'
|
||||||
@@ -197,7 +193,6 @@ const chinaAzureSettings: ProviderSettings = {
|
|||||||
settings: {
|
settings: {
|
||||||
host: 'https://login.chinacloudapi.cn/',
|
host: 'https://login.chinacloudapi.cn/',
|
||||||
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
||||||
signInResourceId: 'https://management.core.chinacloudapi.cn/',
|
|
||||||
graphResource: {
|
graphResource: {
|
||||||
id: 'https://graph.chinacloudapi.cn/',
|
id: 'https://graph.chinacloudapi.cn/',
|
||||||
endpoint: 'https://graph.chinacloudapi.cn'
|
endpoint: 'https://graph.chinacloudapi.cn'
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export function registerAzureResourceCommands(appContext: AppContext, tree: Azur
|
|||||||
const accountNode = node as AzureResourceAccountTreeNode;
|
const accountNode = node as AzureResourceAccountTreeNode;
|
||||||
const azureAccount = accountNode.account as AzureAccount;
|
const azureAccount = accountNode.account as AzureAccount;
|
||||||
|
|
||||||
const tokens = await azdata.accounts.getSecurityToken(azureAccount, azdata.AzureResource.MicrosoftResourceManagement);
|
|
||||||
|
|
||||||
const terminalService = appContext.getService<IAzureTerminalService>(AzureResourceServiceNames.terminalService);
|
const terminalService = appContext.getService<IAzureTerminalService>(AzureResourceServiceNames.terminalService);
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ export function registerAzureResourceCommands(appContext: AppContext, tree: Azur
|
|||||||
tenant = azureAccount.properties.tenants[listOfTenants.indexOf(pickedTenant)];
|
tenant = azureAccount.properties.tenants[listOfTenants.indexOf(pickedTenant)];
|
||||||
}
|
}
|
||||||
|
|
||||||
await terminalService.getOrCreateCloudConsole(azureAccount, tenant, tokens);
|
await terminalService.getOrCreateCloudConsole(azureAccount, tenant);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
console.error(ex);
|
console.error(ex);
|
||||||
vscode.window.showErrorMessage(ex);
|
vscode.window.showErrorMessage(ex);
|
||||||
@@ -91,13 +90,14 @@ export function registerAzureResourceCommands(appContext: AppContext, tree: Azur
|
|||||||
const subscriptions = (await accountNode.getCachedSubscriptions()) || <azureResource.AzureResourceSubscription[]>[];
|
const subscriptions = (await accountNode.getCachedSubscriptions()) || <azureResource.AzureResourceSubscription[]>[];
|
||||||
if (subscriptions.length === 0) {
|
if (subscriptions.length === 0) {
|
||||||
try {
|
try {
|
||||||
const tokens = await azdata.accounts.getSecurityToken(account, azdata.AzureResource.ResourceManagement);
|
|
||||||
|
|
||||||
for (const tenant of account.properties.tenants) {
|
for (const tenant of account.properties.tenants) {
|
||||||
const token = tokens[tenant.id].token;
|
const response = await azdata.accounts.getAccountSecurityToken(account, tenant.id, azdata.AzureResource.ResourceManagement);
|
||||||
const tokenType = tokens[tenant.id].tokenType;
|
|
||||||
|
|
||||||
subscriptions.push(...await subscriptionService.getSubscriptions(account, new TokenCredentials(token, tokenType)));
|
const token = response.token;
|
||||||
|
const tokenType = response.tokenType;
|
||||||
|
|
||||||
|
subscriptions.push(...await subscriptionService.getSubscriptions(account, new TokenCredentials(token, tokenType), tenant.id));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
account.isStale = true;
|
account.isStale = true;
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import * as msRest from '@azure/ms-rest-js';
|
|||||||
import { Account } from 'azdata';
|
import { Account } from 'azdata';
|
||||||
|
|
||||||
import { azureResource } from './azure-resource';
|
import { azureResource } from './azure-resource';
|
||||||
import { AzureAccount, AzureAccountSecurityToken, Tenant } from '../account-provider/interfaces';
|
import { AzureAccount, Tenant } from '../account-provider/interfaces';
|
||||||
|
|
||||||
export interface IAzureResourceSubscriptionService {
|
export interface IAzureResourceSubscriptionService {
|
||||||
getSubscriptions(account: Account, credential: msRest.ServiceClientCredentials): Promise<azureResource.AzureResourceSubscription[]>;
|
getSubscriptions(account: Account, credential: msRest.ServiceClientCredentials, tenantId: string): Promise<azureResource.AzureResourceSubscription[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAzureResourceSubscriptionFilterService {
|
export interface IAzureResourceSubscriptionFilterService {
|
||||||
@@ -20,7 +20,7 @@ export interface IAzureResourceSubscriptionFilterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IAzureTerminalService {
|
export interface IAzureTerminalService {
|
||||||
getOrCreateCloudConsole(account: AzureAccount, tenant: Tenant, tokens: { [key: string]: AzureAccountSecurityToken }): Promise<void>;
|
getOrCreateCloudConsole(account: AzureAccount, tenant: Tenant): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAzureResourceCacheService {
|
export interface IAzureResourceCacheService {
|
||||||
@@ -31,9 +31,6 @@ export interface IAzureResourceCacheService {
|
|||||||
update<T>(key: string, value: T): void;
|
update<T>(key: string, value: T): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAzureResourceTenantService {
|
|
||||||
getTenantId(subscription: azureResource.AzureResourceSubscription, account: Account, credential: msRest.ServiceClientCredentials): Promise<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAzureResourceNodeWithProviderId {
|
export interface IAzureResourceNodeWithProviderId {
|
||||||
resourceProviderId: string;
|
resourceProviderId: string;
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ export abstract class ResourceTreeDataProviderBase<T extends azureResource.Azure
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getResources(element: azureResource.IAzureResourceNode): Promise<T[]> {
|
private async getResources(element: azureResource.IAzureResourceNode): Promise<T[]> {
|
||||||
const tokens = await azdata.accounts.getSecurityToken(element.account, azdata.AzureResource.ResourceManagement);
|
const response = await azdata.accounts.getAccountSecurityToken(element.account, element.tenantId, azdata.AzureResource.ResourceManagement);
|
||||||
const credential = new msRest.TokenCredentials(tokens[element.tenantId].token, tokens[element.tenantId].tokenType);
|
const credential = new msRest.TokenCredentials(response.token, response.tokenType);
|
||||||
|
|
||||||
const resources: T[] = await this._resourceService.getResources(element.subscription, credential, element.account) || <T[]>[];
|
const resources: T[] = await this._resourceService.getResources(element.subscription, credential, element.account) || <T[]>[];
|
||||||
return resources;
|
return resources;
|
||||||
|
|||||||
@@ -10,14 +10,15 @@ import { azureResource } from '../azure-resource';
|
|||||||
import { IAzureResourceSubscriptionService } from '../interfaces';
|
import { IAzureResourceSubscriptionService } from '../interfaces';
|
||||||
|
|
||||||
export class AzureResourceSubscriptionService implements IAzureResourceSubscriptionService {
|
export class AzureResourceSubscriptionService implements IAzureResourceSubscriptionService {
|
||||||
public async getSubscriptions(account: Account, credential: any): Promise<azureResource.AzureResourceSubscription[]> {
|
public async getSubscriptions(account: Account, credential: any, tenantId: string): Promise<azureResource.AzureResourceSubscription[]> {
|
||||||
const subscriptions: azureResource.AzureResourceSubscription[] = [];
|
const subscriptions: azureResource.AzureResourceSubscription[] = [];
|
||||||
|
|
||||||
const subClient = new SubscriptionClient(credential, { baseUri: account.properties.providerSettings.settings.armResource.endpoint });
|
const subClient = new SubscriptionClient(credential, { baseUri: account.properties.providerSettings.settings.armResource.endpoint });
|
||||||
const subs = await subClient.subscriptions.list();
|
const subs = await subClient.subscriptions.list();
|
||||||
subs.forEach((sub) => subscriptions.push({
|
subs.forEach((sub) => subscriptions.push({
|
||||||
id: sub.subscriptionId,
|
id: sub.subscriptionId,
|
||||||
name: sub.displayName
|
name: sub.displayName,
|
||||||
|
tenant: tenantId
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return subscriptions;
|
return subscriptions;
|
||||||
|
|||||||
@@ -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 { SubscriptionClient } from '@azure/arm-subscriptions';
|
|
||||||
|
|
||||||
import { azureResource } from '../azure-resource';
|
|
||||||
import { IAzureResourceTenantService } from '../interfaces';
|
|
||||||
import { Account } from 'azdata';
|
|
||||||
|
|
||||||
export class AzureResourceTenantService implements IAzureResourceTenantService {
|
|
||||||
public async getTenantId(subscription: azureResource.AzureResourceSubscription, account: Account, credentials: any): Promise<string> {
|
|
||||||
const subClient = new SubscriptionClient(credentials, { baseUri: account.properties.providerSettings.settings.armResource.endpoint });
|
|
||||||
|
|
||||||
const result = await subClient.subscriptions.get(subscription.id);
|
|
||||||
return result.subscriptionId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,14 @@
|
|||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
import * as azdata from 'azdata';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as nls from 'vscode-nls';
|
import * as nls from 'vscode-nls';
|
||||||
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
import * as WS from 'ws';
|
import * as WS from 'ws';
|
||||||
|
|
||||||
import { IAzureTerminalService } from '../interfaces';
|
import { IAzureTerminalService } from '../interfaces';
|
||||||
import { AzureAccount, AzureAccountSecurityToken, Tenant } from '../../account-provider/interfaces';
|
import { AzureAccount, Tenant } from '../../account-provider/interfaces';
|
||||||
|
|
||||||
const localize = nls.loadMessageBundle();
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
@@ -48,13 +48,13 @@ export class AzureTerminalService implements IAzureTerminalService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getOrCreateCloudConsole(account: AzureAccount, tenant: Tenant, tokens: { [key: string]: AzureAccountSecurityToken }): Promise<void> {
|
public async getOrCreateCloudConsole(account: AzureAccount, tenant: Tenant): Promise<void> {
|
||||||
const token = tokens[tenant.id].token;
|
const token = await azdata.accounts.getAccountSecurityToken(account, tenant.id, azdata.AzureResource.MicrosoftResourceManagement);
|
||||||
const settings: AxiosRequestConfig = {
|
const settings: AxiosRequestConfig = {
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token.token}`
|
||||||
},
|
},
|
||||||
validateStatus: () => true
|
validateStatus: () => true
|
||||||
};
|
};
|
||||||
@@ -93,7 +93,7 @@ export class AzureTerminalService implements IAzureTerminalService {
|
|||||||
}
|
}
|
||||||
const consoleUri = provisionResult.data.properties.uri;
|
const consoleUri = provisionResult.data.properties.uri;
|
||||||
|
|
||||||
return this.createTerminal(consoleUri, token, account.displayInfo.displayName, preferredShell);
|
return this.createTerminal(consoleUri, token.token, account.displayInfo.displayName, preferredShell);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ import { AzureResourceSubscriptionTreeNode } from './subscriptionTreeNode';
|
|||||||
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
|
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
|
||||||
import { AzureResourceErrorMessageUtil } from '../utils';
|
import { AzureResourceErrorMessageUtil } from '../utils';
|
||||||
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
|
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
|
||||||
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureResourceTenantService } from '../../azureResource/interfaces';
|
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../../azureResource/interfaces';
|
||||||
|
import { AzureAccount } from '../../account-provider/interfaces';
|
||||||
|
|
||||||
export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNodeBase {
|
export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNodeBase {
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly account: azdata.Account,
|
public readonly account: AzureAccount,
|
||||||
appContext: AppContext,
|
appContext: AppContext,
|
||||||
treeChangeHandler: IAzureResourceTreeChangeHandler
|
treeChangeHandler: IAzureResourceTreeChangeHandler
|
||||||
) {
|
) {
|
||||||
@@ -32,7 +33,6 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
|
|||||||
|
|
||||||
this._subscriptionService = this.appContext.getService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService);
|
this._subscriptionService = this.appContext.getService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService);
|
||||||
this._subscriptionFilterService = this.appContext.getService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService);
|
this._subscriptionFilterService = this.appContext.getService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService);
|
||||||
this._tenantService = this.appContext.getService<IAzureResourceTenantService>(AzureResourceServiceNames.tenantService);
|
|
||||||
|
|
||||||
this._id = `account_${this.account.key.accountId}`;
|
this._id = `account_${this.account.key.accountId}`;
|
||||||
this.setCacheKey(`${this._id}.subscriptions`);
|
this.setCacheKey(`${this._id}.subscriptions`);
|
||||||
@@ -42,15 +42,13 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
|
|||||||
public async getChildren(): Promise<TreeNode[]> {
|
public async getChildren(): Promise<TreeNode[]> {
|
||||||
try {
|
try {
|
||||||
let subscriptions: azureResource.AzureResourceSubscription[] = [];
|
let subscriptions: azureResource.AzureResourceSubscription[] = [];
|
||||||
const tokens = await azdata.accounts.getSecurityToken(this.account, azdata.AzureResource.ResourceManagement);
|
|
||||||
|
|
||||||
if (this._isClearingCache) {
|
if (this._isClearingCache) {
|
||||||
try {
|
try {
|
||||||
for (const tenant of this.account.properties.tenants) {
|
for (const tenant of this.account.properties.tenants) {
|
||||||
const token = tokens[tenant.id].token;
|
const token = await azdata.accounts.getAccountSecurityToken(this.account, tenant.id, azdata.AzureResource.ResourceManagement);
|
||||||
const tokenType = tokens[tenant.id].tokenType;
|
|
||||||
|
|
||||||
subscriptions.push(...(await this._subscriptionService.getSubscriptions(this.account, new TokenCredentials(token, tokenType)) || <azureResource.AzureResourceSubscription[]>[]));
|
subscriptions.push(...(await this._subscriptionService.getSubscriptions(this.account, new TokenCredentials(token.token, token.tokenType), tenant.id) || <azureResource.AzureResourceSubscription[]>[]));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new AzureResourceCredentialError(localize('azure.resource.tree.accountTreeNode.credentialError', "Failed to get credential for account {0}. Please refresh the account.", this.account.key.accountId), error);
|
throw new AzureResourceCredentialError(localize('azure.resource.tree.accountTreeNode.credentialError', "Failed to get credential for account {0}. Please refresh the account.", this.account.key.accountId), error);
|
||||||
@@ -80,8 +78,8 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
|
|||||||
return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.noSubscriptionsLabel, this)];
|
return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.noSubscriptionsLabel, this)];
|
||||||
} else {
|
} else {
|
||||||
// Filter out everything that we can't authenticate to.
|
// Filter out everything that we can't authenticate to.
|
||||||
subscriptions = subscriptions.filter(s => {
|
subscriptions = subscriptions.filter(async s => {
|
||||||
const token = tokens[s.id];
|
const token = await azdata.accounts.getAccountSecurityToken(this.account, s.tenant, azdata.AzureResource.ResourceManagement);
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.info(`Account does not have permissions to view subscription ${JSON.stringify(s)}.`);
|
console.info(`Account does not have permissions to view subscription ${JSON.stringify(s)}.`);
|
||||||
return false;
|
return false;
|
||||||
@@ -90,10 +88,7 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
|
|||||||
});
|
});
|
||||||
|
|
||||||
let subTreeNodes = await Promise.all(subscriptions.map(async (subscription) => {
|
let subTreeNodes = await Promise.all(subscriptions.map(async (subscription) => {
|
||||||
const token = tokens[subscription.id];
|
return new AzureResourceSubscriptionTreeNode(this.account, subscription, subscription.tenant, this.appContext, this.treeChangeHandler, this);
|
||||||
const tenantId = await this._tenantService.getTenantId(subscription, this.account, new TokenCredentials(token.token, token.tokenType));
|
|
||||||
|
|
||||||
return new AzureResourceSubscriptionTreeNode(this.account, subscription, tenantId, this.appContext, this.treeChangeHandler, this);
|
|
||||||
}));
|
}));
|
||||||
return subTreeNodes.sort((a, b) => a.subscription.name.localeCompare(b.subscription.name));
|
return subTreeNodes.sort((a, b) => a.subscription.name.localeCompare(b.subscription.name));
|
||||||
}
|
}
|
||||||
@@ -166,7 +161,6 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
|
|||||||
|
|
||||||
private _subscriptionService: IAzureResourceSubscriptionService = undefined;
|
private _subscriptionService: IAzureResourceSubscriptionService = undefined;
|
||||||
private _subscriptionFilterService: IAzureResourceSubscriptionFilterService = undefined;
|
private _subscriptionFilterService: IAzureResourceSubscriptionFilterService = undefined;
|
||||||
private _tenantService: IAzureResourceTenantService = undefined;
|
|
||||||
|
|
||||||
private _id: string = undefined;
|
private _id: string = undefined;
|
||||||
private _label: string = undefined;
|
private _label: string = undefined;
|
||||||
|
|||||||
@@ -151,13 +151,13 @@ export async function getSubscriptions(appContext: AppContext, account?: azdata.
|
|||||||
}
|
}
|
||||||
|
|
||||||
const subscriptionService = appContext.getService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService);
|
const subscriptionService = appContext.getService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService);
|
||||||
const tokens = await azdata.accounts.getSecurityToken(account, azdata.AzureResource.ResourceManagement);
|
await Promise.all(account.properties.tenants.map(async (tenant: { id: string; }) => {
|
||||||
await Promise.all(account.properties.tenants.map(async (tenant: { id: string | number; }) => {
|
|
||||||
try {
|
try {
|
||||||
const token = tokens[tenant.id].token;
|
const response = await azdata.accounts.getAccountSecurityToken(account, tenant.id, azdata.AzureResource.ResourceManagement);
|
||||||
const tokenType = tokens[tenant.id].tokenType;
|
const token = response.token;
|
||||||
|
const tokenType = response.tokenType;
|
||||||
|
|
||||||
result.subscriptions.push(...await subscriptionService.getSubscriptions(account, new TokenCredentials(token, tokenType)));
|
result.subscriptions.push(...await subscriptionService.getSubscriptions(account, new TokenCredentials(token, tokenType), tenant.id));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = new Error(localize('azure.accounts.getSubscriptions.queryError', "Error fetching subscriptions for account {0} tenant {1} : {2}",
|
const error = new Error(localize('azure.accounts.getSubscriptions.queryError', "Error fetching subscriptions for account {0} tenant {1} : {2}",
|
||||||
account.displayInfo.displayName,
|
account.displayInfo.displayName,
|
||||||
|
|||||||
@@ -17,12 +17,11 @@ import { AzureResourceDatabaseServerService } from './azureResource/providers/da
|
|||||||
import { AzureResourceDatabaseProvider } from './azureResource/providers/database/databaseProvider';
|
import { AzureResourceDatabaseProvider } from './azureResource/providers/database/databaseProvider';
|
||||||
import { AzureResourceDatabaseService } from './azureResource/providers/database/databaseService';
|
import { AzureResourceDatabaseService } from './azureResource/providers/database/databaseService';
|
||||||
import { AzureResourceService } from './azureResource/resourceService';
|
import { AzureResourceService } from './azureResource/resourceService';
|
||||||
import { IAzureResourceCacheService, IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureResourceTenantService, IAzureTerminalService } from './azureResource/interfaces';
|
import { IAzureResourceCacheService, IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureTerminalService } from './azureResource/interfaces';
|
||||||
import { AzureResourceServiceNames } from './azureResource/constants';
|
import { AzureResourceServiceNames } from './azureResource/constants';
|
||||||
import { AzureResourceSubscriptionService } from './azureResource/services/subscriptionService';
|
import { AzureResourceSubscriptionService } from './azureResource/services/subscriptionService';
|
||||||
import { AzureResourceSubscriptionFilterService } from './azureResource/services/subscriptionFilterService';
|
import { AzureResourceSubscriptionFilterService } from './azureResource/services/subscriptionFilterService';
|
||||||
import { AzureResourceCacheService } from './azureResource/services/cacheService';
|
import { AzureResourceCacheService } from './azureResource/services/cacheService';
|
||||||
import { AzureResourceTenantService } from './azureResource/services/tenantService';
|
|
||||||
import { registerAzureResourceCommands } from './azureResource/commands';
|
import { registerAzureResourceCommands } from './azureResource/commands';
|
||||||
import { AzureResourceTreeProvider } from './azureResource/tree/treeProvider';
|
import { AzureResourceTreeProvider } from './azureResource/tree/treeProvider';
|
||||||
import { SqlInstanceResourceService } from './azureResource/providers/sqlinstance/sqlInstanceService';
|
import { SqlInstanceResourceService } from './azureResource/providers/sqlinstance/sqlInstanceService';
|
||||||
@@ -153,7 +152,6 @@ function registerAzureServices(appContext: AppContext): void {
|
|||||||
appContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, new AzureResourceCacheService(extensionContext));
|
appContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, new AzureResourceCacheService(extensionContext));
|
||||||
appContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, new AzureResourceSubscriptionService());
|
appContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, new AzureResourceSubscriptionService());
|
||||||
appContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, new AzureResourceSubscriptionFilterService(new AzureResourceCacheService(extensionContext)));
|
appContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, new AzureResourceSubscriptionFilterService(new AzureResourceCacheService(extensionContext)));
|
||||||
appContext.registerService<IAzureResourceTenantService>(AzureResourceServiceNames.tenantService, new AzureResourceTenantService());
|
|
||||||
appContext.registerService<IAzureTerminalService>(AzureResourceServiceNames.terminalService, new AzureTerminalService(extensionContext));
|
appContext.registerService<IAzureTerminalService>(AzureResourceServiceNames.terminalService, new AzureTerminalService(extensionContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,114 +4,240 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as should from 'should';
|
import * as should from 'should';
|
||||||
import * as os from 'os';
|
import * as TypeMoq from 'typemoq';
|
||||||
|
// import * as azdata from 'azdata';
|
||||||
|
// import * as vscode from 'vscode';
|
||||||
|
// import * as sinon from 'sinon';
|
||||||
import 'mocha';
|
import 'mocha';
|
||||||
import * as vscode from 'vscode';
|
import { AzureAuthCodeGrant } from '../../../account-provider/auths/azureAuthCodeGrant';
|
||||||
|
// import { AzureDeviceCode } from '../../../account-provider/auths/azureDeviceCode';
|
||||||
import { PromptFailedResult, AccountKey } from 'azdata';
|
import { Token, TokenClaims, AccessToken, RefreshToken, OAuthTokenResponse, TokenPostData } from '../../../account-provider/auths/azureAuth';
|
||||||
import { AzureAuth, AccessToken, RefreshToken, TokenClaims, TokenRefreshResponse } from '../../../account-provider/auths/azureAuth';
|
import { Tenant, AzureAccount } from '../../../account-provider/interfaces';
|
||||||
import { AzureAccount, AzureAuthType, Deferred, Tenant } from '../../../account-provider/interfaces';
|
|
||||||
import providerSettings from '../../../account-provider/providerSettings';
|
import providerSettings from '../../../account-provider/providerSettings';
|
||||||
import { SimpleTokenCache } from '../../../account-provider/simpleTokenCache';
|
import { AzureResource } from 'azdata';
|
||||||
import { CredentialsTestProvider } from '../../stubs/credentialsTestProvider';
|
import { AxiosResponse } from 'axios';
|
||||||
|
|
||||||
class BasicAzureAuth extends AzureAuth {
|
|
||||||
public async login(): Promise<AzureAccount | PromptFailedResult> {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public async autoOAuthCancelled(): Promise<void> {
|
let azureAuthCodeGrant: TypeMoq.IMock<AzureAuthCodeGrant>;
|
||||||
throw new Error('Method not implemented.');
|
// let azureDeviceCode: TypeMoq.IMock<AzureDeviceCode>;
|
||||||
}
|
|
||||||
|
|
||||||
public async promptForConsent(): Promise<{ tokenRefreshResponse: TokenRefreshResponse, authCompleteDeferred: Deferred<void> } | undefined> {
|
const mockToken: Token = {
|
||||||
throw new Error('Method not implemented.');
|
key: 'someUniqueId',
|
||||||
}
|
token: 'test_token',
|
||||||
}
|
tokenType: 'Bearer'
|
||||||
|
|
||||||
let baseAuth: AzureAuth;
|
|
||||||
|
|
||||||
const accountKey: AccountKey = {
|
|
||||||
accountId: 'SomeAccountKey',
|
|
||||||
providerId: 'providerId',
|
|
||||||
};
|
};
|
||||||
|
let mockAccessToken: AccessToken;
|
||||||
|
let mockRefreshToken: RefreshToken;
|
||||||
|
|
||||||
const accessToken: AccessToken = {
|
const mockClaims = {
|
||||||
key: accountKey.accountId,
|
name: 'Name',
|
||||||
token: '123'
|
email: 'example@example.com',
|
||||||
};
|
sub: 'someUniqueId'
|
||||||
|
|
||||||
const refreshToken: RefreshToken = {
|
|
||||||
key: accountKey.accountId,
|
|
||||||
token: '321'
|
|
||||||
};
|
|
||||||
|
|
||||||
const resourceId = 'resource';
|
|
||||||
const tenantId = 'tenant';
|
|
||||||
|
|
||||||
const tenant: Tenant = {
|
|
||||||
id: tenantId,
|
|
||||||
displayName: 'common'
|
|
||||||
};
|
|
||||||
|
|
||||||
// These tests don't work on Linux systems because gnome-keyring doesn't like running on headless machines.
|
|
||||||
describe('AccountProvider.AzureAuth', function (): void {
|
|
||||||
beforeEach(async function (): Promise<void> {
|
|
||||||
const tokenCache = new SimpleTokenCache('testTokenService', os.tmpdir(), true, new CredentialsTestProvider());
|
|
||||||
await tokenCache.init();
|
|
||||||
baseAuth = new BasicAzureAuth(providerSettings[0].metadata, tokenCache, undefined, undefined, AzureAuthType.AuthCodeGrant, 'Auth Code Grant');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Basic token set and get', async function (): Promise<void> {
|
|
||||||
await baseAuth.setCachedToken(accountKey, accessToken, refreshToken);
|
|
||||||
const result = await baseAuth.getCachedToken(accountKey);
|
|
||||||
|
|
||||||
should(JSON.stringify(result.accessToken)).be.equal(JSON.stringify(accessToken));
|
|
||||||
should(JSON.stringify(result.refreshToken)).be.equal(JSON.stringify(refreshToken));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Token set and get with tenant and resource id', async function (): Promise<void> {
|
|
||||||
await baseAuth.setCachedToken(accountKey, accessToken, refreshToken, resourceId, tenantId);
|
|
||||||
let result = await baseAuth.getCachedToken(accountKey, resourceId, tenantId);
|
|
||||||
|
|
||||||
should(JSON.stringify(result.accessToken)).be.equal(JSON.stringify(accessToken));
|
|
||||||
should(JSON.stringify(result.refreshToken)).be.equal(JSON.stringify(refreshToken));
|
|
||||||
|
|
||||||
await baseAuth.clearCredentials(accountKey);
|
|
||||||
result = await baseAuth.getCachedToken(accountKey, resourceId, tenantId);
|
|
||||||
should(result).be.undefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Token set with resource ID and get without tenant and resource id', async function (): Promise<void> {
|
|
||||||
await baseAuth.setCachedToken(accountKey, accessToken, refreshToken, resourceId, tenantId);
|
|
||||||
const result = await baseAuth.getCachedToken(accountKey);
|
|
||||||
|
|
||||||
should(JSON.stringify(result)).be.undefined();
|
|
||||||
should(JSON.stringify(result)).be.undefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Create an account object', async function (): Promise<void> {
|
|
||||||
const tokenClaims = {
|
|
||||||
idp: 'live.com',
|
|
||||||
name: 'TestAccount',
|
|
||||||
} as TokenClaims;
|
} as TokenClaims;
|
||||||
|
|
||||||
const account = baseAuth.createAccount(tokenClaims, 'someKey', undefined);
|
const mockTenant: Tenant = {
|
||||||
|
displayName: 'Tenant Name',
|
||||||
|
id: 'tenantID',
|
||||||
|
tenantCategory: 'Home',
|
||||||
|
userId: 'test_user'
|
||||||
|
};
|
||||||
|
|
||||||
should(account.properties.azureAuthType).be.equal(AzureAuthType.AuthCodeGrant);
|
let mockAccount: AzureAccount;
|
||||||
should(account.key.accountId).be.equal('someKey');
|
|
||||||
should(account.properties.isMsAccount).be.equal(true);
|
const provider = providerSettings[0].metadata;
|
||||||
|
|
||||||
|
describe('Azure Authentication', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
azureAuthCodeGrant = TypeMoq.Mock.ofType<AzureAuthCodeGrant>(AzureAuthCodeGrant, TypeMoq.MockBehavior.Loose, true, provider);
|
||||||
|
// azureDeviceCode = TypeMoq.Mock.ofType<AzureDeviceCode>();
|
||||||
|
|
||||||
|
azureAuthCodeGrant.callBase = true;
|
||||||
|
// authDeviceCode.callBase = true;
|
||||||
|
|
||||||
|
mockAccount = {
|
||||||
|
isStale: false,
|
||||||
|
properties: {
|
||||||
|
tenants: [mockTenant]
|
||||||
|
}
|
||||||
|
} as AzureAccount;
|
||||||
|
|
||||||
|
mockAccessToken = {
|
||||||
|
...mockToken
|
||||||
|
};
|
||||||
|
mockRefreshToken = {
|
||||||
|
...mockToken
|
||||||
|
};
|
||||||
});
|
});
|
||||||
it('Should handle ignored tenants', async function (): Promise<void> {
|
|
||||||
// Don't sit on the await openConsentDialog if test is failing
|
|
||||||
this.timeout(3000);
|
|
||||||
|
|
||||||
const configuration = vscode.workspace.getConfiguration('azure.tenant.config');
|
it('accountHydration should yield a valid account', async function () {
|
||||||
const values = [tenantId];
|
|
||||||
|
|
||||||
await configuration.update('filter', values, vscode.ConfigurationTarget.Global);
|
azureAuthCodeGrant.setup(x => x.getTenants(mockToken)).returns((): Promise<Tenant[]> => {
|
||||||
const x = await baseAuth.openConsentDialog(tenant, resourceId);
|
return Promise.resolve([
|
||||||
|
mockTenant
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
should(x).be.false();
|
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.getAccountSecurityToken(mockAccount, TypeMoq.It.isAny(), TypeMoq.It.isAny());
|
||||||
|
should(securityToken).be.undefined();
|
||||||
|
});
|
||||||
|
it('dont find correct resources', async function () {
|
||||||
|
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, TypeMoq.It.isAny(), -1);
|
||||||
|
should(securityToken).be.undefined();
|
||||||
|
});
|
||||||
|
it('incorrect tenant', async function () {
|
||||||
|
await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, 'invalid_tenant', AzureResource.MicrosoftResourceManagement).should.be.rejected();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saved token exists and can be reused', async function () {
|
||||||
|
delete (mockAccessToken as any).tokenType;
|
||||||
|
azureAuthCodeGrant.setup(x => x.getSavedToken(mockTenant, provider.settings.microsoftResource, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||||
|
return Promise.resolve({
|
||||||
|
accessToken: mockAccessToken,
|
||||||
|
refreshToken: mockRefreshToken,
|
||||||
|
expiresOn: `${(new Date().getTime() / 1000) + (10 * 60)}`
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
|
||||||
|
|
||||||
|
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.getSavedToken(mockTenant, provider.settings.microsoftResource, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||||
|
return Promise.resolve({
|
||||||
|
accessToken: mockAccessToken,
|
||||||
|
refreshToken: mockRefreshToken,
|
||||||
|
expiresOn: undefined
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
azureAuthCodeGrant.setup(x => x.refreshToken(mockTenant, provider.settings.microsoftResource, mockRefreshToken)).returns((): Promise<OAuthTokenResponse> => {
|
||||||
|
const mockToken: AccessToken = JSON.parse(JSON.stringify(mockAccessToken));
|
||||||
|
delete (mockToken as any).invalidData;
|
||||||
|
return Promise.resolve({
|
||||||
|
accessToken: mockToken
|
||||||
|
} as OAuthTokenResponse);
|
||||||
|
});
|
||||||
|
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
|
||||||
|
|
||||||
|
should((securityToken as any).invalidData).be.undefined(); // Ensure its a new one
|
||||||
|
should(securityToken.tokenType).be.equal('Bearer', 'tokenType should be bearer on a successful getSecurityToken from cache')
|
||||||
|
|
||||||
|
azureAuthCodeGrant.verify(x => x.refreshToken(mockTenant, provider.settings.microsoftResource, mockRefreshToken), TypeMoq.Times.once());
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('no saved token', function () {
|
||||||
|
it('no base token', async function () {
|
||||||
|
azureAuthCodeGrant.setup(x => x.getSavedToken(mockTenant, provider.settings.microsoftResource, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
azureAuthCodeGrant.setup(x => x.getSavedToken(azureAuthCodeGrant.object.commonTenant, provider.settings.microsoftResource, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement).should.be.rejected();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('base token exists', async function () {
|
||||||
|
azureAuthCodeGrant.setup(x => x.getSavedToken(mockTenant, provider.settings.microsoftResource, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
azureAuthCodeGrant.setup(x => x.getSavedToken(azureAuthCodeGrant.object.commonTenant, provider.settings.microsoftResource, mockAccount.key)).returns((): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken, expiresOn: string }> => {
|
||||||
|
return Promise.resolve({
|
||||||
|
accessToken: mockAccessToken,
|
||||||
|
refreshToken: mockRefreshToken,
|
||||||
|
expiresOn: ''
|
||||||
|
});
|
||||||
|
});
|
||||||
|
delete (mockAccessToken as any).tokenType;
|
||||||
|
|
||||||
|
azureAuthCodeGrant.setup(x => x.refreshToken(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
accessToken: mockAccessToken
|
||||||
|
} as OAuthTokenResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
const securityToken: Token = await azureAuthCodeGrant.object.getAccountSecurityToken(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.handleInteractionRequired(mockTenant, provider.settings.microsoftResource)).returns(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
accessToken: mockAccessToken
|
||||||
|
} as OAuthTokenResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const result = await azureAuthCodeGrant.object.getToken(mockTenant, provider.settings.microsoftResource, {} as TokenPostData);
|
||||||
|
|
||||||
|
azureAuthCodeGrant.verify(x => x.handleInteractionRequired(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.getToken(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.getTokenHelper(mockTenant, provider.settings.microsoftResource, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
accessToken: mockAccessToken
|
||||||
|
} as OAuthTokenResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const result = await azureAuthCodeGrant.object.getToken(mockTenant, provider.settings.microsoftResource, {} as TokenPostData);
|
||||||
|
|
||||||
|
azureAuthCodeGrant.verify(x => x.getTokenHelper(mockTenant, provider.settings.microsoftResource, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
|
|
||||||
|
should(result.accessToken).be.deepEqual(mockAccessToken);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|||||||
@@ -41,12 +41,15 @@ const mockAccount: AzureAccount = {
|
|||||||
isStale: false
|
isStale: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockTenantId: string = 'mock_tenant';
|
||||||
|
|
||||||
|
|
||||||
const mockSubscription: azureResource.AzureResourceSubscription = {
|
const mockSubscription: azureResource.AzureResourceSubscription = {
|
||||||
id: 'mock_subscription',
|
id: 'mock_subscription',
|
||||||
name: 'mock subscription'
|
name: 'mock subscription',
|
||||||
|
tenant: mockTenantId
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockTenantId: string = 'mock_tenant';
|
|
||||||
|
|
||||||
const mockResourceRootNode: azureResource.IAzureResourceNode = {
|
const mockResourceRootNode: azureResource.IAzureResourceNode = {
|
||||||
account: mockAccount,
|
account: mockAccount,
|
||||||
@@ -61,8 +64,7 @@ const mockResourceRootNode: azureResource.IAzureResourceNode = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockTokens: { [key: string]: any } = {};
|
const mockToken = {
|
||||||
mockTokens[mockTenantId] = {
|
|
||||||
token: 'mock_token',
|
token: 'mock_token',
|
||||||
tokenType: 'Bearer'
|
tokenType: 'Bearer'
|
||||||
};
|
};
|
||||||
@@ -106,7 +108,7 @@ describe('AzureResourceDatabaseTreeDataProvider.getChildren', function (): void
|
|||||||
mockDatabaseService = TypeMoq.Mock.ofType<IAzureResourceService<azureResource.AzureResourceDatabase>>();
|
mockDatabaseService = TypeMoq.Mock.ofType<IAzureResourceService<azureResource.AzureResourceDatabase>>();
|
||||||
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||||
|
|
||||||
sinon.stub(azdata.accounts, 'getSecurityToken').returns(Promise.resolve(mockTokens));
|
sinon.stub(azdata.accounts, 'getAccountSecurityToken').returns(Promise.resolve(mockToken));
|
||||||
mockDatabaseService.setup((o) => o.getResources(mockSubscription, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(mockDatabases));
|
mockDatabaseService.setup((o) => o.getResources(mockSubscription, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(mockDatabases));
|
||||||
mockExtensionContext.setup((o) => o.asAbsolutePath(TypeMoq.It.isAnyString())).returns(() => TypeMoq.It.isAnyString());
|
mockExtensionContext.setup((o) => o.asAbsolutePath(TypeMoq.It.isAnyString())).returns(() => TypeMoq.It.isAnyString());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,12 +41,14 @@ const mockAccount: AzureAccount = {
|
|||||||
isStale: false
|
isStale: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockTenantId: string = 'mock_tenant';
|
||||||
|
|
||||||
const mockSubscription: azureResource.AzureResourceSubscription = {
|
const mockSubscription: azureResource.AzureResourceSubscription = {
|
||||||
id: 'mock_subscription',
|
id: 'mock_subscription',
|
||||||
name: 'mock subscription'
|
name: 'mock subscription',
|
||||||
|
tenant: mockTenantId
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockTenantId: string = 'mock_tenant';
|
|
||||||
|
|
||||||
const mockResourceRootNode: azureResource.IAzureResourceNode = {
|
const mockResourceRootNode: azureResource.IAzureResourceNode = {
|
||||||
account: mockAccount,
|
account: mockAccount,
|
||||||
@@ -61,8 +63,7 @@ const mockResourceRootNode: azureResource.IAzureResourceNode = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockTokens: { [key: string]: any } = {};
|
const mockToken = {
|
||||||
mockTokens[mockTenantId] = {
|
|
||||||
token: 'mock_token',
|
token: 'mock_token',
|
||||||
tokenType: 'Bearer'
|
tokenType: 'Bearer'
|
||||||
};
|
};
|
||||||
@@ -106,7 +107,7 @@ describe('AzureResourceDatabaseServerTreeDataProvider.getChildren', function ():
|
|||||||
mockDatabaseServerService = TypeMoq.Mock.ofType<IAzureResourceService<azureResource.AzureResourceDatabaseServer>>();
|
mockDatabaseServerService = TypeMoq.Mock.ofType<IAzureResourceService<azureResource.AzureResourceDatabaseServer>>();
|
||||||
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||||
|
|
||||||
sinon.stub(azdata.accounts, 'getSecurityToken').returns(Promise.resolve(mockTokens));
|
sinon.stub(azdata.accounts, 'getAccountSecurityToken').returns(Promise.resolve(mockToken));
|
||||||
mockDatabaseServerService.setup((o) => o.getResources(mockSubscription, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(mockDatabaseServers));
|
mockDatabaseServerService.setup((o) => o.getResources(mockSubscription, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(mockDatabaseServers));
|
||||||
mockExtensionContext.setup((o) => o.asAbsolutePath(TypeMoq.It.isAnyString())).returns(() => TypeMoq.It.isAnyString());
|
mockExtensionContext.setup((o) => o.asAbsolutePath(TypeMoq.It.isAnyString())).returns(() => TypeMoq.It.isAnyString());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,13 +33,14 @@ const mockAccount: AzureAccount = {
|
|||||||
isStale: false
|
isStale: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockTenantId: string = 'mock_tenant';
|
||||||
|
|
||||||
const mockSubscription: azureResource.AzureResourceSubscription = {
|
const mockSubscription: azureResource.AzureResourceSubscription = {
|
||||||
id: 'mock_subscription',
|
id: 'mock_subscription',
|
||||||
name: 'mock subscription'
|
name: 'mock subscription',
|
||||||
|
tenant: mockTenantId
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockTenantId: string = 'mock_tenant';
|
|
||||||
|
|
||||||
let mockResourceTreeDataProvider1: TypeMoq.IMock<azureResource.IAzureResourceTreeDataProvider>;
|
let mockResourceTreeDataProvider1: TypeMoq.IMock<azureResource.IAzureResourceTreeDataProvider>;
|
||||||
let mockResourceProvider1: TypeMoq.IMock<azureResource.IAzureResourceProvider>;
|
let mockResourceProvider1: TypeMoq.IMock<azureResource.IAzureResourceProvider>;
|
||||||
|
|
||||||
|
|||||||
@@ -36,12 +36,14 @@ const mockAccount: AzureAccount = {
|
|||||||
isStale: false
|
isStale: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockTenantId: string = 'mock_tenant';
|
||||||
|
|
||||||
const mockSubscription: azureResource.AzureResourceSubscription = {
|
const mockSubscription: azureResource.AzureResourceSubscription = {
|
||||||
id: 'mock_subscription',
|
id: 'mock_subscription',
|
||||||
name: 'mock subscription'
|
name: 'mock subscription',
|
||||||
|
tenant: mockTenantId
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockTenantId: string = 'mock_tenant';
|
|
||||||
|
|
||||||
const mockResourceProviderId: string = 'mock_resource_provider';
|
const mockResourceProviderId: string = 'mock_resource_provider';
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
IAzureResourceCacheService,
|
IAzureResourceCacheService,
|
||||||
IAzureResourceSubscriptionService,
|
IAzureResourceSubscriptionService,
|
||||||
IAzureResourceSubscriptionFilterService,
|
IAzureResourceSubscriptionFilterService,
|
||||||
IAzureResourceTenantService
|
|
||||||
} from '../../../azureResource/interfaces';
|
} from '../../../azureResource/interfaces';
|
||||||
import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler';
|
import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler';
|
||||||
import { AzureResourceAccountTreeNode } from '../../../azureResource/tree/accountTreeNode';
|
import { AzureResourceAccountTreeNode } from '../../../azureResource/tree/accountTreeNode';
|
||||||
@@ -31,7 +30,6 @@ let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
|
|||||||
let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>;
|
let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>;
|
||||||
let mockSubscriptionService: TypeMoq.IMock<IAzureResourceSubscriptionService>;
|
let mockSubscriptionService: TypeMoq.IMock<IAzureResourceSubscriptionService>;
|
||||||
let mockSubscriptionFilterService: TypeMoq.IMock<IAzureResourceSubscriptionFilterService>;
|
let mockSubscriptionFilterService: TypeMoq.IMock<IAzureResourceSubscriptionFilterService>;
|
||||||
let mockTenantService: TypeMoq.IMock<IAzureResourceTenantService>;
|
|
||||||
let mockAppContext: AppContext;
|
let mockAppContext: AppContext;
|
||||||
let getSecurityTokenStub: sinon.SinonStub;
|
let getSecurityTokenStub: sinon.SinonStub;
|
||||||
let mockTreeChangeHandler: TypeMoq.IMock<IAzureResourceTreeChangeHandler>;
|
let mockTreeChangeHandler: TypeMoq.IMock<IAzureResourceTreeChangeHandler>;
|
||||||
@@ -63,28 +61,27 @@ const mockAccount: azdata.Account = {
|
|||||||
|
|
||||||
const mockSubscription1: azureResource.AzureResourceSubscription = {
|
const mockSubscription1: azureResource.AzureResourceSubscription = {
|
||||||
id: 'mock_subscription_1',
|
id: 'mock_subscription_1',
|
||||||
name: 'mock subscription 1'
|
name: 'mock subscription 1',
|
||||||
|
tenant: mockTenantId
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockSubscription2: azureResource.AzureResourceSubscription = {
|
const mockSubscription2: azureResource.AzureResourceSubscription = {
|
||||||
id: 'mock_subscription_2',
|
id: 'mock_subscription_2',
|
||||||
name: 'mock subscription 2'
|
name: 'mock subscription 2',
|
||||||
|
tenant: mockTenantId
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockSubscriptions = [mockSubscription1, mockSubscription2];
|
const mockSubscriptions = [mockSubscription1, mockSubscription2];
|
||||||
|
|
||||||
const mockFilteredSubscriptions = [mockSubscription1];
|
const mockFilteredSubscriptions = [mockSubscription1];
|
||||||
|
|
||||||
const mockTokens: { [key: string]: any } = {};
|
const mockToken = {
|
||||||
|
|
||||||
[mockSubscription1.id, mockSubscription2.id, mockTenantId].forEach(s => {
|
|
||||||
mockTokens[s] = {
|
|
||||||
token: 'mock_token',
|
token: 'mock_token',
|
||||||
tokenType: 'Bearer'
|
tokenType: 'Bearer'
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
const mockCredential = new TokenCredentials(mockTokens[mockTenantId].token, mockTokens[mockTenantId].tokenType);
|
|
||||||
|
const mockCredential = new TokenCredentials(mockToken.token, mockToken.tokenType);
|
||||||
|
|
||||||
let mockSubscriptionCache: azureResource.AzureResourceSubscription[] = [];
|
let mockSubscriptionCache: azureResource.AzureResourceSubscription[] = [];
|
||||||
|
|
||||||
@@ -94,7 +91,6 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
|
|||||||
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
|
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
|
||||||
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||||
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
|
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
|
||||||
mockTenantService = TypeMoq.Mock.ofType<IAzureResourceTenantService>();
|
|
||||||
|
|
||||||
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
|
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
|
||||||
|
|
||||||
@@ -104,13 +100,11 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
|
|||||||
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||||
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
|
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
|
||||||
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||||
mockAppContext.registerService<IAzureResourceTenantService>(AzureResourceServiceNames.tenantService, mockTenantService.object);
|
|
||||||
|
|
||||||
getSecurityTokenStub = sinon.stub(azdata.accounts, 'getSecurityToken').returns(Promise.resolve(mockTokens));
|
getSecurityTokenStub = sinon.stub(azdata.accounts, 'getAccountSecurityToken').returns(Promise.resolve(mockToken));
|
||||||
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
|
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
|
||||||
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
|
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
|
||||||
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache = mockSubscriptions);
|
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache = mockSubscriptions);
|
||||||
mockTenantService.setup((o) => o.getTenantId(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(mockTenantId));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function (): void {
|
afterEach(function (): void {
|
||||||
@@ -138,7 +132,7 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should be correct when there are subscriptions listed.', async function (): Promise<void> {
|
it('Should be correct when there are subscriptions listed.', async function (): Promise<void> {
|
||||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions));
|
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential, mockTenantId)).returns(() => Promise.resolve(mockSubscriptions));
|
||||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined));
|
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined));
|
||||||
|
|
||||||
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
|
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
|
||||||
@@ -158,7 +152,7 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should be correct when there are subscriptions filtered.', async function (): Promise<void> {
|
it('Should be correct when there are subscriptions filtered.', async function (): Promise<void> {
|
||||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions));
|
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential, mockTenantId)).returns(() => Promise.resolve(mockSubscriptions));
|
||||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
|
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
|
||||||
|
|
||||||
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
|
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
|
||||||
@@ -184,7 +178,6 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
|||||||
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
|
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
|
||||||
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||||
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
|
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
|
||||||
mockTenantService = TypeMoq.Mock.ofType<IAzureResourceTenantService>();
|
|
||||||
|
|
||||||
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
|
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
|
||||||
|
|
||||||
@@ -194,13 +187,11 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
|||||||
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||||
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
|
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
|
||||||
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||||
mockAppContext.registerService<IAzureResourceTenantService>(AzureResourceServiceNames.tenantService, mockTenantService.object);
|
|
||||||
|
|
||||||
sinon.stub(azdata.accounts, 'getSecurityToken').returns(Promise.resolve(mockTokens));
|
sinon.stub(azdata.accounts, 'getAccountSecurityToken').returns(Promise.resolve(mockToken));
|
||||||
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
|
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
|
||||||
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
|
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
|
||||||
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache = mockSubscriptions);
|
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache = mockSubscriptions);
|
||||||
mockTenantService.setup((o) => o.getTenantId(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(mockTenantId));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function (): void {
|
afterEach(function (): void {
|
||||||
@@ -208,14 +199,14 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should load subscriptions from scratch and update cache when it is clearing cache.', async function (): Promise<void> {
|
it('Should load subscriptions from scratch and update cache when it is clearing cache.', async function (): Promise<void> {
|
||||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions));
|
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential, mockTenantId)).returns(() => Promise.resolve(mockSubscriptions));
|
||||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
|
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
|
||||||
|
|
||||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||||
|
|
||||||
const children = await accountTreeNode.getChildren();
|
const children = await accountTreeNode.getChildren();
|
||||||
|
|
||||||
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredential), TypeMoq.Times.once());
|
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredential, mockTenantId), TypeMoq.Times.once());
|
||||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(0));
|
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(0));
|
||||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once());
|
mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once());
|
||||||
@@ -241,7 +232,7 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should load subscriptions from cache when it is not clearing cache.', async function (): Promise<void> {
|
it('Should load subscriptions from cache when it is not clearing cache.', async function (): Promise<void> {
|
||||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions));
|
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential, mockTenantId)).returns(() => Promise.resolve(mockSubscriptions));
|
||||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined));
|
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined));
|
||||||
|
|
||||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||||
@@ -250,7 +241,7 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
|||||||
const children = await accountTreeNode.getChildren();
|
const children = await accountTreeNode.getChildren();
|
||||||
|
|
||||||
|
|
||||||
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredential), TypeMoq.Times.once());
|
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredential, mockTenantId), TypeMoq.Times.once());
|
||||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
|
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
|
||||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
|
|
||||||
@@ -262,7 +253,7 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should handle when there is no subscriptions.', async function (): Promise<void> {
|
it('Should handle when there is no subscriptions.', async function (): Promise<void> {
|
||||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(undefined));
|
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential, mockTenantId)).returns(() => Promise.resolve(undefined));
|
||||||
|
|
||||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||||
|
|
||||||
@@ -278,7 +269,7 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should honor subscription filtering.', async function (): Promise<void> {
|
it('Should honor subscription filtering.', async function (): Promise<void> {
|
||||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions));
|
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential, mockTenantId)).returns(() => Promise.resolve(mockSubscriptions));
|
||||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
|
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
|
||||||
|
|
||||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||||
@@ -296,7 +287,7 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should handle errors.', async function (): Promise<void> {
|
it('Should handle errors.', async function (): Promise<void> {
|
||||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions));
|
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential, mockTenantId)).returns(() => Promise.resolve(mockSubscriptions));
|
||||||
|
|
||||||
const mockError = 'Test error';
|
const mockError = 'Test error';
|
||||||
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => { throw new Error(mockError); });
|
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => { throw new Error(mockError); });
|
||||||
@@ -305,8 +296,8 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
|
|||||||
|
|
||||||
const children = await accountTreeNode.getChildren();
|
const children = await accountTreeNode.getChildren();
|
||||||
|
|
||||||
should(getSecurityTokenStub.calledOnce).be.true('getSecurityToken should have been called exactly once');
|
should(getSecurityTokenStub.calledTwice).be.true('getSecurityToken should have been called exactly twice - once per subscription');
|
||||||
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredential), TypeMoq.Times.once());
|
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredential, mockTenantId), TypeMoq.Times.once());
|
||||||
mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once());
|
mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once());
|
||||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never());
|
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never());
|
||||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
@@ -325,7 +316,6 @@ describe('AzureResourceAccountTreeNode.clearCache', function (): void {
|
|||||||
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
|
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
|
||||||
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||||
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
|
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
|
||||||
mockTenantService = TypeMoq.Mock.ofType<IAzureResourceTenantService>();
|
|
||||||
|
|
||||||
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
|
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
|
||||||
|
|
||||||
@@ -335,13 +325,11 @@ describe('AzureResourceAccountTreeNode.clearCache', function (): void {
|
|||||||
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||||
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
|
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
|
||||||
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||||
mockAppContext.registerService<IAzureResourceTenantService>(AzureResourceServiceNames.tenantService, mockTenantService.object);
|
|
||||||
|
|
||||||
sinon.stub(azdata.accounts, 'getSecurityToken').returns(Promise.resolve(mockTokens));
|
sinon.stub(azdata.accounts, 'getAccountSecurityToken').returns(Promise.resolve(mockToken));
|
||||||
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
|
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
|
||||||
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
|
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
|
||||||
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache = mockSubscriptions);
|
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache = mockSubscriptions);
|
||||||
mockTenantService.setup((o) => o.getTenantId(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(mockTenantId));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function (): void {
|
afterEach(function (): void {
|
||||||
|
|||||||
@@ -43,13 +43,14 @@ const mockAccount: azdata.Account = {
|
|||||||
isStale: false
|
isStale: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockTenantId: string = 'mock_tenant';
|
||||||
|
|
||||||
const mockSubscription: azureResource.AzureResourceSubscription = {
|
const mockSubscription: azureResource.AzureResourceSubscription = {
|
||||||
id: 'mock_subscription',
|
id: 'mock_subscription',
|
||||||
name: 'mock subscription'
|
name: 'mock subscription',
|
||||||
|
tenant: mockTenantId
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockTenantId: string = 'mock_tenant';
|
|
||||||
|
|
||||||
let mockResourceTreeDataProvider1: TypeMoq.IMock<azureResource.IAzureResourceTreeDataProvider>;
|
let mockResourceTreeDataProvider1: TypeMoq.IMock<azureResource.IAzureResourceTreeDataProvider>;
|
||||||
let mockResourceProvider1: TypeMoq.IMock<azureResource.IAzureResourceProvider>;
|
let mockResourceProvider1: TypeMoq.IMock<azureResource.IAzureResourceProvider>;
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,13 @@ export class Logger {
|
|||||||
private static _piiLogging: boolean = false;
|
private static _piiLogging: boolean = false;
|
||||||
|
|
||||||
static log(msg: any, ...vals: any[]) {
|
static log(msg: any, ...vals: any[]) {
|
||||||
if (vals && vals.length > 0) {
|
const fullMessage = `${msg} - ${vals.map(v => JSON.stringify(v)).join(' - ')}`;
|
||||||
return console.log(msg, vals);
|
console.log(fullMessage);
|
||||||
}
|
|
||||||
console.log(msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static error(msg: any, ...vals: any[]) {
|
static error(msg: any, ...vals: any[]) {
|
||||||
if (vals && vals.length > 0) {
|
const fullMessage = `${msg} - ${vals.map(v => JSON.stringify(v)).join(' - ')}`;
|
||||||
return console.error(msg, vals);
|
console.error(fullMessage);
|
||||||
}
|
|
||||||
console.error(msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static pii(msg: any, ...vals: any[]) {
|
static pii(msg: any, ...vals: any[]) {
|
||||||
|
|||||||
@@ -70,22 +70,22 @@ export class AccountFeature implements StaticFeature {
|
|||||||
account = accountList[0];
|
account = accountList[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const securityToken: { [key: string]: any } = await azdata.accounts.getSecurityToken(account, azdata.AzureResource.AzureKeyVault);
|
|
||||||
const tenant = account.properties.tenants.find((t: { [key: string]: string }) => request.authority.includes(t.id));
|
const tenant = account.properties.tenants.find((t: { [key: string]: string }) => request.authority.includes(t.id));
|
||||||
const unauthorizedMessage = localize('mssql.insufficientlyPrivelagedAzureAccount', "The configured Azure account for {0} does not have sufficient permissions for Azure Key Vault to access a column master key for Always Encrypted.", account.key.accountId);
|
const unauthorizedMessage = localize('mssql.insufficientlyPrivelagedAzureAccount', "The configured Azure account for {0} does not have sufficient permissions for Azure Key Vault to access a column master key for Always Encrypted.", account.key.accountId);
|
||||||
if (!tenant) {
|
if (!tenant) {
|
||||||
window.showErrorMessage(unauthorizedMessage);
|
window.showErrorMessage(unauthorizedMessage);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
let tokenBundle = securityToken[tenant.id];
|
const securityToken = await azdata.accounts.getAccountSecurityToken(account, tenant, azdata.AzureResource.AzureKeyVault);
|
||||||
if (!tokenBundle) {
|
|
||||||
|
if (!securityToken?.token) {
|
||||||
window.showErrorMessage(unauthorizedMessage);
|
window.showErrorMessage(unauthorizedMessage);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let params: contracts.RequestSecurityTokenResponse = {
|
let params: contracts.RequestSecurityTokenResponse = {
|
||||||
accountKey: JSON.stringify(account.key),
|
accountKey: JSON.stringify(account.key),
|
||||||
token: securityToken[tenant.id].token
|
token: securityToken.token
|
||||||
};
|
};
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
|
|||||||
10
src/sql/azdata.d.ts
vendored
10
src/sql/azdata.d.ts
vendored
@@ -2131,9 +2131,18 @@ declare module 'azdata' {
|
|||||||
* @param account Account to generate security token for (defaults to
|
* @param account Account to generate security token for (defaults to
|
||||||
* AzureResource.ResourceManagement if not given)
|
* AzureResource.ResourceManagement if not given)
|
||||||
* @return Promise to return the security token
|
* @return Promise to return the security token
|
||||||
|
* @deprecated use getAccountSecurityToken
|
||||||
*/
|
*/
|
||||||
export function getSecurityToken(account: Account, resource?: AzureResource): Thenable<{ [key: string]: any }>;
|
export function getSecurityToken(account: Account, resource?: AzureResource): Thenable<{ [key: string]: any }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a security token by asking the account's provider
|
||||||
|
* @param account
|
||||||
|
* @param tenant
|
||||||
|
* @param resource
|
||||||
|
*/
|
||||||
|
export function getAccountSecurityToken(account: Account, tenant: string, resource: AzureResource): Thenable<{ token: string, tokenType?: string } | undefined>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [event](#Event) which fires when the accounts have changed.
|
* An [event](#Event) which fires when the accounts have changed.
|
||||||
*/
|
*/
|
||||||
@@ -2279,6 +2288,7 @@ declare module 'azdata' {
|
|||||||
* @param account The account to generate a security token for
|
* @param account The account to generate a security token for
|
||||||
* @param resource The resource to get the token for
|
* @param resource The resource to get the token for
|
||||||
* @return Promise to return a security token object
|
* @return Promise to return a security token object
|
||||||
|
* @deprecated use getAccountSecurityToken
|
||||||
*/
|
*/
|
||||||
getSecurityToken(account: Account, resource: AzureResource): Thenable<{} | undefined>;
|
getSecurityToken(account: Account, resource: AzureResource): Thenable<{} | undefined>;
|
||||||
|
|
||||||
|
|||||||
10
src/sql/azdata.proposed.d.ts
vendored
10
src/sql/azdata.proposed.d.ts
vendored
@@ -494,4 +494,14 @@ declare module 'azdata' {
|
|||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AccountProvider {
|
||||||
|
/**
|
||||||
|
* Generates a security token for the provided account and tenant
|
||||||
|
* @param account The account to generate a security token for
|
||||||
|
* @param resource The resource to get the token for
|
||||||
|
* @return Promise to return a security token object
|
||||||
|
*/
|
||||||
|
getAccountSecurityToken(account: Account, tenant: string, resource: AzureResource): Thenable<{ token: string } | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ export interface IAccountManagementService {
|
|||||||
getAccountProviderMetadata(): Thenable<azdata.AccountProviderMetadata[]>;
|
getAccountProviderMetadata(): Thenable<azdata.AccountProviderMetadata[]>;
|
||||||
getAccountsForProvider(providerId: string): Thenable<azdata.Account[]>;
|
getAccountsForProvider(providerId: string): Thenable<azdata.Account[]>;
|
||||||
getAccounts(): Thenable<azdata.Account[]>;
|
getAccounts(): Thenable<azdata.Account[]>;
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Thenable<{ [key: string]: { token: string } }>;
|
getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Thenable<{ [key: string]: { token: string } }>;
|
||||||
|
getAccountSecurityToken(account: azdata.Account, tenant: string, resource: azdata.AzureResource): Thenable<{ token: string }>;
|
||||||
removeAccount(accountKey: azdata.AccountKey): Thenable<boolean>;
|
removeAccount(accountKey: azdata.AccountKey): Thenable<boolean>;
|
||||||
removeAccounts(): Thenable<boolean>;
|
removeAccounts(): Thenable<boolean>;
|
||||||
refreshAccount(account: azdata.Account): Thenable<azdata.Account>;
|
refreshAccount(account: azdata.Account): Thenable<azdata.Account>;
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ export class TestAccountManagementService implements IAccountManagementService {
|
|||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAccountSecurityToken(account: azdata.Account, tenant: string, resource: azdata.AzureResource): Thenable<{ token: string }> {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
removeAccount(accountKey: azdata.AccountKey): Thenable<boolean> {
|
removeAccount(accountKey: azdata.AccountKey): Thenable<boolean> {
|
||||||
throw new Error('Method not implemented');
|
throw new Error('Method not implemented');
|
||||||
}
|
}
|
||||||
@@ -100,6 +104,9 @@ export class AccountProviderStub implements azdata.AccountProvider {
|
|||||||
return Promise.resolve({});
|
return Promise.resolve({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAccountSecurityToken(account: azdata.Account, tenant: string, resource: azdata.AzureResource): Thenable<{ token: string }> {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
initialize(storedAccounts: azdata.Account[]): Thenable<azdata.Account[]> {
|
initialize(storedAccounts: azdata.Account[]): Thenable<azdata.Account[]> {
|
||||||
return Promise.resolve(storedAccounts);
|
return Promise.resolve(storedAccounts);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,9 +75,13 @@ export class MainThreadAccountManagement extends Disposable implements MainThrea
|
|||||||
clear(accountKey: azdata.AccountKey): Thenable<void> {
|
clear(accountKey: azdata.AccountKey): Thenable<void> {
|
||||||
return self._proxy.$clear(handle, accountKey);
|
return self._proxy.$clear(handle, accountKey);
|
||||||
},
|
},
|
||||||
|
|
||||||
getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Thenable<{}> {
|
getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Thenable<{}> {
|
||||||
return self._proxy.$getSecurityToken(account, resource);
|
return self._proxy.$getSecurityToken(account, resource);
|
||||||
},
|
},
|
||||||
|
getAccountSecurityToken(account: azdata.Account, tenant: string, resource: azdata.AzureResource): Thenable<{ token: string }> {
|
||||||
|
return self._proxy.$getAccountSecurityToken(account, tenant, resource);
|
||||||
|
},
|
||||||
initialize(restoredAccounts: azdata.Account[]): Thenable<azdata.Account[]> {
|
initialize(restoredAccounts: azdata.Account[]): Thenable<azdata.Account[]> {
|
||||||
return self._proxy.$initialize(handle, restoredAccounts);
|
return self._proxy.$initialize(handle, restoredAccounts);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -89,10 +89,7 @@ export class ExtHostAccountManagement extends ExtHostAccountManagementShape {
|
|||||||
return Promise.all(promises).then(() => resultAccounts);
|
return Promise.all(promises).then(() => resultAccounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public $getSecurityToken(account: azdata.Account, resource?: azdata.AzureResource): Thenable<{}> {
|
public $getSecurityToken(account: azdata.Account, resource: azdata.AzureResource = AzureResource.ResourceManagement): Thenable<{}> {
|
||||||
if (resource === undefined) {
|
|
||||||
resource = AzureResource.ResourceManagement;
|
|
||||||
}
|
|
||||||
return this.$getAllAccounts().then(() => {
|
return this.$getAllAccounts().then(() => {
|
||||||
for (const handle in this._accounts) {
|
for (const handle in this._accounts) {
|
||||||
const providerHandle = parseInt(handle);
|
const providerHandle = parseInt(handle);
|
||||||
@@ -105,6 +102,20 @@ export class ExtHostAccountManagement extends ExtHostAccountManagementShape {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public $getAccountSecurityToken(account: azdata.Account, tenant: string, resource: azdata.AzureResource = AzureResource.ResourceManagement): Thenable<{ token: string }> {
|
||||||
|
return this.$getAllAccounts().then(() => {
|
||||||
|
for (const handle in this._accounts) {
|
||||||
|
const providerHandle = parseInt(handle);
|
||||||
|
if (firstIndex(this._accounts[handle], (acct) => acct.key.accountId === account.key.accountId) !== -1) {
|
||||||
|
return this._withProvider(providerHandle, (provider: azdata.AccountProvider) => provider.getAccountSecurityToken(account, tenant, resource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Account ${account.key.accountId} not found.`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public get onDidChangeAccounts(): Event<azdata.DidChangeAccountsParams> {
|
public get onDidChangeAccounts(): Event<azdata.DidChangeAccountsParams> {
|
||||||
return this._onDidChangeAccounts.event;
|
return this._onDidChangeAccounts.event;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,9 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp
|
|||||||
getSecurityToken(account: azdata.Account, resource?: azdata.AzureResource): Thenable<{}> {
|
getSecurityToken(account: azdata.Account, resource?: azdata.AzureResource): Thenable<{}> {
|
||||||
return extHostAccountManagement.$getSecurityToken(account, resource);
|
return extHostAccountManagement.$getSecurityToken(account, resource);
|
||||||
},
|
},
|
||||||
|
getAccountSecurityToken(account: azdata.Account, tenant: string, resource?: azdata.AzureResource): Thenable<{ token: string }> {
|
||||||
|
return extHostAccountManagement.$getAccountSecurityToken(account, tenant, resource);
|
||||||
|
},
|
||||||
onDidChangeAccounts(listener: (e: azdata.DidChangeAccountsParams) => void, thisArgs?: any, disposables?: extHostTypes.Disposable[]) {
|
onDidChangeAccounts(listener: (e: azdata.DidChangeAccountsParams) => void, thisArgs?: any, disposables?: extHostTypes.Disposable[]) {
|
||||||
return extHostAccountManagement.onDidChangeAccounts(listener, thisArgs, disposables);
|
return extHostAccountManagement.onDidChangeAccounts(listener, thisArgs, disposables);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export abstract class ExtHostAccountManagementShape {
|
|||||||
$autoOAuthCancelled(handle: number): Thenable<void> { throw ni(); }
|
$autoOAuthCancelled(handle: number): Thenable<void> { throw ni(); }
|
||||||
$clear(handle: number, accountKey: azdata.AccountKey): Thenable<void> { throw ni(); }
|
$clear(handle: number, accountKey: azdata.AccountKey): Thenable<void> { throw ni(); }
|
||||||
$getSecurityToken(account: azdata.Account, resource?: azdata.AzureResource): Thenable<{}> { throw ni(); }
|
$getSecurityToken(account: azdata.Account, resource?: azdata.AzureResource): Thenable<{}> { throw ni(); }
|
||||||
|
$getAccountSecurityToken(account: azdata.Account, tenant: string, resource?: azdata.AzureResource): Thenable<{ token: string }> { throw ni(); }
|
||||||
$initialize(handle: number, restoredAccounts: azdata.Account[]): Thenable<azdata.Account[]> { throw ni(); }
|
$initialize(handle: number, restoredAccounts: azdata.Account[]): Thenable<azdata.Account[]> { throw ni(); }
|
||||||
$prompt(handle: number): Thenable<azdata.Account | azdata.PromptFailedResult> { throw ni(); }
|
$prompt(handle: number): Thenable<azdata.Account | azdata.PromptFailedResult> { throw ni(); }
|
||||||
$refresh(handle: number, account: azdata.Account): Thenable<azdata.Account | azdata.PromptFailedResult> { throw ni(); }
|
$refresh(handle: number, account: azdata.Account): Thenable<azdata.Account | azdata.PromptFailedResult> { throw ni(); }
|
||||||
|
|||||||
@@ -244,6 +244,19 @@ export class AccountManagementService implements IAccountManagementService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a security token by asking the account's provider
|
||||||
|
* @param account Account to generate security token for
|
||||||
|
* @param tenant Tenant to generate security token for
|
||||||
|
* @param resource The resource to get the security token for
|
||||||
|
* @return Promise to return the security token
|
||||||
|
*/
|
||||||
|
public getAccountSecurityToken(account: azdata.Account, tenant: string, resource: azdata.AzureResource): Thenable<{ token: string }> {
|
||||||
|
return this.doWithProvider(account.key.providerId, provider => {
|
||||||
|
return provider.provider.getAccountSecurityToken(account, tenant, resource);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes an account from the account store and clears sensitive data in the provider
|
* Removes an account from the account store and clears sensitive data in the provider
|
||||||
* @param accountKey Key for the account to remove
|
* @param accountKey Key for the account to remove
|
||||||
|
|||||||
@@ -813,8 +813,8 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
|||||||
const accounts = await this._accountManagementService.getAccounts();
|
const accounts = await this._accountManagementService.getAccounts();
|
||||||
const azureAccounts = accounts.filter(a => a.key.providerId.startsWith('azure'));
|
const azureAccounts = accounts.filter(a => a.key.providerId.startsWith('azure'));
|
||||||
if (azureAccounts && azureAccounts.length > 0) {
|
if (azureAccounts && azureAccounts.length > 0) {
|
||||||
let accountName = (connection.authenticationType === Constants.azureMFA || connection.authenticationType === Constants.azureMFAAndUser) ? connection.azureAccount : connection.userName;
|
let accountId = (connection.authenticationType === Constants.azureMFA || connection.authenticationType === Constants.azureMFAAndUser) ? connection.azureAccount : connection.userName;
|
||||||
let account = find(azureAccounts, account => account.key.accountId === accountName);
|
let account = find(azureAccounts, account => account.key.accountId === accountId);
|
||||||
if (account) {
|
if (account) {
|
||||||
this._logService.debug(`Getting security token for Azure account ${account.key.accountId}`);
|
this._logService.debug(`Getting security token for Azure account ${account.key.accountId}`);
|
||||||
if (account.isStale) {
|
if (account.isStale) {
|
||||||
@@ -827,26 +827,17 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tokensByTenant = await this._accountManagementService.getSecurityToken(account, azureResource);
|
|
||||||
this._logService.debug(`Got tokens for tenants [${Object.keys(tokensByTenant).join(',')}]`);
|
|
||||||
let token: string;
|
|
||||||
const tenantId = connection.azureTenantId;
|
const tenantId = connection.azureTenantId;
|
||||||
if (tenantId && tokensByTenant[tenantId]) {
|
const token = await this._accountManagementService.getAccountSecurityToken(account, tenantId, azureResource);
|
||||||
token = tokensByTenant[tenantId].token;
|
this._logService.debug(`Got token for tenant ${token}`);
|
||||||
} else {
|
if (!token) {
|
||||||
this._logService.debug(`No security token found for specific tenant ${tenantId} - falling back to first one`);
|
|
||||||
const tokens = values(tokensByTenant);
|
|
||||||
if (tokens.length === 0) {
|
|
||||||
this._logService.info(`No security tokens found for account`);
|
this._logService.info(`No security tokens found for account`);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
token = tokens[0].token;
|
connection.options['azureAccountToken'] = token.token;
|
||||||
}
|
|
||||||
connection.options['azureAccountToken'] = token;
|
|
||||||
connection.options['password'] = '';
|
connection.options['password'] = '';
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
this._logService.info(`Could not find Azure account with name ${accountName}`);
|
this._logService.info(`Could not find Azure account with name ${accountId}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._logService.info(`Could not find any Azure accounts from accounts : [${accounts.map(a => `${a.key.accountId} (${a.key.providerId})`).join(',')}]`);
|
this._logService.info(`Could not find any Azure accounts from accounts : [${accounts.map(a => `${a.key.accountId} (${a.key.providerId})`).join(',')}]`);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import 'vs/css!./media/sqlConnection';
|
import 'vs/css!./media/sqlConnection';
|
||||||
|
|
||||||
import { Button } from 'sql/base/browser/ui/button/button';
|
import { Button } from 'sql/base/browser/ui/button/button';
|
||||||
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
|
import { SelectBox, SelectOptionItemSQL } from 'sql/base/browser/ui/selectBox/selectBox';
|
||||||
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
|
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
|
||||||
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
||||||
import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper';
|
import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper';
|
||||||
@@ -520,12 +520,18 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
|||||||
let oldSelection = this._azureAccountDropdown.value;
|
let oldSelection = this._azureAccountDropdown.value;
|
||||||
const accounts = await this._accountManagementService.getAccounts();
|
const accounts = await this._accountManagementService.getAccounts();
|
||||||
this._azureAccountList = accounts.filter(a => a.key.providerId.startsWith('azure'));
|
this._azureAccountList = accounts.filter(a => a.key.providerId.startsWith('azure'));
|
||||||
let accountDropdownOptions = this._azureAccountList.map(account => account.displayInfo.displayName);
|
let accountDropdownOptions: SelectOptionItemSQL[] = this._azureAccountList.map(account => {
|
||||||
|
return {
|
||||||
|
text: account.displayInfo.displayName,
|
||||||
|
value: account.key.accountId
|
||||||
|
} as SelectOptionItemSQL;
|
||||||
|
});
|
||||||
|
|
||||||
if (accountDropdownOptions.length === 0) {
|
if (accountDropdownOptions.length === 0) {
|
||||||
// If there are no accounts add a blank option so that add account isn't automatically selected
|
// If there are no accounts add a blank option so that add account isn't automatically selected
|
||||||
accountDropdownOptions.unshift('');
|
accountDropdownOptions.unshift({ text: '', value: '' });
|
||||||
}
|
}
|
||||||
accountDropdownOptions.push(this._addAzureAccountMessage);
|
accountDropdownOptions.push({ text: this._addAzureAccountMessage, value: this._addAzureAccountMessage });
|
||||||
this._azureAccountDropdown.setOptions(accountDropdownOptions);
|
this._azureAccountDropdown.setOptions(accountDropdownOptions);
|
||||||
this._azureAccountDropdown.selectWithOptionName(oldSelection);
|
this._azureAccountDropdown.selectWithOptionName(oldSelection);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1299,6 +1299,7 @@ suite('SQL ConnectionManagementService tests', () => {
|
|||||||
let servername = 'test-database.database.windows.net';
|
let servername = 'test-database.database.windows.net';
|
||||||
azureConnectionProfile.serverName = servername;
|
azureConnectionProfile.serverName = servername;
|
||||||
let providerId = 'azure_PublicCloud';
|
let providerId = 'azure_PublicCloud';
|
||||||
|
azureConnectionProfile.azureTenantId = 'testTenant';
|
||||||
|
|
||||||
// Set up the account management service to return a token for the given user
|
// Set up the account management service to return a token for the given user
|
||||||
accountManagementService.setup(x => x.getAccountsForProvider(TypeMoq.It.isAny())).returns(providerId => Promise.resolve<azdata.Account[]>([
|
accountManagementService.setup(x => x.getAccountsForProvider(TypeMoq.It.isAny())).returns(providerId => Promise.resolve<azdata.Account[]>([
|
||||||
@@ -1327,10 +1328,9 @@ suite('SQL ConnectionManagementService tests', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
let testToken = 'testToken';
|
let testToken = 'testToken';
|
||||||
accountManagementService.setup(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({
|
accountManagementService.setup(x => x.getAccountSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({
|
||||||
azure_publicCloud: {
|
token: testToken,
|
||||||
token: testToken
|
tokenType: 'Bearer'
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
connectionStore.setup(x => x.addSavedPassword(TypeMoq.It.is(profile => profile.authenticationType === 'AzureMFA'))).returns(profile => Promise.resolve({
|
connectionStore.setup(x => x.addSavedPassword(TypeMoq.It.is(profile => profile.authenticationType === 'AzureMFA'))).returns(profile => Promise.resolve({
|
||||||
profile: profile,
|
profile: profile,
|
||||||
@@ -1384,11 +1384,8 @@ suite('SQL ConnectionManagementService tests', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
let testToken = 'testToken';
|
let returnedToken = { token: 'testToken', tokenType: 'Bearer' };
|
||||||
let returnedTokens = {};
|
accountManagementService.setup(x => x.getAccountSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(returnedToken));
|
||||||
returnedTokens['azure_publicCloud'] = { token: 'badToken' };
|
|
||||||
returnedTokens[azureTenantId] = { token: testToken };
|
|
||||||
accountManagementService.setup(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(returnedTokens));
|
|
||||||
connectionStore.setup(x => x.addSavedPassword(TypeMoq.It.is(profile => profile.authenticationType === 'AzureMFA'))).returns(profile => Promise.resolve({
|
connectionStore.setup(x => x.addSavedPassword(TypeMoq.It.is(profile => profile.authenticationType === 'AzureMFA'))).returns(profile => Promise.resolve({
|
||||||
profile: profile,
|
profile: profile,
|
||||||
savedCred: false
|
savedCred: false
|
||||||
@@ -1399,7 +1396,7 @@ suite('SQL ConnectionManagementService tests', () => {
|
|||||||
|
|
||||||
// Then the returned profile has the account token set corresponding to the requested tenant
|
// Then the returned profile has the account token set corresponding to the requested tenant
|
||||||
assert.equal(profileWithCredentials.userName, azureConnectionProfile.userName);
|
assert.equal(profileWithCredentials.userName, azureConnectionProfile.userName);
|
||||||
assert.equal(profileWithCredentials.options['azureAccountToken'], testToken);
|
assert.equal(profileWithCredentials.options['azureAccountToken'], returnedToken.token);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getConnections test', () => {
|
test('getConnections test', () => {
|
||||||
|
|||||||
@@ -60,7 +60,12 @@ export class FirewallRuleDialogController {
|
|||||||
private async handleOnCreateFirewallRule(): Promise<void> {
|
private async handleOnCreateFirewallRule(): Promise<void> {
|
||||||
const resourceProviderId = this._resourceProviderId;
|
const resourceProviderId = this._resourceProviderId;
|
||||||
try {
|
try {
|
||||||
const securityTokenMappings = await this._accountManagementService.getSecurityToken(this._firewallRuleDialog.viewModel.selectedAccount!, AzureResource.ResourceManagement);
|
const tenantId = this._connection.azureTenantId;
|
||||||
|
const token = await this._accountManagementService.getAccountSecurityToken(this._firewallRuleDialog.viewModel.selectedAccount!, tenantId, AzureResource.ResourceManagement);
|
||||||
|
const securityTokenMappings = {
|
||||||
|
[tenantId]: token
|
||||||
|
};
|
||||||
|
|
||||||
const firewallRuleInfo: azdata.FirewallRuleInfo = {
|
const firewallRuleInfo: azdata.FirewallRuleInfo = {
|
||||||
startIpAddress: this._firewallRuleDialog.viewModel.isIPAddressSelected ? this._firewallRuleDialog.viewModel.defaultIPAddress : this._firewallRuleDialog.viewModel.fromSubnetIPRange,
|
startIpAddress: this._firewallRuleDialog.viewModel.isIPAddressSelected ? this._firewallRuleDialog.viewModel.defaultIPAddress : this._firewallRuleDialog.viewModel.fromSubnetIPRange,
|
||||||
endIpAddress: this._firewallRuleDialog.viewModel.isIPAddressSelected ? this._firewallRuleDialog.viewModel.defaultIPAddress : this._firewallRuleDialog.viewModel.toSubnetIPRange,
|
endIpAddress: this._firewallRuleDialog.viewModel.isIPAddressSelected ? this._firewallRuleDialog.viewModel.defaultIPAddress : this._firewallRuleDialog.viewModel.toSubnetIPRange,
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ suite('Firewall rule dialog controller tests', () => {
|
|||||||
providerName: mssqlProviderName,
|
providerName: mssqlProviderName,
|
||||||
options: {},
|
options: {},
|
||||||
saveProfile: true,
|
saveProfile: true,
|
||||||
id: ''
|
id: '',
|
||||||
|
azureTenantId: 'someTenant'
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -137,7 +138,7 @@ suite('Firewall rule dialog controller tests', () => {
|
|||||||
|
|
||||||
// Then: it should get security token from account management service and call create firewall rule in resource provider
|
// Then: it should get security token from account management service and call create firewall rule in resource provider
|
||||||
await deferredPromise;
|
await deferredPromise;
|
||||||
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockAccountManagementService.verify(x => x.getAccountSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
mockFirewallRuleDialog.verify(x => x.close(), TypeMoq.Times.once());
|
mockFirewallRuleDialog.verify(x => x.close(), TypeMoq.Times.once());
|
||||||
mockFirewallRuleDialog.verify(x => x.onServiceComplete(), TypeMoq.Times.once());
|
mockFirewallRuleDialog.verify(x => x.onServiceComplete(), TypeMoq.Times.once());
|
||||||
@@ -164,7 +165,7 @@ suite('Firewall rule dialog controller tests', () => {
|
|||||||
|
|
||||||
// Then: it should get security token from account management service and an error dialog should have been opened
|
// Then: it should get security token from account management service and an error dialog should have been opened
|
||||||
await deferredPromise;
|
await deferredPromise;
|
||||||
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockAccountManagementService.verify(x => x.getAccountSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never());
|
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never());
|
||||||
});
|
});
|
||||||
@@ -191,7 +192,7 @@ suite('Firewall rule dialog controller tests', () => {
|
|||||||
// Then: it should get security token from account management service and an error dialog should have been opened
|
// Then: it should get security token from account management service and an error dialog should have been opened
|
||||||
await deferredPromise;
|
await deferredPromise;
|
||||||
|
|
||||||
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockAccountManagementService.verify(x => x.getAccountSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
});
|
});
|
||||||
@@ -217,7 +218,7 @@ suite('Firewall rule dialog controller tests', () => {
|
|||||||
|
|
||||||
// Then: it should get security token from account management service and an error dialog should have been opened
|
// Then: it should get security token from account management service and an error dialog should have been opened
|
||||||
await deferredPromise;
|
await deferredPromise;
|
||||||
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockAccountManagementService.verify(x => x.getAccountSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
});
|
});
|
||||||
@@ -226,8 +227,8 @@ suite('Firewall rule dialog controller tests', () => {
|
|||||||
function getMockAccountManagementService(resolveSecurityToken: boolean): TypeMoq.Mock<TestAccountManagementService> {
|
function getMockAccountManagementService(resolveSecurityToken: boolean): TypeMoq.Mock<TestAccountManagementService> {
|
||||||
let accountManagementTestService = new TestAccountManagementService();
|
let accountManagementTestService = new TestAccountManagementService();
|
||||||
let mockAccountManagementService = TypeMoq.Mock.ofInstance(accountManagementTestService);
|
let mockAccountManagementService = TypeMoq.Mock.ofInstance(accountManagementTestService);
|
||||||
mockAccountManagementService.setup(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
mockAccountManagementService.setup(x => x.getAccountSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||||
.returns(() => resolveSecurityToken ? Promise.resolve({}) : Promise.reject(null));
|
.returns(() => resolveSecurityToken ? Promise.resolve({ token: 'token' }) : Promise.reject(null));
|
||||||
return mockAccountManagementService;
|
return mockAccountManagementService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -442,6 +442,8 @@ function getMockAccountManagementService(accounts: azdata.Account[]): TypeMoq.Mo
|
|||||||
.returns(() => Promise.resolve(accounts));
|
.returns(() => Promise.resolve(accounts));
|
||||||
mockAccountManagementService.setup(x => x.getSecurityToken(TypeMoq.It.isValue(accounts[0]), TypeMoq.It.isAny()))
|
mockAccountManagementService.setup(x => x.getSecurityToken(TypeMoq.It.isValue(accounts[0]), TypeMoq.It.isAny()))
|
||||||
.returns(() => Promise.resolve({}));
|
.returns(() => Promise.resolve({}));
|
||||||
|
mockAccountManagementService.setup(x => x.getAccountSecurityToken(TypeMoq.It.isValue(accounts[0]), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||||
|
.returns(() => Promise.resolve(undefined));
|
||||||
mockAccountManagementService.setup(x => x.updateAccountListEvent)
|
mockAccountManagementService.setup(x => x.updateAccountListEvent)
|
||||||
.returns(() => () => { return undefined; });
|
.returns(() => () => { return undefined; });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user