From f7439a89033d0d690a1ba345dc0b0b816f135b15 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 3 Mar 2021 14:41:07 +0000 Subject: [PATCH 01/11] New config settings to use for Unattended install user update --- .../Configuration/Models/GlobalSettings.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 45abc39268..b8c95aca12 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -94,6 +94,23 @@ namespace Umbraco.Cms.Core.Configuration.Models /// the Run level. /// public bool InstallUnattended { get; set; } = false; + + /// + /// Gets or sets a value to use for creating a user with a name for Unattended Installs + /// + public string UnattendedUserName { get; set; } = string.Empty; + + /// + /// Gets or sets a value to use for creating a user with an email for Unattended Installs + /// + public string UnattendedUserEmail { get; set; } = string.Empty; + + /// + /// Gets or sets a value to use for creating a user with a password for Unattended Installs + /// + public string UnattendedUserPassword { get; set; } = string.Empty; + + /// /// Gets or sets a value indicating whether to disable the election for a single server. /// From 20d6c7e536bbf1a13abfa688423ed0830d98ff24 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 3 Mar 2021 15:09:42 +0000 Subject: [PATCH 02/11] Adds a new empty event with no extra data so we can subscribe when an Unattended install has completed --- .../Events/UnattendedInstallNotification.cs | 12 ++++++++++++ src/Umbraco.Infrastructure/RuntimeState.cs | 15 ++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Core/Events/UnattendedInstallNotification.cs diff --git a/src/Umbraco.Core/Events/UnattendedInstallNotification.cs b/src/Umbraco.Core/Events/UnattendedInstallNotification.cs new file mode 100644 index 0000000000..5bfb64e08f --- /dev/null +++ b/src/Umbraco.Core/Events/UnattendedInstallNotification.cs @@ -0,0 +1,12 @@ +using System; +using Umbraco.Cms.Core.Events; + +namespace Umbraco.Core.Events +{ + /// + /// Used to notify that an Unattended install has completed + /// + public class UnattendedInstallNotification : INotification + { + } +} diff --git a/src/Umbraco.Infrastructure/RuntimeState.cs b/src/Umbraco.Infrastructure/RuntimeState.cs index b62c30e4d2..4df0fac537 100644 --- a/src/Umbraco.Infrastructure/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/RuntimeState.cs @@ -2,15 +2,16 @@ using System; using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Core.Events; namespace Umbraco.Cms.Core { @@ -24,6 +25,7 @@ namespace Umbraco.Cms.Core private readonly IUmbracoDatabaseFactory _databaseFactory; private readonly ILogger _logger; private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; + private readonly IEventAggregator _eventAggregator; /// /// The initial @@ -42,13 +44,15 @@ namespace Umbraco.Cms.Core IUmbracoVersion umbracoVersion, IUmbracoDatabaseFactory databaseFactory, ILogger logger, - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory) + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + IEventAggregator eventAggregator) { _globalSettings = globalSettings.Value; _umbracoVersion = umbracoVersion; _databaseFactory = databaseFactory; _logger = logger; _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; + _eventAggregator = eventAggregator; } @@ -194,7 +198,7 @@ namespace Umbraco.Cms.Core Reason = reason; } - public void DoUnattendedInstall() + public async void DoUnattendedInstall() { // unattended install is not enabled if (_globalSettings.InstallUnattended == false) return; @@ -232,6 +236,11 @@ namespace Umbraco.Cms.Core creator.InitializeDatabaseSchema(); database.CompleteTransaction(); _logger.LogInformation("Unattended install completed."); + + // Emit an event with EventAggregator that unattended install completed + // Then this event can be listened for and create an unattended user + await _eventAggregator.PublishAsync(new UnattendedInstallNotification()); + } catch (Exception ex) { From b0f078fa4bdaa502946c7286823b7e5ff471204f Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 3 Mar 2021 16:04:28 +0000 Subject: [PATCH 03/11] Trying to get the event/notification handler setup & happy :S --- .../UmbracoBuilderExtensions.cs | 7 ++ ...CreateUnattendedUserNotificationHandler.cs | 84 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 9dd375c9af..c782b46d8c 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -53,6 +53,7 @@ using Umbraco.Cms.Web.Common.Routing; using Umbraco.Cms.Web.Common.Security; using Umbraco.Cms.Web.Common.Templates; using Umbraco.Cms.Web.Common.UmbracoContext; +using Umbraco.Core.Events; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Extensions @@ -270,6 +271,12 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); + // This is a lovely file with no real groupings it seems + // Put close to install & upgrade stuff for the notification/event listener for + builder.AddNotificationHandler(); + + + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs b/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs new file mode 100644 index 0000000000..6886368662 --- /dev/null +++ b/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs @@ -0,0 +1,84 @@ +using System; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Core.Events; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Install +{ + public class CreateUnattendedUserNotificationHandler : INotificationHandler + { + private readonly GlobalSettings _globalSettings; + private readonly IUserService _userService; + private readonly IBackOfficeUserManager _userManager; + + public CreateUnattendedUserNotificationHandler(IOptions globalSettings, IUserService userService, IBackOfficeUserManager userManager) + { + _globalSettings = globalSettings.Value; + _userService = userService; + _userManager = userManager; + } + + /// Listening for when the UnattendedInstallNotification fired after a sucessfulk + /// + /// + public async void Handle(UmbracoApplicationStarting notification) + { + // Ensure we have the setting enabled (Sanity check) + // In theory this should always be true as the event only fired when a sucessfull + if (_globalSettings.InstallUnattended == false) + return; + + var unattendedName = _globalSettings.UnattendedUserName; + var unattendedEmail = _globalSettings.UnattendedUserEmail; + var unattendedPassword = _globalSettings.UnattendedUserPassword; + + // Missing configuration values (json, env variables etc) + if (unattendedName.IsNullOrWhiteSpace() + || unattendedEmail.IsNullOrWhiteSpace() + || unattendedPassword.IsNullOrWhiteSpace()) + { + return; + } + + var admin = _userService.GetUserById(Core.Constants.Security.SuperUserId); + if (admin == null) + { + throw new InvalidOperationException("Could not find the super user!"); + } + + // User email/login has already been modified + if (admin.Email == unattendedEmail) + return; + + // Update name, email & login & save user + admin.Name = unattendedName.Trim(); + admin.Email = unattendedEmail.Trim(); + admin.Username = unattendedEmail.Trim(); + _userService.Save(admin); + + // Change Password for the default user we ship out of the box + // Uses same approach as NewInstall Step + var membershipUser = await _userManager.FindByIdAsync(Core.Constants.Security.SuperUserId.ToString()); + if (membershipUser == null) + { + throw new InvalidOperationException($"No user found in membership provider with id of {Core.Constants.Security.SuperUserId}."); + } + + //To change the password here we actually need to reset it since we don't have an old one to use to change + var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser); + if (string.IsNullOrWhiteSpace(resetToken)) + throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); + + var resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, unattendedPassword.Trim()); + if (!resetResult.Succeeded) + throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); + + throw new NotImplementedException(); + } + + } +} From aa9ff2e670287523da7709a9269cf38463de1dc2 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 3 Mar 2021 16:31:17 +0000 Subject: [PATCH 04/11] The event fires and hits the Handle in the notification/event handler But it implodes when I inject/add UserManager to this --- .../UmbracoBuilderExtensions.cs | 3 +- .../UmbracoBuilderExtensions.cs | 11 ++++-- ...CreateUnattendedUserNotificationHandler.cs | 36 +++++++++---------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index e28c8e4196..e205336678 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -47,7 +47,8 @@ namespace Umbraco.Extensions .AddPreviewSupport() .AddHostedServices() .AddDistributedCache() - .AddModelsBuilderDashboard(); + .AddModelsBuilderDashboard() + .AddUnattedInstallCreateUser(); // Put last to test that everything else injected/setup & happy /// /// Adds Umbraco back office authentication requirements diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index c782b46d8c..7a92eebd3b 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -271,9 +271,7 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); - // This is a lovely file with no real groupings it seems - // Put close to install & upgrade stuff for the notification/event listener for - builder.AddNotificationHandler(); + @@ -298,6 +296,13 @@ namespace Umbraco.Extensions return builder; } + public static IUmbracoBuilder AddUnattedInstallCreateUser(this IUmbracoBuilder builder) + { + builder.AddNotificationHandler(); + //builder.AddNotificationHandler(); + return builder; + } + // TODO: Does this need to exist and/or be public? public static IUmbracoBuilder AddWebServer(this IUmbracoBuilder builder) { diff --git a/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs b/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs index 6886368662..82ffef158a 100644 --- a/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs +++ b/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs @@ -2,30 +2,27 @@ using System; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Core.Events; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.Install { - public class CreateUnattendedUserNotificationHandler : INotificationHandler + public class CreateUnattendedUserNotificationHandler : INotificationHandler { private readonly GlobalSettings _globalSettings; private readonly IUserService _userService; - private readonly IBackOfficeUserManager _userManager; - public CreateUnattendedUserNotificationHandler(IOptions globalSettings, IUserService userService, IBackOfficeUserManager userManager) + public CreateUnattendedUserNotificationHandler(IOptions globalSettings, IUserService userService) { _globalSettings = globalSettings.Value; _userService = userService; - _userManager = userManager; } /// Listening for when the UnattendedInstallNotification fired after a sucessfulk /// /// - public async void Handle(UmbracoApplicationStarting notification) + public async void Handle(UnattendedInstallNotification notification) { // Ensure we have the setting enabled (Sanity check) // In theory this should always be true as the event only fired when a sucessfull @@ -62,22 +59,23 @@ namespace Umbraco.Cms.Web.Common.Install // Change Password for the default user we ship out of the box // Uses same approach as NewInstall Step - var membershipUser = await _userManager.FindByIdAsync(Core.Constants.Security.SuperUserId.ToString()); - if (membershipUser == null) - { - throw new InvalidOperationException($"No user found in membership provider with id of {Core.Constants.Security.SuperUserId}."); - } - //To change the password here we actually need to reset it since we don't have an old one to use to change - var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser); - if (string.IsNullOrWhiteSpace(resetToken)) - throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); + // TODO: usermanager why you no inject?! - var resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, unattendedPassword.Trim()); - if (!resetResult.Succeeded) - throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); + //var membershipUser = await _userManager.FindByIdAsync(Core.Constants.Security.SuperUserId.ToString()); + //if (membershipUser == null) + //{ + // throw new InvalidOperationException($"No user found in membership provider with id of {Core.Constants.Security.SuperUserId}."); + //} - throw new NotImplementedException(); + ////To change the password here we actually need to reset it since we don't have an old one to use to change + //var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser); + //if (string.IsNullOrWhiteSpace(resetToken)) + // throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); + + //var resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, unattendedPassword.Trim()); + //if (!resetResult.Succeeded) + // throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); } } From 008fd4271ce17907b6cb9fab0195543d266522b9 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 3 Mar 2021 19:48:01 +0100 Subject: [PATCH 05/11] Fix unattented admin install --- .../Runtime/CoreRuntime.cs | 9 +- src/Umbraco.Infrastructure/RuntimeState.cs | 4 +- .../UmbracoBuilderExtensions.cs | 4 +- ...CreateUnattendedUserNotificationHandler.cs | 108 ++++++++++-------- 4 files changed, 74 insertions(+), 51 deletions(-) diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 4c05f56d5c..4877fd88c1 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Composing; @@ -10,7 +11,6 @@ using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; -using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Infrastructure.Runtime { @@ -25,6 +25,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime private readonly IUmbracoDatabaseFactory _databaseFactory; private readonly IEventAggregator _eventAggregator; private readonly IHostingEnvironment _hostingEnvironment; + private readonly IServiceScopeFactory _serviceScopeFactory; /// /// Initializes a new instance of the class. @@ -38,7 +39,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime IMainDom mainDom, IUmbracoDatabaseFactory databaseFactory, IEventAggregator eventAggregator, - IHostingEnvironment hostingEnvironment) + IHostingEnvironment hostingEnvironment, + IServiceScopeFactory serviceScopeFactory) { State = state; _loggerFactory = loggerFactory; @@ -49,6 +51,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime _databaseFactory = databaseFactory; _eventAggregator = eventAggregator; _hostingEnvironment = hostingEnvironment; + _serviceScopeFactory = serviceScopeFactory; _logger = _loggerFactory.CreateLogger(); } @@ -60,6 +63,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime /// public async Task StartAsync(CancellationToken cancellationToken) { + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + StaticApplicationLogging.Initialize(_loggerFactory); AppDomain.CurrentDomain.UnhandledException += (_, args) => diff --git a/src/Umbraco.Infrastructure/RuntimeState.cs b/src/Umbraco.Infrastructure/RuntimeState.cs index 4df0fac537..a599af8b0e 100644 --- a/src/Umbraco.Infrastructure/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/RuntimeState.cs @@ -198,7 +198,7 @@ namespace Umbraco.Cms.Core Reason = reason; } - public async void DoUnattendedInstall() + public void DoUnattendedInstall() { // unattended install is not enabled if (_globalSettings.InstallUnattended == false) return; @@ -239,7 +239,7 @@ namespace Umbraco.Cms.Core // Emit an event with EventAggregator that unattended install completed // Then this event can be listened for and create an unattended user - await _eventAggregator.PublishAsync(new UnattendedInstallNotification()); + _eventAggregator.Publish(new UnattendedInstallNotification()); } catch (Exception ex) diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 7a92eebd3b..23254d5ada 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -271,7 +271,7 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); - + @@ -298,7 +298,7 @@ namespace Umbraco.Extensions public static IUmbracoBuilder AddUnattedInstallCreateUser(this IUmbracoBuilder builder) { - builder.AddNotificationHandler(); + builder.AddNotificationAsyncHandler(); //builder.AddNotificationHandler(); return builder; } diff --git a/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs b/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs index 82ffef158a..54137152f0 100644 --- a/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs +++ b/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs @@ -1,81 +1,99 @@ using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Core.Events; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.Install { - public class CreateUnattendedUserNotificationHandler : INotificationHandler + public class CreateUnattendedUserNotificationHandler : INotificationAsyncHandler { private readonly GlobalSettings _globalSettings; private readonly IUserService _userService; + private readonly IServiceScopeFactory _serviceScopeFactory; - public CreateUnattendedUserNotificationHandler(IOptions globalSettings, IUserService userService) + public CreateUnattendedUserNotificationHandler(IOptions globalSettings, IUserService userService, IServiceScopeFactory serviceScopeFactory) { _globalSettings = globalSettings.Value; _userService = userService; + _serviceScopeFactory = serviceScopeFactory; } /// Listening for when the UnattendedInstallNotification fired after a sucessfulk /// /// - public async void Handle(UnattendedInstallNotification notification) + public async Task HandleAsync(UnattendedInstallNotification notification, CancellationToken cancellationToken) { - // Ensure we have the setting enabled (Sanity check) - // In theory this should always be true as the event only fired when a sucessfull - if (_globalSettings.InstallUnattended == false) - return; - var unattendedName = _globalSettings.UnattendedUserName; - var unattendedEmail = _globalSettings.UnattendedUserEmail; - var unattendedPassword = _globalSettings.UnattendedUserPassword; + // Ensure we have the setting enabled (Sanity check) + // In theory this should always be true as the event only fired when a sucessfull + if (_globalSettings.InstallUnattended == false) + { + return; + } - // Missing configuration values (json, env variables etc) - if (unattendedName.IsNullOrWhiteSpace() - || unattendedEmail.IsNullOrWhiteSpace() - || unattendedPassword.IsNullOrWhiteSpace()) - { - return; - } + var unattendedName = _globalSettings.UnattendedUserName; + var unattendedEmail = _globalSettings.UnattendedUserEmail; + var unattendedPassword = _globalSettings.UnattendedUserPassword; - var admin = _userService.GetUserById(Core.Constants.Security.SuperUserId); - if (admin == null) - { - throw new InvalidOperationException("Could not find the super user!"); - } + // Missing configuration values (json, env variables etc) + if (unattendedName.IsNullOrWhiteSpace() + || unattendedEmail.IsNullOrWhiteSpace() + || unattendedPassword.IsNullOrWhiteSpace()) + { + return; + } - // User email/login has already been modified - if (admin.Email == unattendedEmail) - return; + IUser admin = _userService.GetUserById(Core.Constants.Security.SuperUserId); + if (admin == null) + { + throw new InvalidOperationException("Could not find the super user!"); + } - // Update name, email & login & save user - admin.Name = unattendedName.Trim(); - admin.Email = unattendedEmail.Trim(); - admin.Username = unattendedEmail.Trim(); - _userService.Save(admin); + // User email/login has already been modified + if (admin.Email == unattendedEmail) + { + return; + } - // Change Password for the default user we ship out of the box - // Uses same approach as NewInstall Step + // Update name, email & login & save user + admin.Name = unattendedName.Trim(); + admin.Email = unattendedEmail.Trim(); + admin.Username = unattendedEmail.Trim(); + _userService.Save(admin); - // TODO: usermanager why you no inject?! + // Change Password for the default user we ship out of the box + // Uses same approach as NewInstall Step - //var membershipUser = await _userManager.FindByIdAsync(Core.Constants.Security.SuperUserId.ToString()); - //if (membershipUser == null) - //{ - // throw new InvalidOperationException($"No user found in membership provider with id of {Core.Constants.Security.SuperUserId}."); - //} + // TODO: usermanager why you no inject?! + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IBackOfficeUserManager backOfficeUserManager = scope.ServiceProvider.GetRequiredService(); + BackOfficeIdentityUser membershipUser = await backOfficeUserManager.FindByIdAsync(Core.Constants.Security.SuperUserId.ToString()); + if (membershipUser == null) + { + throw new InvalidOperationException($"No user found in membership provider with id of {Core.Constants.Security.SuperUserId}."); + } - ////To change the password here we actually need to reset it since we don't have an old one to use to change - //var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser); - //if (string.IsNullOrWhiteSpace(resetToken)) - // throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); + //To change the password here we actually need to reset it since we don't have an old one to use to change + var resetToken = await backOfficeUserManager.GeneratePasswordResetTokenAsync(membershipUser); + if (string.IsNullOrWhiteSpace(resetToken)) + { + throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); + } - //var resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, unattendedPassword.Trim()); - //if (!resetResult.Succeeded) - // throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); + IdentityResult resetResult = await backOfficeUserManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, unattendedPassword.Trim()); + if (!resetResult.Succeeded) + { + throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); + } } } From ea05685db6441c152f1bd72d1c989bc11439aa6a Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 5 Mar 2021 17:13:09 +0000 Subject: [PATCH 06/11] Revert changes as not needed --- src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 4877fd88c1..4c05f56d5c 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -1,7 +1,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Composing; @@ -11,6 +10,7 @@ using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; +using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Infrastructure.Runtime { @@ -25,7 +25,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime private readonly IUmbracoDatabaseFactory _databaseFactory; private readonly IEventAggregator _eventAggregator; private readonly IHostingEnvironment _hostingEnvironment; - private readonly IServiceScopeFactory _serviceScopeFactory; /// /// Initializes a new instance of the class. @@ -39,8 +38,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime IMainDom mainDom, IUmbracoDatabaseFactory databaseFactory, IEventAggregator eventAggregator, - IHostingEnvironment hostingEnvironment, - IServiceScopeFactory serviceScopeFactory) + IHostingEnvironment hostingEnvironment) { State = state; _loggerFactory = loggerFactory; @@ -51,7 +49,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime _databaseFactory = databaseFactory; _eventAggregator = eventAggregator; _hostingEnvironment = hostingEnvironment; - _serviceScopeFactory = serviceScopeFactory; _logger = _loggerFactory.CreateLogger(); } @@ -63,8 +60,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime /// public async Task StartAsync(CancellationToken cancellationToken) { - using IServiceScope scope = _serviceScopeFactory.CreateScope(); - StaticApplicationLogging.Initialize(_loggerFactory); AppDomain.CurrentDomain.UnhandledException += (_, args) => From 86fff278c65e7263ac3e01952b97ba3f85f2f076 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 5 Mar 2021 17:15:53 +0000 Subject: [PATCH 07/11] Update src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs --- .../DependencyInjection/UmbracoBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index e205336678..25334e4e6e 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -48,7 +48,7 @@ namespace Umbraco.Extensions .AddHostedServices() .AddDistributedCache() .AddModelsBuilderDashboard() - .AddUnattedInstallCreateUser(); // Put last to test that everything else injected/setup & happy + .AddUnattedInstallCreateUser(); /// /// Adds Umbraco back office authentication requirements From e11079c568ee3015b4e3e3a0afea58334254ad50 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 5 Mar 2021 17:15:59 +0000 Subject: [PATCH 08/11] Update src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs --- .../DependencyInjection/UmbracoBuilderExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 23254d5ada..1fc3021315 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -299,7 +299,6 @@ namespace Umbraco.Extensions public static IUmbracoBuilder AddUnattedInstallCreateUser(this IUmbracoBuilder builder) { builder.AddNotificationAsyncHandler(); - //builder.AddNotificationHandler(); return builder; } From 254d88e3c843f9fa2068e34ae32b7d13398cfb8e Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 5 Mar 2021 17:16:05 +0000 Subject: [PATCH 09/11] Update src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs --- .../Install/CreateUnattendedUserNotificationHandler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs b/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs index 54137152f0..d16657f488 100644 --- a/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs +++ b/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs @@ -73,7 +73,6 @@ namespace Umbraco.Cms.Web.Common.Install // Change Password for the default user we ship out of the box // Uses same approach as NewInstall Step - // TODO: usermanager why you no inject?! using IServiceScope scope = _serviceScopeFactory.CreateScope(); IBackOfficeUserManager backOfficeUserManager = scope.ServiceProvider.GetRequiredService(); BackOfficeIdentityUser membershipUser = await backOfficeUserManager.FindByIdAsync(Core.Constants.Security.SuperUserId.ToString()); From 820be3c063f9688d29426e8f4d2d04b48d86f14f Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 5 Mar 2021 17:18:32 +0000 Subject: [PATCH 10/11] Remove the extra whitespace I added --- .../DependencyInjection/UmbracoBuilderExtensions.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 1fc3021315..f096293cd9 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -271,10 +271,6 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); - - - - builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); From 24d4b4c9fb22cef57216b191abcd32a31bbabcbb Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Sun, 7 Mar 2021 19:20:16 +0100 Subject: [PATCH 11/11] Added validation and moved Unattended Settings into their own model. + Added DataAnnonation validation on all our settings --- .../Configuration/Models/GlobalSettings.cs | 27 ----- .../Models/UnattendedSettings.cs | 38 ++++++ .../Validation/UnattendedSettingsValidator.cs | 44 +++++++ src/Umbraco.Core/Constants-Configuration.cs | 1 + .../UmbracoBuilder.Configuration.cs | 55 +++++---- src/Umbraco.Core/Umbraco.Core.csproj | 1 + src/Umbraco.Infrastructure/RuntimeState.cs | 15 ++- .../Testing/UmbracoIntegrationTest.cs | 2 +- ...CreateUnattendedUserNotificationHandler.cs | 111 +++++++++--------- 9 files changed, 183 insertions(+), 111 deletions(-) create mode 100644 src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs create mode 100644 src/Umbraco.Core/Configuration/Models/Validation/UnattendedSettingsValidator.cs diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index b8c95aca12..f8cc97acb8 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -84,33 +84,6 @@ namespace Umbraco.Cms.Core.Configuration.Models /// public bool InstallMissingDatabase { get; set; } = false; - /// - /// Gets or sets a value indicating whether unattended installs are enabled. - /// - /// - /// By default, when a database connection string is configured and it is possible to connect to - /// the database, but the database is empty, the runtime enters the Install level. - /// If this option is set to true an unattended install will be performed and the runtime enters - /// the Run level. - /// - public bool InstallUnattended { get; set; } = false; - - /// - /// Gets or sets a value to use for creating a user with a name for Unattended Installs - /// - public string UnattendedUserName { get; set; } = string.Empty; - - /// - /// Gets or sets a value to use for creating a user with an email for Unattended Installs - /// - public string UnattendedUserEmail { get; set; } = string.Empty; - - /// - /// Gets or sets a value to use for creating a user with a password for Unattended Installs - /// - public string UnattendedUserPassword { get; set; } = string.Empty; - - /// /// Gets or sets a value indicating whether to disable the election for a single server. /// diff --git a/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs b/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs new file mode 100644 index 0000000000..f8779d817c --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations; + +namespace Umbraco.Cms.Core.Configuration.Models +{ + + /// + /// Typed configuration options for unattended settings. + /// + public class UnattendedSettings + { + /// + /// Gets or sets a value indicating whether unattended installs are enabled. + /// + /// + /// By default, when a database connection string is configured and it is possible to connect to + /// the database, but the database is empty, the runtime enters the Install level. + /// If this option is set to true an unattended install will be performed and the runtime enters + /// the Run level. + /// + public bool InstallUnattended { get; set; } = false; + + /// + /// Gets or sets a value to use for creating a user with a name for Unattended Installs + /// + public string UnattendedUserName { get; set; } = null; + + /// + /// Gets or sets a value to use for creating a user with an email for Unattended Installs + /// + [EmailAddress] + public string UnattendedUserEmail { get; set; } = null; + + /// + /// Gets or sets a value to use for creating a user with a password for Unattended Installs + /// + public string UnattendedUserPassword { get; set; } = null; + } +} diff --git a/src/Umbraco.Core/Configuration/Models/Validation/UnattendedSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/UnattendedSettingsValidator.cs new file mode 100644 index 0000000000..3c073ac100 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/Validation/UnattendedSettingsValidator.cs @@ -0,0 +1,44 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Microsoft.Extensions.Options; + +namespace Umbraco.Cms.Core.Configuration.Models.Validation +{ + /// + /// Validator for configuration representated as . + /// + public class UnattendedSettingsValidator + : IValidateOptions + { + /// + public ValidateOptionsResult Validate(string name, UnattendedSettings options) + { + if (options.InstallUnattended) + { + int setValues = 0; + if (!string.IsNullOrEmpty(options.UnattendedUserName)) + { + setValues++; + } + + if (!string.IsNullOrEmpty(options.UnattendedUserEmail)) + { + setValues++; + } + + if (!string.IsNullOrEmpty(options.UnattendedUserPassword)) + { + setValues++; + } + + if (0 < setValues && setValues < 3) + { + return ValidateOptionsResult.Fail($"Configuration entry {Constants.Configuration.ConfigUnattended} contains invalid values.\nIf any of the {nameof(options.UnattendedUserName)}, {nameof(options.UnattendedUserEmail)}, {nameof(options.UnattendedUserPassword)} are set, all of them are required."); + } + } + + return ValidateOptionsResult.Success; + } + } +} diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index 6ad3e0fda0..0d62094dad 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -30,6 +30,7 @@ public const string ConfigCoreDebug = ConfigCorePrefix + "Debug"; public const string ConfigExceptionFilter = ConfigPrefix + "ExceptionFilter"; public const string ConfigGlobal = ConfigPrefix + "Global"; + public const string ConfigUnattended = ConfigPrefix + "Unattended"; public const string ConfigHealthChecks = ConfigPrefix + "HealthChecks"; public const string ConfigHosting = ConfigPrefix + "Hosting"; public const string ConfigImaging = ConfigPrefix + "Imaging"; diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index f987e29eac..47a98ea9e1 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -10,6 +10,14 @@ namespace Umbraco.Cms.Core.DependencyInjection /// public static partial class UmbracoBuilderExtensions { + + private static OptionsBuilder AddOptions(IUmbracoBuilder builder, string key) + where TOptions : class + { + return builder.Services.AddOptions() + .Bind(builder.Config.GetSection(key)) + .ValidateDataAnnotations(); + } /// /// Add Umbraco configuration services and options /// @@ -20,31 +28,34 @@ namespace Umbraco.Cms.Core.DependencyInjection builder.Services.AddSingleton, GlobalSettingsValidator>(); builder.Services.AddSingleton, HealthChecksSettingsValidator>(); builder.Services.AddSingleton, RequestHandlerSettingsValidator>(); + builder.Services.AddSingleton, UnattendedSettingsValidator>(); // Register configuration sections. - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigActiveDirectory)); + builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigModelsBuilder), o => o.BindNonPublicProperties = true); builder.Services.Configure(builder.Config.GetSection("ConnectionStrings"), o => o.BindNonPublicProperties = true); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigContent)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigCoreDebug)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigExceptionFilter)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigGlobal)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigHealthChecks)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigHosting)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigImaging)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigExamine)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigKeepAlive)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigLogging)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigMemberPassword)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigModelsBuilder), o => o.BindNonPublicProperties = true); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigNuCache)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigRequestHandler)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigRuntime)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigSecurity)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigTours)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigTypeFinder)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigUserPassword)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigWebRouting)); - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigPlugins)); + + AddOptions(builder, Constants.Configuration.ConfigActiveDirectory); + AddOptions(builder, Constants.Configuration.ConfigContent); + AddOptions(builder, Constants.Configuration.ConfigCoreDebug); + AddOptions(builder, Constants.Configuration.ConfigExceptionFilter); + AddOptions(builder, Constants.Configuration.ConfigGlobal); + AddOptions(builder, Constants.Configuration.ConfigHealthChecks); + AddOptions(builder, Constants.Configuration.ConfigHosting); + AddOptions(builder, Constants.Configuration.ConfigImaging); + AddOptions(builder, Constants.Configuration.ConfigExamine); + AddOptions(builder, Constants.Configuration.ConfigKeepAlive); + AddOptions(builder, Constants.Configuration.ConfigLogging); + AddOptions(builder, Constants.Configuration.ConfigMemberPassword); + AddOptions(builder, Constants.Configuration.ConfigNuCache); + AddOptions(builder, Constants.Configuration.ConfigRequestHandler); + AddOptions(builder, Constants.Configuration.ConfigRuntime); + AddOptions(builder, Constants.Configuration.ConfigSecurity); + AddOptions(builder, Constants.Configuration.ConfigTours); + AddOptions(builder, Constants.Configuration.ConfigTypeFinder); + AddOptions(builder, Constants.Configuration.ConfigUserPassword); + AddOptions(builder, Constants.Configuration.ConfigWebRouting); + AddOptions(builder, Constants.Configuration.ConfigPlugins); + AddOptions(builder, Constants.Configuration.ConfigUnattended); return builder; } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 1166bc1270..be36173981 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Umbraco.Infrastructure/RuntimeState.cs b/src/Umbraco.Infrastructure/RuntimeState.cs index a599af8b0e..fc8f5f3912 100644 --- a/src/Umbraco.Infrastructure/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/RuntimeState.cs @@ -20,7 +20,8 @@ namespace Umbraco.Cms.Core /// public class RuntimeState : IRuntimeState { - private readonly GlobalSettings _globalSettings; + private readonly IOptions _globalSettings; + private readonly IOptions _unattendedSettings; private readonly IUmbracoVersion _umbracoVersion; private readonly IUmbracoDatabaseFactory _databaseFactory; private readonly ILogger _logger; @@ -41,13 +42,15 @@ namespace Umbraco.Cms.Core /// public RuntimeState( IOptions globalSettings, + IOptions unattendedSettings, IUmbracoVersion umbracoVersion, IUmbracoDatabaseFactory databaseFactory, ILogger logger, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, IEventAggregator eventAggregator) { - _globalSettings = globalSettings.Value; + _globalSettings = globalSettings; + _unattendedSettings = unattendedSettings; _umbracoVersion = umbracoVersion; _databaseFactory = databaseFactory; _logger = logger; @@ -102,7 +105,7 @@ namespace Umbraco.Cms.Core // cannot connect to configured database, this is bad, fail _logger.LogDebug("Could not connect to database."); - if (_globalSettings.InstallMissingDatabase) + if (_globalSettings.Value.InstallMissingDatabase) { // ok to install on a configured but missing database Level = RuntimeLevel.Install; @@ -201,13 +204,13 @@ namespace Umbraco.Cms.Core public void DoUnattendedInstall() { // unattended install is not enabled - if (_globalSettings.InstallUnattended == false) return; + if (_unattendedSettings.Value.InstallUnattended == false) return; // no connection string set if (_databaseFactory.Configured == false) return; var connect = false; - var tries = _globalSettings.InstallMissingDatabase ? 2 : 5; + var tries = _globalSettings.Value.InstallMissingDatabase ? 2 : 5; for (var i = 0;;) { connect = _databaseFactory.CanConnect; @@ -288,7 +291,7 @@ namespace Umbraco.Cms.Core // anything other than install wants a database - see if we can connect // (since this is an already existing database, assume localdb is ready) bool canConnect; - var tries = _globalSettings.InstallMissingDatabase ? 2 : 5; + var tries = _globalSettings.Value.InstallMissingDatabase ? 2 : 5; for (var i = 0; ;) { canConnect = databaseFactory.CanConnect; diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 133320b853..4ee90c4d55 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -107,7 +107,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing [SetUp] public virtual void Setup() { - InMemoryConfiguration[Constants.Configuration.ConfigGlobal + ":" + nameof(GlobalSettings.InstallUnattended)] = "true"; + InMemoryConfiguration[Constants.Configuration.ConfigUnattended + ":" + nameof(UnattendedSettings.InstallUnattended)] = "true"; IHostBuilder hostBuilder = CreateHostBuilder(); IHost host = hostBuilder.Build(); diff --git a/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs b/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs index d16657f488..7017298a8e 100644 --- a/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs +++ b/src/Umbraco.Web.Common/Install/CreateUnattendedUserNotificationHandler.cs @@ -16,83 +16,84 @@ namespace Umbraco.Cms.Web.Common.Install { public class CreateUnattendedUserNotificationHandler : INotificationAsyncHandler { - private readonly GlobalSettings _globalSettings; + private readonly IOptions _unattendedSettings; private readonly IUserService _userService; private readonly IServiceScopeFactory _serviceScopeFactory; - public CreateUnattendedUserNotificationHandler(IOptions globalSettings, IUserService userService, IServiceScopeFactory serviceScopeFactory) + public CreateUnattendedUserNotificationHandler(IOptions unattendedSettings, IUserService userService, IServiceScopeFactory serviceScopeFactory) { - _globalSettings = globalSettings.Value; + _unattendedSettings = unattendedSettings; _userService = userService; _serviceScopeFactory = serviceScopeFactory; } + /// /// Listening for when the UnattendedInstallNotification fired after a sucessfulk /// /// public async Task HandleAsync(UnattendedInstallNotification notification, CancellationToken cancellationToken) { - // Ensure we have the setting enabled (Sanity check) - // In theory this should always be true as the event only fired when a sucessfull - if (_globalSettings.InstallUnattended == false) - { - return; - } + var unattendedSettings = _unattendedSettings.Value; + // Ensure we have the setting enabled (Sanity check) + // In theory this should always be true as the event only fired when a sucessfull + if (_unattendedSettings.Value.InstallUnattended == false) + { + return; + } - var unattendedName = _globalSettings.UnattendedUserName; - var unattendedEmail = _globalSettings.UnattendedUserEmail; - var unattendedPassword = _globalSettings.UnattendedUserPassword; + var unattendedName = unattendedSettings.UnattendedUserName; + var unattendedEmail = unattendedSettings.UnattendedUserEmail; + var unattendedPassword = unattendedSettings.UnattendedUserPassword; - // Missing configuration values (json, env variables etc) - if (unattendedName.IsNullOrWhiteSpace() - || unattendedEmail.IsNullOrWhiteSpace() - || unattendedPassword.IsNullOrWhiteSpace()) - { - return; - } + // Missing configuration values (json, env variables etc) + if (unattendedName.IsNullOrWhiteSpace() + || unattendedEmail.IsNullOrWhiteSpace() + || unattendedPassword.IsNullOrWhiteSpace()) + { + return; + } - IUser admin = _userService.GetUserById(Core.Constants.Security.SuperUserId); - if (admin == null) - { - throw new InvalidOperationException("Could not find the super user!"); - } + IUser admin = _userService.GetUserById(Core.Constants.Security.SuperUserId); + if (admin == null) + { + throw new InvalidOperationException("Could not find the super user!"); + } - // User email/login has already been modified - if (admin.Email == unattendedEmail) - { - return; - } + // User email/login has already been modified + if (admin.Email == unattendedEmail) + { + return; + } - // Update name, email & login & save user - admin.Name = unattendedName.Trim(); - admin.Email = unattendedEmail.Trim(); - admin.Username = unattendedEmail.Trim(); - _userService.Save(admin); + // Update name, email & login & save user + admin.Name = unattendedName.Trim(); + admin.Email = unattendedEmail.Trim(); + admin.Username = unattendedEmail.Trim(); + _userService.Save(admin); - // Change Password for the default user we ship out of the box - // Uses same approach as NewInstall Step + // Change Password for the default user we ship out of the box + // Uses same approach as NewInstall Step + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IBackOfficeUserManager backOfficeUserManager = scope.ServiceProvider.GetRequiredService(); + BackOfficeIdentityUser membershipUser = await backOfficeUserManager.FindByIdAsync(Core.Constants.Security.SuperUserId.ToString()); + if (membershipUser == null) + { + throw new InvalidOperationException($"No user found in membership provider with id of {Core.Constants.Security.SuperUserId}."); + } - using IServiceScope scope = _serviceScopeFactory.CreateScope(); - IBackOfficeUserManager backOfficeUserManager = scope.ServiceProvider.GetRequiredService(); - BackOfficeIdentityUser membershipUser = await backOfficeUserManager.FindByIdAsync(Core.Constants.Security.SuperUserId.ToString()); - if (membershipUser == null) - { - throw new InvalidOperationException($"No user found in membership provider with id of {Core.Constants.Security.SuperUserId}."); - } + //To change the password here we actually need to reset it since we don't have an old one to use to change + var resetToken = await backOfficeUserManager.GeneratePasswordResetTokenAsync(membershipUser); + if (string.IsNullOrWhiteSpace(resetToken)) + { + throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); + } - //To change the password here we actually need to reset it since we don't have an old one to use to change - var resetToken = await backOfficeUserManager.GeneratePasswordResetTokenAsync(membershipUser); - if (string.IsNullOrWhiteSpace(resetToken)) - { - throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); - } - - IdentityResult resetResult = await backOfficeUserManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, unattendedPassword.Trim()); - if (!resetResult.Succeeded) - { - throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); - } + IdentityResult resetResult = await backOfficeUserManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, unattendedPassword.Trim()); + if (!resetResult.Succeeded) + { + throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); + } } }