Files
Sharepoint-Toolbox/.planning/milestones/v1.0-phases/02-permissions/02-RESEARCH.md
Dev 724fdc550d chore: complete v1.0 milestone
Archive 5 phases (36 plans) to milestones/v1.0-phases/.
Archive roadmap, requirements, and audit to milestones/.
Evolve PROJECT.md with shipped state and validated requirements.
Collapse ROADMAP.md to one-line milestone summary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:19:03 +02:00

29 KiB

Phase 2: Permissions - Research

Researched: 2026-04-02 Domain: SharePoint CSOM/PnP.Framework permissions scanning, WPF DataGrid + ListView, CSV/HTML export Confidence: HIGH


<phase_requirements>

Phase Requirements

ID Description Research Support
PERM-01 User can scan permissions on a single SharePoint site with configurable depth CSOM Web.RoleAssignments, HasUniqueRoleAssignments — depth controlled by folder-level filter; PermissionsService.ScanSiteAsync
PERM-02 User can scan permissions across multiple selected sites in one operation Site picker dialog (SitePickerDialog) calls Get-PnPTenantSite equivalent via CSOM Tenant API; loop calls ScanSiteAsync per URL
PERM-03 Permissions scan includes owners, members, guests, external users, and broken inheritance Web.SiteUsers, SiteCollectionAdmin flag, RoleAssignment.Member.PrincipalType, IsGuestUser, external = #ext# in LoginName
PERM-04 User can choose to include or exclude inherited permissions HasUniqueRoleAssignments guard already present in PS reference; ViewModel scan option IncludeInherited bool
PERM-05 User can export permissions report to CSV (raw data) CsvExportService using System.Text writer — no third-party library needed
PERM-06 User can export permissions report to interactive HTML (sortable, filterable, groupable by user) Self-contained HTML with vanilla JS — exact pattern ported from PS reference Export-PermissionsToHTML
PERM-07 SharePoint 5,000-item list view threshold handled via pagination — no silent failures on large libraries SharePointPaginationHelper.GetAllItemsAsync already built in Phase 1 — mandatory use
</phase_requirements>

Summary

Phase 2 builds the first real feature on top of the Phase 1 infrastructure. The technical domain is SharePoint CSOM permissions scanning via PnP.Framework 1.18.0 (already a project dependency), WPF UI for the Permissions tab, and file export (CSV + self-contained HTML).

The reference PowerShell script (Sharepoint_ToolBox.ps1) contains a complete, working implementation of every piece needed: Generate-PnPSitePermissionRpt (scan engine), Get-PnPPermissions (per-object extractor), Export-PermissionsToHTML (HTML report), and Merge-PermissionRows (CSV merge). The C# port is primarily a faithful translation of that logic — not a design problem.

The largest technical risk is the multi-site scan: the site picker requires calling the SharePoint Online Tenant API (Microsoft.Online.SharePoint.TenantAdministration.Tenant) via the -admin URL, which requires admin consent on the Azure app registration. The per-site scan (PERM-01) has no such dependency. The multi-site path (PERM-02) must connect to https://tenant-admin.sharepoint.com rather than the regular tenant URL.

Primary recommendation: Port the PS reference logic directly into a PermissionsService class; use SharePointPaginationHelper for all folder enumeration; generate HTML as a string resource embedded in the assembly so no file-system template is needed.


Standard Stack

Core

Library Version Purpose Why Standard
PnP.Framework 1.18.0 CSOM wrapper — ClientContext, Web, List, RoleAssignment Already in project; gives ExecuteQueryAsync and all SharePoint client objects
Microsoft.SharePoint.Client (bundled with PnP.Framework) CSOM types: Web, List, ListItem, RoleAssignment, RoleDefinitionBindingCollection, PrincipalType The actual API surface for permissions
System.Text (built-in) .NET 10 CSV generation via StringBuilder No dependency needed; CSV is flat text
CommunityToolkit.Mvvm 8.4.2 [ObservableProperty], AsyncRelayCommand, ObservableRecipient Already in project; all VMs use it

