Files
Sharepoint-Toolbox/.planning/phases/07-user-access-audit/07-06-PLAN.md
Dev 19e4c3852d docs(07): create phase plan - 8 plans across 5 waves
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:32:39 +02:00

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
07-01
SharepointToolbox/Services/Export/UserAccessCsvExportService.cs
SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs
true
UACC-02
truths artifacts key_links
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
path provides contains
SharepointToolbox/Services/Export/UserAccessCsvExportService.cs CSV export for user access audit results class UserAccessCsvExportService
path provides contains
SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs HTML export for user access audit results class UserAccessHtmlExportService
from to via pattern
SharepointToolbox/Services/Export/UserAccessCsvExportService.cs SharepointToolbox/Core/Models/UserAccessEntry.cs Takes IReadOnlyList<UserAccessEntry> as input UserAccessEntry
from to via pattern
SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs SharepointToolbox/Core/Models/UserAccessEntry.cs Takes IReadOnlyList<UserAccessEntry> as input UserAccessEntry
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

<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
Task 1: Implement UserAccessCsvExportService SharepointToolbox/Services/Export/UserAccessCsvExportService.cs 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)
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`