diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISecuredWriteRepository.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISecuredWriteRepository.cs
index fe431e10..52f7c71f 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISecuredWriteRepository.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISecuredWriteRepository.cs
@@ -46,6 +46,11 @@ public interface ISecuredWriteRepository
/// Site id filter; null matches every site.
/// Number of rows to skip (offset paging).
/// Maximum number of rows to return.
+ /// When non-null, only rows whose SiteId (site
+ /// identifier) is in the set match; null matches every site. Applied IN ADDITION
+ /// to . Used by the ManagementActor to constrain an unfiltered
+ /// list to a site-scoped caller's permitted sites at the query level (arch-review R2 N3),
+ /// so paging and totals stay correct.
/// Cancellation token.
/// A task that resolves to a page of matching rows, newest submission first.
Task> QueryAsync(
@@ -53,6 +58,7 @@ public interface ISecuredWriteRepository
string? siteId,
int skip,
int take,
+ IReadOnlyCollection? permittedSiteIds = null,
CancellationToken ct = default);
///
@@ -106,7 +112,16 @@ public interface ISecuredWriteRepository
///
/// Status filter; null matches every status.
/// Site id filter; null matches every site.
+ /// When non-null, only rows whose SiteId (site
+ /// identifier) is in the set match; null matches every site. Applied IN ADDITION
+ /// to . Used by the ManagementActor to constrain an unfiltered
+ /// list to a site-scoped caller's permitted sites at the query level (arch-review R2 N3),
+ /// so paging and totals stay correct.
/// Cancellation token.
/// A task that resolves to the count of matching rows.
- Task CountAsync(string? status, string? siteId, CancellationToken ct = default);
+ Task CountAsync(
+ string? status,
+ string? siteId,
+ IReadOnlyCollection? permittedSiteIds = null,
+ CancellationToken ct = default);
}
diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SecuredWriteRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SecuredWriteRepository.cs
index cab9d433..1ad871e1 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SecuredWriteRepository.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SecuredWriteRepository.cs
@@ -57,6 +57,7 @@ public class SecuredWriteRepository : ISecuredWriteRepository
string? siteId,
int skip,
int take,
+ IReadOnlyCollection? permittedSiteIds = null,
CancellationToken ct = default)
{
IQueryable query = _context.Set()
@@ -72,6 +73,11 @@ public class SecuredWriteRepository : ISecuredWriteRepository
query = query.Where(p => p.SiteId == siteId);
}
+ if (permittedSiteIds is not null)
+ {
+ query = query.Where(p => permittedSiteIds.Contains(p.SiteId));
+ }
+
return await query
.OrderByDescending(p => p.SubmittedAtUtc)
.ThenByDescending(p => p.Id)
@@ -128,7 +134,11 @@ WHERE Id = {id}
}
///
- public async Task CountAsync(string? status, string? siteId, CancellationToken ct = default)
+ public async Task CountAsync(
+ string? status,
+ string? siteId,
+ IReadOnlyCollection? permittedSiteIds = null,
+ CancellationToken ct = default)
{
IQueryable query = _context.Set()
.AsNoTracking();
@@ -143,6 +153,11 @@ WHERE Id = {id}
query = query.Where(p => p.SiteId == siteId);
}
+ if (permittedSiteIds is not null)
+ {
+ query = query.Where(p => permittedSiteIds.Contains(p.SiteId));
+ }
+
return await query.CountAsync(ct);
}
}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/SecuredWriteRepositoryTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/SecuredWriteRepositoryTests.cs
index 3ccff9fa..2788fac8 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/SecuredWriteRepositoryTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/SecuredWriteRepositoryTests.cs
@@ -164,6 +164,72 @@ public class SecuredWriteRepositoryTests : IClassFixture
Assert.Equal(0, await repo.CountAsync(status: "Executed", siteId: siteId));
}
+ [SkippableFact]
+ public async Task QueryAsync_PermittedSiteIds_ReturnsOnlyPermittedRows()
+ {
+ Skip.IfNot(_fixture.Available, _fixture.SkipReason);
+
+ // Distinct per-run site identifiers stand in for "SITE1"/"SITE2"/"SITE3" so the
+ // permitted-set filter is asserted deterministically against a shared MSSQL.
+ var site1 = NewSiteId();
+ var site2 = NewSiteId();
+ var site3 = NewSiteId();
+ await using var context = CreateContext();
+ var repo = new SecuredWriteRepository(context);
+
+ await repo.AddAsync(NewRow(site1, status: "Pending"));
+ await repo.AddAsync(NewRow(site1, status: "Pending"));
+ await repo.AddAsync(NewRow(site2, status: "Pending"));
+ await repo.AddAsync(NewRow(site3, status: "Pending"));
+
+ var rows = await repo.QueryAsync(status: null, siteId: null, skip: 0, take: 100,
+ permittedSiteIds: new[] { site1, site3 });
+ Assert.Equal(3, rows.Count);
+ Assert.DoesNotContain(rows, r => r.SiteId == site2);
+ }
+
+ [SkippableFact]
+ public async Task CountAsync_PermittedSiteIds_CountsOnlyPermittedRows()
+ {
+ Skip.IfNot(_fixture.Available, _fixture.SkipReason);
+
+ var site1 = NewSiteId();
+ var site2 = NewSiteId();
+ var site3 = NewSiteId();
+ await using var context = CreateContext();
+ var repo = new SecuredWriteRepository(context);
+
+ await repo.AddAsync(NewRow(site1, status: "Pending"));
+ await repo.AddAsync(NewRow(site1, status: "Pending"));
+ await repo.AddAsync(NewRow(site2, status: "Pending"));
+ await repo.AddAsync(NewRow(site3, status: "Pending"));
+
+ var count = await repo.CountAsync(status: null, siteId: null,
+ permittedSiteIds: new[] { site1, site3 });
+ Assert.Equal(3, count);
+ }
+
+ [SkippableFact]
+ public async Task QueryAsync_NullPermittedSiteIds_IsUnfiltered_BackCompat()
+ {
+ Skip.IfNot(_fixture.Available, _fixture.SkipReason);
+
+ // 4 rows on a single unique site; null permittedSiteIds must not filter them
+ // out — exactly today's behavior.
+ var siteId = NewSiteId();
+ await using var context = CreateContext();
+ var repo = new SecuredWriteRepository(context);
+
+ await repo.AddAsync(NewRow(siteId, status: "Pending"));
+ await repo.AddAsync(NewRow(siteId, status: "Pending"));
+ await repo.AddAsync(NewRow(siteId, status: "Executed"));
+ await repo.AddAsync(NewRow(siteId, status: "Rejected"));
+
+ var rows = await repo.QueryAsync(status: null, siteId: siteId, skip: 0, take: 100,
+ permittedSiteIds: null);
+ Assert.Equal(4, rows.Count);
+ }
+
// --- helpers ------------------------------------------------------------
private ScadaBridgeDbContext CreateContext()