fix(management): honor MgmtDeployArtifactsCommand.SiteId + enforce site scope (arch-review C2)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -29,6 +29,7 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
private readonly ITemplateEngineRepository _templateRepo;
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly ServiceCollection _services;
|
||||
private ArtifactDeploymentService _artifactService = null!;
|
||||
|
||||
public ManagementActorTests()
|
||||
{
|
||||
@@ -56,6 +57,35 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
new(new AuthenticatedUser("scopeduser", "Scoped User", roles, permittedSiteIds),
|
||||
command, Guid.NewGuid().ToString("N"));
|
||||
|
||||
/// <summary>
|
||||
/// Registers a substitutable <see cref="ArtifactDeploymentService"/> into the
|
||||
/// harness DI so DeployArtifacts tests can verify which deploy path was taken
|
||||
/// (single-site vs. fleet-wide) without exercising the real cluster comms.
|
||||
/// The virtual <c>DeployToSiteAsync</c>/<c>DeployToAllSitesAsync</c> members are
|
||||
/// intercepted; all other members keep their real (unused) bodies.
|
||||
/// </summary>
|
||||
private void RegisterArtifactService()
|
||||
{
|
||||
var comms = new CommunicationService(
|
||||
Options.Create(new CommunicationOptions()),
|
||||
NullLogger<CommunicationService>.Instance);
|
||||
_artifactService = Substitute.For<ArtifactDeploymentService>(
|
||||
Substitute.For<ISiteRepository>(),
|
||||
Substitute.For<IDeploymentManagerRepository>(),
|
||||
Substitute.For<ITemplateEngineRepository>(),
|
||||
Substitute.For<IExternalSystemRepository>(),
|
||||
Substitute.For<INotificationRepository>(),
|
||||
comms,
|
||||
Substitute.For<IAuditService>(),
|
||||
Options.Create(new DeploymentManagerOptions()),
|
||||
NullLogger<ArtifactDeploymentService>.Instance);
|
||||
_services.AddScoped(_ => _artifactService);
|
||||
}
|
||||
|
||||
private static Result<ArtifactDeploymentSummary> DeploySummary() =>
|
||||
Result<ArtifactDeploymentSummary>.Success(
|
||||
new ArtifactDeploymentSummary("deploy-1", Array.Empty<SiteArtifactResult>(), 1, 0));
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
Shutdown();
|
||||
@@ -1285,6 +1315,69 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
Assert.Contains("deploy-2", response.JsonData);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// DeployArtifacts honors SiteId + site-scope enforcement (arch-review C2)
|
||||
//
|
||||
// MgmtDeployArtifactsCommand.SiteId is now honored: a value routes to the
|
||||
// single-site deploy path; null routes fleet-wide. A site-scoped (non-Admin)
|
||||
// Deployer may not deploy fleet-wide, and may not target a site outside scope.
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void DeployArtifacts_WithSiteId_CallsSingleSiteDeploy()
|
||||
{
|
||||
RegisterArtifactService();
|
||||
_artifactService.DeployToSiteAsync(7, "testuser", Arg.Any<CancellationToken>())
|
||||
.Returns(DeploySummary());
|
||||
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new MgmtDeployArtifactsCommand(SiteId: 7), "Deployer"));
|
||||
|
||||
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
_artifactService.Received(1).DeployToSiteAsync(7, "testuser", Arg.Any<CancellationToken>());
|
||||
_artifactService.DidNotReceiveWithAnyArgs().DeployToAllSitesAsync(default!, default);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeployArtifacts_FleetWide_CallsAllSitesDeploy()
|
||||
{
|
||||
RegisterArtifactService();
|
||||
_artifactService.DeployToAllSitesAsync("testuser", Arg.Any<CancellationToken>())
|
||||
.Returns(DeploySummary());
|
||||
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new MgmtDeployArtifactsCommand(SiteId: null), "Deployer"));
|
||||
|
||||
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
_artifactService.Received(1).DeployToAllSitesAsync("testuser", Arg.Any<CancellationToken>());
|
||||
_artifactService.DidNotReceiveWithAnyArgs().DeployToSiteAsync(default, default!, default);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeployArtifacts_SiteScopedUser_OutOfScopeSite_Unauthorized()
|
||||
{
|
||||
RegisterArtifactService();
|
||||
|
||||
var actor = CreateActor();
|
||||
actor.Tell(ScopedEnvelope(new MgmtDeployArtifactsCommand(SiteId: 7), new[] { "3" }, "Deployer"));
|
||||
|
||||
ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
_artifactService.DidNotReceiveWithAnyArgs().DeployToSiteAsync(default, default!, default);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeployArtifacts_SiteScopedUser_FleetWide_Unauthorized()
|
||||
{
|
||||
RegisterArtifactService();
|
||||
|
||||
var actor = CreateActor();
|
||||
actor.Tell(ScopedEnvelope(new MgmtDeployArtifactsCommand(SiteId: null), new[] { "3" }, "Deployer"));
|
||||
|
||||
var resp = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("fleet-wide", resp.Message, StringComparison.OrdinalIgnoreCase);
|
||||
_artifactService.DidNotReceiveWithAnyArgs().DeployToAllSitesAsync(default!, default);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// SetInstanceOverrides atomicity (finding -015)
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user