Files
Sharepoint-Toolbox/.planning/phases/08-simplified-permissions/08-01-PLAN.md
Dev c871effa87 docs(08-simplified-permissions): create phase plan (6 plans, 5 waves)
Plans cover plain-language permission labels, risk-level color coding,
summary counts, detail-level toggle, export integration, and unit tests.
PermissionEntry record is NOT modified — uses wrapper pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:00:08 +02:00

18 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
08-simplified-permissions 01 execute 1
SharepointToolbox/Core/Models/RiskLevel.cs
SharepointToolbox/Core/Models/SimplifiedPermissionEntry.cs
SharepointToolbox/Core/Models/PermissionSummary.cs
SharepointToolbox/Core/Helpers/PermissionLevelMapping.cs
true
SIMP-01
SIMP-02
truths artifacts key_links
RiskLevel enum distinguishes High, Medium, Low, and ReadOnly access tiers
PermissionLevelMapping maps all standard SharePoint role names to plain-language labels and risk levels
SimplifiedPermissionEntry wraps PermissionEntry with computed simplified labels and risk level without modifying the original record
PermissionSummary groups permission entries by risk level with counts
Unknown/custom role names fall back to the raw name with a Medium risk level
path provides contains
SharepointToolbox/Core/Models/RiskLevel.cs Risk level classification enum enum RiskLevel
path provides contains
SharepointToolbox/Core/Helpers/PermissionLevelMapping.cs Static mapping from SP role names to plain-language labels class PermissionLevelMapping
path provides contains
SharepointToolbox/Core/Models/SimplifiedPermissionEntry.cs Presentation wrapper for PermissionEntry with simplified fields class SimplifiedPermissionEntry
path provides contains
SharepointToolbox/Core/Models/PermissionSummary.cs Aggregation model for summary counts by risk level record PermissionSummary
from to via pattern
SharepointToolbox/Core/Models/SimplifiedPermissionEntry.cs SharepointToolbox/Core/Helpers/PermissionLevelMapping.cs Static method call to resolve labels and risk level PermissionLevelMapping.Get
from to via pattern
SharepointToolbox/Core/Models/SimplifiedPermissionEntry.cs SharepointToolbox/Core/Models/PermissionEntry.cs Wraps original entry as Inner property PermissionEntry Inner
Define the data models and mapping layer for simplified permissions: RiskLevel enum, PermissionLevelMapping helper, SimplifiedPermissionEntry wrapper, and PermissionSummary aggregation model.

Purpose: All subsequent plans import these types. The mapping layer is the core of SIMP-01 (plain-language labels) and SIMP-02 (risk level color coding). PermissionEntry is immutable and NOT modified — SimplifiedPermissionEntry wraps it as a presentation concern. Output: RiskLevel.cs, PermissionLevelMapping.cs, SimplifiedPermissionEntry.cs, PermissionSummary.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/STATE.md From SharepointToolbox/Core/Models/PermissionEntry.cs: ```csharp namespace SharepointToolbox.Core.Models;

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 (Limited Access already removed) string GrantedThrough, // "Direct Permissions" | "SharePoint Group: " string PrincipalType // "SharePointGroup" | "User" | "External User" );


From SharepointToolbox/Core/Helpers/PermissionEntryHelper.cs:
```csharp
public static class PermissionEntryHelper
{
    public static bool IsExternalUser(string loginName);
    public static IReadOnlyList<string> FilterPermissionLevels(IEnumerable<string> levels);
    public static bool IsSharingLinksGroup(string loginName);
}
Task 1: Create RiskLevel enum and PermissionLevelMapping helper SharepointToolbox/Core/Models/RiskLevel.cs, SharepointToolbox/Core/Helpers/PermissionLevelMapping.cs Create `SharepointToolbox/Core/Models/RiskLevel.cs`:
```csharp
namespace SharepointToolbox.Core.Models;

/// <summary>
/// Classifies a SharePoint permission level by its access risk.
/// Used for color coding in both WPF DataGrid and HTML export.
/// </summary>
public enum RiskLevel
{
    /// <summary>Full Control, Site Collection Administrator — can delete site, manage permissions.</summary>
    High,
    /// <summary>Contribute, Edit, Design — can modify content.</summary>
    Medium,
    /// <summary>Read, Restricted View — can view but not modify.</summary>
    Low,
    /// <summary>View Only — most restricted legitimate access.</summary>
    ReadOnly
}
```

