Automated install user with Environment Variables & unattended.user.json (#9930)

* Try to update admin user unattended

This will fail because we're not in install runtime state

* Create a new user instead of trying to update the default admin

* Create a new user instead of trying to update the default admin

* Use same logic from NewInstallStep to modify the SuperUser aka -1

* Add back stuff after merge conflict from v8/dev

* Add event to be raised

* Trying to wire up events

* Remove commented out code - just need to figure out why event is not hit/triggered

* Read Appsettings as opposed to ENV variables

* Use a JSON file that deletes itself as storing secrets in web.config will be accidently committed

* Remove component based event - Component were only initialized after DB creation

* Move UnattendedInstall down after _factory

* Remove commented out code

* Fixed issue where upgrader UI would show up - needed to recheck the Runtimelevel after UnattenedInstall

* Apply suggestions from code review - Thanks Marc :)

Co-authored-by: Marc Goodson <marc@moriyama.co.uk>

Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
Co-authored-by: Marc Goodson <marc@moriyama.co.uk>
This commit is contained in:
Warren Buckley
2021-06-16 10:00:29 +01:00
committed by GitHub
parent 348d1676cd
commit dc334c1015
3 changed files with 141 additions and 6 deletions

View File

@@ -0,0 +1,9 @@
namespace Umbraco.Core.Events
{
/// <summary>
/// Used to notify that an Unattended install has completed
/// </summary>
public class UnattendedInstallEventArgs : System.ComponentModel.CancelEventArgs
{
}
}

View File

