Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DefaultAdminAccountOption added #67

Merged
merged 18 commits into from
Aug 23, 2020
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions FactorioWebInterface/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public static class Constants
public const string GitHubCallbackFilePathKey = "WebHookGitHubCallbackFile";
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 = ";;";

public const string TempSavesDirectoryName = "saves";
Expand Down
14 changes: 7 additions & 7 deletions FactorioWebInterface/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,11 @@ public static async Task Main(string[] args)
Log.Information("Starting factorio web interface");

var host = CreateWebHostBuilder(args).Build();

// This makes sure the databases are setup.
SeedData(host);

var services = host.Services;

await Task.WhenAll(
// This makes sure the databases are setup.
SeedData(host),
services.GetService<IFactorioServerDataService>().Init(),
services.GetService<DiscordBot>().Init(),
services.GetService<IDiscordService>().Init());
Expand Down Expand Up @@ -79,7 +77,7 @@ public static IHostBuilder CreateWebHostBuilder(string[] args) =>
.UseSerilog();
});

private static void SeedData(IHost host)
private static async Task SeedData(IHost host)
{
using (var scope = host.Services.CreateScope())
{
Expand All @@ -93,8 +91,10 @@ private static void SeedData(IHost host)

var roleManager = services.GetService<RoleManager<IdentityRole>>();

roleManager.CreateAsync(new IdentityRole(Constants.RootRole));
roleManager.CreateAsync(new IdentityRole(Constants.AdminRole));
await roleManager.CreateAsync(new IdentityRole(Constants.RootRole));
await roleManager.CreateAsync(new IdentityRole(Constants.AdminRole));

await services.GetService<IDefaultAdminAccountService>().SetupDefaultUserAsync();
}
}
}
Expand Down
93 changes: 93 additions & 0 deletions FactorioWebInterface/Services/DefaultAdminAccountService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using FactorioWebInterface.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.IO.Abstractions;
using System.Linq;
using System.Threading.Tasks;

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

public class DefaultAdminAccountService : IDefaultAdminAccountService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<IDefaultAdminAccountService> _logger;
private readonly IFileSystem _fileSystem;

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

public async Task SetupDefaultUserAsync()
{
if (await NoUsers())
{
await CreateDefaultUserAsync();
}
}

private async Task<bool> NoUsers()
{
int count = await _userManager.Users.CountAsync();
return count == 0;
}

private async Task CreateDefaultUserAsync()
{
ApplicationUser user = new ApplicationUser()
{
Id = Constants.DefaultAdminAccount,
UserName = Constants.DefaultAdminName
};

var result = await _userManager.CreateAsync(user);
if (!result.Succeeded)
{
_logger.LogError("Could not create {UserId}", Constants.DefaultAdminAccount);
return;
}

result = await _userManager.AddToRoleAsync(user, Constants.RootRole);
if (!result.Succeeded)
{
_logger.LogError("Could not add role to {UserId}", Constants.DefaultAdminAccount);
return;
}

result = await _userManager.AddToRoleAsync(user, Constants.AdminRole);
if (!result.Succeeded)
{
_logger.LogError("Could not add role to {UserId}", Constants.DefaultAdminAccount);
return;
}

string password = Guid.NewGuid().ToString();
result = await _userManager.AddPasswordAsync(user, password);
if (!result.Succeeded)
{
_logger.LogError("Could not add password to {UserId} ", Constants.DefaultAdminAccount);
return;
}
_logger.LogWarning("{UserId} created, see {passwordFile} for password. It is recommended to change the {UserId} password on the user's account page", Constants.DefaultAdminAccount, Constants.DefaultAdminFile, Constants.DefaultAdminAccount);

const string warningTag = "! - Warning - !";
string path = GetDefaultAccountFilePath();
_fileSystem.File.WriteAllText(path, $"{warningTag}\nIt is recommended to change the {Constants.DefaultAdminName} password on the user's account page and to delete this file.\n{warningTag}\nUsername: {Constants.DefaultAdminName}\nPassword: {password}");
}

private string GetDefaultAccountFilePath()
{
string path = AppDomain.CurrentDomain.BaseDirectory!;
return _fileSystem.Path.Combine(path, Constants.DefaultAdminFile);
}
}
}
78 changes: 42 additions & 36 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,6 +79,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton<BanHubEventHandlerService, BanHubEventHandlerService>();
services.AddSingleton<FactorioAdminServiceEventHandlerService, FactorioAdminServiceEventHandlerService>();
services.AddSingleton<IFactorioModPortalService, FactorioModPortalService>();
services.AddTransient<IDefaultAdminAccountService, DefaultAdminAccountService>();

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

Expand Down Expand Up @@ -233,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,48 @@
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>()
.AddTransient<UserManager<ApplicationUser>>();
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,86 @@
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 UserManager<ApplicationUser> userManager;
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>>();
userManager = serviceProvider.GetService<UserManager<ApplicationUser>>();
defaultAdminAccountService = serviceProvider.GetRequiredService<DefaultAdminAccountService>();
DefaultAdminAccountServiceHelper.SetupRoles(roleManager);
}

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

[Fact]
public async Task CreatedSuccessfully_WhenNoUsers()
{
// Arrange.
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($"It is recommended to change the {Constants.DefaultAdminName} password on the user's account page and to delete this file.", fileContent);
Assert.Contains($"Username: {Constants.DefaultAdminName}", fileContent);
Assert.Contains("Password:", fileContent);

logger.AssertContainsLog(LogLevel.Warning, $"{Constants.DefaultAdminAccount} created, see {Constants.DefaultAdminFile} for password. It is recommended to change the {Constants.DefaultAdminAccount} password on the user's account page");
}

[Fact]
public async Task NotCreated_WhenUsers()
{
// Arrange.
DefaultAdminAccountServiceHelper.SetupFileSystem(fileSystem);
await userManager.CreateAsync(new ApplicationUser() { Id = "Test", UserName = "Test" });

// Act.
await defaultAdminAccountService.SetupDefaultUserAsync();

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

Assert.Equal("Test", user.Id);
Assert.Equal("Test", user.UserName);
Assert.False(fileSystem.File.Exists(DefaultAdminAccountServiceHelper.filePath));
}
}
}