Modify connection management to support Active Directory authType for non-SQL DBs (#8434)

* First attempt to add Azure MFA login for PostgreSQL

* Finish merge with master

* Fix auth type default selection

* Add AzureMFAAndUser auth type for Orcas

* Fix formatting

* Update change log

* Incorporate some review comments

* Missed an occurrence of AzureResource

* Try to move all changes out of azdata.d.ts and sqlops.d.ts

* Concrete implementation of ConnectionProfile in azdata no longer has azureAccount

* Use enum names instead of numbers in config files
This commit is contained in:
Rich Smith
2019-11-26 10:32:59 -08:00
committed by Karl Burtram
parent d9997cebfc
commit b631530753
20 changed files with 231 additions and 52 deletions

View File

@@ -158,7 +158,8 @@ export class AzureAccountProvider implements azdata.AccountProvider {
const resourceIdMap = new Map<azdata.AzureResource, string>([ const resourceIdMap = new Map<azdata.AzureResource, string>([
[azdata.AzureResource.ResourceManagement, self._metadata.settings.armResource.id], [azdata.AzureResource.ResourceManagement, self._metadata.settings.armResource.id],
[azdata.AzureResource.Sql, self._metadata.settings.sqlResource.id] [azdata.AzureResource.Sql, self._metadata.settings.sqlResource.id],
[azdata.AzureResource.OssRdbms, self._metadata.settings.ossRdbmsResource.id]
]); ]);
let accessTokenPromises: Thenable<void>[] = []; let accessTokenPromises: Thenable<void>[] = [];

View File

@@ -74,6 +74,11 @@ interface Settings {
*/ */
sqlResource?: Resource; sqlResource?: Resource;
/**
* Information that describes the OSS RDBMS resource
*/
ossRdbmsResource?: Resource;
/** /**
* A list of tenant IDs to authenticate against. If defined, then these IDs will be used * A list of tenant IDs to authenticate against. If defined, then these IDs will be used
* instead of querying the tenants endpoint of the armResource * instead of querying the tenants endpoint of the armResource

View File

@@ -31,6 +31,10 @@ const publicAzureSettings: ProviderSettings = {
id: 'https://database.windows.net/', id: 'https://database.windows.net/',
endpoint: 'https://database.windows.net' endpoint: 'https://database.windows.net'
}, },
ossRdbmsResource: {
id: 'https://ossrdbms-aad.database.windows.net',
endpoint: 'https://ossrdbms-aad.database.windows.net'
},
redirectUri: 'http://localhost/redirect' redirectUri: 'http://localhost/redirect'
} }
} }

View File

@@ -87,7 +87,13 @@
"description": "%cms.connectionOptions.authType.description%", "description": "%cms.connectionOptions.authType.description%",
"groupName": "Security", "groupName": "Security",
"valueType": "category", "valueType": "category",
"defaultValue": null, "defaultValue": "SqlLogin",
"defaultValueOsOverrides": [
{
"os": "Windows",
"defaultValueOverride": "Integrated"
}
],
"objectType": null, "objectType": null,
"categoryValues": [ "categoryValues": [
{ {

View File

@@ -557,7 +557,13 @@
"description": "%mssql.connectionOptions.authType.description%", "description": "%mssql.connectionOptions.authType.description%",
"groupName": "Security", "groupName": "Security",
"valueType": "category", "valueType": "category",
"defaultValue": null, "defaultValue": "SqlLogin",
"defaultValueOsOverrides": [
{
"os": "Windows",
"defaultValueOverride": "Integrated"
}
],
"objectType": null, "objectType": null,
"categoryValues": [ "categoryValues": [
{ {

View File

@@ -138,6 +138,7 @@ class ConnectionParam implements azdata.connection.Connection, azdata.IConnectio
public saveProfile: boolean; public saveProfile: boolean;
public id: string; public id: string;
public azureTenantId?: string; public azureTenantId?: string;
public azureAccount?: string;
public providerName: string; public providerName: string;
public connectionId: string; public connectionId: string;

View File

@@ -86,4 +86,31 @@ declare module 'azdata' {
*/ */
onDidClick: vscode.Event<any>; onDidClick: vscode.Event<any>;
} }
/*
* Add optional azureAccount for connectionWidget.
*/
export interface IConnectionProfile extends ConnectionInfo {
azureAccount?: string;
}
/*
* Add optional per-OS default value.
*/
export interface DefaultValueOsOverride {
os: string;
defaultValueOverride: string;
}
export interface ConnectionOption {
defaultValueOsOverrides?: DefaultValueOsOverride[];
}
/*
* Add OssRdbms for sqlops AzureResource.
*/
export enum AzureResource {
OssRdbms = 2
}
} }

