fix(management): ListInstances pages, site-scopes and prefix-searches in SQL (review 2026-10-21 08/P2)
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user