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);
}
}
}