Supporting

Library Version Purpose When to Use
Microsoft.Win32.SaveFileDialog (built-in WPF) File save dialog for CSV/HTML export When user clicks "Save Report"
System.Diagnostics.Process (built-in) Open exported file in browser/Excel "Open Report" button

Alternatives Considered

Instead of Could Use Tradeoff
String-built HTML export RazorLight or T4 Overkill for a single-template report; adds dependency; the PS reference proves a self-contained string approach is maintainable
CsvHelper for CSV System.Text manual CsvHelper is the standard but adds a NuGet dep; the PS reference Export-Csv proves the schema is simple enough for manual construction

Installation: No new packages required. All dependencies are already in SharepointToolbox.csproj.


Architecture Patterns

SharepointToolbox/
├── Core/
│   └── Models/
│       ├── PermissionEntry.cs         # Data model for one permission row
│       └── ScanOptions.cs             # IncludeInherited, ScanFolders, FolderDepth, IncludeSubsites
├── Services/
│   ├── PermissionsService.cs          # Scan engine — calls CSOM, yields PermissionEntry
│   ├── SiteListService.cs             # Loads tenant site list via Tenant admin API
│   └── Export/
│       ├── CsvExportService.cs        # Writes PermissionEntry[] → CSV file
│       └── HtmlExportService.cs       # Writes PermissionEntry[] → self-contained HTML
├── ViewModels/
│   └── Tabs/
│       └── PermissionsViewModel.cs    # FeatureViewModelBase subclass
└── Views/
    ├── Tabs/
    │   └── PermissionsView.xaml       # Replaces FeatureTabBase stub in MainWindow
    └── Dialogs/
        └── SitePickerDialog.xaml      # Multi-site selection dialog

Pattern 1: PermissionEntry data model

What: A flat record that represents one permission assignment on one object (site, library, folder). Mirrors the PS $entry object exactly.

When to use: All scan output is typed as IReadOnlyList<PermissionEntry> — service produces it, export services consume it.

// Core/Models/PermissionEntry.cs
public record PermissionEntry(
    string ObjectType,          // "Site Collection" | "Site" | "List" | "Folder"
    string Title,               // Display name
    string Url,                 // Direct link
    bool   HasUniquePermissions,
    string Users,               // Semicolon-joined display names
    string UserLogins,          // Semicolon-joined emails/login names
    string PermissionLevels,    // Semicolon-joined role names (excluding "Limited Access")
    string GrantedThrough,      // "Direct Permissions" | "SharePoint Group: <name>"
    string PrincipalType        // "SharePointGroup" | "User" | "SharePointGroup" etc.
);

Pattern 2: ScanOptions value object

What: Immutable options passed to PermissionsService. Replaces the PS script globals.

// Core/Models/ScanOptions.cs
public record ScanOptions(
    bool   IncludeInherited   = false,
    bool   ScanFolders        = true,
    int    FolderDepth        = 1,      // 999 = unlimited (mirrors PS $PermFolderDepth)
    bool   IncludeSubsites    = false
);

Pattern 3: PermissionsService scan engine

What: Async method that scans one ClientContext site and yields entries. Multi-site scanning is a loop in the ViewModel calling this per site.

When to use: Called once per site URL. Callers pass the ClientContext from SessionManager.

// Services/PermissionsService.cs
public class PermissionsService
{
    // Returns all PermissionEntry rows for one site.
    // Always uses SharePointPaginationHelper for folder enumeration.
    public async Task<IReadOnlyList<PermissionEntry>> ScanSiteAsync(
        ClientContext ctx,
        ScanOptions options,
        IProgress<OperationProgress> progress,
        CancellationToken ct);
}

Internal structure mirrors the PS reference exactly:

  1. Load site collection admins → emit one PermissionEntry with ObjectType = "Site Collection"
  2. Call GetWebPermissions(ctx.Web) which calls GetPermissionsForObject(web)
  3. GetListPermissions(web) — iterate non-hidden, non-system lists
  4. If ScanFolders: call GetFolderPermissions(list) using SharePointPaginationHelper.GetAllItemsAsync
  5. If IncludeSubsites: recurse into web.Webs

