---
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&lt;IReadOnlyList&lt;PermissionEntry&gt;&gt;
    - 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>
