Files
Sharepoint-Toolbox/.planning/milestones/v1.0-phases/02-permissions/02-04-PLAN.md
Dev 724fdc550d 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>
2026-04-07 09:19:03 +02:00

12 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 04 execute 2
02-02
SharepointToolbox/Services/Export/CsvExportService.cs
SharepointToolbox/Services/Export/HtmlExportService.cs
true
PERM-05
PERM-06
truths artifacts key_links
CsvExportService.BuildCsv produces a valid CSV string with the correct 9-column header and one data row per merged permission entry
Entries with identical Users + PermissionLevels + GrantedThrough but different URLs are merged into one row with pipe-joined URLs (Merge-PermissionRows port)
HtmlExportService.BuildHtml produces a self-contained HTML file (no external CSS/JS dependencies) that contains all user display names from the input
HTML report includes stats cards: Total Entries, Unique Permission Sets, Distinct Users/Groups
CSV fields with commas or quotes are correctly escaped per RFC 4180
path provides exports
SharepointToolbox/Services/Export/CsvExportService.cs Merges PermissionEntry rows and writes CSV
CsvExportService
path provides exports
SharepointToolbox/Services/Export/HtmlExportService.cs Generates self-contained interactive HTML report
HtmlExportService
from to via pattern
CsvExportService.cs PermissionEntry groups by (Users, PermissionLevels, GrantedThrough) GroupBy
from to via pattern
HtmlExportService.cs PermissionEntry iterates all entries to build HTML rows foreach.*PermissionEntry
Implement the two export services: `CsvExportService` (port of PS `Merge-PermissionRows` + `Export-Csv`) and `HtmlExportService` (port of PS `Export-PermissionsToHTML`). Both services consume `IReadOnlyList` and write files.

Purpose: Deliver PERM-05 (CSV export) and PERM-06 (HTML export). These are pure data-transformation services with no UI dependency — they can be verified fully by the automated test stubs created in Plan 01. Output: 2 export service files.

<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.md From SharepointToolbox/Core/Models/PermissionEntry.cs: ```csharp 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 string GrantedThrough, // "Direct Permissions" | "SharePoint Group: " string PrincipalType // "SharePointGroup" | "User" | "External User" ); ```

CSV merge logic (port of PS Merge-PermissionRows):

  • Group by key: (Users, PermissionLevels, GrantedThrough)
  • For each group: collect all Urls, join with " | "
  • Collect all Titles, join with " | "
  • Take first ObjectType, HasUniquePermissions from group

CSV columns (9 total): Object, Title, URL, HasUniquePermissions, Users, UserLogins, Type, Permissions, GrantedThrough CSV escaping: enclose every field in double quotes, escape internal quotes by doubling them.

HTML report key features (port of PS Export-PermissionsToHTML):

  • Stats cards: Total Entries (count of entries), Unique Permission Sets (count of distinct PermissionLevels values), Distinct Users/Groups (count of distinct users across all UserLogins)
  • Filter input (vanilla JS filterTable())
  • Type badge: color-coded span for ObjectType ("Site Collection"=blue, "Site"=green, "List"=yellow, "Folder"=gray)
  • Unique vs Inherited badge per row (HasUniquePermissions → green "Unique", else gray "Inherited")
  • User pills with data-email attribute for each login in UserLogins (split by ;)
  • Self-contained: all CSS and JS inline in the HTML string — no external file dependencies
  • Table columns: Object, Title, URL, Unique, Users, Permissions, Granted Through
Task 1: Implement CsvExportService SharepointToolbox/Services/Export/CsvExportService.cs - BuildCsv(IReadOnlyList<PermissionEntry> entries) returns string - Header row: Object,Title,URL,HasUniquePermissions,Users,UserLogins,Type,Permissions,GrantedThrough (all quoted) - Merge rows: entries grouped by (Users, PermissionLevels, GrantedThrough) → one output row per group with URLs pipe-joined - Fields with commas, double quotes, or newlines are wrapped in double quotes with internal quotes doubled - WriteAsync(entries, filePath, ct) calls BuildCsv then writes UTF-8 with BOM (for Excel compatibility) - The test from Plan 01 (BuildCsv_WithDuplicateUserPermissionGrantedThrough_MergesLocations) passes Create `SharepointToolbox/Services/Export/` directory if it doesn't exist. Create `SharepointToolbox/Services/Export/CsvExportService.cs`:
```csharp
namespace SharepointToolbox.Services.Export;
public class CsvExportService
{
    private const string Header =
        "\"Object\",\"Title\",\"URL\",\"HasUniquePermissions\",\"Users\",\"UserLogins\",\"Type\",\"Permissions\",\"GrantedThrough\"";

    public string BuildCsv(IReadOnlyList<PermissionEntry> entries)
    {
        var sb = new StringBuilder();
        sb.AppendLine(Header);
        // Merge: group by (Users, PermissionLevels, GrantedThrough)
        var merged = entries
            .GroupBy(e => (e.Users, e.PermissionLevels, e.GrantedThrough))
            .Select(g => new
            {
                ObjectType   = g.First().ObjectType,
                Title        = string.Join(" | ", g.Select(e => e.Title).Distinct()),
                Url          = string.Join(" | ", g.Select(e => e.Url).Distinct()),
                HasUnique    = g.First().HasUniquePermissions,
                Users        = g.Key.Users,
                UserLogins   = g.First().UserLogins,
                PrincipalType= g.First().PrincipalType,
                Permissions  = g.Key.PermissionLevels,
                GrantedThrough = g.Key.GrantedThrough
            });
        foreach (var row in merged)
            sb.AppendLine(string.Join(",", new[]
            {
                Csv(row.ObjectType), Csv(row.Title), Csv(row.Url),
                Csv(row.HasUnique.ToString()), Csv(row.Users), Csv(row.UserLogins),
                Csv(row.PrincipalType), Csv(row.Permissions), Csv(row.GrantedThrough)
            }));
        return sb.ToString();
    }

    public async Task WriteAsync(IReadOnlyList<PermissionEntry> entries, string filePath, CancellationToken ct)
    {
        var csv = BuildCsv(entries);
        await File.WriteAllTextAsync(filePath, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct);
    }

    private static string Csv(string value)
    {
        if (string.IsNullOrEmpty(value)) return "\"\"";
        return $"\"{value.Replace("\"", "\"\"")}\"";
    }
}
```

