Register created app as public client (fix connect AADSTS7000218) #1
@@ -12,6 +12,10 @@ namespace SharepointToolbox.Web.Services;
|
|||||||
/// Retry is safe because the wrapped operation closure re-issues its own CSOM loads on each
|
/// 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
|
/// 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.
|
/// token without re-authentication. Each site is elevated at most once per circuit to prevent loops.
|
||||||
|
///
|
||||||
|
/// Both the admin-endpoint grant and the post-grant operation are retried with backoff: the
|
||||||
|
/// tenant admin endpoint can transiently 403 on a cold token, and the site-collection admin grant
|
||||||
|
/// is eventually consistent (notably on Group/Teams-connected sites), taking a few seconds to apply.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ElevationCoordinator : IElevationCoordinator
|
public class ElevationCoordinator : IElevationCoordinator
|
||||||
{
|
{
|
||||||
@@ -62,11 +66,28 @@ public class ElevationCoordinator : IElevationCoordinator
|
|||||||
// so the logs distinguish "grant failed/no-op" from "scan still fails for another reason".
|
// so the logs distinguish "grant failed/no-op" from "scan still fails for another reason".
|
||||||
await VerifyAdminAsync(siteUrl, ct);
|
await VerifyAdminAsync(siteUrl, ct);
|
||||||
|
|
||||||
// Re-run once. The closure re-issues its loads; the now-granted admin right applies.
|
// The site-collection admin grant is eventually consistent — on Group/Teams sites it
|
||||||
return await operation(ct);
|
// can take a few seconds to propagate to the content endpoint. Retry with backoff.
|
||||||
|
for (int attempt = 1; ; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await operation(ct);
|
||||||
|
}
|
||||||
|
catch (SharePointAccessDeniedException) when (attempt < MaxBackoffAttempts)
|
||||||
|
{
|
||||||
|
var delay = TimeSpan.FromSeconds(BackoffBaseSeconds * attempt);
|
||||||
|
Log.Warning("Post-elevation scan still denied for {Site} (attempt {N}/{Max}); retrying in {Delay}s.",
|
||||||
|
siteUrl, attempt, MaxBackoffAttempts, delay.TotalSeconds);
|
||||||
|
await Task.Delay(delay, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const int MaxBackoffAttempts = 4;
|
||||||
|
private const int BackoffBaseSeconds = 3;
|
||||||
|
|
||||||
private async Task ElevateAsync(string siteUrl, CancellationToken ct)
|
private async Task ElevateAsync(string siteUrl, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var profile = _session.CurrentProfile
|
var profile = _session.CurrentProfile
|
||||||
@@ -82,20 +103,34 @@ public class ElevationCoordinator : IElevationCoordinator
|
|||||||
ClientLogo = profile.ClientLogo,
|
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);
|
||||||
|
|
||||||
|
for (int attempt = 1; ; attempt++)
|
||||||
{
|
{
|
||||||
var adminCtx = await _sessionManager.GetOrCreateContextAsync(adminProfile, ct);
|
try
|
||||||
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.
|
||||||
// loginName empty → ElevateAsync resolves the current (delegated) user from the admin context.
|
await _ownership.ElevateAsync(adminCtx, siteUrl, loginName: string.Empty, ct);
|
||||||
await _ownership.ElevateAsync(adminCtx, siteUrl, loginName: string.Empty, ct);
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
// The admin endpoint can transiently 403 on a cold token / first call; it clears within
|
||||||
{
|
// seconds. A genuine lack of tenant-admin rights keeps failing and surfaces after retries.
|
||||||
Log.Error(ex, "Auto-elevate ownership failed for {Site}", siteUrl);
|
catch (SharePointAccessDeniedException ex) when (attempt < MaxBackoffAttempts)
|
||||||
throw new InvalidOperationException(
|
{
|
||||||
$"Auto-elevate ownership failed for {siteUrl}. Granting site-collection admin requires " +
|
var delay = TimeSpan.FromSeconds(BackoffBaseSeconds * attempt);
|
||||||
$"SharePoint tenant administrator rights on the signed-in account. ({ex.Message})", ex);
|
Log.Warning("Admin endpoint denied for {Site} (attempt {N}/{Max}); retrying in {Delay}s. {Err}",
|
||||||
|
siteUrl, attempt, MaxBackoffAttempts, delay.TotalSeconds, ex.Message);
|
||||||
|
await Task.Delay(delay, ct);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Auto-elevate ownership failed for {Site}", siteUrl);
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user