Relay: end a user's sessions into a vault the moment they lose access to it

This commit is contained in:
jaap-jan committed 2026-10-02 14:26:44 +02:00
1 parent 965c9a7043
commit 170caa1391
12 files changed
+281 -38

No files matched your search

@@ -2,6 +2,7 @@ using System.Net;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Text;
using DodoSSH.Api.Features.Events;
using DodoSSH.Contracts;
using DodoSSH.Domain;
using DodoSSH.Infrastructure;
@@ -266,6 +267,56 @@ public sealed class RelayEndpointTests(ApiFixture fixture)
.ShouldBe(RelayCloseReason.IdleTimeout);
}
/// <remarks>
/// <para>
/// A session is authorised once, when its ticket is issued, and used to run on for as long as it carried
/// bytes — removing somebody from a team left their shells into its machines open for up to the session's
/// maximum lifetime. The announcement that tells their event socket to re-read its vaults now has the
/// relay ask again too.
/// </para>
/// <para>
/// Access is withdrawn here by deleting the vault rather than by a team removal, because what is under
/// test is the relay hearing the announcement and asking the access model again — any way of making the
/// answer change will do, and this one needs no team. The removal path announces through the same call;
/// see <c>TeamService.RemoveMemberAsync</c>.
/// </para>
/// </remarks>
[Fact]
public async Task ASessionWhoseUserLosesAccess_IsClosedWithItsOwnCode()
{
using var target = LoopbackTarget.Echo();
var (subject, hostId) = await SeedRoutedHostAsync(target, "10.40.8.1");
var ticket = await IssueAsync(subject, hostId);
using var timeout = Timeout();
using var socket = await ConnectAsync(ticket, timeout.Token);
await RoundTripAsync(socket, "still here", timeout.Token);
Guid userId;
await using (var scope = fixture.CreateScope())
{
var database = scope.ServiceProvider.GetRequiredService<DodoDbContext>();
userId = (await database.Users.SingleAsync(u => u.Subject == subject, timeout.Token)).Id;
var vaultId = (await database.Hosts.SingleAsync(h => h.Id == hostId, timeout.Token)).VaultId;
var vault = await database.Vaults.SingleAsync(v => v.Id == vaultId, timeout.Token);
vault.DeletedAtUtc = Now;
await database.SaveChangesAsync(timeout.Token);
scope.ServiceProvider.GetRequiredService<IVaultEventPublisher>().VaultAccessChanged(userId);
}
await ReadUntilCloseAsync(socket, timeout.Token);
socket.CloseStatus.ShouldBe((WebSocketCloseStatus)RelayProtocol.AccessRevokedCloseCode);
(await WaitForClosedAuditAsync(ticket.SessionId, timeout.Token)).CloseReason
.ShouldBe(RelayCloseReason.AccessRevoked);
}
[Fact]
public async Task AUserAtTheCap_IsRefusedATicket_AndASocketOverItIsClosed()
{