Create `SharepointToolbox/Core/Helpers/PermissionLevelMapping.cs`:

```csharp
using SharepointToolbox.Core.Models;

namespace SharepointToolbox.Core.Helpers;

/// <summary>
/// Maps SharePoint built-in permission level names to human-readable labels and risk levels.
/// Used by SimplifiedPermissionEntry and export services to translate raw role names
/// into plain-language descriptions that non-technical users can understand.
/// </summary>
public static class PermissionLevelMapping
{
    /// <summary>
    /// Result of looking up a SharePoint role name.
    /// </summary>
    public record MappingResult(string Label, RiskLevel RiskLevel);

    /// <summary>
    /// Known SharePoint built-in permission level mappings.
    /// Keys are case-insensitive via the dictionary comparer.
    /// </summary>
    private static readonly Dictionary<string, MappingResult> Mappings = new(StringComparer.OrdinalIgnoreCase)
    {
        // High risk — full administrative access
        ["Full Control"]                 = new("Full control (can manage everything)", RiskLevel.High),
        ["Site Collection Administrator"] = new("Site collection admin (full control)", RiskLevel.High),

        // Medium risk — can modify content
        ["Contribute"]       = new("Can edit files and list items", RiskLevel.Medium),
        ["Edit"]             = new("Can edit files, lists, and pages", RiskLevel.Medium),
        ["Design"]           = new("Can edit pages and use design tools", RiskLevel.Medium),
        ["Approve"]          = new("Can approve content and list items", RiskLevel.Medium),
        ["Manage Hierarchy"] = new("Can create sites and manage pages", RiskLevel.Medium),

        // Low risk — read access
        ["Read"]             = new("Can view files and pages", RiskLevel.Low),
        ["Restricted Read"]  = new("Can view pages only (no download)", RiskLevel.Low),

        // Read-only — most restricted
        ["View Only"]        = new("Can view files in browser only", RiskLevel.ReadOnly),
        ["Restricted View"]  = new("Restricted view access", RiskLevel.ReadOnly),
    };

    /// <summary>
    /// Gets the human-readable label and risk level for a SharePoint role name.
    /// Returns the mapped result for known roles; for unknown/custom roles,
    /// returns the raw name as-is with Medium risk level.
    /// </summary>
    public static MappingResult GetMapping(string roleName)
    {
        if (string.IsNullOrWhiteSpace(roleName))
            return new MappingResult(roleName, RiskLevel.Low);

        return Mappings.TryGetValue(roleName.Trim(), out var result)
            ? result
            : new MappingResult(roleName.Trim(), RiskLevel.Medium);
    }

    /// <summary>
    /// Resolves a semicolon-delimited PermissionLevels string into individual mapping results.
    /// This handles the PermissionEntry.PermissionLevels format (e.g. "Full Control; Contribute").
    /// </summary>
    public static IReadOnlyList<MappingResult> GetMappings(string permissionLevels)
    {
        if (string.IsNullOrWhiteSpace(permissionLevels))
            return Array.Empty<MappingResult>();

        return permissionLevels
            .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Select(GetMapping)
            .ToList();
    }

    /// <summary>
    /// Returns the highest (most dangerous) risk level from a semicolon-delimited permission levels string.
    /// Used for row-level color coding when an entry has multiple roles.
    /// </summary>
    public static RiskLevel GetHighestRisk(string permissionLevels)
    {
        var mappings = GetMappings(permissionLevels);
        if (mappings.Count == 0) return RiskLevel.Low;

        // High < Medium < Low < ReadOnly in enum order — Min gives highest risk
        return mappings.Min(m => m.RiskLevel);
    }

    /// <summary>
    /// Converts a semicolon-delimited PermissionLevels string into a simplified labels string.
    /// E.g. "Full Control; Contribute" becomes "Full control (can manage everything); Can edit files and list items"
    /// </summary>
    public static string GetSimplifiedLabels(string permissionLevels)
    {
        var mappings = GetMappings(permissionLevels);
        return string.Join("; ", mappings.Select(m => m.Label));
    }
}
```

