Files
Sharepoint-Toolbox/.planning/phases/07-user-access-audit/07-01-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

233 lines
9.8 KiB
Markdown

---
phase: 07-user-access-audit
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- SharepointToolbox/Core/Models/UserAccessEntry.cs
- SharepointToolbox/Services/IUserAccessAuditService.cs
- SharepointToolbox/Services/IGraphUserSearchService.cs
autonomous: true
requirements:
- UACC-01
- UACC-02
must_haves:
truths:
- "UserAccessEntry record exists with all fields needed for audit results display and export"
- "IUserAccessAuditService interface defines the contract for scanning permissions filtered by user"
- "IGraphUserSearchService interface defines the contract for Graph API people-picker autocomplete"
- "AccessType enum distinguishes Direct, Group, and Inherited access"
artifacts:
- path: "SharepointToolbox/Core/Models/UserAccessEntry.cs"
provides: "Data model for user-centric audit results"
contains: "record UserAccessEntry"
- path: "SharepointToolbox/Services/IUserAccessAuditService.cs"
provides: "Service contract for user access auditing"
contains: "interface IUserAccessAuditService"
- path: "SharepointToolbox/Services/IGraphUserSearchService.cs"
provides: "Service contract for Graph API user search"
contains: "interface IGraphUserSearchService"
key_links: []
---
<objective>
Define the data models and service interfaces that all subsequent plans depend on. This is the Wave 0 contract layer: UserAccessEntry record, AccessType enum, IUserAccessAuditService, and IGraphUserSearchService.
Purpose: Every other plan in this phase imports these types. Defining them first prevents circular dependencies and gives executors concrete contracts.
Output: UserAccessEntry.cs, IUserAccessAuditService.cs, IGraphUserSearchService.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
@.planning/phases/07-user-access-audit/07-CONTEXT.md
<interfaces>
<!-- Existing models this builds alongside -->
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
string GrantedThrough, // "Direct Permissions" | "SharePoint Group: <name>"
string PrincipalType // "SharePointGroup" | "User" | "External User"
);
```
From SharepointToolbox/Core/Models/SiteInfo.cs:
```csharp
namespace SharepointToolbox.Core.Models;
public record SiteInfo(string Url, string Title);
```
From SharepointToolbox/Core/Models/ScanOptions.cs (inferred from usage):
```csharp
public record ScanOptions(bool IncludeInherited, bool ScanFolders, int FolderDepth, bool IncludeSubsites);
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create UserAccessEntry model and AccessType enum</name>
<files>SharepointToolbox/Core/Models/UserAccessEntry.cs</files>
<action>
Create `SharepointToolbox/Core/Models/UserAccessEntry.cs` with:
```csharp
namespace SharepointToolbox.Core.Models;
/// <summary>
/// Classifies how a user received a permission assignment.
/// </summary>
public enum AccessType
{
/// <summary>User is directly assigned a role on the object.</summary>
Direct,
/// <summary>User is a member of a SharePoint group that has the role.</summary>
Group,
/// <summary>Permission is inherited from a parent object (not unique).</summary>
Inherited
}
/// <summary>
/// One row in the User Access Audit results grid.
/// Represents a single permission that a specific user holds on a specific object.
/// </summary>
public record UserAccessEntry(
string UserDisplayName, // e.g. "Alice Smith"
string UserLogin, // e.g. "alice@contoso.com" or "i:0#.f|membership|alice@contoso.com"
string SiteUrl, // The site collection URL where this permission exists
string SiteTitle, // The site collection title
string ObjectType, // "Site Collection" | "Site" | "List" | "Folder"
string ObjectTitle, // Name of the list/folder/site
string ObjectUrl, // URL of the specific object
string PermissionLevel, // e.g. "Full Control", "Contribute"
AccessType AccessType, // Direct | Group | Inherited
string GrantedThrough, // "Direct Permissions" | "SharePoint Group: Members" etc.
bool IsHighPrivilege, // True for Full Control, Site Collection Administrator
bool IsExternalUser // True if login contains #EXT#
);
```
Design notes:
- Each row is one user + one object + one permission level (fully denormalized for DataGrid binding)
- IsHighPrivilege pre-computed during scan for warning icon display without re-evaluation
- IsExternalUser pre-computed using PermissionEntryHelper.IsExternalUser pattern
- SiteUrl + SiteTitle included so results can group by site across multi-site scans
</action>
<verify>
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
</verify>
<done>UserAccessEntry.cs and AccessType enum exist in Core/Models/, compile without errors, contain all 12 fields.</done>
</task>
<task type="auto">
<name>Task 2: Create IUserAccessAuditService and IGraphUserSearchService interfaces</name>
<files>SharepointToolbox/Services/IUserAccessAuditService.cs, SharepointToolbox/Services/IGraphUserSearchService.cs</files>
<action>
Create `SharepointToolbox/Services/IUserAccessAuditService.cs`:
```csharp
using SharepointToolbox.Core.Models;
namespace SharepointToolbox.Services;
/// <summary>
/// Scans permissions across selected sites and filters results to show
/// only what specific user(s) can access.
/// </summary>
public interface IUserAccessAuditService
{
/// <summary>
/// Scans all selected sites for permissions, then filters results to entries
/// matching the specified user logins. Returns a flat list of UserAccessEntry
/// records suitable for DataGrid binding and export.
/// </summary>
/// <param name="sessionManager">Session manager for creating authenticated contexts.</param>
/// <param name="targetUserLogins">Login names (emails) of users to audit.</param>
/// <param name="sites">Sites to scan.</param>
/// <param name="options">Scan depth options (inherited, folders, subsites).</param>
/// <param name="progress">Progress reporter.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Flat list of access entries for the target users.</returns>
Task<IReadOnlyList<UserAccessEntry>> AuditUsersAsync(
ISessionManager sessionManager,
IReadOnlyList<string> targetUserLogins,
IReadOnlyList<SiteInfo> sites,
ScanOptions options,
IProgress<OperationProgress> progress,
CancellationToken ct);
}
```
Create `SharepointToolbox/Services/IGraphUserSearchService.cs`:
```csharp
namespace SharepointToolbox.Services;
/// <summary>
/// Searches tenant users via Microsoft Graph API for the people-picker autocomplete.
/// </summary>
public interface IGraphUserSearchService
{
/// <summary>
/// Searches for users in the tenant whose display name or email matches the query.
/// Returns up to <paramref name="maxResults"/> matches.
/// </summary>
/// <param name="clientId">The Azure AD app client ID for Graph authentication.</param>
/// <param name="query">Partial name or email to search for.</param>
/// <param name="maxResults">Maximum number of results to return (default 10).</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of (DisplayName, Email/UPN) tuples.</returns>
Task<IReadOnlyList<GraphUserResult>> SearchUsersAsync(
string clientId,
string query,
int maxResults = 10,
CancellationToken ct = default);
}
/// <summary>
/// Represents a user returned by the Graph API people search.
/// </summary>
public record GraphUserResult(string DisplayName, string UserPrincipalName, string? Mail);
```
</action>
<verify>
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
</verify>
<done>Both interface files exist in Services/, compile without errors, IUserAccessAuditService.AuditUsersAsync and IGraphUserSearchService.SearchUsersAsync are defined with correct signatures.</done>
</task>
</tasks>
<verification>
- `dotnet build SharepointToolbox/SharepointToolbox.csproj` succeeds with 0 errors
- UserAccessEntry.cs contains record with 12 fields and AccessType enum
- IUserAccessAuditService.cs contains AuditUsersAsync method signature
- IGraphUserSearchService.cs contains SearchUsersAsync method signature and GraphUserResult record
</verification>
<success_criteria>
All three files compile cleanly. The contracts are established: downstream plans (07-02 through 07-08) can import UserAccessEntry, AccessType, IUserAccessAuditService, IGraphUserSearchService, and GraphUserResult without ambiguity.
</success_criteria>
<output>
After completion, create `.planning/phases/07-user-access-audit/07-01-SUMMARY.md`
</output>