namespace MoShiro.Backend.Application; /// /// 能力(capability):一项具体的权限。端点声明自己需要哪一项, 是唯一的判断入口, /// 会话、API key 都走它。设计见 docs/security/access.md 第 3 节。 /// public static class Capability { /// 任何已认证的主体(会话或 API key);只有登出、查看自己这类端点用它。 public const string Authenticated = "authenticated"; public const string PublicRead = "public.read"; public const string FleetRead = "fleet.read"; public const string FleetWrite = "fleet.write"; public const string FleetEnroll = "fleet.enroll"; public const string FleetRevoke = "fleet.revoke"; public const string TasksRead = "tasks.read"; public const string TasksWrite = "tasks.write"; public const string DataRead = "data.read"; public const string DataWrite = "data.write"; public const string SettingsRead = "settings.read"; public const string SettingsWrite = "settings.write"; public const string UpdateRead = "update.read"; public const string UpdateConfirm = "update.confirm"; public const string KeysRead = "keys.read"; public const string KeysManage = "keys.manage"; public const string AccountsRead = "accounts.read"; public const string AccountsManage = "accounts.manage"; public const string AuditRead = "audit.read"; public const string EventsRead = "events.read"; public const string OpsMetrics = "ops.metrics"; // 预留:外部 webhook 回调和数据拉取(docs/security/access.md 6.3),目录里先存在,实现放后面 public const string DataExport = "data.export"; public const string WebhooksRead = "webhooks.read"; public const string WebhooksManage = "webhooks.manage"; } /// 一项能力的元数据。Trust:改变信任或有破坏性的能力(吊销、账户),界面上特别标出,授权与其他能力一样靠 API key 或登录会话;真正由 passkey 签名保护的是操作员集合,那是节点自己验证的;Reserved:还没有对应的功能。 public sealed record CapabilityInfo(string Id, string Group, bool Trust = false, bool Reserved = false, params string[] Implies); public sealed record CapabilityPreset(string Id, IReadOnlyList Capabilities); /// 能力目录:分组、依赖(写蕴含读)、预设,以及创建 API key 时的校验。 public static class CapabilityCatalog { public static IReadOnlyList All { get; } = [ new(Capability.PublicRead, "public"), new(Capability.FleetRead, "fleet"), new(Capability.FleetWrite, "fleet", false, false, Capability.FleetRead), new(Capability.FleetEnroll, "fleet", false, false, Capability.FleetRead), new(Capability.FleetRevoke, "fleet", true, false, Capability.FleetRead), new(Capability.TasksRead, "tasks"), new(Capability.TasksWrite, "tasks", false, false, Capability.TasksRead), new(Capability.DataRead, "data"), new(Capability.DataWrite, "data", false, false, Capability.DataRead), new(Capability.DataExport, "data", false, true, Capability.DataRead), new(Capability.SettingsRead, "settings"), new(Capability.SettingsWrite, "settings", false, false, Capability.SettingsRead), new(Capability.UpdateRead, "update"), new(Capability.UpdateConfirm, "update", false, false, Capability.UpdateRead), new(Capability.KeysRead, "keys"), new(Capability.KeysManage, "keys", false, false, Capability.KeysRead), new(Capability.AccountsRead, "accounts"), new(Capability.AccountsManage, "accounts", true, false, Capability.AccountsRead), new(Capability.EventsRead, "ops"), new(Capability.AuditRead, "ops"), new(Capability.OpsMetrics, "ops"), new(Capability.WebhooksRead, "integrations", false, true), new(Capability.WebhooksManage, "integrations", false, true, Capability.WebhooksRead), ]; private static readonly Dictionary s_byId = All.ToDictionary(c => c.Id, StringComparer.Ordinal); public static IReadOnlyList Groups { get; } = [.. All.Select(c => c.Group).Distinct()]; public static IReadOnlyList Presets { get; } = [ new("read-only", [.. All.Where(c => !c.Reserved && c.Id.EndsWith(".read", StringComparison.Ordinal)).Select(c => c.Id)]), new("operations", Expand([Capability.PublicRead, Capability.FleetWrite, Capability.TasksWrite, Capability.DataWrite, Capability.SettingsWrite, Capability.UpdateConfirm, Capability.EventsRead, Capability.AuditRead])), new("enroll", Expand([Capability.FleetEnroll])), new("all", [.. All.Select(c => c.Id)]), ]; public static bool IsKnown(string id) => s_byId.ContainsKey(id); /// 补上蕴含的能力(写蕴含读),去重并排序,结果稳定。 public static IReadOnlyList Expand(IEnumerable ids) { var result = new SortedSet(StringComparer.Ordinal); foreach (string id in ids) { Add(result, id); } return [.. result]; } private static void Add(SortedSet set, string id) { if (id is Capability.Authenticated || !set.Add(id) || !s_byId.TryGetValue(id, out CapabilityInfo? info)) { return; } foreach (string implied in info.Implies) { Add(set, implied); } } /// /// 登录会话的能力:全部,包括信任类,密码登录和 passkey 登录一样。区别在于敏感操作:信任类的操作还要有效的提权窗口(), /// 窗口要当场触碰 passkey 才能开,所以密码泄露的人做不了这些,日常管理却不需要每次触碰。 /// public static IReadOnlyList ForSession() => [.. All.Select(c => c.Id)]; }