Handle don't ask again button (#11236)

* Handle don't ask again button

* Change to ignore

* Change language

* Pass in tenant name and ID

* Fix message

* Don't allow common tenant to get interaction required prompt

* Move things around
This commit is contained in:
Amir Omidi
2020-07-13 14:52:41 -07:00
committed by GitHub
parent 3c0d819911
commit 22f85ad4ff
6 changed files with 68 additions and 20 deletions

View File

@@ -28,6 +28,11 @@
"type": "array", "type": "array",
"default": null, "default": null,
"description": "%azure.resource.config.filter.description%" "description": "%azure.resource.config.filter.description%"
},
"azure.tenant.config.filter": {
"type":"array",
"default": [],
"description": "%azure.tenant.config.filter.description%"
} }
} }
}, },

View File

@@ -13,6 +13,7 @@
"azure.resource.startterminal.title": "Start Cloud Shell", "azure.resource.startterminal.title": "Start Cloud Shell",
"azure.resource.connectsqlserver.title": "Connect", "azure.resource.connectsqlserver.title": "Connect",
"azure.resource.connectsqldb.title": "Add to Servers", "azure.resource.connectsqldb.title": "Add to Servers",
"azure.tenant.config.filter.description": "The list of tenant IDs to ignore when querying azure resources. Each element is a tenant id.",
"accounts.clearTokenCache": "Clear Azure Account Token Cache", "accounts.clearTokenCache": "Clear Azure Account Token Cache",

View File

