Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m22s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m17s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Independent review (cold fork = MERGEABLE-WITH-NITS; Codex = BLOCKED, caught concurrency defects the fork missed). All actionable findings folded in: - HIGH (Codex) atomic first-claim-wins: ClaimLocalAdmin now writes the three credential rows in ONE transaction guarded by the unique ConfigElement.Key index (lost race -> DbUpdateException -> 409), so concurrent claims can't produce a mixed-state credential. - HIGH (Codex) consistent login snapshot: VerifyLocalAdminLogin reads hash+stamp in one query and drops rehash-on-verify, so a login racing a password change can't capture a stamp newer than the hash it verified (concurrent change -> old password fails, or the issued cookie carries the pre-change stamp -> revoked next request). - MEDIUM (Codex) env-seed migration race: LocalAdminSeedService is now a RunOnce BackgroundService that awaits SystemStartup.WaitForDatabase (the migrator is a BackgroundService; registration order didn't guarantee the schema) + try/catch. - MEDIUM (fork M1) ForwardedHeaders: reverted the strict-opt-in flip — it would regress /iptv M3U/XMLTV/HLS absolute-URL generation (Request.Scheme) behind a proxy without KnownProxies. Kept #285 behavior; KnownProxies still recommended. - LOW (Codex/fork) require X-CSRF on /api/auth/logout + /password (the [SkipApiAuthorization] surface isn't covered by the filter's CSRF check; closes forced-logout CSRF). - ChangeLocalAdminPassword also writes hash+stamp atomically. Input length caps on username/password. Deferred with a tracked gate: MEDIUM (Codex) side-effecting [RequiresAuthentication] GETs (troubleshoot playback/archive) aren't CSRF-covered -> #301, gates PR2 (latent in PR1: the SPA still uses the machine key). Verify: full ErsatzTV.Tests green (1501); no OpenAPI/generated drift. Docs updated (api-conventions §9, decisions.md). Refs #295 #301 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
61 lines
2.3 KiB
C#
61 lines
2.3 KiB
C#
using ErsatzTV.Application.Auth;
|
|
using ErsatzTV.Core;
|
|
using LanguageExt;
|
|
using MediatR;
|
|
|
|
namespace ErsatzTV.Services.RunOnce;
|
|
|
|
/// <summary>
|
|
/// Recovery/bootstrap: when <c>Auth:LocalAdmin:Password</c> is configured, (re)seeds the single local
|
|
/// administrator at startup (issue #295). Overwrites any existing credential and rotates the security
|
|
/// stamp, so an operator locked out of the browser UI can reset by setting the env and restarting. A
|
|
/// no-op when unset. Follows the RunOnce pattern (waits for the database to be ready — the migrator is a
|
|
/// BackgroundService, so registration order alone does not guarantee the schema exists).
|
|
/// </summary>
|
|
public class LocalAdminSeedService(
|
|
IServiceScopeFactory serviceScopeFactory,
|
|
IConfiguration configuration,
|
|
SystemStartup systemStartup,
|
|
ILogger<LocalAdminSeedService> logger) : BackgroundService
|
|
{
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
await Task.Yield();
|
|
|
|
string password = configuration["Auth:LocalAdmin:Password"];
|
|
if (string.IsNullOrWhiteSpace(password))
|
|
{
|
|
return;
|
|
}
|
|
|
|
await systemStartup.WaitForDatabase(stoppingToken);
|
|
if (stoppingToken.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string username = configuration["Auth:LocalAdmin:Username"];
|
|
|
|
try
|
|
{
|
|
using IServiceScope scope = serviceScopeFactory.CreateScope();
|
|
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
|
Either<BaseError, Unit> result =
|
|
await mediator.Send(new SeedLocalAdminFromEnvironment(username, password), stoppingToken);
|
|
|
|
result.Match(
|
|
Right: _ => logger.LogWarning(
|
|
"Seeded the local administrator from Auth:LocalAdmin:* configuration (any existing "
|
|
+ "credential was overwritten and all sessions revoked). Unset Auth:LocalAdmin:Password "
|
|
+ "after signing in."),
|
|
Left: error => logger.LogError(
|
|
"Failed to seed the local administrator from configuration: {Error}",
|
|
error.Value));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Failed to seed the local administrator from configuration");
|
|
}
|
|
}
|
|
}
|