Part 3 - Protecting the Delayed Quotes Action and Extending the PIP
Having secured the live quote action, we now want to enforce the rule for access control to the delayed quotes. As stated in the Getting Started section, the rule states that everyone has full access but free subscriptions are limited to a number of daily requests defined by the product team - currently set to 50.
We will need a way of accessing the maximum number of requests and the current number of requests today. This is information that will be provided by the Policy Information Point (PIP). However, this is information that is custom to the AcmeCorp business so it will involve extending the PIP with a custom attribute value provider.
Before we get into accessing this new business data, let's look at how the authorization policy needs to be extended for the delayed quotes.
Extending the authorization policy
The first thing we need to do is to define the new attributes that will be used in the rule for the delayed quotes. We will put these in a custom category as they do not fit neatly on to any of the standard OASIS categories. Add the following definitions after the declaration of the SubscriptionLevel
attribute.
category quoteCat = 'urn:acmecorp-quotes'
attribute MaxQuotesPerDay
{
id = 'maxQuotes'
type = integer
category = quoteCat
}
attribute CurrentQuotesToday
{
id = 'currentQuotes'
type = integer
category = quoteCat
}
Here we can see the new category and then two integer attributes, MaxQuotesPerDay
and CurrentQuotesToday
. These names are for use within the ALFA; the id
and category
are used by the attribute value provider to render the business data as an attribute. This decoupling allows the ALFA to be as readable as possible, while keeping a standard definition of the attribute under the covers.
With our new attributes defined we can now add an additional rule, after the live one, to enforce the delayed quotes authorization.
rule DelayMustBeWithinLimits
{
target clause Action == 'delayed'
condition not CurrentQuotesToday < MaxQuotesPerDay
deny
}
Our new policy is defined so let's look at how we put that into action at runtime.
Extending the PIP
You extend the PIP using attribute value providers. There are a number supplied out-of-the-box with Enforcer (we are already using one to access claims in the request). However, in this case, the attributes we need are custom to AcmeCorp and so we need to create a custom attribute value provider. Fortunately, this is quite straightforward in Enforcer.
Create a folder called PIP
in the project to hold the classes you are about to define.
The first class to create is one that models the required attributes
public class RateLimits
{
private const string QuotesCategory = "urn:acmecorp-quotes";
private const string MaxRequestsId = "maxQuotes";
private const string CurrentRequestsId = "currentQuotes";
[PolicyAttributeValue(QuotesCategory, MaxRequestsId, Sensitivity = PolicyAttributeSensitivity.NonSensitive)]
public long? MaxRequestsPerDay { get; set; }
[PolicyAttributeValue(QuotesCategory, CurrentRequestsId, Sensitivity = PolicyAttributeSensitivity.NonSensitive)]
public long? CurrentRequestsPerDay { get; set; }
}
The [PolicyAttributeValue]
attributes allow us to specify how the id
(or name
) and category
of the attribute (as seen in the ALFA) map to properties of the class. The types for attributes should all be nullable in case the value is not available.
Now we create a class, RateLimitingAttributeValueProvider
, derived from RecordAttributeValueProvder<T>
where T
is the RateLimits
class. The resulting type has a core responsiblity to return an instance of the RateLimits
class when requested. Here is the implementation that you should add to your project.
public class RateLimitingAttributeValueProvider : RecordAttributeValueProvider<RateLimits>
{
private readonly PolicyAttribute subscriptionLevel = new PolicyAttribute("subscriptionLevel", PolicyValueType.String,
PolicyAttributeCategories.Subject);
private readonly PolicyAttribute currentRequestsFromQuery = new PolicyAttribute("currentRequests", PolicyValueType.String,
HttpRequestAttributeCategories.RequestQuery);
private readonly Dictionary<string, long> rateLimitMap = new Dictionary<string, long>
{
[SubscriptionLevels.Pro] = long.MaxValue,
[SubscriptionLevels.Standard] = long.MaxValue,
[SubscriptionLevels.Free] = 50,
};
protected override async Task<RateLimits> GetRecordValue(IAttributeResolver attributeResolver)
{
IReadOnlyCollection<string> subscriptionValues = await attributeResolver.Resolve<string>(subscriptionLevel);
if (subscriptionValues.Count == 0)
{
return new RateLimits();
}
IReadOnlyCollection<string> currentRequestValues = await attributeResolver.Resolve<string>(currentRequestsFromQuery);
long currentRequests = 0;
if (currentRequestValues.Count > 0)
{
if (long.TryParse(currentRequestValues.First(), out long parsedValue))
{
currentRequests = parsedValue;
}
}
long maxRequests = rateLimitMap[subscriptionValues.First()];
return new RateLimits
{
MaxRequestsPerDay = maxRequests,
CurrentRequestsPerDay = currentRequests
};
}
}
The key member here is the GetRecordValue
method which takes an attribute resolver. This attribute resolver allows the method to get the value of other attributes it needs to supply its own. In the case of RateLimitingAttributeValueProvider
we need the subscription level to work out the maximum number of requests per day.
The current number of requests would normally be retrieved from AcmeCorp infrastructure but for our testing purposes we are going to allow the caller to supply this value as a currentRequests
query parameter. This will require us to add an additional out-of-the-box attribute value provider to the PIP during startup.
Finally, we amend the startup to load the new attribute value providers into Enforcer. The last two lines of the following code snippet show the required configuration. The HttpRequestAttributeValueProvider
allows access to the query parameters of the request as attributes.
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>();
Protecting the delayed quotes action
As we did with the live quotes action, we protect the delayed quotes action using the [EnforcerAuthorization]
attribute on the GetDelayed
action method of the QuoteController
. This time we identify the Resourcetype
as "quote"
and the Action
as "delayed"
. This means the attributes will match the new rule's target clause
and so it will be evaluated.
[HttpGet]
[Route("{symbol}")]
[EnforcerAuthorization(ResourceType = "quote", Action = "delayed")]
public async Task<ObjectResult> GetDelayed(string symbol)
{
return Ok(await quoteService.GetDelayedPrice(symbol));
}
You can now run and test your code. Hitting the delayed quotes endpoint with any API Key will work. However, if you add a currentRequest=50
query parameter to simulate there have already been 50 request, using the API key 789
will result in a 403 as the new rule will issue a deny.
https://localhost:5001/quote/GOOG?apiKey=789¤tRequests=50