Part 4 - Returning Content on a Deny
So far we have simply returned a 403 when a request is denied. However, it can be a good idea to return some content, in addition to the status code, explaining the reason for the authorization failure.
When you use the [EnforcerAuthorization]
attribute it uses a deny handler, to generate the response, in the event of the policy evaluating to a deny
. The 403 we are currently seeing is generated by the default deny handler. You can plug in another deny handler that will be used for API based denies. These deny handlers typically look for pieces of advice, generated by the policy evaluation, that they can map to a response. Let's amend the policy to generate advice and then look at how we can turn this into a response message for the caller.
Adding advice to the policy
In ALFA, advice needs to be declared before it is used. This is another example of how we can have a name used in the ALFA, that makes the ALFA readable, but maps on to a name known by the authorization infrastructure that can then take action when it is generated. We are going to pass data along with the advice, in the form of an attribute, so we will add another attribute to our ALFA to represent this. Add the following statements to your policy after the existing attribute declarations.
attribute AccessDeniedReason
{
id = 'deniedReason'
type = string
category = quoteCat
}
advice AccessDenied = 'accessDeniedAdvice'
Now we can issue this advice in the rules. You may have been wondering why we made our rules issue denies rather than permits. It is so we can attach advice when a deny is issued. Only advice that is issued via an on deny
ALFA statement gets propagated when a deny occurs.
For the live quotes the access denied message is simply that the caller requires a pro subscription. Let's see what that looks like.
rule LiveMustBePro
{
target clause Resource == 'live'
condition not SubscriptionLevel == 'pro'
deny
on deny
{
advice AccessDenied
{
AccessDeniedReason = 'Live quotes are only available with a pro subscription'
}
}
}
For the delayed quotes it would be useful to tell the user the limit they have breached. For this we need to include the MaxQuotesPerDay
attribute in the message. The problem is that MaxQuotesdPerDay
is an integer and we need to add it to a string. Fortunately, both OASIS and Enforcer itself define functions that you can use in the your ALFA for type conversion (and much more). The most useful function in this situation is the Enforcer supplied ToString
function. We need to bring the Enforcer functions in to scope with an import
statement. Put the following just before the definition of the ReadQuotes
policy.
import Enforcer.Functions.*
How we can use the ToString
function to build the access denied message using the MaxQuotesPerDay
attribute. Modify the DelayMustBeWithinLimits
rule as follows.
rule DelayMustBeWithinLimits
{
target clause Resource == 'delay'
condition not CurrentQuotesToday < MaxQuotesPerDay
deny
on deny
{
advice AccessDenied
{
AccessDeniedReason = 'free subscriptions are limited to ' + ToString(MaxQuotesPerDay) + ' quotes per day'
}
}
}
Our policy is now emitting advice on deny. The next step is to add more sophistcated deny handling to render the associated attributes into the response.
Adding a JSON deny handler
Enforcer ships with a deny handler that is designed to be used with APIs: JsonEnforcerAuthorizationApiDenyHandler<T>
. The T
in this case is a class that defines the mapping from a type of named advice into an object containing the data from the associated attributes. Here is a class for the access denied advice. Add this to your models folder.
[EnforcerAdvice("accessDeniedAdvice")]
public class DenyReason
{
private const string QuotesCategory = "urn:acmecorp-quotes";
[PolicyAttribute(QuotesCategory, "deniedReason")]
public string Reason { get; set; }
}
This class uses the [EnforcerAdvice]
attribute to identify the advice it is used to process (this is the name that the ALFA definition of the advice points to). It then specifies, against each property, how advice attribute data is assigned to properties. As there may be more than one piece of matching advice, each piece is mapped to a separate object and then an array of these objects is serialized into the response as JSON.
To enable the new API deny handling we simply add a line to the startup configuration by calling AddParameterizedJsonApiDenyHandler<DenyReason>
as follows:
services.AddEnforcer("AcmeQuotes.ReadQuotes", o =>
{
o.Licensee = Constants.Licensee;
o.LicenseKey = Constants.LicenseKey;
})
.AddEmbeddedPolicyStore($"{Assembly.GetExecutingAssembly().GetName().Name}.Policy")
.AddPolicyEnforcementPoint(o => o.Bias = PepBias.Deny)
.AddDefaultAdviceHandling()
.AddClaimsAttributeValueProvider(o => { })
.AddHttpRequestAttributeValueProvider()
.AddPolicyAttributeProvider<RateLimitingAttributeValueProvider>()
.AddParameterizedJsonApiDenyHandler<DenyReason>(o => { });
You can now run and test the API. The following URLs will both issue denies for different reasons which you should see in the response.
https://localhost:5001/quote/GOOG/live?apiKey=456
https://localhost:5001/quote/GOOG/apiKey=789?currentRequests=50
Conclusion
You have seen how we can use ALFA to define a human readable authorization policy, which takes us beyond simple roles into the realm of fine-grained permissions. You have also seen how Enforcer can, through the use of attributes and configuration, bring business data into the authorization process. Finally, you have seen that the mechanics of extending Enforcer are straightforward, with the supplied base classes and extensibility points. For more information about Enforcer, see the product documentation.