This library uses the permissions defined in the capability annotations of the OData model to apply authorization policies
to an OData service based on Microsoft.AspNetCore.OData
.
In your Startup.cs
file:
using Microsoft.AspNetCore.OData.Authorization
public void ConfigureServices(IServiceCollection services)
{
// odata authorization services
services.AddOData()
.AddODataAuthorization(options => {
// you need to register an authentication scheme/handler
// This works similar to services.AddAuthentication
options.ConfigureAuthentication("DefaultAuthScheme").AddScheme(/* ... */)
});
service.AddRouting();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
// OData register authorization middleware
app.UseOdataAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapODataRoute("odata", "odata", GetEdmModel());
});
}
- ODataAuthorizationSample: Simple API with permission restrictions and OData authorization middleware set up with a custom authentication handler
- CookieAuthenticationSample: Basic API with permissions restrictions and a cookie-based authentication handler
By default, the library will try extract permissions from the
authenticated user's claims. Specifically, it will look for
claims with the key Scope
. If your app is storing user scopes differently (e.g. using a different key), you can provider a scope finder delegate that returns a list of scopes from the current user:
services.AddODataAuthorization(options => {
options.ScopeFinder = (context) => {
var scopesClaim = context.User?.FindFirst("Permissions");
return Task.FromResult(scopes.Value.Split(" ").AsEnumerable());
};
options.ConfigureAuthentication().AddJWTAuthenticationScheme();
})
For a complete working example, check the sample application.
On each request, the library extracts from the model the permissions restrictions that should apply to the route being accessed and creates an authorization policy based on those permissions. Deeper down the request pipeline, the AspNetCore filter-based authorization system will call the OData authorization handler to verify whether the current user's permissions match the ones required by the policy.
Note: If there are not permission restrictions defined for an some target (entity set/singleton/operation) in the model, then endpoints to that target will be authorized by default regardless of the user's permissions.
For the following examples, let's assume that we are working with an OData model that has the following scopes defined:
Permission scope name | Where it's defined |
---|---|
Customers.Read |
ReadRestrictions of Customers entity set |
Customers.ReadByKey |
ReadByKeyRestrictions of Customers |
Customers.Insert |
InsertRestrictions of Customers |
Customers.Delete |
DeleteRestrictions of Customers |
Customers.Update |
UpdateRestrictions of Customers |
CustomerOrders.Read |
ReadRestrictions of NavigationRestrictions of Customers on Orders property |
CustomerOrders.ReadByKey |
ReadByKeyRestriction of NavigationRestrictions of Customers on Orders property |
CustomerOrders.Insert |
InsertRestrictions of NavigationRestrictions of Customers on Orders property |
CustomerOrders.Update |
UpdateRestrictions of NavigatonRestrictions of Customers on Orders property |
CustomerOrders.Delete |
UpdateRestrictions of NavigationRestrictions of Customers on Orders property |
Orders.Read |
ReadRestrictions of Orders entity set |
Orders.ReadByKey |
ReadByKeyRestrictions of Orders |
Orders.Update |
UpdateRestrictions of Orders |
Orders.Delete |
DeleteRestrictions of Orders |
Orders.Insert |
InsertRestrictions of Orders |
Order.CalculateTax |
OperationRestrictions of CalculateTax bound function |
UpdateTaxRate |
OperationRestrictions of UpdateTaxRate unbound action |
TopProduct.Read |
ReadRestrictions of TopProduct singleton |
For CRUD operations on entity sets and singleton, the permissions of the corresponding insert/update/delete/read restrictions are applied.
Endpoint | Required permission scopes |
---|---|
GET Customers |
Customers.Read |
GET Customers(1) |
Customers.Read OR Customers.ReadByKey` |
DELETE Customers/1 |
Customers.Delete |
POST Customers |
Customers.Insert |
PUT Customers |
Customers.Update |
PATCH Customers |
Customers.Update |
Note, in the case of Customers(1)
, permissions will be extracted from two places if available. Permissions will be extracted from both ReadRestrictions
as well as the ReadByKeyRestrictions
property of the ReadRestrictions
. If the user has any of the permissions defined in either the ReadRestrictions
or
ReadByKeyRestrictions
, then access will be granted.
For example, if the model defines permission scopes Customers.Read
in the ReadRestrictions
, and Customers.ReadByKey
in the ReadByKeyRestrictions
, then access to the GET Customers(1)
endpoint will be granted to uers with either the Customers.Read
or Customers.ReadByKey
permissions.
The OperationRestricitons
of the function or action are applied. For function and action imports, the OperationRestrictions
of the underlying function/action are applied.
Endpoint | Required permission scopes |
---|---|
GET Orders(1)/CalculateTax |
Order.CalculateTax |
POST UpdateTaxRate |
UpdateTaxRate |
Note: If functions are overloaded, the operation restrictions of the specific overload being called will apply.
The ReadRestrictions
or UpdateRestrictions
of the entity or singleton whose property are being accessed are applied.
Endpoint | Restrictions applied |
---|---|
GET Customers(1)/Address/City |
Customers.Read OR Customers.ReadByKey |
GET TopProduct/Price |
TopProduct.Read |
DELETE or PUT or POST Customers(1)/Email |
Customers.Update |
These apply the ReadRestrictions
and UpdateRestrictions
of the entity/singleton that contains the navigation property where the link is read/added/removed/modified.
Endpoint | Restrictions applied |
---|---|
GET Customers(1)/Orders/$ref |
Customers.Read OR Customers.ReadByKey |
GET TopCustomer/Orders/$ref |
TopProduct.Read |
DELETE or PUT or POST Customers(1)/Orders/$ref |
Customers.Update |
If the endpoint accesses a navigation properties and nested paths in general, the authorization middleware will check whether the user has permissions to access each segment of the path.
Given the endpoint GET Customers(1)/Orders
, the middleware will check whether the user
has read access to Customers(1) and then read access to Orders. The permissions that are checked for
reading Customers(1) are extracted from the ReadRestrictions
(including ReadByKeyRestrictions
) of
the Customers
entity set. The permissions checked for Orders are extracted from both the ReadRestrictions
of Orders
and the ReadRestrictions
of the NavigationRestrictions
of Customers
that apply to the Orders
property (NS.EntityContainer.Customers/{key}/Orders
path).
Assuming the model defines the scopes Customers.Read
, Customers.ReadByKey
, CustomersOrders.Read
and Orders.Read
,
the required scopes to read the endpoint would be:
(Customers.Read OR Customers.ReadByKey) AND (CustomerOrders.Read OR Orders.Read)
Endpoint | Restrictions applied |
---|---|
GET Customers(1)/Orders |
(Customers.Read OR Customers.ReadByKey) AND (CustomerOrders.Read OR Orders.Read)) |
GET Customers(1)/Orders(1)/Price |
(Customers.Read OR Customers.ReadByKey) AND (CustomerOrders.Read OR CustomerOrders.ReadByKey OR Orders.Read OR Orders.ReadByKey) |
DELETE Customers(1)/Orders(1) |
(Customers.Update) AND (CustomerOrders.Delete OR Orders.Delete) |
PUT Customers(1)/Orders(1) |
(Customers.Update) AND (CustomerOrders.Update or Orders.Update) |
POST Customers(1)/Orders |
(Customers.Update) AND (CustomerOrders.Insert or Orders.Insert) |
GET Customers(1)/Orders(2)/Product |
(Customers.Read OR Customers.ReadByKey) AND (CustomerOrders.Read OR CustomerOrders.ReadByKey OR Orders.Read OR Orders.ReadByKey) AND (OrderProduct.Read OR OrderProduct.ReadByKey OR Products.Read) |
Note that a POST, PUT, PATCH or DELETE access to a navigation property is considered an update access to the entity that the navigation property belongs to.
- Only supports AspNetCore APIs using endpoing routing, i.e. AspNetCore 3.1
- Does not support
RestrictedProperties
- Permissions are extracted from the model on each request, no caching is performed. It's not clear whether it's guaranteed that the model will not change after startup.