15 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 | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-user-access-audit | 06 | execute | 2 |
|
|
true |
|
|
Purpose: Audit results must be exportable for compliance documentation and sharing with stakeholders. Output: UserAccessCsvExportService.cs, UserAccessHtmlExportService.cs
<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>
@.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 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:
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
```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)
cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5
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.
Task 2: Implement UserAccessHtmlExportService
SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs
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)
cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5
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.
- `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
<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>
After completion, create `.planning/phases/07-user-access-audit/07-06-SUMMARY.md`