diff --git a/extensions/azurecore/src/account-provider/azureAccountProvider.ts b/extensions/azurecore/src/account-provider/azureAccountProvider.ts index 3b07a96f95..1bc8107ec1 100644 --- a/extensions/azurecore/src/account-provider/azureAccountProvider.ts +++ b/extensions/azurecore/src/account-provider/azureAccountProvider.ts @@ -158,7 +158,8 @@ export class AzureAccountProvider implements azdata.AccountProvider { const resourceIdMap = new Map([ [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[] = []; diff --git a/extensions/azurecore/src/account-provider/interfaces.ts b/extensions/azurecore/src/account-provider/interfaces.ts index c99887c034..9874554aff 100644 --- a/extensions/azurecore/src/account-provider/interfaces.ts +++ b/extensions/azurecore/src/account-provider/interfaces.ts @@ -74,6 +74,11 @@ interface Settings { */ 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 * instead of querying the tenants endpoint of the armResource diff --git a/extensions/azurecore/src/account-provider/providerSettings.ts b/extensions/azurecore/src/account-provider/providerSettings.ts index a793516ab7..ab0c326de8 100644 --- a/extensions/azurecore/src/account-provider/providerSettings.ts +++ b/extensions/azurecore/src/account-provider/providerSettings.ts @@ -31,6 +31,10 @@ const publicAzureSettings: ProviderSettings = { id: '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' } } diff --git a/extensions/cms/package.json b/extensions/cms/package.json index 32f2bcf3f3..d1ee19e30e 100644 --- a/extensions/cms/package.json +++ b/extensions/cms/package.json @@ -87,7 +87,13 @@ "description": "%cms.connectionOptions.authType.description%", "groupName": "Security", "valueType": "category", - "defaultValue": null, + "defaultValue": "SqlLogin", + "defaultValueOsOverrides": [ + { + "os": "Windows", + "defaultValueOverride": "Integrated" + } + ], "objectType": null, "categoryValues": [ { diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 441d002910..97a3e531ef 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -557,7 +557,13 @@ "description": "%mssql.connectionOptions.authType.description%", "groupName": "Security", "valueType": "category", - "defaultValue": null, + "defaultValue": "SqlLogin", + "defaultValueOsOverrides": [ + { + "os": "Windows", + "defaultValueOverride": "Integrated" + } + ], "objectType": null, "categoryValues": [ { diff --git a/extensions/mssql/src/sqlClusterLookUp.ts b/extensions/mssql/src/sqlClusterLookUp.ts index 79017c7d85..9028996efa 100644 --- a/extensions/mssql/src/sqlClusterLookUp.ts +++ b/extensions/mssql/src/sqlClusterLookUp.ts @@ -138,6 +138,7 @@ class ConnectionParam implements azdata.connection.Connection, azdata.IConnectio public saveProfile: boolean; public id: string; public azureTenantId?: string; + public azureAccount?: string; public providerName: string; public connectionId: string; diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 42df75ca0e..55a2a25a2e 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -86,4 +86,31 @@ declare module 'azdata' { */ onDidClick: vscode.Event; } + + /* + * 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 + } } diff --git a/src/sql/platform/accounts/common/interfaces.ts b/src/sql/platform/accounts/common/interfaces.ts index 5ba010d0e7..36e27b30ec 100644 --- a/src/sql/platform/accounts/common/interfaces.ts +++ b/src/sql/platform/accounts/common/interfaces.ts @@ -44,7 +44,8 @@ export interface IAccountManagementService { // Enum matching the AzureResource enum from azdata.d.ts export enum AzureResource { ResourceManagement = 0, - Sql = 1 + Sql = 1, + OssRdbms = 2 } export interface IAccountStore { diff --git a/src/sql/platform/capabilities/common/capabilitiesService.ts b/src/sql/platform/capabilities/common/capabilitiesService.ts index 948a7a6b39..dd2d8ceb71 100644 --- a/src/sql/platform/capabilities/common/capabilitiesService.ts +++ b/src/sql/platform/capabilities/common/capabilitiesService.ts @@ -20,6 +20,7 @@ export const clientCapabilities = { export interface ConnectionProviderProperties { providerId: string; displayName: string; + azureResource?: string; connectionOptions: azdata.ConnectionOption[]; } diff --git a/src/sql/platform/connection/common/connectionProfile.ts b/src/sql/platform/connection/common/connectionProfile.ts index 60ef2fe236..6e49c27f63 100644 --- a/src/sql/platform/connection/common/connectionProfile.ts +++ b/src/sql/platform/connection/common/connectionProfile.ts @@ -43,6 +43,7 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa this.saveProfile = model.saveProfile; this._id = model.id; this.azureTenantId = model.azureTenantId; + this.azureAccount = model.azureAccount; if (this.capabilitiesService && model.providerName) { let capabilities = this.capabilitiesService.getCapabilities(model.providerName); if (capabilities && capabilities.connection && capabilities.connection.connectionOptions) { @@ -112,6 +113,14 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa 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 { return this.options['registeredServerDescription']; } @@ -196,7 +205,8 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa options: this.options, saveProfile: this.saveProfile, id: this.id, - azureTenantId: this.azureTenantId + azureTenantId: this.azureTenantId, + azureAccount: this.azureAccount }; return result; diff --git a/src/sql/platform/connection/common/constants.ts b/src/sql/platform/connection/common/constants.ts index a101ce272d..ee35e120f8 100644 --- a/src/sql/platform/connection/common/constants.ts +++ b/src/sql/platform/connection/common/constants.ts @@ -24,6 +24,7 @@ export const passwordChars = '***************'; export const sqlLogin = 'SqlLogin'; export const integrated = 'Integrated'; export const azureMFA = 'AzureMFA'; +export const azureMFAAndUser = 'AzureMFAAndUser'; /* CMS constants */ export const cmsProviderName = 'MSSQL-CMS'; diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 48e9ab2bc8..0ed5d04494 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -1817,4 +1817,11 @@ declare module 'sqlops' { */ export function connect(connectionProfile: IConnectionProfile, saveConnection?: boolean, showDashboard?: boolean): Thenable; } + + /* + * Add OssRdbms for sqlops AzureResource. + */ + export enum AzureResource { + OssRdbms = 2 + } } diff --git a/src/sql/workbench/api/browser/mainThreadConnectionManagement.ts b/src/sql/workbench/api/browser/mainThreadConnectionManagement.ts index dbfa4d2e7e..11bb1faf66 100644 --- a/src/sql/workbench/api/browser/mainThreadConnectionManagement.ts +++ b/src/sql/workbench/api/browser/mainThreadConnectionManagement.ts @@ -60,6 +60,7 @@ export class MainThreadConnectionManagement extends Disposable implements MainTh saveProfile: inputProfile.saveProfile, id: inputProfile.id, azureTenantId: inputProfile.azureTenantId, + azureAccount: inputProfile.azureAccount, options: inputProfile.options }; return outputProfile; diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index ade52e6de0..6d1839acc5 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -390,7 +390,8 @@ export class TreeComponentItem extends vsExtTypes.TreeItem { export enum AzureResource { ResourceManagement = 0, - Sql = 1 + Sql = 1, + OssRdbms = 2 } export class TreeItem extends vsExtTypes.TreeItem { @@ -697,6 +698,14 @@ export class ConnectionProfile { this.options['azureTenantId'] = value; } + get azureAccount(): string { + return this.options['azureAccount']; + } + + set azureAccount(value: string) { + this.options['azureAccount'] = value; + } + options: { [key: string]: any } = {}; static createFrom(options: { [key: string]: any }): ConnectionProfile { diff --git a/src/sql/workbench/contrib/connection/common/connectionProviderExtension.ts b/src/sql/workbench/contrib/connection/common/connectionProviderExtension.ts index 4e804365ef..4f16704917 100644 --- a/src/sql/workbench/contrib/connection/common/connectionProviderExtension.ts +++ b/src/sql/workbench/contrib/connection/common/connectionProviderExtension.ts @@ -132,6 +132,21 @@ const ConnectionProviderContrib: IJSONSchema = { defaultValue: { type: 'any' }, + defaultValueOsOverrides: { + type: 'array', + items: { + type: 'object', + properties: { + os: { + type: 'string', + enum: ['Windows', 'Macintosh', 'Linux'] + }, + defaultValueOverride: { + type: 'any' + } + } + } + }, objectType: { type: 'any' }, diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookUtils.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookUtils.test.ts index a583a42d26..a9b3c41fad 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookUtils.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookUtils.test.ts @@ -26,7 +26,8 @@ suite('notebookUtils', function (): void { saveProfile: true, id: '', options: {}, - azureTenantId: undefined + azureTenantId: undefined, + azureAccount: undefined }; test('Should format server and database name correctly for attach to', async function (): Promise { diff --git a/src/sql/workbench/services/connection/browser/cmsConnectionWidget.ts b/src/sql/workbench/services/connection/browser/cmsConnectionWidget.ts index 93b9c8cdc9..bf9005caa3 100644 --- a/src/sql/workbench/services/connection/browser/cmsConnectionWidget.ts +++ b/src/sql/workbench/services/connection/browser/cmsConnectionWidget.ts @@ -56,12 +56,9 @@ export class CmsConnectionWidget extends ConnectionWidget { _clipboardService, _configurationService, _accountManagementService, _logService); let authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType]; if (authTypeOption) { - if (OS === OperatingSystem.Windows) { - authTypeOption.defaultValue = this.getAuthTypeDisplayName(AuthenticationType.Integrated); - } else { - authTypeOption.defaultValue = this.getAuthTypeDisplayName(AuthenticationType.SqlLogin); - } - this._authTypeSelectBox = new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeOption.defaultValue, this._contextViewService, undefined, { ariaLabel: authTypeOption.displayName }); + let authTypeDefault = this.getAuthTypeDefault(authTypeOption, OS); + let authTypeDefaultDisplay = this.getAuthTypeDisplayName(authTypeDefault); + this._authTypeSelectBox = new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeDefaultDisplay, this._contextViewService, undefined, { ariaLabel: authTypeOption.displayName }); } } diff --git a/src/sql/workbench/services/connection/browser/connectionManagementService.ts b/src/sql/workbench/services/connection/browser/connectionManagementService.ts index 7ccb5e0906..dbff428293 100644 --- a/src/sql/workbench/services/connection/browser/connectionManagementService.ts +++ b/src/sql/workbench/services/connection/browser/connectionManagementService.ts @@ -72,6 +72,8 @@ export class ConnectionManagementService extends Disposable implements IConnecti private _mementoContext: Memento; private _mementoObj: any; private static readonly CONNECTION_MEMENTO = 'ConnectionManagement'; + private static readonly _azureResources: AzureResource[] = + [AzureResource.ResourceManagement, AzureResource.Sql, AzureResource.OssRdbms]; constructor( private _connectionStore: ConnectionStore, @@ -700,22 +702,40 @@ export class ConnectionManagementService extends Disposable implements IConnecti 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 * and clears it if it isn't. * @param connection The connection to fill in or update */ private async fillInOrClearAzureToken(connection: interfaces.IConnectionProfile): Promise { - if (connection.authenticationType !== Constants.azureMFA) { + if (connection.authenticationType !== Constants.azureMFA && connection.authenticationType !== Constants.azureMFAAndUser) { connection.options['azureAccountToken'] = undefined; return true; } if (connection.options['azureAccountToken']) { return true; } + let azureResource = this.getAzureResourceForConnection(connection); let accounts = await this._accountManagementService.getAccountsForProvider('azurePublicCloud'); 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.isStale) { try { @@ -725,7 +745,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti return false; } } - let tokensByTenant = await this._accountManagementService.getSecurityToken(account, AzureResource.Sql); + let tokensByTenant = await this._accountManagementService.getSecurityToken(account, azureResource); let token: string; let tenantId = connection.azureTenantId; if (tenantId && tokensByTenant[tenantId]) { diff --git a/src/sql/workbench/services/connection/browser/connectionWidget.ts b/src/sql/workbench/services/connection/browser/connectionWidget.ts index 1fe1891686..f4b61d12db 100644 --- a/src/sql/workbench/services/connection/browser/connectionWidget.ts +++ b/src/sql/workbench/services/connection/browser/connectionWidget.ts @@ -37,6 +37,13 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ILogService } from 'vs/platform/log/common/log'; import { find } from 'vs/base/common/arrays'; +export enum AuthenticationType { + SqlLogin = 'SqlLogin', + Integrated = 'Integrated', + AzureMFA = 'AzureMFA', + AzureMFAAndUser = 'AzureMFAAndUser' +} + export class ConnectionWidget extends lifecycle.Disposable { private _previousGroupOption: string; private _serverGroupOptions: IConnectionProfileGroup[]; @@ -65,12 +72,16 @@ export class ConnectionWidget extends lifecycle.Disposable { protected _optionsMaps: { [optionType: number]: azdata.ConnectionOption }; protected _tableContainer: HTMLElement; protected _providerName: string; - protected _authTypeMap: { [providerName: string]: AuthenticationType[] } = { - [Constants.mssqlProviderName]: [AuthenticationType.SqlLogin, AuthenticationType.Integrated, AuthenticationType.AzureMFA] - }; protected _connectionNameInputBox: InputBox; protected _databaseNameInputBox: Dropdown; 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 = { id: '', name: localize('defaultServerGroup', ""), @@ -115,16 +126,36 @@ export class ConnectionWidget extends lifecycle.Disposable { let authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType]; if (authTypeOption) { - if (OS === OperatingSystem.Windows) { - authTypeOption.defaultValue = this.getAuthTypeDisplayName(AuthenticationType.Integrated); - } else { - authTypeOption.defaultValue = this.getAuthTypeDisplayName(AuthenticationType.SqlLogin); - } - this._authTypeSelectBox = new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeOption.defaultValue, this._contextViewService, undefined, { ariaLabel: authTypeOption.displayName }); + let authTypeDefault = this.getAuthTypeDefault(authTypeOption, OS); + let authTypeDefaultDisplay = this.getAuthTypeDisplayName(authTypeDefault); + this._authTypeSelectBox = new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeDefaultDisplay, this._contextViewService, undefined, { ariaLabel: authTypeOption.displayName }); } 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 { this._serverGroupOptions = [this.DefaultServerGroup]; 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 let self = this; 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, { validationOptions: { 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 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.inputElement.type = 'password'; this._password = ''; // 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 let accountLabel = localize('connection.azureAccountDropdownLabel', "Account"); @@ -414,17 +445,26 @@ export class ConnectionWidget extends lifecycle.Disposable { } private setConnectButton(): void { - let showUsernameAndPassword: boolean; + let showUsername: boolean; 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); } protected onAuthTypeSelected(selectedAuthType: string) { 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._passwordInputBox.disable(); this._userNameInputBox.hideMessage(); @@ -442,18 +482,37 @@ export class ConnectionWidget extends lifecycle.Disposable { } if (currentAuthType === AuthenticationType.AzureMFA) { - this.fillInAzureAccountOptions().then(() => { + 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.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'); } else { this._azureAccountDropdown.disable(); - DOM.removeClass(this._tableContainer, 'hide-username-password'); - DOM.addClass(this._tableContainer, 'hide-azure-accounts'); 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) { this.onAuthTypeSelected(this._authTypeSelectBox.value); } 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'); } - if (this.authType === AuthenticationType.AzureMFA) { + if (this.authType === AuthenticationType.AzureMFA || this.authType === AuthenticationType.AzureMFAAndUser) { 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(); let tenantId = connectionInfo.azureTenantId; 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) { let displayName: string; - let authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType]; + let authTypeOption: azdata.ConnectionOption = this._optionsMaps[ConnectionOptionSpecialType.authType]; if (authTypeOption) { authTypeOption.categoryValues.forEach(c => { @@ -673,7 +735,7 @@ export class ConnectionWidget extends lifecycle.Disposable { private getAuthTypeName(authTypeDisplayName: string) { let authTypeName: string; - let authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType]; + let authTypeOption: azdata.ConnectionOption = this._optionsMaps[ConnectionOptionSpecialType.authType]; authTypeOption.categoryValues.forEach(c => { if (c.displayName === authTypeDisplayName) { authTypeName = c.name; @@ -716,6 +778,8 @@ export class ConnectionWidget extends lifecycle.Disposable { this._userNameInputBox.enable(); this._passwordInputBox.enable(); this._rememberPasswordCheckBox.enabled = true; + } else if (currentAuthType === AuthenticationType.AzureMFAAndUser) { + this._userNameInputBox.enable(); } if (this._focusedBeforeHandleOnConnection) { @@ -755,8 +819,12 @@ export class ConnectionWidget extends lifecycle.Disposable { 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 { - if (this.authType !== AuthenticationType.AzureMFA) { + if (this.authType !== AuthenticationType.AzureMFA && this.authType !== AuthenticationType.AzureMFAAndUser) { return true; } @@ -808,6 +876,7 @@ export class ConnectionWidget extends lifecycle.Disposable { model.userName = this.userName; model.password = this.password; model.authenticationType = this.authenticationType; + model.azureAccount = this.azureAccount; model.savePassword = this._rememberPasswordCheckBox.checked; model.connectionName = this.connectionName; model.databaseName = this.databaseName; @@ -825,7 +894,7 @@ export class ConnectionWidget extends lifecycle.Disposable { model.groupId = this.findGroupId(model.groupFullName); } } - if (this.authType === AuthenticationType.AzureMFA) { + if (this.authType === AuthenticationType.AzureMFA || this.authType === AuthenticationType.AzureMFAAndUser) { model.azureTenantId = this._azureTenantId; } } @@ -846,8 +915,7 @@ export class ConnectionWidget extends lifecycle.Disposable { } private getMatchingAuthType(displayName: string): AuthenticationType { - const authType = this._authTypeMap[this._providerName]; - return authType ? find(authType, authType => this.getAuthTypeDisplayName(authType) === displayName) : undefined; + return find(ConnectionWidget._authTypes, authType => this.getAuthTypeDisplayName(authType) === displayName); } public closeDatabaseDropdown(): void { @@ -873,9 +941,3 @@ export class ConnectionWidget extends lifecycle.Disposable { } } } - -export enum AuthenticationType { - SqlLogin = 'SqlLogin', - Integrated = 'Integrated', - AzureMFA = 'AzureMFA' -} diff --git a/src/sql/workbench/services/connection/browser/media/connectionDialog.css b/src/sql/workbench/services/connection/browser/media/connectionDialog.css index bff9ebf932..e07aada784 100644 --- a/src/sql/workbench/services/connection/browser/media/connectionDialog.css +++ b/src/sql/workbench/services/connection/browser/media/connectionDialog.css @@ -138,7 +138,11 @@ display: none; } -.hide-username-password .username-password-row { +.hide-username .username-row { + display: none; +} + +.hide-password .password-row { display: none; }