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>
This commit is contained in:
307
.planning/milestones/v1.0-phases/02-permissions/02-02-PLAN.md
Normal file
307
.planning/milestones/v1.0-phases/02-permissions/02-02-PLAN.md
Normal file
@@ -0,0 +1,307 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user