Change to saving the admin promote password in redis

For initial creation of an admin account, when no account with the `admin` role
is found on startup, wave creates a password and displays it in the log.

Previously, this value was saved temporarely in a file, /app/admin.txt.
On some deployments e.g. kubernetes this caused crashes, because this location
isnt writable by the "app" user wave is running as.

Now, wave saves this value in redis instead.
This commit is contained in:
4censord 2024-02-17 16:23:12 +01:00
parent 1126ac87c4
commit c5cf0b1362
No known key found for this signature in database
GPG key ID: C8E54F9D13822E41
2 changed files with 23 additions and 9 deletions

View file

@ -5,6 +5,8 @@
@using Wave.Components.Account.Shared @using Wave.Components.Account.Shared
@using Wave.Data @using Wave.Data
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.Extensions.Caching.Distributed;
@using System.Text;
@attribute [Authorize] @attribute [Authorize]
@inject IdentityUserAccessor UserAccessor @inject IdentityUserAccessor UserAccessor
@ -13,6 +15,7 @@
@inject SignInManager<ApplicationUser> SignInManager @inject SignInManager<ApplicationUser> SignInManager
@inject IdentityRedirectManager RedirectManager @inject IdentityRedirectManager RedirectManager
@inject IStringLocalizer<Admin> Localizer @inject IStringLocalizer<Admin> Localizer
@inject IDistributedCache WaveDistributedCache
<PageTitle>@(TitlePrefix + Localizer["Title"])</PageTitle> <PageTitle>@(TitlePrefix + Localizer["Title"])</PageTitle>
@ -52,8 +55,8 @@
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
User = await UserAccessor.GetRequiredUserAsync(HttpContext); User = await UserAccessor.GetRequiredUserAsync(HttpContext);
string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "admin.txt"); Password = await WaveDistributedCache.GetStringAsync("admin_promote_key")
if (File.Exists(path)) Password = await File.ReadAllTextAsync(path); ?? "";
} }
private async Task Promote() { private async Task Promote() {
@ -76,7 +79,7 @@
} }
} }
await UserManager.AddToRoleAsync(User, "Admin"); await UserManager.AddToRoleAsync(User, "Admin");
File.Delete(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "admin.txt")); await WaveDistributedCache.RemoveAsync("admin_promote_key");
await SignInManager.RefreshSignInAsync(User); await SignInManager.RefreshSignInAsync(User);
Message = "You have been promoted, this tool is now disabled."; Message = "You have been promoted, this tool is now disabled.";
} }
@ -84,4 +87,4 @@
private sealed class InputModel { private sealed class InputModel {
public string Password { get; set; } = string.Empty; public string Password { get; set; } = string.Empty;
} }
} }

View file

@ -7,8 +7,10 @@
using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using StackExchange.Redis; using StackExchange.Redis;
using System.Text;
using Tomlyn.Extensions.Configuration; using Tomlyn.Extensions.Configuration;
using Wave.Components; using Wave.Components;
using Wave.Components.Account; using Wave.Components.Account;
@ -79,7 +81,7 @@
.AddPolicy("ArticleDeletePermissions", p => p.RequireRole("Moderator", "Admin")) .AddPolicy("ArticleDeletePermissions", p => p.RequireRole("Moderator", "Admin"))
.AddPolicy("CategoryManagePermissions", p => p.RequireRole("Admin")) .AddPolicy("CategoryManagePermissions", p => p.RequireRole("Admin"))
.AddPolicy("RoleAssignPermissions", p => p.RequireRole("Admin")) .AddPolicy("RoleAssignPermissions", p => p.RequireRole("Admin"))
.AddPolicy("ArticleEditOrReviewPermissions", p => p.RequireRole("Author", "Reviewer", "Admin")); .AddPolicy("ArticleEditOrReviewPermissions", p => p.RequireRole("Author", "Reviewer", "Admin"));
builder.Services.AddAuthentication(options => { builder.Services.AddAuthentication(options => {
options.DefaultScheme = IdentityConstants.ApplicationScheme; options.DefaultScheme = IdentityConstants.ApplicationScheme;
@ -90,7 +92,7 @@
#region Identity #region Identity
string connectionString = builder.Configuration.GetConnectionString("DefaultConnection") string connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContextFactory<ApplicationDbContext>(options => builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString)); options.UseNpgsql(connectionString));
@ -116,7 +118,7 @@
builder.Services.Configure<Features>(builder.Configuration.GetSection(nameof(Features))); builder.Services.Configure<Features>(builder.Configuration.GetSection(nameof(Features)));
builder.Services.Configure<Customization>(builder.Configuration.GetSection(nameof(Customization))); builder.Services.Configure<Customization>(builder.Configuration.GetSection(nameof(Customization)));
builder.Services.AddCascadingValue("TitlePrefix", builder.Services.AddCascadingValue("TitlePrefix",
sf => (sf.GetService<IOptions<Customization>>()?.Value.AppName ?? "Wave") + " - "); sf => (sf.GetService<IOptions<Customization>>()?.Value.AppName ?? "Wave") + " - ");
var smtpConfig = builder.Configuration.GetSection("Email:Smtp"); var smtpConfig = builder.Configuration.GetSection("Email:Smtp");
@ -190,10 +192,19 @@
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>(); var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
if (userManager.GetUsersInRoleAsync("Admin").Result.Any() is false) { if (userManager.GetUsersInRoleAsync("Admin").Result.Any() is false) {
string admin = Guid.NewGuid().ToString("N")[..16]; IDistributedCache cache = app.Services.GetRequiredService<IDistributedCache>();
// Check first wheter the password exists already
string admin = await cache.GetStringAsync("admin_promote_key");
// If it does not exist, create a new one and save it to redis
if (string.IsNullOrWhiteSpace(admin)){
admin = Guid.NewGuid().ToString("N")[..16];
await cache.SetAsync("admin_promote_key", Encoding.UTF8.GetBytes(admin), new DistributedCacheEntryOptions{});
}
app.Logger.LogWarning("There is currently no user in your installation with the admin role, " + app.Logger.LogWarning("There is currently no user in your installation with the admin role, " +
"go to /Admin and use the following password to self promote your account: {admin}", admin); "go to /Admin and use the following password to self promote your account: {admin}", admin);
File.WriteAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "admin.txt"), admin);
} }
} }