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;
}
}