using Microsoft.AspNetCore.Components.Authorization; using ScadaLink.Commons.Entities.Sites; using ScadaLink.Security; namespace ScadaLink.CentralUI.Auth; /// /// Resolves the set of sites the current user is permitted to operate on, from /// the SiteId claims attached at login (CentralUI-002). /// /// The design (Component-CentralUI, CLAUDE.md "Security & Auth") makes the /// Deployment role site-scoped: a Deployment user mapped through an LDAP group /// with site-scope rules carries one /// claim per permitted site (the claim value is the integer Site.Id). /// A Deployment user with no SiteId claim — and any Admin/Design user — is /// system-wide. /// /// /// Deployment and Monitoring pages must filter every site/instance list through /// and re-check /// before any cross-site command, so a scoped user cannot view or act on sites /// outside their grant. /// /// public sealed class SiteScopeService { private readonly AuthenticationStateProvider _authStateProvider; private (bool IsSystemWide, IReadOnlySet Sites)? _cached; public SiteScopeService(AuthenticationStateProvider authStateProvider) { _authStateProvider = authStateProvider; } /// /// True when the user is not restricted to a site subset (no SiteId /// claims). System-wide users see and act on every site. /// public async Task IsSystemWideAsync() => (await ResolveAsync()).IsSystemWide; /// /// The set of Site.Id values the user may operate on. Empty for a /// system-wide user (callers should consult /// or use the filter/allowed helpers, which already account for that). /// public async Task> PermittedSiteIdsAsync() => (await ResolveAsync()).Sites; /// /// Returns the subset of the user is permitted to /// see. A system-wide user gets the full list back unchanged. /// public async Task> FilterSitesAsync(IEnumerable sites) { var (isSystemWide, allowed) = await ResolveAsync(); if (isSystemWide) return sites.ToList(); return sites.Where(s => allowed.Contains(s.Id)).ToList(); } /// /// True when the user may operate on the site with the given Site.Id. /// Must be re-checked server-side before any mutating cross-site command. /// public async Task IsSiteAllowedAsync(int siteId) { var (isSystemWide, allowed) = await ResolveAsync(); return isSystemWide || allowed.Contains(siteId); } private async Task<(bool IsSystemWide, IReadOnlySet Sites)> ResolveAsync() { if (_cached is { } cached) return cached; var state = await _authStateProvider.GetAuthenticationStateAsync(); var siteClaims = state.User.FindAll(JwtTokenService.SiteIdClaimType); var ids = new HashSet(); foreach (var claim in siteClaims) { if (int.TryParse(claim.Value, out var id)) ids.Add(id); } // No SiteId claims => system-wide. This mirrors SiteScopeAuthorizationHandler: // absence of scope rules means an unrestricted deployer. var result = (IsSystemWide: ids.Count == 0, Sites: (IReadOnlySet)ids); _cached = result; return result; } }