feat: the node itself requires an owner passkey (shared by agent and probe) and reports it; the panel derives the account and passkey it is bound to [CI]

This commit is contained in:
justme committed 2026-10-08 06:15:07 +08:00
1 parent 8c37db1577
commit 2ae63653c4
23 files changed
+328 -10

No files matched your search

@@ -27,10 +27,12 @@ public sealed partial class AgentService
private readonly TrustService _trust;
private readonly BinaryBindingService _bindings;
private readonly NodeOwnerService _owners;
public AgentService(BinaryBindingService bindings, IAgentStore agents, AgentRuntime runtime, IAgentGateway gateway, TaskPlanner planner, ISettingsStore settings, NodeInfoService nodes, LinkService links, TrustService trust)
public AgentService(BinaryBindingService bindings, NodeOwnerService owners, IAgentStore agents, AgentRuntime runtime, IAgentGateway gateway, TaskPlanner planner, ISettingsStore settings, NodeInfoService nodes, LinkService links, TrustService trust)
{
_bindings = bindings;
_owners = owners;
_trust = trust;
_links = links;
_nodes = nodes;
@@ -151,6 +153,7 @@ public sealed partial class AgentService
await _agents.DeleteAsync(id, ct);
await _nodes.ForgetAsync(id.Value, ct);
await _bindings.ForgetAsync(id.Value, ct);
_owners.Forget(id.Value);
await _links.ForgetNodeAsync(id.Value, ct);
_runtime.Remove(id);
await AfterAgentSetChangedAsync(ct);
@@ -69,9 +69,11 @@ public sealed class MoShiroNodeEventSink : INodeEventSink
private readonly ReleaseService _releases;
private readonly ScheduleService _schedule;
private readonly IProbeStore _probes;
private readonly NodeOwnerService _owners;
public MoShiroNodeEventSink(IAgentStore agents, AgentRuntime runtime, IEventStore events, LiveHub live, AgentService service, NodeInfoService nodes, ReleaseService releases, ScheduleService schedule, IProbeStore probes)
public MoShiroNodeEventSink(IAgentStore agents, AgentRuntime runtime, IEventStore events, LiveHub live, AgentService service, NodeInfoService nodes, ReleaseService releases, ScheduleService schedule, IProbeStore probes, NodeOwnerService owners)
{
_owners = owners;
_releases = releases;
_schedule = schedule;
_probes = probes;
@@ -148,6 +150,7 @@ public sealed class MoShiroNodeEventSink : INodeEventSink
{
ProbeHelloMeta? meta = raw.Deserialize(MoShiroJsonContext.Default.ProbeHelloMeta);
await _nodes.SetPlatformAsync(id.Value, NodeKinds.Probe, meta?.Platform, ct);
_owners.Report(id.Value, meta?.Owners);
if (meta?.Ipv6 is { } ipv6)
{
await _probes.SetIpv6DiagnosticsAsync(id.Value, JsonSerializer.Serialize(ipv6, MoShiroJsonContext.Default.Ipv6Diagnostic), ct);
@@ -229,6 +232,7 @@ public sealed class MoShiroNodeEventSink : INodeEventSink
}
}
_owners.Report(id.Value, meta.Owners);
AgentRecord? before = await _agents.GetAsync(id, ct);
string? limitsJson = meta.LocalLimits is null ? null : JsonSerializer.Serialize(meta.LocalLimits, LocalLimitsJson.Default.LocalLimits);
string? ipv6Json = meta.Ipv6 is null ? null : JsonSerializer.Serialize(meta.Ipv6, MoShiroJsonContext.Default.Ipv6Diagnostic);
@@ -0,0 +1,68 @@
using System.Collections.Concurrent;
namespace MoShiro.Backend.Application;
/// <summary>节点的一个属主:它报上来的 passkey 凭证 id,以及面板上对应的 passkey 名称和账户(对不上任何已注册的 passkey 时为空)。</summary>
public sealed record NodeOwner(string CredentialId, string? PasskeyName, string? Username);
/// <summary>
/// 节点的属主:Agent 和 Probe 握手时报上来的、安装时固定在节点本地的 passkey 凭证 id。
/// <b>属主的强制在节点本地</b>(没有属主的节点不运行,见 <c>OwnerGuard</c>),面板这边只是顺着凭证 id 反查出它绑定在哪个账户的哪把 passkey 上——
/// 以后给节点更多的接口和操作(extension 会话、加密内容)时,据此判断谁有资格用。上报的只是自述,真正的授权由节点自己对 passkey 签名的校验把关。
/// 在内存里:节点每次连上来都会重新报。
/// </summary>
public sealed class NodeOwnerService
{
private const int MaxOwners = 16;
private const int MaxCredentialIdLength = 256;
private readonly ConcurrentDictionary<string, string[]> _reported = new(StringComparer.Ordinal);
private readonly IAuthStore _auth;
public NodeOwnerService(IAuthStore auth)
{
_auth = auth;
}
public void Report(string nodeId, IReadOnlyList<string>? credentialIds)
{
string[] clean = credentialIds is null ? [] : [.. credentialIds.Where(id => id.Length is > 0 and <= MaxCredentialIdLength).Distinct(StringComparer.Ordinal).Take(MaxOwners)];
if (clean.Length == 0)
{
_reported.TryRemove(nodeId, out _);
}
else
{
_reported[nodeId] = clean;
}
}
public void Forget(string nodeId) => _reported.TryRemove(nodeId, out _);
/// <summary>每个节点的属主(只含报过的节点)。</summary>
public async ValueTask<IReadOnlyDictionary<string, IReadOnlyList<NodeOwner>>> ListAsync(CancellationToken ct)
{
var result = new Dictionary<string, IReadOnlyList<NodeOwner>>(StringComparer.Ordinal);
var users = new Dictionary<string, string?>(StringComparer.Ordinal);
foreach ((string nodeId, string[] ids) in _reported)
{
var owners = new List<NodeOwner>();
foreach (string id in ids)
{
PasskeyRecord? passkey = await _auth.FindPasskeyAsync(id, ct);
string? username = null;
if (passkey is not null && !users.TryGetValue(passkey.UserId, out username))
{
username = (await _auth.GetUserAsync(passkey.UserId, ct))?.Username;
users[passkey.UserId] = username;
}
owners.Add(new NodeOwner(id, passkey?.Name, username));
}
result[nodeId] = owners;
}
return result;
}
}
@@ -30,10 +30,12 @@ public sealed partial class ProbeService
private readonly LinkService _links;
private readonly BinaryBindingService _bindings;
private readonly NodeOwnerService _owners;
public ProbeService(BinaryBindingService bindings, IProbeStore probes, IAgentStore agents, AgentRuntime runtime, IAgentGateway gateway, TimeProvider time, NodeInfoService nodes, LinkService links)
public ProbeService(BinaryBindingService bindings, NodeOwnerService owners, IProbeStore probes, IAgentStore agents, AgentRuntime runtime, IAgentGateway gateway, TimeProvider time, NodeInfoService nodes, LinkService links)
{
_bindings = bindings;
_owners = owners;
_links = links;
_nodes = nodes;
_probes = probes;
@@ -98,6 +100,7 @@ public sealed partial class ProbeService
{
await _nodes.ForgetAsync(id, ct);
await _bindings.ForgetAsync(id, ct);
_owners.Forget(id);
await _links.ForgetNodeAsync(id, ct);
_gateway.NotifyAllTaskSetsChanged();
}
@@ -39,6 +39,7 @@ public static class ApplicationServiceCollectionExtensions
services.AddSingleton<AuditService>();
services.AddSingleton<ServerIdentityService>();
services.AddSingleton<BinaryBindingService>();
services.AddSingleton<NodeOwnerService>();
services.AddSingleton<Fleet.Server.IBinaryAttestor>(sp => sp.GetRequiredService<BinaryBindingService>());
services.AddSingleton<Fleet.Server.IServerIdentity>(sp => sp.GetRequiredService<ServerIdentityService>());
services.AddSingleton<TrustService>();