Files
schmidt.florian 1f575c9da2
ci / build-test (push) Successful in 1m14s
Update: drop the API-key requirement from the update trigger
Owner's call: MeterVault__AllowInAppUpdate is now the whole gate. One click on
the banner, no key, no prompt, and the REST endpoint no longer asks for one
either.

What that means, recorded so it is not rediscovered later: with the flag on,
anything that can reach MeterVault can trigger a rebuild and restart. On the
realistic threat model that is a repeatable denial of service — minutes of
downtime and a pegged CPU per request — rather than code injection, because the
build comes from the owner's own repository. It becomes remote code execution
if that repository is ever compromised. The flag still defaults off, and that
default is now the only thing between an upgrade and an open trigger, so
UpdateRunnerTests pins it along with the fact that configuring API keys does not
imply consent to rebuild the host.

Kept one guard, which is not authentication: the REST endpoint requires an
X-MeterVault-Update header. Without it any website could POST to the endpoint
through the browser of someone on the network — a plain HTML form is enough,
and no key means nothing else would stop it. A form cannot set a custom header
and a cross-origin fetch that tries is stopped by a preflight nothing here
answers, so this costs a deliberate caller one flag and costs the button
nothing, since it runs over the Blazor circuit rather than HTTP.

The confirmation dialog stays, now purely as a guard against a stray click
costing several minutes of downtime. Every triggered update is logged as a
warning: with no key there is no caller to attribute it to, and the restart
discards anything held in memory.

Claude-Session: https://claude.ai/code/session_01V6joyergfvVLFEizH1hJLd
2026-07-18 20:39:12 +02:00

67 lines
2.7 KiB
C#

using MeterVault.Infrastructure.Options;
using MeterVault.Infrastructure.Update;
using Microsoft.Extensions.Logging.Abstractions;
namespace MeterVault.Integration.Tests;
/// <summary>
/// The gate in front of the in-app update. <c>AllowInAppUpdate</c> is the whole gate by explicit
/// operator choice — no key — so the flag defaulting off is the only thing standing between an
/// upgrade and a network-triggerable rebuild-and-restart as root.
/// </summary>
/// <remarks>
/// None of these launch anything: they assert the conditions that must hold <em>before</em> a launch
/// is even attempted, which is the part worth pinning. The launch itself needs systemd and a real
/// <c>/usr/bin/update</c>.
/// </remarks>
public sealed class UpdateRunnerTests
{
[Fact]
public void Disabled_by_default()
{
// The single most important assertion in this file: with no key required, an operator who
// upgrades without reading the notes must not silently acquire an open trigger.
Assert.False(new MeterVaultOptions().AllowInAppUpdate);
Assert.Equal(UpdateAvailability.NotEnabled, NewRunner(new MeterVaultOptions()).Availability);
}
[Fact]
public void Configuring_api_keys_alone_does_not_enable_it()
{
// The two settings are independent: having an API key for the REST API is not consent to
// rebuild the host.
var options = new MeterVaultOptions();
options.ApiKeys.Add("some-key");
Assert.Equal(UpdateAvailability.NotEnabled, NewRunner(options).Availability);
}
[Fact]
public async Task Refuses_to_launch_when_not_allowed_even_if_asked_directly()
{
// Defence in depth: LaunchAsync re-checks rather than trusting its caller to have done so.
var launch = await NewRunner(new MeterVaultOptions()).LaunchAsync();
Assert.False(launch.Started);
Assert.Contains("NotEnabled", launch.Message, StringComparison.Ordinal);
}
[Fact]
public void Reports_unsupported_where_there_is_no_in_place_updater()
{
// On a dev box or in a container there is no /usr/bin/update, so an enabled runner still
// declines rather than half-running something.
if (UpdateRunner.IsSupportedHere)
{
return; // running on a provisioned LXC; the negative case cannot be observed here
}
Assert.Equal(UpdateAvailability.NotSupportedHere, NewRunner(Enabled()).Availability);
}
private static MeterVaultOptions Enabled() => new() { AllowInAppUpdate = true };
private static UpdateRunner NewRunner(MeterVaultOptions options) =>
new(Microsoft.Extensions.Options.Options.Create(options), NullLogger<UpdateRunner>.Instance);
}