using Serilog; using SharepointToolbox.Web.Core.Helpers; using SharepointToolbox.Web.Services.Session; namespace SharepointToolbox.Web.Services; /// /// Scoped per Blazor circuit. Catches from any /// wrapped operation and, when AutoTakeOwnership is enabled, grants the current user /// site-collection admin on the failing site (via the tenant admin endpoint) before retrying. /// /// Retry is safe because the wrapped operation closure re-issues its own CSOM loads on each /// attempt; the granted permission is server-side and takes effect for the existing delegated /// token without re-authentication. Each site is elevated at most once per circuit to prevent loops. /// public class ElevationCoordinator : IElevationCoordinator { private readonly ISessionManager _sessionManager; private readonly IOwnershipElevationService _ownership; private readonly IUserSessionService _session; private readonly HashSet _elevatedSites = new(StringComparer.OrdinalIgnoreCase); public ElevationCoordinator( ISessionManager sessionManager, IOwnershipElevationService ownership, IUserSessionService session) { _sessionManager = sessionManager; _ownership = ownership; _session = session; } public async Task RunAsync(Func operation, CancellationToken ct) => await RunAsync(async c => { await operation(c); return null; }, ct); public async Task RunAsync(Func> operation, CancellationToken ct) { try { return await operation(ct); } catch (SharePointAccessDeniedException ex) { if (!_session.Settings.AutoTakeOwnership) throw; var siteUrl = ex.SiteUrl.TrimEnd('/'); var key = siteUrl.ToLowerInvariant(); // Already elevated this site and still denied → elevation can't fix it. Surface original. if (_elevatedSites.Contains(key)) throw; // Elevation targets the tenant admin endpoint; denials there aren't site-ownership issues. if (siteUrl.Contains("-admin.sharepoint.com", StringComparison.OrdinalIgnoreCase)) throw; await ElevateAsync(siteUrl, ct); _elevatedSites.Add(key); // Re-run once. The closure re-issues its loads; the now-granted admin right applies. return await operation(ct); } } private async Task ElevateAsync(string siteUrl, CancellationToken ct) { var profile = _session.CurrentProfile ?? throw new InvalidOperationException("Cannot elevate ownership: no active profile."); var adminProfile = new Core.Models.TenantProfile { Id = profile.Id, Name = profile.Name, TenantUrl = BuildAdminUrl(siteUrl), TenantId = profile.TenantId, ClientId = profile.ClientId, ClientLogo = profile.ClientLogo, }; try { var adminCtx = await _sessionManager.GetOrCreateContextAsync(adminProfile, ct); Log.Information("Auto-elevating site-collection admin ownership for {Site} via {Admin}", siteUrl, adminProfile.TenantUrl); // loginName empty → ElevateAsync resolves the current (delegated) user from the admin context. await _ownership.ElevateAsync(adminCtx, siteUrl, loginName: string.Empty, ct); } catch (Exception ex) when (ex is not OperationCanceledException) { throw new InvalidOperationException( $"Auto-elevate ownership failed for {siteUrl}. Granting site-collection admin requires " + $"SharePoint tenant administrator rights on the signed-in account. ({ex.Message})", ex); } } // https://abcube.sharepoint.com/sites/Foo → https://abcube-admin.sharepoint.com private static string BuildAdminUrl(string siteUrl) { if (!Uri.TryCreate(siteUrl, UriKind.Absolute, out var uri)) return siteUrl; var adminHost = uri.Host.Replace(".sharepoint.com", "-admin.sharepoint.com", StringComparison.OrdinalIgnoreCase); return $"{uri.Scheme}://{adminHost}"; } }