From 1c36ea89d002f6210d0094ec833e3644a3fcf61e Mon Sep 17 00:00:00 2001 From: kawa Date: Tue, 2 Jun 2026 14:26:01 +0200 Subject: [PATCH] Classify bare HTTP 403 as access-denied for Group/Teams sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Microsoft 365 Group / Teams-connected sites surface access-denied on some CSOM calls as a raw "(403) FORBIDDEN" WebException carrying 0x80070005 (E_ACCESSDENIED), not as a typed ServerException with ServerErrorTypeName = System.UnauthorizedAccessException. IsAccessDenied only matched the typed shape, so those denials became generic InvalidOperationExceptions the elevation coordinator never caught — no auto-elevation ran and the operation failed even for a SharePoint admin. Walk the inner-exception chain and treat any of these as access-denied: the typed ServerException, a WebException with HTTP 403, or a message containing the E_ACCESSDENIED HRESULT. Per-site dedupe still caps elevation to one retry, so a 403 elevation cannot fix (policy/endpoint block) won't loop. Co-Authored-By: Claude Opus 4.8 (1M context) --- Core/Helpers/ExecuteQueryRetryHelper.cs | 27 +++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Core/Helpers/ExecuteQueryRetryHelper.cs b/Core/Helpers/ExecuteQueryRetryHelper.cs index 332924e..4b349be 100644 --- a/Core/Helpers/ExecuteQueryRetryHelper.cs +++ b/Core/Helpers/ExecuteQueryRetryHelper.cs @@ -84,10 +84,29 @@ public static class ExecuteQueryRetryHelper return new InvalidOperationException(enriched, ex); } - private static bool IsAccessDenied(Exception ex) => - ex is ServerUnauthorizedAccessException || - (ex is ServerException se && - string.Equals(se.ServerErrorTypeName, "System.UnauthorizedAccessException", StringComparison.Ordinal)); + // Access-denied reaches us in two shapes: a typed CSOM ServerException + // (ServerErrorTypeName = System.UnauthorizedAccessException), and — notably on + // Microsoft 365 Group / Teams-connected sites — a bare HTTP (403) FORBIDDEN + // WebException carrying "Access is denied ... 0x80070005 (E_ACCESSDENIED)". + // Both are ownership issues elevation can fix, so classify either as access-denied. + private static bool IsAccessDenied(Exception ex) + { + for (Exception? cur = ex; cur is not null; cur = cur.InnerException) + { + if (cur is ServerUnauthorizedAccessException) + return true; + if (cur is ServerException se && + string.Equals(se.ServerErrorTypeName, "System.UnauthorizedAccessException", StringComparison.Ordinal)) + return true; + if (cur is WebException we && we.Response is HttpWebResponse resp && + resp.StatusCode == HttpStatusCode.Forbidden) + return true; + if (cur.Message.Contains("0x80070005", StringComparison.OrdinalIgnoreCase) || + cur.Message.Contains("E_ACCESSDENIED", StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } internal static bool IsThrottleException(Exception ex) {