--- phase: 02-permissions plan: 02 type: execute wave: 1 depends_on: - 02-01 files_modified: - SharepointToolbox/Core/Models/PermissionEntry.cs - SharepointToolbox/Core/Models/ScanOptions.cs - SharepointToolbox/Services/IPermissionsService.cs - SharepointToolbox/Services/PermissionsService.cs autonomous: true requirements: - PERM-01 - PERM-03 - PERM-04 - PERM-07 must_haves: truths: - "PermissionsService.ScanSiteAsync returns at least one PermissionEntry for a site that has permission assignments (verified in test via mock)" - "With IncludeInherited=false, items where HasUniqueRoleAssignments=false produce zero PermissionEntry rows" - "External users (LoginName contains #EXT#) are represented with PrincipalType='External User' in the returned entries" - "Limited Access permission level is filtered out — entries containing only Limited Access are dropped entirely" - "System lists (App Packages, Workflow History, etc.) produce zero entries" - "Folder enumeration always uses SharePointPaginationHelper.GetAllItemsAsync — never raw list enumeration" artifacts: - path: "SharepointToolbox/Core/Models/PermissionEntry.cs" provides: "Flat record for one permission assignment" exports: ["PermissionEntry"] - path: "SharepointToolbox/Core/Models/ScanOptions.cs" provides: "Immutable scan configuration value object" exports: ["ScanOptions"] - path: "SharepointToolbox/Services/IPermissionsService.cs" provides: "Interface enabling ViewModel mocking" exports: ["IPermissionsService"] - path: "SharepointToolbox/Services/PermissionsService.cs" provides: "CSOM scan engine — port of PS Generate-PnPSitePermissionRpt" exports: ["PermissionsService"] key_links: - from: "PermissionsService.cs" to: "SharePointPaginationHelper.GetAllItemsAsync" via: "folder enumeration" pattern: "SharePointPaginationHelper\\.GetAllItemsAsync" - from: "PermissionsService.cs" to: "ExecuteQueryRetryHelper.ExecuteQueryRetryAsync" via: "CSOM round-trips" pattern: "ExecuteQueryRetryHelper\\.ExecuteQueryRetryAsync" - from: "PermissionsService.cs" to: "PermissionEntryHelper.IsExternalUser" via: "user classification" pattern: "PermissionEntryHelper\\.IsExternalUser" --- Create the core data models and the `PermissionsService` scan engine — a faithful C# port of the PowerShell `Generate-PnPSitePermissionRpt` / `Get-PnPPermissions` functions. This is the most technically dense plan in Phase 2; every other plan depends on these types and this service. Purpose: Establish the contracts (PermissionEntry, ScanOptions, IPermissionsService) that all subsequent plans build against, then implement the scan logic. Output: 4 files — 2 models, 1 interface, 1 service implementation. @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/phases/02-permissions/02-RESEARCH.md From SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs: ```csharp namespace SharepointToolbox.Core.Helpers; public static class SharePointPaginationHelper { // Yields all items in a SharePoint list using ListItemCollectionPosition pagination. // ALWAYS use this for folder/item enumeration — never raw list enumeration. public static async IAsyncEnumerable GetAllItemsAsync( ClientContext ctx, List list, CamlQuery baseQuery, IProgress progress, [EnumeratorCancellation] CancellationToken ct); } ``` From SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs: ```csharp namespace SharepointToolbox.Core.Helpers; public static class ExecuteQueryRetryHelper { // Executes ctx.ExecuteQueryAsync with automatic retry on 429/503. // ALWAYS use instead of ctx.ExecuteQueryAsync directly. public static async Task ExecuteQueryRetryAsync( ClientContext ctx, IProgress progress, CancellationToken ct); } ``` From SharepointToolbox/Core/Helpers/PermissionEntryHelper.cs (created in Plan 01): ```csharp namespace SharepointToolbox.Core.Helpers; public static class PermissionEntryHelper { public static bool IsExternalUser(string loginName); public static IReadOnlyList FilterPermissionLevels(IEnumerable levels); public static bool IsSharingLinksGroup(string loginName); } ``` From SharepointToolbox/Services/SessionManager.cs: ```csharp // ClientContext is obtained via SessionManager.GetOrCreateContextAsync(profile, ct) // PermissionsService receives an already-obtained ClientContext — it never calls SessionManager directly. ``` Task 1: Define data models and IPermissionsService interface SharepointToolbox/Core/Models/PermissionEntry.cs SharepointToolbox/Core/Models/ScanOptions.cs SharepointToolbox/Services/IPermissionsService.cs - PermissionEntry is a record with 9 string/bool positional fields matching the PS reference `$entry` object (ObjectType, Title, Url, HasUniquePermissions, Users, UserLogins, PermissionLevels, GrantedThrough, PrincipalType) - ScanOptions is a record with defaults: IncludeInherited=false, ScanFolders=true, FolderDepth=1, IncludeSubsites=false - IPermissionsService has exactly one method: ScanSiteAsync returning Task<IReadOnlyList<PermissionEntry>> - Existing Plan 01 test stubs that reference these types now compile (no more "type not found" errors) Create PermissionEntry.cs in `SharepointToolbox/Core/Models/`: ```csharp namespace SharepointToolbox.Core.Models; public record PermissionEntry( string ObjectType, // "Site Collection" | "Site" | "List" | "Folder" string Title, string Url, bool HasUniquePermissions, string Users, // Semicolon-joined display names string UserLogins, // Semicolon-joined login names string PermissionLevels, // Semicolon-joined role names (Limited Access already removed) string GrantedThrough, // "Direct Permissions" | "SharePoint Group: " string PrincipalType // "SharePointGroup" | "User" | "External User" ); ``` Create ScanOptions.cs in `SharepointToolbox/Core/Models/`: ```csharp namespace SharepointToolbox.Core.Models; public record ScanOptions( bool IncludeInherited = false, bool ScanFolders = true, int FolderDepth = 1, bool IncludeSubsites = false ); ``` Create IPermissionsService.cs in `SharepointToolbox/Services/`: ```csharp using Microsoft.SharePoint.Client; using SharepointToolbox.Core.Models; namespace SharepointToolbox.Services; public interface IPermissionsService { Task> ScanSiteAsync( ClientContext ctx, ScanOptions options, IProgress progress, CancellationToken ct); } ``` dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~PermissionsServiceTests" -x 2>&1 | tail -10 PermissionsServiceTests compiles (no CS0246 errors). Tests that reference IPermissionsService now skip cleanly rather than failing to compile. dotnet build produces 0 errors. Task 2: Implement PermissionsService scan engine SharepointToolbox/Services/PermissionsService.cs - ScanSiteAsync returns PermissionEntry rows for Site Collection admins, Web, Lists, and (if ScanFolders) Folders - With IncludeInherited=false: objects where HasUniqueRoleAssignments=false produce zero rows - With IncludeInherited=true: all objects regardless of inheritance produce rows - SharingLinks groups and "Limited Access System Group" are skipped entirely - Limited Access permission level is removed from PermissionLevels; if all levels removed, the row is dropped - External users (LoginName contains #EXT#) have PrincipalType="External User" - System lists (see ExcludedLists set) produce zero entries - Folder enumeration uses SharePointPaginationHelper.GetAllItemsAsync (never raw iteration) - Every CSOM round-trip uses ExecuteQueryRetryHelper.ExecuteQueryRetryAsync - CSOM Load uses batched Include() in one call per object (not N+1) Create `SharepointToolbox/Services/PermissionsService.cs`. This is a faithful port of PS `Generate-PnPSitePermissionRpt` and `Get-PnPPermissions` (PS reference lines 1361-1989). Class structure: ```csharp public class PermissionsService : IPermissionsService { // Port of PS lines 1914-1926 private static readonly HashSet 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" }; public async Task> ScanSiteAsync( ClientContext ctx, ScanOptions options, IProgress progress, CancellationToken ct) { ... } // Private: get site collection admins → PermissionEntry with ObjectType="Site Collection" private async Task> GetSiteCollectionAdminsAsync( ClientContext ctx, IProgress progress, CancellationToken ct) { ... } // Private: port of Get-PnPPermissions for a Web object private async Task> GetWebPermissionsAsync( ClientContext ctx, Web web, ScanOptions options, IProgress progress, CancellationToken ct) { ... } // Private: port of Get-PnPPermissions for a List object private async Task> GetListPermissionsAsync( ClientContext ctx, List list, ScanOptions options, IProgress progress, CancellationToken ct) { ... } // Private: enumerate folders in list via SharePointPaginationHelper, get permissions per folder private async Task> GetFolderPermissionsAsync( ClientContext ctx, List list, ScanOptions options, IProgress progress, CancellationToken ct) { ... } // Private: core per-object extractor — batched ctx.Load + ExecuteQueryRetryAsync private async Task> ExtractPermissionsAsync( ClientContext ctx, SecurableObject obj, string objectType, string title, string url, ScanOptions options, IProgress progress, CancellationToken ct) { ... } } ``` Implementation notes: - CSOM batched load pattern (one round-trip per object): ```csharp 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); ``` - Skip if !HasUniqueRoleAssignments when IncludeInherited=false - For each RoleAssignment: skip if IsSharingLinksGroup(Member.LoginName) - Build permission levels list, call FilterPermissionLevels, skip row if empty - Determine PrincipalType: if IsExternalUser(LoginName) → "External User"; else if Member.PrincipalType == PrincipalType.SharePointGroup → "SharePointGroup"; else → "User" - GrantedThrough: if PrincipalType is SharePointGroup → "SharePoint Group: {Member.Title}"; else → "Direct Permissions" - For Folder enumeration: CAML query is `` with ViewAttributes `Scope='RecursiveAll'` limited by FolderDepth (if FolderDepth != 999, filter by folder depth level) - Site collection admins: `ctx.Load(ctx.Web, w => w.SiteUsers)` then filter where `siteUser.IsSiteAdmin == true` - FolderDepth: folders at depth > options.FolderDepth are skipped (depth = URL segment count relative to list root) - ct must be checked via `ct.ThrowIfCancellationRequested()` at the start of each private method Namespace: `SharepointToolbox.Services` Usings: `Microsoft.SharePoint.Client`, `SharepointToolbox.Core.Models`, `SharepointToolbox.Core.Helpers` dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~PermissionsServiceTests|FullyQualifiedName~PermissionEntryClassificationTests" -x Classification tests (3) still pass. PermissionsServiceTests skips cleanly (no compile errors). `dotnet build SharepointToolbox.slnx` succeeds with 0 errors. The service implements IPermissionsService. - `dotnet build C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.slnx` → 0 errors - `dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.slnx` → all Phase 1 tests pass, classification tests pass, new stubs skip - PermissionsService.cs references SharePointPaginationHelper.GetAllItemsAsync for folder enumeration (grep verifiable) - PermissionsService implements IPermissionsService (grep: `class PermissionsService : IPermissionsService`) - PermissionEntry, ScanOptions, IPermissionsService defined and exported - PermissionsService fully implements the scan logic (all 5 scan paths: site collection admins, web, lists, folders, subsites) - All Phase 1 tests remain green - CsvExportServiceTests and HtmlExportServiceTests now compile (they reference PermissionEntry which exists) After completion, create `.planning/phases/02-permissions/02-02-SUMMARY.md`