Pattern 4: CSOM load pattern for permissions

What: The CSOM pattern for reading RoleAssignments requires explicit ctx.Load + ExecuteQueryAsync for each level. This is the exact translation of the PS Get-PnPProperty calls.

// Source: PnP.Framework CSOM patterns (verified against PS reference lines 1807-1848)
ctx.Load(obj, o => o.HasUniqueRoleAssignments, o => o.RoleAssignments.Include(
    ra => ra.Member.Title,
    ra => ra.Member.Email,
    ra => ra.Member.LoginName,
    ra => ra.Member.PrincipalType,
    ra => ra.RoleDefinitionBindings.Include(rdb => rdb.Name)
));
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);

bool hasUnique = obj.HasUniqueRoleAssignments;
foreach (var ra in obj.RoleAssignments)
{
    // ra.Member.PrincipalType, ra.RoleDefinitionBindings are populated
}

Critical: Load the Include expression in ONE ctx.Load call rather than multiple round-trips. The PS script calls Get-PnPProperty multiple times (one per property) which is N+1. The C# version should batch into one load.

Pattern 5: SitePickerDialog (multi-site, PERM-02)

What: A WPF Window with a ListView (checkboxes), filter textbox, "Load Sites", "Select All", "Deselect All", OK/Cancel. Mirrors the PS Show-SitePicker function.

Loading tenant sites: Requires connecting to https://{tenant}-admin.sharepoint.com and calling:

// Requires Microsoft.Online.SharePoint.TenantAdministration.Tenant — included in PnP.Framework
var tenantCtx = new ClientContext(adminUrl);
// Auth via SessionManager using admin URL
var tenant = new Tenant(tenantCtx);
var siteProps = tenant.GetSitePropertiesFromSharePoint("", true);
tenantCtx.Load(siteProps);
await tenantCtx.ExecuteQueryAsync();

Admin URL derivation: https://contoso.sharepoint.comhttps://contoso-admin.sharepoint.com. Pattern from PS line 333: replace .sharepoint.com with -admin.sharepoint.com.

IMPORTANT: The user must have SharePoint admin rights for this to work. Auth uses the same SessionManager.GetOrCreateContextAsync with the admin URL (a different key from the regular tenant URL).

Pattern 6: HTML export — self-contained string

What: The HTML report is generated as a C# string (embedded resource template or string builder), faithful port of Export-PermissionsToHTML. No file template on disk.

Key features to preserve from PS reference:

  • Stats cards: Total Entries, Unique Permission Sets, Distinct Users/Groups
  • Filter input (vanilla JS filterTable())
  • Collapsible SharePoint Group member lists (grp-tog/grp-members CSS toggle)
  • User pills with data-email for context menu (copy email, mailto)
  • Type badges: color-coded for Site Collection / Site / List / Folder
  • Unique vs Inherited badge per row

The HTML template is ~200 lines of CSS + HTML + ~50 lines JS. Store as a const string in HtmlExportService or as an embedded .html resource file.

Pattern 7: CSV export — merge rows first

What: Mirrors Merge-PermissionRows from PS: rows with identical Users|PermissionLevels|GrantedThrough are merged, collecting all their locations into a pipe-joined string.

// Services/Export/CsvExportService.cs
// CSV columns: Object, Title, URL, HasUniquePermissions, Users, UserLogins, Type, Permissions, GrantedThrough
// Merge before writing: group by (Users, PermissionLevels, GrantedThrough), join locations with " | "

