fix(management): ListInstances pages, site-scopes and prefix-searches in SQL (review 2026-10-21 08/P2)

This commit is contained in:
Joseph Doherty
2026-10-22 09:23:39 -04:00
parent b0c863c6a1
commit baab6ac78e
4 changed files with 69 additions and 24 deletions
+1 -1
View File
@@ -573,7 +573,7 @@ scadabridge --url <url> instance list [--site-id <int>] [--template-id <int>] [-
|--------|----------|-------------|
| `--site-id` | no | Filter by site ID |
| `--template-id` | no | Filter by template ID |
| `--search` | no | Search term matched against instance name |
| `--search` | no | Unique-name **prefix** (`StartsWith`; was a substring match before review 2026-10-21 08/P2) |
| `--skip` | no | Offset paging: number of items to skip (default 0). Applied after site-scope filtering. |
| `--take` | no | Offset paging: max items to return (clamped 1..1000; omit for unlimited) |
@@ -946,17 +946,29 @@ public class ManagementActor : ReceiveActor
private static async Task<object?> HandleListInstances(IServiceProvider sp, ListInstancesCommand cmd, AuthenticatedUser user)
{
var repo = sp.GetRequiredService<ICentralUiRepository>();
var instances = await repo.GetInstancesFilteredAsync(cmd.SiteId, cmd.TemplateId, cmd.SearchTerm);
// Filter by permitted sites for site-scoped users
// Paged, site-scoped and searched IN SQL (review 2026-10-21 08/P2): this used to read
// every matching instance (the whole table for an unfiltered CLI `instance list`),
// scope-filter and page in memory, and search with a leading-wildcard LIKE. The scope
// is passed as the repository's site filter, so a page can never surface an
// out-of-scope instance. Search is a unique-name PREFIX (StartsWith), which can use
// IX_Instances_UniqueName. The response stays a bare array; the total is not returned.
IReadOnlyCollection<int>? siteIds = cmd.SiteId is { } siteId ? new[] { siteId } : null;
if (user.PermittedSiteIds.Length > 0 && !user.Roles.Contains(Roles.Administrator, StringComparer.OrdinalIgnoreCase))
{
var permittedIds = new HashSet<string>(user.PermittedSiteIds);
instances = instances.Where(i => permittedIds.Contains(i.SiteId.ToString())).ToList();
// A non-integer grant is DROPPED (can only narrow), never ignored (would widen).
var permitted = user.PermittedSiteIds
.Select(id => int.TryParse(id, out var parsed) ? (int?)parsed : null)
.OfType<int>()
.ToHashSet();
siteIds = siteIds is null
? permitted.ToList()
: siteIds.Where(permitted.Contains).ToList();
}
// Paging is applied AFTER the site-scope filter so a scoped user never sees
// an out-of-scope instance surface into their page window.
return Page(instances, cmd.Skip, cmd.Take);
var take = cmd.Take is > 0 ? Math.Min(cmd.Take.Value, 1000) : int.MaxValue;
var repo = sp.GetRequiredService<ICentralUiRepository>();
var (rows, _) = await repo.GetInstancesPagedAsync(siteIds, cmd.TemplateId, cmd.SearchTerm, Math.Max(0, cmd.Skip), take);
return rows;
}
/// <summary>