Usings: `System.IO`, `System.Text`, `SharepointToolbox.Core.Models`.
Namespace: `SharepointToolbox.Services.Export`.
dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~CsvExportServiceTests" -x All 3 CsvExportServiceTests pass (header row present, merge works, empty list returns header only). dotnet build 0 errors. Task 2: Implement HtmlExportService SharepointToolbox/Services/Export/HtmlExportService.cs - BuildHtml(IReadOnlyList<PermissionEntry> entries) returns a self-contained HTML string - Output contains user display names from the input (test: BuildHtml_WithKnownEntries_ContainsUserNames passes) - Output contains all inline CSS and JS — no <link> or <script src=...> tags - Stats cards reflect: Total Entries count, Unique Permission Sets (distinct PermissionLevels values), Distinct Users (distinct entries in UserLogins split by semicolon) - Type badge CSS classes: site-coll, site, list, folder — color-coded - Unique/Inherited badge based on HasUniquePermissions - Filter input calls JS filterTable() on keyup — filters by any visible text in the row - External user tag: if UserLogins contains "#EXT#", user pill gets class "external-user" and data-email attribute - WriteAsync(entries, filePath, ct) writes UTF-8 (no BOM for HTML) - The test from Plan 01 (BuildHtml_WithExternalUser_ContainsExtHashMarker) passes — HTML contains "external-user" class Create `SharepointToolbox/Services/Export/HtmlExportService.cs`.
Structure the HTML report as a multi-line C# string literal inside BuildHtml(). Use `StringBuilder` to assemble:
1. HTML head (with inline CSS): table styles, badge styles (site-coll=blue, site=green, list=amber, folder=gray, unique=green, inherited=gray), user pill styles, external-user pill style (orange border), stats card styles, filter input style
2. Body open: h1 "SharePoint Permissions Report", stats cards div (compute counts from entries), filter input
3. Table with columns: Object | Title | URL | Unique | Users/Groups | Permission Level | Granted Through
4. For each entry: one `<tr>` with:
   - `<td><span class="{objectTypeCss}">{ObjectType}</span></td>`
   - `<td>{Title}</td>`
   - `<td><a href="{Url}" target="_blank">Link</a></td>`
   - `<td><span class="{uniqueCss}">{Unique/Inherited}</span></td>`
   - `<td>` + user pills: split UserLogins by ';', split Users by ';', zip them, render `<span class="user-pill {externalClass}" data-email="{login}">{name}</span>`
   - `<td>{PermissionLevels}</td>`
   - `<td>{GrantedThrough}</td>`
5. Inline JS: filterTable() function that iterates `<tr>` elements and shows/hides based on input text match against `tr.textContent`
6. Close body/html

Helper method `private static string ObjectTypeCss(string t)`:
- "Site Collection" → "badge site-coll"
- "Site" → "badge site"
- "List" → "badge list"
- "Folder" → "badge folder"
- else → "badge"

Stats computation:
```csharp
var totalEntries = entries.Count;
var uniquePermSets = entries.Select(e => e.PermissionLevels).Distinct().Count();
var distinctUsers = entries.SelectMany(e => e.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries))
    .Select(u => u.Trim()).Where(u => u.Length > 0).Distinct().Count();
```

Namespace: `SharepointToolbox.Services.Export`.
Usings: `System.IO`, `System.Text`, `SharepointToolbox.Core.Models`.
dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~HtmlExportServiceTests" -x All 3 HtmlExportServiceTests pass (user name present, empty list produces valid HTML, external user gets external-user class). dotnet build 0 errors. - `dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.slnx` → all tests pass (Phase 1 + new export tests) - CsvExportServiceTests: 3 green - HtmlExportServiceTests: 3 green - HTML output contains no external script/link tags (grep verifiable: no `src=` or `href=` outside the table)

<success_criteria>

  • CsvExportService merges rows by (Users, PermissionLevels, GrantedThrough) before writing
  • CSV uses UTF-8 with BOM for Excel compatibility
  • HtmlExportService produces self-contained HTML with inline CSS and JS
  • HTML correctly marks external users with "external-user" CSS class
  • All 6 export tests pass (3 CSV + 3 HTML) </success_criteria>
After completion, create `.planning/phases/02-permissions/02-04-SUMMARY.md`