Skip to content

Commit

Permalink
Refactor to allow testing + add sample test.
Browse files Browse the repository at this point in the history
  • Loading branch information
grilledham committed Aug 22, 2020
1 parent c76db92 commit 5ab2b29
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 60 deletions.
2 changes: 2 additions & 0 deletions FactorioWebInterface/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public static class Constants
public const string PlaguesScriptDefaultPathKey = "PlaguesScriptDefaultPath";

public const string DefaultAdminAccount = "DefaultAdminAccount";
public const string DefaultAdminName = "Admin";
public const string DefaultAdminFile = "DefaultUser.txt";

public const string DiscordBotCommandPrefix = ";;";

Expand Down
2 changes: 1 addition & 1 deletion FactorioWebInterface/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ private static void SeedData(IHost host)
roleManager.CreateAsync(new IdentityRole(Constants.RootRole));
roleManager.CreateAsync(new IdentityRole(Constants.AdminRole));

_ = services.GetService<DefaultAdminAccountService>().SetupDefaultUserAsync();
_ = services.GetService<IDefaultAdminAccountService>().SetupDefaultUserAsync();
}
}
}
Expand Down
55 changes: 33 additions & 22 deletions FactorioWebInterface/Services/DefaultAdminAccountService.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
using FactorioWebInterface.Data;
using Microsoft.AspNetCore.Identity;
using Serilog;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Threading.Tasks;

