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