docs: complete v2.3 project research (STACK, FEATURES, ARCHITECTURE, PITFALLS)
Research covers all five v2.3 features: automated app registration, app removal, auto-take ownership, group expansion in HTML reports, and report consolidation toggle. No new NuGet packages required. Build order and phase implications documented. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -800,3 +800,324 @@ bitmap.Freeze(); // Makes it immutable and thread-safe; also releases the file h
|
||||
---
|
||||
|
||||
*v2.2 pitfalls appended: 2026-04-08*
|
||||
|
||||
---
|
||||
|
||||
# v2.3 Pitfalls: Tenant Management & Report Enhancements
|
||||
|
||||
**Milestone:** v2.3 — App registration, auto-ownership, HTML group expansion, report consolidation
|
||||
**Researched:** 2026-04-09
|
||||
**Confidence:** HIGH for app registration sequence and group expansion limits (official Microsoft Learn docs); MEDIUM for auto-ownership security implications (multiple official sources cross-verified); MEDIUM for report consolidation (general deduplication principles applied to specific codebase model)
|
||||
|
||||
These pitfalls are specific to the four new feature areas in v2.3. They complement all prior pitfall sections above.
|
||||
|
||||
---
|
||||
|
||||
## Critical Pitfalls (v2.3)
|
||||
|
||||
### Pitfall v2.3-1: Missing Service Principal Creation After App Registration
|
||||
|
||||
**What goes wrong:**
|
||||
`POST /applications` creates the application object (the registration) but does NOT automatically create the service principal (enterprise app entry) in the target tenant. Attempting to grant permissions or use the app before creating the service principal produces cryptic 400/404 errors with no clear explanation. The application appears in Entra "App registrations" but is absent from "Enterprise applications."
|
||||
|
||||
**Why it happens:**
|
||||
The distinction between the application object (one across all tenants, lives in home tenant) and the service principal (one per tenant that uses the app) is not obvious. Most UI flows in the Azure portal create both atomically; the Graph API does not.
|
||||
|
||||
**Consequences:**
|
||||
Permission grants fail. Admin consent cannot be completed. The automated registration path appears broken with no recoverable error message.
|
||||
|
||||
**Prevention:**
|
||||
Implement app creation as a three-step atomic transaction with rollback on any failure:
|
||||
1. `POST /applications` — capture `appId` and object `id`
|
||||
2. `POST /servicePrincipals` with `{ "appId": "<appId>" }` — capture service principal `id`
|
||||
3. `POST /servicePrincipals/{spId}/appRoleAssignments` — grant each required app role
|
||||
|
||||
If step 2 or 3 fail, delete the application object created in step 1 to avoid orphaned registrations. Surface the failure with a specific message: "App was registered but could not be configured. It has been removed. Try again or use the manual setup guide."
|
||||
|
||||
**Detection:**
|
||||
- App appears in Azure portal App Registrations but not in Enterprise Applications.
|
||||
- Token acquisition fails with AADSTS700016 ("Application not found in directory").
|
||||
- `appRoleAssignment` POST returns 404 "Resource not found."
|
||||
|
||||
**Phase to address:** App Registration feature — before writing any registration code.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall v2.3-2: Circular Consent Dependency for the Automated Registration Path
|
||||
|
||||
**What goes wrong:**
|
||||
The automated path calls Graph APIs to create an app registration on a target tenant. These calls require the MSP own `TenantProfile.ClientId` app to have `Application.ReadWrite.All` and `AppRoleAssignment.ReadWrite.All` delegated permissions consented. These permissions are high-privilege and almost certainly not in the MSP app current consent grant (which was configured for SharePoint auditing). Without them, the automated path fails with 403 Forbidden on the very first Graph call.
|
||||
|
||||
**Why it happens:**
|
||||
The MSP app was registered for auditing scopes (SharePoint, Graph user read). App management scopes are a distinct, highly privileged category. Developers test against their own dev tenant where they have unrestricted access and never hit this problem.
|
||||
|
||||
**Consequences:**
|
||||
The "auto via Graph API" mode works only in the narrow case where the MSP has pre-configured their own app with these elevated permissions. For all other deployments, it fails silently or with a confusing 403.
|
||||
|
||||
**Prevention:**
|
||||
- Design two modes from day one: **automated** (MSP app already has `Application.ReadWrite.All` + `AppRoleAssignment.ReadWrite.All`) and **guided fallback** (step-by-step portal instructions shown in UI).
|
||||
- Before attempting the automated path, detect whether the required permissions are available: request a token with the required scopes and handle `MsalUiRequiredException` or `MsalServiceException` with error code `insufficient_scope` as a signal to fall back.
|
||||
- The guided fallback must be a first-class feature, not an afterthought. It should produce a pre-filled PowerShell script or direct portal URLs the target tenant admin can follow.
|
||||
- Never crash on a 403; always degrade gracefully to guided mode.
|
||||
|
||||
**Detection:**
|
||||
- MSAL token request returns `insufficient_scope` or Graph returns `Authorization_RequestDenied`.
|
||||
- Works on dev machine (dev has Global Admin + explicit consent), fails on first real MSP deployment.
|
||||
|
||||
**Phase to address:** App Registration design — resolve guided vs. automated split before implementation begins.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall v2.3-3: App Removal Leaves Orphaned Service Principals in Target Tenant
|
||||
|
||||
**What goes wrong:**
|
||||
`DELETE /applications/{objectId}` removes the application object from the home tenant but does NOT delete the service principal in the target tenant. OAuth2 permission grants and app role assignments linked to that service principal also remain. On re-registration, Entra may reject with a duplicate `appId` error, or the target tenant accumulates zombie enterprise app entries that confuse tenant admins.
|
||||
|
||||
**Why it happens:**
|
||||
The service principal is owned by the target tenant Entra directory, not by the application home tenant. The MSP app may not have permission to delete service principals in the target tenant.
|
||||
|
||||
**Prevention:**
|
||||
Define the removal sequence as:
|
||||
1. Revoke all app role assignments: `DELETE /servicePrincipals/{spId}/appRoleAssignments/{id}` for each grant
|
||||
2. Delete the service principal: `DELETE /servicePrincipals/{spId}`
|
||||
3. Delete the application object: `DELETE /applications/{appObjectId}`
|
||||
|
||||
If step 2 fails with 403 (cross-tenant restriction), surface a guided step: "Open the target tenant Azure portal -> Enterprise Applications -> search for the app name -> Delete." Do not silently skip — leaving an orphaned SP is a security artifact.
|
||||
|
||||
Require the stored `ManagedAppObjectId` and `ManagedServicePrincipalId` fields (see Pitfall v2.3-5) for this operation; never search by display name.
|
||||
|
||||
**Detection:**
|
||||
- After deletion, the Enterprise Application still appears in the target tenant portal.
|
||||
- Re-registration attempt produces `AADSTS70011: Invalid scope. The scope ... is not valid`.
|
||||
|
||||
**Phase to address:** App Removal feature.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall v2.3-4: Auto-Ownership Elevation Not Cleaned Up on Crash or Token Expiry
|
||||
|
||||
**What goes wrong:**
|
||||
The "auto-take ownership on access denied" flow elevates the tool to site collection administrator, performs the scan, then removes itself. If the app crashes mid-scan, the user closes the window, or the MSAL token expires and the removal call fails, the elevation is never reverted. The MSP account now has persistent, undocumented site collection admin rights on a client site — a security and compliance risk.
|
||||
|
||||
**Why it happens:**
|
||||
The take-ownership -> act -> release pattern requires reliable cleanup in all failure paths. WPF desktop apps can be terminated by the OS (BSOD, force close, low memory). Token expiry is time-based and unpredictable. No amount of `try/finally` protects against hard process termination.
|
||||
|
||||
**Consequences:**
|
||||
- MSP account silently holds elevated permissions on client sites.
|
||||
- If audited, the MSP appears to have persistent admin access without justification.
|
||||
- Client tenant admins may notice unexplained site collection admins and raise a security concern.
|
||||
|
||||
**Prevention:**
|
||||
- `try/finally` is necessary but not sufficient. Also maintain a persistent "cleanup pending" list in a local JSON file (e.g., `pending_ownership_cleanup.json`). Write the site URL and elevation timestamp to this file BEFORE the elevation happens. Remove the entry AFTER successful cleanup.
|
||||
- On every app startup, check this file and surface a non-dismissable warning listing any pending cleanups with links to the SharePoint admin center for manual resolution.
|
||||
- The UI toggle label should reflect the risk: "Auto-take site ownership on access denied (will attempt to release after scan)."
|
||||
- Log every elevation and every release attempt to Serilog with outcome (success/failure), site URL, and timestamp.
|
||||
|
||||
**Detection:**
|
||||
- After a scan that uses auto-ownership, check the site Site Collection Administrators in SharePoint admin center. The MSP account should not be present.
|
||||
- Simulate a crash mid-scan; restart the app. Verify the cleanup warning appears.
|
||||
|
||||
**Phase to address:** Auto-Ownership feature — persistence mechanism and startup check must be built before the elevation logic.
|
||||
|
||||
---
|
||||
|
||||
## Moderate Pitfalls (v2.3)
|
||||
|
||||
### Pitfall v2.3-5: TenantProfile Model Missing Fields for Registration Metadata
|
||||
|
||||
**What goes wrong:**
|
||||
`TenantProfile` currently has `Name`, `TenantUrl`, `ClientId`, and `ClientLogo`. After app registration, the tool needs to store the created application Graph object ID, `appId`, and service principal ID for later removal. Without these fields, removal requires searching by display name — fragile if a tenant admin renamed the app — or is impossible programmatically.
|
||||
|
||||
**Prevention:**
|
||||
Extend `TenantProfile` with optional fields before writing any registration code:
|
||||
|
||||
```csharp
|
||||
public string? ManagedAppObjectId { get; set; } // Graph object ID of created application
|
||||
public string? ManagedAppId { get; set; } // appId (client ID) of created app
|
||||
public string? ManagedServicePrincipalId { get; set; }
|
||||
public DateTimeOffset? ManagedAppRegisteredAt { get; set; }
|
||||
```
|
||||
|
||||
These are nullable: profiles created before v2.3 or using manually configured app registrations will have them null, which signals "use guided removal."
|
||||
|
||||
Persist atomically to the JSON profile file immediately after successful registration (using the existing write-then-replace pattern from the foundation pitfall section).
|
||||
|
||||
**Phase to address:** App Registration feature — model change must precede implementation of both registration and removal.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall v2.3-6: `$expand=members` Silently Truncates Group Members at ~20
|
||||
|
||||
**What goes wrong:**
|
||||
The simplest approach to get group members for HTML report expansion is `GET /groups/{id}?$expand=members`. This is hard-capped at approximately 20 members and is not paginable — `$top` does not increase the limit for expanded navigational properties. For any real-world group (department group, "All Employees"), the expanded list is silently incomplete with no `@odata.nextLink` or warning.
|
||||
|
||||
**Why it happens:**
|
||||
`$expand` is a navigational shortcut for small relationships, not for large collection fetches. Developers use it because it retrieves the parent object and its members in one call.
|
||||
|
||||
**Prevention:**
|
||||
Always use the dedicated endpoint: `GET /groups/{id}/transitiveMembers?$select=displayName,mail,userPrincipalName&$top=999` and follow `@odata.nextLink` until exhausted. `transitiveMembers` resolves nested group membership server-side, eliminating the need for manual recursion in most cases.
|
||||
|
||||
Group member data must be resolved server-side at report generation time (in C#). The HTML output is a static offline file — no live Graph calls are possible after export.
|
||||
|
||||
**Phase to address:** HTML Group Expansion feature.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall v2.3-7: Nested Group Recursion Without Cycle Detection
|
||||
|
||||
**What goes wrong:**
|
||||
If `transitiveMembers` is not used and manual recursion is implemented, groups can form cycles in edge cases. Even without true cycles, the same group ID can appear via multiple paths (group A and group B both contain group C), causing its members to be listed twice.
|
||||
|
||||
**Prevention:**
|
||||
- Prefer `transitiveMembers` over manual recursion for M365/Entra groups — Graph resolves transitivity server-side.
|
||||
- If manual recursion is needed (e.g., for SharePoint groups which are not M365 groups), maintain a `HashSet<string>` of visited group IDs. If a group ID is already in the set, skip it.
|
||||
- Cap recursion depth at 5. Surface a "(nesting limit reached)" indicator in the HTML if the cap is hit.
|
||||
|
||||
**Phase to address:** HTML Group Expansion feature.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall v2.3-8: Report Consolidation Changes the Output Schema Users Depend On
|
||||
|
||||
**What goes wrong:**
|
||||
`UserAccessEntry` is a flat record: one row = one permission assignment. Users (and any downstream automation) expect this structure. Consolidation merges rows for the same user across sites/objects into a single row with aggregated data. This is a breaking change to the report format. Existing users treating the CSV export as structured input will have their scripts break silently (wrong row count, missing columns, or changed column semantics).
|
||||
|
||||
**Why it happens:**
|
||||
Consolidation is useful but changes the fundamental shape of the data. If it is on by default or a persistent global setting, users who do not read release notes discover the breakage in production.
|
||||
|
||||
**Prevention:**
|
||||
- Consolidation toggle must be **off by default** and **per-report-generation** (a checkbox at export time, not a persistent global preference).
|
||||
- Introduce a new `ConsolidatedUserAccessEntry` type; do not modify `UserAccessEntry`. The existing audit pipeline, CSV export, and HTML export continue to use `UserAccessEntry` unchanged.
|
||||
- Consolidation produces a clearly labelled report (e.g., a "Consolidated View" header in the HTML, or a `_consolidated` filename suffix for CSV).
|
||||
- Both CSV and HTML exports must honour the toggle consistently. A mismatch (CSV not consolidated, HTML consolidated for the same run) is a data integrity error.
|
||||
|
||||
**Phase to address:** Report Consolidation feature — model and toggle design must be settled before building the consolidation logic.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall v2.3-9: Graph API Throttling During Bulk Group Expansion at Report Generation
|
||||
|
||||
**What goes wrong:**
|
||||
A user access report across 20 sites may surface 50+ distinct groups. Expanding all of them via sequential Graph calls can trigger HTTP 429. After September 2025, Microsoft reduced per-app per-user throttling limits to half of the tenant total, making this more likely under sustained MSP use.
|
||||
|
||||
**Prevention:**
|
||||
- Cache group membership results within a single report generation run: if the same `groupId` appears in multiple sites, resolve it once and reuse the result. A `Dictionary<string, IReadOnlyList<GroupMember>>` keyed by group ID is sufficient.
|
||||
- Process group expansions with bounded concurrency: `SemaphoreSlim(3)` (max 3 concurrent) rather than `Task.WhenAll` over all groups.
|
||||
- Apply exponential backoff on 429 responses using the `Retry-After` response header value.
|
||||
- The existing `BulkOperationRunner` pattern can be adapted for this purpose.
|
||||
|
||||
**Phase to address:** HTML Group Expansion feature.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall v2.3-10: SharePoint Admin Role Required for Site Ownership Changes (Not Just Global Admin)
|
||||
|
||||
**What goes wrong:**
|
||||
Adding a user as site collection administrator via PnP Framework or Graph requires the authenticated account to be a SharePoint Administrator (the role in the Microsoft 365 admin center), not just a Global Administrator. A user can be Global Admin in Entra without being SharePoint Admin. In testing environments the developer is typically both; in production MSP deployments a dedicated service account may only have the roles explicitly needed for auditing.
|
||||
|
||||
**Why it happens:**
|
||||
SharePoint has its own RBAC layer. PnP `AddAdministratorToSiteAsync` and equivalent CSOM calls check SharePoint-level admin role, not just Entra admin roles.
|
||||
|
||||
**Prevention:**
|
||||
- Before enabling the auto-ownership feature for a profile, validate that the current authenticated account has SharePoint admin rights. Attempt a low-risk admin API call (e.g., `GET /admin/sharepoint/sites`) and handle 403 as "insufficient permissions — SharePoint Administrator role required."
|
||||
- Document the requirement in the UI tooltip and guided setup text.
|
||||
- Test the feature against an account that is Global Admin but NOT SharePoint Admin to confirm the error path and message.
|
||||
|
||||
**Phase to address:** Auto-Ownership feature.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall v2.3-11: HTML Report Size Explosion from Embedded Group Member Data
|
||||
|
||||
**What goes wrong:**
|
||||
The current HTML export embeds all data inline as JavaScript variables. Expanding group members for a large report (50 groups x 200 members) embeds 10,000 additional name/email strings inline. Report file size can grow from ~200 KB to 5+ MB. Opening the file in Edge on an older machine becomes slow; in extreme cases the browser tab crashes.
|
||||
|
||||
**Prevention:**
|
||||
- Cap embedded member data at a configurable limit (e.g., 200 members per group). Display the actual count alongside a "(showing first 200 of 1,450)" indicator.
|
||||
- Render member lists as hidden `<div>` blocks toggled by the existing clickable-expand JavaScript pattern — do not pre-render all member rows into visible DOM nodes.
|
||||
- Do not attempt to implement live API calls from the HTML file. It is a static offline report and has no authentication context.
|
||||
|
||||
**Phase to address:** HTML Group Expansion feature.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall v2.3-12: App Registration Display Name Collision
|
||||
|
||||
**What goes wrong:**
|
||||
Using a fixed display name (e.g., "SharePoint Toolbox") for every app registration created across all client tenants, combined with looking up apps by display name for removal, causes the removal flow to target the wrong app if a tenant admin manually created another app with the same name.
|
||||
|
||||
**Prevention:**
|
||||
- Use a unique display name per registration that includes a recognizable prefix and ideally the MSP name, e.g., "SharePoint Toolbox - Contoso MSP."
|
||||
- Never use display name for targeting deletions. Always use the stored `ManagedAppObjectId` (see Pitfall v2.3-5).
|
||||
|
||||
**Phase to address:** App Registration feature.
|
||||
|
||||
---
|
||||
|
||||
## Phase-Specific Warnings (v2.3)
|
||||
|
||||
| Phase Topic | Likely Pitfall | Mitigation |
|
||||
|---|---|---|
|
||||
| App Registration — design | Automated path fails due to missing `Application.ReadWrite.All` (v2.3-2) | Design guided fallback before automated path; detect permission gaps before first API call |
|
||||
| App Registration — data model | `TenantProfile` cannot store created app IDs (v2.3-5) | Add nullable fields to model; persist atomically after registration |
|
||||
| App Registration — sequence | Forgetting `POST /servicePrincipals` after `POST /applications` (v2.3-1) | Implement as atomic 3-step transaction with rollback |
|
||||
| App Registration — display name | Collision with manually created apps (v2.3-12) | Unique name including MSP identifier; never search/delete by name |
|
||||
| App Removal | Orphaned service principal in target tenant (v2.3-3) | Three-step removal with guided fallback if cross-tenant SP deletion fails |
|
||||
| Auto-Ownership — cleanup | Elevation not reverted on crash (v2.3-4) | Persistent cleanup-pending JSON + startup check + non-dismissable warning |
|
||||
| Auto-Ownership — permissions | Works in dev (Global Admin), fails in production (no SharePoint Admin role) (v2.3-10) | Validate SharePoint admin role before first elevation; test against restricted account |
|
||||
| Group Expansion — member fetch | `$expand=members` silently truncates at ~20 (v2.3-6) | Use `transitiveMembers` with `$top=999` + follow `@odata.nextLink` |
|
||||
| Group Expansion — recursion | Cycle / duplication in nested groups (v2.3-7) | `HashSet<string>` visited set; prefer `transitiveMembers` over manual recursion |
|
||||
| Group Expansion — throttling | 429 from bulk group member fetches (v2.3-9) | Per-session member cache; `SemaphoreSlim(3)`; exponential backoff on 429 |
|
||||
| Group Expansion — HTML size | Report file grows to 5+ MB (v2.3-11) | Cap members per group; lazy-render hidden blocks; display "first N of M" indicator |
|
||||
| Report Consolidation — schema | Breaking change to row structure (v2.3-8) | Off by default; new model type; consistent CSV+HTML behaviour |
|
||||
|
||||
---
|
||||
|
||||
## v2.3 Integration Gotchas
|
||||
|
||||
| Integration | Common Mistake | Correct Approach |
|
||||
|---|---|---|
|
||||
| Graph `POST /applications` | Assuming service principal is auto-created | Always follow with `POST /servicePrincipals { "appId": "..." }` before granting permissions |
|
||||
| Graph admin consent grant | Using delegated flow without `Application.ReadWrite.All` pre-consented | Detect missing scope at startup; fall back to guided mode gracefully |
|
||||
| Graph group members | `$expand=members` on group object | `GET /groups/{id}/transitiveMembers?$select=...&$top=999` + follow `nextLink` |
|
||||
| PnP set site collection admin | Global Admin account without SharePoint Admin role | Validate SharePoint admin role before attempting; test against restricted account |
|
||||
| Auto-ownership cleanup | `try/finally` assumed sufficient | Persistent JSON cleanup list + startup check handles hard process termination |
|
||||
| `TenantProfile` for removal | Search for app by display name | Store `ManagedAppObjectId` at registration time; use object ID for all subsequent operations |
|
||||
| Report consolidation toggle | Persistent global setting silently changes future exports | Per-export-run checkbox, off by default; new model type; never modify `UserAccessEntry` |
|
||||
|
||||
---
|
||||
|
||||
## v2.3 "Looks Done But Isn't" Checklist
|
||||
|
||||
- [ ] **App registration — service principal:** After automated registration, verify the app appears in the target tenant Enterprise Applications (not just App Registrations).
|
||||
- [ ] **App registration — guided fallback:** Disable `Application.ReadWrite.All` on the MSP app and attempt automated registration. Verify graceful fallback to guided mode with a clear explanation, not a crash.
|
||||
- [ ] **App removal — SP cleanup:** After removal, verify the Enterprise Application is gone from the target tenant. If SP deletion failed, verify the guided manual step is surfaced.
|
||||
- [ ] **Auto-ownership — cleanup on crash:** Start an auto-ownership scan, force-close the app mid-scan, restart. Verify the cleanup-pending warning appears with the site URL.
|
||||
- [ ] **Auto-ownership — release after scan:** Complete a full auto-ownership scan. Verify the MSP account is no longer in the site collection admins list.
|
||||
- [ ] **Group expansion — large group:** Expand a group with 200+ members. Verify all members are shown (not just 20), or the cap indicator is correct.
|
||||
- [ ] **Group expansion — nested groups:** Expand a group that contains a sub-group. Verify sub-group members appear without duplicates.
|
||||
- [ ] **Group expansion — throttle recovery:** Simulate 429 during group expansion. Verify the operation pauses, logs "Retrying in Xs", and completes.
|
||||
- [ ] **Report consolidation — off by default:** Generate a user access report without enabling the toggle. Verify the output is identical to v2.2 output for the same data.
|
||||
- [ ] **Report consolidation — CSV + HTML consistency:** Enable consolidation and export both CSV and HTML. Verify both show the same number of consolidated rows.
|
||||
- [ ] **TenantProfile persistence:** After app registration, open the profile JSON file and verify `ManagedAppObjectId`, `ManagedAppId`, and `ManagedServicePrincipalId` are present and non-empty.
|
||||
|
||||
---
|
||||
|
||||
## v2.3 Sources
|
||||
|
||||
- Microsoft Learn: Create application — Graph v1.0 — https://learn.microsoft.com/en-us/graph/api/application-post-applications?view=graph-rest-1.0
|
||||
- Microsoft Learn: Grant and revoke API permissions programmatically — https://learn.microsoft.com/en-us/graph/permissions-grant-via-msgraph
|
||||
- Microsoft Learn: Grant tenant-wide admin consent to an application — https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/grant-admin-consent
|
||||
- Microsoft Learn: Grant an appRoleAssignment to a service principal — https://learn.microsoft.com/en-us/graph/api/serviceprincipal-post-approleassignments?view=graph-rest-1.0
|
||||
- Microsoft Learn: List group transitive members — Graph v1.0 — https://learn.microsoft.com/en-us/graph/api/group-list-transitivemembers?view=graph-rest-1.0
|
||||
- Microsoft Learn: Microsoft Graph service-specific throttling limits — https://learn.microsoft.com/en-us/graph/throttling-limits
|
||||
- Microsoft Q&A: How to use $expand=members parameter with pagination — https://learn.microsoft.com/en-us/answers/questions/5526721/how-to-use-the-expand-members-parameter-with-pagin
|
||||
- Microsoft Learn: Create SharePoint site ownership policy — https://learn.microsoft.com/en-us/sharepoint/create-sharepoint-site-ownership-policy
|
||||
- PnP PowerShell GitHub Issue #542: Add-PnPSiteCollectionAdmin Access Is Denied — https://github.com/pnp/powershell/issues/542
|
||||
- Pim Widdershoven: Privilege escalation using Azure App Registration and Microsoft Graph — https://www.pimwiddershoven.nl/entry/privilege-escalation-azure-app-registration-microsoft-graph/
|
||||
- 4Spot Consulting: Deduplication Pitfalls — When Not to Merge Data — https://4spotconsulting.com/when-clean-data-damages-your-business-the-perils-of-over-deduplication/
|
||||
- Existing codebase: `TenantProfile.cs`, `UserAccessEntry.cs`, `UserAccessHtmlExportService.cs`, `SessionManager.cs` (reviewed 2026-04-09)
|
||||
|
||||
---
|
||||
|
||||
*v2.3 pitfalls appended: 2026-04-09*
|
||||
|
||||
Reference in New Issue
Block a user