View File

@@ -44,7 +44,8 @@ export interface IAccountManagementService {
// Enum matching the AzureResource enum from azdata.d.ts // Enum matching the AzureResource enum from azdata.d.ts
export enum AzureResource { export enum AzureResource {
ResourceManagement = 0, ResourceManagement = 0,
Sql = 1 Sql = 1,
OssRdbms = 2
} }
export interface IAccountStore { export interface IAccountStore {

View File

@@ -20,6 +20,7 @@ export const clientCapabilities = {
export interface ConnectionProviderProperties { export interface ConnectionProviderProperties {
providerId: string; providerId: string;
displayName: string; displayName: string;
azureResource?: string;
connectionOptions: azdata.ConnectionOption[]; connectionOptions: azdata.ConnectionOption[];
} }

View File

@@ -43,6 +43,7 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa
this.saveProfile = model.saveProfile; this.saveProfile = model.saveProfile;
this._id = model.id; this._id = model.id;
this.azureTenantId = model.azureTenantId; this.azureTenantId = model.azureTenantId;
this.azureAccount = model.azureAccount;
if (this.capabilitiesService && model.providerName) { if (this.capabilitiesService && model.providerName) {
let capabilities = this.capabilitiesService.getCapabilities(model.providerName); let capabilities = this.capabilitiesService.getCapabilities(model.providerName);
if (capabilities && capabilities.connection && capabilities.connection.connectionOptions) { if (capabilities && capabilities.connection && capabilities.connection.connectionOptions) {
@@ -112,6 +113,14 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa
this.options['azureTenantId'] = value; this.options['azureTenantId'] = value;
} }
public get azureAccount(): string | undefined {
return this.options['azureAccount'];
}
public set azureAccount(value: string | undefined) {
this.options['azureAccount'] = value;
}
public get registeredServerDescription(): string { public get registeredServerDescription(): string {
return this.options['registeredServerDescription']; return this.options['registeredServerDescription'];
} }
@@ -196,7 +205,8 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa
options: this.options, options: this.options,
saveProfile: this.saveProfile, saveProfile: this.saveProfile,
id: this.id, id: this.id,
azureTenantId: this.azureTenantId azureTenantId: this.azureTenantId,
azureAccount: this.azureAccount
}; };
return result; return result;

View File

@@ -24,6 +24,7 @@ export const passwordChars = '***************';
export const sqlLogin = 'SqlLogin'; export const sqlLogin = 'SqlLogin';
export const integrated = 'Integrated'; export const integrated = 'Integrated';
export const azureMFA = 'AzureMFA'; export const azureMFA = 'AzureMFA';
export const azureMFAAndUser = 'AzureMFAAndUser';
/* CMS constants */ /* CMS constants */
export const cmsProviderName = 'MSSQL-CMS'; export const cmsProviderName = 'MSSQL-CMS';

View File

@@ -1817,4 +1817,11 @@ declare module 'sqlops' {
*/ */
export function connect(connectionProfile: IConnectionProfile, saveConnection?: boolean, showDashboard?: boolean): Thenable<ConnectionResult>; export function connect(connectionProfile: IConnectionProfile, saveConnection?: boolean, showDashboard?: boolean): Thenable<ConnectionResult>;
} }
/*
* Add OssRdbms for sqlops AzureResource.
*/
export enum AzureResource {
OssRdbms = 2
}
} }

View File

@@ -60,6 +60,7 @@ export class MainThreadConnectionManagement extends Disposable implements MainTh
saveProfile: inputProfile.saveProfile, saveProfile: inputProfile.saveProfile,
id: inputProfile.id, id: inputProfile.id,
azureTenantId: inputProfile.azureTenantId, azureTenantId: inputProfile.azureTenantId,
azureAccount: inputProfile.azureAccount,
options: inputProfile.options options: inputProfile.options
}; };
return outputProfile; return outputProfile;

