fix(07): fix people picker selection and audit service authentication
People picker ListBox used MouseBinding which fires before SelectedItem updates, causing null CommandParameter. Replaced with SelectionChanged event handler in code-behind. AuditUsersAsync created TenantProfile with empty ClientId, causing ArgumentException in SessionManager. Added currentProfile parameter to pass the authenticated tenant's ClientId through. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,13 @@ public class UserAccessAuditServiceTests
|
|||||||
FolderDepth: 1,
|
FolderDepth: 1,
|
||||||
IncludeSubsites: false);
|
IncludeSubsites: false);
|
||||||
|
|
||||||
|
private static TenantProfile DefaultProfile => new()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
TenantUrl = "https://contoso.sharepoint.com",
|
||||||
|
ClientId = "test-client-id"
|
||||||
|
};
|
||||||
|
|
||||||
// ── Test 1: Filter by target user login ───────────────────────────────────
|
// ── Test 1: Filter by target user login ───────────────────────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -77,6 +84,7 @@ public class UserAccessAuditServiceTests
|
|||||||
|
|
||||||
var result = await svc.AuditUsersAsync(
|
var result = await svc.AuditUsersAsync(
|
||||||
session.Object,
|
session.Object,
|
||||||
|
DefaultProfile,
|
||||||
new[] { "alice@contoso.com" },
|
new[] { "alice@contoso.com" },
|
||||||
new[] { MakeSite() },
|
new[] { MakeSite() },
|
||||||
DefaultOptions,
|
DefaultOptions,
|
||||||
@@ -103,6 +111,7 @@ public class UserAccessAuditServiceTests
|
|||||||
|
|
||||||
var result = await svc.AuditUsersAsync(
|
var result = await svc.AuditUsersAsync(
|
||||||
session.Object,
|
session.Object,
|
||||||
|
DefaultProfile,
|
||||||
new[] { "alice@contoso.com" },
|
new[] { "alice@contoso.com" },
|
||||||
new[] { MakeSite() },
|
new[] { MakeSite() },
|
||||||
DefaultOptions,
|
DefaultOptions,
|
||||||
@@ -127,6 +136,7 @@ public class UserAccessAuditServiceTests
|
|||||||
|
|
||||||
var result = await svc.AuditUsersAsync(
|
var result = await svc.AuditUsersAsync(
|
||||||
session.Object,
|
session.Object,
|
||||||
|
DefaultProfile,
|
||||||
new[] { "alice@contoso.com" },
|
new[] { "alice@contoso.com" },
|
||||||
new[] { MakeSite() },
|
new[] { MakeSite() },
|
||||||
DefaultOptions,
|
DefaultOptions,
|
||||||
@@ -151,6 +161,7 @@ public class UserAccessAuditServiceTests
|
|||||||
|
|
||||||
var result = await svc.AuditUsersAsync(
|
var result = await svc.AuditUsersAsync(
|
||||||
session.Object,
|
session.Object,
|
||||||
|
DefaultProfile,
|
||||||
new[] { "alice@contoso.com" },
|
new[] { "alice@contoso.com" },
|
||||||
new[] { MakeSite() },
|
new[] { MakeSite() },
|
||||||
DefaultOptions,
|
DefaultOptions,
|
||||||
@@ -175,6 +186,7 @@ public class UserAccessAuditServiceTests
|
|||||||
|
|
||||||
var result = await svc.AuditUsersAsync(
|
var result = await svc.AuditUsersAsync(
|
||||||
session.Object,
|
session.Object,
|
||||||
|
DefaultProfile,
|
||||||
new[] { "alice@contoso.com" },
|
new[] { "alice@contoso.com" },
|
||||||
new[] { MakeSite() },
|
new[] { MakeSite() },
|
||||||
DefaultOptions,
|
DefaultOptions,
|
||||||
@@ -199,6 +211,7 @@ public class UserAccessAuditServiceTests
|
|||||||
|
|
||||||
var result = await svc.AuditUsersAsync(
|
var result = await svc.AuditUsersAsync(
|
||||||
session.Object,
|
session.Object,
|
||||||
|
DefaultProfile,
|
||||||
new[] { "alice@contoso.com" },
|
new[] { "alice@contoso.com" },
|
||||||
new[] { MakeSite() },
|
new[] { MakeSite() },
|
||||||
DefaultOptions,
|
DefaultOptions,
|
||||||
@@ -223,6 +236,7 @@ public class UserAccessAuditServiceTests
|
|||||||
|
|
||||||
var result = await svc.AuditUsersAsync(
|
var result = await svc.AuditUsersAsync(
|
||||||
session.Object,
|
session.Object,
|
||||||
|
DefaultProfile,
|
||||||
new[] { "alice@contoso.com" },
|
new[] { "alice@contoso.com" },
|
||||||
new[] { MakeSite() },
|
new[] { MakeSite() },
|
||||||
DefaultOptions,
|
DefaultOptions,
|
||||||
@@ -248,6 +262,7 @@ public class UserAccessAuditServiceTests
|
|||||||
|
|
||||||
var result = await svc.AuditUsersAsync(
|
var result = await svc.AuditUsersAsync(
|
||||||
session.Object,
|
session.Object,
|
||||||
|
DefaultProfile,
|
||||||
new[] { extLogin },
|
new[] { extLogin },
|
||||||
new[] { MakeSite() },
|
new[] { MakeSite() },
|
||||||
DefaultOptions,
|
DefaultOptions,
|
||||||
@@ -272,6 +287,7 @@ public class UserAccessAuditServiceTests
|
|||||||
|
|
||||||
var result = await svc.AuditUsersAsync(
|
var result = await svc.AuditUsersAsync(
|
||||||
session.Object,
|
session.Object,
|
||||||
|
DefaultProfile,
|
||||||
new[] { "alice@x.com", "bob@x.com" },
|
new[] { "alice@x.com", "bob@x.com" },
|
||||||
new[] { MakeSite() },
|
new[] { MakeSite() },
|
||||||
DefaultOptions,
|
DefaultOptions,
|
||||||
@@ -298,6 +314,7 @@ public class UserAccessAuditServiceTests
|
|||||||
|
|
||||||
var result = await svc.AuditUsersAsync(
|
var result = await svc.AuditUsersAsync(
|
||||||
session.Object,
|
session.Object,
|
||||||
|
DefaultProfile,
|
||||||
new[] { "alice@contoso.com" },
|
new[] { "alice@contoso.com" },
|
||||||
new[] { MakeSite() },
|
new[] { MakeSite() },
|
||||||
DefaultOptions,
|
DefaultOptions,
|
||||||
@@ -324,6 +341,7 @@ public class UserAccessAuditServiceTests
|
|||||||
|
|
||||||
var result = await svc.AuditUsersAsync(
|
var result = await svc.AuditUsersAsync(
|
||||||
session.Object,
|
session.Object,
|
||||||
|
DefaultProfile,
|
||||||
Array.Empty<string>(),
|
Array.Empty<string>(),
|
||||||
new[] { MakeSite() },
|
new[] { MakeSite() },
|
||||||
DefaultOptions,
|
DefaultOptions,
|
||||||
@@ -367,6 +385,7 @@ public class UserAccessAuditServiceTests
|
|||||||
|
|
||||||
var result = await svc.AuditUsersAsync(
|
var result = await svc.AuditUsersAsync(
|
||||||
mockSession.Object,
|
mockSession.Object,
|
||||||
|
DefaultProfile,
|
||||||
new[] { "alice@contoso.com" },
|
new[] { "alice@contoso.com" },
|
||||||
sites,
|
sites,
|
||||||
DefaultOptions,
|
DefaultOptions,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ public class UserAccessAuditViewModelTests
|
|||||||
mockAudit
|
mockAudit
|
||||||
.Setup(s => s.AuditUsersAsync(
|
.Setup(s => s.AuditUsersAsync(
|
||||||
It.IsAny<ISessionManager>(),
|
It.IsAny<ISessionManager>(),
|
||||||
|
It.IsAny<TenantProfile>(),
|
||||||
It.IsAny<IReadOnlyList<string>>(),
|
It.IsAny<IReadOnlyList<string>>(),
|
||||||
It.IsAny<IReadOnlyList<SiteInfo>>(),
|
It.IsAny<IReadOnlyList<SiteInfo>>(),
|
||||||
It.IsAny<ScanOptions>(),
|
It.IsAny<ScanOptions>(),
|
||||||
@@ -65,6 +66,14 @@ public class UserAccessAuditViewModelTests
|
|||||||
mockSession.Object,
|
mockSession.Object,
|
||||||
NullLogger<FeatureViewModelBase>.Instance);
|
NullLogger<FeatureViewModelBase>.Instance);
|
||||||
|
|
||||||
|
// Set a default profile so RunOperationAsync doesn't early-return
|
||||||
|
vm._currentProfile = new TenantProfile
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
TenantUrl = "https://contoso.sharepoint.com",
|
||||||
|
ClientId = "test-client-id"
|
||||||
|
};
|
||||||
|
|
||||||
return (vm, mockAudit, mockGraph);
|
return (vm, mockAudit, mockGraph);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +92,7 @@ public class UserAccessAuditViewModelTests
|
|||||||
auditMock.Verify(
|
auditMock.Verify(
|
||||||
s => s.AuditUsersAsync(
|
s => s.AuditUsersAsync(
|
||||||
It.IsAny<ISessionManager>(),
|
It.IsAny<ISessionManager>(),
|
||||||
|
It.IsAny<TenantProfile>(),
|
||||||
It.IsAny<IReadOnlyList<string>>(),
|
It.IsAny<IReadOnlyList<string>>(),
|
||||||
It.IsAny<IReadOnlyList<SiteInfo>>(),
|
It.IsAny<IReadOnlyList<SiteInfo>>(),
|
||||||
It.IsAny<ScanOptions>(),
|
It.IsAny<ScanOptions>(),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public interface IUserAccessAuditService
|
|||||||
/// <returns>Flat list of access entries for the target users.</returns>
|
/// <returns>Flat list of access entries for the target users.</returns>
|
||||||
Task<IReadOnlyList<UserAccessEntry>> AuditUsersAsync(
|
Task<IReadOnlyList<UserAccessEntry>> AuditUsersAsync(
|
||||||
ISessionManager sessionManager,
|
ISessionManager sessionManager,
|
||||||
|
TenantProfile currentProfile,
|
||||||
IReadOnlyList<string> targetUserLogins,
|
IReadOnlyList<string> targetUserLogins,
|
||||||
IReadOnlyList<SiteInfo> sites,
|
IReadOnlyList<SiteInfo> sites,
|
||||||
ScanOptions options,
|
ScanOptions options,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class UserAccessAuditService : IUserAccessAuditService
|
|||||||
|
|
||||||
public async Task<IReadOnlyList<UserAccessEntry>> AuditUsersAsync(
|
public async Task<IReadOnlyList<UserAccessEntry>> AuditUsersAsync(
|
||||||
ISessionManager sessionManager,
|
ISessionManager sessionManager,
|
||||||
|
TenantProfile currentProfile,
|
||||||
IReadOnlyList<string> targetUserLogins,
|
IReadOnlyList<string> targetUserLogins,
|
||||||
IReadOnlyList<SiteInfo> sites,
|
IReadOnlyList<SiteInfo> sites,
|
||||||
ScanOptions options,
|
ScanOptions options,
|
||||||
@@ -53,7 +54,7 @@ public class UserAccessAuditService : IUserAccessAuditService
|
|||||||
var profile = new TenantProfile
|
var profile = new TenantProfile
|
||||||
{
|
{
|
||||||
TenantUrl = site.Url,
|
TenantUrl = site.Url,
|
||||||
ClientId = string.Empty, // Will be set by SessionManager from cached session
|
ClientId = currentProfile.ClientId,
|
||||||
Name = site.Title
|
Name = site.Title
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -229,8 +229,15 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
|
|||||||
FolderDepth: 1,
|
FolderDepth: 1,
|
||||||
IncludeSubsites: IncludeSubsites);
|
IncludeSubsites: IncludeSubsites);
|
||||||
|
|
||||||
|
if (_currentProfile == null)
|
||||||
|
{
|
||||||
|
StatusMessage = "No tenant profile selected. Please connect first.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var entries = await _auditService.AuditUsersAsync(
|
var entries = await _auditService.AuditUsersAsync(
|
||||||
_sessionManager,
|
_sessionManager,
|
||||||
|
_currentProfile,
|
||||||
userLogins,
|
userLogins,
|
||||||
effectiveSites,
|
effectiveSites,
|
||||||
scanOptions,
|
scanOptions,
|
||||||
|
|||||||
@@ -31,8 +31,10 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</TextBlock.Style>
|
</TextBlock.Style>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<ListBox ItemsSource="{Binding SearchResults}" MaxHeight="120"
|
<ListBox x:Name="SearchResultsListBox"
|
||||||
ScrollViewer.VerticalScrollBarVisibility="Auto" Margin="0,0,0,4">
|
ItemsSource="{Binding SearchResults}" MaxHeight="120"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto" Margin="0,0,0,4"
|
||||||
|
SelectionChanged="SearchResultsListBox_SelectionChanged">
|
||||||
<ListBox.Style>
|
<ListBox.Style>
|
||||||
<Style TargetType="ListBox">
|
<Style TargetType="ListBox">
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
@@ -54,11 +56,6 @@
|
|||||||
</TextBlock>
|
</TextBlock>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
<ListBox.InputBindings>
|
|
||||||
<MouseBinding MouseAction="LeftClick"
|
|
||||||
Command="{Binding AddUserCommand}"
|
|
||||||
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=ListBox}, Path=SelectedItem}" />
|
|
||||||
</ListBox.InputBindings>
|
|
||||||
</ListBox>
|
</ListBox>
|
||||||
<ItemsControl ItemsSource="{Binding SelectedUsers}" Margin="0,0,0,4">
|
<ItemsControl ItemsSource="{Binding SelectedUsers}" Margin="0,0,0,4">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using SharepointToolbox.Services;
|
||||||
using SharepointToolbox.ViewModels.Tabs;
|
using SharepointToolbox.ViewModels.Tabs;
|
||||||
|
|
||||||
namespace SharepointToolbox.Views.Tabs;
|
namespace SharepointToolbox.Views.Tabs;
|
||||||
@@ -10,4 +11,17 @@ public partial class UserAccessAuditView : UserControl
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
DataContext = viewModel;
|
DataContext = viewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SearchResultsListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is ListBox listBox && listBox.SelectedItem is GraphUserResult user)
|
||||||
|
{
|
||||||
|
var vm = (UserAccessAuditViewModel)DataContext;
|
||||||
|
if (vm.AddUserCommand.CanExecute(user))
|
||||||
|
vm.AddUserCommand.Execute(user);
|
||||||
|
|
||||||
|
// Clear selection so the same item can be re-selected if needed
|
||||||
|
listBox.SelectedItem = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user