namespace FactorioWebInterface.Services
{
public class DefaultAdminAccountService
public interface IDefaultAdminAccountService
{
Task SetupDefaultUserAsync();
}

public class DefaultAdminAccountService : IDefaultAdminAccountService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly string userName = "Admin";
private readonly ILogger<IDefaultAdminAccountService> _logger;
private readonly IFileSystem _fileSystem;

public DefaultAdminAccountService(UserManager<ApplicationUser> userManager)
public DefaultAdminAccountService(UserManager<ApplicationUser> userManager, ILogger<IDefaultAdminAccountService> logger, IFileSystem fileSystem)
{
_userManager = userManager;
_logger = logger;
_fileSystem = fileSystem;
}

private enum AccountsNumbers
Expand All @@ -36,7 +45,7 @@ public async Task SetupDefaultUserAsync()
{
case AccountsNumbers.DefaultIsOnlyRootAccount:
case AccountsNumbers.OnlyDefaultAccount:
Log.Information("{UserId} could not be created, another account already exists", Constants.DefaultAdminAccount);
_logger.LogInformation("{UserId} could not be created, another account already exists", Constants.DefaultAdminAccount);
return;
case AccountsNumbers.MultipleAccounts:
await ValidateOrClearDefaultUserAsync(id, true);
Expand Down Expand Up @@ -80,7 +89,7 @@ private async Task ValidateOrClearDefaultUserAsync(string id, bool force = false
ApplicationUser userResult = await _userManager.FindByIdAsync(id);
if (await ValidateDefaultUserAsync(userResult) && !force)
{
Log.Information("Valid {UserId} already exists", Constants.DefaultAdminAccount);
_logger.LogInformation("Valid {UserId} already exists", Constants.DefaultAdminAccount);
return;
}

Expand All @@ -89,16 +98,16 @@ private async Task ValidateOrClearDefaultUserAsync(string id, bool force = false
var deleteResult = await _userManager.DeleteAsync(userResult);
if (!deleteResult.Succeeded)
{
Log.Error("{UserId} couldn't be deleted! This may pose a security risk for your application. Will attempt to delete again at next reboot", Constants.DefaultAdminAccount);
_logger.LogError("{UserId} couldn't be deleted! This may pose a security risk for your application. Will attempt to delete again at next reboot", Constants.DefaultAdminAccount);
}
Log.Information("{UserId} deleted", Constants.DefaultAdminAccount);
_logger.LogInformation("{UserId} deleted", Constants.DefaultAdminAccount);
DeleteDefaultAccountFile();
}
}

private async Task<bool> ValidateDefaultUserAsync(ApplicationUser user)
{
if (user == null || user.UserName != userName)
if (user == null || user.UserName != Constants.DefaultAdminName)
{
return false;
}
Expand All @@ -121,62 +130,64 @@ private async Task CreateDefaultUserAsync(string id)
ApplicationUser user = new ApplicationUser()
{
Id = id,
UserName = userName
UserName = Constants.DefaultAdminName
};

var result = await _userManager.CreateAsync(user);
if (!result.Succeeded)
{
Log.Error("Couldn't create {UserId}", Constants.DefaultAdminAccount);
_logger.LogError("Couldn't create {UserId}", Constants.DefaultAdminAccount);
return;
}

result = await _userManager.AddToRoleAsync(user, Constants.RootRole);
if (!result.Succeeded)
{
Log.Error("Couldn't add role to {UserId}", Constants.DefaultAdminAccount);
_logger.LogError("Couldn't add role to {UserId}", Constants.DefaultAdminAccount);
return;
}

result = await _userManager.AddToRoleAsync(user, Constants.AdminRole);
if (!result.Succeeded)
{
Log.Error("Couldn't add role to {UserId}", Constants.DefaultAdminAccount);
_logger.LogError("Couldn't add role to {UserId}", Constants.DefaultAdminAccount);
return;
}

string password = Guid.NewGuid().ToString();
result = await _userManager.AddPasswordAsync(user, password);
if (!result.Succeeded)
{
Log.Error("Couldn't add password to {UserId} ", Constants.DefaultAdminAccount);
_logger.LogError("Couldn't add password to {UserId} ", Constants.DefaultAdminAccount);
return;
}
Log.Warning("{UserId} created. This action potential exposes your interface, creating a new account and restarting this web interface will disable the default admin account", Constants.DefaultAdminAccount);
_logger.LogWarning("{UserId} created. This action potential exposes your interface, creating a new account and restarting this web interface will disable the default admin account", Constants.DefaultAdminAccount);

string warningTag = "! - Warning - !";
var path = GetDefaultAccountFilePath();
DeleteDefaultAccountFile();
File.WriteAllText(path, $"{warningTag}\nThis account is unsecure. Please setup a personal account.\n{warningTag}\nUsername: {userName}\nPassword: {password}");
_fileSystem.File.WriteAllText(path, $"{warningTag}\nThis account is unsecure. Please setup a personal account.\n{warningTag}\nUsername: {Constants.DefaultAdminName}\nPassword: {password}");
}

private void DeleteDefaultAccountFile()
{
var path = GetDefaultAccountFilePath();
try
{
File.Delete(path);
} catch {
Log.Information("Couldn't delete DefaultAccount file");
_fileSystem.File.Delete(path);
}
catch
{
_logger.LogInformation("Couldn't delete DefaultAccount file");
return;
}
Log.Information("DefaultAccount file deleted");
_logger.LogInformation("DefaultAccount file deleted");
}

private string GetDefaultAccountFilePath()
private static string GetDefaultAccountFilePath()
{
string path = AppDomain.CurrentDomain.BaseDirectory!;
path = Path.Combine(path, "logs/DefaultUser.txt");
path = Path.Combine(path, Constants.DefaultAdminFile);
return path;
}
}
Expand Down
79 changes: 42 additions & 37 deletions FactorioWebInterface/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,42 +50,7 @@ public void ConfigureServices(IServiceCollection services)

services.AddSingleton<IDbContextFactory, DbContextFactory>();

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();

services.Configure<IdentityOptions>(options =>
{
// Password settings.
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
// Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
StringBuilder sb = new StringBuilder();
for (int i = 32; i < ushort.MaxValue; i++)
{
sb.Append((char)i);
}
string set = sb.ToString();
// User settings.
options.User.AllowedUserNameCharacters = set;
//"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+()[]{}"; // Todo Find out all allowed characters for discord username.
options.User.RequireUniqueEmail = false;
});

services.Configure<SecurityStampValidatorOptions>(options =>
{
// enables immediate logout, after updating the user's stat.
options.ValidationInterval = TimeSpan.Zero;
});
SetupIdentity(services);

//services.ConfigureApplicationCookie(options => options.LoginPath = "");

Expand Down Expand Up @@ -114,7 +79,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton<BanHubEventHandlerService, BanHubEventHandlerService>();
services.AddSingleton<FactorioAdminServiceEventHandlerService, FactorioAdminServiceEventHandlerService>();
services.AddSingleton<IFactorioModPortalService, FactorioModPortalService>();
services.AddScoped<DefaultAdminAccountService, DefaultAdminAccountService>();
services.AddTransient<IDefaultAdminAccountService, DefaultAdminAccountService>();

services.AddRouting(o => o.LowercaseUrls = true);

Expand Down Expand Up @@ -234,6 +199,46 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
});
}

