I'd like to reject an OData request if it has $top option value exceeding a global limit. ODataQueryOptions is created manually and I don't know how to correctly trigger its validation.
Assemblies affected
<PackageReference Include="Microsoft.AspNetCore.OData" Version="7.0.0" />
Reproduce steps
I do global setup like this:
public static void UseODataRoutes(this IRouteBuilder routeBuilder,
IEdmModel edmModel)
{
routeBuilder.MapODataServiceRoute(RouteName,
RoutePrefix,
containerBuilder => containerBuilder.AddService(ServiceLifetime.Singleton, provider => edmModel));
routeBuilder.Count().Filter().OrderBy().Select().MaxTop(1);
routeBuilder.EnableDependencyInjection();
}
And manually create an ODataQueryOptions<TEntity> instance:
public ODataQueryFactory(IHttpContextAccessor httpAccessor, IPerRouteContainer container)
{
_request = httpAccessor.HttpContext.Request;
_odataServiceProvider = container.GetODataRootContainer(ODataExtentions.RouteName);
}
public ODataQueryOptions<TEntity> CreateQueryOptions<TEntity>()
where TEntity : class
{
var model = _odataServiceProvider.GetService<IEdmModel>();
var path = _request.ODataFeature().Path;
var context = new ODataQueryContext(model, typeof(TEntity), path);
var queryOptions = new ODataQueryOptions<TEntity>(context, _request);
return queryOptions;
}
The problem is that the global MaxTop is ignored leading to successful GET /foo?$top=10 request even if MaxTop(1) has been called.
However, if I just add:
queryOptions.Validate(new ODataValidationSettings()
{
MaxTop = 1
});
To my factory method, then the request with $top=10 produce a perfectly looking exception leading to 400 response. That's my goal.
How to automatically trigger this validation or automatically create an ODataValidationSettings instance using all the global settings previously passed to IRouteBuilder?
P.S. I'd like to avoid manual ODataValidationSettings creation and use the standard Odata API instead.
To validate the query using only global options, pass an empty ODataValidationSettings parameter:
queryOptions.Validate(new ODataValidationSettings());
More info on GitHub (I directly asked OData developers).
P.S. I still have no idea how to set a default global limit to prevent selecting all the entities from the db if $top hasn't been specified. The default validation doesn't cover this case.
Related
I want to be able to provide a claim from the current user directly in the parameters of a controller. So that I can write unit tests without touching the ClaimPrincipal magic.
Like the [FromUri] or [FromBody], maybe [FromClaim]?
I tried implementing a CustomModelProvider as specified in this documentation from Microsoft: https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-2.2
But I do not know how can I provide the ClaimsPrincipal or List.
Also the ValueProvider returns a string, so I am unsure that this is actually feasible.
This is my attempt of a ClaimModelBinder
public class ClaimModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));
var modelName = bindingContext.ModelName;
// Try to fetch the value of the argument by name
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None) return Task.CompletedTask;
bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
var value = valueProviderResult.FirstValue;
// TODO: Unsure, how to continue after this.
// Check if the argument value is null or empty
if (string.IsNullOrEmpty(value)) return Task.CompletedTask;
int id = 0;
if (!int.TryParse(value, out id))
{
// Non-integer arguments result in model state errors
bindingContext.ModelState.TryAddModelError(
modelName,
"Author Id must be an integer.");
return Task.CompletedTask;
}
// Model will be null if not found, including for
// out of range id values (0, -3, etc.)
bindingContext.Result = ModelBindingResult.Success(null);
return Task.CompletedTask;
}
}
Can you provide a source for "constructing a ClaimsPrincipal for testing is far easier and more correct than what you are trying to do"?
The source is me. As to why I said it, it's based on the understanding of how the ASP NET Core framework is written, as I demonstrate below.
To answer your question, Controller has a User property to access claims, there is no need to write a Model Binder to access claims when there is already a User property, unless of course you cannot access claims from that User property due to your claims logic being different. But you haven't made such mentions.
"I want to be able to provide a claim from the current user directly in the parameters of a controller. So that I can write unit tests without touching the ClaimPrincipal magic."
I interpreted this as,
"I want to write unit tests for my controller that has logic involving the Claims Principal but I do not know how to provide a fake Claims Principal so I'm going to avoid that and pass a method parameter instead"
The ClaimsPrincipal can be unmagiced as follows.
Controller has a User property but it is Get only. Magic
HttpContext has a User property that is Get and Set (Nice) but Controller.HttpContext is Get only (Not So Nice)
Controller has a ControllerContext property which is Get and Set, ControllerContext has a HttpContext property which is Get and Set. Jackpot!
This is the source code of ControllerBase which is what Controller and ApiController derive from,
public abstract class ControllerBase
{
/* simplified below */
public ControllerContext ControllerContext
{
get => _controllerContext;
set => _controllerContext = value;
}
/* ... */
public HttpContext HttpContext => ControllerContext.HttpContext;
/* ... */
public ClaimsPrincipal User => HttpContext?.User;
}
As you see here, the User you access is a convenience Getter that ultimately accesses ControllerContext.HttpContext.User. Knowing this information, you can unit test a controller that uses a ClaimsPrincipal as follows.
// Create a principal according to your requirements, following is exemplary
var principal = new ClaimsPrincipal(new ClaimsIdentity(new []
{
// you might have to use ClaimTypes.Name even for JWTs issued as sub.
new Claim(JwtRegisteredClaimNames.Sub, "1234"),
new Claim(JwtRegisteredClaimNames.Iss, "www.example.com"),
}, "Bearer"));
var httpContext = new DefaultHttpContext();
httpContext.User = principal;
// Fake anything you want
httpContext.Request.Headers = /* ... */
var controller = new ControllerUnderTest(...);
controller.ControllerContext = new ControllerContext();
controller.ControllerContext.HttpContext = httpContext;
// Test the action, no need to pass claims as parameters because the User property is set
var result = controller.ActionThatUsesUserClaims(...);
Assert.Something(result, expected);
This is how ASP NET Core works every time a a real web request is received. It literally does the above to make the controller functional and ready for you to use.
All of the above are part of the public ASP NET Core api and are not subject to breaking changes without a major version bumb so they are safe to use. In fact, this is one of the things that set ASP Net Core apart from the old ASP NET MVC which was a nightmare to test as it did not expose any of the above publicly.
Having said all this, for some reason I have overlooked, if you really need to write a model binder to provide claims, inject the HTTPContextAccessor. But that requires you to check the type of the method parameters and branch execution. One branch would bind properties from the value provider while the other binds from the HttpContext. But why bother when you can do the above with 0 refactoring?
We're using ASP.NET Core 2.2 to build a Web API project that effectively is a smart proxy for another API. We don't want to do anything special with access tokens though, and just forward them when calling the other API.
I want to avoid "sync over async" calls, especially in constructors and DI resolvers and factories, while still relying on the framework to handle retrieving access tokens from the current http request.
Here's a representative repro of our scenario:
public class TheOtherApi : ITheOtherApi
{
private readonly HttpClient _client;
public TheOtherApi(HttpClient client, IHttpContextAccessor httpContextAccessor)
{
_client = client;
// Yuck! Sync over async, inside a constructor!
var token = httpContextAccessor.HttpContext.GetTokenAsync("access_token").Result;
client.SetBearerToken(token);
}
public Task<string> ForwardSomethingCool(MyCommand command)
{
// E.g.
var response await _client.PostAsync(command.path, command.body);
return await response.Content.ReadAsStringAsync();
}
}
Registrations in Startup along these lines:
services.AddScoped<ITheOtherApi, TheOtherApi>();
services.AddHttpContextAccessor();
services.AddScoped(_=> new HttpClient { BaseAddress = new Uri("https://example.org/api") });
This perfectly demonstrates my problem: there's a .Result in there, that I want to get rid of entirely, without just moving it to some kind of factory function registered in Startup.
Searching for a synchronous way to get the access token, I went down into the source of GetTokenAsync(...) and see what other method I could use intead. I find that it in fact has all sorts of side-effects, for example it does AuthenticateAsync(...) before doing what the method name suggests ("getting a token" from the context).
I actually only want the logic from GetTokenValue(...) without the other bits, and hurray (!): it is not async.... but that method relies on AuthenticationProperties which I don't have readily available from the HttpContextAccessor?
My current workaround (or 'solution'?) is to do the work myself:
httpContextAccessor.HttpContext.Request.Headers
.TryGetValue("Authorization", out var authorizationHeader);
var bearerToken = authorizationHeader
.SingleOrDefault()
?.Replace("bearer ", "", StringComparison.InvariantCultureIgnoreCase);
if (string.IsNullOrWhiteSpace(bearerToken))
{
throw new ArgumentException(nameof(httpContextAccessor), "HttpContextAccessor resulted in no Access Token");
}
_client.SetBearerToken(token);
How could I synchronously get the access_token from a IHttpContextAccessor without writing my own entire headers/string manipulation helper function to extract it from the Headers?
I want to use GraphQL in my project, so I want to know which one is the better option:
public class UserQuery : ObjectGraphType<object>
{
public UserQuery(UserData data)
{
Name = "Query";
Field<User>("user", resolve: context => data.GetUser(1));
}
}
So for implementing data.GetUser(1), I have a two options:
Option 1: Call Http endpoint and fetch the response through the HttpResponse
var client = new Restclient("BaseUrl");
client.Execute(new RestRequest("api/v1/account/1"));
Advantage: Doen't need to validate the request and response, also all custom filtering and exception handling will be applied automatiaccly*
Disadvantage: Double Http call, also return bigger response*
Option 2: Call Api directly through the Controller
var controller = new AccountController();
controller.GetById(1);
Advantage: Return lighter object and better performance*
Disadvantage: Have to apply all validations and exceptions handling again.*
I'm using Microsoft.AspNet.OData v6.0.0 and expect that setting the MaxTop value to 10 will enable the $top query option.
However, requesting the URL http://localhost:23344/odata/v4/Resources?$top=10 still gives me the error:
{"error":{"code":"","message":"The query specified in the URI is not valid. The limit of '0' for Top query has been exceeded. The value from the incoming request is '10'.","innererror":{"message":"The limit of '0' for Top query has been exceeded. The value from the incoming request is '10'.","type":"Microsoft.OData.ODataException","stacktrace":" at System.Web.OData.Query.Validators.TopQueryValidator.Validate(TopQueryOption topQueryOption, ODataValidationSettings validationSettings)\r\n at System.Web.OData.Query.TopQueryOption.Validate(ODataValidationSettings validationSettings)\r\n at System.Web.OData.Query.Validators.ODataQueryValidator.Validate(ODataQueryOptions options, ODataValidationSettings validationSettings)\r\n at System.Web.OData.Query.ODataQueryOptions.Validate(ODataValidationSettings validationSettings)\r\n at System.Web.OData.EnableQueryAttribute.ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions)\r\n at System.Web.OData.EnableQueryAttribute.ExecuteQuery(Object response, HttpRequestMessage request, HttpActionDescriptor actionDescriptor, ODataQueryContext queryContext)\r\n at System.Web.OData.EnableQueryAttribute.OnActionExecuted(HttpActionExecutedContext actionExecutedContext)"}}}
As though the top query still has a limit of 0.
Controller
public class ResourcesController : ODataController
{
private IResourceService resourceService;
public ResourcesController(IResourceService resourceService)
{
this.resourceService = resourceService;
}
[EnableQuery(MaxTop=10)]
public IQueryable<Resource> Get()
{
return resourceService.GetResources().AsQueryable();
}
[EnableQuery]
public SingleResult<Resource> Get([FromODataUri] int key)
{
var result = resourceService.GetResources().Where(r => r.Id == key).AsQueryable();
return SingleResult.Create(result);
}
}
WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder
{
Namespace = "MyNamespace",
ContainerName = "DefaultContainer"
};
builder.EntitySet<Resource>("Resources");
builder.EntityType<Resource>().Select().Count().Expand().OrderBy();
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: "odata/v4",
model: builder.GetEdmModel());
}
What I've found
Github issue describing possible bug with behaviour of MaxTop
What does work
Every other query option I've enabled, including $skip.
What I've Tried
As in this question Setting config.Select().Expand().Filter().OrderBy().MaxTop(null).Count(); in the WebApiConfig.cs before config.mapODataServiceRoute(.... Didn't work.
Adding [Page(MaxTop = 100)] to my Resource model as in the same question. Didn't work.
Setting [Page] attribute on model. From WebApi OData documentation "if you set the Page Attribute, by default it will enable the $top with no-limit maximum value". Didn't work.
Setting [EnableQuery(PageSize=10)] attribute on controller. From WebApi OData documentation "if you set the Page Attribute, by default it will enable the $top with no-limit maximum value". Enabled paging but Didn't work.
The error says the limit was 0 for top in every case
I had the same issue and discovered that if you specify any rules in builder like you did.
builder.EntitySet<Resource>("Resources");
builder.EntityType<Resource>().Select().Count().Expand().OrderBy();
Value set by attribute will be overriden and set to 0.
If you remove those entries and put global configuration like
config.Select().Expand().Filter().OrderBy().MaxTop(600).Count();
It will work.
Also you can define MaxTop using fluent interface on builder like this.
builder.EntityType<T>().Page(100, 100);
And it'll work even if you define other rules in builder for entity type.
Summarizing
It's probably caused that new configuration is created when you define some config in fluent builder interface and you cannot use attributes (EnableQueryAttribute on controller and Page on model).
It's probably a bug because they still recommend attribute approach. I will report it as issue on OData repository.
I had the same error. In my case I tried to hide a specific field from an entity with the Select(..) extension on EntityType<..>().
builder
.EntityType<User>()
.Select(System.Web.OData.Query.SelectExpandType.Disabled, "Password");
This happens inside my GetModel()-method. So I just changed the order in which I set the global settings:
config.Count().Filter().OrderBy().Expand().Select().MaxTop(100);
IEdmModel model;
model = GetModel();
This is for the People coming with the same issue for .NetCore 6.0 . These filters can be added in the Program.cs with the AddController (controller extension) as shown below.
builder.Services.AddControllers(options =>
{
options.Filters.Add(new EnableQueryAttribute()
{
AllowedQueryOptions = AllowedQueryOptions.All,
AllowedOrderByProperties = null,
AllowedLogicalOperators = AllowedLogicalOperators.All,
});
}).AddOData(o => o.Select().Filter().OrderBy().SetMaxTop(100));
Required namespaces are
using Microsoft.AspNetCore.OData;
using Microsoft.AspNetCore.OData.Query;
I am new to this so i will start with the code and after that i will explain.
The problem is this
[HttpGet, ODataRoute("({key})")]
public SingleResult<Employee> GetByKey([FromODataUri] string key)
{
var result = EmployeesHolder.Employees.Where(id => id.Name == key).AsQueryable();
return SingleResult<Employee>.Create<Employee>(result);
}
[HttpGet, ODataRoute("({key})")]
public SingleResult<Employee> Get([FromODataUri] int key)
{
var result = EmployeesHolder.Employees.Where(id => id.Id == key).AsQueryable();
return SingleResult<Employee>.Create<Employee>(result);
}
I have those 2 actions but for one i want to search by a string and for the other by number (although this is not the problem). If i leave it this way it will work for the (int) case but for the string "....odata/Employees('someName')" i will get a : HTTP 404 (and it's normal) but if i try to be more specific with the method which takes a string
Code in webApiConfig.
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Employee>("Employees");
builder.Function("GetByKeyFromConfig").Returns<SingleResult<Employee>>().Parameter<string>("Key");
Code in Controller
[ODataRoutePrefix("Employees")]
public class FooController : ODataController
{
[HttpGet, ODataRoute("GetByKeyFromConfig(Key={key})")]
public SingleResult<Employee> GetByKey([FromODataUri] string key)
{ ... }
}
i get an expcetion
{"The complex type
'System.Web.Http.SingleResult`1[[OData_Path.Employee, OData_Path,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]' refers to the
entity type 'OData_Path.Employee' through the property 'Queryable'."}
If i change the return type in for the method in WebApiConfig
builder.Function("GetByKeyFromConfig").Returns<Employee>().Parameter<string>("Key");
I get this which i have no idea why.
{"The path template 'Employees/GetByKeyFromConfig(Key={key})' on the
action 'GetByKey' in controller 'Foo' is not a valid OData path
template. The request URI is not valid. Since the segment 'Employees'
refers to a collection, this must be the last segment in the request
URI or it must be followed by an function or action that can be bound
to it otherwise all intermediate segments must refer to a single
resource."}
I have searched and tried to get explanation , but each time i found an answer it does not work. i am struggling for days.
After the updates taken from the 2 answers
still have the Invalid OData path template exception
WebApiConfig Code
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Employee>("Employees").EntityType.HasKey(p => p.Name);
var employeeType = builder.EntityType<Employee>();
employeeType.Collection.Function("GetByKey").Returns<Employee>().Parameter<int>("Key");
config.EnableUnqualifiedNameCall(unqualifiedNameCall: true);
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: null,
model: builder.GetEdmModel());
Controller Code
[EnableQuery, HttpGet, ODataRoute("Employees/GetByKey(Key={Key})")]
public SingleResult<Employee> GetByKey([FromODataUri] int Key)
{
var single = Employees.Where(n => n.Id == Key).AsQueryable();
return SingleResult<Employee>.Create<Employee>(single);
}
I've also tried using a specific Namespace
builder.Namespace = "NamespaceX";
[EnableQuery, HttpGet, ODataRoute("Employees/NamespaceX.GetByKey(Key={Key})")]
And
[EnableQuery, HttpGet, ODataRoute("Employees/NamespaceX.GetByKey")]
While you can solve your problem with OData functions, a cleaner solution would be to use alternate keys. As Fan indicated, Web API OData provides an implementation of alternate keys that will allow you to request Employees by name or number with a more straightforward syntax:
GET /Employees(123)
GET /Employees(Name='Fred')
You will need to add the following code to your OData configuration.
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Library;
// config is an instance of HttpConfiguration
config.EnableAlternateKeys(true);
// builder is an instance of ODataConventionModelBuilder
var edmModel = builder.GetEdmModel() as EdmModel;
var employeeType = edmModel.FindDeclaredType(typeof(Employee).FullName) as IEdmEntityType;
var employeeNameProp = employeeType.FindProperty("Name");
edmModel.AddAlternateKeyAnnotation(employeeType, new Dictionary<string, IEdmProperty> { { "Name", employeeNameProp } });
Make sure you add the alternate key annotations before you pass the model to config.MapODataServiceRoute.
In your controller, add a method to retrieve Employees by name (very similar to the GetByKey method in your question).
[HttpGet]
[ODataRoute("Employees(Name={name})")]
public IHttpActionResult GetEmployeeByName([FromODataUri] string name)
{
var result = EmployeesHolder.Employees.FirstOrDefault(e => e.Name == name);
if (result == null)
{
return this.NotFound();
}
return this.Ok(result);
}
Can this document about function support for OData/Webapi help you? http://odata.github.io/WebApi/#04-06-function-parameter-support there is some problem in your second approach.
you are call with Employees/GetByKeyFromConfig(Key={key}) so you should declare the function like:
builder.EntityType<Employee>().Collection.Function("GetByKeyFromConfig")
you should call with namespace, like Employees/yournamespace.GetByKeyFromConfig
The first scenario can you use this feature? http://odata.github.io/WebApi/#04-17-Alternate-Key
Hope this can help,Thanks.
Update:
Function declaration is right now, the error message shown because your ODataRoute is wrong, remove your ODataRoute, that function can be called as Employees/Default.GetByKey(1), WebAPI/OData can route to this function by default.
Or add the namespace to ODataRoute, it's Default by default, change builder.Namespace is not right, you have to change the function's namespace:
var func = employeeType.Collection.Function("GetByKey").Returns<Employee>().Parameter<int>("Key");
func.Namespace = "NamespaceX";
Then ODataRoute like [EnableQuery, HttpGet, ODataRoute("Employees/NamespaceX.GetByKey(Key={Key})")] should work.
There are two issues with the code you paste,
1. You try to bind a function to Employees collection, the model build is incorrect.
It should be something like
var employeeType = builder.EntityType();
employeeType .Collection.Function("GetByKeyFromConfig").Returns().Parameter("Key");
You can refer to examples in link https://github.com/OData/ODataSamples/tree/master/WebApi/v4/ODataFunctionSample for different ways to bind functions.
In the ODataRoute, we either need to specify the function with namespace or enable unqualified function call via this config in register method "config.EnableUnqualifiedNameCall(unqualifiedNameCall: true);".
Refer to link http://odata.github.io/WebApi/#03-03-attrribute-routing and search unqualified.
Let me know if this does not resolve your issue.