Files
Sharepoint-Toolbox/.planning/milestones/v1.0-phases/02-permissions/02-04-PLAN.md
Dev 655bb79a99
All checks were successful
Release zip package / release (push) Successful in 10s
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:15:14 +02:00

251 lines
12 KiB
Markdown

---
phase: 02-permissions
plan: 04
type: execute
wave: 2
depends_on:
- 02-02
files_modified:
- SharepointToolbox/Services/Export/CsvExportService.cs
- SharepointToolbox/Services/Export/HtmlExportService.cs
autonomous: true
requirements:
- PERM-05
- PERM-06
must_haves:
truths:
- "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"
artifacts:
- path: "SharepointToolbox/Services/Export/CsvExportService.cs"
provides: "Merges PermissionEntry rows and writes CSV"
exports: ["CsvExportService"]
- path: "SharepointToolbox/Services/Export/HtmlExportService.cs"
provides: "Generates self-contained interactive HTML report"
exports: ["HtmlExportService"]
key_links:
- from: "CsvExportService.cs"
to: "PermissionEntry"
via: "groups by (Users, PermissionLevels, GrantedThrough)"
pattern: "GroupBy"
- from: "HtmlExportService.cs"
to: "PermissionEntry"
via: "iterates all entries to build HTML rows"
pattern: "foreach.*PermissionEntry"
---
<objective>
Implement the two export services: `CsvExportService` (port of PS `Merge-PermissionRows` + `Export-Csv`) and `HtmlExportService` (port of PS `Export-PermissionsToHTML`). Both services consume `IReadOnlyList<PermissionEntry>` 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.
</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>
<!-- PermissionEntry defined in Plan 02 -->
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: <name>"
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
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Implement CsvExportService</name>
<files>
SharepointToolbox/Services/Export/CsvExportService.cs
</files>
<behavior>
- BuildCsv(IReadOnlyList&lt;PermissionEntry&gt; 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
</behavior>
<action>
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`.
</action>
<verify>
<automated>dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~CsvExportServiceTests" -x</automated>
</verify>
<done>All 3 CsvExportServiceTests pass (header row present, merge works, empty list returns header only). dotnet build 0 errors.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Implement HtmlExportService</name>
<files>
SharepointToolbox/Services/Export/HtmlExportService.cs
</files>
<behavior>
- BuildHtml(IReadOnlyList&lt;PermissionEntry&gt; 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 &lt;link&gt; or &lt;script src=...&gt; 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
</behavior>
<action>
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`.
</action>
<verify>
<automated>dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~HtmlExportServiceTests" -x</automated>
</verify>
<done>All 3 HtmlExportServiceTests pass (user name present, empty list produces valid HTML, external user gets external-user class). dotnet build 0 errors.</done>
</task>
</tasks>
<verification>
- `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)
</verification>
<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>
<output>
After completion, create `.planning/phases/02-permissions/02-04-SUMMARY.md`
</output>