Applying data restrictions to route authorization
I introduced our concept of data restrictions in the context of filtering out entities from data access queries. I then had to clarify that data restrictions are not tied to data access – they are part of the domain logic. In this post I will demonstrate how we use those same data restrictions to protect web pages, using the same example of sensitive cases in Dovetail Support Center.
In our application, we have standard CRUD endpoints for each of our top level entities: New/Create/View. The view endpoint for a case is case/GUID. To properly implement the sensitive cases feature, we had to make sure that a user without the “View Sensitive Cases” would get an access denied error if they attempted to load the page for a sensitive case.
A side note about FubuMVC authorization
FubuMVC has a very powerful configuration model for declaring and decorating endpoints (routes/web pages). I’m not going to dive deep into the configuration, but you can read Jeremy Miller’s introduction. When configuring an endpoint, if you attach any authorization policies (IAuthorizationPolicy
), an authorization behavior will be added to the endpoint to execute your policies at runtime. When a request comes in to the endpoint, each policy gets a chance to vote on whether the request can continue. If the policies do not vote to Allow, the request is interrupted and an HTTP 403 is returned to the client.
RestrictedDataAuthorizationPolicy
We already have a class that defines whether a user can view a case – SensitiveCaseDataRestriction, which I introduced in my post about filtering at the data access level. In the context of a web request to view a Case, we have a single Case instance. If we could execute the data restrictions against the given Case, we could determine if the request should continue or return a 403. That is exactly what the RestrictedDataAuthorizationPolicy does:
public class RestrictedDataAuthorizationPolicy<T> : IAuthorizationPolicy where T : DomainEntity { private readonly IEnumerable<IDataRestriction<T>> _dataRestrictions; public RestrictedDataAuthorizationPolicy(IEnumerable<IDataRestriction<T>> dataRestrictions) { _dataRestrictions = dataRestrictions; } public AuthorizationRight RightsFor(IFubuRequest request) { var entityFilter = new SingleEntityFilter<T>(request.Get<T>()); _dataRestrictions.Each(entityFilter.ApplyRestriction); return entityFilter.CanView ? AuthorizationRight.None : AuthorizationRight.Deny; } }
It takes advantage of the fact that IDataRestrictions operate against the IDataSourceFilter<T>
interface. In the data access context, the IDataSourceFilter implementation would build a SQL WHERE clause. In this context, we already have an entity instance (the call to request.Get<T>()
in line 12 above), so we just need to see if it “passes” all of the rules. The SingleEntityFilter class is an implementation of IDataSourceFilter that keeps track of a single boolean value: CanView. It is set to true
initially, but successive calls to ApplyRestriction()
could set it to false
if the entity does not meet the requirements of an IDataRestriction. After all of the data restrictions are applied, the CanView property tells us if we need to deny access to the current web request.
Applying the policy
The final step is to wire it all up together. We need to attach the RestricedDataAuthorizationPolicy to the appropriate endpoints (the “View” routes for our entities). FubuMVC allows us to define a convention so that all of the appropriate endpoints have the policy applied automatically. The implementation details of a FubuMVC convention are beyond the scope of this post, but I’ll include the code to give you a hint at the possibilities. It isn’t the most succinct code, but it should give you an idea of how powerful it is to be able to query and modify the composition of your endpoints at startup time.
public class AuthorizeByDataRestrictionsConvention : IConfigurationAction { public void Configure(BehaviorGraph endpoints) { // "view" actions are declared on classes that implement CrudController interface // ex: CaseController : CrudController<Case> var viewEntityActions = endpoints.Behaviors.Select(x => x.FirstCall()) .Where(x => x.HandlerType.IsCrudController()) .Where(x => x.Method.Name == "View"); foreach (var action in viewEntityActions) { var entityType = action.HandlerType.FindInterfaceThatCloses(typeof(CrudController<>)).GetGenericArguments()[0]; var endpoint = action.ParentChain(); var policyType = typeof(RestrictedDataAuthorizationPolicy<>).MakeGenericType(entityType); endpoint.Authorization.AddPolicy(policyType); } } }