Refuse production startup when the Heimdall app secret is blank
A missing secret silently switched Discord and Patreon member sign-in off. Outside Development and Testing the validator now fails naming Heimdall:AppSecretFile and Heimdall:AppSecret, never a value. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
920ecb1fbe
commit
1739d5552a
3 changed files with 42 additions and 2 deletions
|
|
@ -8,7 +8,8 @@
|
|||
- File set but missing, unreadable, a directory, or empty: startup fails in every environment. The message names
|
||||
`Heimdall:AppSecretFile` and the path, never the secret.
|
||||
- Only `Heimdall__AppSecret` set: still works; startup logs a deprecation warning (without the value).
|
||||
- Neither set: Discord and Patreon sign-in stay off, as before.
|
||||
- Neither set: startup fails outside Development and Testing, naming `Heimdall:AppSecretFile` and `Heimdall:AppSecret`
|
||||
(`StartupConfigurationValidator`). Development and Testing still start, with Discord and Patreon sign-in off.
|
||||
|
||||
Under systemd, pass the file with `LoadCredential=heimdall-app-secret:<root-owned file>` and
|
||||
`Environment=Heimdall__AppSecretFile=%d/heimdall-app-secret`.
|
||||
|
|
|
|||
|
|
@ -105,6 +105,13 @@ public sealed class StartupConfigurationValidator(
|
|||
failures.Add("Heimdall:PatronSupportIntakeSecret is required when patron support intake is enabled.");
|
||||
}
|
||||
|
||||
// The options are already resolved, so a secret from Heimdall:AppSecretFile or Heimdall:AppSecret lands in AppSecret.
|
||||
// Blank would silently switch Discord and Patreon member sign-in off.
|
||||
if (string.IsNullOrWhiteSpace(heimdallOptions.Value.AppSecret))
|
||||
{
|
||||
failures.Add("Heimdall:AppSecretFile (or Heimdall:AppSecret) must supply the Heimdall app secret outside development.");
|
||||
}
|
||||
|
||||
if (stripeOptions.Value.EnableCheckout)
|
||||
{
|
||||
if (!stripeOptions.Value.IsCheckoutConfigured)
|
||||
|
|
|
|||
|
|
@ -145,6 +145,36 @@ public sealed class HostTrustTests : IClassFixture<TestWebApplicationFactory>
|
|||
Assert.DoesNotContain(canary, refusal.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Production_without_a_heimdall_app_secret_refuses_startup_naming_the_options(string blank)
|
||||
{
|
||||
var refusal = Assert.Throws<InvalidOperationException>(
|
||||
() => Validate(keysPath: "/keys", appSecret: blank));
|
||||
|
||||
Assert.Contains("Heimdall:AppSecretFile", refusal.Message);
|
||||
Assert.Contains("Heimdall:AppSecret", refusal.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Production_with_a_heimdall_app_secret_passes_and_the_refusal_never_echoes_it()
|
||||
{
|
||||
Validate(keysPath: "/keys", appSecret: ValidAppSecret);
|
||||
|
||||
var refusal = Assert.Throws<InvalidOperationException>(
|
||||
() => Validate(keysPath: null, appSecret: ValidAppSecret));
|
||||
Assert.DoesNotContain(ValidAppSecret, refusal.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Development")]
|
||||
[InlineData("Testing")]
|
||||
public void Development_and_Testing_start_without_a_heimdall_app_secret(string environment)
|
||||
{
|
||||
Validate(keysPath: null, environment: environment, appSecret: "");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Production_without_network_tag_secret_fails_and_never_echoes_values()
|
||||
{
|
||||
|
|
@ -315,12 +345,14 @@ public sealed class HostTrustTests : IClassFixture<TestWebApplicationFactory>
|
|||
}
|
||||
|
||||
private const string ValidTagSecret = "tag-secret-0123456789-0123456789-abcdef";
|
||||
private const string ValidAppSecret = "app-secret-5c0e91d4";
|
||||
|
||||
private static void Validate(
|
||||
string? keysPath,
|
||||
string environment = "Production",
|
||||
string? connectionString = "Host=db",
|
||||
string? tagSecret = ValidTagSecret,
|
||||
string appSecret = ValidAppSecret,
|
||||
int ipv4Length = 32,
|
||||
int ipv6Length = 56,
|
||||
string[]? allowedOrigins = null,
|
||||
|
|
@ -344,7 +376,7 @@ public sealed class HostTrustTests : IClassFixture<TestWebApplicationFactory>
|
|||
hostEnvironment,
|
||||
Options.Create(new GitHubOAuthOptions { ClientId = "id", ClientSecret = "secret" }),
|
||||
Options.Create(new GitHubAppOptions { EnableWebhookSync = false }),
|
||||
Options.Create(new HeimdallOptions { EnablePatronSupportIntake = false }),
|
||||
Options.Create(new HeimdallOptions { EnablePatronSupportIntake = false, AppSecret = appSecret }),
|
||||
Options.Create(new StripeOptions()),
|
||||
Options.Create(new BifrostHostOptions
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue