A library that allows using policy based rules for authorization in asp.net core.
Web api requests can be evaluated by policy in two ways: by principal and http method (GET, POST, etc) or by principal and resource type.
[ Authorization Middleware ] [ Endpoint Middleware ]
__________ ____________________________ ______________ __________________________
| | | Authorization Policy | | Controller | | Resource Authorization |
| Client | Request -> | Inspect: Identity/Method | -> | | -> | |
| | Response <- | | <- | | <- | Inspect objects |
‾‾‾‾‾‾‾‾‾‾ ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ ‾‾‾‾‾‾‾‾‾‾‾‾‾‾ ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
[ Request Policies ] [ Resource Policies ]
Policies are evaluated at runtime as predicates created from expression trees. If that's new to you, think of it as a Linq query that's generated at runtime. These predicates are generated by the McRule libary, see that package's Readme for information on the supported pattern matching grammar.
The general idea is that these predicates inspect a property on an object and return true or false. RequstPolicy and ResourcePolicy are the two general types and contain a List that encodes the rules.
ResourcePolicies include a TargetType property to determine scope. Requirements are usually a list of PropertyRequirements, as they evaluate against the properties of a model object.
If the simple language in this readme makes seem like a bot wrote this, that's not it. Usage of the library was intended to be simple, so the parts a developer or operator touches are simple.
McAttributes is the example app used while developing this library. It comes from something else I needed for work but is just a simple web api + razor pages app with a SQLite database. Test data is included.
Usage of the library assumes your application implementes IRuleProvider. There's a naive example in Program.cs of the example app, but you'll want to implement your own to load policies as data. You'll also want to examine service setup and middleware usings. Look to SayController.cs and SayKVController.cs for examples of ResourcePolicy usage.
These include a Route and Action property, which is used to determine whether it should be included for evaluation at authorization time. Requirement types for these policies generaly includes ClaimRequirement and RoleRequirements, both of these evaluate against the ClaimsPrincipal. RequestPolicy failure short circuits the pipeline and returns a 401 Unauthorized response.
Resource policies evaluate at the middleware endpoint as part of the controller's processing. These policies are selected by inferring tyoe of the model being processed by a controller. The idea was inspired by the Resource Based Authorization articles that became part of Andrew Lock's book ASP.NET Core In Action. It's great, go buy it.
These types are intended to be used as predicates in a query for a given record type. This allows for defining policies that restrict a subjects view of a database by appending a predicate to the user provided query.
The business use case I have is for providing HR information to multiple busines organizations. Each org has their own IT staff that automate processes with this data, some IT units are shared by mutiple organizations, others are not. The overall view of the data itself should be limited based on who's asking.
This is what a couple policies might look like for a shared services IT administrators accessing personell records:
var policySet = new [] {
new FilterPolicy
{
Name = "SuperGeek IT.Admin users in the SharedServices org can access Customer A records.",
TargetType = typeof(PersonellRecord).Name,
Requirements = new List<Requirement> {
new RoleRequirement("IT.Admin"),
new ClaimRequirement("Agency", "SuperGeek"),
new ClaimRequirement("Organization", "SharedServices"),
new PropertyRequirement("Agency", "~Customer A")
};
}
, new FilterPolicy {
Name = "SuperGeek IT.Admin users in the SharedServices org can access Customer B records.",
TargetType = typeof(PersonellRecord).Name,
Requirements = new List<Requirement> {
new RoleRequirement("IT.Admin"),
new ClaimRequirement("Agency", "SuperGeek"),
new ClaimRequirement("Organization", "SharedServices"),
new PropertyRequirement("Agency", "~Customer B")
};
}
};The Requirements stated include role, claim and property requirements. Role and claim requirements are matched against the principal making a request, property requirements are what become the predicate. Requirements inside a single policy are joined with an AND operator, while policies are joined with an OR. The conjoined policy set would end up looking something like:
x => (
((x.Agency != null)
AndAlso x.Agency.Equals("Customer A", CurrentCulture))
OrElse Invoke(x =>
((x.Agency != null)
AndAlso x.Agency.Equals("Customer B", CurrentCulture)), x)
)This is the generated expression tree passed to a ToString(). It's not quite how you'd write it by hand,
'x' for the parameter in multiple nested closures is a bit odd, but it works. The policy iteself generates
an expression tree which is assembled recursively and using the same parameter name simplifies that. An
important point about the expression is that it can be handed to Linq and EF will generate complementary
SQL. Simply return return db.Set.Where<User>(filterExpression) from your controller and EF takes it from there. For
bonus points, if you're using OData with asp.net core, the user's query is applied after returning from
the controller so there's no additional work on your part.
Note: Case insensitive string matching for SQL generated queries depends on your backend database, out of the box. PostgreSQL for instance, is case sensitive by default. In order for case-insensitive matches to generate propery with the underlying McRule library, an ExpressionGenerator class must be implemented and used when generating the expression, example here. There isn't yet a way to inject those with this authorization library but it's on my TODO list.