namespace MoShiro.Backend.Application; /// /// 提权窗口的种类(docs/access.md 第 4 节):加节点(15 分钟)、吊销和删除(5 分钟)、改凭证(60 秒,用一次就没有)。 /// 窗口属于会话,不属于用户:换一台设备、一个浏览器就要重新触碰 passkey。 /// public enum ElevationClass { Enroll, Revoke, Credential, } public sealed record ElevationWindow(ElevationClass Class, long ExpiresAtMs); public static class Elevation { public static string IdOf(ElevationClass elevation) => elevation switch { ElevationClass.Enroll => "enroll", ElevationClass.Revoke => "revoke", _ => "credential", }; public static bool TryParse(string? id, out ElevationClass elevation) { (bool ok, elevation) = id switch { "enroll" => (true, ElevationClass.Enroll), "revoke" => (true, ElevationClass.Revoke), "credential" => (true, ElevationClass.Credential), _ => (false, default), }; return ok; } /// 端点要求的能力需要哪一种窗口;不是信任类的能力返回 null。 public static ElevationClass? For(string capability) => capability switch { Capability.FleetRevoke => ElevationClass.Revoke, Capability.AccountsManage => ElevationClass.Credential, _ => null, }; } /// /// 会话的提权窗口,只在内存里:重启之后需要重新触碰 passkey,这正是想要的。API key 不经过这里(它自己带着能力,创建时已经签过)。 /// public sealed class ElevationService { private readonly BackendOptions _options; private readonly TimeProvider _time; private readonly object _gate = new(); private readonly Dictionary> _windows = []; public ElevationService(BackendOptions options, TimeProvider time) { _options = options; _time = time; } public TimeSpan LifetimeOf(ElevationClass elevation) => elevation switch { ElevationClass.Enroll => TimeSpan.FromMinutes(_options.ElevationEnrollMinutes), ElevationClass.Revoke => TimeSpan.FromMinutes(_options.ElevationRevokeMinutes), _ => TimeSpan.FromSeconds(_options.ElevationCredentialSeconds), }; /// 开窗口;已经有的窗口重新计时。返回到期时间。 public long Grant(string sessionId, ElevationClass elevation) { long now = _time.GetUtcNow().ToUnixTimeMilliseconds(); long expires = now + (long)LifetimeOf(elevation).TotalMilliseconds; lock (_gate) { Prune(now); if (!_windows.TryGetValue(sessionId, out Dictionary? own)) { _windows[sessionId] = own = []; } own[elevation] = expires; } return expires; } /// 窗口是否有效。 且是凭证类:同时用掉(一次有效)。 public bool Check(string sessionId, ElevationClass elevation, bool consume) { long now = _time.GetUtcNow().ToUnixTimeMilliseconds(); lock (_gate) { if (!_windows.TryGetValue(sessionId, out Dictionary? own) || !own.TryGetValue(elevation, out long expires) || expires <= now) { return false; } if (consume && elevation == ElevationClass.Credential) { own.Remove(elevation); } return true; } } public IReadOnlyList Active(string sessionId) { long now = _time.GetUtcNow().ToUnixTimeMilliseconds(); lock (_gate) { return _windows.TryGetValue(sessionId, out Dictionary? own) ? [.. own.Where(w => w.Value > now).OrderBy(w => w.Key).Select(w => new ElevationWindow(w.Key, w.Value))] : []; } } /// 主动结束全部窗口(点「结束」或退出登录)。 public void End(string sessionId) { lock (_gate) { _windows.Remove(sessionId); } } private void Prune(long now) { foreach (string session in _windows.Where(p => p.Value.All(w => w.Value <= now)).Select(p => p.Key).ToList()) { _windows.Remove(session); } } }