Handling code duplication in API versioning .NET Core - c#

I'm currently trying to support API versioning using .NET Core API.
I have read many articles about the matter , Couldn't find a really good code examples for it.
Everyone is posting about the controller and how to add API version to each end point but none is actually talking about the headache afterwards. Meaning duplicating the models and the functions (service/handler)
Let's say I have a User controller which has more than 5 end points.
One of these end point is GET User. We needed to remove a field(age field) in the response and it's a breaking change. So we added 2 end point one support the default V1 and the other support V2
[ApiController]
[Route("api/User")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class UserController : ControllerBase {
[HttpGet("user")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> GetUser([FromQuery] string id)
{
return Ok(await _service.GetUser(id));
}
[HttpGet("user")]
[MapToApiVersion("2.0")]
public async Task<IActionResult> GetUserV2([FromQuery] string id)
{
return Ok(await _service.GetUser(id));
}
}
assuming the GetUser() function has a heavy logic ( +30 lines of codes)
the only difference between V1 and V2 is in the model itself one will return the age
and one will not.
What is the better approach to handle such situation?
is it better to duplicate GetUser() as
GetUser(int id)
GetUserV2(int id)
Or pass a version number to the function and do the change accordingly
GetUser(int id , int version)
for my personal opinion. I prefer the duplication as it will be less complicated and easy to read. But duplicating all code also seems useless.
As this is my first time trying to support versioning. I would really appreciate some thoughts and ideas from you !

There is no "one size fits all" solution. What makes sense for your particular application will vary. Here are few ideas that may work for you. There is no preference in order nor is any one particular solution necessarily better than the other. Some options can even be combined together.
Option 1
Move as much logic as possible out of your controllers. Controllers are just a way to represent your API over HTTP. By delegating as much of the logic as possible into collaborators, you can likely reduce a lot of duplication.
Ideally, an action method should be less than 10 lines of code. Extension methods, custom results, and so on can help reduce duplication.
Option 2
Define a clear versioning policy; for example N-2. This can really help clamp down on duplication, but not necessarily eliminate it. Managing duplication across 3 versions is much more manageable if it's unbound.
It should be noted that sharing across versions also comes with some inherent risks (which you might be willing to accept). For example, a change or fix could affect multiple versions and in unexpected or undesirable ways. This is more likely to occur when interleaving multiple versions on a single controller. Some services choose a Copy & Paste approach for new versions to retain the same base implementation, but then allow the implementations to evolve independently. That doesn't mean you can't have shared components, just be careful what you share.
Option 3
Use nullable attributes and ensure your serialization options do not emit null attributes. This obviously doesn't work if you allow or use explicit null values.
For example, the age attribute can be removed using a single model like this:
public class User
{
// other attributes omitted for brevity
public int? Age { get; set; }
}
[HttpGet("user")]
[MapToApiVersion("2.0")]
public async Task<IActionResult> GetUserV2([FromQuery] string id)
{
var user = await _service.GetUser(id);
// if nulls are not emitted, then this effective 'removes' the
// 'age' member using a single model
user.Age = null;
return Ok(user);
}
Option 4
Use an adapter. This could get tedious if you don't have a fixed versioning policy, but is manageable for a limited number of versions. You could also using templating or source generators to render the code for you.
public class User2Adapter
{
private readonly User inner;
public User2Adapter(User user) => inner = user;
public FirstName => inner.FirstName;
public LastName => inner.LastName;
}
[HttpGet("user")]
[MapToApiVersion("2.0")]
public async Task<IActionResult> GetUserV2([FromQuery] string id)
{
return Ok(new User2Adapter(await _service.GetUser(id)));
}
This approach is used for serializing ProblemDetails using Newtonsoft.Json (see here)
This can also be achieved with anonymous types:
[HttpGet("user")]
[MapToApiVersion("2.0")]
public async Task<IActionResult> GetUserV2([FromQuery] string id)
{
var user = await _service.GetUser(id);
var userV2 = new
{
firstName = user.FirstName,
lastName = user.LastName,
};
return Ok(userV2);
}
Option 5
Use a custom OutputFormatter. The default implementation in SystemJsonTextOutputFormatter doesn't honor the specified object type unless the supplied object itself is null. You can change this behavior.
A complete implementation would be a bit verbose, but you can imagine that you might have something like this (abridged):
public class VersionedJsonOutputFormatter : TextOutputFormatter
{
private readonly Dictionary<ApiVersion, Dictionary<Type, Type>> map = new()
{
[new ApiVersion(1.0)] = new()
{
[typeof(User)] = typeof(User),
},
[new ApiVersion(2.0)] = new()
{
[typeof(User)] = typeof(User2),
},
}
public VersionedJsonOutputFormatter(
JsonSerializerOptions jsonSerializerOptions)
{
// TODO: copy SystemJsonTextOutputFormatter implementation
}
public override async Task WriteResponseBodyAsync(
OutputFormatterWriteContext context,
Encoding selectedEncoding)
{
// IMPORTANT: abridged with many assumptions; look at
// SystemJsonTextOutputFormatter implementation
var httpContext = context.HttpContext;
var apiVersion = httpContext.GetRequestedApiVersion();
var objectType = map[apiVersion][context.Object.GetType()];
var ct = httpContext.RequestAborted;
try
{
await JsonSerializer.SerializeAsync(
responseStream,
context.Object,
objectType,
SerializerOptions,
ct);
await responseStream.FlushAsync(ct);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
}
}
}
This is just one approach. There are plenty of variations on how you can change the mapping.
Option 6
This one area where OData (or even EF) really shines. The use of an Entity Data Model (EDM) separates the model over the wire vs the code model. You can have a single, unified code model with a different EDM per API version that controls how that is serialized over the wire. I'm not sure you can yank only the specific bits that you want for EDM and serialization, but if you can, it just might get you what you want with minimal effort. This is approach is certainly useful for APIs outside of the context of OData.
The OData examples for API Versioning show this at work. I've never tried using things in a purely non-OData way, but that doesn't mean it can't be made to work.

I would prefer the
GetUser(int id , int version)
and add a few comments on why you're using this version varible and use a switch case inside rather than writing duplicate code.
For me personally, writing such duplicate code is not a very good practice as I find it redundant.

Related

Repository Interface with (or without) IProgress

I've got a repository interface (simplified example code):
public interface IPersonRepository
{
Task<PersonDTO> Get();
}
With two implementations.
One for a direct connection to a database:
public SqlPersonRepository : SqlRepository, IPersonRepository
{
public SqlPersonRepository(IDbConnectionProvider dbCon) : base(dbCon) { }
public async Task<PersonDTO> Get()
{
// use dbCon and dapper to get PersonDTO from database
}
}
And another one for remote access via web api:
public ApiPersonRepository : ApiRepository, IPersonRepository
{
public ApiPersonRepository(IApiConnectionProvider apiCon) : base(apiCon) { }
public async Task<PersonDTO> Get()
{
// use apiCon (contains base url and access token) to perform an HTTP GET request
}
}
The interface makes sense here, because the server can use the SqlPersonRepository. And the remote (native) client can use the ApiPersonRepository. And for most all of the the use cases, this is all I need.
However, my application supports an extraction of a subset of data from the server so that the client application can run while offline. In this case, I'm not just grabbing one person, I'm grabbing a large set of data (several to tens of megabytes) which many times will be downloaded over a slow mobile connection. I need to pass in an IProgress implementation so I can report progress.
In those cases, I need an ApiDatabaseRepository that looks like this:
public ApiDatabaseRepository : ApiRepository, IDatabaseRepository
{
public ApiDatabaseRepository(IApiConnectionProvider apiCon) : base(apiCon) { }
public async Task<DatabaseDTO> Get(IProgress<int?> progress)
{
// use apiCon (contains base url and access token) to perform an HTTP GET request
// as data is pulled down, report back a percent downloaded, e.g.
progress.Report(percentDownloaded);
}
}
However the SqlDatabaseRepository does NOT need to use IProgress (even if Dapper COULD report progress against a database query, which I don't think it can). Regardless, I'm not worried about progress when querying the database directly, but I am worried about it when making an API call.
So the easy solution, is that the SqlDatabaseRepository implementation accepts the IProgress parameter, with a default value of null, and then the implementing method just ignores that value.
public SqlDatabaseRepository : SqlRepository, IDatabaseRepository
{
public SqlDatabaseRepository(IDbConnectionProvider dbCon) : base(dbCon) { }
public async Task<DatabaseDTO> Get(IProgress<int?> progress = null)
{
// use dbCon and dapper to get DatabaseDTO from database
// progress is never used
}
}
But that smells funny. And when things smell funny, I wonder if I'm doing something wrong. This method signature would give the indication that progress will be reported, even though it won't.
Is there a design pattern or a different architecture I should be using in this case?
Oversimplifying this, you basically have 2 options: having a consistent interface or not.
There are, of course other design patterns which might work here, (e.g.; some decorators and a factory method), but I believe them to be overkill.
If you stick to the general rule that consistent interface is desired, I think having a "not entirely implemented" callback technique isn't that bad. You could also consider just to implement it - or at least: make it return something which makes sense.
I would definitely avoid a construction with 2 different interfaces of some kind. Although sometimes this is the better option (when checking if something supports something), e.g.; testing if a hardware component is available - I see it as overkill in your scenario. It would also put more logic at the caller side, and unless you want to open a process-dialog screen only in this scenario, I would avoid it.
A last note: there are alternative progress report patterns such as using an event, or, passing in an optional callback method. This latter looks like your solution but is in fact a little different.
Still this faces you with the same issue in the end, but might be worth to consider.
There are many more solutions - but given the context you provided, I am not sure if they apply. And keep in mind, this is highly opinion based.

What type of collection should be returned from EFCore 3.1 in WebAPI?

I'm writing ASP.NET Core 3.1 Web API and I caught myself being confused... if I use the right type to return.
So here is my code (I removed paging for simplicity):
public class UserData
{
public IEnumerable<UserDto> GetUsers()
{
var users = _fni.Users.AsNoTracking()
.OrderBy(row => row.UserName);
return _mapper.Map<IEnumerable<UserDto>>(users);
}
}
[HttpGet]
public IActionResult GetUsers()
{
var users = _userData.GetUsers();
return Ok(users);
}
Users are queried from the database and immediately returned to the API's client. In C# 8 era, what's the best way to return this collection?
IEnumerable
List<UserDto>
async Task<List<UserDto>>
IAsyncEnumerable
EDIT: The question is about what should be returned by EFCore3.1, in the Data Repository in UserData class. Not from the Controller.
Here is the documentation from Microsoft regarding return types from Controller. See if that helps you
https://learn.microsoft.com/en-us/aspnet/core/web-api/action-return-types?view=aspnetcore-3.1
Generally IEnumerable is a good choice to seal the results before send them to the request owner. Thus you can be sure that the collection did not change during the transmission. (by any mid-level function etc.)
IAsyncEnumerable is also a good choice if you are planning to iterate the whole list immediately.
Check this to see various use cases of IAsyncEnumerable: https://dotnetcoretutorials.com/2019/01/09/iasyncenumerable-in-c-8/
Also, as far as I can understand this code part belongs to a repository. You may need to reconsider using dto mapping inside of a repository. Since the repository pattern only responsible for the management of the data in a lower level, mapping the data to the transfer objects is not a responsibility of the repository pattern. (Breaks the single responsibility rule)

Conflicting routes in ASP.NET Core when using Attribute based routing

Context
I am trying to build an ASP.NET Core web API controller that will expose the following methods with specific semantics:
/api/experimental/cars — get entire collection
[HttpGet("/api/experimental/cars/")]
public Task<List<Car>> AllCars()
/api/experimental/cars/123 — get car by id "123"
[HttpGet("/api/experimental/cars/{carId}")]
public Task<Car> CarById([FromRoute] string carId)
/api/experimental/cars?nameFilter=Maz — get cars that match nameFilter = "Maz"
[HttpGet("/api/experimental/cars/{nameFilter?}")]
public Task<List<Car>> CarsByNameFilter([FromQuery] string nameFilter = "")
/api/experimental/cars?nameFilter=Maz&rating=2 — get cars that match nameFilter = "Maz" and with rating greater or equal to 2
[HttpGet("/api/experimental/cars/{nameFilter?}/{rating?}")]
public Task<List<Car>> CarsByNameAndRatingFilter([FromQuery] string nameFilter = "", [FromQuery] int rating = 1)
Note: I really want to keep the controller class clean and have a single method per Web API route — is it possible?
Problem
As you could guess, there's an issue with these API definitions. Basically, AllCars is intercepting pretty much all the requests. (When I was able to get at least the /api/experimental/cars/{carId} working, the query-string based APIs were still not working and intercepted by another method...
I tried many possible route syntaxes to express what I want with no luck. Is it even possible to use the default routing mechanism or I need to implement my own Router class or Middleware or something else?
Update 1: Problem definition
I know I can join at least three methods and their routes into a single WebAPI method that is being smart about the received parameters. Notice that this is exactly what I am trying to avoid.
Why?
Reason 1: I saw that in non-.NET routers, it worked well and there's no technical impossibility to implement semantic based route resolution.
Reason 2: I perceive all four URL patterns mentioned above as four different routes. One may not agree with me and it's okay, but for my purposes the methods and the routes are different and have to stay different.
Reason 3.1: This keeps controller code clean. Every method only handles one specific case. Parameter names are sufficient to properly resolve the routes (at least in humans head, therefore machine can do it too -- it's easy to formalize the algorithm). If client make a request with an unsupported query parameter, it should result in HTTP 404 Not Found or HTTP 400 Bad Request -- totally fine (client rather construct correct URLs).
Reason 3.2: On contrary, if I join the methods and use a more generic route, my implementation needs to be 'smart' about the combination of parameters. This is effectively, a leak of routing abstractions into a layer where it does not belong in my architecture. Complex validation is another thing I don't want to see in the Controller -- less code is better.
Update 2: Nancy — Another .NET example (other that .NET Core WebApi)
There is Nancy (a .NET framework) which perfectly deals with this aspect of routing: https://github.com/NancyFx/Nancy/wiki/Defining-routes#pattern The issue is that in my project we're not using it... Nancy works as a perfect example of a tool that leaves exact definition of routing semantics to the client, instead of enforcing too tight rules on what is the route vs what is not.
You could Achieve this with just two routes:
[HttpGet("/api/experimental/cars/")]
public Task<List<Car>> SearchCars([FromQuery] string nameFilter = "", [FromQuery] int rating = 1)
and
[HttpGet("/api/experimental/cars/{carId}")]
public Task<Car> CarById([FromRoute] string carId)
I.e one route which brings the entire set back but can be filtered accordingly and one the brings back a single Car object by Id.
You will notice that the SearchCars method doesn't include the parameters in the route, FromQuery will catch these anyway.
EDIT:
if your request becomes complex it can be nice to define a custom request object type to wrap all your filters together:
public class MyRequestObject
{
public string NameFilter {get;set;}
public int Rating {get;set;}
}
then:
[HttpGet("/api/experimental/cars/")]
public Task<List<Car>> SearchCars([FromQuery] MyRequestObject requestParams)
Take a look at the following suggested routes that when tested do not conflict with each other and still allow for all the actions to be segregated.
[Route("api/experimental/cars")]
public class CarsController : Controller {
//GET api/experimental/cars
[HttpGet("")]
public IActionResult AllCars() { ... }
//GET api/experimental/cars/123
[HttpGet("{carId}")]
public IActionResult CarById(string carId) { ... }
//GET api/experimental/cars/named/Maz
//GET api/experimental/cars/named?filter=Maz
[HttpGet("named/{filter?}")]
public IActionResult CarsByNameFilter(string filter = "") { ... }
//GET api/experimental/cars/filtered?rating=2&name=Maz
//GET api/experimental/cars/filtered?rating=2
//GET api/experimental/cars/filtered?name=Maz
[HttpGet("filtered")]
public IActionResult CarsByNameAndRatingFilter(string name = "", int rating = 1) { ... }
}
My experience with this topic tells me that the best way to implement the APIs I wanted is to have two methods:
class CarsController {
// [HttpGet("/api/experimental/cars/")]
[HttpGet("/api/experimental/cars/{carId}")]
public Task<IEnumerable<Car>> CarById([FromRoute] string carId)
{
if (carId == null)
return GetAllCars();
else
return GetCarWithId(carId);
}
// [HttpGet("/api/experimental/cars/{nameFilter?}")]
[HttpGet("/api/experimental/cars/{nameFilter?}/{rating?}")]
public Task<IEnumerable<Car>> CarsByNameAndRatingFilter([FromQuery] string nameFilter = "", [FromQuery] int rating = 1)
{
// TODO Validate the combination of query string parameters for your specific API/business rules.
var filter = new Filter {
NameFilter = nameFilter,
Rating = rating
};
return GetCarsMatchingFilter(filter);
}
}
The first API is almost trivial. Even though returning a single item within a wrapping collection object may not look nice, it minimizes the number of API methods (which I personally am fine with).
The second API is trickier: in a way, it works as the façade pattern. I.e. that API will respond to pretty much all the possible /api/experimental/cars? based routes. Therefore, we need to very carefully validate the combination of received arguments before doing the actual work.

private controller actionresult with outputcache

I have a public actionresult which I would like to use as a sort of pre-processor for work before handing off to a private action result. I'm planning on using the first controller action to make a call to an external API which places the requesting IP address on a scale measuring potential fraudulent activity of that address. The possible levels that may be returned by this API call are Low, Medium, High.
The general idea of what I'm thinking is as follows:
public async Task<ActionResult> RiskCheck(string id, int page) {
// Check risk for request with external API
var riskLevel = await SomeRiskCheckAsync();
return PageOutput(id, page, riskLevel);
}
[OutputCache(Location = OutputCacheLocation.Server, Duration = Int32.MaxValue, VaryByParam = "id;page;riskLevel")]
private async Task<ActionResult> PageOutput(string id, int page, string riskLevel) {
if (riskLevel.Equals("Low") {
return View("Low_Risk");
} else if (riskLevel.Equals("Medium")) {
return View("Medium_Risk");
} else {
return View("High_Risk");
}
}
The end goal is that there will be 3 cached views corresponding to each unique combination of id and page such that I can alleviate the need to continuously re-render views which will always be the same output.
I have two questions about this approach. First, when applied to a private method like this will the output actually be cached or does it need to be a traditional client facing action result?
Second, I feel like I might be able to combine both views into one if I were able to use a VaryByCustom guard on RiskCheck and use that custom override to check the risk ahead of time. This is actually the approach I went for first. However when I went to override GetVaryByCustomString(HttpContext context, string custom) I realized that there is no async version provided.
public override GetVaryByCustomString(HttpContext context, string custom) {
// Can't await this result
var riskLevel = await SomeRiskCheckAsync();
return riskLevel;
}
I can't afford to not be able to keep this part of the application asynchronous as it will be hit frequently.
To answer your first question:
No the output caching will not be applied. The attribute has to be on the actual public action method because that is all the MVC framework knows about and has the ability to interrogate, whereas MVC knows nothing about your call to PageOutput and even if it did it's arguable what it should do with that piece of information i.e. what if there was another call to another private function with different output cache settings... things could get out of hand rather quickly.
My opinion on the second question:
I think you might be using the async framework for the hell of it, not to solve a problem :) You probably should think about getting rid of it to make life easier. As for combining the views into one, that is something that I'd need more clarification about what the views contain and how likely they are to differ from one another over time as functionality is increased in the product spec.

Web API OData Security per Entity

Background:
I have a very large OData model that is currently using WCF Data Services (OData) to expose it. However, Microsoft has stated that WCF Data Services is dead and that Web API OData is the way they will be going.
So I am researching ways to get Web API OData to work as well as WCF Data Services.
Problem Setup:
Some parts of the model do not need to be secured but some do. For example, the Customers list needs security to restrict who can read it, but I have other lists, like the list of Products, that any one can view.
The Customers entity has many many associations that can reach it. If you count 2+ level associations, the are many hundreds of ways that Customers can be reached (via associations). For example Prodcuts.First().Orders.First().Customer. Since Customers are the core of my system, you can start with most any entity and eventually associate your way to the Customers list.
WCF Data Services has a way for me to put security on a specific entity via a method like this:
[QueryInterceptor("Customers")]
public Expression<Func<Customer, bool>> CheckCustomerAccess()
{
return DoesCurrentUserHaveAccessToCustomers();
}
As I look at Web API OData, I am not seeing anything like this. Plus I am very concerned because the controllers I am making don't seem to get called when an association is followed. (Meaning I can't put security in the CustomersController.)
I am worried that I will have to try to somehow enumerate all the ways that associations can some how get to customers and put security on each one.
Question:
Is there a way to put security on a specific entity in Web API OData? (Without having to enumerate all the associations that could somehow expand down to that entity?)
UPDATE: At this point in time I would recommend that you follow the solution posted by vaccano, which is based on input from the OData
team.
What you need to do is to create a new Attribute inheriting from EnableQueryAttribute for OData 4 (or QuerableAttribute depending on which version of Web API\OData you are talking with) and override the ValidateQuery (its the same method as when inheriting from QuerableAttribute) to check for the existence of a suitable SelectExpand attribute.
To setup a new fresh project to test this do the following:
Create a new ASP.Net project with Web API 2
Create your entity framework data context.
Add a new "Web API 2 OData Controller ..." controller.
In the WebApiConfigRegister(...) method add the below:
Code:
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Customer>("Customers");
builder.EntitySet<Order>("Orders");
builder.EntitySet<OrderDetail>("OrderDetails");
config.Routes.MapODataServiceRoute("odata", "odata", builder.GetEdmModel());
//config.AddODataQueryFilter();
config.AddODataQueryFilter(new SecureAccessAttribute());
In the above, Customer, Order and OrderDetail are my entity framework entities. The config.AddODataQueryFilter(new SecureAccessAttribute()) registers my SecureAccessAttribute for use.
SecureAccessAttribute is implemented as below:
Code:
public class SecureAccessAttribute : EnableQueryAttribute
{
public override void ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions)
{
if(queryOptions.SelectExpand != null
&& queryOptions.SelectExpand.RawExpand != null
&& queryOptions.SelectExpand.RawExpand.Contains("Orders"))
{
//Check here if user is allowed to view orders.
throw new InvalidOperationException();
}
base.ValidateQuery(request, queryOptions);
}
}
Please note that I allow access to the Customers controller, but I limit access to Orders. The only Controller I have implemented is the one below:
public class CustomersController : ODataController
{
private Entities db = new Entities();
[SecureAccess(MaxExpansionDepth=2)]
public IQueryable<Customer> GetCustomers()
{
return db.Customers;
}
// GET: odata/Customers(5)
[EnableQuery]
public SingleResult<Customer> GetCustomer([FromODataUri] int key)
{
return SingleResult.Create(db.Customers.Where(customer => customer.Id == key));
}
}
Apply the attribute in ALL actions that you want to secure. It works exactly as the EnableQueryAttribute. A complete sample (including Nuget packages end everything, making this a 50Mb download) can be found here: http://1drv.ms/1zRmmVj
I just want to also comment a bit on some other solutions:
Leyenda's solution does not work simply because it is the other way around, but otherwise was super close! The truth is that the builder will look in the entity framework to expand properties and will not hit the Customers controller at all! I do not even have one, and if you remove the security attribute, it will still retrieve the orders just fine if you add the expand command to your query.
Setting the model builder will prohibit access to the entities you removed globally and from everyone, so it is not a good solution.
Feng Zhao's solution could work, but you would have to manually remove the items you wanted to secure in every query, everywhere, which is not a good solution.
I got this answer when I asked the Web API OData team. It seems very similar to the answer I accepted, but it uses an IAuthorizationFilter.
In interest of completeness I thought I would post it here:
For entity set or navigation property appears in the path, we could define a message handler or an authorization filter, and in that check the target entity set requested by the user. E.g., some code snippet:
public class CustomAuthorizationFilter : IAuthorizationFilter
{
public bool AllowMultiple { get { return false; } }
public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(
HttpActionContext actionContext,
CancellationToken cancellationToken,
Func<Task<HttpResponseMessage>> continuation)
{
// check the auth
var request = actionContext.Request;
var odataPath = request.ODataProperties().Path;
if (odataPath != null && odataPath.NavigationSource != null &&
odataPath.NavigationSource.Name == "Products")
{
// only allow admin access
IEnumerable<string> users;
request.Headers.TryGetValues("user", out users);
if (users == null || users.FirstOrDefault() != "admin")
{
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
}
return continuation();
}
}
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new CustomAuthorizationFilter());
For $expand authorization in query option, a sample.
Or create per user or per group edm model. A sample.
While I think that the solution provided by #SKleanthous is very good. However, we can do better. It has some issues which aren't going to be an issue in a majority of cases, I feel like it they were sufficient enough of a problem that I didn't want to leave it to chance.
The logic checks the RawExpand property, which can have a lot of stuff in it based on nested $selects and $expands. This means that the only reasonable way you can grab information out is with Contains(), which is flawed.
Being forced into using Contains causes other matching problems, say you $select a property that contains that restricted property as a substring, Ex: Orders and 'OrdersTitle' or 'TotalOrders'
Nothing is gaurenteeing that a property named Orders is of an "OrderType" that you are trying to restrict. Navigation property names are not set in stone, and could get changed without the magic string being changed in this attribute. Potential maintenance nightmare.
TL;DR: We want to protect ourselves from specific Entities, but more specifically, their types without false positives.
Here's an extension method to grab all the types (technically IEdmTypes) out of a ODataQueryOptions class:
public static class ODataQueryOptionsExtensions
{
public static List<IEdmType> GetAllExpandedEdmTypes(this ODataQueryOptions self)
{
//Define a recursive function here.
//I chose to do it this way as I didn't want a utility method for this functionality. Break it out at your discretion.
Action<SelectExpandClause, List<IEdmType>> fillTypesRecursive = null;
fillTypesRecursive = (selectExpandClause, typeList) =>
{
//No clause? Skip.
if (selectExpandClause == null)
{
return;
}
foreach (var selectedItem in selectExpandClause.SelectedItems)
{
//We're only looking for the expanded navigation items, as we are restricting authorization based on the entity as a whole, not it's parts.
var expandItem = (selectedItem as ExpandedNavigationSelectItem);
if (expandItem != null)
{
//https://msdn.microsoft.com/en-us/library/microsoft.data.odata.query.semanticast.expandednavigationselectitem.pathtonavigationproperty(v=vs.113).aspx
//The documentation states: "Gets the Path for this expand level. This path includes zero or more type segments followed by exactly one Navigation Property."
//Assuming the documentation is correct, we can assume there will always be one NavigationPropertySegment at the end that we can use.
typeList.Add(expandItem.PathToNavigationProperty.OfType<NavigationPropertySegment>().Last().EdmType);
//Fill child expansions. If it's null, it will be skipped.
fillTypesRecursive(expandItem.SelectAndExpand, typeList);
}
}
};
//Fill a list and send it out.
List<IEdmType> types = new List<IEdmType>();
fillTypesRecursive(self.SelectExpand?.SelectExpandClause, types);
return types;
}
}
Great, we can get a list of all expanded properties in a single line of code! That's pretty cool! Let's use it in an attribute:
public class SecureEnableQueryAttribute : EnableQueryAttribute
{
public List<Type> RestrictedTypes => new List<Type>() { typeof(MyLib.Entities.Order) };
public override void ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions)
{
List<IEdmType> expandedTypes = queryOptions.GetAllExpandedEdmTypes();
List<string> expandedTypeNames = new List<string>();
//For single navigation properties
expandedTypeNames.AddRange(expandedTypes.OfType<EdmEntityType>().Select(entityType => entityType.FullTypeName()));
//For collection navigation properties
expandedTypeNames.AddRange(expandedTypes.OfType<EdmCollectionType>().Select(collectionType => collectionType.ElementType.Definition.FullTypeName()));
//Simply a blanket "If it exists" statement. Feel free to be as granular as you like with how you restrict the types.
bool restrictedTypeExists = RestrictedTypes.Select(rt => rt.FullName).Any(rtName => expandedTypeNames.Contains(rtName));
if (restrictedTypeExists)
{
throw new InvalidOperationException();
}
base.ValidateQuery(request, queryOptions);
}
}
From what I can tell, the only navigation properties are EdmEntityType (Single Property) and EdmCollectionType (Collection Property). Getting the type name of the collection is a little different just because it will call it a "Collection(MyLib.MyType)" instead of just a "MyLib.MyType". We don't really care if it's a collection or not, so we get the Type of the Inner Elements.
I've been using this in production code for a while now with great success. Hopefully you will find an equal amount with this solution.
You could remove certain properties from the EDM programmatically:
var employees = modelBuilder.EntitySet<Employee>("Employees");
employees.EntityType.Ignore(emp => emp.Salary);
from http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-security-guidance
Would it be feasible to move this to your database? Assuming you're using SQL server, set up users which match the profiles you need for each client profile. Keeping it simple, one account with customer access and one without.
If you then map the user making a data request to one of these profiles and modify your connection string to include the related credentials. Then if they make a request to an entity they are not permitted to, they will get an exception.
Firstly, sorry if this is a misunderstanding of the problem. Even though I'm suggesting it, I can see a number of pitfalls most immediate being the extra data access control and maintenance within your db.
Also, I'm wondering if something can be done within the T4 template which generates your entity model. Where the association is defined, it might be possible to inject some permission control there. Again this would put the control in a different layer - I'm just putting it out there in case someone who knows T4s better than me can see a way to make this work.
The ValidateQuery override will help with detecting when a user explicitly expands or selects a navigable property, however it won't help you when a user uses a wildcard. For example, /Customers?$expand=*. Instead, what you likely want to do is change the model for certain users. This can be done using the EnableQueryAttribute's GetModel override.
For example, first create a method to generate your OData Model
public IEdmModel GetModel(bool includeCustomerOrders)
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
var customerType = builder.EntitySet<Customer>("Customers").EntityType;
if (!includeCustomerOrders)
{
customerType.Ignore(c => c.Orders);
}
builder.EntitySet<Order>("Orders");
builder.EntitySet<OrderDetail>("OrderDetails");
return build.GetModel();
}
... then in a class that inherits from EnableQueryAttribute, override GetModel:
public class SecureAccessAttribute : EnableQueryAttribute
{
public override IEdmModel GetModel(Type elementClrType, HttpRequestMessage request, HttpActionDescriptor actionDescriptor)
{
bool includeOrders = /* Check if user can access orders */;
return GetModel(includeOrders);
}
}
Note that this will create a bunch of the same models on multiple calls. Consider caching various versions of your IEdmModel to increase performance of each call.
You can put your own Queryable attribute on Customers.Get() or whichever method is used to access the Customers entity (either directly or through a navigation property). In the implementation of your attribute, you can override the ValidateQuery method to check the access rights, like this:
public class MyQueryableAttribute : QueryableAttribute
{
public override void ValidateQuery(HttpRequestMessage request,
ODataQueryOptions queryOptions)
{
if (!DoesCurrentUserHaveAccessToCustomers)
{
throw new ODataException("User cannot access Customer data");
}
base.ValidateQuery(request, queryOptions);
}
}
I don't know why your controller isn't called on navigation properties. It should be...

Categories

Resources