feat: add guided unified installer #58
6 changed files with 343 additions and 11 deletions
7
.github/workflows/windows-build.yml
vendored
7
.github/workflows/windows-build.yml
vendored
|
|
@ -36,7 +36,12 @@ jobs:
|
||||||
run: ./tests/windows/Test-DesktopContracts.ps1
|
run: ./tests/windows/Test-DesktopContracts.ps1
|
||||||
|
|
||||||
- name: Test Desktop security
|
- name: Test Desktop security
|
||||||
run: dotnet test tests/ServerMonitorManager.Desktop.Security.Tests/ServerMonitorManager.Desktop.Security.Tests.csproj --configuration Release -p:RestoreLockedMode=true
|
run: dotnet test tests/ServerMonitorManager.Desktop.Security.Tests/ServerMonitorManager.Desktop.Security.Tests.csproj --configuration Release -p:RestoreLockedMode=true --filter Category!=LiveRelease
|
||||||
|
|
||||||
|
- name: Test Desktop security (live release)
|
||||||
|
env:
|
||||||
|
SMM_TEST_RELEASE_TAG: v0.1.0-alpha.18
|
||||||
|
run: dotnet test tests/ServerMonitorManager.Desktop.Security.Tests/ServerMonitorManager.Desktop.Security.Tests.csproj --configuration Release -p:RestoreLockedMode=true --filter Category=LiveRelease
|
||||||
|
|
||||||
- name: Build test-signed MSIX installer
|
- name: Build test-signed MSIX installer
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
|
||||||
|
|
@ -574,6 +574,7 @@ prepare_control_state() {
|
||||||
chown -R "$CONTROL_USER:$CONTROL_USER" "$STATE_DIR/control"
|
chown -R "$CONTROL_USER:$CONTROL_USER" "$STATE_DIR/control"
|
||||||
find "$STATE_DIR/control" -type d -exec chmod 0700 {} +
|
find "$STATE_DIR/control" -type d -exec chmod 0700 {} +
|
||||||
find "$STATE_DIR/control" -type f -exec chmod 0600 {} +
|
find "$STATE_DIR/control" -type f -exec chmod 0600 {} +
|
||||||
|
repair_mesh_state_permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
reverse_control_state_migration() {
|
reverse_control_state_migration() {
|
||||||
|
|
@ -682,6 +683,17 @@ read_mesh_value() {
|
||||||
awk -F '=' -v key="$key" '$1 == key { print substr($0, index($0, "=") + 1); exit }' "$ETC_DIR/mesh.env"
|
awk -F '=' -v key="$key" '$1 == key { print substr($0, index($0, "=") + 1); exit }' "$ETC_DIR/mesh.env"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensure_mesh_state() {
|
||||||
|
install -d -m 0770 -o root -g "$CONTROL_USER" "$MESH_DIR"
|
||||||
|
touch "$MESH_DIR/nodes.tsv"
|
||||||
|
chown root:"$CONTROL_USER" "$MESH_DIR/nodes.tsv"
|
||||||
|
chmod 0660 "$MESH_DIR/nodes.tsv"
|
||||||
|
}
|
||||||
|
|
||||||
|
repair_mesh_state_permissions() {
|
||||||
|
ensure_mesh_state
|
||||||
|
}
|
||||||
|
|
||||||
render_hub_wireguard_config() {
|
render_hub_wireguard_config() {
|
||||||
local private_key endpoint port node_id address public_key status
|
local private_key endpoint port node_id address public_key status
|
||||||
private_key="$(cat "$WG_DIR/hub.key")"
|
private_key="$(cat "$WG_DIR/hub.key")"
|
||||||
|
|
@ -719,8 +731,10 @@ mesh_init() {
|
||||||
fi
|
fi
|
||||||
[[ "$public_endpoint" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$ ]] \
|
[[ "$public_endpoint" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$ ]] \
|
||||||
|| fail "Invalid WireGuard public endpoint."
|
|| fail "Invalid WireGuard public endpoint."
|
||||||
|
ensure_system_user "$CONTROL_USER"
|
||||||
ensure_mesh_packages
|
ensure_mesh_packages
|
||||||
install -d -m 0700 "$WG_DIR" "$MESH_DIR" /etc/wireguard
|
install -d -m 0700 -o root -g root "$WG_DIR" /etc/wireguard
|
||||||
|
ensure_mesh_state
|
||||||
if [[ ! -f "$WG_DIR/hub.key" ]]; then
|
if [[ ! -f "$WG_DIR/hub.key" ]]; then
|
||||||
umask 077
|
umask 077
|
||||||
wg genkey >"$WG_DIR/hub.key"
|
wg genkey >"$WG_DIR/hub.key"
|
||||||
|
|
@ -736,8 +750,6 @@ HUB_PUBLIC_KEY=$hub_public
|
||||||
MESH_NETWORK=$MESH_NETWORK
|
MESH_NETWORK=$MESH_NETWORK
|
||||||
EOF
|
EOF
|
||||||
chmod 0644 "$ETC_DIR/mesh.env"
|
chmod 0644 "$ETC_DIR/mesh.env"
|
||||||
touch "$MESH_DIR/nodes.tsv"
|
|
||||||
chmod 0600 "$MESH_DIR/nodes.tsv"
|
|
||||||
printf '%s\n' 'net.ipv4.ip_forward=1' >"/etc/sysctl.d/90-ochenstarik-smm-mesh.conf"
|
printf '%s\n' 'net.ipv4.ip_forward=1' >"/etc/sysctl.d/90-ochenstarik-smm-mesh.conf"
|
||||||
sysctl --system >/dev/null
|
sysctl --system >/dev/null
|
||||||
write_mesh_firewall
|
write_mesh_firewall
|
||||||
|
|
@ -761,9 +773,7 @@ EOF
|
||||||
|
|
||||||
reserve_node_address() {
|
reserve_node_address() {
|
||||||
local node_id="$1" existing host address
|
local node_id="$1" existing host address
|
||||||
install -d -m 0700 "$MESH_DIR"
|
ensure_mesh_state
|
||||||
touch "$MESH_DIR/nodes.tsv"
|
|
||||||
chmod 0600 "$MESH_DIR/nodes.tsv"
|
|
||||||
existing="$(awk -F '\t' -v node="$node_id" '$1 == node { print $2; exit }' "$MESH_DIR/nodes.tsv")"
|
existing="$(awk -F '\t' -v node="$node_id" '$1 == node { print $2; exit }' "$MESH_DIR/nodes.tsv")"
|
||||||
if [[ -n "$existing" ]]; then
|
if [[ -n "$existing" ]]; then
|
||||||
printf '%s\n' "$existing"
|
printf '%s\n' "$existing"
|
||||||
|
|
@ -1359,7 +1369,8 @@ add_mesh_peer() {
|
||||||
awk -F '\t' -v OFS='\t' -v node="$node_id" -v address="$address" -v key="$public_key" \
|
awk -F '\t' -v OFS='\t' -v node="$node_id" -v address="$address" -v key="$public_key" \
|
||||||
'$1 == node { print node, address, key, "active"; found=1; next } { print } END { if (!found) exit 1 }' \
|
'$1 == node { print node, address, key, "active"; found=1; next } { print } END { if (!found) exit 1 }' \
|
||||||
"$MESH_DIR/nodes.tsv" >"$tmp" || { rm -f -- "$tmp"; fail "Peer reservation is missing."; }
|
"$MESH_DIR/nodes.tsv" >"$tmp" || { rm -f -- "$tmp"; fail "Peer reservation is missing."; }
|
||||||
chmod 0600 "$tmp"
|
chown root:"$CONTROL_USER" "$tmp"
|
||||||
|
chmod 0660 "$tmp"
|
||||||
mv -- "$tmp" "$MESH_DIR/nodes.tsv"
|
mv -- "$tmp" "$MESH_DIR/nodes.tsv"
|
||||||
render_hub_wireguard_config
|
render_hub_wireguard_config
|
||||||
systemctl restart wg-quick@smm0.service
|
systemctl restart wg-quick@smm0.service
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ LockPersonality=true
|
||||||
RestrictSUIDSGID=true
|
RestrictSUIDSGID=true
|
||||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||||
UMask=0077
|
UMask=0077
|
||||||
ReadWritePaths=/var/lib/ochenstarik-server-monitor-manager/control
|
ReadWritePaths=/var/lib/ochenstarik-server-monitor-manager/control /var/lib/ochenstarik-server-monitor-manager/mesh
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ServerMonitorManager_Desktop;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ServerMonitorManager.Desktop.Security.Tests;
|
||||||
|
|
||||||
|
[Trait("Category", "LiveRelease")]
|
||||||
|
public sealed class LiveReleaseUpdateVerificationTests : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private const string DefaultReleaseTag = "v0.1.0-alpha.14";
|
||||||
|
private const string Repository = "ochenstarik-ui/server-monitor-manager";
|
||||||
|
private readonly string _tempDir;
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
private readonly string _tag;
|
||||||
|
|
||||||
|
public LiveReleaseUpdateVerificationTests()
|
||||||
|
{
|
||||||
|
_tempDir = Path.Combine(Path.GetTempPath(), $"smm-live-release-tests-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(_tempDir);
|
||||||
|
_http = new HttpClient();
|
||||||
|
_http.DefaultRequestHeaders.Add("User-Agent", "ServerMonitorManager.Desktop.Tests");
|
||||||
|
_tag = Environment.GetEnvironmentVariable("SMM_TEST_RELEASE_TAG") ?? DefaultReleaseTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(string ManifestPath, string SigPath, string PemPath)> DownloadReleaseArtifactsAsync()
|
||||||
|
{
|
||||||
|
var manifestUrl = $"https://github.com/{Repository}/releases/download/{_tag}/server-monitor-manager-manifest.json";
|
||||||
|
var sigUrl = $"https://github.com/{Repository}/releases/download/{_tag}/server-monitor-manager-manifest.sig";
|
||||||
|
var pemUrl = $"https://github.com/{Repository}/releases/download/{_tag}/server-monitor-manager-manifest.pem";
|
||||||
|
|
||||||
|
var manifestPath = Path.Combine(_tempDir, "server-monitor-manager-manifest.json");
|
||||||
|
var sigPath = Path.Combine(_tempDir, "server-monitor-manager-manifest.sig");
|
||||||
|
var pemPath = Path.Combine(_tempDir, "server-monitor-manager-manifest.pem");
|
||||||
|
|
||||||
|
var manifestBytes = await _http.GetByteArrayAsync(manifestUrl, TestContext.Current.CancellationToken);
|
||||||
|
var sigBytes = await _http.GetByteArrayAsync(sigUrl, TestContext.Current.CancellationToken);
|
||||||
|
var pemBytes = await _http.GetByteArrayAsync(pemUrl, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
await File.WriteAllBytesAsync(manifestPath, manifestBytes, TestContext.Current.CancellationToken);
|
||||||
|
await File.WriteAllBytesAsync(sigPath, sigBytes, TestContext.Current.CancellationToken);
|
||||||
|
await File.WriteAllBytesAsync(pemPath, pemBytes, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
return (manifestPath, sigPath, pemPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AcceptanceRealReleaseManifestAndSignatureAreAccepted()
|
||||||
|
{
|
||||||
|
var (manifestPath, sigPath, pemPath) = await DownloadReleaseArtifactsAsync();
|
||||||
|
|
||||||
|
var fileStorage = new TestDirectoryFileStorage(_tempDir);
|
||||||
|
var httpTransport = new DefaultHttpTransport();
|
||||||
|
var verifier = new ProcessSignatureVerifier(fileStorage, httpTransport);
|
||||||
|
|
||||||
|
// 1. ProcessSignatureVerifier directly verifies real release material
|
||||||
|
await verifier.VerifySignatureAsync(sigPath, manifestPath, pemPath, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
// 2. Parse and verify manifest content
|
||||||
|
var manifestJson = await File.ReadAllTextAsync(manifestPath, TestContext.Current.CancellationToken);
|
||||||
|
var node = JsonNode.Parse(manifestJson);
|
||||||
|
Assert.NotNull(node);
|
||||||
|
Assert.Equal(_tag, node["version"]?.GetValue<string>());
|
||||||
|
|
||||||
|
var msixHash = node["hashes"]?["ServerMonitorManager-win-x64.msix"]?.GetValue<string>();
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(msixHash));
|
||||||
|
Assert.Equal(64, msixHash.Length);
|
||||||
|
|
||||||
|
// 3. UpdateService end-to-end against real release material
|
||||||
|
var mockHttp = new MockHttpTransport
|
||||||
|
{
|
||||||
|
GetStringAsyncFunc = url =>
|
||||||
|
{
|
||||||
|
if (url.EndsWith("releases/latest", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return Task.FromResult($@"{{
|
||||||
|
""tag_name"": ""{_tag}"",
|
||||||
|
""assets"": [
|
||||||
|
{{ ""name"": ""server-monitor-manager-manifest.json"", ""browser_download_url"": ""https://github.com/{Repository}/releases/download/{_tag}/server-monitor-manager-manifest.json"" }},
|
||||||
|
{{ ""name"": ""server-monitor-manager-manifest.sig"", ""browser_download_url"": ""https://github.com/{Repository}/releases/download/{_tag}/server-monitor-manager-manifest.sig"" }},
|
||||||
|
{{ ""name"": ""server-monitor-manager-manifest.pem"", ""browser_download_url"": ""https://github.com/{Repository}/releases/download/{_tag}/server-monitor-manager-manifest.pem"" }},
|
||||||
|
{{ ""name"": ""ServerMonitorManager-win-x64.msix"", ""browser_download_url"": ""https://github.com/{Repository}/releases/download/{_tag}/ServerMonitorManager-win-x64.msix"" }}
|
||||||
|
]
|
||||||
|
}}");
|
||||||
|
}
|
||||||
|
if (url.EndsWith("server-monitor-manager-manifest.json", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return File.ReadAllTextAsync(manifestPath, TestContext.Current.CancellationToken);
|
||||||
|
}
|
||||||
|
if (url.EndsWith("server-monitor-manager-manifest.sig", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return File.ReadAllTextAsync(sigPath, TestContext.Current.CancellationToken);
|
||||||
|
}
|
||||||
|
if (url.EndsWith("server-monitor-manager-manifest.pem", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return File.ReadAllTextAsync(pemPath, TestContext.Current.CancellationToken);
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException($"Unexpected URL: {url}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = new UpdateService(mockHttp, verifier, fileStorage);
|
||||||
|
var updateInfo = await service.CheckForUpdatesAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.NotNull(updateInfo);
|
||||||
|
Assert.Equal(_tag, updateInfo.Version);
|
||||||
|
Assert.Equal(msixHash, updateInfo.ExpectedHash);
|
||||||
|
Assert.Contains(_tag, updateInfo.DownloadUrl, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Negative1TamperedHashInRealManifestIsRejected()
|
||||||
|
{
|
||||||
|
var (manifestPath, sigPath, pemPath) = await DownloadReleaseArtifactsAsync();
|
||||||
|
|
||||||
|
// Alter manifest content by tampering with the hash
|
||||||
|
var originalManifest = await File.ReadAllTextAsync(manifestPath, TestContext.Current.CancellationToken);
|
||||||
|
var node = JsonNode.Parse(originalManifest);
|
||||||
|
Assert.NotNull(node);
|
||||||
|
if (node["hashes"]?["ServerMonitorManager-win-x64.msix"] is not null)
|
||||||
|
{
|
||||||
|
node["hashes"]!["ServerMonitorManager-win-x64.msix"] = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
node["hashes"] = new JsonObject
|
||||||
|
{
|
||||||
|
["ServerMonitorManager-win-x64.msix"] = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var tamperedManifest = node.ToJsonString();
|
||||||
|
|
||||||
|
var tamperedManifestPath = Path.Combine(_tempDir, "tampered-manifest.json");
|
||||||
|
await File.WriteAllTextAsync(tamperedManifestPath, tamperedManifest, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var fileStorage = new TestDirectoryFileStorage(_tempDir);
|
||||||
|
var httpTransport = new DefaultHttpTransport();
|
||||||
|
var verifier = new ProcessSignatureVerifier(fileStorage, httpTransport);
|
||||||
|
|
||||||
|
// Verification of tampered manifest with real signature must fail
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
|
verifier.VerifySignatureAsync(sigPath, tamperedManifestPath, pemPath, TestContext.Current.CancellationToken));
|
||||||
|
Assert.Contains("Signature verification failed", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Negative2RealManifestWithSignatureFromDifferentIdentityIsRejected()
|
||||||
|
{
|
||||||
|
var (manifestPath, _, pemPath) = await DownloadReleaseArtifactsAsync();
|
||||||
|
|
||||||
|
// Create a fake signature signed by a different key
|
||||||
|
using var rsa = RSA.Create();
|
||||||
|
var fakeSigBytes = rsa.SignData(
|
||||||
|
await File.ReadAllBytesAsync(manifestPath, TestContext.Current.CancellationToken),
|
||||||
|
HashAlgorithmName.SHA256,
|
||||||
|
RSASignaturePadding.Pkcs1);
|
||||||
|
var fakeSigPath = Path.Combine(_tempDir, "fake-identity.sig");
|
||||||
|
await File.WriteAllTextAsync(fakeSigPath, Convert.ToBase64String(fakeSigBytes), TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var fileStorage = new TestDirectoryFileStorage(_tempDir);
|
||||||
|
var httpTransport = new DefaultHttpTransport();
|
||||||
|
var verifier = new ProcessSignatureVerifier(fileStorage, httpTransport);
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
|
verifier.VerifySignatureAsync(fakeSigPath, manifestPath, pemPath, TestContext.Current.CancellationToken));
|
||||||
|
Assert.Contains("Signature verification failed", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Negative3MissingCertificateIsRejected()
|
||||||
|
{
|
||||||
|
var (manifestPath, sigPath, _) = await DownloadReleaseArtifactsAsync();
|
||||||
|
var nonExistentPemPath = Path.Combine(_tempDir, "non-existent-certificate.pem");
|
||||||
|
|
||||||
|
var fileStorage = new TestDirectoryFileStorage(_tempDir);
|
||||||
|
var httpTransport = new DefaultHttpTransport();
|
||||||
|
var verifier = new ProcessSignatureVerifier(fileStorage, httpTransport);
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
|
verifier.VerifySignatureAsync(sigPath, manifestPath, nonExistentPemPath, TestContext.Current.CancellationToken));
|
||||||
|
Assert.Contains("Signature verification failed", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Negative4CertificateFromDifferentWorkflowIsRejected()
|
||||||
|
{
|
||||||
|
var (manifestPath, sigPath, _) = await DownloadReleaseArtifactsAsync();
|
||||||
|
|
||||||
|
// Create a custom self-signed certificate with a different subject/workflow identity
|
||||||
|
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||||
|
var req = new CertificateRequest(
|
||||||
|
"CN=Untrusted Workflow Fake Cert",
|
||||||
|
ecdsa,
|
||||||
|
HashAlgorithmName.SHA256);
|
||||||
|
using var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1));
|
||||||
|
var fakePemPath = Path.Combine(_tempDir, "fake-workflow-cert.pem");
|
||||||
|
await File.WriteAllTextAsync(fakePemPath, cert.ExportCertificatePem(), TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var fileStorage = new TestDirectoryFileStorage(_tempDir);
|
||||||
|
var httpTransport = new DefaultHttpTransport();
|
||||||
|
var verifier = new ProcessSignatureVerifier(fileStorage, httpTransport);
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
|
verifier.VerifySignatureAsync(sigPath, manifestPath, fakePemPath, TestContext.Current.CancellationToken));
|
||||||
|
Assert.Contains("Signature verification failed", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
_http.Dispose();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_tempDir))
|
||||||
|
{
|
||||||
|
Directory.Delete(_tempDir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Best effort cleanup
|
||||||
|
}
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestDirectoryFileStorage(string directory) : IFileStorage
|
||||||
|
{
|
||||||
|
public string GetTempFolder() => directory;
|
||||||
|
public bool FileExists(string path) => File.Exists(path);
|
||||||
|
public Task WriteAllBytesAsync(string path, byte[] bytes, System.Threading.CancellationToken cancellationToken = default) =>
|
||||||
|
File.WriteAllBytesAsync(path, bytes, cancellationToken);
|
||||||
|
public Task WriteAllTextAsync(string path, string text, System.Threading.CancellationToken cancellationToken = default) =>
|
||||||
|
File.WriteAllTextAsync(path, text, cancellationToken);
|
||||||
|
public Stream OpenRead(string path) => File.OpenRead(path);
|
||||||
|
public void LaunchFile(string path) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -245,6 +245,7 @@ printf '%s' backup >"$control_state_fixture/backups/manifest.json"
|
||||||
command install "${arguments[@]}"
|
command install "${arguments[@]}"
|
||||||
}
|
}
|
||||||
chown() { :; }
|
chown() { :; }
|
||||||
|
repair_mesh_state_permissions() { :; }
|
||||||
source <(printf '%s\n%s\n' "$validate_control_state_migration_definition" "$prepare_control_state_definition")
|
source <(printf '%s\n%s\n' "$validate_control_state_migration_definition" "$prepare_control_state_definition")
|
||||||
validate_control_state_migration
|
validate_control_state_migration
|
||||||
prepare_control_state
|
prepare_control_state
|
||||||
|
|
@ -300,6 +301,7 @@ cp "$alpha7_fixture/etc/control.env" "$alpha7_fixture/original.env"
|
||||||
command install "${arguments[@]}"
|
command install "${arguments[@]}"
|
||||||
}
|
}
|
||||||
chown() { :; }
|
chown() { :; }
|
||||||
|
repair_mesh_state_permissions() { :; }
|
||||||
source <(printf '%s\n%s\n%s\n%s\n' \
|
source <(printf '%s\n%s\n%s\n%s\n' \
|
||||||
"$validate_control_state_migration_definition" \
|
"$validate_control_state_migration_definition" \
|
||||||
"$prepare_control_state_definition" \
|
"$prepare_control_state_definition" \
|
||||||
|
|
@ -381,6 +383,7 @@ tar -C "$archive_root" -czf "$recovery_fixture/bootstrap-backups/alpha7.tar.gz"
|
||||||
command install "${arguments[@]}"
|
command install "${arguments[@]}"
|
||||||
}
|
}
|
||||||
chown() { :; }
|
chown() { :; }
|
||||||
|
repair_mesh_state_permissions() { :; }
|
||||||
source <(printf '%s\n%s\n%s\n' \
|
source <(printf '%s\n%s\n%s\n' \
|
||||||
"$record_control_legacy_state_definition" \
|
"$record_control_legacy_state_definition" \
|
||||||
"$prepare_control_state_definition" \
|
"$prepare_control_state_definition" \
|
||||||
|
|
@ -537,7 +540,18 @@ rm -rf "$role_fixture/lib/agent"
|
||||||
rm -rf -- "$role_fixture"
|
rm -rf -- "$role_fixture"
|
||||||
|
|
||||||
grep -Fq 'UMask=0077' "$root/deploy/ochenstarik-smm-control.service"
|
grep -Fq 'UMask=0077' "$root/deploy/ochenstarik-smm-control.service"
|
||||||
grep -Fq 'ReadWritePaths=/var/lib/ochenstarik-server-monitor-manager/control' "$root/deploy/ochenstarik-smm-control.service"
|
grep -Fq 'ReadWritePaths=/var/lib/ochenstarik-server-monitor-manager/control /var/lib/ochenstarik-server-monitor-manager/mesh' "$root/deploy/ochenstarik-smm-control.service"
|
||||||
|
grep -Fq 'install -d -m 0770 -o root -g "$CONTROL_USER" "$MESH_DIR"' "$bootstrap"
|
||||||
|
grep -Fq 'chown root:"$CONTROL_USER" "$MESH_DIR/nodes.tsv"' "$bootstrap"
|
||||||
|
grep -Fq 'chmod 0660 "$MESH_DIR/nodes.tsv"' "$bootstrap"
|
||||||
|
grep -Fq 'install -d -m 0700 -o root -g root "$WG_DIR" /etc/wireguard' "$bootstrap"
|
||||||
|
mesh_init_definition="$(extract_bootstrap_function mesh_init)"
|
||||||
|
grep -Fq ' ensure_system_user "$CONTROL_USER"' <<<"$mesh_init_definition"
|
||||||
|
[[ "$(grep -Fc ' ensure_mesh_state' "$bootstrap")" -eq 3 ]]
|
||||||
|
grep -Fq ' repair_mesh_state_permissions' <<<"$prepare_control_state_definition"
|
||||||
|
if [[ "$(uname -s)" != MINGW* ]] && command -v sudo >/dev/null 2>&1; then
|
||||||
|
bash "$root/tests/bootstrap/test-mesh-state-permissions.sh"
|
||||||
|
fi
|
||||||
native_smoke="$root/tests/bootstrap/run-native-systemd-smoke.sh"
|
native_smoke="$root/tests/bootstrap/run-native-systemd-smoke.sh"
|
||||||
grep -Fq 'node_code="$(sudo "$system_bootstrap" node-code smoke-node)"' "$native_smoke"
|
grep -Fq 'node_code="$(sudo "$system_bootstrap" node-code smoke-node)"' "$native_smoke"
|
||||||
grep -Fq 'export SMM_ENROLL_CODE="$node_code"' "$native_smoke"
|
grep -Fq 'export SMM_ENROLL_CODE="$node_code"' "$native_smoke"
|
||||||
|
|
@ -880,4 +894,3 @@ grep -Fq 'record_installed_version agent' "$bootstrap" || {
|
||||||
}
|
}
|
||||||
|
|
||||||
printf '%s\n' "BOOTSTRAP_CONTRACT=PASS"
|
printf '%s\n' "BOOTSTRAP_CONTRACT=PASS"
|
||||||
|
|
||||||
|
|
|
||||||
61
tests/bootstrap/test-mesh-state-permissions.sh
Normal file
61
tests/bootstrap/test-mesh-state-permissions.sh
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
|
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
bootstrap="$root/deploy/ochenstarik-server-monitor-manager.sh"
|
||||||
|
fixture="$(mktemp -d -t smm-mesh-permissions.XXXXXXXX)"
|
||||||
|
test_user="$(id -un)"
|
||||||
|
test_group="$(id -gn)"
|
||||||
|
mesh_dir="$fixture/state/mesh"
|
||||||
|
wg_dir="$fixture/etc/wireguard"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
sudo rm -rf -- "$fixture"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
extract_function() {
|
||||||
|
local name="$1"
|
||||||
|
awk -v name="$name" '
|
||||||
|
$0 == name "() {" { capture=1 }
|
||||||
|
capture && $0 != name "() {" && /^[A-Za-z_][A-Za-z0-9_]*\(\) \{$/ { exit }
|
||||||
|
capture { print }
|
||||||
|
' "$bootstrap"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_definition="$(extract_function ensure_mesh_state)"
|
||||||
|
repair_definition="$(extract_function repair_mesh_state_permissions)"
|
||||||
|
runner="$fixture/apply-permissions.sh"
|
||||||
|
{
|
||||||
|
printf '%s\n%s\n' "$ensure_definition" "$repair_definition"
|
||||||
|
printf '%s\n' 'ensure_mesh_state'
|
||||||
|
} >"$runner"
|
||||||
|
|
||||||
|
sudo env MESH_DIR="$mesh_dir" CONTROL_USER="$test_group" bash "$runner"
|
||||||
|
[[ "$(sudo stat -c '%a:%U:%G' "$mesh_dir")" == "770:root:$test_group" ]]
|
||||||
|
[[ "$(sudo stat -c '%a:%U:%G' "$mesh_dir/nodes.tsv")" == "660:root:$test_group" ]]
|
||||||
|
|
||||||
|
printf '%s\n' $'fixture-node\t10.77.0.2\t-\treserved' >>"$mesh_dir/nodes.tsv"
|
||||||
|
grep -Fq 'fixture-node' "$mesh_dir/nodes.tsv"
|
||||||
|
|
||||||
|
sudo install -d -m 0700 -o root -g root "$wg_dir"
|
||||||
|
printf '%s\n' 'private-hub-key' | sudo tee "$wg_dir/hub.key" >/dev/null
|
||||||
|
sudo chown root:root "$wg_dir/hub.key"
|
||||||
|
sudo chmod 0600 "$wg_dir/hub.key"
|
||||||
|
if sudo -u "$test_user" test -r "$wg_dir/hub.key"; then
|
||||||
|
printf '%s\n' 'Control-equivalent user can read the Hub private key' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo chmod 0700 "$mesh_dir"
|
||||||
|
sudo chmod 0600 "$mesh_dir/nodes.tsv"
|
||||||
|
{
|
||||||
|
printf '%s\n%s\n' "$ensure_definition" "$repair_definition"
|
||||||
|
printf '%s\n' 'repair_mesh_state_permissions'
|
||||||
|
} >"$runner"
|
||||||
|
sudo env MESH_DIR="$mesh_dir" CONTROL_USER="$test_group" bash "$runner"
|
||||||
|
[[ "$(sudo stat -c '%a:%U:%G' "$mesh_dir")" == "770:root:$test_group" ]]
|
||||||
|
[[ "$(sudo stat -c '%a:%U:%G' "$mesh_dir/nodes.tsv")" == "660:root:$test_group" ]]
|
||||||
|
|
||||||
|
printf '%s\n' 'MESH_STATE_PERMISSIONS=PASS'
|
||||||
Loading…
Reference in a new issue