@@ -100,7 +100,7 @@ export abstract class AzureAuth implements vscode.Disposable {
protected readonly MicrosoftAccountType: string = 'microsoft'; protected readonly MicrosoftAccountType: string = 'microsoft';
protected readonly loginEndpointUrl: string; protected readonly loginEndpointUrl: string;
protected readonly commonTenant: string; protected 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;
@@ -117,7 +117,10 @@ export abstract class AzureAuth implements vscode.Disposable {
public readonly userFriendlyName: string public readonly userFriendlyName: string
) { ) {
this.loginEndpointUrl = this.metadata.settings.host; this.loginEndpointUrl = this.metadata.settings.host;
this.commonTenant = 'common'; this.commonTenant = {
id: 'common',
displayName: 'common',
};
this.redirectUri = this.metadata.settings.redirectUri; this.redirectUri = this.metadata.settings.redirectUri;
this.clientId = this.metadata.settings.clientId; this.clientId = this.metadata.settings.clientId;
@@ -390,7 +393,7 @@ export abstract class AzureAuth implements vscode.Disposable {
let refreshResponse: TokenRefreshResponse; let refreshResponse: TokenRefreshResponse;
try { try {
const tokenUrl = `${this.loginEndpointUrl}${tenant}/oauth2/token`; const tokenUrl = `${this.loginEndpointUrl}${tenant.id}/oauth2/token`;
const tokenResponse = await this.makePostRequest(tokenUrl, postData); const tokenResponse = await this.makePostRequest(tokenUrl, postData);
Logger.pii(JSON.stringify(tokenResponse.data)); Logger.pii(JSON.stringify(tokenResponse.data));
const tokenClaims = this.getTokenClaims(tokenResponse.data.access_token); const tokenClaims = this.getTokenClaims(tokenResponse.data.access_token);
@@ -412,7 +415,7 @@ export abstract class AzureAuth implements vscode.Disposable {
if (ex?.response?.data?.error === 'interaction_required') { if (ex?.response?.data?.error === 'interaction_required') {
const shouldOpenLink = await this.openConsentDialog(tenant, resourceId); const shouldOpenLink = await this.openConsentDialog(tenant, resourceId);
if (shouldOpenLink === true) { if (shouldOpenLink === true) {
const { tokenRefreshResponse, authCompleteDeferred } = await this.promptForConsent(resourceEndpoint, tenant); const { tokenRefreshResponse, authCompleteDeferred } = await this.promptForConsent(resourceEndpoint, tenant.id);
refreshResponse = tokenRefreshResponse; refreshResponse = tokenRefreshResponse;
authCompleteDeferred.resolve(); authCompleteDeferred.resolve();
} else { } else {
@@ -423,7 +426,7 @@ export abstract class AzureAuth implements vscode.Disposable {
} }
} }
this.memdb.set(this.createMemdbString(refreshResponse.accessToken.key, tenant, resourceId), refreshResponse.expiresOn); this.memdb.set(this.createMemdbString(refreshResponse.accessToken.key, tenant.id, resourceId), refreshResponse.expiresOn);
return refreshResponse; return refreshResponse;
} catch (err) { } catch (err) {
const msg = localize('azure.noToken', "Retrieving the Azure token failed. Please sign in again."); const msg = localize('azure.noToken', "Retrieving the Azure token failed. Please sign in again.");
@@ -432,24 +435,63 @@ export abstract class AzureAuth implements vscode.Disposable {
} }
} }
private async openConsentDialog(tenantId: string, resourceId: string): Promise<boolean> { private async openConsentDialog(tenant: Tenant, resourceId: string): Promise<boolean> {
if (!tenant.displayName && !tenant.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 configuration = vscode.workspace.getConfiguration('azure.tenant.config');
let values: string[] = configuration.get('filter') ?? [];
return new Set<string>(values);
};
// The user wants to ignore this tenant.
if (getTenantConfigurationSet().has(tenant?.displayName ?? tenant?.id)) {
return false;
}
const updateTenantConfigurationSet = (set: Set<string>): void => {
const configuration = vscode.workspace.getConfiguration('azure.tenant.config');
configuration.update('filter', Array.from(set), vscode.ConfigurationTarget.Global);
};
interface ConsentMessageItem extends vscode.MessageItem { interface ConsentMessageItem extends vscode.MessageItem {
booleanResult: boolean; booleanResult: boolean;
action?: (tenantId: string) => void;
} }
const openItem: ConsentMessageItem = { const openItem: ConsentMessageItem = {
title: localize('open', "Open"), title: localize('azurecore.consentDialog.open', "Open"),
booleanResult: true booleanResult: true
}; };
const closeItem: ConsentMessageItem = { const closeItem: ConsentMessageItem = {
title: localize('cancel', "Cancel"), title: localize('azurecore.consentDialog.cancel', "Cancel"),
isCloseAffordance: true, isCloseAffordance: true,
booleanResult: false booleanResult: false
}; };
const messageBody = localize('azurecore.consentDialog.body', "Your tenant {0} requires you to re-authenticate again to access {1} resources. Press Open to start the authentication process.", tenantId, resourceId); const dontAskAgainItem: ConsentMessageItem = {
const result = await vscode.window.showInformationMessage(messageBody, { modal: true }, openItem, closeItem); title: localize('azurecore.consentDialog.ignore', "Ignore Tenant"),
booleanResult: false,
action: (tenantId: string) => {
let set = getTenantConfigurationSet();
set.add(tenantId);
updateTenantConfigurationSet(set);
}
};
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 result = await vscode.window.showInformationMessage(messageBody, { modal: true }, openItem, closeItem, dontAskAgainItem);
if (result.action) {
result.action(tenant.id);
}
return result.booleanResult; return result.booleanResult;
} }
@@ -463,19 +505,19 @@ export abstract class AzureAuth implements vscode.Disposable {
} }
} }
private async refreshAccessToken(account: azdata.AccountKey, rt: RefreshToken, tenant?: Tenant, resource?: Resource): Promise<TokenRefreshResponse> { private async refreshAccessToken(account: azdata.AccountKey, rt: RefreshToken, tenant: Tenant = this.commonTenant, resource?: Resource): Promise<TokenRefreshResponse> {
const postData: { [key: string]: string } = { const postData: { [key: string]: string } = {
grant_type: 'refresh_token', grant_type: 'refresh_token',
refresh_token: rt.token, refresh_token: rt.token,
client_id: this.clientId, client_id: this.clientId,
tenant: this.commonTenant, tenant: tenant.id,
}; };
if (resource) { if (resource) {
postData.resource = resource.endpoint; postData.resource = resource.endpoint;
} }
const getTokenResponse = await this.getToken(postData, tenant?.id, resource?.id, resource?.endpoint); const getTokenResponse = await this.getToken(postData, tenant, resource?.id, resource?.endpoint);
const accessToken = getTokenResponse?.accessToken; const accessToken = getTokenResponse?.accessToken;
const refreshToken = getTokenResponse?.refreshToken; const refreshToken = getTokenResponse?.refreshToken;

View File

@@ -57,7 +57,7 @@ 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): Promise<{ tokenRefreshResponse: TokenRefreshResponse, authCompleteDeferred: Deferred<void> } | undefined> { public async promptForConsent(resourceEndpoint: string, tenant: string = this.commonTenant.id): Promise<{ tokenRefreshResponse: TokenRefreshResponse, authCompleteDeferred: Deferred<void> } | undefined> {
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 });
@@ -102,7 +102,7 @@ export class AzureAuthCodeGrant extends AzureAuth {
return this.server.shutdown(); return this.server.shutdown();
} }
public async loginWithLocalServer(authCompletePromise: Promise<void>, resourceId: string, tenant: string = this.commonTenant): Promise<AuthCodeResponse | undefined> { public async loginWithLocalServer(authCompletePromise: Promise<void>, resourceId: string, tenant: string = this.commonTenant.id): Promise<AuthCodeResponse | undefined> {
this.server = new SimpleWebServer(); this.server = new SimpleWebServer();
const nonce = crypto.randomBytes(16).toString('base64'); const nonce = crypto.randomBytes(16).toString('base64');
let serverPort: string; let serverPort: string;
@@ -147,7 +147,7 @@ export class AzureAuthCodeGrant extends AzureAuth {
}; };
} }
public async loginWithoutLocalServer(resourceId: string, tenant: string = this.commonTenant): Promise<AuthCodeResponse | undefined> { 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 = crypto.randomBytes(16).toString('base64');
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);

View File

@@ -57,7 +57,7 @@ export class AzureDeviceCode extends AzureAuth {
} }
public async promptForConsent(resourceId: string, tenant: string = this.commonTenant): Promise<undefined> { public async promptForConsent(resourceId: string, tenant: string = this.commonTenant.id): Promise<undefined> {
vscode.window.showErrorMessage(localize('azure.deviceCodeDoesNotSupportConsent', "Device code authentication does not support prompting for consent. Switch the authentication method in settings to code grant.")); vscode.window.showErrorMessage(localize('azure.deviceCodeDoesNotSupportConsent', "Device code authentication does not support prompting for consent. Switch the authentication method in settings to code grant."));
return undefined; return undefined;
} }
@@ -144,7 +144,7 @@ export class AzureDeviceCode extends AzureAuth {
const postData = { const postData = {
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, tenant: this.commonTenant.id,
code: info.device_code code: info.device_code
}; };

View File

@@ -22,12 +22,12 @@ export interface Tenant {
/** /**
* Identifier of the user in the tenant * Identifier of the user in the tenant
*/ */
userId: string; userId?: string;
/** /**
* The category the user has set their tenant to (e.g. Home Tenant) * The category the user has set their tenant to (e.g. Home Tenant)
*/ */
tenantCategory: string; tenantCategory?: string;
} }
/** /**