diff --git a/src/Ocelot/Authorization/IRolesAuthorizer.cs b/src/Ocelot/Authorization/IRolesAuthorizer.cs new file mode 100644 index 000000000..472359ac0 --- /dev/null +++ b/src/Ocelot/Authorization/IRolesAuthorizer.cs @@ -0,0 +1,9 @@ +using Ocelot.Responses; +using System.Security.Claims; + +namespace Ocelot.Authorization; + +public interface IRolesAuthorizer +{ + Response Authorize(ClaimsPrincipal claimsPrincipal, List routeRequiredRole, string roleKey); +} diff --git a/src/Ocelot/Authorization/IScopesAuthorizer.cs b/src/Ocelot/Authorization/IScopesAuthorizer.cs index d626251ec..11aa3a629 100644 --- a/src/Ocelot/Authorization/IScopesAuthorizer.cs +++ b/src/Ocelot/Authorization/IScopesAuthorizer.cs @@ -1,10 +1,10 @@ using Ocelot.Responses; using System.Security.Claims; -namespace Ocelot.Authorization +namespace Ocelot.Authorization; + +public interface IScopesAuthorizer { - public interface IScopesAuthorizer - { - Response Authorize(ClaimsPrincipal claimsPrincipal, List routeAllowedScopes); - } + Response Authorize(ClaimsPrincipal claimsPrincipal, List routeAllowedScopes, + string scopeKey); } diff --git a/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs b/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs index 84ccf526d..b83446f7c 100644 --- a/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs +++ b/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs @@ -11,27 +11,33 @@ public class AuthorizationMiddleware : OcelotMiddleware private readonly RequestDelegate _next; private readonly IClaimsAuthorizer _claimsAuthorizer; private readonly IScopesAuthorizer _scopesAuthorizer; + private readonly IRolesAuthorizer _rolesAuthorizer; public AuthorizationMiddleware(RequestDelegate next, IClaimsAuthorizer claimsAuthorizer, IScopesAuthorizer scopesAuthorizer, + IRolesAuthorizer rolesAuthorizer, IOcelotLoggerFactory loggerFactory) : base(loggerFactory.CreateLogger()) { _next = next; _claimsAuthorizer = claimsAuthorizer; _scopesAuthorizer = scopesAuthorizer; + _rolesAuthorizer = rolesAuthorizer; } + // Note roles is a duplicate of scopes - should refactor based on type + // Note scopes and roles are processed as OR + // TODO: Create logic to process policies that we use in the API public async Task Invoke(HttpContext httpContext) { var downstreamRoute = httpContext.Items.DownstreamRoute(); - + var options = downstreamRoute.AuthenticationOptions; if (!IsOptionsHttpMethod(httpContext) && IsAuthenticatedRoute(downstreamRoute)) { Logger.LogInformation("route is authenticated scopes must be checked"); - var authorized = _scopesAuthorizer.Authorize(httpContext.User, downstreamRoute.AuthenticationOptions.AllowedScopes); + var authorized = _scopesAuthorizer.Authorize(httpContext.User, options.AllowedScopes, options.ScopeKey); if (authorized.IsError) { @@ -54,6 +60,33 @@ public async Task Invoke(HttpContext httpContext) } } + if (!IsOptionsHttpMethod(httpContext) && IsAuthenticatedRoute(downstreamRoute)) + { + Logger.LogInformation("route and scope is authenticated role must be checked"); + + var authorizedRole = _rolesAuthorizer.Authorize(httpContext.User, options.RequiredRole, options.RoleKey); + + if (authorizedRole.IsError) + { + Logger.LogWarning("error authorizing user roles"); + + httpContext.Items.UpsertErrors(authorizedRole.Errors); + return; + } + + if (IsAuthorized(authorizedRole)) + { + Logger.LogInformation("user has the required role and is authorized calling next authorization checks"); + } + else + { + Logger.LogWarning("user does not have the required role and is not authorized setting pipeline error"); + + httpContext.Items.SetError(new UnauthorizedError( + $"{httpContext.User.Identity.Name} unable to access {downstreamRoute.UpstreamPathTemplate.OriginalValue}")); + } + } + if (!IsOptionsHttpMethod(httpContext) && IsAuthorizedRoute(downstreamRoute)) { Logger.LogInformation("route is authorized"); diff --git a/src/Ocelot/Authorization/RolesAuthorizer.cs b/src/Ocelot/Authorization/RolesAuthorizer.cs new file mode 100644 index 000000000..62925094f --- /dev/null +++ b/src/Ocelot/Authorization/RolesAuthorizer.cs @@ -0,0 +1,44 @@ +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Responses; +using System.Security.Claims; + +namespace Ocelot.Authorization; + +public class RolesAuthorizer : IRolesAuthorizer +{ + private readonly IClaimsParser _claimsParser; + + public RolesAuthorizer(IClaimsParser claimsParser) + { + _claimsParser = claimsParser; + } + + public Response Authorize(ClaimsPrincipal claimsPrincipal, List routeRequiredRole, string roleKey) + { + if (routeRequiredRole == null || routeRequiredRole.Count == 0) + { + return new OkResponse(true); + } + + roleKey ??= "role"; + + var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, roleKey); + + if (values.IsError) + { + return new ErrorResponse(values.Errors); + } + + var userRoles = values.Data; + + var matchedRoles = routeRequiredRole.Intersect(userRoles).ToList(); // Note this is an OR + + if (matchedRoles.Count == 0) + { + return new ErrorResponse( + new ScopeNotAuthorizedError($"no one user role: '{string.Join(",", userRoles)}' match with some allowed role: '{string.Join(",", routeRequiredRole)}'")); + } + + return new OkResponse(true); + } +} diff --git a/src/Ocelot/Authorization/ScopesAuthorizer.cs b/src/Ocelot/Authorization/ScopesAuthorizer.cs index a905fe86a..0c49f37f0 100644 --- a/src/Ocelot/Authorization/ScopesAuthorizer.cs +++ b/src/Ocelot/Authorization/ScopesAuthorizer.cs @@ -1,27 +1,27 @@ using Ocelot.Infrastructure.Claims.Parser; using Ocelot.Responses; using System.Security.Claims; - + namespace Ocelot.Authorization { public class ScopesAuthorizer : IScopesAuthorizer { private readonly IClaimsParser _claimsParser; - private const string Scope = "scope"; public ScopesAuthorizer(IClaimsParser claimsParser) { _claimsParser = claimsParser; } - public Response Authorize(ClaimsPrincipal claimsPrincipal, List routeAllowedScopes) + public Response Authorize(ClaimsPrincipal claimsPrincipal, List routeAllowedScopes, string scopeKey) { if (routeAllowedScopes == null || routeAllowedScopes.Count == 0) { return new OkResponse(true); } - var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, Scope); + scopeKey ??= "scope"; + var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, scopeKey); if (values.IsError) { diff --git a/src/Ocelot/Configuration/AuthenticationOptions.cs b/src/Ocelot/Configuration/AuthenticationOptions.cs index 978a940a8..85ad3bba9 100644 --- a/src/Ocelot/Configuration/AuthenticationOptions.cs +++ b/src/Ocelot/Configuration/AuthenticationOptions.cs @@ -4,25 +4,55 @@ namespace Ocelot.Configuration { public sealed class AuthenticationOptions { - public AuthenticationOptions(List allowedScopes, string authenticationProviderKey) - { - AllowedScopes = allowedScopes; - AuthenticationProviderKey = authenticationProviderKey; - AuthenticationProviderKeys = Array.Empty(); - } - public AuthenticationOptions(FileAuthenticationOptions from) { AllowedScopes = from.AllowedScopes ?? new(); - AuthenticationProviderKey = from.AuthenticationProviderKey ?? string.Empty; - AuthenticationProviderKeys = from.AuthenticationProviderKeys ?? Array.Empty(); + BuildAuthenticationProviderKeys(from.AuthenticationProviderKey, from.AuthenticationProviderKeys); + PolicyName = from.PolicyName; + RequiredRole = from.RequiredRole; + ScopeKey = from.ScopeKey; + RoleKey = from.RoleKey; } public AuthenticationOptions(List allowedScopes, string authenticationProviderKey, string[] authenticationProviderKeys) { AllowedScopes = allowedScopes ?? new(); - AuthenticationProviderKey = authenticationProviderKey ?? string.Empty; - AuthenticationProviderKeys = authenticationProviderKeys ?? Array.Empty(); + BuildAuthenticationProviderKeys(authenticationProviderKey, authenticationProviderKeys); + } + + public AuthenticationOptions(List allowedScopes, string[] authenticationProviderKeys, List requiredRole, string scopeKey, string roleKey, string policyName) + { + AllowedScopes = allowedScopes; + BuildAuthenticationProviderKeys(null, authenticationProviderKeys); + PolicyName = policyName; + RequiredRole = requiredRole; + ScopeKey = scopeKey; + RoleKey = roleKey; + } + + /// + /// Builds auth keys migrating legacy key to new ones. + /// + /// The legacy . + /// New to build. + private void BuildAuthenticationProviderKeys(string legacyKey, string[] keys) + { + keys ??= Array.Empty(); + if (string.IsNullOrEmpty(legacyKey)) + { + AuthenticationProviderKeys = keys; + AuthenticationProviderKey = string.Empty; + return; + } + + // Add legacy Key to new Keys array as the first element + var arr = new string[keys.Length + 1]; + arr[0] = legacyKey; + Array.Copy(keys, 0, arr, 1, keys.Length); + + // Update the object + AuthenticationProviderKeys = arr; + AuthenticationProviderKey = string.Empty; } public List AllowedScopes { get; } @@ -34,7 +64,7 @@ public AuthenticationOptions(List allowedScopes, string authenticationPr /// A value of the scheme name. /// [Obsolete("Use the " + nameof(AuthenticationProviderKeys) + " property!")] - public string AuthenticationProviderKey { get; } + public string AuthenticationProviderKey { get; private set; } /// /// Multiple authentication schemes registered in DI services with appropriate authentication providers. @@ -45,6 +75,11 @@ public AuthenticationOptions(List allowedScopes, string authenticationPr /// /// An array of values of the scheme names. /// - public string[] AuthenticationProviderKeys { get; } + public string[] AuthenticationProviderKeys { get; private set; } + + public List RequiredRole { get; } + public string ScopeKey { get; } + public string RoleKey { get; } + public string PolicyName { get; } } } diff --git a/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs index 0b85ee809..7eb07442f 100644 --- a/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs +++ b/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs @@ -3,31 +3,55 @@ namespace Ocelot.Configuration.Builder public class AuthenticationOptionsBuilder { private List _allowedScopes = new(); - private string _authenticationProviderKey; + private List _requiredRole = new(); private string[] _authenticationProviderKeys = Array.Empty(); + private string _roleKey; + private string _scopeKey; + private string _policyName; public AuthenticationOptionsBuilder WithAllowedScopes(List allowedScopes) { _allowedScopes = allowedScopes; return this; } - + + public AuthenticationOptionsBuilder WithRequiredRole(List requiredRole) + { + _requiredRole = requiredRole; + return this; + } + [Obsolete("Use the " + nameof(WithAuthenticationProviderKeys) + " property!")] public AuthenticationOptionsBuilder WithAuthenticationProviderKey(string authenticationProviderKey) + => WithAuthenticationProviderKeys(authenticationProviderKey); + + public AuthenticationOptionsBuilder WithAuthenticationProviderKeys(params string[] authenticationProviderKeys) { - _authenticationProviderKey = authenticationProviderKey; + _authenticationProviderKeys = authenticationProviderKeys; return this; } - public AuthenticationOptionsBuilder WithAuthenticationProviderKeys(string[] authenticationProviderKeys) + public AuthenticationOptionsBuilder WithRoleKey(string roleKey) { - _authenticationProviderKeys = authenticationProviderKeys; + _roleKey = roleKey; + return this; + } + + public AuthenticationOptionsBuilder WithScopeKey(string scopeKey) + { + _scopeKey = scopeKey; + return this; + } + + public AuthenticationOptionsBuilder WithPolicyName(string policyName) + { + _policyName = policyName; return this; } public AuthenticationOptions Build() { - return new AuthenticationOptions(_allowedScopes, _authenticationProviderKey, _authenticationProviderKeys); + return new AuthenticationOptions(_allowedScopes, _authenticationProviderKeys, _requiredRole, _scopeKey, _roleKey, _policyName); } } } diff --git a/src/Ocelot/Configuration/Creator/AuthenticationOptionsCreator.cs b/src/Ocelot/Configuration/Creator/AuthenticationOptionsCreator.cs index d26d39357..75f9a79c3 100644 --- a/src/Ocelot/Configuration/Creator/AuthenticationOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/AuthenticationOptionsCreator.cs @@ -1,10 +1,8 @@ using Ocelot.Configuration.File; -namespace Ocelot.Configuration.Creator +namespace Ocelot.Configuration.Creator; + +public class AuthenticationOptionsCreator : IAuthenticationOptionsCreator { - public class AuthenticationOptionsCreator : IAuthenticationOptionsCreator - { - public AuthenticationOptions Create(FileRoute route) - => new(route?.AuthenticationOptions ?? new()); - } + public AuthenticationOptions Create(FileRoute route) => new(route?.AuthenticationOptions ?? new()); } diff --git a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs index 0dd93a9c6..6d972092d 100644 --- a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs +++ b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs @@ -6,6 +6,7 @@ public FileAuthenticationOptions() { AllowedScopes = new(); AuthenticationProviderKeys = Array.Empty(); + RequiredRole = new(); } public FileAuthenticationOptions(FileAuthenticationOptions from) @@ -19,13 +20,21 @@ public FileAuthenticationOptions(FileAuthenticationOptions from) [Obsolete("Use the " + nameof(AuthenticationProviderKeys) + " property!")] public string AuthenticationProviderKey { get; set; } - public string[] AuthenticationProviderKeys { get; set; } + public List RequiredRole { get; set; } + public string ScopeKey { get; set; } + public string RoleKey { get; set; } + public string PolicyName { get; set; } + public override string ToString() => new StringBuilder() .Append($"{nameof(AuthenticationProviderKey)}:'{AuthenticationProviderKey}',") .Append($"{nameof(AuthenticationProviderKeys)}:[{string.Join(',', AuthenticationProviderKeys.Select(x => $"'{x}'"))}],") - .Append($"{nameof(AllowedScopes)}:[{string.Join(',', AllowedScopes.Select(x => $"'{x}'"))}]") + .Append($"{nameof(AllowedScopes)}:[{string.Join(',', AllowedScopes.Select(x => $"'{x}'"))}],") + .Append($"{nameof(RequiredRole)}:[").AppendJoin(',', RequiredRole).Append("],") + .Append($"{nameof(ScopeKey)}:[").AppendJoin(',', ScopeKey).Append("],") + .Append($"{nameof(RoleKey)}:[").AppendJoin(',', RoleKey).Append("],") + .Append($"{nameof(PolicyName)}:[").AppendJoin(',', PolicyName).Append(']') .ToString(); } } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 642697744..56726a0f5 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -90,6 +90,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); diff --git a/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs index 1a094bbca..a3c3fc61b 100644 --- a/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs @@ -20,17 +20,19 @@ public class AuthorizationMiddlewareTests : UnitTest private readonly AuthorizationMiddleware _middleware; private readonly RequestDelegate _next; private readonly HttpContext _httpContext; + private readonly Mock _authRolesService; public AuthorizationMiddlewareTests() { _httpContext = new DefaultHttpContext(); _authService = new Mock(); _authScopesService = new Mock(); + _authRolesService = new Mock(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; - _middleware = new AuthorizationMiddleware(_next, _authService.Object, _authScopesService.Object, _loggerFactory.Object); + _middleware = new AuthorizationMiddleware(_next, _authService.Object, _authScopesService.Object, _authRolesService.Object, _loggerFactory.Object); } [Fact] diff --git a/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs index d0e8d599e..d9cd320fa 100644 --- a/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs @@ -46,7 +46,7 @@ public void Create_OptionsObjIsNotNull_CreatedSuccessfully(bool isAuthentication // Arrange string authenticationProviderKey = !isAuthenticationProviderKeys ? "Test" : null; string[] authenticationProviderKeys = isAuthenticationProviderKeys ? - new string[] { "Test #1", "Test #2" } : null; + new string[] { "Test #1", "Test #2" } : Array.Empty(); var fileRoute = new FileRoute() { AuthenticationOptions = new FileAuthenticationOptions @@ -56,11 +56,13 @@ public void Create_OptionsObjIsNotNull_CreatedSuccessfully(bool isAuthentication AuthenticationProviderKeys = authenticationProviderKeys, }, }; - var expected = new AuthenticationOptionsBuilder() - .WithAllowedScopes(fileRoute.AuthenticationOptions?.AllowedScopes) - .WithAuthenticationProviderKey(authenticationProviderKey) - .WithAuthenticationProviderKeys(authenticationProviderKeys) - .Build(); + + var b = new AuthenticationOptionsBuilder() + .WithAllowedScopes(fileRoute.AuthenticationOptions?.AllowedScopes); + b = isAuthenticationProviderKeys + ? b.WithAuthenticationProviderKeys(authenticationProviderKeys) + : b.WithAuthenticationProviderKey(authenticationProviderKey); + var expected = b.Build(); // Act var actual = _authOptionsCreator.Create(fileRoute); diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs index 6aa6a398c..f619ffaa6 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs @@ -432,7 +432,7 @@ public void Configuration_is_invalid_with_invalid_authentication_provider() this.Given(x => x.GivenAConfiguration(route)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Authentication Options AuthenticationProviderKey:'Test',AuthenticationProviderKeys:['Test #1','Test #2'],AllowedScopes:[] is unsupported authentication provider")) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Authentication Options AuthenticationProviderKey:'Test',AuthenticationProviderKeys:['Test #1','Test #2'],AllowedScopes:[],RequiredRole:[],ScopeKey:[],RoleKey:[],PolicyName:[] is unsupported authentication provider")) .BDDfy(); } @@ -536,7 +536,7 @@ public void Configuration_is_valid_with_duplicate_routes_but_one_upstreamhost_is .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) .BDDfy(); - } + } [Fact] public void Configuration_is_invalid_with_invalid_rate_limit_configuration() @@ -718,7 +718,7 @@ public void Configuration_is_not_valid_when_host_and_port_is_empty() .Then(x => x.ThenTheResultIsNotValid()) .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!")) .BDDfy(); - } + } [Fact] [Trait("PR", "1312")] @@ -900,8 +900,8 @@ public async Task Configuration_is_invalid_when_placeholder_is_used_twice_in_dow DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, ServiceName = "test", - }; - + }; + private static FileRoute GivenRouteWithUpstreamHeaderTemplates(string upstream, string downstream, Dictionary templates) => new() { UpstreamPathTemplate = upstream, diff --git a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs index 1b2bfadb3..a15123e41 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs @@ -191,8 +191,8 @@ public void should_not_be_valid_if_enable_rate_limiting_true_and_period_has_valu .Then(_ => ThenTheResultIsInvalid()) .And(_ => ThenTheErrorsContains("RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period")) .BDDfy(); - } - + } + [Theory] [InlineData(null, false)] [InlineData("", false)] @@ -234,7 +234,7 @@ public void should_not_be_valid_if_specified_authentication_provider_isnt_regist this.Given(_ => GivenThe(fileRoute)) .When(_ => WhenIValidate()) .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains($"Authentication Options AuthenticationProviderKey:'JwtLads',AuthenticationProviderKeys:[],AllowedScopes:[] is unsupported authentication provider")) + .And(_ => ThenTheErrorsContains($"Authentication Options AuthenticationProviderKey:'JwtLads',AuthenticationProviderKeys:[],AllowedScopes:[],RequiredRole:[],ScopeKey:[],RoleKey:[],PolicyName:[] is unsupported authentication provider")) .BDDfy(); } diff --git a/test/Ocelot.UnitTests/Infrastructure/RoleAuthorizerTests.cs b/test/Ocelot.UnitTests/Infrastructure/RoleAuthorizerTests.cs new file mode 100644 index 000000000..9d3ffecf9 --- /dev/null +++ b/test/Ocelot.UnitTests/Infrastructure/RoleAuthorizerTests.cs @@ -0,0 +1,109 @@ +using Ocelot.Authorization; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Responses; +using System.Security.Claims; + +namespace Ocelot.UnitTests.Infrastructure; + +public class RoleAuthorizerTests : UnitTest +{ + private readonly RolesAuthorizer _authorizer; + private readonly Mock _parser; + private ClaimsPrincipal _principal; + private List _requiredRole; + private Response _result; + + public RoleAuthorizerTests() + { + _parser = new Mock(); + _authorizer = new RolesAuthorizer(_parser.Object); + } + + [Fact] + public void Should_return_ok_if_no_allowed_scopes() + { + this.Given(_ => GivenTheFollowing(new ClaimsPrincipal())) + .And(_ => GivenTheFollowing(new List())) + .When(_ => WhenIAuthorize()) + .Then(_ => ThenTheFollowingIsReturned(new OkResponse(true))) + .BDDfy(); + } + + [Fact] + public void Should_return_ok_if_null_allowed_scopes() + { + this.Given(_ => GivenTheFollowing(new ClaimsPrincipal())) + .And(_ => GivenTheFollowing((List)null)) + .When(_ => WhenIAuthorize()) + .Then(_ => ThenTheFollowingIsReturned(new OkResponse(true))) + .BDDfy(); + } + + [Fact] + public void Should_return_error_if_claims_parser_returns_error() + { + var fakeError = new FakeError(); + this.Given(_ => GivenTheFollowing(new ClaimsPrincipal())) + .And(_ => GivenTheParserReturns(new ErrorResponse>(fakeError))) + .And(_ => GivenTheFollowing(new List() { "doesntmatter" })) + .When(_ => WhenIAuthorize()) + .Then(_ => ThenTheFollowingIsReturned(new ErrorResponse(fakeError))) + .BDDfy(); + } + + [Fact] + public void Should_match_role_and_return_ok_result() + { + var claimsPrincipal = new ClaimsPrincipal(); + var requiredRole = new List() { "someRole" }; + + this.Given(_ => GivenTheFollowing(claimsPrincipal)) + .And(_ => GivenTheParserReturns(new OkResponse>(requiredRole))) + .And(_ => GivenTheFollowing(requiredRole)) + .When(_ => WhenIAuthorize()) + .Then(_ => ThenTheFollowingIsReturned(new OkResponse(true))) + .BDDfy(); + } + + [Fact] + public void Should_not_match_role_and_return_error_result() + { + var fakeError = new FakeError(); + var claimsPrincipal = new ClaimsPrincipal(); + var requiredRole = new List() { "someRole" }; + var userRoles = new List() { "anotherRole" }; + + this.Given(_ => GivenTheFollowing(claimsPrincipal)) + .And(_ => GivenTheParserReturns(new OkResponse>(userRoles))) + .And(_ => GivenTheFollowing(requiredRole)) + .When(_ => WhenIAuthorize()) + .Then(_ => ThenTheFollowingIsReturned(new ErrorResponse(fakeError))) + .BDDfy(); + } + + private void GivenTheParserReturns(Response> response) + { + _parser.Setup(x => x.GetValuesByClaimType(It.IsAny>(), It.IsAny())).Returns(response); + } + + private void GivenTheFollowing(ClaimsPrincipal principal) + { + _principal = principal; + } + + private void GivenTheFollowing(List requiredRole) + { + _requiredRole = requiredRole; + } + + private void WhenIAuthorize() + { + _result = _authorizer.Authorize(_principal, _requiredRole, null); + } + + private void ThenTheFollowingIsReturned(Response expected) + { + _result.Data.ShouldBe(expected.Data); + _result.IsError.ShouldBe(expected.IsError); + } +} diff --git a/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs b/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs index b94774bbe..9bc3c0a66 100644 --- a/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs @@ -99,7 +99,7 @@ private void GivenTheFollowing(List allowedScopes) private void WhenIAuthorize() { - _result = _authorizer.Authorize(_principal, _allowedScopes); + _result = _authorizer.Authorize(_principal, _allowedScopes, null); } private void ThenTheFollowingIsReturned(Response expected)