diff --git a/Packages.props b/Packages.props index d0b1e6b4..882c9ca6 100644 --- a/Packages.props +++ b/Packages.props @@ -28,6 +28,7 @@ + diff --git a/bin/nuget/Microsoft.SqlServer.Migration.Logins.1.0.20221103.24.nupkg b/bin/nuget/Microsoft.SqlServer.Migration.Logins.1.0.20221103.24.nupkg new file mode 100644 index 00000000..7eb80001 Binary files /dev/null and b/bin/nuget/Microsoft.SqlServer.Migration.Logins.1.0.20221103.24.nupkg differ diff --git a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj index ee92b620..3658ceed 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj +++ b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj @@ -53,6 +53,7 @@ + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Migration/Contracts/StartLoginMigration.cs b/src/Microsoft.SqlTools.ServiceLayer/Migration/Contracts/StartLoginMigration.cs new file mode 100644 index 00000000..41ecdb05 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Migration/Contracts/StartLoginMigration.cs @@ -0,0 +1,135 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlServer.DataCollection.Common.Contracts.OperationsInfrastructure; +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using System.Collections.Generic; + +namespace Microsoft.SqlTools.ServiceLayer.Migration.Contracts +{ + /// + /// Represents the steps in login migration. + /// + public enum LoginMigrationStep + { + /// + /// Run pre-migration validations + /// + StartValidations, + + /// + /// Step to hash passwords and migrate logins + /// + MigrateLogins, + + /// + /// Step to establish users and logins from source to target + /// + EstablishUserMapping, + + + /// + /// Step to migrate server roles + /// + MigrateServerRoles, + + /// + /// Step to establish roles + /// + EstablishServerRoleMapping, + + /// + /// Step to map all the grant/deny permissions for logins + /// + SetLoginPermissions, + + /// + /// Step to map all server roles grant/deny permissions + /// + SetServerRolePermissions + } + + public class StartLoginMigrationParams + { + /// + /// Connection string to connect to source + /// + public string SourceConnectionString { get; set; } + + /// + /// Connection string to connect to target + /// + public string TargetConnectionString { get; set; } + + /// + /// List of logins to migrate + /// + public List LoginList { get; set; } + + /// + /// Azure active directory domain name (required for Windows Auth) + /// + public string AADDomainName{ get; set; } + } + + public class LoginMigrationResult + { + /// + /// Start time of the assessment + /// + public IDictionary> ExceptionMap { get; set; } + + /// + /// The login migration step that just completed + /// + public LoginMigrationStep CompletedStep { get; set; } + + /// + /// How long this step took + /// + public string ElapsedTime{ get; set; } + } + + public class StartLoginMigrationRequest + { + public static readonly + RequestType Type = + RequestType.Create("migration/startloginmigration"); + } + + public class ValidateLoginMigrationRequest + { + public static readonly + RequestType Type = + RequestType.Create("migration/validateloginmigration"); + } + + public class MigrateLoginsRequest + { + public static readonly + RequestType Type = + RequestType.Create("migration/migratelogins"); + } + + public class EstablishUserMappingRequest + { + public static readonly + RequestType Type = + RequestType.Create("migration/establishusermapping"); + } + public class MigrateServerRolesAndSetPermissionsRequest + { + public static readonly + RequestType Type = + RequestType.Create("migration/migrateserverrolesandsetpermissions"); + } + + public class LoginMigrationNotification + { + public static readonly + EventType Type = + EventType.Create("migration/loginmigrationnotification"); + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Migration/MigrationService.cs b/src/Microsoft.SqlTools.ServiceLayer/Migration/MigrationService.cs index f0c9e035..1afb7fcd 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Migration/MigrationService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Migration/MigrationService.cs @@ -24,6 +24,7 @@ using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.Migration.Contracts; using Microsoft.SqlTools.Utility; +using Microsoft.SqlServer.Migration.Logins; using Microsoft.SqlServer.Migration.SkuRecommendation.Aggregation; using Microsoft.SqlServer.Migration.SkuRecommendation.Models.Sql; using Microsoft.SqlServer.Migration.SkuRecommendation; @@ -39,8 +40,12 @@ using Microsoft.SqlServer.Migration.SkuRecommendation.ElasticStrategy.AzureSqlMa using Microsoft.SqlServer.Migration.SkuRecommendation.ElasticStrategy.AzureSqlDatabase; using Microsoft.SqlServer.Migration.SkuRecommendation.Models; using Microsoft.SqlServer.Migration.SkuRecommendation.Utils; +using Microsoft.SqlServer.DataCollection.Common.Contracts.OperationsInfrastructure; +using System.Threading; +using Microsoft.SqlServer.Migration.Logins.Contracts; using Microsoft.SqlServer.Migration.Assessment.Common.Models; using Microsoft.SqlServer.Migration.Assessment.Common.Utils; +using Microsoft.SqlTools.ServiceLayer.Migration.Utils; namespace Microsoft.SqlTools.ServiceLayer.Migration { @@ -121,6 +126,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Migration this.ServiceHost.SetRequestHandler(StopPerfDataCollectionRequest.Type, HandleStopPerfDataCollectionRequest); this.ServiceHost.SetRequestHandler(RefreshPerfDataCollectionRequest.Type, HandleRefreshPerfDataCollectionRequest); this.ServiceHost.SetRequestHandler(GetSkuRecommendationsRequest.Type, HandleGetSkuRecommendationsRequest); + this.ServiceHost.SetRequestHandler(StartLoginMigrationRequest.Type, HandleStartLoginMigration); + this.ServiceHost.SetRequestHandler(ValidateLoginMigrationRequest.Type, HandleValidateLoginMigration); + this.ServiceHost.SetRequestHandler(MigrateLoginsRequest.Type, HandleMigrateLogins); + this.ServiceHost.SetRequestHandler(EstablishUserMappingRequest.Type, HandleEstablishUserMapping); + this.ServiceHost.SetRequestHandler(MigrateServerRolesAndSetPermissionsRequest.Type, HandleMigrateServerRolesAndSetPermissions); } /// @@ -325,6 +335,231 @@ namespace Microsoft.SqlTools.ServiceLayer.Migration } } + internal async Task HandleStartLoginMigration( + StartLoginMigrationParams parameters, + RequestContext requestContext) + { + try + { + ILoginsMigration loginMigration = new LoginsMigration(parameters.SourceConnectionString, parameters.TargetConnectionString, + null, parameters.LoginList, parameters.AADDomainName); + + IDictionary> exceptionMap = new Dictionary>(); + + exceptionMap.AddExceptions( await loginMigration.StartValidations(CancellationToken.None) ); + exceptionMap.AddExceptions( await loginMigration.MigrateLogins(CancellationToken.None) ); + exceptionMap.AddExceptions( loginMigration.MigrateServerRoles(CancellationToken.None) ); + exceptionMap.AddExceptions( loginMigration.EstablishUserMapping(CancellationToken.None) ); + exceptionMap.AddExceptions( await loginMigration.EstablishServerRoleMapping(CancellationToken.None) ); + exceptionMap.AddExceptions( loginMigration.SetLoginPermissions(CancellationToken.None) ); + exceptionMap.AddExceptions( loginMigration.SetServerRolePermissions(CancellationToken.None) ); + + LoginMigrationResult results = new LoginMigrationResult() + { + ExceptionMap = exceptionMap + }; + + await requestContext.SendResult(results); + } + catch (Exception e) + { + await requestContext.SendError(e.ToString()); + } + } + + internal async Task HandleValidateLoginMigration( + StartLoginMigrationParams parameters, + RequestContext requestContext) + { + try + { + ILoginsMigration loginMigration = new LoginsMigration(parameters.SourceConnectionString, parameters.TargetConnectionString, + null, parameters.LoginList, parameters.AADDomainName); + + IDictionary> exceptionMap = new Dictionary>(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + exceptionMap.AddExceptions( await loginMigration.StartValidations(CancellationToken.None) ); + stopWatch.Stop(); + TimeSpan elapsedTime = stopWatch.Elapsed; + + LoginMigrationResult results = new LoginMigrationResult() + { + ExceptionMap = exceptionMap, + CompletedStep = LoginMigrationStep.StartValidations, + ElapsedTime = MigrationServiceHelper.FormatTimeSpan(elapsedTime) + + }; + + await requestContext.SendResult(results); + } + catch (Exception e) + { + await requestContext.SendError(e.ToString()); + } + } + + internal async Task HandleMigrateLogins( + StartLoginMigrationParams parameters, + RequestContext requestContext) + { + try + { + ILoginsMigration loginMigration = new LoginsMigration(parameters.SourceConnectionString, parameters.TargetConnectionString, + null, parameters.LoginList, parameters.AADDomainName); + + IDictionary> exceptionMap = new Dictionary>(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + exceptionMap.AddExceptions( await loginMigration.StartValidations(CancellationToken.None) ); + exceptionMap.AddExceptions( await loginMigration.MigrateLogins(CancellationToken.None) ); + stopWatch.Stop(); + TimeSpan elapsedTime = stopWatch.Elapsed; + + LoginMigrationResult results = new LoginMigrationResult() + { + ExceptionMap = exceptionMap, + CompletedStep = LoginMigrationStep.MigrateLogins, + ElapsedTime = MigrationServiceHelper.FormatTimeSpan(elapsedTime) + }; + + await requestContext.SendResult(results); + } + catch (Exception e) + { + await requestContext.SendError(e.ToString()); + } + } + + internal async Task HandleEstablishUserMapping( + StartLoginMigrationParams parameters, + RequestContext requestContext) + { + try + { + ILoginsMigration loginMigration = new LoginsMigration(parameters.SourceConnectionString, parameters.TargetConnectionString, + null, parameters.LoginList, parameters.AADDomainName); + + IDictionary> exceptionMap = new Dictionary>(); + + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + exceptionMap.AddExceptions( await loginMigration.StartValidations(CancellationToken.None) ); + exceptionMap.AddExceptions( loginMigration.EstablishUserMapping(CancellationToken.None) ); + stopWatch.Stop(); + TimeSpan elapsedTime = stopWatch.Elapsed; + + LoginMigrationResult results = new LoginMigrationResult() + { + ExceptionMap = exceptionMap, + CompletedStep = LoginMigrationStep.EstablishUserMapping, + ElapsedTime = MigrationServiceHelper.FormatTimeSpan(elapsedTime) + }; + + await requestContext.SendResult(results); + } + catch (Exception e) + { + await requestContext.SendError(e.ToString()); + } + } + + internal async Task HandleMigrateServerRolesAndSetPermissions( + StartLoginMigrationParams parameters, + RequestContext requestContext) + { + try + { + ILoginsMigration loginMigration = new LoginsMigration(parameters.SourceConnectionString, parameters.TargetConnectionString, + null, parameters.LoginList, parameters.AADDomainName); + + IDictionary> exceptionMap = new Dictionary>(); + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + exceptionMap.AddExceptions(await loginMigration.StartValidations(CancellationToken.None)); + stopWatch.Stop(); + TimeSpan elapsedTime = stopWatch.Elapsed; + + await this.ServiceHost.SendEvent( + LoginMigrationNotification.Type, + new LoginMigrationResult() + { + ExceptionMap = exceptionMap, + CompletedStep = LoginMigrationStep.StartValidations, + ElapsedTime = MigrationServiceHelper.FormatTimeSpan(elapsedTime) + }); + + stopWatch.Restart(); + exceptionMap.AddExceptions(loginMigration.MigrateServerRoles(CancellationToken.None)); + stopWatch.Stop(); + elapsedTime = stopWatch.Elapsed; + + await this.ServiceHost.SendEvent( + LoginMigrationNotification.Type, + new LoginMigrationResult() + { + ExceptionMap = exceptionMap, + CompletedStep = LoginMigrationStep.MigrateServerRoles, + ElapsedTime = MigrationServiceHelper.FormatTimeSpan(elapsedTime) + }); + + stopWatch.Restart(); + exceptionMap.AddExceptions(await loginMigration.EstablishServerRoleMapping(CancellationToken.None)); + stopWatch.Stop(); + elapsedTime = stopWatch.Elapsed; + + await this.ServiceHost.SendEvent( + LoginMigrationNotification.Type, + new LoginMigrationResult() + { + ExceptionMap = exceptionMap, + CompletedStep = LoginMigrationStep.EstablishServerRoleMapping, + ElapsedTime = MigrationServiceHelper.FormatTimeSpan(elapsedTime) + }); + + stopWatch.Restart(); + exceptionMap.AddExceptions(loginMigration.SetLoginPermissions(CancellationToken.None)); + stopWatch.Stop(); + elapsedTime = stopWatch.Elapsed; + + await this.ServiceHost.SendEvent( + LoginMigrationNotification.Type, + new LoginMigrationResult() + { + ExceptionMap = exceptionMap, + CompletedStep = LoginMigrationStep.SetLoginPermissions, + ElapsedTime = MigrationServiceHelper.FormatTimeSpan(elapsedTime) + }); + + stopWatch.Restart(); + exceptionMap.AddExceptions(loginMigration.SetServerRolePermissions(CancellationToken.None)); + stopWatch.Stop(); + elapsedTime = stopWatch.Elapsed; + + await this.ServiceHost.SendEvent( + LoginMigrationNotification.Type, + new LoginMigrationResult() + { + ExceptionMap = exceptionMap, + CompletedStep = LoginMigrationStep.SetServerRolePermissions, + ElapsedTime = MigrationServiceHelper.FormatTimeSpan(elapsedTime) + }); + + LoginMigrationResult results = new LoginMigrationResult() + { + ExceptionMap = exceptionMap, + CompletedStep = LoginMigrationStep.SetServerRolePermissions, + ElapsedTime = MigrationServiceHelper.FormatTimeSpan(elapsedTime) + }; + + await requestContext.SendResult(results); + } + catch (Exception e) + { + await requestContext.SendError(e.ToString()); + } + } + internal RecommendationResultSet GenerateBaselineRecommendations(SqlInstanceRequirements req, GetSkuRecommendationsParams parameters) { RecommendationResultSet resultSet = new RecommendationResultSet(); diff --git a/src/Microsoft.SqlTools.ServiceLayer/Migration/Utils/ExtensionMethods.cs b/src/Microsoft.SqlTools.ServiceLayer/Migration/Utils/ExtensionMethods.cs new file mode 100644 index 00000000..d355b313 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Migration/Utils/ExtensionMethods.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Linq; +using Microsoft.SqlServer.DataCollection.Common.Contracts.OperationsInfrastructure; + +namespace Microsoft.SqlTools.ServiceLayer.Migration.Utils +{ + internal static class ExtensionMethods + { + public static void AddExceptions(this IDictionary> exceptionMap1, IDictionary> exceptionMap2) + { + foreach (var keyValuePair2 in exceptionMap2) + { + // If the dictionary already contains the key then merge them + if (exceptionMap1.ContainsKey(keyValuePair2.Key)) + { + foreach (var value in keyValuePair2.Value) + { + exceptionMap1[keyValuePair2.Key].Append(value); + } + continue; + } + exceptionMap1.Add(keyValuePair2); + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Migration/Utils/MigrationServiceHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/Migration/Utils/MigrationServiceHelper.cs new file mode 100644 index 00000000..3c4541d6 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Migration/Utils/MigrationServiceHelper.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.SqlTools.ServiceLayer.Migration.Utils +{ + internal static class MigrationServiceHelper + { + public static string FormatTimeSpan(TimeSpan ts) + { + return String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10); + } + } +}