CoreRbacContractTests.cs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. using System.Security.Claims;
  2. using Admin.NET.Core;
  3. using Admin.NET.Core.Service;
  4. using Microsoft.AspNetCore.Http;
  5. using Microsoft.AspNetCore.Mvc;
  6. using Microsoft.AspNetCore.Mvc.Abstractions;
  7. using Microsoft.AspNetCore.Mvc.Filters;
  8. using Microsoft.AspNetCore.Mvc.ModelBinding;
  9. using Microsoft.AspNetCore.Routing;
  10. using Microsoft.Extensions.DependencyInjection;
  11. using Xunit;
  12. namespace Admin.NET.Test.Security;
  13. public class CoreRbacContractTests
  14. {
  15. [Theory]
  16. [InlineData("/aidop/s8/dashboard", "/aidop/s8/dashboard")]
  17. [InlineData(" /aidop/s8/dashboard/ ", "/aidop/s8/dashboard")]
  18. [InlineData("/", "/")]
  19. public void NormalizeMenuPath_AcceptsOnlyCanonicalPcPaths(string input, string expected)
  20. {
  21. Assert.Equal(expected, SysMenuService.NormalizeMenuPath(input));
  22. }
  23. [Theory]
  24. [InlineData(null)]
  25. [InlineData("")]
  26. [InlineData("aidop/s8/dashboard")]
  27. [InlineData("//external.example/path")]
  28. [InlineData("/aidop/s8/dashboard?tenant=other")]
  29. [InlineData("/aidop/s8/dashboard#fragment")]
  30. public void NormalizeMenuPath_RejectsAmbiguousOrExternalPaths(string input)
  31. {
  32. Assert.Null(SysMenuService.NormalizeMenuPath(input));
  33. }
  34. [Fact]
  35. public async Task Attribute_AllowsAuthenticatedUser_WhenServiceGrantsPath()
  36. {
  37. var context = CreateContext(new StubMenuAccessService(true), authenticated: true);
  38. await new RequirePcMenuPathAttribute("/aidop/s8/dashboard").OnAuthorizationAsync(context);
  39. Assert.Null(context.Result);
  40. }
  41. [Fact]
  42. public async Task Attribute_AllowsWhenAnyConfiguredPathIsGranted()
  43. {
  44. var context = CreateContext(
  45. new StubMenuAccessService(grantedPath: "/aidop/s7/fqc/result-list"),
  46. authenticated: true);
  47. await new RequirePcMenuPathAttribute(
  48. "/aidop/s7/fqc/task-list",
  49. "/aidop/s7/fqc/result-list").OnAuthorizationAsync(context);
  50. Assert.Null(context.Result);
  51. }
  52. [Fact]
  53. public async Task Attribute_Returns401_WhenAuthenticationIsMissing()
  54. {
  55. var context = CreateContext(new StubMenuAccessService(true), authenticated: false);
  56. await new RequirePcMenuPathAttribute("/aidop/s8/dashboard").OnAuthorizationAsync(context);
  57. Assert.IsType<UnauthorizedResult>(context.Result);
  58. }
  59. [Fact]
  60. public async Task Attribute_FailsClosed_WhenServiceDeniesIsMissingOrThrows()
  61. {
  62. foreach (var accessService in new ICurrentUserMenuAccessService[]
  63. {
  64. new StubMenuAccessService(false),
  65. new StubMenuAccessService(exception: new InvalidOperationException("db unavailable")),
  66. null
  67. })
  68. {
  69. var context = CreateContext(accessService, authenticated: true);
  70. await new RequirePcMenuPathAttribute("/aidop/s8/dashboard").OnAuthorizationAsync(context);
  71. var result = Assert.IsType<ObjectResult>(context.Result);
  72. Assert.Equal(StatusCodes.Status403Forbidden, result.StatusCode);
  73. }
  74. }
  75. [Fact]
  76. public void Attribute_RejectsEmptyContract()
  77. {
  78. Assert.Throws<ArgumentException>(() => new RequirePcMenuPathAttribute(" "));
  79. }
  80. private static AuthorizationFilterContext CreateContext(
  81. ICurrentUserMenuAccessService accessService,
  82. bool authenticated)
  83. {
  84. var services = new ServiceCollection();
  85. if (accessService != null)
  86. services.AddSingleton(accessService);
  87. var httpContext = new DefaultHttpContext
  88. {
  89. RequestServices = services.BuildServiceProvider(),
  90. User = new ClaimsPrincipal(new ClaimsIdentity(
  91. new[] { new Claim(ClaimTypes.NameIdentifier, "1") },
  92. authenticated ? "test" : null))
  93. };
  94. var actionContext = new ActionContext(
  95. httpContext,
  96. new RouteData(),
  97. new ActionDescriptor(),
  98. new ModelStateDictionary());
  99. return new AuthorizationFilterContext(actionContext, new List<IFilterMetadata>());
  100. }
  101. private sealed class StubMenuAccessService : ICurrentUserMenuAccessService
  102. {
  103. private readonly bool _result;
  104. private readonly Exception _exception;
  105. private readonly string _grantedPath;
  106. public StubMenuAccessService(
  107. bool result = false,
  108. Exception exception = null,
  109. string grantedPath = null)
  110. {
  111. _result = result;
  112. _exception = exception;
  113. _grantedPath = grantedPath;
  114. }
  115. public Task<bool> HasMenuPathAsync(string menuPath) =>
  116. _exception == null
  117. ? Task.FromResult(_result || string.Equals(_grantedPath, menuPath, StringComparison.Ordinal))
  118. : Task.FromException<bool>(_exception);
  119. }
  120. }