internal static void SetupIdentity(IServiceCollection services)
{
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();

services.Configure<IdentityOptions>(options =>
{
// Password settings.
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
// Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
StringBuilder sb = new StringBuilder();
for (int i = 32; i < ushort.MaxValue; i++)
{
sb.Append((char)i);
}
string set = sb.ToString();
// User settings.
options.User.AllowedUserNameCharacters = set;
//"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+()[]{}"; // Todo Find out all allowed characters for discord username.
options.User.RequireUniqueEmail = false;
});

services.Configure<SecurityStampValidatorOptions>(options =>
{
// enables immediate logout, after updating the user's stat.
options.ValidationInterval = TimeSpan.Zero;
});
}

private string GenerateToken()
{
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, Constants.FactorioWrapperClaim) };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using FactorioWebInterface;
using FactorioWebInterface.Data;
using FactorioWebInterface.Services;
using FactorioWebInterfaceTests.Utils;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;

namespace FactorioWebInterfaceTests.Services.DefaultAdminAccountServiceTests
{
public static class DefaultAdminAccountServiceHelper
{
public static readonly string directoryPath = AppDomain.CurrentDomain.BaseDirectory!;
public static readonly string filePath = Path.Combine(directoryPath, Constants.DefaultAdminFile);

public static ServiceProvider MakeDefaultAdminAccountServiceProvider()
{
var dbContextFactory = new TestDbContextFactory();

var serviceCollection = new ServiceCollection();

serviceCollection
.AddSingleton(typeof(ILogger<>), typeof(TestLogger<>))
.AddSingleton<IFileSystem, MockFileSystem>()
.AddTransient<ApplicationDbContext>(_ => dbContextFactory.Create<ApplicationDbContext>())
.AddTransient<DefaultAdminAccountService>();
Startup.SetupIdentity(serviceCollection);

return serviceCollection.BuildServiceProvider();
}

public static void SetupRoles(RoleManager<IdentityRole> roleManager)
{
roleManager.CreateAsync(new IdentityRole(Constants.RootRole));
roleManager.CreateAsync(new IdentityRole(Constants.AdminRole));
}

public static void SetupFileSystem(MockFileSystem fileSystem)
{
fileSystem.Directory.CreateDirectory(directoryPath);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using FactorioWebInterface;
using FactorioWebInterface.Data;
using FactorioWebInterface.Services;
using FactorioWebInterfaceTests.Utils;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace FactorioWebInterfaceTests.Services.DefaultAdminAccountServiceTests
{
public class SetupDefaultUserAsync : IDisposable
{
private readonly ServiceProvider serviceProvider;
private readonly TestLogger<IDefaultAdminAccountService> logger;
private readonly MockFileSystem fileSystem;
private readonly RoleManager<IdentityRole> roleManager;
private readonly DefaultAdminAccountService defaultAdminAccountService;

public SetupDefaultUserAsync()
{
serviceProvider = DefaultAdminAccountServiceHelper.MakeDefaultAdminAccountServiceProvider();
logger = (TestLogger<IDefaultAdminAccountService>)serviceProvider.GetRequiredService<ILogger<IDefaultAdminAccountService>>();
fileSystem = (MockFileSystem)serviceProvider.GetRequiredService<IFileSystem>();
roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();
defaultAdminAccountService = serviceProvider.GetRequiredService<DefaultAdminAccountService>();
}

public void Dispose()
{
serviceProvider.Dispose();
}

[Fact]
public async Task CreatedSuccessfully()
{
// Arrange.
DefaultAdminAccountServiceHelper.SetupRoles(roleManager);
DefaultAdminAccountServiceHelper.SetupFileSystem(fileSystem);

// Act.
await defaultAdminAccountService.SetupDefaultUserAsync();

// Assert.
var db = serviceProvider.GetRequiredService<ApplicationDbContext>();
var user = db.Users.Single();

Assert.Equal(Constants.DefaultAdminAccount, user.Id);
Assert.Equal(Constants.DefaultAdminName, user.UserName);
Assert.NotEmpty(user.PasswordHash);

string fileContent = fileSystem.File.ReadAllText(DefaultAdminAccountServiceHelper.filePath);
Assert.Contains("This account is unsecure. Please setup a personal account", fileContent);
Assert.Contains($"Username: {Constants.DefaultAdminName}", fileContent);
Assert.Contains("Password:", fileContent);

logger.AssertContainsLog(LogLevel.Warning, $"{Constants.DefaultAdminAccount} created. This action potential exposes your interface, creating a new account and restarting this web interface will disable the default admin account");
}
}
}

0 comments on commit 5ab2b29

Please sign in to comment.