---
phase: 18-auto-take-ownership
plan: 02
type: execute
wave: 2
depends_on: ["18-01"]
files_modified:
  - SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs
  - SharepointToolbox/Views/Tabs/PermissionsView.xaml
  - SharepointToolbox/Localization/Strings.resx
  - SharepointToolbox/Localization/Strings.fr.resx
  - SharepointToolbox.Tests/ViewModels/PermissionsViewModelOwnershipTests.cs
autonomous: true
requirements:
  - OWN-02
must_haves:
  truths:
    - "When toggle OFF, access-denied exceptions propagate normally (no elevation attempt)"
    - "When toggle ON and scan hits access denied, app calls ElevateAsync once then retries ScanSiteAsync"
    - "Successful scans never call ElevateAsync"
    - "Auto-elevated entries have WasAutoElevated=true in the results"
    - "Auto-elevated rows are visually distinct in the DataGrid (amber highlight)"
  artifacts:
    - path: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs"
      provides: "Scan-loop catch/elevate/retry logic"
      contains: "ServerUnauthorizedAccessException"
    - path: "SharepointToolbox/Views/Tabs/PermissionsView.xaml"
      provides: "Visual differentiation for auto-elevated rows"
      contains: "WasAutoElevated"
    - path: "SharepointToolbox.Tests/ViewModels/PermissionsViewModelOwnershipTests.cs"
      provides: "Unit tests for elevation behavior"
      contains: "PermissionsViewModelOwnershipTests"
  key_links:
    - from: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs"
      to: "SharepointToolbox/Services/IOwnershipElevationService.cs"
      via: "ElevateAsync call inside catch block"
      pattern: "ElevateAsync"
    - from: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs"
      to: "SharepointToolbox/Services/SettingsService.cs"
      via: "Reading AutoTakeOwnership toggle state"
      pattern: "AutoTakeOwnership"
    - from: "SharepointToolbox/Views/Tabs/PermissionsView.xaml"
      to: "SharepointToolbox/Core/Models/PermissionEntry.cs"
      via: "DataTrigger on WasAutoElevated"
      pattern: "WasAutoElevated"
---

<objective>
Wire the auto-elevation logic into the permission scan loop: catch ServerUnauthorizedAccessException, call Tenant.SetSiteAdmin via IOwnershipElevationService, retry the scan, and tag returned entries with WasAutoElevated=true. Add visual differentiation in the DataGrid.

Purpose: Complete OWN-02 — scans no longer block on access-denied sites when the toggle is ON.
Output: PermissionsViewModel catches access denied and auto-elevates, DataGrid shows amber highlight for elevated rows.
</objective>

<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/18-auto-take-ownership/18-RESEARCH.md
@.planning/phases/18-auto-take-ownership/18-01-SUMMARY.md

<interfaces>
<!-- From Plan 01 outputs -->

From SharepointToolbox/Services/IOwnershipElevationService.cs:
```csharp
public interface IOwnershipElevationService
{
    Task ElevateAsync(ClientContext tenantAdminCtx, string siteUrl, string loginName, CancellationToken ct);
}
```

From SharepointToolbox/Core/Models/PermissionEntry.cs (after Plan 01):
```csharp
public record PermissionEntry(
    string ObjectType, string Title, string Url,
    bool HasUniquePermissions, string Users, string UserLogins,
    string PermissionLevels, string GrantedThrough, string PrincipalType,
    bool WasAutoElevated = false
);
```

From SharepointToolbox/Core/Models/AppSettings.cs (after Plan 01):
```csharp
public class AppSettings
{
    public string DataFolder { get; set; } = string.Empty;
    public string Lang { get; set; } = "en";
    public bool AutoTakeOwnership { get; set; } = false;
}
```

From SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs (existing scan loop):
```csharp
// Lines 202-237: RunOperationAsync — foreach (var url in nonEmpty)
// Creates TenantProfile per URL, gets ctx via _sessionManager.GetOrCreateContextAsync
// Calls _permissionsService.ScanSiteAsync(ctx, scanOptions, progress, ct)
// Adds entries to allEntries

// Constructor (production): IPermissionsService, ISiteListService, ISessionManager,
//   CsvExportService, HtmlExportService, IBrandingService, ILogger, ISharePointGroupResolver?
// Constructor (test): IPermissionsService, ISiteListService, ISessionManager, ILogger, IBrandingService?
```

From SharepointToolbox/App.xaml.cs (DI):
```csharp
// Line 119-124: PermissionsViewModel registered as AddTransient
// IOwnershipElevationService registered by Plan 01
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Scan-loop elevation logic + PermissionsViewModel wiring + tests</name>
  <files>
    SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs,
    SharepointToolbox/App.xaml.cs,
    SharepointToolbox.Tests/ViewModels/PermissionsViewModelOwnershipTests.cs
  </files>
  <behavior>
    - Test: When AutoTakeOwnership=false and ScanSiteAsync throws ServerUnauthorizedAccessException, the exception propagates (not caught)
    - Test: When AutoTakeOwnership=true and ScanSiteAsync throws ServerUnauthorizedAccessException, ElevateAsync is called once with correct args, then ScanSiteAsync retried
    - Test: When ScanSiteAsync succeeds on first try, ElevateAsync is never called
    - Test: After successful elevation+retry, returned PermissionEntry items have WasAutoElevated=true
    - Test: If elevation itself throws, the exception propagates (no infinite retry)
  </behavior>
  <action>
1. Add `IOwnershipElevationService? _ownershipService` and `SettingsService _settingsService` fields to `PermissionsViewModel`. Inject `IOwnershipElevationService?` as an optional last parameter in the production constructor (matching the `ISharePointGroupResolver?` pattern). Inject `SettingsService` as a required parameter.

2. Update both constructors:
   - Production constructor: add `SettingsService settingsService` (required) and `IOwnershipElevationService? ownershipService = null` (optional, last).
   - Test constructor: add `SettingsService? settingsService = null` and `IOwnershipElevationService? ownershipService = null` as optional params.

3. Update DI registration in `App.xaml.cs` — no change needed if using optional injection (DI resolves registered services automatically). But verify the constructor parameter order matches DI expectations. If needed, add explicit resolution.

4. Add a `DeriveAdminUrl` internal static helper method in `PermissionsViewModel`:
   ```csharp
   internal static string DeriveAdminUrl(string tenantUrl)
   {
       var uri = new Uri(tenantUrl.TrimEnd('/'));
       var host = uri.Host;
       if (host.Contains("-admin.sharepoint.com", StringComparison.OrdinalIgnoreCase))
           return tenantUrl;
       var adminHost = host.Replace(".sharepoint.com", "-admin.sharepoint.com",
           StringComparison.OrdinalIgnoreCase);
       return $"{uri.Scheme}://{adminHost}";
   }
   ```

5. Modify `RunOperationAsync` scan loop (lines 221-237). Replace the direct `ScanSiteAsync` call with a try/catch pattern. Catch BOTH `ServerUnauthorizedAccessException` and `WebException` with 403 status (see Pitfall 4 in RESEARCH.md). Use `when` filter to check toggle state:

   ```csharp
   foreach (var url in nonEmpty)
   {
       ct.ThrowIfCancellationRequested();
       progress.Report(new OperationProgress(i, nonEmpty.Count, $"Scanning {url}..."));

       var profile = new TenantProfile
       {
           TenantUrl = url,
           ClientId = _currentProfile?.ClientId ?? string.Empty,
           Name = _currentProfile?.Name ?? string.Empty
       };

       bool wasElevated = false;
       IReadOnlyList<PermissionEntry> siteEntries;

       try
       {
           var ctx = await _sessionManager.GetOrCreateContextAsync(profile, ct);
           siteEntries = await _permissionsService.ScanSiteAsync(ctx, scanOptions, progress, ct);
       }
       catch (Exception ex) when (IsAccessDenied(ex) && _ownershipService != null && await IsAutoTakeOwnershipEnabled())
       {
           _logger.LogWarning("Access denied on {Url}, auto-elevating ownership...", url);
           var adminUrl = DeriveAdminUrl(_currentProfile?.TenantUrl ?? url);
           var adminProfile = new TenantProfile
           {
               TenantUrl = adminUrl,
               ClientId = _currentProfile?.ClientId ?? string.Empty,
               Name = _currentProfile?.Name ?? string.Empty
           };
           var adminCtx = await _sessionManager.GetOrCreateContextAsync(adminProfile, ct);
           // Get current user login from the site context
           var siteProfile = profile;
           var siteCtx = await _sessionManager.GetOrCreateContextAsync(siteProfile, ct);
           siteCtx.Load(siteCtx.Web, w => w.CurrentUser);
           await siteCtx.ExecuteQueryAsync();
           var loginName = siteCtx.Web.CurrentUser.LoginName;

           await _ownershipService.ElevateAsync(adminCtx, url, loginName, ct);

           // Retry scan with fresh context
           var retryCtx = await _sessionManager.GetOrCreateContextAsync(profile, ct);
           siteEntries = await _permissionsService.ScanSiteAsync(retryCtx, scanOptions, progress, ct);
           wasElevated = true;
       }

       if (wasElevated)
           allEntries.AddRange(siteEntries.Select(e => e with { WasAutoElevated = true }));
       else
           allEntries.AddRange(siteEntries);

       i++;
   }
   ```

6. Add the `IsAccessDenied` helper (private static):
   ```csharp
   private static bool IsAccessDenied(Exception ex)
   {
       if (ex is Microsoft.SharePoint.Client.ServerUnauthorizedAccessException) return true;
       if (ex is System.Net.WebException webEx && webEx.Response is System.Net.HttpWebResponse resp
           && resp.StatusCode == System.Net.HttpStatusCode.Forbidden) return true;
       return false;
   }
   ```

7. Add the `IsAutoTakeOwnershipEnabled` helper (private async):
   ```csharp
   private async Task<bool> IsAutoTakeOwnershipEnabled()
   {
       if (_settingsService == null) return false;
       var settings = await _settingsService.GetSettingsAsync();
       return settings.AutoTakeOwnership;
   }
   ```

8. Create `PermissionsViewModelOwnershipTests.cs` with mock IPermissionsService, ISessionManager, IOwnershipElevationService, and SettingsService. Test all 5 behaviors listed above. Use the internal test constructor. For the "throws ServerUnauthorizedAccessException" test, configure mock ScanSiteAsync to throw on first call then return entries on second call.

IMPORTANT: The `when` clause with `await` requires C# 8+ async exception filters. If the compiler rejects `await` in `when`, refactor to check settings BEFORE the try block: `var autoOwn = await IsAutoTakeOwnershipEnabled();` then use `when (IsAccessDenied(ex) && _ownershipService != null && autoOwn)`.
  </action>
  <verify>
    <automated>dotnet build SharepointToolbox.sln && dotnet test SharepointToolbox.Tests --no-build --filter "FullyQualifiedName~PermissionsViewModelOwnership"</automated>
  </verify>
  <done>PermissionsViewModel catches access-denied during scans, auto-elevates via IOwnershipElevationService when toggle is ON, retries the scan, and tags entries with WasAutoElevated=true. Toggle OFF = no change in behavior. All 5 test scenarios pass.</done>
</task>

<task type="auto">
  <name>Task 2: DataGrid visual differentiation + localization for elevated rows</name>
  <files>
    SharepointToolbox/Views/Tabs/PermissionsView.xaml,
    SharepointToolbox/Localization/Strings.resx,
    SharepointToolbox/Localization/Strings.fr.resx
  </files>
  <action>
1. In `PermissionsView.xaml`, add a DataTrigger for `WasAutoElevated` inside the `DataGrid.RowStyle` (after the existing RiskLevel triggers, around line 234):
   ```xml
   <DataTrigger Binding="{Binding WasAutoElevated}" Value="True">
     <Setter Property="Background" Value="#FFF9E6" />
     <Setter Property="ToolTip" Value="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[permissions.elevated.tooltip]}" />
   </DataTrigger>
   ```

   Note: WasAutoElevated is on `PermissionEntry` (raw mode). When simplified mode is active, `SimplifiedPermissionEntry` wraps `PermissionEntry` — check whether `SimplifiedPermissionEntry.WrapAll` preserves the `WasAutoElevated` flag. If `SimplifiedPermissionEntry` does not expose it, the trigger only applies in raw mode (acceptable for v2.3).

2. Add a small indicator column in the DataGrid columns (before "Object Type"), showing a lock icon for elevated rows:
   ```xml
   <DataGridTemplateColumn Header="" Width="24" IsReadOnly="True">
     <DataGridTemplateColumn.CellTemplate>
       <DataTemplate>
         <TextBlock Text="&#x1F513;" FontSize="12" HorizontalAlignment="Center"
                    Visibility="{Binding WasAutoElevated, Converter={StaticResource BoolToVisibilityConverter}}" />
       </DataTemplate>
     </DataGridTemplateColumn.CellTemplate>
   </DataGridTemplateColumn>
   ```

   If `BoolToVisibilityConverter` is not registered, use a DataTrigger style instead:
   ```xml
   <DataGridTemplateColumn Header="" Width="24" IsReadOnly="True">
     <DataGridTemplateColumn.CellTemplate>
       <DataTemplate>
         <TextBlock Text="&#x26A0;" FontSize="12" HorizontalAlignment="Center">
           <TextBlock.Style>
             <Style TargetType="TextBlock">
               <Setter Property="Visibility" Value="Collapsed" />
               <Style.Triggers>
                 <DataTrigger Binding="{Binding WasAutoElevated}" Value="True">
                   <Setter Property="Visibility" Value="Visible" />
                 </DataTrigger>
               </Style.Triggers>
             </Style>
           </TextBlock.Style>
         </TextBlock>
       </DataTemplate>
     </DataGridTemplateColumn.CellTemplate>
   </DataGridTemplateColumn>
   ```

3. Add localization keys to `Strings.resx`:
   - `permissions.elevated.tooltip` = "This site was automatically elevated — ownership was taken to complete the scan"

4. Add French translation to `Strings.fr.resx`:
   - `permissions.elevated.tooltip` = "Ce site a ete eleve automatiquement — la propriete a ete prise pour completer le scan"
  </action>
  <verify>
    <automated>dotnet build SharepointToolbox.sln && dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~LocaleCompleteness" --no-build</automated>
  </verify>
  <done>DataGrid rows with WasAutoElevated=true show amber background + warning icon column. Tooltip explains the elevation. EN + FR localization keys present. LocaleCompletenessTests pass.</done>
</task>

</tasks>

<verification>
1. `dotnet build SharepointToolbox.sln` — zero errors
2. `dotnet test SharepointToolbox.Tests --no-build` — full suite green
3. `dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~PermissionsViewModelOwnership" --no-build` — elevation tests pass
4. `dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~LocaleCompleteness" --no-build` — locale keys complete
</verification>

<success_criteria>
- Access-denied during scan with toggle ON triggers auto-elevation + retry
- Access-denied during scan with toggle OFF propagates normally
- Successful scans never attempt elevation
- Elevated entries tagged with WasAutoElevated=true
- Elevated rows visually distinct in DataGrid (amber + icon)
- Full test suite green with no regressions
</success_criteria>

<output>
After completion, create `.planning/phases/18-auto-take-ownership/18-02-SUMMARY.md`
</output>
