15 KiB
15 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-permissions | 02 | execute | 1 |
|
|
true |
|
|
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.
<execution_context> @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/phases/02-permissions/02-RESEARCH.mdFrom SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs:
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<ListItem> GetAllItemsAsync(
ClientContext ctx,
List list,
CamlQuery baseQuery,
IProgress<OperationProgress> progress,
[EnumeratorCancellation] CancellationToken ct);
}
From SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs:
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<OperationProgress> progress,
CancellationToken ct);
}
From SharepointToolbox/Core/Helpers/PermissionEntryHelper.cs (created in Plan 01):
namespace SharepointToolbox.Core.Helpers;
public static class PermissionEntryHelper
{
public static bool IsExternalUser(string loginName);
public static IReadOnlyList<string> FilterPermissionLevels(IEnumerable<string> levels);
public static bool IsSharingLinksGroup(string loginName);
}
From SharepointToolbox/Services/SessionManager.cs:
// ClientContext is obtained via SessionManager.GetOrCreateContextAsync(profile, ct)
// PermissionsService receives an already-obtained ClientContext — it never calls SessionManager directly.
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<IReadOnlyList<PermissionEntry>> ScanSiteAsync(
ClientContext ctx,
ScanOptions options,
IProgress<OperationProgress> 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<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" };
public async Task<IReadOnlyList<PermissionEntry>> ScanSiteAsync(
ClientContext ctx, ScanOptions options,
IProgress<OperationProgress> progress, CancellationToken ct)
{ ... }
// Private: get site collection admins → PermissionEntry with ObjectType="Site Collection"
private async Task<IEnumerable<PermissionEntry>> GetSiteCollectionAdminsAsync(
ClientContext ctx, IProgress<OperationProgress> progress, CancellationToken ct) { ... }
// Private: port of Get-PnPPermissions for a Web object
private async Task<IEnumerable<PermissionEntry>> GetWebPermissionsAsync(
ClientContext ctx, Web web, ScanOptions options,
IProgress<OperationProgress> progress, CancellationToken ct) { ... }
// Private: port of Get-PnPPermissions for a List object
private async Task<IEnumerable<PermissionEntry>> GetListPermissionsAsync(
ClientContext ctx, List list, ScanOptions options,
IProgress<OperationProgress> progress, CancellationToken ct) { ... }
// Private: enumerate folders in list via SharePointPaginationHelper, get permissions per folder
private async Task<IEnumerable<PermissionEntry>> GetFolderPermissionsAsync(
ClientContext ctx, List list, ScanOptions options,
IProgress<OperationProgress> progress, CancellationToken ct) { ... }
// Private: core per-object extractor — batched ctx.Load + ExecuteQueryRetryAsync
private async Task<IEnumerable<PermissionEntry>> ExtractPermissionsAsync(
ClientContext ctx, SecurableObject obj, string objectType, string title,
string url, ScanOptions options,
IProgress<OperationProgress> 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 `<OrderBy><FieldRef Name='ID'/></OrderBy>` 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`)
<success_criteria>
- 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) </success_criteria>