using DodoSSH.Api.Authorization; using DodoSSH.Domain.Authorization; namespace DodoSSH.Api.Features.Relay; /// /// Ends a user's relay sessions to vaults they can no longer reach, when their access changes. /// /// /// /// A relay session is authorised once, when its ticket is issued, and used to run on for as long as it /// carried bytes — up to the maximum lifetime, twelve hours by default. Removing somebody from a team took /// their keys and their vault list away at once, and left any shell they had open through the relay running /// into the team's machines until it went idle. /// /// /// Told by the same announcement that tells their event socket to re-read its vaults — see /// — and it asks the same question the ticket /// endpoint asked, against the database as it now stands. Off the announcing request, because that request /// has already committed and answered, and a slow re-check must not hold it. /// /// internal sealed class RelayAccessWatch( RelaySessionRegistry sessions, IServiceScopeFactory scopes, ILogger logger) { /// Re-checks every session the user has open, in the background. /// The re-check, for a caller that wants to wait for it; the announcement does not. internal Task AccessChangedAsync(Guid userId) { var open = sessions.SessionsOf(userId); return open.Count == 0 ? Task.CompletedTask : Task.Run(() => RecheckAsync(userId, open)); } private async Task RecheckAsync(Guid userId, IReadOnlyList open) { try { var scope = scopes.CreateAsyncScope(); await using var _ = scope.ConfigureAwait(false); var access = scope.ServiceProvider.GetRequiredService(); foreach (var lease in open) { var resolved = await access.ResolveAsync(userId, lease.VaultId, CancellationToken.None) .ConfigureAwait(false); if (!resolved.Granted || !resolved.Permissions.HasFlag(PermissionFlags.Connect)) { lease.Revoke(); } } } catch (Exception exception) when (exception is not OutOfMemoryException) { RelayLog.AccessRecheckFailed(logger, userId, exception); } } }