Anti-Patterns to Avoid

  • Multiple ExecuteQuery calls per object: Load RoleAssignments with full Include() in one round-trip, not sequential Load+Execute per property (the N+1 problem the PS script has).
  • Storing ClientContext in the ViewModel: ViewModel calls SessionManager.GetOrCreateContextAsync at scan start, passes it to service, does not cache it.
  • Modifying ObservableCollection from background thread: Accumulate in List<PermissionEntry> during scan, assign as new ObservableCollection<PermissionEntry>(list) via Dispatcher.InvokeAsync after completion.
  • Silent Limited Access inclusion: Filter out Limited Access from RoleDefinitionBindings — PS reference line 1814 does this; C# port must too.
  • Scanning system lists: Use the same ExcludedLists array from PS line 1914-1926. Failure to exclude them causes noise in output (App Packages, Workflow History, etc.).
  • Direct ctx.ExecuteQueryAsync() on folder lists: MUST go through SharePointPaginationHelper.GetAllItemsAsync. Never raw enumerate a list.

Don't Hand-Roll

Problem Don't Build Use Instead Why
SharePoint 5,000-item pagination Custom CAML loop SharePointPaginationHelper.GetAllItemsAsync Already built and tested in Phase 1
Throttle retry Custom retry loop ExecuteQueryRetryHelper.ExecuteQueryRetryAsync Already built and tested in Phase 1
Async command + progress + cancel Custom ICommand FeatureViewModelBase + AsyncRelayCommand Pattern established in Phase 1
CSV escaping Manual replace string.Format with double-quote wrapping + escape internal quotes Standard CSV: "value with ""quotes"""

Key insight: The entire Phase 1 infrastructure was built specifically to be reused here. PermissionsService should be a pure service that takes a ClientContext and returns data — it never touches UI. The ViewModel handles threading.


Common Pitfalls

Pitfall 1: Tenant Admin URL for site listing

What goes wrong: Connecting to https://contoso.sharepoint.com and calling the Tenant API returns "Access denied" or throws. Why it happens: The Tenant class in Microsoft.Online.SharePoint.TenantAdministration requires connecting to the -admin URL. How to avoid: Derive admin URL: Regex.Replace(tenantUrl, @"(https://[^.]+)(\.sharepoint\.com.*)", "$1-admin$2"). SessionManager treats the admin URL as a separate key — it will trigger a new interactive login if not already cached. Warning signs: ServerException: Access denied or 401 on Tenant.GetSitePropertiesFromSharePoint.

Pitfall 2: RoleAssignments not loaded — empty collection silently

What goes wrong: Iterating obj.RoleAssignments produces 0 items even though the site has permissions. Why it happens: CSOM lazy loading — RoleAssignments is not populated unless explicitly loaded with ctx.Load. How to avoid: Always use the batched ctx.Load(obj, o => o.HasUniqueRoleAssignments, o => o.RoleAssignments.Include(...)) pattern before ExecuteQueryAsync. Warning signs: Empty output for sites that definitely have permissions.

What goes wrong: The report shows SharingLinks.{GUID} entries or "Limited Access System Group" as users. Why it happens: SharePoint creates these internal groups for link sharing. They appear as SharePointGroup principals. How to avoid: Skip role assignments where Member.LoginName matches ^SharingLinks\. or equals Limited Access System Group. PS reference line 1831. Warning signs: Output contains rows with GUIDs in the Users column.

Pitfall 4: Limited Access permission level is noise

What goes wrong: Users who only have "Limited Access" (implicit from accessing a subsite/item) appear as full permission entries. Why it happens: SharePoint auto-grants "Limited Access" on parent objects when a user has explicit access to a child item. How to avoid: After building PermissionLevels list from RoleDefinitionBindings.Name, filter out "Limited Access". If the resulting list is empty, skip the entire row. PS reference lines 1813-1815. Warning signs: Hundreds of extra rows with only "Limited Access" listed.

Pitfall 5: External user detection

What goes wrong: External users are not separately classified; they appear as regular users. Why it happens: SharePoint external users have #EXT# in their LoginName (e.g., user_domain.com#EXT#@tenant.onmicrosoft.com). PrincipalType is still User. How to avoid: Check loginName.Contains("#EXT#", StringComparison.OrdinalIgnoreCase) to tag user as external. PERM-03 requires external users be identifiable — this is the detection mechanism. Warning signs: PERM-03 acceptance test can't distinguish external from internal users.

