using SharepointToolbox.Web.Core.Helpers; using SharepointToolbox.Web.Core.Models; using SharepointToolbox.Web.Infrastructure.Auth; using SharepointToolbox.Web.Services.Session; namespace SharepointToolbox.Web.Services; /// /// Enumerates every site collection in a tenant via the SharePoint tenant-admin endpoint /// (Tenant.GetSitePropertiesFromSharePointByFilters), paging through all results. /// The auth model only changes how the admin-host context is built: /// /// • Certificate (app-only) profiles build the admin context through the cert factory — the /// same path the background report scheduler uses (), which /// relies only on the SharePoint Sites.FullControl.All application permission the cert /// app already holds. (The earlier Graph /sites/getAllSites path was dropped: it needs /// a separate Graph Sites.Read.All grant the cert app is not provisioned with, so it /// returned empty/403 and tenant-wide audits silently fell back to the root site alone.) /// • Delegated profiles build the admin context through the session manager; this requires the /// signed-in user to be a SharePoint administrator. /// /// The Graph /sites?search=* endpoint was deliberately abandoned for both: it ranks by /// relevance and is capped server-side, silently dropping sites and returning varying counts. /// public class SiteDiscoveryService : ISiteDiscoveryService { private readonly ISessionManager _sessionManager; private readonly IAppOnlyContextFactory _appOnly; public SiteDiscoveryService( ISessionManager sessionManager, IAppOnlyContextFactory appOnly) { _sessionManager = sessionManager; _appOnly = appOnly; } public async Task> SearchSitesAsync( TenantProfile profile, string? query = null, CancellationToken ct = default) { ArgumentException.ThrowIfNullOrEmpty(profile.TenantUrl); var adminUrl = TenantSiteEnumerator.BuildAdminUrl(profile.TenantUrl); // App-only profiles: build the admin-host context through the cert factory (matches the // scheduler), enumerating under the SharePoint app permission the cert already grants. if (_appOnly.IsConfigured(profile)) { using var adminCtx = await _appOnly.CreateContextAsync(profile, adminUrl, ct); return await TenantSiteEnumerator.EnumerateAsync(adminCtx, ct); } // Delegated profiles: enumeration only exists on the tenant admin endpoint. var adminProfile = profile.CloneForSite(adminUrl); var ctx = await _sessionManager.GetOrCreateContextAsync(adminProfile, ct); return await TenantSiteEnumerator.EnumerateAsync(ctx, ct); } }