mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
fix: Add UserIdentityScopeSessionStoreOptions to avoid future breaking changes
This commit is contained in:
+9
-13
@@ -32,21 +32,17 @@ public class UserIdentityScopedSessionStore : DelegatingAgentSessionStore
|
||||
/// <param name="contextAccessor">
|
||||
/// The <see cref="IHttpContextAccessor"/> used to retrieve the current user's claims.
|
||||
/// </param>
|
||||
/// <param name="claimType">
|
||||
/// The claim type to extract from the user's identity for scoping. Defaults to <see cref="ClaimsIdentity.DefaultNameClaimType"/>.
|
||||
/// </param>
|
||||
/// <param name="strict">
|
||||
/// If <see langword="true"/>, an exception is thrown when the specified claim is not found.
|
||||
/// If <see langword="false"/>, the conversation ID is passed through unmodified when the claim is absent.
|
||||
/// </param>
|
||||
public UserIdentityScopedSessionStore(AgentSessionStore innerStore,
|
||||
IHttpContextAccessor? contextAccessor,
|
||||
string claimType = ClaimsIdentity.DefaultNameClaimType,
|
||||
bool strict = true) : base(innerStore)
|
||||
/// <param name="options">The options for configuring the session store. If null, defaults are used.</param>
|
||||
public UserIdentityScopedSessionStore(
|
||||
AgentSessionStore innerStore,
|
||||
IHttpContextAccessor? contextAccessor,
|
||||
UserIdentityScopedSessionStoreOptions? options = null) : base(innerStore)
|
||||
{
|
||||
options ??= new UserIdentityScopedSessionStoreOptions();
|
||||
|
||||
this._httpContextAccessor = contextAccessor;
|
||||
this._claimType = Throw.IfNullOrWhitespace(claimType);
|
||||
this._strict = strict;
|
||||
this._claimType = Throw.IfNullOrWhitespace(options.ClaimType);
|
||||
this._strict = options.Strict;
|
||||
}
|
||||
|
||||
private string? GetScopeFromIdentity()
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring <see cref="UserIdentityScopedSessionStore"/>.
|
||||
/// </summary>
|
||||
public class UserIdentityScopedSessionStoreOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the claim type to extract from the user's identity for scoping.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Defaults to <see cref="ClaimsIdentity.DefaultNameClaimType"/>.
|
||||
/// </remarks>
|
||||
public string ClaimType { get; set; } = ClaimsIdentity.DefaultNameClaimType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether an exception should be thrown when the specified claim is not found.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <see langword="true"/>, an exception is thrown when the specified claim is not found.
|
||||
/// If <see langword="false"/>, the conversation ID is passed through unmodified when the claim is absent.
|
||||
/// Defaults to <see langword="true"/>.
|
||||
/// </remarks>
|
||||
public bool Strict { get; set; } = true;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace Microsoft.Agents.AI.Hosting;
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="DelegatingAgentSessionStore"/> implements the decorator pattern for <see cref="AgentSessionStore"/>s,
|
||||
/// enabling the creation of pipeliens where each layer can add functionality while delegating core operations to an
|
||||
/// enabling the creation of pipelines where each layer can add functionality while delegating core operations to an
|
||||
/// underlying store.
|
||||
/// </para>
|
||||
/// <para>
|
||||
@@ -23,7 +23,7 @@ namespace Microsoft.Agents.AI.Hosting;
|
||||
/// interface.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class DelegatingAgentSessionStore : AgentSessionStore
|
||||
public abstract class DelegatingAgentSessionStore : AgentSessionStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DelegatingAgentSessionStore"/> class with the specified inner
|
||||
|
||||
+48
-21
@@ -52,7 +52,23 @@ public class UserIdentityScopedSessionStoreTests
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RequiresInnerStore() =>
|
||||
Assert.Throws<ArgumentNullException>("innerStore", () => new UserIdentityScopedSessionStore(null!, this._httpContextAccessorMock.Object));
|
||||
Assert.Throws<ArgumentNullException>("innerStore", () => new UserIdentityScopedSessionStore(null!, this._httpContextAccessorMock.Object, CreateOptions()));
|
||||
|
||||
/// <summary>
|
||||
/// Verify that constructor uses default options when options is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void UsesDefaultOptionsWhenNull()
|
||||
{
|
||||
// Arrange
|
||||
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, TestUserId);
|
||||
|
||||
// Act - should not throw and use default claim type
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, options: null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(store);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that constructor accepts null IHttpContextAccessor.
|
||||
@@ -61,7 +77,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
public void Constructor_WithNullHttpContextAccessor_DoesNotThrow()
|
||||
{
|
||||
// Act & Assert - should not throw
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, contextAccessor: null, strict: false);
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, contextAccessor: null, CreateOptions(strict: false));
|
||||
Assert.NotNull(store);
|
||||
}
|
||||
|
||||
@@ -70,21 +86,21 @@ public class UserIdentityScopedSessionStoreTests
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RequiresClaimType_NotNull() =>
|
||||
Assert.Throws<ArgumentNullException>("claimType", () => new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, claimType: null!));
|
||||
Assert.Throws<ArgumentNullException>("options.ClaimType", () => new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions(claimType: null!)));
|
||||
|
||||
/// <summary>
|
||||
/// Verify that constructor throws ArgumentException when claimType is empty.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RequiresClaimType_NotEmpty() =>
|
||||
Assert.Throws<ArgumentException>("claimType", () => new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, claimType: string.Empty));
|
||||
Assert.Throws<ArgumentException>("options.ClaimType", () => new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions(claimType: string.Empty)));
|
||||
|
||||
/// <summary>
|
||||
/// Verify that constructor throws ArgumentException when claimType is whitespace.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RequiresClaimType_NotWhitespace() =>
|
||||
Assert.Throws<ArgumentException>("claimType", () => new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, claimType: " "));
|
||||
Assert.Throws<ArgumentException>("options.ClaimType", () => new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions(claimType: " ")));
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -98,7 +114,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
{
|
||||
// Arrange
|
||||
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, TestUserId);
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
|
||||
|
||||
// Act
|
||||
await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
|
||||
@@ -123,7 +139,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
var store = new UserIdentityScopedSessionStore(
|
||||
this._innerStoreMock.Object,
|
||||
this._httpContextAccessorMock.Object,
|
||||
claimType: CustomClaimType);
|
||||
CreateOptions(claimType: CustomClaimType));
|
||||
|
||||
// Act
|
||||
await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
|
||||
@@ -148,7 +164,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
var store = new UserIdentityScopedSessionStore(
|
||||
this._innerStoreMock.Object,
|
||||
this._httpContextAccessorMock.Object,
|
||||
strict: true);
|
||||
CreateOptions(strict: true));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
@@ -168,7 +184,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
var store = new UserIdentityScopedSessionStore(
|
||||
this._innerStoreMock.Object,
|
||||
this._httpContextAccessorMock.Object,
|
||||
strict: false);
|
||||
CreateOptions(strict: false));
|
||||
|
||||
// Act - should not throw
|
||||
await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
|
||||
@@ -190,7 +206,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
{
|
||||
// Arrange
|
||||
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, TestUserId);
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
|
||||
|
||||
// Act
|
||||
var result = await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
|
||||
@@ -211,7 +227,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
{
|
||||
// Arrange
|
||||
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, TestUserId);
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
|
||||
var sessionToSave = new TestAgentSession();
|
||||
|
||||
// Act
|
||||
@@ -238,7 +254,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
var store = new UserIdentityScopedSessionStore(
|
||||
this._innerStoreMock.Object,
|
||||
this._httpContextAccessorMock.Object,
|
||||
claimType: CustomClaimType);
|
||||
CreateOptions(claimType: CustomClaimType));
|
||||
var sessionToSave = new TestAgentSession();
|
||||
|
||||
// Act
|
||||
@@ -265,7 +281,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
var store = new UserIdentityScopedSessionStore(
|
||||
this._innerStoreMock.Object,
|
||||
this._httpContextAccessorMock.Object,
|
||||
strict: true);
|
||||
CreateOptions(strict: true));
|
||||
var sessionToSave = new TestAgentSession();
|
||||
|
||||
// Act & Assert
|
||||
@@ -286,7 +302,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
var store = new UserIdentityScopedSessionStore(
|
||||
this._innerStoreMock.Object,
|
||||
this._httpContextAccessorMock.Object,
|
||||
strict: false);
|
||||
CreateOptions(strict: false));
|
||||
var sessionToSave = new TestAgentSession();
|
||||
|
||||
// Act - should not throw
|
||||
@@ -317,7 +333,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
var store = new UserIdentityScopedSessionStore(
|
||||
this._innerStoreMock.Object,
|
||||
this._httpContextAccessorMock.Object,
|
||||
strict: true);
|
||||
CreateOptions(strict: true));
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
@@ -335,7 +351,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
var store = new UserIdentityScopedSessionStore(
|
||||
this._innerStoreMock.Object,
|
||||
this._httpContextAccessorMock.Object,
|
||||
strict: false);
|
||||
CreateOptions(strict: false));
|
||||
|
||||
// Act - should not throw
|
||||
await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
|
||||
@@ -376,12 +392,12 @@ public class UserIdentityScopedSessionStoreTests
|
||||
|
||||
// Act - User 1
|
||||
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, User1);
|
||||
var store1 = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
|
||||
var store1 = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
|
||||
await store1.GetSessionAsync(this._agentMock.Object, TestConversationId);
|
||||
|
||||
// Act - User 2
|
||||
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, User2);
|
||||
var store2 = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
|
||||
var store2 = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
|
||||
await store2.GetSessionAsync(this._agentMock.Object, TestConversationId);
|
||||
|
||||
// Assert
|
||||
@@ -399,7 +415,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
// Arrange
|
||||
const string UserIdWithColon = "user:with:colons";
|
||||
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, UserIdWithColon);
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
|
||||
|
||||
// Act
|
||||
await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
|
||||
@@ -422,7 +438,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
// Arrange
|
||||
const string UserIdWithBackslash = @"domain\user";
|
||||
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, UserIdWithBackslash);
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
|
||||
|
||||
// Act
|
||||
await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
|
||||
@@ -445,7 +461,7 @@ public class UserIdentityScopedSessionStoreTests
|
||||
// Arrange
|
||||
const string UserIdWithBoth = @"domain\user:role";
|
||||
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, UserIdWithBoth);
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
|
||||
var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
|
||||
|
||||
// Act
|
||||
await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
|
||||
@@ -463,6 +479,17 @@ public class UserIdentityScopedSessionStoreTests
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static UserIdentityScopedSessionStoreOptions CreateOptions(
|
||||
string? claimType = ClaimsIdentity.DefaultNameClaimType,
|
||||
bool strict = true)
|
||||
{
|
||||
return new UserIdentityScopedSessionStoreOptions
|
||||
{
|
||||
ClaimType = claimType!,
|
||||
Strict = strict
|
||||
};
|
||||
}
|
||||
|
||||
private void SetupHttpContextWithClaim(string claimType, string claimValue)
|
||||
{
|
||||
var claims = new[] { new Claim(claimType, claimValue) };
|
||||
|
||||
Reference in New Issue
Block a user