From 7eee5180a23fef38df5ffa70a7efb90eff8292c2 Mon Sep 17 00:00:00 2001 From: Alex Ma Date: Wed, 7 Dec 2022 14:24:26 -0800 Subject: [PATCH] Add change password function and handling (#1771) * added ChangePassword to connectionService * added changepasswordparams * added more code * added more changes to connectionservice * added more changes * added small test * added changepasswordrequest * added different ServerConnection constructor * consolidated changepassword * added exception catch * added passwordChangeFail params * added changePassword to it's own function * simplified changePassword * made fixes to test * added new test * added one additional connection test * added response callback * removed unnecessary SendError * added localized empty password error * added updated error messages * added small fix to check * added changes based on feedback * added minor change * fix tests * renamed messages to errorDetails * simplified error message * small change to connectionservice message * error message change * added environment newline * added error retry messages to STS * added regex * added newline handling --- .../Connection/ConnectionService.cs | 61 +++++++++++++++++ .../Contracts/ChangePasswordParams.cs | 18 +++++ .../Contracts/ChangePasswordRequest.cs | 19 ++++++ .../Contracts/PasswordChangeResponse.cs | 23 +++++++ .../Localization/sr.cs | 66 ++++++++++++++++++ .../Localization/sr.resx | 24 +++++++ .../Localization/sr.strings | 14 ++++ .../Localization/sr.xlf | 30 +++++++++ .../Connection/ConnectionServiceTests.cs | 67 +++++++++++++++++++ 9 files changed, 322 insertions(+) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ChangePasswordParams.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ChangePasswordRequest.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/PasswordChangeResponse.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs index 256f5923..0521326f 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs @@ -22,6 +22,7 @@ using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.Utility; using System.Diagnostics; +using System.Text.RegularExpressions; namespace Microsoft.SqlTools.ServiceLayer.Connection { @@ -1060,6 +1061,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection // Register request and event handlers with the Service Host serviceHost.SetRequestHandler(ConnectionRequest.Type, HandleConnectRequest); serviceHost.SetRequestHandler(CancelConnectRequest.Type, HandleCancelConnectRequest); + serviceHost.SetRequestHandler(ChangePasswordRequest.Type, HandleChangePasswordRequest); serviceHost.SetRequestHandler(DisconnectRequest.Type, HandleDisconnectRequest); serviceHost.SetRequestHandler(ListDatabasesRequest.Type, HandleListDatabasesRequest); serviceHost.SetRequestHandler(ChangeDatabaseRequest.Type, HandleChangeDatabaseRequest); @@ -1137,6 +1139,65 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection }).ContinueWithOnFaulted(null); } + /// + /// Handle new change password requests + /// + /// + /// + /// + protected async Task HandleChangePasswordRequest( + ChangePasswordParams changePasswordParams, + RequestContext requestContext) + { + Logger.Write(TraceEventType.Verbose, "HandleChangePasswordRequest"); + PasswordChangeResponse newResponse = new PasswordChangeResponse(); + try + { + ChangePassword(changePasswordParams); + newResponse.Result = true; + } + catch (Exception ex) + { + newResponse.Result = false; + newResponse.ErrorMessage = ex.InnerException != null ? (ex.Message + Environment.NewLine + Environment.NewLine + ex.InnerException.Message) : ex.Message; + newResponse.ErrorMessage = Regex.Replace(newResponse.ErrorMessage, @"\r?\nChanged database context to '\w+'\.", ""); + newResponse.ErrorMessage = Regex.Replace(newResponse.ErrorMessage, @"\r?\nChanged language setting to \w+\.", ""); + if (newResponse.ErrorMessage.Equals(SR.PasswordChangeEmptyPassword)) + { + newResponse.ErrorMessage += Environment.NewLine + Environment.NewLine + SR.PasswordChangeEmptyPasswordRetry; + } + else if (newResponse.ErrorMessage.Contains(SR.PasswordChangeDNMReqs)) + { + newResponse.ErrorMessage += Environment.NewLine + Environment.NewLine + SR.PasswordChangeDNMReqsRetry; + } + else if (newResponse.ErrorMessage.Contains(SR.PasswordChangePWCannotBeUsed)) + { + newResponse.ErrorMessage += Environment.NewLine + Environment.NewLine + SR.PasswordChangePWCannotBeUsedRetry; + } + } + await requestContext.SendResult(newResponse); + } + + public void ChangePassword(ChangePasswordParams changePasswordParams) + { + // Empty passwords are not valid. + if (string.IsNullOrEmpty(changePasswordParams.NewPassword)) + { + throw new Exception(SR.PasswordChangeEmptyPassword); + } + + // result is null if the ConnectParams was successfully validated + ConnectionCompleteParams result = ValidateConnectParams(changePasswordParams); + if (result != null) + { + throw new Exception(result.ErrorMessage, new Exception(result.Messages)); + } + + // Change the password of the connection + ServerConnection serverConnection = new ServerConnection(changePasswordParams.Connection.ServerName, changePasswordParams.Connection.UserName, changePasswordParams.Connection.Password); + serverConnection.ChangePassword(changePasswordParams.NewPassword); + } + /// /// Handle cancel connect requests /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ChangePasswordParams.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ChangePasswordParams.cs new file mode 100644 index 00000000..4eaa7642 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ChangePasswordParams.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts +{ + /// + /// Parameters for the Change Password Request. + /// + public class ChangePasswordParams : ConnectParams + { + /// + /// The password to change the account of the connection to. + /// + public string NewPassword { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ChangePasswordRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ChangePasswordRequest.cs new file mode 100644 index 00000000..8bd09313 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ChangePasswordRequest.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts +{ + /// + /// Connect request mapping entry + /// + public class ChangePasswordRequest + { + public static readonly + RequestType Type = + RequestType.Create("connection/changepassword"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/PasswordChangeResponse.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/PasswordChangeResponse.cs new file mode 100644 index 00000000..954171d9 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/PasswordChangeResponse.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts +{ + /// + /// Parameters to be sent back after a password change attempt. + /// + public class PasswordChangeResponse + { + /// + /// Status indicating if password change was successful or not. + /// + public bool Result { get; set; } + + /// + /// Error message for the password change, if an error occured. + /// + public string? ErrorMessage { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index 14aff68d..070e8359 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -61,6 +61,54 @@ namespace Microsoft.SqlTools.ServiceLayer } } + public static string PasswordChangeEmptyPassword + { + get + { + return Keys.GetString(Keys.PasswordChangeEmptyPassword); + } + } + + public static string PasswordChangeEmptyPasswordRetry + { + get + { + return Keys.GetString(Keys.PasswordChangeEmptyPasswordRetry); + } + } + + public static string PasswordChangeDNMReqs + { + get + { + return Keys.GetString(Keys.PasswordChangeDNMReqs); + } + } + + public static string PasswordChangeDNMReqsRetry + { + get + { + return Keys.GetString(Keys.PasswordChangeDNMReqsRetry); + } + } + + public static string PasswordChangePWCannotBeUsed + { + get + { + return Keys.GetString(Keys.PasswordChangePWCannotBeUsed); + } + } + + public static string PasswordChangePWCannotBeUsedRetry + { + get + { + return Keys.GetString(Keys.PasswordChangePWCannotBeUsedRetry); + } + } + public static string ConnectionParamsValidateNullOwnerUri { get @@ -10082,6 +10130,24 @@ namespace Microsoft.SqlTools.ServiceLayer public const string ConnectionServiceConnectionCanceled = "ConnectionServiceConnectionCanceled"; + public const string PasswordChangeEmptyPassword = "PasswordChangeEmptyPassword"; + + + public const string PasswordChangeEmptyPasswordRetry = "PasswordChangeEmptyPasswordRetry"; + + + public const string PasswordChangeDNMReqs = "PasswordChangeDNMReqs"; + + + public const string PasswordChangeDNMReqsRetry = "PasswordChangeDNMReqsRetry"; + + + public const string PasswordChangePWCannotBeUsed = "PasswordChangePWCannotBeUsed"; + + + public const string PasswordChangePWCannotBeUsedRetry = "PasswordChangePWCannotBeUsedRetry"; + + public const string ConnectionParamsValidateNullOwnerUri = "ConnectionParamsValidateNullOwnerUri"; diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx index 8669e7ed..a5d8d954 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx @@ -168,6 +168,30 @@ Connection canceled + + New password cannot be empty + + + + Press OK to input a new password that is not empty. + + + + password does not meet operating system policy requirements + + + + Press OK to input a new password that meets operating system policy requirements. + + + + password cannot be used at this time + + + + Press OK to input a different password. + + OwnerUri cannot be null or empty diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index 7a52cb53..07abb28e 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -45,6 +45,20 @@ ConnectionServiceConnStringInvalidIntent(string intent) = Invalid value '{0}' fo ConnectionServiceConnectionCanceled = Connection canceled +### Password Change + +PasswordChangeEmptyPassword = New password cannot be empty + +PasswordChangeEmptyPasswordRetry = Press OK to input a new password that is not empty. + +PasswordChangeDNMReqs = password does not meet operating system policy requirements + +PasswordChangeDNMReqsRetry = Press OK to input a new password that meets operating system policy requirements. + +PasswordChangePWCannotBeUsed = password cannot be used at this time + +PasswordChangePWCannotBeUsedRetry = Press OK to input a different password. + ### Connection Params Validation Errors ConnectionParamsValidateNullOwnerUri = OwnerUri cannot be null or empty diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf index 3de1029f..ba07576f 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf @@ -6505,6 +6505,36 @@ The Query Processor estimates that implementing the following index could improv Actual CPU Cost + + New password cannot be empty + New password cannot be empty + + + + Press OK to input a new password that is not empty. + Press OK to input a new password that is not empty. + + + + password does not meet operating system policy requirements + password does not meet operating system policy requirements + + + + Press OK to input a new password that meets operating system policy requirements. + Press OK to input a new password that meets operating system policy requirements. + + + + password cannot be used at this time + password cannot be used at this time + + + + Press OK to input a different password. + Press OK to input a different password. + + \ No newline at end of file diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Connection/ConnectionServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Connection/ConnectionServiceTests.cs index ce8f4952..b3a3a119 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Connection/ConnectionServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Connection/ConnectionServiceTests.cs @@ -1763,5 +1763,72 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Connection Assert.IsFalse(ConnectionService.IsDbPool("db")); Assert.IsFalse(ConnectionService.IsDbPool(null)); } + + /// + /// Verify that providing an empty password to change password will fire an error. + /// + [Test] + public async Task ConnectionEmptyPasswordChange() + { + var serviceHostMock = new Mock(); + + var connectionService = ConnectionService.Instance; + connectionService.ServiceHost = serviceHostMock.Object; + + // Set up an initial connection + const string ownerUri = "file://my/sample/file.sql"; + ChangePasswordParams testConnectionParams = new ChangePasswordParams() + { + OwnerUri = ownerUri, + Connection = TestObjects.GetTestConnectionDetails(), + NewPassword = "" + }; + Assert.Throws(() => connectionService.ChangePassword(testConnectionParams)); + } + + /// + /// Verify that providing an invalid connection parameter value to change password will fire an error. + /// + [Test] + public async Task ConnectionInvalidParamPasswordChange() + { + var serviceHostMock = new Mock(); + + var connectionService = ConnectionService.Instance; + connectionService.ServiceHost = serviceHostMock.Object; + + // Set up an initial connection + const string ownerUri = "file://my/sample/file.sql"; + ChangePasswordParams testConnectionParams = new ChangePasswordParams() + { + OwnerUri = ownerUri, + Connection = { }, + NewPassword = "TestPassword" + }; + Assert.Throws(() => connectionService.ChangePassword(testConnectionParams)); + } + + /// + /// Verify that providing a non actual connection and a fake password to change password will throw an error. + /// + [Test] + public async Task InvalidConnectionPasswordChange() + { + var serviceHostMock = new Mock(); + + var connectionService = ConnectionService.Instance; + connectionService.ServiceHost = serviceHostMock.Object; + + // Set up an initial connection + const string ownerUri = "file://my/sample/file.sql"; + ChangePasswordParams testConnectionParams = new ChangePasswordParams() + { + OwnerUri = ownerUri, + Connection = TestObjects.GetTestConnectionDetails(), + NewPassword = "TestPassword" + }; + Assert.Throws( + () => connectionService.ChangePassword(testConnectionParams)); + } } }