Implementing a Custom AttributeValueProvider
While the rules, policies and policy sets define the structure of an authorization policy, it is the attributes that drive the policy to various outcomes. In Enforcer, AttributeValueProvders are used to produce the values of attributes as the Policy Decision Point (PDP) requests them from the Policy Information Point (PIP).
Enforcer ships with a number of built-in AttributeValueProviders but it is likely you will want to create your own for your domain specific attributes.
What is an AttributeValueProvider?
In a technical sense, an AttributeValueProvider is any .NET type that extends the interface RSK.Enforcer.PIP.IAttributeValueProvider
. This interface is defined as followed:
public interface IAttributeValueProvider
{
ValueTask<AttributeValueResult<T>> GetValue<T>(PolicyAttribute attribute,
IAttributeResolver attributeResolver,
IEnforcerLogger evaluationLogger);
}
Through IAttributeValueProvider
, the PDP requests an attribute value and, if the attribute is not supported, the AttributeValueProvider returns an empty result (remember that attributes values are, in fact, bags of values and so an empty bag indicates the AttributeValueProvider does not support that attribute or has no values to provide in this context).
Custom AttributeValueProviders the Easy Way
There is a lot of low-level complexity when implementing an AttributeValueProvider. However, for most situations, this complexity has been taken care of for you. RecordAttributeValueProvider<TRecord>
is a base class that simplifies attribute resolution so you can concentrate on the source of the data rather than the underlying mechanics of AttributeValueProviders.
RecordAttributeValueProvider<TRecord>
The idea behind the record based AttributeValueProvider is that you create an instance of a type (TRecord
) that you populate with data and this data is automatically turned into attribute values. There are two patterns of use of the RecordAttributeValueProvider
: derivation and delegation. Which you use depends on level of complexity and how you want Enforcer to gain access to the attribute values.
Derive from RecordAttributeValueProvider<TRecord>
RecordAttributeValueProvider<TRecord>
is an abstract class. When you derive from it you must implement one method:
protected abstract Task<TRecord> GetRecordValue(IAttributeResolver attributeResolver);
The purpose of this method is to provide the populated record object that models the attribute values. But how are the record object’s properties translated into attribute values? This is the role of the PolicyAttributeValueAttribute
custom attribute. You annotate the properties of the TRecord
type with this custom attribute and RecordAttrbuteValueProvider
translates the properties into attribute values accordingly. Consider the following Document class:
public class Document
{
private int Id { get; set; }
public string Owner { get; set; }
public string[] AllowedDepartments { get; set; }
public Date? PublishDate { get; set; }
}
You can imagine authorization rules like the following:
- Only the
Owner
can update or delete the document. - People in allowed departments can read the document if the current date is greater than or equal to the
PublishDate
. - Documents with no
PublishDate
are only visible to theOwner
.
We, therefore, need the Owner
, AllowedDepartments
and PublishDate
exposed as attributes for our policy.
public class Document
{
private int Id { get; set; }
[PolicyAttributeValue(PolicyAttributeCategories.Resource, "owner")]
public string Owner { get; set; }
[PolicyAttributeValue(PolicyAttributeCategories.Resource, "allowedDepartment")]
public string[] AllowedDepartments { get; set; }
[PolicyAttributeValue(PolicyAttributeCategories.Resource, "publishedDate")]
public Date? PublishDate { get; set; }
}
You may recall that as well as having category and name/id, an attribute also has a type. The following table shows how .NET types are mapped to policy attribute types:
.Net Type | Attribute Type |
---|---|
string | String |
double | Double |
bool | Boolean |
Date | Date |
Time | Time |
DateTime | DateTime |
long | Integer |
int | Integer |
uint | Integer |
short | Integer |
ushort | Integer |
byte | Integer |
sbyte | Integer |
TimeSpan | Duration |
Policy attributes can have multiple values and so are modelled on bags. The bag can, therefore, contain 0, 1 or many values. To support this, the attribute properties in the record must either be nullable types (to support 0 or 1 value) or arrays/IEnumerable<T>
(to support 0, 1, or many values). Now we have our record type we can create the AttributeValueProvider to deliver its attributes.
public class DocumentAttributeValueProvider : RecordAttributeValueProvider<Document>
{
protected override Task<Document> GetRecordValue(IAttributeResolver attributeResolver)
{
// Code to retrieve Document
}
}
GetRecordValue
is an asynchronous method allowing you to, for example, go to a database to retrieve a specific document. However, to do this you will need the Id of the document. If we are checking access to a document then the document Id will be contained in the standard Resource
attribute for the request. To enable the attribute value provider to get the values of other attributes, it is passed an attributeResolver
.
protected override async Task<Document> GetRecordValue(IAttributeResolver attributeResolver)
{
IReadOnlyCollection<string> resourceValues =
await attributeResolver.Resolve<string>(OasisPolicyAttributes.Resource);
string resourceId = resourceValues.Single();
Document record = await documentService.GetDocument(resourceId);
return record;
}
One thing to note about GetRecordValue
is that it will not be called unless an attribute supported by the record is being requested. For any specific policy evaluation, it will be called at most once (the returned value is cached during the evaluation to ensure consistent values). What should you do if you cannot create the record? That depends on the domain you are modelling. There are two scenarios:
- It should be treated as an error that you cannot retrieve the record. In this case throw an exception or return
null
. - The attributes should just be treated as missing. In this case return an object with all properties set to
null
.
Deriving from RecordAttributeValueProvider<T>
is straightforward and allows you to put the AttributeValueProvider in a Dependency Injection (DI) container.
services.AddEnforcer("global", o =>
{
o.Licensee = "Rock Solid knowledge";
o.LicenseKey = "<license key>";
})
.AddAttributeValueProvider<DocumentAttributeValueProvider>();
What if you want to pass a custom AttributeValueProvider as the request context when you call evaluate on the Policy Enforcement Point? Creating a class for this purpose feels like overkill and this is what DelegatingRecordAttributeValueProvider<T>
is for.
Delegate to DelegatingRecordAttributeValueProvider<T>
DelegatingRecordAttributeValueProvider<T>
is very similar to RecordAttributeValueProvider<T>
but instead of deriving from it to provide the record, you pass a delegate into the constructor that provides the record. The delegate type is Func<IAttributeResolver, Task<T>>
such that the delegate is passed an IAttributeResolver
and must return the record as an asynchronous result.
Func<IAttributeResolver, Task<Document>> recordProvider = async iar =>
{
IReadOnlyCollection<string> resourceValues =
await iar.Resolve<string>(OasisPolicyAttributes.Resource);
string resourceId = resourceValues.Single();
Document record = await documentService.GetDocument(resourceId);
return record;
};
var requestContext = new DelegatingRecordAttributeValueProvider<Document>(recordProvider);
The record-based attribute value providers cater for all but a small minority of cases of writing a custom AttributeValueProvider. However, if the model they use (for example you do not want any caching of values even within a single policy evaluation) you can derive from the low level AttributeValueProvider base class. This is an advanced topic and is beyond the scope of this chapter.