View File

@@ -390,7 +390,8 @@ export class TreeComponentItem extends vsExtTypes.TreeItem {
export enum AzureResource { export enum AzureResource {
ResourceManagement = 0, ResourceManagement = 0,
Sql = 1 Sql = 1,
OssRdbms = 2
} }
export class TreeItem extends vsExtTypes.TreeItem { export class TreeItem extends vsExtTypes.TreeItem {
@@ -697,6 +698,14 @@ export class ConnectionProfile {
this.options['azureTenantId'] = value; this.options['azureTenantId'] = value;
} }
get azureAccount(): string {
return this.options['azureAccount'];
}
set azureAccount(value: string) {
this.options['azureAccount'] = value;
}
options: { [key: string]: any } = {}; options: { [key: string]: any } = {};
static createFrom(options: { [key: string]: any }): ConnectionProfile { static createFrom(options: { [key: string]: any }): ConnectionProfile {

View File

@@ -132,6 +132,21 @@ const ConnectionProviderContrib: IJSONSchema = {
defaultValue: { defaultValue: {
type: 'any' type: 'any'
}, },
defaultValueOsOverrides: {
type: 'array',
items: {
type: 'object',
properties: {
os: {
type: 'string',
enum: ['Windows', 'Macintosh', 'Linux']
},
defaultValueOverride: {
type: 'any'
}
}
}
},
objectType: { objectType: {
type: 'any' type: 'any'
}, },

View File

@@ -26,7 +26,8 @@ suite('notebookUtils', function (): void {
saveProfile: true, saveProfile: true,
id: '', id: '',
options: {}, options: {},
azureTenantId: undefined azureTenantId: undefined,
azureAccount: undefined
}; };
test('Should format server and database name correctly for attach to', async function (): Promise<void> { test('Should format server and database name correctly for attach to', async function (): Promise<void> {

View File

@@ -56,12 +56,9 @@ export class CmsConnectionWidget extends ConnectionWidget {
_clipboardService, _configurationService, _accountManagementService, _logService); _clipboardService, _configurationService, _accountManagementService, _logService);
let authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType]; let authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
if (authTypeOption) { if (authTypeOption) {
if (OS === OperatingSystem.Windows) { let authTypeDefault = this.getAuthTypeDefault(authTypeOption, OS);
authTypeOption.defaultValue = this.getAuthTypeDisplayName(AuthenticationType.Integrated); let authTypeDefaultDisplay = this.getAuthTypeDisplayName(authTypeDefault);
} else { this._authTypeSelectBox = new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeDefaultDisplay, this._contextViewService, undefined, { ariaLabel: authTypeOption.displayName });
authTypeOption.defaultValue = this.getAuthTypeDisplayName(AuthenticationType.SqlLogin);
}
this._authTypeSelectBox = new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeOption.defaultValue, this._contextViewService, undefined, { ariaLabel: authTypeOption.displayName });
} }
} }

View File

@@ -72,6 +72,8 @@ export class ConnectionManagementService extends Disposable implements IConnecti
private _mementoContext: Memento; private _mementoContext: Memento;
private _mementoObj: any; private _mementoObj: any;
private static readonly CONNECTION_MEMENTO = 'ConnectionManagement'; private static readonly CONNECTION_MEMENTO = 'ConnectionManagement';
private static readonly _azureResources: AzureResource[] =
[AzureResource.ResourceManagement, AzureResource.Sql, AzureResource.OssRdbms];
constructor( constructor(
private _connectionStore: ConnectionStore, private _connectionStore: ConnectionStore,
@@ -700,22 +702,40 @@ export class ConnectionManagementService extends Disposable implements IConnecti
return defaultProvider && this._providers.has(defaultProvider) ? defaultProvider : undefined; return defaultProvider && this._providers.has(defaultProvider) ? defaultProvider : undefined;
} }
/**
* Previously, the only resource available for AAD access tokens was for Azure SQL / SQL Server.
* Use that as a default if the provider extension does not configure a different one. If one is
* configured, then use it.
* @param connection The connection to fill in or update
*/
private getAzureResourceForConnection(connection: interfaces.IConnectionProfile): azdata.AzureResource {
let provider = this._providers.get(connection.providerName);
if (!provider || !provider.properties || !provider.properties.azureResource) {
return AzureResource.Sql;
}
let result = find(ConnectionManagementService._azureResources, r => AzureResource[r] === provider.properties.azureResource);
return result ? result : AzureResource.Sql;
}
/** /**
* Fills in the Azure account token if it's needed for this connection and doesn't already have one * Fills in the Azure account token if it's needed for this connection and doesn't already have one
* and clears it if it isn't. * and clears it if it isn't.
* @param connection The connection to fill in or update * @param connection The connection to fill in or update
*/ */
private async fillInOrClearAzureToken(connection: interfaces.IConnectionProfile): Promise<boolean> { private async fillInOrClearAzureToken(connection: interfaces.IConnectionProfile): Promise<boolean> {
if (connection.authenticationType !== Constants.azureMFA) { if (connection.authenticationType !== Constants.azureMFA && connection.authenticationType !== Constants.azureMFAAndUser) {
connection.options['azureAccountToken'] = undefined; connection.options['azureAccountToken'] = undefined;
return true; return true;
} }
if (connection.options['azureAccountToken']) { if (connection.options['azureAccountToken']) {
return true; return true;
} }
let azureResource = this.getAzureResourceForConnection(connection);
let accounts = await this._accountManagementService.getAccountsForProvider('azurePublicCloud'); let accounts = await this._accountManagementService.getAccountsForProvider('azurePublicCloud');
if (accounts && accounts.length > 0) { if (accounts && accounts.length > 0) {
let account = find(accounts, account => account.key.accountId === connection.userName); let accountName = (connection.authenticationType !== Constants.azureMFA) ? connection.azureAccount : connection.userName;
let account = find(accounts, account => account.key.accountId === accountName);
if (account) { if (account) {
if (account.isStale) { if (account.isStale) {
try { try {
@@ -725,7 +745,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti
return false; return false;
} }
} }
let tokensByTenant = await this._accountManagementService.getSecurityToken(account, AzureResource.Sql); let tokensByTenant = await this._accountManagementService.getSecurityToken(account, azureResource);
let token: string; let token: string;
let tenantId = connection.azureTenantId; let tenantId = connection.azureTenantId;
if (tenantId && tokensByTenant[tenantId]) { if (tenantId && tokensByTenant[tenantId]) {

View File

@@ -37,6 +37,13 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { find } from 'vs/base/common/arrays'; import { find } from 'vs/base/common/arrays';
export enum AuthenticationType {
SqlLogin = 'SqlLogin',
Integrated = 'Integrated',
AzureMFA = 'AzureMFA',
AzureMFAAndUser = 'AzureMFAAndUser'
}
export class ConnectionWidget extends lifecycle.Disposable { export class ConnectionWidget extends lifecycle.Disposable {
private _previousGroupOption: string; private _previousGroupOption: string;
private _serverGroupOptions: IConnectionProfileGroup[]; private _serverGroupOptions: IConnectionProfileGroup[];
@@ -65,12 +72,16 @@ export class ConnectionWidget extends lifecycle.Disposable {
protected _optionsMaps: { [optionType: number]: azdata.ConnectionOption }; protected _optionsMaps: { [optionType: number]: azdata.ConnectionOption };
protected _tableContainer: HTMLElement; protected _tableContainer: HTMLElement;
protected _providerName: string; protected _providerName: string;
protected _authTypeMap: { [providerName: string]: AuthenticationType[] } = {
[Constants.mssqlProviderName]: [AuthenticationType.SqlLogin, AuthenticationType.Integrated, AuthenticationType.AzureMFA]
};
protected _connectionNameInputBox: InputBox; protected _connectionNameInputBox: InputBox;
protected _databaseNameInputBox: Dropdown; protected _databaseNameInputBox: Dropdown;
protected _advancedButton: Button; protected _advancedButton: Button;
private static readonly _authTypes: AuthenticationType[] =
[AuthenticationType.AzureMFA, AuthenticationType.AzureMFAAndUser, AuthenticationType.Integrated, AuthenticationType.SqlLogin];
private static readonly _osByName = {
Windows: OperatingSystem.Windows,
Macintosh: OperatingSystem.Macintosh,
Linux: OperatingSystem.Linux
};
public DefaultServerGroup: IConnectionProfileGroup = { public DefaultServerGroup: IConnectionProfileGroup = {
id: '', id: '',
name: localize('defaultServerGroup', "<Default>"), name: localize('defaultServerGroup', "<Default>"),
@@ -115,16 +126,36 @@ export class ConnectionWidget extends lifecycle.Disposable {
let authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType]; let authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
if (authTypeOption) { if (authTypeOption) {
if (OS === OperatingSystem.Windows) { let authTypeDefault = this.getAuthTypeDefault(authTypeOption, OS);
authTypeOption.defaultValue = this.getAuthTypeDisplayName(AuthenticationType.Integrated); let authTypeDefaultDisplay = this.getAuthTypeDisplayName(authTypeDefault);
} else { this._authTypeSelectBox = new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeDefaultDisplay, this._contextViewService, undefined, { ariaLabel: authTypeOption.displayName });
authTypeOption.defaultValue = this.getAuthTypeDisplayName(AuthenticationType.SqlLogin);
}
this._authTypeSelectBox = new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeOption.defaultValue, this._contextViewService, undefined, { ariaLabel: authTypeOption.displayName });
} }
this._providerName = providerName; this._providerName = providerName;
} }
protected getAuthTypeDefault(option: azdata.ConnectionOption, os: OperatingSystem): string {
// Check for OS-specific default value
if (option.defaultValueOsOverrides) {
let result = find(option.defaultValueOsOverrides, d => ConnectionWidget._osByName[d.os] === os);
if (result) {
return result.defaultValueOverride;
}
}
// No OS-specific default, and so return global default, if any.
if (option.defaultValue) {
return option.defaultValue;
}
// No default value specified at all. Return first category value.
if (option.categoryValues.length > 0) {
return option.categoryValues[0].name;
}
// Give up.
return undefined;
}
public createConnectionWidget(container: HTMLElement, authTypeChanged: boolean = false): void { public createConnectionWidget(container: HTMLElement, authTypeChanged: boolean = false): void {
this._serverGroupOptions = [this.DefaultServerGroup]; this._serverGroupOptions = [this.DefaultServerGroup];
this._serverGroupSelectBox = new SelectBox(this._serverGroupOptions.map(g => g.name), this.DefaultServerGroup.name, this._contextViewService, undefined, { ariaLabel: this._serverGroupDisplayString }); this._serverGroupSelectBox = new SelectBox(this._serverGroupOptions.map(g => g.name), this.DefaultServerGroup.name, this._contextViewService, undefined, { ariaLabel: this._serverGroupDisplayString });
@@ -210,7 +241,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
// Username // Username
let self = this; let self = this;
let userNameOption = this._optionsMaps[ConnectionOptionSpecialType.userName]; let userNameOption = this._optionsMaps[ConnectionOptionSpecialType.userName];
let userName = DialogHelper.appendRow(this._tableContainer, userNameOption.displayName, 'connection-label', 'connection-input', 'username-password-row'); let userName = DialogHelper.appendRow(this._tableContainer, userNameOption.displayName, 'connection-label', 'connection-input', 'username-row');
this._userNameInputBox = new InputBox(userName, this._contextViewService, { this._userNameInputBox = new InputBox(userName, this._contextViewService, {
validationOptions: { validationOptions: {
validation: (value: string) => self.validateUsername(value, userNameOption.isRequired) ? ({ type: MessageType.ERROR, content: localize('connectionWidget.missingRequireField', "{0} is required.", userNameOption.displayName) }) : null validation: (value: string) => self.validateUsername(value, userNameOption.isRequired) ? ({ type: MessageType.ERROR, content: localize('connectionWidget.missingRequireField', "{0} is required.", userNameOption.displayName) }) : null
@@ -219,14 +250,14 @@ export class ConnectionWidget extends lifecycle.Disposable {
}); });
// Password // Password
let passwordOption = this._optionsMaps[ConnectionOptionSpecialType.password]; let passwordOption = this._optionsMaps[ConnectionOptionSpecialType.password];
let password = DialogHelper.appendRow(this._tableContainer, passwordOption.displayName, 'connection-label', 'connection-input', 'username-password-row'); let password = DialogHelper.appendRow(this._tableContainer, passwordOption.displayName, 'connection-label', 'connection-input', 'password-row');
this._passwordInputBox = new InputBox(password, this._contextViewService, { ariaLabel: passwordOption.displayName }); this._passwordInputBox = new InputBox(password, this._contextViewService, { ariaLabel: passwordOption.displayName });
this._passwordInputBox.inputElement.type = 'password'; this._passwordInputBox.inputElement.type = 'password';
this._password = ''; this._password = '';
// Remember password // Remember password
let rememberPasswordLabel = localize('rememberPassword', "Remember password"); let rememberPasswordLabel = localize('rememberPassword', "Remember password");
this._rememberPasswordCheckBox = this.appendCheckbox(this._tableContainer, rememberPasswordLabel, 'connection-input', 'username-password-row', false); this._rememberPasswordCheckBox = this.appendCheckbox(this._tableContainer, rememberPasswordLabel, 'connection-input', 'password-row', false);
// Azure account picker // Azure account picker
let accountLabel = localize('connection.azureAccountDropdownLabel', "Account"); let accountLabel = localize('connection.azureAccountDropdownLabel', "Account");
@@ -414,17 +445,26 @@ export class ConnectionWidget extends lifecycle.Disposable {
} }
private setConnectButton(): void { private setConnectButton(): void {
let showUsernameAndPassword: boolean; let showUsername: boolean;
if (this.authType) { if (this.authType) {
showUsernameAndPassword = this.authType === AuthenticationType.SqlLogin; showUsername = this.authType === AuthenticationType.SqlLogin || this.authType === AuthenticationType.AzureMFAAndUser;
} }
showUsernameAndPassword ? this._callbacks.onSetConnectButton(!!this.serverName && !!this.userName) : showUsername ? this._callbacks.onSetConnectButton(!!this.serverName && !!this.userName) :
this._callbacks.onSetConnectButton(!!this.serverName); this._callbacks.onSetConnectButton(!!this.serverName);
} }
protected onAuthTypeSelected(selectedAuthType: string) { protected onAuthTypeSelected(selectedAuthType: string) {
let currentAuthType = this.getMatchingAuthType(selectedAuthType); let currentAuthType = this.getMatchingAuthType(selectedAuthType);
if (currentAuthType !== AuthenticationType.SqlLogin) { if (currentAuthType === AuthenticationType.AzureMFAAndUser) {
this._userNameInputBox.enable();
this._passwordInputBox.disable();
this._passwordInputBox.hideMessage();
this._passwordInputBox.value = '';
this._password = '';
this._rememberPasswordCheckBox.checked = false;
this._rememberPasswordCheckBox.enabled = false;
} else if (currentAuthType !== AuthenticationType.SqlLogin) {
this._userNameInputBox.disable(); this._userNameInputBox.disable();
this._passwordInputBox.disable(); this._passwordInputBox.disable();
this._userNameInputBox.hideMessage(); this._userNameInputBox.hideMessage();
@@ -442,18 +482,37 @@ export class ConnectionWidget extends lifecycle.Disposable {
} }
if (currentAuthType === AuthenticationType.AzureMFA) { if (currentAuthType === AuthenticationType.AzureMFA) {
this.fillInAzureAccountOptions().then(() => { this.fillInAzureAccountOptions().then(async () => {
// Don't enable the control until we've populated it // Don't enable the control until we've populated it
this._azureAccountDropdown.enable(); this._azureAccountDropdown.enable();
// Populate tenants
await this.onAzureAccountSelected();
this._azureTenantDropdown.enable();
}).catch(err => this._logService.error(`Unexpected error populating Azure Account dropdown : ${err}`)); }).catch(err => this._logService.error(`Unexpected error populating Azure Account dropdown : ${err}`));
// Immediately show/hide appropriate elements though so user gets immediate feedback while we load accounts // Immediately show/hide appropriate elements though so user gets immediate feedback while we load accounts
DOM.addClass(this._tableContainer, 'hide-username-password'); DOM.addClass(this._tableContainer, 'hide-username');
DOM.addClass(this._tableContainer, 'hide-password');
DOM.removeClass(this._tableContainer, 'hide-azure-accounts');
} else if (currentAuthType === AuthenticationType.AzureMFAAndUser) {
this.fillInAzureAccountOptions().then(async () => {
// Don't enable the control until we've populated it
this._azureAccountDropdown.enable();
// Populate tenants
await this.onAzureAccountSelected();
this._azureTenantDropdown.enable();
}).catch(err => this._logService.error(`Unexpected error populating Azure Account dropdown : ${err}`));
// Immediately show/hide appropriate elements though so user gets immediate feedback while we load accounts
DOM.removeClass(this._tableContainer, 'hide-username');
DOM.addClass(this._tableContainer, 'hide-password');
DOM.removeClass(this._tableContainer, 'hide-azure-accounts'); DOM.removeClass(this._tableContainer, 'hide-azure-accounts');
} else { } else {
this._azureAccountDropdown.disable(); this._azureAccountDropdown.disable();
DOM.removeClass(this._tableContainer, 'hide-username-password');
DOM.addClass(this._tableContainer, 'hide-azure-accounts');
this._azureAccountDropdown.hideMessage(); this._azureAccountDropdown.hideMessage();
this._azureTenantDropdown.disable();
this._azureTenantDropdown.hideMessage();
DOM.removeClass(this._tableContainer, 'hide-username');
DOM.removeClass(this._tableContainer, 'hide-password');
DOM.addClass(this._tableContainer, 'hide-azure-accounts');
} }
} }
@@ -629,13 +688,16 @@ export class ConnectionWidget extends lifecycle.Disposable {
if (this._authTypeSelectBox) { if (this._authTypeSelectBox) {
this.onAuthTypeSelected(this._authTypeSelectBox.value); this.onAuthTypeSelected(this._authTypeSelectBox.value);
} else { } else {
DOM.removeClass(this._tableContainer, 'hide-username-password'); DOM.removeClass(this._tableContainer, 'hide-username');
DOM.removeClass(this._tableContainer, 'hide-password');
DOM.addClass(this._tableContainer, 'hide-azure-accounts'); DOM.addClass(this._tableContainer, 'hide-azure-accounts');
} }
if (this.authType === AuthenticationType.AzureMFA) { if (this.authType === AuthenticationType.AzureMFA || this.authType === AuthenticationType.AzureMFAAndUser) {
this.fillInAzureAccountOptions().then(async () => { this.fillInAzureAccountOptions().then(async () => {
this._azureAccountDropdown.selectWithOptionName(this.getModelValue(connectionInfo.userName)); let accountName = (this.authType === AuthenticationType.AzureMFA)
? connectionInfo.userName : connectionInfo.azureAccount;
this._azureAccountDropdown.selectWithOptionName(this.getModelValue(accountName));
await this.onAzureAccountSelected(); await this.onAzureAccountSelected();
let tenantId = connectionInfo.azureTenantId; let tenantId = connectionInfo.azureTenantId;
let account = find(this._azureAccountList, account => account.key.accountId === this._azureAccountDropdown.value); let account = find(this._azureAccountList, account => account.key.accountId === this._azureAccountDropdown.value);
@@ -659,7 +721,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
protected getAuthTypeDisplayName(authTypeName: string) { protected getAuthTypeDisplayName(authTypeName: string) {
let displayName: string; let displayName: string;
let authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType]; let authTypeOption: azdata.ConnectionOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
if (authTypeOption) { if (authTypeOption) {
authTypeOption.categoryValues.forEach(c => { authTypeOption.categoryValues.forEach(c => {
@@ -673,7 +735,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
private getAuthTypeName(authTypeDisplayName: string) { private getAuthTypeName(authTypeDisplayName: string) {
let authTypeName: string; let authTypeName: string;
let authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType]; let authTypeOption: azdata.ConnectionOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
authTypeOption.categoryValues.forEach(c => { authTypeOption.categoryValues.forEach(c => {
if (c.displayName === authTypeDisplayName) { if (c.displayName === authTypeDisplayName) {
authTypeName = c.name; authTypeName = c.name;
@@ -716,6 +778,8 @@ export class ConnectionWidget extends lifecycle.Disposable {
this._userNameInputBox.enable(); this._userNameInputBox.enable();
this._passwordInputBox.enable(); this._passwordInputBox.enable();
this._rememberPasswordCheckBox.enabled = true; this._rememberPasswordCheckBox.enabled = true;
} else if (currentAuthType === AuthenticationType.AzureMFAAndUser) {
this._userNameInputBox.enable();
} }
if (this._focusedBeforeHandleOnConnection) { if (this._focusedBeforeHandleOnConnection) {
@@ -755,8 +819,12 @@ export class ConnectionWidget extends lifecycle.Disposable {
return this._authTypeSelectBox ? this.getAuthTypeName(this._authTypeSelectBox.value) : undefined; return this._authTypeSelectBox ? this.getAuthTypeName(this._authTypeSelectBox.value) : undefined;
} }
public get azureAccount(): string {
return this.authenticationType === AuthenticationType.AzureMFAAndUser ? this._azureAccountDropdown.value : undefined;
}
private validateAzureAccountSelection(showMessage: boolean = true): boolean { private validateAzureAccountSelection(showMessage: boolean = true): boolean {
if (this.authType !== AuthenticationType.AzureMFA) { if (this.authType !== AuthenticationType.AzureMFA && this.authType !== AuthenticationType.AzureMFAAndUser) {
return true; return true;
} }
@@ -808,6 +876,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
model.userName = this.userName; model.userName = this.userName;
model.password = this.password; model.password = this.password;
model.authenticationType = this.authenticationType; model.authenticationType = this.authenticationType;
model.azureAccount = this.azureAccount;
model.savePassword = this._rememberPasswordCheckBox.checked; model.savePassword = this._rememberPasswordCheckBox.checked;
model.connectionName = this.connectionName; model.connectionName = this.connectionName;
model.databaseName = this.databaseName; model.databaseName = this.databaseName;
@@ -825,7 +894,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
model.groupId = this.findGroupId(model.groupFullName); model.groupId = this.findGroupId(model.groupFullName);
} }
} }
if (this.authType === AuthenticationType.AzureMFA) { if (this.authType === AuthenticationType.AzureMFA || this.authType === AuthenticationType.AzureMFAAndUser) {
model.azureTenantId = this._azureTenantId; model.azureTenantId = this._azureTenantId;
} }
} }
@@ -846,8 +915,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
} }
private getMatchingAuthType(displayName: string): AuthenticationType { private getMatchingAuthType(displayName: string): AuthenticationType {
const authType = this._authTypeMap[this._providerName]; return find(ConnectionWidget._authTypes, authType => this.getAuthTypeDisplayName(authType) === displayName);
return authType ? find(authType, authType => this.getAuthTypeDisplayName(authType) === displayName) : undefined;
} }
public closeDatabaseDropdown(): void { public closeDatabaseDropdown(): void {
@@ -873,9 +941,3 @@ export class ConnectionWidget extends lifecycle.Disposable {
} }
} }
} }
export enum AuthenticationType {
SqlLogin = 'SqlLogin',
Integrated = 'Integrated',
AzureMFA = 'AzureMFA'
}

View File

@@ -138,7 +138,11 @@
display: none; display: none;
} }
.hide-username-password .username-password-row { .hide-username .username-row {
display: none;
}
.hide-password .password-row {
display: none; display: none;
} }