using System.Net; using System.Security.Cryptography; using System.Text; using System.Text.Json.Nodes; using Fleet.Identity; using Microsoft.Extensions.DependencyInjection; using MoShiro.Backend.Application; namespace MoShiro.Backend.Tests; /// /// 每个 Agent 和 Probe 都必须有属主(passkey):属主是节点敏感内容(extension 会话、加密数据)绑定的人,装上时固定在节点本地。 /// 没有信任根、没选属主、选了操作员集合之外的凭证,都不能创建加入码;安装脚本和容器入口缺了属主也不装。 /// public sealed class OwnerPolicyTests { private static OperatorSet Set(params string[] ids) => new(1, OperatorTrust.Genesis, "example.test", ["https://example.test"], [.. ids.Select(id => new OperatorKey(id, [1, 2, 3]))]); [Fact] public void WithoutATrustRoot_AnOwnerCannotBeChosen_WhenOwnersAreRequired() { Assert.False(OwnerPolicy.Resolve(required: true, null, null).IsSuccess); Assert.False(OwnerPolicy.Resolve(required: true, null, ["a"]).IsSuccess); Assert.Empty(OwnerPolicy.Resolve(required: false, null, null).Value); // 没有信任根的老部署:不固定属主 } [Fact] public void ByDefaultEveryOperatorIsAnOwner_AndAnExplicitChoiceMustBeNonEmptyAndKnown() { OperatorSet set = Set("a", "b"); Assert.Equal(["a", "b"], OwnerPolicy.Resolve(true, set, null).Value); Assert.Equal(["b"], OwnerPolicy.Resolve(true, set, ["b", "b"]).Value); Assert.False(OwnerPolicy.Resolve(true, set, []).IsSuccess); // 一把都不选:不行 Assert.False(OwnerPolicy.Resolve(true, set, ["a", "stranger"]).IsSuccess); // 不是操作员集合里的凭证 Assert.False(OwnerPolicy.Resolve(false, set, ["stranger"]).IsSuccess); } } [Collection("probe-e2e")] public sealed class OwnerEndToEndTests : IDisposable { private readonly BackendFixture _backend; private readonly BackendOptions _options; private readonly bool _before; public OwnerEndToEndTests(BackendFixture backend) { _backend = backend; _options = backend.App.Services.GetRequiredService(); _before = _options.RequireOwner; _options.RequireOwner = true; } public void Dispose() => _options.RequireOwner = _before; [Fact] public async Task ANodeCannotBeAddedWithoutAnOwner() { Operator op = await SharedOperator.GetAsync(_backend); // 共享的操作员:不要释放它 HttpClient session = op.Session; string known = op.Passkey.Id; using HttpResponseMessage none = await session.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "agent", ["controllers"] = new JsonArray() })); Assert.Equal(HttpStatusCode.BadRequest, none.StatusCode); Assert.Contains("owner", await none.Content.ReadAsStringAsync(), StringComparison.Ordinal); using HttpResponseMessage stranger = await session.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "probe", ["controllers"] = new JsonArray("not-an-operator") })); Assert.Equal(HttpStatusCode.BadRequest, stranger.StatusCode); // 明确选了一把,或者不指定(默认是全部操作员):可以,命令里带着创世指纹和属主 using HttpResponseMessage chosen = await session.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "agent", ["controllers"] = new JsonArray(known) })); Assert.Equal(HttpStatusCode.Created, chosen.StatusCode); string linux = JsonNode.Parse(await chosen.Content.ReadAsStringAsync())!["install_commands"]!["linux"]!.GetValue(); Assert.Contains("MOSHIRO_GENESIS=", linux, StringComparison.Ordinal); Assert.Contains("MOSHIRO_CONTROLLERS=", linux, StringComparison.Ordinal); Assert.Contains(known, linux, StringComparison.Ordinal); using HttpResponseMessage defaults = await session.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "probe" })); Assert.Equal(HttpStatusCode.Created, defaults.StatusCode); } [Fact] public async Task TheInstallScripts_RefuseANodeWithoutAnOwner() { // 脚本是静态的,所以可以直接检查:缺了信任根指纹或属主就不装,只有明确设置 MOSHIRO_ALLOW_NO_OWNER=1(没有信任根的老面板)才放行 using HttpClient anonymous = _backend.NewClient(); foreach (string path in new[] { "/install", "/install.ps1" }) { string script = await anonymous.GetStringAsync(path); Assert.Contains("MOSHIRO_ALLOW_NO_OWNER", script, StringComparison.Ordinal); Assert.Contains("this node has no owner", script, StringComparison.Ordinal); // 只按容器判断「已经装过」:podman 的 inspect 不分类型,本机残留的旧镜像会被误当成已安装的容器 Assert.Contains("container inspect", script, StringComparison.Ordinal); Assert.DoesNotContain("inspect \"$container\"", script.Replace("container inspect", ""), StringComparison.Ordinal); } } }