Pitfall 6: Multi-site scan — wrong ClientContext per site

What goes wrong: All sites scanned using the same ClientContext from the first site, so permissions returned are from the wrong site. Why it happens: ClientContext is URL-specific. Reusing one context to query another site URL gives wrong or empty results. How to avoid: Call SessionManager.GetOrCreateContextAsync(profile with siteUrl) for each site URL in the multi-site loop. Each site gets its own context from SessionManager's cache. Warning signs: All sites in multi-scan show identical permissions matching only the first site.

Pitfall 7: PermissionsView replaces the FeatureTabBase stub

What goes wrong: Permissions tab still shows "Coming soon" after implementing the ViewModel. Why it happens: MainWindow.xaml has <controls:FeatureTabBase /> as a stub placeholder for the Permissions tab. How to avoid: Replace that <controls:FeatureTabBase /> with <views:PermissionsView /> in MainWindow.xaml. Register PermissionsViewModel in DI. Wire DataContext in code-behind. Warning signs: Running the app shows "Coming soon" on the Permissions tab.


Code Examples

CSOM load for permissions (batched, one round-trip per object)

// Source: PS reference lines 1807-1848, translated to CSOM Include() pattern
ctx.Load(web,
    w => w.HasUniqueRoleAssignments,
    w => w.RoleAssignments.Include(
        ra => ra.Member.Title,
        ra => ra.Member.Email,
        ra => ra.Member.LoginName,
        ra => ra.Member.PrincipalType,
        ra => ra.RoleDefinitionBindings.Include(rdb => rdb.Name)));
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);

Admin URL derivation

// Source: PS reference line 333
static string DeriveAdminUrl(string tenantUrl)
{
    // https://contoso.sharepoint.com  →  https://contoso-admin.sharepoint.com
    return Regex.Replace(
        tenantUrl.TrimEnd('/'),
        @"(https://[^.]+)(\.sharepoint\.com)",
        "$1-admin$2",
        RegexOptions.IgnoreCase);
}

External user detection

// Source: SharePoint Online behavior — external users always have #EXT# in LoginName
static bool IsExternalUser(string loginName)
    => loginName.Contains("#EXT#", StringComparison.OrdinalIgnoreCase);

System list exclusion list (port from PS reference line 1914)

// Source: PS reference lines 1914-1926
private static readonly HashSet<string> ExcludedLists = new(StringComparer.OrdinalIgnoreCase)
{
    "Access Requests", "App Packages", "appdata", "appfiles", "Apps in Testing",
    "Cache Profiles", "Composed Looks", "Content and Structure Reports",
    "Content type publishing error log", "Converted Forms", "Device Channels",
    "Form Templates", "fpdatasources", "List Template Gallery",
    "Long Running Operation Status", "Maintenance Log Library", "Images",
    "site collection images", "Master Docs", "Master Page Gallery", "MicroFeed",
    "NintexFormXml", "Quick Deploy Items", "Relationships List", "Reusable Content",
    "Reporting Metadata", "Reporting Templates", "Search Config List", "Site Assets",
    "Preservation Hold Library", "Site Pages", "Solution Gallery", "Style Library",
    "Suggested Content Browser Locations", "Theme Gallery", "TaxonomyHiddenList",
    "User Information List", "Web Part Gallery", "wfpub", "wfsvc",
    "Workflow History", "Workflow Tasks", "Pages"
};

CSV row building (with proper escaping)

// Source: CSV RFC 4180 — enclose all fields in quotes, escape internal quotes by doubling
static string CsvField(string value)
{
    if (string.IsNullOrEmpty(value)) return "\"\"";
    return $"\"{value.Replace("\"", "\"\"")}\"";
}

Localization keys needed (new keys for Phase 2)

Based on PS reference Sharepoint_ToolBox.ps1 lines 2751-2761, these keys need adding to Strings.resx:

grp.scan.opts          = "Scan Options"
chk.scan.folders       = "Scan Folders"
chk.recursive          = "Recursive (subsites)"
lbl.folder.depth       = "Folder depth:"
chk.max.depth          = "Maximum (all levels)"
chk.inherited.perms    = "Include Inherited Permissions"
grp.export.fmt         = "Export Format"
rad.csv.perms          = "CSV"
rad.html.perms         = "HTML"
btn.gen.perms          = "Generate Report"
btn.open.perms         = "Open Report"
btn.view.sites         = "View Sites"
perm.site.url          = "Site URL:"
perm.or.select         = "or select multiple sites:"
perm.sites.selected    = "{0} site(s) selected"

State of the Art

Old Approach Current Approach When Changed Impact
PnP.PowerShell Get-PnPSite / Get-PnPProperty CSOM ClientContext.Load + Include() expressions Always — C# uses CSOM directly More efficient: one round-trip per object instead of N PnP cmdlet calls
PS Export-Csv (flat rows) Merge rows by user+permission+grantedThrough, then export Same as PS reference Deduplicated report — one row per user/permission combination covering multiple locations

No deprecated items: PnP.Framework 1.18.0 (the project's chosen library) remains the current stable CSOM wrapper for .NET. The CSOM patterns used are long-stable.


Open Questions

  1. Tenant admin consent for site listing (PERM-02)

    • What we know: The PS script uses Get-PnPTenantSite which requires the user to be a SharePoint admin and connects to {tenant}-admin.sharepoint.com
    • What's unclear: The Azure app registration's required permissions. The PS script uses -Interactive login with the same ClientId — if the admin user consents during login, it works. The C# app uses the same interactive MSAL flow.
    • Recommendation: Plan the SitePickerDialog to catch ServerException with "Access denied" and surface a clear message: "Site listing requires SharePoint administrator permissions. Connect with an admin account." Do not fail silently.
  2. Guest user classification boundary

    • What we know: #EXT# in LoginName = external. IsGuestUser property exists on User object in CSOM but requires additional load.
    • What's unclear: The exact PERM-03 acceptance criteria for "guests" — is it #EXT# detection sufficient, or does it require User.IsGuestUser?
    • Recommendation: Use #EXT# detection as the primary external user flag (matches PS reference behavior). The Type field in PermissionEntry can carry "External User" when detected. Verify acceptance criteria during plan review.
  3. WPF DataGrid vs ListView for results display

    • What we know: Phase 1 UI uses simple controls. Results can be large (thousands of rows). WPF DataGrid provides built-in column sorting; ListView with GridView is lighter-weight.
    • What's unclear: Virtualization requirements — with 10,000+ rows, DataGrid needs VirtualizingPanel.IsVirtualizing="True" (which is default) and EnableRowVirtualization="True".
    • Recommendation: Use WPF DataGrid with VirtualizingStackPanel (default). It handles large result sets with virtualization enabled. Do not use a plain ListBox or ListView.

Validation Architecture

Test Framework

Property Value
Framework xUnit 2.9.3
Config file none — runner picks up via xunit.runner.visualstudio
Quick run command dotnet test SharepointToolbox.Tests\SharepointToolbox.Tests.csproj -x
Full suite command dotnet test SharepointToolbox.slnx

Phase Requirements → Test Map

Req ID Behavior Test Type Automated Command File Exists?
PERM-01 PermissionsService.ScanSiteAsync returns entries for a mocked web unit dotnet test --filter "FullyQualifiedName~PermissionsServiceTests" Wave 0
PERM-02 Multi-site loop in ViewModel calls service once per site URL unit dotnet test --filter "FullyQualifiedName~PermissionsViewModelTests" Wave 0
PERM-03 External user detection: #EXT# in login name → classified correctly unit dotnet test --filter "FullyQualifiedName~PermissionEntryClassificationTests" Wave 0
PERM-04 With IncludeInherited=false, items with HasUniqueRoleAssignments=false are skipped unit dotnet test --filter "FullyQualifiedName~PermissionsServiceTests" Wave 0
PERM-05 CsvExportService produces correct CSV text for known input unit dotnet test --filter "FullyQualifiedName~CsvExportServiceTests" Wave 0
PERM-06 HtmlExportService produces HTML containing expected user names unit dotnet test --filter "FullyQualifiedName~HtmlExportServiceTests" Wave 0
PERM-07 SharePointPaginationHelper already tested in Phase 1 — pagination used in folder scan unit (existing) dotnet test --filter "FullyQualifiedName~SharePointPaginationHelperTests" (Phase 1)

Note on CSOM service testing: PermissionsService uses a live ClientContext. Unit tests should use an interface IPermissionsService with a mock for ViewModel tests. The concrete service itself is covered by the existing project convention of marking live-SharePoint tests as [Trait("Category", "Integration")] and Skip-ping them in the automated suite (same pattern as GetOrCreateContextAsync_CreatesContext).

Sampling Rate

  • Per task commit: dotnet test SharepointToolbox.Tests\SharepointToolbox.Tests.csproj -x
  • Per wave merge: dotnet test SharepointToolbox.slnx
  • Phase gate: Full suite green before /gsd:verify-work

Wave 0 Gaps

  • SharepointToolbox.Tests/Services/PermissionsServiceTests.cs — covers PERM-01, PERM-04 (via mock ClientContext wrapper interface)
  • SharepointToolbox.Tests/ViewModels/PermissionsViewModelTests.cs — covers PERM-02 (multi-site loop)
  • SharepointToolbox.Tests/Services/PermissionEntryClassificationTests.cs — covers PERM-03 (external user, principal type classification)
  • SharepointToolbox.Tests/Services/Export/CsvExportServiceTests.cs — covers PERM-05
  • SharepointToolbox.Tests/Services/Export/HtmlExportServiceTests.cs — covers PERM-06
  • Interface IPermissionsService — needed for ViewModel mocking

Sources

Primary (HIGH confidence)

  • Sharepoint_ToolBox.ps1 lines 1361-1989 — Complete working reference implementation of permissions scan, merge, CSV and HTML export
  • SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs — Pagination helper already built in Phase 1, mandatory for PERM-07
  • SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs — Retry helper already built in Phase 1
  • SharepointToolbox/ViewModels/FeatureViewModelBase.cs — Base class all feature VMs extend
  • SharepointToolbox/Services/SessionManager.cs — Single source of ClientContext objects
  • SharepointToolbox/SharepointToolbox.csproj — Confirmed PnP.Framework 1.18.0, no new packages needed
  • SharepointToolbox/MainWindow.xaml — Confirmed Permissions tab is currently <controls:FeatureTabBase /> stub
  • Sharepoint_ToolBox.ps1 lines 2751-2761 — All localization keys for Permissions tab UI controls

Secondary (MEDIUM confidence)

  • PS reference lines 333, 398, 1864 — Admin URL derivation pattern (-admin.sharepoint.com for Tenant API)
  • PS reference lines 1914-1926 — System list exclusion list (verified complete set used in production)
  • PS reference lines 1831 — SharingLinks group filtering (production-verified pattern)

Tertiary (LOW confidence)

  • Microsoft.Online.SharePoint.TenantAdministration.Tenant API availability in PnP.Framework 1.18.0 — assumed included based on PnP.Framework scope, not explicitly verified in package contents. If not available, fallback is the Microsoft Graph sites API which requires different auth scopes.

Metadata

Confidence breakdown:

  • Standard stack: HIGH — all packages already in project, no new deps needed
  • Architecture: HIGH — PS reference is a complete working blueprint; translation is straightforward
  • Pitfalls: HIGH — sourced directly from production PS code behavior and CSOM known patterns
  • Tenant API (multi-site): MEDIUM — admin URL pattern confirmed from PS but Tenant class availability in the exact PnP.Framework version not inspected in nuget package manifest

Research date: 2026-04-02 Valid until: 2026-05-02 (PnP.Framework 1.18.0 is stable; no expected breaking changes in 30 days)