Design notes:
- Case-insensitive lookup handles variations in SharePoint role name casing
- Unknown/custom roles default to Medium (conservative — forces admin review)
- GetHighestRisk uses enum ordering (High=0 is most dangerous) for row-level color
- Semicolon-split methods handle the PermissionEntry.PermissionLevels format directly
cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5 RiskLevel.cs contains 4-value enum (High, Medium, Low, ReadOnly). PermissionLevelMapping.cs has GetMapping, GetMappings, GetHighestRisk, and GetSimplifiedLabels. All standard SP roles mapped. Unknown roles fallback to Medium. Project compiles. Task 2: Create SimplifiedPermissionEntry wrapper and PermissionSummary model SharepointToolbox/Core/Models/SimplifiedPermissionEntry.cs, SharepointToolbox/Core/Models/PermissionSummary.cs Create `SharepointToolbox/Core/Models/SimplifiedPermissionEntry.cs`:
```csharp
using SharepointToolbox.Core.Helpers;

namespace SharepointToolbox.Core.Models;

/// <summary>
/// Presentation wrapper around PermissionEntry that adds simplified labels
/// and risk level classification without modifying the immutable source record.
/// Used as the DataGrid ItemsSource when simplified mode is active.
/// </summary>
public class SimplifiedPermissionEntry
{
    /// <summary>The original immutable PermissionEntry.</summary>
    public PermissionEntry Inner { get; }

    /// <summary>
    /// Human-readable labels for the permission levels.
    /// E.g. "Can edit files and list items" instead of "Contribute".
    /// </summary>
    public string SimplifiedLabels { get; }

    /// <summary>
    /// The highest risk level across all permission levels on this entry.
    /// Used for row-level color coding.
    /// </summary>
    public RiskLevel RiskLevel { get; }

    /// <summary>
    /// Individual mapping results for each permission level in the entry.
    /// Used when detailed breakdown per-role is needed.
    /// </summary>
    public IReadOnlyList<PermissionLevelMapping.MappingResult> Mappings { get; }

    // ── Passthrough properties for DataGrid binding ──

    public string ObjectType => Inner.ObjectType;
    public string Title => Inner.Title;
    public string Url => Inner.Url;
    public bool HasUniquePermissions => Inner.HasUniquePermissions;
    public string Users => Inner.Users;
    public string UserLogins => Inner.UserLogins;
    public string PermissionLevels => Inner.PermissionLevels;
    public string GrantedThrough => Inner.GrantedThrough;
    public string PrincipalType => Inner.PrincipalType;

    public SimplifiedPermissionEntry(PermissionEntry entry)
    {
        Inner = entry;
        Mappings = PermissionLevelMapping.GetMappings(entry.PermissionLevels);
        SimplifiedLabels = PermissionLevelMapping.GetSimplifiedLabels(entry.PermissionLevels);
        RiskLevel = PermissionLevelMapping.GetHighestRisk(entry.PermissionLevels);
    }

    /// <summary>
    /// Creates SimplifiedPermissionEntry wrappers for a collection of entries.
    /// </summary>
    public static IReadOnlyList<SimplifiedPermissionEntry> WrapAll(
        IEnumerable<PermissionEntry> entries)
    {
        return entries.Select(e => new SimplifiedPermissionEntry(e)).ToList();
    }
}
```

