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>
501 lines
29 KiB
Markdown
501 lines
29 KiB
Markdown
# 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
|
|
|
|
### Recommended Project Structure
|
|
|
|
```
|
|
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.
|
|
|
|
```csharp
|
|
// 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.
|
|
|
|
```csharp
|
|
// 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`.
|
|
|
|
```csharp
|
|
// 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.
|
|
|
|
```csharp
|
|
// 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:
|
|
```csharp
|
|
// 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.com` → `https://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.
|
|
|
|
```csharp
|
|
// 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.
|
|
|
|
### Pitfall 3: `SharingLinks` and system groups pollute output
|
|
|
|
**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)
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
// 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)
|
|
|
|
```csharp
|
|
// 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)
|
|
|
|
```csharp
|
|
// 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)
|