@@ -1,13 +1,17 @@
using System;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Threading;
using System.Web;
using System.Web.Hosting;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Events;
using Umbraco.Core.Exceptions;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
@@ -16,6 +20,9 @@ using Umbraco.Core.Migrations.Install;
using Umbraco.Core.Migrations.Upgrade;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Mappers;
using Umbraco.Core.Scoping;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Core.Sync;
namespace Umbraco.Core.Runtime
@@ -119,6 +126,9 @@ namespace Umbraco.Core.Runtime
try
{
// Setup event listener
UnattendedInstalled += CoreRuntime_UnattendedInstalled;
// throws if not full-trust
new AspNetHostingPermission(AspNetHostingPermissionLevel.Unrestricted).Demand();
@@ -162,8 +172,7 @@ namespace Umbraco.Core.Runtime
// run handlers
RuntimeOptions.DoRuntimeEssentials(composition, appCaches, typeLoader, databaseFactory);
// determines if unattended install is enabled and performs it if required
DoUnattendedInstall(databaseFactory);
// register runtime-level services
// there should be none, really - this is here "just in case"
@@ -190,6 +199,13 @@ namespace Umbraco.Core.Runtime
// create the factory
_factory = Current.Factory = composition.CreateFactory();
// determines if unattended install is enabled and performs it if required
DoUnattendedInstall(databaseFactory);
// determine our runtime level (AFTER UNATTENDED INSTALL)
// TODO: Feels kinda weird to call this again
DetermineRuntimeLevel(databaseFactory, ProfilingLogger);
// if level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade
if (_state.Reason == RuntimeLevelReason.UpgradeMigrations && _state.Level == RuntimeLevel.Run)
{
@@ -203,8 +219,6 @@ namespace Umbraco.Core.Runtime
// create & initialize the components
_components = _factory.GetInstance<ComponentCollection>();
_components.Initialize();
}
catch (Exception e)
{
@@ -242,6 +256,93 @@ namespace Umbraco.Core.Runtime
return _factory;
}
private void CoreRuntime_UnattendedInstalled(IRuntime sender, UnattendedInstallEventArgs e)
{
var unattendedName = Environment.GetEnvironmentVariable("UnattendedUserName");
var unattendedEmail = Environment.GetEnvironmentVariable("UnattendedUserEmail");
var unattendedPassword = Environment.GetEnvironmentVariable("UnattendedUserPassword");
var fileExists = false;
var filePath = IOHelper.MapPath("~/App_Data/unattended.user.json");
// No values store in ENV vars - try fallback file of /app_data/unattended.user.json
if (unattendedName.IsNullOrWhiteSpace()
|| unattendedEmail.IsNullOrWhiteSpace()
|| unattendedPassword.IsNullOrWhiteSpace())
{
fileExists = File.Exists(filePath);
if (fileExists == false)
{
return;
}
// Attempt to deserialize JSON
try
{
var fileContents = File.ReadAllText(filePath);
var credentials = JsonConvert.DeserializeObject<UnattendedUserConfig>(fileContents);
unattendedName = credentials.Name;
unattendedEmail = credentials.Email;
unattendedPassword = credentials.Password;
}
catch (Exception ex)
{
throw;
}
}
// ENV Variables & JSON still empty
if (unattendedName.IsNullOrWhiteSpace()
|| unattendedEmail.IsNullOrWhiteSpace()
|| unattendedPassword.IsNullOrWhiteSpace())
{
return;
}
// Update user details
var currentProvider = MembershipProviderExtensions.GetUsersMembershipProvider();
var admin = Current.Services.UserService.GetUserById(Constants.Security.SuperUserId);
if (admin == null)
{
throw new InvalidOperationException("Could not find the super user!");
}
var membershipUser = currentProvider.GetUser(Constants.Security.SuperUserId, true);
if (membershipUser == null)
{
throw new InvalidOperationException($"No user found in membership provider with id of {Constants.Security.SuperUserId}.");
}
try
{
var success = membershipUser.ChangePassword("default", unattendedPassword.Trim());
if (success == false)
{
throw new FormatException("Password must be at least " + currentProvider.MinRequiredPasswordLength + " characters long and contain at least " + currentProvider.MinRequiredNonAlphanumericCharacters + " symbols");
}
}
catch (Exception)
{
throw new FormatException("Password must be at least " + currentProvider.MinRequiredPasswordLength + " characters long and contain at least " + currentProvider.MinRequiredNonAlphanumericCharacters + " symbols");
}
admin.Email = unattendedEmail.Trim();
admin.Name = unattendedName.Trim();
admin.Username = unattendedEmail.Trim();
Current.Services.UserService.Save(admin);
// Delete JSON file if it existed to tidy
if (fileExists)
{
File.Delete(filePath);
}
}
private void DoUnattendedInstall(IUmbracoDatabaseFactory databaseFactory)
{
// unattended install is not enabled
@@ -285,6 +386,11 @@ namespace Umbraco.Core.Runtime
var creator = new DatabaseSchemaCreator(database, Logger);
creator.InitializeDatabaseSchema();
database.CompleteTransaction();
// Emit an event that unattended install completed
// Then this event can be listened for and create an unattended user
UnattendedInstalled?.Invoke(this, new UnattendedInstallEventArgs());
Logger.Info<CoreRuntime>("Unattended install completed.");
}
catch (Exception ex)
@@ -397,6 +503,7 @@ namespace Umbraco.Core.Runtime
public virtual void Terminate()
{
_components?.Terminate();
UnattendedInstalled -= CoreRuntime_UnattendedInstalled;
}
/// <summary>
@@ -404,7 +511,7 @@ namespace Umbraco.Core.Runtime
/// </summary>
public virtual void Compose(Composition composition)
{
// nothing
// Nothing
}
#region Getters
@@ -465,5 +572,23 @@ namespace Umbraco.Core.Runtime
}
#endregion
/// <summary>
/// Event to be used to notify when the Unattended Install has finished
/// </summary>
public static event TypedEventHandler<IRuntime, UnattendedInstallEventArgs> UnattendedInstalled;
[DataContract]
public class UnattendedUserConfig
{
[DataMember(Name = "name")]
public string Name { get; set; }
[DataMember(Name = "email")]
public string Email { get; set; }
[DataMember(Name = "password")]
public string Password { get; set; }
}
}
}

View File

@@ -131,6 +131,7 @@
<Compile Include="Constants-CharArrays.cs" />
<Compile Include="Collections\EventClearingObservableCollection.cs" />
<Compile Include="Constants-SqlTemplates.cs" />
<Compile Include="Events\UnattendedInstallEventArgs.cs" />
<Compile Include="Logging\ILogger2.cs" />
<Compile Include="Logging\Logger2Extensions.cs" />
<Compile Include="Dashboards\ContentDashboardSettings.cs" />