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

405 lines
18 KiB
Markdown

---
phase: 08-simplified-permissions
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- SharepointToolbox/Core/Models/RiskLevel.cs
- SharepointToolbox/Core/Models/SimplifiedPermissionEntry.cs
- SharepointToolbox/Core/Models/PermissionSummary.cs
- SharepointToolbox/Core/Helpers/PermissionLevelMapping.cs
autonomous: true
requirements:
- SIMP-01
- SIMP-02
must_haves:
truths:
- "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"
artifacts:
- path: "SharepointToolbox/Core/Models/RiskLevel.cs"
provides: "Risk level classification enum"
contains: "enum RiskLevel"
- path: "SharepointToolbox/Core/Helpers/PermissionLevelMapping.cs"
provides: "Static mapping from SP role names to plain-language labels"
contains: "class PermissionLevelMapping"
- path: "SharepointToolbox/Core/Models/SimplifiedPermissionEntry.cs"
provides: "Presentation wrapper for PermissionEntry with simplified fields"
contains: "class SimplifiedPermissionEntry"
- path: "SharepointToolbox/Core/Models/PermissionSummary.cs"
provides: "Aggregation model for summary counts by risk level"
contains: "record PermissionSummary"
key_links:
- from: "SharepointToolbox/Core/Models/SimplifiedPermissionEntry.cs"
to: "SharepointToolbox/Core/Helpers/PermissionLevelMapping.cs"
via: "Static method call to resolve labels and risk level"
pattern: "PermissionLevelMapping\\.Get"
- from: "SharepointToolbox/Core/Models/SimplifiedPermissionEntry.cs"
to: "SharepointToolbox/Core/Models/PermissionEntry.cs"
via: "Wraps original entry as Inner property"
pattern: "PermissionEntry Inner"
---
<objective>
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
</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/STATE.md
<interfaces>
<!-- PermissionEntry is READ-ONLY — do NOT modify this record -->
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: <name>"
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);
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create RiskLevel enum and PermissionLevelMapping helper</name>
<files>SharepointToolbox/Core/Models/RiskLevel.cs, SharepointToolbox/Core/Helpers/PermissionLevelMapping.cs</files>
<action>
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
</action>
<verify>
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
</verify>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: Create SimplifiedPermissionEntry wrapper and PermissionSummary model</name>
<files>SharepointToolbox/Core/Models/SimplifiedPermissionEntry.cs, SharepointToolbox/Core/Models/PermissionSummary.cs</files>
<action>
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
</action>
<verify>
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
</verify>
<done>SimplifiedPermissionEntry wraps PermissionEntry with SimplifiedLabels, RiskLevel, Mappings, and all passthrough properties. PermissionSummary + PermissionSummaryBuilder provide grouped counts. Project compiles cleanly. PermissionEntry.cs is NOT modified.</done>
</task>
</tasks>
<verification>
- `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
</verification>
<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>
<output>
After completion, create `.planning/phases/08-simplified-permissions/08-01-SUMMARY.md`
</output>