308 lines
15 KiB
Markdown
308 lines
15 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<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>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/phases/02-permissions/02-RESEARCH.md
|
|
|
|
<interfaces>
|
|
<!-- Phase 1 helpers that PermissionsService MUST use. -->
|
|
|
|
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<ListItem> GetAllItemsAsync(
|
|
ClientContext ctx,
|
|
List list,
|
|
CamlQuery baseQuery,
|
|
IProgress<OperationProgress> 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<OperationProgress> 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<string> FilterPermissionLevels(IEnumerable<string> 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.
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Define data models and IPermissionsService interface</name>
|
|
<files>
|
|
SharepointToolbox/Core/Models/PermissionEntry.cs
|
|
SharepointToolbox/Core/Models/ScanOptions.cs
|
|
SharepointToolbox/Services/IPermissionsService.cs
|
|
</files>
|
|
<behavior>
|
|
- 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)
|
|
</behavior>
|
|
<action>
|
|
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: <name>"
|
|
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<IReadOnlyList<PermissionEntry>> ScanSiteAsync(
|
|
ClientContext ctx,
|
|
ScanOptions options,
|
|
IProgress<OperationProgress> progress,
|
|
CancellationToken ct);
|
|
}
|
|
```
|
|
</action>
|
|
<verify>
|
|
<automated>dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~PermissionsServiceTests" -x 2>&1 | tail -10</automated>
|
|
</verify>
|
|
<done>PermissionsServiceTests compiles (no CS0246 errors). Tests that reference IPermissionsService now skip cleanly rather than failing to compile. dotnet build produces 0 errors.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Implement PermissionsService scan engine</name>
|
|
<files>
|
|
SharepointToolbox/Services/PermissionsService.cs
|
|
</files>
|
|
<behavior>
|
|
- 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)
|
|
</behavior>
|
|
<action>
|
|
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`
|
|
</action>
|
|
<verify>
|
|
<automated>dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~PermissionsServiceTests|FullyQualifiedName~PermissionEntryClassificationTests" -x</automated>
|
|
</verify>
|
|
<done>Classification tests (3) still pass. PermissionsServiceTests skips cleanly (no compile errors). `dotnet build SharepointToolbox.slnx` succeeds with 0 errors. The service implements IPermissionsService.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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`)
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/02-permissions/02-02-SUMMARY.md`
|
|
</output>
|