mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 13:10:13 +00:00
80d50a6d92
Security finding (CWE-863): the SignalR user id was the bare UPN and every revoke cut that UPN across the hub. A tenant admin could add any UPN (a Global Admin, another customer's operator) and remove, disable or demote it in a loop, restarting that person's live connections in every tenant. - Negotiate binds the user id to the sign-in tenant plus principal key from the validated request context (SignalRGroupHelper.UserId); a caller without a principal key negotiates without a user id, since every revocable join gate needs the key. - ISignalRNotificationService.DisconnectUserAsync takes the tenant and the key. LiveStreamRevoker is its only caller and names the tenant each grant resolved under: member routes the route's tenant, self-service routes the managing tenant, Global Admin routes the tenant of the UPN's identity binding. Replacing or deleting a binding now cuts the streams signed in under the previous one. - A self-service assignment confers scope only while its assignee is bound to the owning tenant, so a self-service grant resolves under the owner alone and the owner's revokes never need another tenant. - Remove, disable and enable of a UPN without a row in the tenant answer 404 and change nothing: no audit, no push revoke, no end of an assignment, no cut. - GlobalAdminService.DisableGlobalAdminAsync and its repository method had no caller and are removed. - Tests pin the finding's attack, the 404s, the self-service gate, the binding routes and the call sites; each part was mutated back to the old behaviour and failed its tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
3.9 KiB
C#
98 lines
3.9 KiB
C#
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using AutopilotMonitor.Functions.Helpers;
|
|
using AutopilotMonitor.Functions.Services;
|
|
|
|
namespace AutopilotMonitor.Functions.Tests;
|
|
|
|
/// <summary>
|
|
/// In-memory ISignalRNotificationService for unit tests. Records every call so tests can
|
|
/// assert that producer code (TenantNotificationService, GlobalNotificationService, …) emits
|
|
/// the right SignalR push when it should — without instantiating the real Azure SignalR
|
|
/// ServiceManager (whose constructor validates a connection string and is not test-friendly).
|
|
/// </summary>
|
|
public class FakeSignalRNotificationService : ISignalRNotificationService
|
|
{
|
|
public List<(string TenantId, NotificationAudience Audience, object Dto)> TenantSends { get; } = new();
|
|
public List<(string TenantId, string NotificationId)> TenantDismisses { get; } = new();
|
|
public List<string> TenantDismissAlls { get; } = new();
|
|
public List<object> GlobalSends { get; } = new();
|
|
public List<string> GlobalDismisses { get; } = new();
|
|
public int GlobalDismissAllCount { get; private set; }
|
|
public List<(string TenantId, string SessionId, int Count)> RuleResultsCalls { get; } = new();
|
|
public List<(string TenantId, string SessionId, string Risk)> VulnReportCalls { get; } = new();
|
|
public List<(string TenantId, string SessionId)> SessionDeletedCalls { get; } = new();
|
|
|
|
public Task NotifyRuleResultsAvailableAsync(string tenantId, string sessionId, int resultCount)
|
|
{
|
|
RuleResultsCalls.Add((tenantId, sessionId, resultCount));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task NotifyVulnerabilityReportAvailableAsync(string tenantId, string sessionId, string overallRisk)
|
|
{
|
|
VulnReportCalls.Add((tenantId, sessionId, overallRisk));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task NotifySessionDeletedAsync(string tenantId, string sessionId)
|
|
{
|
|
SessionDeletedCalls.Add((tenantId, sessionId));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task SendTenantNotificationAsync(string tenantId, NotificationAudience audience, object dto)
|
|
{
|
|
TenantSends.Add((tenantId, audience, dto));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task SendTenantNotificationDismissedAsync(string tenantId, string notificationId)
|
|
{
|
|
TenantDismisses.Add((tenantId, notificationId));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task SendTenantNotificationDismissedAllAsync(string tenantId)
|
|
{
|
|
TenantDismissAlls.Add(tenantId);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task SendGlobalNotificationAsync(object dto)
|
|
{
|
|
GlobalSends.Add(dto);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task SendGlobalNotificationDismissedAsync(string notificationId)
|
|
{
|
|
GlobalDismisses.Add(notificationId);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task SendGlobalNotificationDismissedAllAsync()
|
|
{
|
|
GlobalDismissAllCount++;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>The SignalR user id each negotiation was bound to, composed like the real service (null: no user id).</summary>
|
|
public List<string?> NegotiatedUserIds { get; } = new();
|
|
|
|
/// <summary>The SignalR user id each cut addressed, composed like the real service.</summary>
|
|
public List<string> DisconnectedUsers { get; } = new();
|
|
|
|
public Task<(string Url, string AccessToken)?> NegotiateClientAsync(string tenantId, string? principalKey)
|
|
{
|
|
NegotiatedUserIds.Add(string.IsNullOrEmpty(principalKey) ? null : SignalRGroupHelper.UserId(tenantId, principalKey));
|
|
return Task.FromResult<(string Url, string AccessToken)?>(("https://fake.signalr.test/client", "fake-token"));
|
|
}
|
|
|
|
public Task DisconnectUserAsync(string tenantId, string principalKey)
|
|
{
|
|
DisconnectedUsers.Add(SignalRGroupHelper.UserId(tenantId, principalKey));
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|