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:
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>();
|
||||
|
||||
Reference in new issue
Block a user