In addition to providing permit/deny responses to requests, policies can use obligations to protect data, both in terms of masking specific columns and/or filtering entire entries. Obligation handlers are used to communicate the masking/filtering requirements from the policy back to the client application.
Example, a sales representative may access all product sales, but not see the credit card numbers used in making the sale. The policy will be configured to allow the operation of viewing sales but with an obligation for the client to mask out any credit card numbers.
namespace AcmeCorp.Global {
import Oasis.Functions.*
import Oasis.Attributes.*
import Enforcer.DataMasking
policy salesLedger
{
apply denyUnlessPermit
rule viewSales
{
target clause Action == "View" and ResourceType == "sales"
condition Subject.Role == "sales"
permit
on permit
{
obligation DataMasking
{
CategoriesToMask = "CreditCardDetails"
}
}
}
}
}
Masking intent is defined in the policy using the DataMasking
obligation. The obligation denotes the categories of data to mask. For example a category of PII may contain data such as first name,last name, email address etc. Enforcer provides a built in masking obligation handler for the DataMasking
obligation.
If you wish to mask multiple categories, use the bagOf
function.
obligation DataMasking
{
CategoriesToMask = bagOf( "CreditCardDetails" , "PII" )
}
DataMasking with controllers
Controllers using the EnforcerAuthorizeAttribute
for authorization and returning one of the following IActionResult
:
ViewResult
PartialViewResult
ObjectResult
JsonResult
ViewComponentResult
can take advantage of data masking.
For example
[EnforcerAuthorization]
public IActionResult Index()
{
return View(new ResponseViewModel()
{
. . .
}
}
The DataMasking
obligation in the policy indicates the categories of data that should be masked, the binding of the category to mask and the data returned from the controller is achieved by decorating properties on the model (ResponseViewModel
) with masking attributes. Enforcer ships with the following attributes out of the box
DefaultValueMaskingCategoryAttribute
ConstantValueMaskingCategoryAttribute
For example the attributes could be applied as follows
public class ResponseViewModel
{
[ConstantValueMaskingCategory("secret",MaskedValue = "********")]
public string Message { get; set; }
[DefaultValueMaskingCategory("identity")]
public string From { get; set; }
public string To { get; set; }
}
When returning a ResponseViewModel
with an obligation configured to mask any data categorized as secret, Enforcer will set the Message property to "**", prior to being sent the view.
DefaultValueMaskingCategoryAttribute
When data masking is performed, the property's value is replaced with the default value for the property type.
The DefaultValueMaskingCategoryAttribute
can also be applied at the class level, this will ensure that all settable properties in the object will be masked appropriatly. This can be combined with the IgnoreForMaskingAttribute
applied to properties to remove the masking of a property.
ConstantValueMaskingCategory
When data masking is performed, the property's value is replaced by the MaskedValue
defined in the attribute.
[ConstantValueMaskingCategory("secret",MaskedValue = "********")]
A web example of masking can be found on GitHub
Masking when invoking the PEP directly
Masking can be applied when invoking the PEP Evaluate
method directly. An instance of the MaskingOutcomeActionHandler
is passed to the Evaluate
method . Any masking obligations encountered when executing the policy will configure the masking
outcome action handler with the categories to mask.
On receipt of a successful policy outcome the masking
outcome action handler can be invoked to apply the required masking to any object.
public void SendNotification(IPolicyEnforcementPoint pep , string message)
{
var masking = new MaskingOutcomeActionHandler();
PolicyEvaluationOutcome canSend = await pep.Evaluate(
context,new OutcomeActionHandler[] {masking});
if (canSend.Outcome == PolicyOutcome.Permit)
{
var resultDataToMask = new Response()
{
Message = "Top secret",
From = "andy@microsoft.com",
To = "Sally@acme.com"
};
masking.ApplyMask(resultDataToMask);
// Send masked Message
}
}
Creating your own custom masking
Data masking is not constrained to the two built in categories. Custom masking behavior can be created by creating your own MaskingCategoryAttribute
.
- Create a new class that derives from
MaskingCategoryAttribute
. - Override the
GetMaskedValue
method, to implement the required masking behavior. The parametervalue
represents the value to be masked, and the method returns the masked value.
An example email mask is shown below
public class EmailMaskingCategoryAttribute : MaskingCategoryAttribute
{
public EmailMaskingCategoryAttribute(
string maskingCategory,
params string[] additionalMaskingCategories)
: base(maskingCategory, additionalMaskingCategories)
{ }
public override object GetMaskedValue(object value)
{
if (value is string emailAddress)
{
string regex = @"[\w-\._\+%]{2}@[\w-\._\+%]{2}";
return Regex.Replace(emailAddress, regex, "**@****");
}
else
{
throw new ArgumentException("Email masking can only be applied to strings");
}
}
}
Apply the new attribute to any property that has an email address that could be masked.
Filtering data
Authorization rules may be required to allow actions against a restricted set of rows from a data source. Consider a set of rows in a database, defining a policy that needs to execute for each row in the database is not practical. An alternative approach is for the policy to allow, in principle, access to the given source, but with an obligation for the requester to perform additional filtering defined by the policy.
For example a sales representative may access product sales for their region but no other.The policy will be configured to allow the operation of viewing product sales but with an obligation for the client to filter the results based on region.
The policy outlined below allows sales staff to view the sales report, with the obligation to filter by region.
namespace Subject
{
attribute Region { id="region" type=string category=subjectCat }
}
namespace Filters
{
attribute Region { id="region" type=string category=resourceCat }
obligation SalesByRegion = "filterSalesByRegion"
}
namespace Policies
{
import Oasis.Attributes
import Oasis.Functions
policy salesReport
{
target clause Action == "view" and Resource=="salesReport"
apply denyUnlessPermit
rule forSalesReps
{
permit
condition Subject.Role == "sales"
// Sales reps can see sales but only for their region
on permit
{
obligation Filters.SalesByRegion
{
Filters.Region = Subject.Region
}
}
}
}
}
The client code invoking the policy constructs an OutcomeActionHandler
to handle the obligation of filtering product sales. Failure of the client code to provide such an OutcomeActionHandler
results in the policy producing a deny result.
public class FilterArguments
{
[PolicyAttribute(PolicyAttributeCategories.Resource, "region")]
public string Region { get; set; }
}
public class FilterSalesByRegion : OutcomeActionHandler<FilterArguments>
{
private Func<SaleEntry, bool> filter = _ => true;
protected override Task Execute(FilterArguments parameters, IEnforcerLogger evaluationLogger)
{
if (parameters.Region != null)
{
filter = se => se.Location == parameters.Region;
}
return Task.CompletedTask;
}
public override string Name => "filterSalesByRegion";
public IEnumerable<SaleEntry> ApplyFilter(IEnumerable<SaleEntry> query)
{
return query.Where(filter);
}
}
With the obligation handler built, the client code invoking the PEP's Evaluate
method supplies an instance of the obligation handler, and on receiving a permit from the policy requests the obligation hanlder to modify the query to generate the sales report.
var salesFilter = new FilterSalesByRegion();
PolicyEvaluationOutcome reportAccess =
await pep.Evaluate(context, new OutcomeActionHandler[] { salesFilter });
if (reportAccess.Outcome == PolicyOutcome.Permit)
{
// Request that the obligation handler updates the query
IEnumerable<SaleEntry> salesReport = salesFilter.ApplyFilter(sales);
foreach (var sale in salesReport)
{
Console.WriteLine(sale);
}
}
else
{
Console.WriteLine("Denied access to sales report");
}
See the following GitHub project for the complete solution.