Create `SharepointToolbox/Core/Models/PermissionSummary.cs`:

```csharp
namespace SharepointToolbox.Core.Models;

/// <summary>
/// Summary counts of permission entries grouped by risk level.
/// Displayed in the summary panel when simplified mode is active.
/// </summary>
public record PermissionSummary(
    /// <summary>Label for this group (e.g. "High Risk", "Read Only").</summary>
    string Label,
    /// <summary>The risk level this group represents.</summary>
    RiskLevel RiskLevel,
    /// <summary>Number of permission entries at this risk level.</summary>
    int Count,
    /// <summary>Number of distinct users at this risk level.</summary>
    int DistinctUsers
);

/// <summary>
/// Computes PermissionSummary groups from SimplifiedPermissionEntry collections.
/// </summary>
public static class PermissionSummaryBuilder
{
    /// <summary>
    /// Risk level display labels.
    /// </summary>
    private static readonly Dictionary<RiskLevel, string> Labels = new()
    {
        [RiskLevel.High] = "High Risk",
        [RiskLevel.Medium] = "Medium Risk",
        [RiskLevel.Low] = "Low Risk",
        [RiskLevel.ReadOnly] = "Read Only",
    };

    /// <summary>
    /// Builds summary counts grouped by risk level from a collection of simplified entries.
    /// Always returns all 4 risk levels, even if count is 0, for consistent UI binding.
    /// </summary>
    public static IReadOnlyList<PermissionSummary> Build(
        IEnumerable<SimplifiedPermissionEntry> entries)
    {
        var grouped = entries
            .GroupBy(e => e.RiskLevel)
            .ToDictionary(g => g.Key, g => g.ToList());

        return Enum.GetValues<RiskLevel>()
            .Select(level =>
            {
                var items = grouped.GetValueOrDefault(level, new List<SimplifiedPermissionEntry>());
                var distinctUsers = items
                    .SelectMany(e => e.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries))
                    .Select(u => u.Trim())
                    .Where(u => u.Length > 0)
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .Count();

                return new PermissionSummary(
                    Label: Labels[level],
                    RiskLevel: level,
                    Count: items.Count,
                    DistinctUsers: distinctUsers);
            })
            .ToList();
    }
}
```

Design notes:
- SimplifiedPermissionEntry is a class (not record) so it can have passthrough properties for DataGrid binding
- All original PermissionEntry fields are exposed as passthrough properties — DataGrid columns bind identically
- SimplifiedLabels and RiskLevel are computed once at construction — no per-render cost
- PermissionSummaryBuilder.Build always returns 4 entries (one per RiskLevel) for consistent summary panel layout
- DistinctUsers uses case-insensitive comparison for login deduplication
cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5 SimplifiedPermissionEntry wraps PermissionEntry with SimplifiedLabels, RiskLevel, Mappings, and all passthrough properties. PermissionSummary + PermissionSummaryBuilder provide grouped counts. Project compiles cleanly. PermissionEntry.cs is NOT modified. - `dotnet build SharepointToolbox/SharepointToolbox.csproj` succeeds with 0 errors - RiskLevel.cs has High, Medium, Low, ReadOnly values - PermissionLevelMapping has 11 known role mappings with labels and risk levels - SimplifiedPermissionEntry wraps PermissionEntry (Inner property) without modifying it - PermissionSummaryBuilder.Build returns 4 summary entries (one per risk level) - No changes to PermissionEntry.cs

<success_criteria> All 4 files compile cleanly. The mapping and wrapper layer is complete: downstream plans (08-02 through 08-05) can import RiskLevel, PermissionLevelMapping, SimplifiedPermissionEntry, and PermissionSummary without ambiguity. PermissionEntry remains immutable and unmodified. </success_criteria>

After completion, create `.planning/phases/08-simplified-permissions/08-01-SUMMARY.md`