333 lines
15 KiB
Markdown
333 lines
15 KiB
Markdown
---
|
|
phase: 07-user-access-audit
|
|
plan: 06
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["07-01"]
|
|
files_modified:
|
|
- SharepointToolbox/Services/Export/UserAccessCsvExportService.cs
|
|
- SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs
|
|
autonomous: true
|
|
requirements:
|
|
- UACC-02
|
|
must_haves:
|
|
truths:
|
|
- "CSV export produces one file per audited user with summary section at top and flat data rows"
|
|
- "CSV filenames include user email and date (e.g. audit_alice@contoso.com_2026-04-07.csv)"
|
|
- "HTML export produces a single self-contained report with collapsible groups, sortable columns, search filter"
|
|
- "HTML report has both group-by-user and group-by-site views togglable via tab/button in header"
|
|
- "HTML report shows per-user summary stats and risk highlights (high-privilege, external users)"
|
|
- "Both exports follow established patterns: UTF-8+BOM for CSV, inline CSS/JS for HTML"
|
|
artifacts:
|
|
- path: "SharepointToolbox/Services/Export/UserAccessCsvExportService.cs"
|
|
provides: "CSV export for user access audit results"
|
|
contains: "class UserAccessCsvExportService"
|
|
- path: "SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs"
|
|
provides: "HTML export for user access audit results"
|
|
contains: "class UserAccessHtmlExportService"
|
|
key_links:
|
|
- from: "SharepointToolbox/Services/Export/UserAccessCsvExportService.cs"
|
|
to: "SharepointToolbox/Core/Models/UserAccessEntry.cs"
|
|
via: "Takes IReadOnlyList<UserAccessEntry> as input"
|
|
pattern: "UserAccessEntry"
|
|
- from: "SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs"
|
|
to: "SharepointToolbox/Core/Models/UserAccessEntry.cs"
|
|
via: "Takes IReadOnlyList<UserAccessEntry> as input"
|
|
pattern: "UserAccessEntry"
|
|
---
|
|
|
|
<objective>
|
|
Implement the two export services for User Access Audit: per-user CSV files with summary headers, and a single interactive HTML report with dual-view toggle, collapsible groups, and risk highlighting.
|
|
|
|
Purpose: Audit results must be exportable for compliance documentation and sharing with stakeholders.
|
|
Output: UserAccessCsvExportService.cs, UserAccessHtmlExportService.cs
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md
|
|
@C:/Users/dev/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/phases/07-user-access-audit/07-CONTEXT.md
|
|
@.planning/phases/07-user-access-audit/07-01-SUMMARY.md
|
|
|
|
<interfaces>
|
|
<!-- From 07-01: Data model for export -->
|
|
From SharepointToolbox/Core/Models/UserAccessEntry.cs:
|
|
```csharp
|
|
public enum AccessType { Direct, Group, Inherited }
|
|
|
|
public record UserAccessEntry(
|
|
string UserDisplayName, string UserLogin,
|
|
string SiteUrl, string SiteTitle,
|
|
string ObjectType, string ObjectTitle, string ObjectUrl,
|
|
string PermissionLevel,
|
|
AccessType AccessType, string GrantedThrough,
|
|
bool IsHighPrivilege, bool IsExternalUser);
|
|
```
|
|
|
|
<!-- Existing export patterns to follow -->
|
|
From SharepointToolbox/Services/Export/CsvExportService.cs:
|
|
```csharp
|
|
public class CsvExportService
|
|
{
|
|
public string BuildCsv(IReadOnlyList<PermissionEntry> entries) { ... }
|
|
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) { /* RFC 4180 escaping */ }
|
|
}
|
|
```
|
|
|
|
From SharepointToolbox/Services/Export/HtmlExportService.cs:
|
|
```csharp
|
|
public class HtmlExportService
|
|
{
|
|
public string BuildHtml(IReadOnlyList<PermissionEntry> entries) { ... }
|
|
public async Task WriteAsync(IReadOnlyList<PermissionEntry> entries, string filePath, CancellationToken ct)
|
|
{
|
|
var html = BuildHtml(entries);
|
|
await File.WriteAllTextAsync(filePath, html, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), ct);
|
|
}
|
|
}
|
|
// Pattern: stats cards, filter input, table, inline JS for filter, inline CSS, badges, user pills
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Implement UserAccessCsvExportService</name>
|
|
<files>SharepointToolbox/Services/Export/UserAccessCsvExportService.cs</files>
|
|
<action>
|
|
Create `SharepointToolbox/Services/Export/UserAccessCsvExportService.cs`:
|
|
|
|
```csharp
|
|
using System.IO;
|
|
using System.Text;
|
|
using SharepointToolbox.Core.Models;
|
|
|
|
namespace SharepointToolbox.Services.Export;
|
|
|
|
/// <summary>
|
|
/// Exports user access audit results to CSV format.
|
|
/// Produces one CSV file per audited user with a summary section at the top.
|
|
/// </summary>
|
|
public class UserAccessCsvExportService
|
|
{
|
|
private const string DataHeader =
|
|
"\"Site\",\"Object Type\",\"Object\",\"URL\",\"Permission Level\",\"Access Type\",\"Granted Through\"";
|
|
|
|
/// <summary>
|
|
/// Builds a CSV string for a single user's access entries.
|
|
/// Includes a summary section at the top followed by data rows.
|
|
/// </summary>
|
|
public string BuildCsv(string userDisplayName, string userLogin, IReadOnlyList<UserAccessEntry> entries)
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
// Summary section
|
|
var sitesCount = entries.Select(e => e.SiteUrl).Distinct().Count();
|
|
var highPrivCount = entries.Count(e => e.IsHighPrivilege);
|
|
|
|
sb.AppendLine($"\"User Access Audit Report\"");
|
|
sb.AppendLine($"\"User\",\"{Csv(userDisplayName)} ({Csv(userLogin)})\"");
|
|
sb.AppendLine($"\"Total Accesses\",\"{entries.Count}\"");
|
|
sb.AppendLine($"\"Sites\",\"{sitesCount}\"");
|
|
sb.AppendLine($"\"High Privilege\",\"{highPrivCount}\"");
|
|
sb.AppendLine($"\"Generated\",\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\"");
|
|
sb.AppendLine(); // Blank line separating summary from data
|
|
|
|
// Data rows
|
|
sb.AppendLine(DataHeader);
|
|
foreach (var entry in entries)
|
|
{
|
|
sb.AppendLine(string.Join(",", new[]
|
|
{
|
|
Csv(entry.SiteTitle),
|
|
Csv(entry.ObjectType),
|
|
Csv(entry.ObjectTitle),
|
|
Csv(entry.ObjectUrl),
|
|
Csv(entry.PermissionLevel),
|
|
Csv(entry.AccessType.ToString()),
|
|
Csv(entry.GrantedThrough)
|
|
}));
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes one CSV file per user to the specified directory.
|
|
/// File names: audit_{email}_{date}.csv
|
|
/// </summary>
|
|
public async Task WriteAsync(
|
|
IReadOnlyList<UserAccessEntry> allEntries,
|
|
string directoryPath,
|
|
CancellationToken ct)
|
|
{
|
|
Directory.CreateDirectory(directoryPath);
|
|
var dateStr = DateTime.Now.ToString("yyyy-MM-dd");
|
|
|
|
// Group by user
|
|
var byUser = allEntries.GroupBy(e => e.UserLogin);
|
|
|
|
foreach (var group in byUser)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
var userLogin = group.Key;
|
|
var displayName = group.First().UserDisplayName;
|
|
var entries = group.ToList();
|
|
|
|
// Sanitize email for filename (replace @ and other invalid chars)
|
|
var safeLogin = SanitizeFileName(userLogin);
|
|
var fileName = $"audit_{safeLogin}_{dateStr}.csv";
|
|
var filePath = Path.Combine(directoryPath, fileName);
|
|
|
|
var csv = BuildCsv(displayName, userLogin, entries);
|
|
await File.WriteAllTextAsync(filePath, csv,
|
|
new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes all entries to a single CSV file (alternative for single-file export).
|
|
/// Used when the ViewModel export command picks a single file path.
|
|
/// </summary>
|
|
public async Task WriteSingleFileAsync(
|
|
IReadOnlyList<UserAccessEntry> entries,
|
|
string filePath,
|
|
CancellationToken ct)
|
|
{
|
|
var sb = new StringBuilder();
|
|
var fullHeader = "\"User\",\"User Login\"," + DataHeader;
|
|
|
|
// Summary
|
|
var users = entries.Select(e => e.UserLogin).Distinct().ToList();
|
|
sb.AppendLine($"\"User Access Audit Report\"");
|
|
sb.AppendLine($"\"Users Audited\",\"{users.Count}\"");
|
|
sb.AppendLine($"\"Total Accesses\",\"{entries.Count}\"");
|
|
sb.AppendLine($"\"Generated\",\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\"");
|
|
sb.AppendLine();
|
|
|
|
sb.AppendLine(fullHeader);
|
|
foreach (var entry in entries)
|
|
{
|
|
sb.AppendLine(string.Join(",", new[]
|
|
{
|
|
Csv(entry.UserDisplayName),
|
|
Csv(entry.UserLogin),
|
|
Csv(entry.SiteTitle),
|
|
Csv(entry.ObjectType),
|
|
Csv(entry.ObjectTitle),
|
|
Csv(entry.ObjectUrl),
|
|
Csv(entry.PermissionLevel),
|
|
Csv(entry.AccessType.ToString()),
|
|
Csv(entry.GrantedThrough)
|
|
}));
|
|
}
|
|
|
|
await File.WriteAllTextAsync(filePath, sb.ToString(),
|
|
new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct);
|
|
}
|
|
|
|
private static string Csv(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value)) return "\"\"";
|
|
return $"\"{value.Replace("\"", "\"\"")}\"";
|
|
}
|
|
|
|
private static string SanitizeFileName(string name)
|
|
{
|
|
var invalid = Path.GetInvalidFileNameChars();
|
|
var sb = new StringBuilder(name.Length);
|
|
foreach (var c in name)
|
|
sb.Append(invalid.Contains(c) ? '_' : c);
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
```
|
|
|
|
Design notes:
|
|
- Two write modes: WriteAsync (per-user files to directory) and WriteSingleFileAsync (all in one file)
|
|
- The ViewModel will use WriteSingleFileAsync for the SaveFileDialog export (simpler UX)
|
|
- WriteAsync with per-user files available for batch export scenarios
|
|
- Summary section at top of each file per CONTEXT.md decision
|
|
- RFC 4180 CSV escaping following existing CsvExportService.Csv() pattern
|
|
- UTF-8 with BOM for Excel compatibility (same as existing exports)
|
|
</action>
|
|
<verify>
|
|
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
|
|
</verify>
|
|
<done>UserAccessCsvExportService.cs compiles, has BuildCsv for per-user CSV, WriteAsync for per-user files, WriteSingleFileAsync for combined export, RFC 4180 escaping, UTF-8+BOM encoding.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Implement UserAccessHtmlExportService</name>
|
|
<files>SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs</files>
|
|
<action>
|
|
Create `SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs`. Follow the HtmlExportService pattern (self-contained HTML with inline CSS/JS, stats cards, filter, table).
|
|
|
|
The HTML report must include:
|
|
1. **Title**: "User Access Audit Report"
|
|
2. **Stats cards** row: Total Accesses, Users Audited, Sites Scanned, High Privilege Count, External Users Count
|
|
3. **Per-user summary section**: For each user, show a card with their name, total accesses, sites count, high-privilege count. Highlight if user has Site Collection Admin access.
|
|
4. **View toggle**: Two buttons "By User" / "By Site" that show/hide the corresponding grouped table (JavaScript toggle, no page reload)
|
|
5. **Filter input**: Text filter that searches across all visible rows
|
|
6. **Table (By User view)**: Grouped by user (collapsible sections). Each group header shows user name + count. Rows: Site, Object Type, Object, Permission Level, Access Type badge, Granted Through
|
|
7. **Table (By Site view)**: Grouped by site (collapsible sections). Each group header shows site title + count. Rows: User, Object Type, Object, Permission Level, Access Type badge, Granted Through
|
|
8. **Access Type badges**: Colored badges — Direct (blue), Group (green), Inherited (gray)
|
|
9. **High-privilege rows**: Warning icon + bold text
|
|
10. **External user badge**: Orange "Guest" pill next to user name
|
|
11. **Inline JS**:
|
|
- `toggleView(view)`: Shows "by-user" or "by-site" div, updates active button state
|
|
- `filterTable()`: Filters visible rows in the active view
|
|
- `toggleGroup(id)`: Collapses/expands a group section
|
|
- `sortTable(col)`: Sorts rows within groups by column
|
|
|
|
The HTML should be ~300-400 lines of generated content. Use StringBuilder like the existing HtmlExportService.
|
|
|
|
Follow the exact same CSS style as HtmlExportService (same font-family, stat-card styles, table styles, badge styles) with additions for:
|
|
- `.access-direct { background: #dbeafe; color: #1e40af; }` (blue)
|
|
- `.access-group { background: #dcfce7; color: #166534; }` (green)
|
|
- `.access-inherited { background: #f3f4f6; color: #374151; }` (gray)
|
|
- `.high-priv { font-weight: 700; }` + warning icon
|
|
- `.guest-badge { background: #fff7ed; color: #9a3412; border: 1px solid #fed7aa; }` (reuse external-user style)
|
|
- `.view-toggle button.active { background: #1a1a2e; color: #fff; }`
|
|
- `.group-header { cursor: pointer; background: #f0f0f0; padding: 10px; font-weight: 600; }`
|
|
|
|
The service should have:
|
|
- `BuildHtml(IReadOnlyList<UserAccessEntry> entries)` — returns full HTML string
|
|
- `WriteAsync(IReadOnlyList<UserAccessEntry> entries, string filePath, CancellationToken ct)` — writes to file (UTF-8 without BOM, same as HtmlExportService)
|
|
</action>
|
|
<verify>
|
|
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
|
|
</verify>
|
|
<done>UserAccessHtmlExportService.cs compiles, produces self-contained HTML with: stats cards, per-user summary, dual-view toggle (by-user/by-site), collapsible groups, filter input, sortable columns, color-coded access type badges, high-privilege warnings, external user badges, inline CSS/JS.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `dotnet build SharepointToolbox/SharepointToolbox.csproj` succeeds with 0 errors
|
|
- UserAccessCsvExportService has BuildCsv + WriteAsync + WriteSingleFileAsync
|
|
- UserAccessHtmlExportService has BuildHtml + WriteAsync
|
|
- HTML output contains inline CSS and JS (no external dependencies)
|
|
- CSV uses RFC 4180 escaping and UTF-8+BOM
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
Both export services compile and follow established patterns. CSV produces per-user files with summary headers. HTML produces an interactive report with dual-view toggle, collapsible groups, color-coded badges, and risk highlighting. Ready for ViewModel export commands in 07-04.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/07-user-access-audit/07-06-SUMMARY.md`
|
|
</output>
|