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:
@@ -189,7 +189,7 @@ The implementation follows the same `GraphClientFactory` + `GraphServiceClient`
|
||||
|
||||
---
|
||||
|
||||
## No New NuGet Packages Required
|
||||
## No New NuGet Packages Required (v2.2)
|
||||
|
||||
| Feature | What's needed | How provided |
|
||||
|---|---|---|
|
||||
@@ -207,7 +207,7 @@ The implementation follows the same `GraphClientFactory` + `GraphServiceClient`
|
||||
|
||||
---
|
||||
|
||||
## Impact on Existing Services
|
||||
## Impact on Existing Services (v2.2)
|
||||
|
||||
### HTML Export Services
|
||||
|
||||
@@ -239,9 +239,196 @@ Add a `BrowseMode` boolean property (bound to a RadioButton or ToggleButton). Wh
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## v2.3 Stack Additions
|
||||
|
||||
**Researched:** 2026-04-09
|
||||
**Scope:** Only what is NEW vs the validated v2.2 stack. The short answer: **no new NuGet packages are required for any v2.3 feature.**
|
||||
|
||||
---
|
||||
|
||||
### Feature 1: App Registration on Target Tenant (Auto + Guided Fallback)
|
||||
|
||||
**What is needed:** Create an Entra app registration in a *target* (client) tenant from within the app, using the already-authenticated delegated token.
|
||||
|
||||
**No new packages required.** The existing `Microsoft.Graph` SDK (currently 5.74.0, latest stable is 5.103.0 as of 2026-02-20) already supports this via:
|
||||
|
||||
- `graphClient.Applications.PostAsync(new Application { DisplayName = "...", RequiredResourceAccess = [...] })` — creates the app object; returns the new `appId`
|
||||
- `graphClient.ServicePrincipals.PostAsync(new ServicePrincipal { AppId = newAppId })` — instantiates the enterprise app in the target tenant so it can be consented
|
||||
- `graphClient.Applications["{objectId}"].DeleteAsync()` — removes the registration (soft-delete, 30-day recycle bin in Entra)
|
||||
|
||||
All three operations are Graph v1.0 endpoints confirmed in official Microsoft Learn documentation (HIGH confidence).
|
||||
|
||||
**Required delegated permissions for these Graph calls:**
|
||||
|
||||
| Operation | Minimum delegated scope |
|
||||
|-----------|------------------------|
|
||||
| Create application (`POST /applications`) | `Application.ReadWrite.All` |
|
||||
| Create service principal (`POST /servicePrincipals`) | `Application.ReadWrite.All` |
|
||||
| Delete application (`DELETE /applications/{id}`) | `Application.ReadWrite.All` |
|
||||
| Grant app role consent (`POST /servicePrincipals/{id}/appRoleAssignments`) | `AppRoleAssignment.ReadWrite.All` |
|
||||
|
||||
The calling user must also hold the **Application Administrator** or **Cloud Application Administrator** Entra role on the target tenant (or Global Administrator). Without the role, the delegated call returns 403 regardless of scope consent.
|
||||
|
||||
**Integration point — `GraphClientFactory` scope extension:**
|
||||
|
||||
The existing `GraphClientFactory.CreateClientAsync` uses `["https://graph.microsoft.com/.default"]`, relying on pre-consented `.default` resolution. For app registration, add an overload:
|
||||
|
||||
```csharp
|
||||
// New method — only used by AppRegistrationService
|
||||
public async Task<GraphServiceClient> CreateRegistrationClientAsync(
|
||||
string clientId, CancellationToken ct)
|
||||
{
|
||||
var pca = await _msalFactory.GetOrCreateAsync(clientId);
|
||||
var accounts = await pca.GetAccountsAsync();
|
||||
var account = accounts.FirstOrDefault();
|
||||
|
||||
// Explicit scopes trigger incremental consent on first call
|
||||
var scopes = new[]
|
||||
{
|
||||
"Application.ReadWrite.All",
|
||||
"AppRoleAssignment.ReadWrite.All"
|
||||
};
|
||||
var tokenProvider = new MsalTokenProvider(pca, account, scopes);
|
||||
var authProvider = new BaseBearerTokenAuthenticationProvider(tokenProvider);
|
||||
return new GraphServiceClient(authProvider);
|
||||
}
|
||||
```
|
||||
|
||||
MSAL will prompt for incremental consent if not yet granted. This keeps the default `CreateClientAsync` scopes unchanged and avoids over-permissioning all Graph calls throughout the app.
|
||||
|
||||
**`TenantProfile` model extension:**
|
||||
|
||||
```csharp
|
||||
// Add to TenantProfile.cs
|
||||
public string? AppObjectId { get; set; } // Entra object ID of the registered app
|
||||
// null until registration completes
|
||||
// used for deletion
|
||||
```
|
||||
|
||||
Stored in JSON (existing ProfileService persistence). No schema migration needed — `System.Text.Json` deserializes missing properties as their default value (`null`).
|
||||
|
||||
**Guided fallback path:** If the automated registration fails (user lacks Application Administrator role, or consent blocked by tenant policy), open a browser to the Entra admin center app registration URL. No additional API calls needed for the fallback path.
|
||||
|
||||
---
|
||||
|
||||
### Feature 2: App Removal from Target Tenant
|
||||
|
||||
**Same stack as Feature 1.** `graphClient.Applications["{objectId}"].DeleteAsync()` is the Graph v1.0 `DELETE /applications/{id}` endpoint. Returns 204 on success. `AppObjectId` stored in `TenantProfile` provides the handle.
|
||||
|
||||
Deletion behavior: apps go to Entra's 30-day deleted items container and can be restored via the admin center. The app does not need to handle restoration — that is admin-center territory.
|
||||
|
||||
---
|
||||
|
||||
### Feature 3: Auto-Take Ownership of Sites on Access Denied (Global Toggle)
|
||||
|
||||
**No new packages required.** PnP Framework 1.18.0 already exposes the SharePoint tenant admin API via the `Tenant` class, which can add a site collection admin without requiring existing access to that site:
|
||||
|
||||
```csharp
|
||||
// Requires ClientContext pointed at the tenant admin site
|
||||
// (e.g., https://contoso-admin.sharepoint.com)
|
||||
var tenant = new Tenant(adminCtx);
|
||||
tenant.SetSiteAdmin(siteUrl, userLogin, isAdmin: true);
|
||||
adminCtx.ExecuteQueryRetry();
|
||||
```
|
||||
|
||||
**Critical constraint (HIGH confidence):** `Tenant.SetSiteAdmin` calls the SharePoint admin tenant API. This bypasses the site-level permission check — it does NOT require the authenticated user to already be a member of the site. It DOES require the user to hold the **SharePoint Administrator** or **Global Administrator** Entra role. If the user lacks this role, the call throws `ServerException` with "Access denied."
|
||||
|
||||
**Integration point — `SessionManager` admin context:**
|
||||
|
||||
The existing `SessionManager.GetOrCreateContextAsync` accepts a `TenantProfile` and uses `profile.TenantUrl` as the site URL. For tenant admin operations, a second context is needed pointing at the admin URL. Add:
|
||||
|
||||
```csharp
|
||||
// New method in SessionManager (no new library, same PnP auth path)
|
||||
public async Task<ClientContext> GetOrCreateAdminContextAsync(
|
||||
TenantProfile profile, CancellationToken ct = default)
|
||||
{
|
||||
// Derive admin URL: https://contoso.sharepoint.com -> https://contoso-admin.sharepoint.com
|
||||
var adminUrl = profile.TenantUrl
|
||||
.TrimEnd('/')
|
||||
.Replace(".sharepoint.com", "-admin.sharepoint.com",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var adminProfile = profile with { TenantUrl = adminUrl };
|
||||
return await GetOrCreateContextAsync(adminProfile, ct);
|
||||
}
|
||||
```
|
||||
|
||||
**Global toggle storage:** Add `AutoTakeOwnership: bool` to `AppSettings` (existing JSON settings file). No new model needed.
|
||||
|
||||
---
|
||||
|
||||
### Feature 4: Expand Groups in HTML Reports (Clickable to Show Members)
|
||||
|
||||
**No new packages required.** Pure C# + inline JavaScript.
|
||||
|
||||
**Server-side group member resolution:** SharePoint group members are already loaded during the permissions scan via CSOM `RoleAssignment.Member`. For SharePoint groups, `Member` is a `Group` object. Load its `Users` collection:
|
||||
|
||||
```csharp
|
||||
// Already inside the permissions scan loop — extend it
|
||||
if (roleAssignment.Member is Group spGroup)
|
||||
{
|
||||
ctx.Load(spGroup.Users);
|
||||
// ExecuteQueryRetry already called in the scan loop
|
||||
// Members available as spGroup.Users
|
||||
}
|
||||
```
|
||||
|
||||
No additional API calls beyond what the existing scan already performs (the Users collection is a CSOM lazy-load — one additional batch per group, amortized over the scan).
|
||||
|
||||
**HTML export change:** Pass group member lists into the export service as part of the existing `PermissionEntry` model (extend with `IReadOnlyList<string>? GroupMembers`). The export service renders members as a collapsible `<details>/<summary>` HTML element inline with each group-access row — pure HTML5, no JS library, no external dependency.
|
||||
|
||||
**Report consolidation pre-processing:** Consolidation is a LINQ step before export. No new model or service.
|
||||
|
||||
---
|
||||
|
||||
### Feature 5: Report Consolidation Toggle (Merge Duplicate User Entries)
|
||||
|
||||
**No new packages required.** Standard LINQ aggregation on the existing `IReadOnlyList<UserAccessEntry>` before handing off to any export service.
|
||||
|
||||
Consolidation merges rows with the same `(UserLogin, SiteUrl, PermissionLevel)` key, collecting distinct `GrantedThrough` values into a semicolon-joined string. Add a `ConsolidateEntries(IReadOnlyList<UserAccessEntry>)` static helper in a shared location (e.g., `UserAccessEntryExtensions`). Toggle stored in `AppSettings` or passed as a flag at export time.
|
||||
|
||||
---
|
||||
|
||||
## No New NuGet Packages Required (v2.3)
|
||||
|
||||
| Feature | What's needed | How provided |
|
||||
|---|---|---|
|
||||
| Create app registration | `graphClient.Applications.PostAsync` | Microsoft.Graph 5.74.0 (existing) |
|
||||
| Create service principal | `graphClient.ServicePrincipals.PostAsync` | Microsoft.Graph 5.74.0 (existing) |
|
||||
| Delete app registration | `graphClient.Applications[id].DeleteAsync` | Microsoft.Graph 5.74.0 (existing) |
|
||||
| Grant app role consent | `graphClient.ServicePrincipals[id].AppRoleAssignments.PostAsync` | Microsoft.Graph 5.74.0 (existing) |
|
||||
| Add site collection admin | `new Tenant(ctx).SetSiteAdmin(...)` | PnP.Framework 1.18.0 (existing) |
|
||||
| Admin site context | `SessionManager.GetOrCreateAdminContextAsync` — new method, no new lib | PnP.Framework + MSAL (existing) |
|
||||
| Group member loading | `ctx.Load(spGroup.Users)` + `ExecuteQueryRetry` | PnP.Framework 1.18.0 (existing) |
|
||||
| HTML group expansion | `<details>/<summary>` HTML5 element | Plain HTML, BCL StringBuilder |
|
||||
| Consolidation logic | `GroupBy` + LINQ | BCL (.NET 10) |
|
||||
| Incremental Graph scopes | `MsalTokenProvider` with explicit scopes | MSAL 4.83.3 (existing) |
|
||||
|
||||
**Do NOT add:**
|
||||
|
||||
| Package | Reason to Skip |
|
||||
|---------|---------------|
|
||||
| `Azure.Identity` | App uses `Microsoft.Identity.Client` (MSAL) directly via `BaseBearerTokenAuthenticationProvider`. Azure.Identity would duplicate auth and conflict with the existing PCA + `MsalCacheHelper` token-cache-sharing pattern. |
|
||||
| PnP Core SDK | Distinct from PnP Framework (CSOM-based). Adding both creates confusion, ~15 MB extra weight, and no benefit since `Tenant.SetSiteAdmin` already exists in PnP.Framework 1.18.0. |
|
||||
| Any HTML template engine (Razor, Scriban) | StringBuilder pattern is established and sufficient. Template engines add complexity with no gain for server-side HTML generation. |
|
||||
| SignalR / polling / background service | Auto-ownership is a synchronous, on-demand CSOM call triggered by an access-denied event. No push infrastructure needed. |
|
||||
|
||||
---
|
||||
|
||||
## Version Bump Consideration (v2.3)
|
||||
|
||||
| Package | Current | Latest Stable | Recommendation |
|
||||
|---------|---------|--------------|----------------|
|
||||
| `Microsoft.Graph` | 5.74.0 | 5.103.0 | Optional. All new Graph API calls work on 5.74.0. Bump only if a specific bug is encountered. All 5.x versions maintain API compatibility. |
|
||||
| `PnP.Framework` | 1.18.0 | Check NuGet before bumping | Hold. `Tenant.SetSiteAdmin` works in 1.18.0. PnP Framework version bumps have historically introduced CSOM interop issues. Bump only with explicit testing. |
|
||||
|
||||
---
|
||||
|
||||
## Existing Stack (Unchanged)
|
||||
|
||||
The full stack as validated through v1.1:
|
||||
The full stack as validated through v2.2:
|
||||
|
||||
| Technology | Version | Purpose |
|
||||
|---|---|---|
|
||||
@@ -249,13 +436,13 @@ The full stack as validated through v1.1:
|
||||
| WPF | built-in | UI framework |
|
||||
| C# 13 | built-in | Language |
|
||||
| PnP.Framework | 1.18.0 | SharePoint CSOM, provisioning engine |
|
||||
| Microsoft.Graph | 5.74.0 | Graph API (users, Teams, Groups) |
|
||||
| Microsoft.Graph | 5.74.0 | Graph API (users, Teams, Groups, app management) |
|
||||
| Microsoft.Identity.Client (MSAL) | 4.83.3 | Multi-tenant auth, token acquisition |
|
||||
| Microsoft.Identity.Client.Extensions.Msal | 4.83.3 | Token cache persistence |
|
||||
| Microsoft.Identity.Client.Broker | 4.82.1 | Windows broker (WAM) |
|
||||
| CommunityToolkit.Mvvm | 8.4.2 | MVVM base classes, source generators |
|
||||
| Microsoft.Extensions.Hosting | 10.0.0 | DI container, app lifetime |
|
||||
| LiveCharts2 (SkiaSharpView.WPF) | 2.0.0-rc5.4 | Storage charts (in use, stable enough) |
|
||||
| LiveCharts2 (SkiaSharpView.WPF) | 2.0.0-rc5.4 | Storage charts |
|
||||
| Serilog | 4.3.1 | Structured logging |
|
||||
| Serilog.Extensions.Hosting | 10.0.0 | ILogger<T> bridge |
|
||||
| Serilog.Sinks.File | 7.0.0 | Rolling file output |
|
||||
@@ -268,8 +455,16 @@ The full stack as validated through v1.1:
|
||||
|
||||
## Sources
|
||||
|
||||
- Microsoft Learn — List users (Graph v1.0): https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-1.0 — permissions, $top max 999, $orderby with ConsistencyLevel, default fields (HIGH confidence, updated 2025-07-23)
|
||||
- Microsoft Learn — Page through a collection (Graph SDKs): https://learn.microsoft.com/en-us/graph/sdks/paging — PageIterator C# pattern, DirectoryPageTokenNotFoundException warning (HIGH confidence, updated 2025-08-06)
|
||||
- Microsoft Learn — Get organizationalBranding: https://learn.microsoft.com/en-us/graph/api/organizationalbranding-get?view=graph-rest-1.0 — branding stream retrieval via localizations/default/bannerLogo (HIGH confidence, updated 2025-11-08) — note: tenant branding pull is optional/future, not required for v2.2 which relies on user-supplied logo files
|
||||
- .NET Perls / BCL docs — Convert.ToBase64String + data URI pattern: confirmed BCL, no library needed (HIGH confidence)
|
||||
- Existing codebase inspection: GraphClientFactory.cs, GraphUserSearchService.cs, HtmlExportService.cs, UserAccessHtmlExportService.cs, TenantProfile.cs, AppSettings.cs — confirmed exact integration points
|
||||
**v2.2 sources:**
|
||||
- Microsoft Learn — List users (Graph v1.0): https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-1.0 — permissions, $top max 999, $orderby with ConsistencyLevel (HIGH confidence)
|
||||
- Microsoft Learn — Page through a collection (Graph SDKs): https://learn.microsoft.com/en-us/graph/sdks/paging — PageIterator C# pattern (HIGH confidence)
|
||||
|
||||
**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 — C# SDK 5.x pattern confirmed, `Application.ReadWrite.All` required (HIGH confidence, updated 2026-03-14)
|
||||
- Microsoft Learn — Create servicePrincipal (Graph v1.0): https://learn.microsoft.com/en-us/graph/api/serviceprincipal-post-serviceprincipals?view=graph-rest-1.0 — `Application.ReadWrite.All` required for multi-tenant apps (HIGH confidence, updated 2026-03-14)
|
||||
- Microsoft Learn — Delete application (Graph v1.0): https://learn.microsoft.com/en-us/graph/api/application-delete?view=graph-rest-1.0 — `graphClient.Applications[id].DeleteAsync()`, 204 response, 30-day soft-delete (HIGH confidence)
|
||||
- Microsoft Learn — Grant permissions programmatically: https://learn.microsoft.com/en-us/graph/permissions-grant-via-msgraph — `AppRoleAssignment.ReadWrite.All` for consent grants, C# SDK 5.x examples (HIGH confidence, updated 2026-03-21)
|
||||
- Microsoft Learn — Choose authentication providers: https://learn.microsoft.com/en-us/graph/sdks/choose-authentication-providers — Interactive provider pattern for desktop apps confirmed (HIGH confidence, updated 2025-08-06)
|
||||
- PnP Core SDK docs — Security/SetSiteCollectionAdmins: https://pnp.github.io/pnpcore/using-the-sdk/admin-sharepoint-security.html — tenant API bypasses site-level permission check, requires SharePoint Admin role (MEDIUM confidence — PnP Core docs; maps to PnP Framework `Tenant.SetSiteAdmin` behavior)
|
||||
- Microsoft.Graph NuGet package: https://www.nuget.org/packages/Microsoft.Graph/ — latest stable 5.103.0 confirmed 2026-02-20 (HIGH confidence)
|
||||
- Codebase — GraphClientFactory.cs, SessionManager.cs, MsalClientFactory.cs — confirmed existing `BaseBearerTokenAuthenticationProvider` + MSAL PCA integration pattern (HIGH confidence, source read directly)
|
||||
|
||||
Reference in New Issue
Block a user