A common use of Enforcer is to control access to websites and Web APIs. There are a number of AspNet Core specific components in Enforcer for invoking the evaluation of a policy and providing attributes for that evaluation.
Invoking the Authorization Engine
There are two ways to invoke the Enforcer authorization engine:
- Inject the PEP into a controller or service.
- Use the
[EnforcerAuthorization]
attribute on a controller action.
Injecting the PEP
The standard Enforcer configuration puts the Policy Enforcement Point (PEP) in the Dependency Injection (DI) container. This means if your controller or service takes a contructor parameter of type Rsk.Enforcer.PEP.IPolicyEnforcementPoint
then it will have access to the PEP, allowing it to trigger a policy evaluation at any point it sees fit. It is worth noting, however, that policy evaluation is non-trivial processing and so should not be done repeatedly throughout a particular code path.
Injecting the PEP Example
public class BookController : ControllerBase
{
IPolicyEnforcementPoint pep;
public BookController(IPolicyEnforcementPoint pep)
{
this.pep = pep;
}
[HttpGet]
[Route("books")]
public async Task<ActionResult> GetAll()
{
var requestContext = new DynamicAttributeValueProvider();
requestContext.AddString(OasisPolicyAttributes.ResourceType, "book")
.AddString(OasisPolicyAttributes.Action, "read");
PolicyEvaluationOutcome result = await pep.Evaluate(requestContext);
if(result.Outcome == PolicyOutcome.Deny) return Forbid();
// proceed with retrieving books
}
}
Using the [EnforcerAuthorization] Attribute
The easiest way to invoke the authorization engine is to apply the [EnforcerAuthorization]
attribute to a controller class or controller action.
- When applied to the controller class, it will automatically trigger a policy evaluation when any controller action on that class is invoked.
- When applied to a controller action, it will automtically trigger a policy evaluation when the individual controller action is invoked.
In the previous example we added the resource type and action attributes to the request context. How does this work with the [EnforcerAuthorization]
attribute? There are three properties you can set on the attribute that map to the three standard Oasis attributes: ResourceType, ResourceAction and Resource.
Note: The ResourceType, ResourceAction and Resource properties need to have their values set explicitly, they will not be inferred from the attribute usage.
Simple use of EnforceAuthorization Attribute Example
public class BookController : ControllerBase
{
[HttpGet]
[Route("books")]
[EnforcerAuthorization(ResourceType="book", Action="read")]
public async Task<ActionResult> GetAll()
{
// proceed with retrieving books
}
}
However, commonly the actual resource being used will be based on the Uri of the operation rather than common for all requests. In this case the resource would be a parameter of the action method. Fortunately you can use the [AuthorizationResource]
attribute on the parameter in question. In the following example, the book's id is identified as the resource for the request.
Identifying the Resource with [AuthorizationResource] Attribute Example
public class BookController : ControllerBase
{
[HttpDelete]
[Route("books/{id}")]
[EnforcerAuthorization(ResourceType="book", Action="delete")]
public async Task<ActionResult> Delete([AuthorizationResource] string id)
{
// proceed with deleting book
}
}
Often, a class, used to deserialize the body of a request, contains information required for authorization. For example, consider the following Book
class
public class Book
{
public string ISBN{ get; set; }
public string Title{ get; set; }
public int PublisherId{ get; set; }
public int double Price{ get; set; }
}
It may be that only users who manage a publisher should be able to add books for that publisher. Books priced over a certain level may need to trigger a review process to verify the pricing is correct. We may want to record the ISBN and title of a new book in an audit log. In this case we need all of these properties to become attributes that can be used in rules and obligations in our policy. Fortunately, you can annotate the class with [PolicyAttributeValue]
attributes and these will be automatically added to the policy engine by the Enforcer Asp.Net Core infrastructure. So the Book class now becomes:
public class Book
{
[PolicyAttributeValue(OasisAttributeCategories.Resource, "isbn")]
public string ISBN{ get; set; }
[PolicyAttributeValue(OasisAttributeCategories.Resource, "title")]
public string Title{ get; set; }
[PolicyAttributeValue(OasisAttributeCategories.Resource, "publisher")]
public int PublisherId{ get; set; }
[PolicyAttributeValue(OasisAttributeCategories.Resource, "price")]
public int double Price{ get; set; }
}
Using the [EnforcerAuthorization]
attribute provides a simple, but very powerful mechanism to invoke the authorization engine. However, the engine doesn't just issue a permit or deny. When the PEP has completed its evaluation, it may have pieces of advice that could not be resolved automtcally via registered Outcome Action Handlers. Also, you may want to take particular actions on a deny such as displaying a specific page to the user. Once the outcome is received, Enforcer will run an unresolved advice handler and, in the case of deny, a deny handler - this allows you to plumb in your own behavior.
Enforcer ships with a default deny handler that simply returns a 403, and a default unresolved advice handler that logs the advice. These can be added via the AddDefaultAdviceHandling
extension method on EnforcerBuilder
.
Both the unresolved advice handler and the deny handler must be in place for the [EnforcerAttribute]
pattern to work. If you add non-default handling for only one of these then you must also add the default handler for the missing one. The following two calls are the equivelent to calling AddDefaultAdviceHandling
:
services.AddEnforcer("global", o =>
{
o.Licensee = "<company name>";
o.LicenseKey = "<license key>";
})
.AddDefaultDenyHandling()
.AddUnresolvedAdviceHandler<DefaultUnresolvedAdviceHandler>();
For many applications simply returning a 403 is not enough: UI based applications may want to return a specific page and APIs send back data describing the authorization failure. To cater for this, Enforcer allows you to register an API deny hander and a view based deny handler in addition to the default. These deny handlers are registered at the applicaiton level so what determines which is used in any specic instance? The [EnforcerAuthorization]
has an optional PreferredHandlerType
property that states which handler should be used in any instance. However, as this property is optional there is a default behavior as detailed in the table below (* indicates any value).
View Deny Handler Registered | API Deny Handler Registered | PreferredHandlerType | Controller has ApiController attribute | Deny Handler Used |
---|---|---|---|---|
No | No | * | * | Default |
Yes | No | None | * | View |
No | Yes | None | * | API |
Yes | No | View | * | View |
Yes | No | None | * | View |
Yes | Yes | View | * | View |
Yes | No | API | * | Default |
No | Yes | API | * | API |
No | Yes | None | * | API |
Yes | Yes | API | * | API |
No | Yes | View | * | Default |
Yes | Yes | None | Yes | API |
Yes | Yes | None | No | API |
In general, the preferred deny handler is used unless it is unavailable whereupon the default is used. If there is ambiguity over which deny handler should be used then the API one selected.
You may register only one View and one API deny handler.
View deny handlers
A view deny handler is intended to return content where the controller action being executed returns a view (as opposed to being an API call). This allows the application to return a rendered page along with the 403 response code when a authorization request is denied.
The Razor deny handler
The Razor deny handler generates a 403 status code with content generated from a Razor template. There are two ways to configure this, depending on the level of control needed.
When needing to show a single deny page and handling a single type of advice, add the deny handler using the AddEnforcerAuthorizationRazorViewDenyHandler<T>(string)
extension method of EnforcerBuilder
. In this instance, the string argument is the path to the Razor template to use for the deny page. The T
is a type used in the model for the Razor template containing properties bound to the attributes set in the advice. The Razor template will be passed a List<T>
as its model to allow multiple pieces of advice to be sent to be delivered.
Example, given the following ALFA definitions:
namespace AcmeCorp.Example
{
category adviceCat = "Advice"
attribute AuthorizeFailureMessage
{
id = "AuthorizationFailureMessageId"
category = adviceCat
type = string
}
advice AuthorizationFailure = "acmecorp:AuthorizationFailure"
}
[EnforcerAdvice("acmecorp:AuthorizationFailure")]
class DenyModel
{
[PolicyAttribute("Advice", "AuthorizationFailureMessageId", MustBePresent = false)]
public string AuthorizationFailureMessage { get; set; }
}
// In Startup.cs...
services.AddEnforcer("global", o =>
{
o.Licensee = "<company name>";
o.LicenseKey = "<license key>";
})
.AddEnforcerAuthorizationRazorViewDenyHandler<EnforcerDenyModel>("~/Views/Shared/DenyView.cshtml")
Note that if the MustBePresent
property is set for any property, Enforcer will make a best effort to honour this. If the Advice is invoked from ALFA code and the attribute value is not present, Enforcer will skip the advice and continue generating the deny page. Warnings detailing the missing attributes will be written to the diagnostic logs.
In cases where the above example does not provide enough control, for example, there may be multiple deny pages for different reasons or different types of Advice may need to be catered for, Enforcer has an alternative offering complete control.
The alternative extension method, AddEnforcerAuthorizationRazorViewDenyHandler<T>()
, can be used to inspect the PolicyEvaluationOutcome
and specify a view template and a model. In this instance, T
is a type that derives from EnforcerAuthorizationDenyHandler
.
Example:
class DenyModel
{
public string AuthorizationFailureMessage { get; set; }
}
class AcmeCorpDenyHandler : EnforcerAuthorizationDenyHandler
{
public override DenyViewResult GetDenyViewResult(PolicyEvaluationOutcome outcome)
{
// Advice and attribute values are available through the outcome argument
return new DenyViewResult(
"~/DenyView.cshtml",
new DenyModel {
AuthorizationFailureMessage = "You shall not pass!"
}
);
}
}
// In Startup.cs...
services.AddEnforcer("global", o =>
{
o.Licensee = "<company name>";
o.LicenseKey = "<license key>";
})
.AddEnforcerAuthorizationRazorViewDenyHandler<AcmeCorpDenyHandler>()
Low level view deny handler
If you want complete control over view rendering you can derive a class from EnforcerAuthorizationViewDenyHandler
. You need to supply an implementation of IDenyPageRenderer
which will be responsible for rendering the content into the HttpContext
given a view path and model (there is an implementation for Razor called the RazorDenyPageRenderer
). Your deny handler implements the abstract method GetDenyViewResult
which is passed the authorization outcome and returns a DenyViewResult
that encapsualtes the view path and the model.
API deny handlers
Typically, for an API, in the case of a forbidden status code (403) you will want to provide either no content (in that case the default deny handler will work) or some kind of content to accompany the status code.
The Json API Deny handler
The easiest content based API deny handler to use is a supplied one that returns Json. JsonEnforcerAuthorizationApiDenyHandler<T>
takes a type T
that is annotated by the [EnforcerAdvice]
attribute and uses this type to extract information from returned advice to populate a Json response. The Json API deny handler is added using the AddParameterizedJsonApiDenyHandler
extendion method of EnforcerBuilder
Example, given the following ALFA definitions:
namespace AcmeCorp.Example
{
category adviceCat = "Advice"
attribute AuthorizeFailureMessage
{
id = "AuthorizationFailureMessageId"
category = adviceCat
type = string
}
advice AuthorizationFailure = "acmecorp:AuthorizationFailure"
}
[EnforcerAdvice("acmecorp:AuthorizationFailure")]
class DenyModel
{
[PolicyAttribute("Advice", "AuthorizationFailureMessageId", MustBePresent = false)]
public string AuthorizationFailureMessage { get; set; }
}
// In Startup.cs...
services.AddEnforcer("global", o =>
{
o.Licensee = "<company name>";
o.LicenseKey = "<license key>";
})
.AddParameterizedJsonApiDenyHandler<EnforcerDenyModel>(o => {})
Note that if the MustBePresent
property is set for any property, Enforcer will make a best effort to honour this. If the Advice is invoked from ALFA code and the attribute value is not present, Enforcer will skip the advice and continue generating the deny page. Warnings detailing the missing attributes will be written to the diagnostic logs.
To allow a greater degree of control over the creation of the JSON, you can derive a class from JsonEnforcerAuthorizationApiDenyHandler
. Here you are responsible for generating ther JSON from the authorization outcome by implementing the abstract method GetJson
. You add a deny handler based on JsonEnforcerAuthorizationApiDenyHandler
by using the AddJsonApiDenyHandler
extension method of EnforcerBuilder
.
Low level API deny handlers
If you want full control over the generation of a response for an API you can derive a class from EnforcerAuthorizationApiDenyHandler
. You need to implement the asynchronous abstract method GetResponseContent
which gets passed the evaluation outcome and returns a Stream
containing the content,
Accessing Information about the Subject
So far we have seen how to identify what the caller is attempting to do; the other part of the picture is who is making the request. This information, in Asp.Net Core is typically taken from the claims in the access token of the authenticated user. Enforcer ships with an attribute value provider for claims. These claims are all assumed to be in the OasisAttributeCategories.Subject
category and the id of the attribtue is the claim name(e.g. email
, sub
or role
). You add the claims attribute value provider using the AddClaimsAttributeValueProvder
extension method on EnforcerBuilder
.
Adding the ClaimsAttributeValueProvider Example
services.AddEnforcer("global", o =>
{
o.Licensee = "<company name>";
o.LicenseKey = "<license key>";
})
.AddClaimsAttributeValueProvider();
Adding attributes dynamically
The DynamicAttributeValueProvider
is a general purpose component allowing code to add attributes it wants dynamically enabling them to be resolved during policy evaluation. In ASP.Net this is particularly useful when there is middleware that needs to add attributes to the request context. Another use may be in developing automated tests (see Unit Testing Policies). It is added by using the AddDynamicAttributeValueProvider
extension method of EnforcerBuilder
.
Adding the Dynamic Attribute Value Provider Example
services.AddEnforcer("global", o =>
{
o.Licensee = "Rock Solid knowledge";
o.LicenseKey = "<license key>";
})
.AddDynamicAttributeValueProvider();
Accessing Information about the HTTP Request
Enforcer has an AttributeValueProvider which can supply information about the incoming HTTP request. To include this AttributeValueProvider, call the AddHttpRequestAttributeValueProvider
extension method during the configuration at start-up.
Adding the HttpRequestAttributeValueProvider Example
services.AddEnforcer("global", o =>
{
o.Licensee = "<company name>";
o.LicenseKey = "<license key>";
})
.AddHttpRequestAttributeValueProvider();
Pre-defined HTTP attributes
The HttpRequestAttributeValueProvider
provides three categories of attributes.
- Request attributes, general attributes about the http request.
- Request header attributes, attributes for specific header values.
- Request query attributes, attributes for specific query string parameters.
Request Attributes
Built-in attributes exist for the Request Attributes category, available in both ALFA and .Net, as shown in the following table.
Attribute | ALFA identifier | Type | Description |
---|---|---|---|
Verb | Enforcer.Attributes.Http.Verb | String | The verb of the request, eg GET, POST, etc |
URL | Enforcer.Attributes.Http.Url | String | The full URL of the request |
Scheme | Enforcer.Attributes.Http.Scheme | String | The scheme, eg. https in https://identityserver.com/Enforcer |
Host | Enforcer.Attributes.Http.Host | String | The host, eg. identityserver.com in https://identityserver.com/Enforcer |
Path | Enforcer.Attributes.Http.Path | String | The request path, eg. Enforcer in in https://identityserver.com/Enforcer |
Query | Enforcer.Attributes.Http.Query | String | The full query string for the request |
Port | Enforcer.Attributes.Http.Port | Integer | The port used for the request |
URL Encoding in the URL, Path or Query will be preserved.
Header and Query Parameter Attributes
Request header attributes and query attributes need to be defined in ALFA before they can be used. The attribute ID/name is used to identify the header or query parameter. Note that query string parameters will have any URL encoding reversed..
The categories are defined as follows:
category httpHeaderCat = "urn:rsk:names:enforcer:1.1:attribute-catergory:http-header"
category httpQueryParameterCat = "urn:rsk:names:enforcer:1.1:attribute-catergory:http-request-query"
Examples
The following snippet defines an attribute which selects the Content-Type
header from a http request.
import Enforcer.Attributes.Http
attribute ContentType
{
id = "Content-Type"
type = string
category = httpHeaderCat
}
The following snippet defines an attribute which selects the value of the query string parameter product
from the request https://shop.identityserver.com?product=Enforcer
.
attribute Product
{
id = "product"
type = string
category = httpQueryParameterCat
}