mirror of
https://github.com/nsnail/Ocelot.git
synced 2025-06-19 10:38:15 +08:00
Merge branch 'master' into develop
This commit is contained in:
@ -7,7 +7,7 @@ namespace Ocelot.Authorisation
|
||||
{
|
||||
using Infrastructure.Claims.Parser;
|
||||
|
||||
public class ClaimsAuthoriser : IAuthoriser
|
||||
public class ClaimsAuthoriser : IClaimsAuthoriser
|
||||
{
|
||||
private readonly IClaimsParser _claimsParser;
|
||||
|
||||
|
@ -5,9 +5,8 @@ namespace Ocelot.Authorisation
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
public interface IAuthoriser
|
||||
public interface IClaimsAuthoriser
|
||||
{
|
||||
Response<bool> Authorise(ClaimsPrincipal claimsPrincipal,
|
||||
Dictionary<string, string> routeClaimsRequirement);
|
||||
Response<bool> Authorise(ClaimsPrincipal claimsPrincipal, Dictionary<string, string> routeClaimsRequirement);
|
||||
}
|
||||
}
|
12
src/Ocelot/Authorisation/IScopesAuthoriser.cs
Normal file
12
src/Ocelot/Authorisation/IScopesAuthoriser.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.Security.Claims;
|
||||
using Ocelot.Responses;
|
||||
|
||||
namespace Ocelot.Authorisation
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
public interface IScopesAuthoriser
|
||||
{
|
||||
Response<bool> Authorise(ClaimsPrincipal claimsPrincipal, List<string> routeAllowedScopes);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using Ocelot.Infrastructure.RequestData;
|
||||
using Ocelot.Logging;
|
||||
using Ocelot.Responses;
|
||||
using Ocelot.Configuration;
|
||||
|
||||
namespace Ocelot.Authorisation.Middleware
|
||||
{
|
||||
@ -13,17 +14,20 @@ namespace Ocelot.Authorisation.Middleware
|
||||
public class AuthorisationMiddleware : OcelotMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IAuthoriser _authoriser;
|
||||
private readonly IClaimsAuthoriser _claimsAuthoriser;
|
||||
private readonly IScopesAuthoriser _scopesAuthoriser;
|
||||
private readonly IOcelotLogger _logger;
|
||||
|
||||
public AuthorisationMiddleware(RequestDelegate next,
|
||||
IRequestScopedDataRepository requestScopedDataRepository,
|
||||
IAuthoriser authoriser,
|
||||
IClaimsAuthoriser claimsAuthoriser,
|
||||
IScopesAuthoriser scopesAuthoriser,
|
||||
IOcelotLoggerFactory loggerFactory)
|
||||
: base(requestScopedDataRepository)
|
||||
{
|
||||
_next = next;
|
||||
_authoriser = authoriser;
|
||||
_claimsAuthoriser = claimsAuthoriser;
|
||||
_scopesAuthoriser = scopesAuthoriser;
|
||||
_logger = loggerFactory.CreateLogger<AuthorisationMiddleware>();
|
||||
}
|
||||
|
||||
@ -31,11 +35,41 @@ namespace Ocelot.Authorisation.Middleware
|
||||
{
|
||||
_logger.LogDebug("started authorisation");
|
||||
|
||||
if (DownstreamRoute.ReRoute.IsAuthorised)
|
||||
if (IsAuthenticatedRoute(DownstreamRoute.ReRoute))
|
||||
{
|
||||
_logger.LogDebug("route is authenticated scopes must be checked");
|
||||
|
||||
var authorised = _scopesAuthoriser.Authorise(context.User, DownstreamRoute.ReRoute.AuthenticationOptions.AllowedScopes);
|
||||
|
||||
if (authorised.IsError)
|
||||
{
|
||||
_logger.LogDebug("error authorising user scopes");
|
||||
|
||||
SetPipelineError(authorised.Errors);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsAuthorised(authorised))
|
||||
{
|
||||
_logger.LogDebug("user scopes is authorised calling next authorisation checks");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("user scopes is not authorised setting pipeline error");
|
||||
|
||||
SetPipelineError(new List<Error>
|
||||
{
|
||||
new UnauthorisedError(
|
||||
$"{context.User.Identity.Name} unable to access {DownstreamRoute.ReRoute.UpstreamPathTemplate.Value}")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (IsAuthorisedRoute(DownstreamRoute.ReRoute))
|
||||
{
|
||||
_logger.LogDebug("route is authorised");
|
||||
|
||||
var authorised = _authoriser.Authorise(context.User, DownstreamRoute.ReRoute.RouteClaimsRequirement);
|
||||
var authorised = _claimsAuthoriser.Authorise(context.User, DownstreamRoute.ReRoute.RouteClaimsRequirement);
|
||||
|
||||
if (authorised.IsError)
|
||||
{
|
||||
@ -78,5 +112,15 @@ namespace Ocelot.Authorisation.Middleware
|
||||
{
|
||||
return authorised.Data;
|
||||
}
|
||||
|
||||
private static bool IsAuthenticatedRoute(ReRoute reRoute)
|
||||
{
|
||||
return reRoute.IsAuthenticated;
|
||||
}
|
||||
|
||||
private static bool IsAuthorisedRoute(ReRoute reRoute)
|
||||
{
|
||||
return reRoute.IsAuthorised;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
12
src/Ocelot/Authorisation/ScopeNotAuthorisedError.cs
Normal file
12
src/Ocelot/Authorisation/ScopeNotAuthorisedError.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using Ocelot.Errors;
|
||||
|
||||
namespace Ocelot.Authorisation
|
||||
{
|
||||
public class ScopeNotAuthorisedError : Error
|
||||
{
|
||||
public ScopeNotAuthorisedError(string message)
|
||||
: base(message, OcelotErrorCode.ScopeNotAuthorisedError)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
51
src/Ocelot/Authorisation/ScopesAuthoriser.cs
Normal file
51
src/Ocelot/Authorisation/ScopesAuthoriser.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using IdentityModel;
|
||||
using Ocelot.Errors;
|
||||
using Ocelot.Responses;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ocelot.Authorisation
|
||||
{
|
||||
using Infrastructure.Claims.Parser;
|
||||
|
||||
public class ScopesAuthoriser : IScopesAuthoriser
|
||||
{
|
||||
private readonly IClaimsParser _claimsParser;
|
||||
|
||||
public ScopesAuthoriser(IClaimsParser claimsParser)
|
||||
{
|
||||
_claimsParser = claimsParser;
|
||||
}
|
||||
|
||||
public Response<bool> Authorise(ClaimsPrincipal claimsPrincipal, List<string> routeAllowedScopes)
|
||||
{
|
||||
if (routeAllowedScopes == null || routeAllowedScopes.Count == 0)
|
||||
{
|
||||
return new OkResponse<bool>(true);
|
||||
}
|
||||
|
||||
var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, JwtClaimTypes.Scope);
|
||||
|
||||
if (values.IsError)
|
||||
{
|
||||
return new ErrorResponse<bool>(values.Errors);
|
||||
}
|
||||
|
||||
var userScopes = values.Data;
|
||||
|
||||
List<string> matchesScopes = routeAllowedScopes.Intersect(userScopes).ToList();
|
||||
|
||||
if (matchesScopes == null || matchesScopes.Count == 0)
|
||||
{
|
||||
return new ErrorResponse<bool>(new List<Error>
|
||||
{
|
||||
new ScopeNotAuthorisedError(
|
||||
$"no one user scope: '{string.Join(",", userScopes)}' match with some allowed scope: '{string.Join(",", routeAllowedScopes)}'")
|
||||
});
|
||||
}
|
||||
|
||||
return new OkResponse<bool>(true);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using Ocelot.Values;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ocelot.Configuration.Builder
|
||||
{
|
||||
@ -11,7 +12,7 @@ namespace Ocelot.Configuration.Builder
|
||||
private string _downstreamPathTemplate;
|
||||
private string _upstreamTemplate;
|
||||
private string _upstreamTemplatePattern;
|
||||
private string _upstreamHttpMethod;
|
||||
private List<HttpMethod> _upstreamHttpMethod;
|
||||
private bool _isAuthenticated;
|
||||
private List<ClaimToThing> _configHeaderExtractorProperties;
|
||||
private List<ClaimToThing> _claimToClaims;
|
||||
@ -66,11 +67,13 @@ namespace Ocelot.Configuration.Builder
|
||||
_upstreamTemplatePattern = input;
|
||||
return this;
|
||||
}
|
||||
public ReRouteBuilder WithUpstreamHttpMethod(string input)
|
||||
|
||||
public ReRouteBuilder WithUpstreamHttpMethod(List<string> input)
|
||||
{
|
||||
_upstreamHttpMethod = input;
|
||||
_upstreamHttpMethod = (input.Count == 0) ? new List<HttpMethod>() : input.Select(x => new HttpMethod(x.Trim())).ToList();
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReRouteBuilder WithIsAuthenticated(bool input)
|
||||
{
|
||||
_isAuthenticated = input;
|
||||
@ -143,7 +146,6 @@ namespace Ocelot.Configuration.Builder
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public ReRouteBuilder WithLoadBalancerKey(string loadBalancerKey)
|
||||
{
|
||||
_loadBalancerKey = loadBalancerKey;
|
||||
@ -180,7 +182,7 @@ namespace Ocelot.Configuration.Builder
|
||||
return new ReRoute(
|
||||
new PathTemplate(_downstreamPathTemplate),
|
||||
new PathTemplate(_upstreamTemplate),
|
||||
new HttpMethod(_upstreamHttpMethod),
|
||||
_upstreamHttpMethod,
|
||||
_upstreamTemplatePattern,
|
||||
_isAuthenticated,
|
||||
_authenticationOptions,
|
||||
|
@ -6,6 +6,7 @@ namespace Ocelot.Configuration.File
|
||||
{
|
||||
public FileReRoute()
|
||||
{
|
||||
UpstreamHttpMethod = new List<string>();
|
||||
AddHeadersToRequest = new Dictionary<string, string>();
|
||||
AddClaimsToRequest = new Dictionary<string, string>();
|
||||
RouteClaimsRequirement = new Dictionary<string, string>();
|
||||
@ -18,7 +19,7 @@ namespace Ocelot.Configuration.File
|
||||
|
||||
public string DownstreamPathTemplate { get; set; }
|
||||
public string UpstreamPathTemplate { get; set; }
|
||||
public string UpstreamHttpMethod { get; set; }
|
||||
public List<string> UpstreamHttpMethod { get; set; }
|
||||
public FileAuthenticationOptions AuthenticationOptions { get; set; }
|
||||
public Dictionary<string, string> AddHeadersToRequest { get; set; }
|
||||
public Dictionary<string, string> AddClaimsToRequest { get; set; }
|
||||
|
@ -8,7 +8,7 @@ namespace Ocelot.Configuration
|
||||
{
|
||||
public ReRoute(PathTemplate downstreamPathTemplate,
|
||||
PathTemplate upstreamPathTemplate,
|
||||
HttpMethod upstreamHttpMethod,
|
||||
List<HttpMethod> upstreamHttpMethod,
|
||||
string upstreamTemplatePattern,
|
||||
bool isAuthenticated,
|
||||
AuthenticationOptions authenticationOptions,
|
||||
@ -64,7 +64,7 @@ namespace Ocelot.Configuration
|
||||
public PathTemplate DownstreamPathTemplate { get; private set; }
|
||||
public PathTemplate UpstreamPathTemplate { get; private set; }
|
||||
public string UpstreamTemplatePattern { get; private set; }
|
||||
public HttpMethod UpstreamHttpMethod { get; private set; }
|
||||
public List<HttpMethod> UpstreamHttpMethod { get; private set; }
|
||||
public bool IsAuthenticated { get; private set; }
|
||||
public bool IsAuthorised { get; private set; }
|
||||
public AuthenticationOptions AuthenticationOptions { get; private set; }
|
||||
|
@ -12,7 +12,7 @@ namespace Ocelot.Configuration.Validator
|
||||
{
|
||||
public Response<ConfigurationValidationResult> IsValid(FileConfiguration configuration)
|
||||
{
|
||||
var result = CheckForDupliateReRoutes(configuration);
|
||||
var result = CheckForDuplicateReRoutes(configuration);
|
||||
|
||||
if (result.IsError)
|
||||
{
|
||||
@ -97,25 +97,41 @@ namespace Ocelot.Configuration.Validator
|
||||
return new ConfigurationValidationResult(false, errors);
|
||||
}
|
||||
|
||||
private ConfigurationValidationResult CheckForDupliateReRoutes(FileConfiguration configuration)
|
||||
{
|
||||
var hasDupes = configuration.ReRoutes
|
||||
.GroupBy(x => new { x.UpstreamPathTemplate, x.UpstreamHttpMethod }).Any(x => x.Skip(1).Any());
|
||||
private ConfigurationValidationResult CheckForDuplicateReRoutes(FileConfiguration configuration)
|
||||
{
|
||||
var duplicatedUpstreamPathTemplates = new List<string>();
|
||||
|
||||
if (!hasDupes)
|
||||
var distinctUpstreamPathTemplates = configuration.ReRoutes.Select(x => x.UpstreamPathTemplate).Distinct();
|
||||
|
||||
foreach (string upstreamPathTemplate in distinctUpstreamPathTemplates)
|
||||
{
|
||||
var reRoutesWithUpstreamPathTemplate = configuration.ReRoutes.Where(x => x.UpstreamPathTemplate == upstreamPathTemplate);
|
||||
|
||||
var hasEmptyListToAllowAllHttpVerbs = reRoutesWithUpstreamPathTemplate.Where(x => x.UpstreamHttpMethod.Count() == 0).Any();
|
||||
var hasDuplicateEmptyListToAllowAllHttpVerbs = reRoutesWithUpstreamPathTemplate.Where(x => x.UpstreamHttpMethod.Count() == 0).Count() > 1;
|
||||
var hasSpecificHttpVerbs = reRoutesWithUpstreamPathTemplate.Where(x => x.UpstreamHttpMethod.Count() > 0).Any();
|
||||
var hasDuplicateSpecificHttpVerbs = reRoutesWithUpstreamPathTemplate.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any();
|
||||
|
||||
if (hasDuplicateEmptyListToAllowAllHttpVerbs || hasDuplicateSpecificHttpVerbs || (hasEmptyListToAllowAllHttpVerbs && hasSpecificHttpVerbs))
|
||||
{
|
||||
duplicatedUpstreamPathTemplates.Add(upstreamPathTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicatedUpstreamPathTemplates.Count() == 0)
|
||||
{
|
||||
return new ConfigurationValidationResult(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var errors = duplicatedUpstreamPathTemplates
|
||||
.Select(d => new DownstreamPathTemplateAlreadyUsedError(string.Format("Duplicate DownstreamPath: {0}", d)))
|
||||
.Cast<Error>()
|
||||
.ToList();
|
||||
|
||||
var dupes = configuration.ReRoutes.GroupBy(x => new { x.UpstreamPathTemplate, x.UpstreamHttpMethod })
|
||||
.Where(x => x.Skip(1).Any());
|
||||
return new ConfigurationValidationResult(true, errors);
|
||||
}
|
||||
|
||||
var errors = dupes
|
||||
.Select(d => new DownstreamPathTemplateAlreadyUsedError(string.Format("Duplicate DownstreamPath: {0}", d.Key.UpstreamPathTemplate)))
|
||||
.Cast<Error>()
|
||||
.ToList();
|
||||
|
||||
return new ConfigurationValidationResult(true, errors);
|
||||
}
|
||||
|
||||
private ConfigurationValidationResult CheckForReRoutesRateLimitOptions(FileConfiguration configuration)
|
||||
|
@ -144,7 +144,8 @@ namespace Ocelot.DependencyInjection
|
||||
services.TryAddSingleton<IRemoveOutputHeaders, RemoveOutputHeaders>();
|
||||
services.TryAddSingleton<IOcelotConfigurationProvider, OcelotConfigurationProvider>();
|
||||
services.TryAddSingleton<IClaimToThingConfigurationParser, ClaimToThingConfigurationParser>();
|
||||
services.TryAddSingleton<IAuthoriser, ClaimsAuthoriser>();
|
||||
services.TryAddSingleton<IClaimsAuthoriser, ClaimsAuthoriser>();
|
||||
services.TryAddSingleton<IScopesAuthoriser, ScopesAuthoriser>();
|
||||
services.TryAddSingleton<IAddClaimsToRequest, AddClaimsToRequest>();
|
||||
services.TryAddSingleton<IAddHeadersToRequest, AddHeadersToRequest>();
|
||||
services.TryAddSingleton<IAddQueriesToRequest, AddQueriesToRequest>();
|
||||
|
@ -26,7 +26,7 @@ namespace Ocelot.DownstreamRouteFinder.Finder
|
||||
{
|
||||
var configuration = await _configProvider.Get();
|
||||
|
||||
var applicableReRoutes = configuration.Data.ReRoutes.Where(r => string.Equals(r.UpstreamHttpMethod.Method.ToLower(), upstreamHttpMethod.ToLower(), StringComparison.CurrentCultureIgnoreCase));
|
||||
var applicableReRoutes = configuration.Data.ReRoutes.Where(r => r.UpstreamHttpMethod.Count == 0 || r.UpstreamHttpMethod.Select(x => x.Method.ToLower()).Contains(upstreamHttpMethod.ToLower()));
|
||||
|
||||
foreach (var reRoute in applicableReRoutes)
|
||||
{
|
||||
|
@ -70,7 +70,7 @@ namespace Ocelot.DownstreamRouteFinder.UrlMatcher
|
||||
|
||||
private bool CharactersDontMatch(char characterOne, char characterTwo)
|
||||
{
|
||||
return characterOne != characterTwo;
|
||||
return char.ToLower(characterOne) != char.ToLower(characterTwo);
|
||||
}
|
||||
|
||||
private bool ContinueScanningUrl(int counterForUrl, int urlLength)
|
||||
|
@ -17,6 +17,7 @@
|
||||
InstructionNotForClaimsError,
|
||||
UnauthorizedError,
|
||||
ClaimValueNotAuthorisedError,
|
||||
ScopeNotAuthorisedError,
|
||||
UserDoesNotHaveClaimError,
|
||||
DownstreamPathTemplateContainsSchemeError,
|
||||
DownstreamPathNullOrEmptyError,
|
||||
|
@ -1,10 +1,10 @@
|
||||
namespace Ocelot.Infrastructure.Claims.Parser
|
||||
{
|
||||
using Errors;
|
||||
using Responses;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using Errors;
|
||||
using Responses;
|
||||
|
||||
public class ClaimsParser : IClaimsParser
|
||||
{
|
||||
@ -37,6 +37,17 @@
|
||||
return new OkResponse<string>(value);
|
||||
}
|
||||
|
||||
|
||||
public Response<List<string>> GetValuesByClaimType(IEnumerable<Claim> claims, string claimType)
|
||||
{
|
||||
List<string> values = new List<string>();
|
||||
|
||||
values.AddRange(claims.Where(x => x.Type == claimType).Select(x => x.Value).ToList());
|
||||
|
||||
return new OkResponse<List<string>>(values);
|
||||
}
|
||||
|
||||
|
||||
private Response<string> GetValue(IEnumerable<Claim> claims, string key)
|
||||
{
|
||||
var claim = claims.FirstOrDefault(c => c.Type == key);
|
||||
|
@ -7,5 +7,6 @@
|
||||
public interface IClaimsParser
|
||||
{
|
||||
Response<string> GetValue(IEnumerable<Claim> claims, string key, string delimiter, int index);
|
||||
Response<List<string>> GetValuesByClaimType(IEnumerable<Claim> claims, string claimType);
|
||||
}
|
||||
}
|
@ -45,7 +45,7 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Twitter" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="1.1.1" />
|
||||
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="1.0.2" />
|
||||
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="1.1.1" />
|
||||
<PackageReference Include="CacheManager.Core" Version="0.9.2" />
|
||||
@ -53,7 +53,7 @@
|
||||
<PackageReference Include="CacheManager.Microsoft.Extensions.Logging" Version="0.9.2" />
|
||||
<PackageReference Include="Consul" Version="0.7.2.1" />
|
||||
<PackageReference Include="Polly" Version="5.0.3" />
|
||||
<PackageReference Include="IdentityServer4" Version="1.0.1" />
|
||||
<PackageReference Include="IdentityServer4" Version="1.5.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="1.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -15,6 +15,7 @@ namespace Ocelot.Responder
|
||||
|
||||
if (errors.Any(e => e.Code == OcelotErrorCode.UnauthorizedError
|
||||
|| e.Code == OcelotErrorCode.ClaimValueNotAuthorisedError
|
||||
|| e.Code == OcelotErrorCode.ScopeNotAuthorisedError
|
||||
|| e.Code == OcelotErrorCode.UserDoesNotHaveClaimError
|
||||
|| e.Code == OcelotErrorCode.CannotFindClaimError))
|
||||
{
|
||||
|
Reference in New Issue
Block a user