Implementing Structured Attributes
Modifying the policy
Structured Attributes are defined in the same way as regular attributes, with one exception: the type
property is not one of the standard data-types, it will be a type you need to declare in the ALFA yourself.
We're going to define a type called Permission
with two string properties, Resource
which will identify individual documents within the CMS, and Action
which will define the actions that the given user is permitted to do to that resource.
Open policy.alfa
in your editor and, between the import statements and the attribute declarations, add the following:
type Permission {
Resource:string
Action:string
}
Now that we have a type for a Structured Attribute defined, we can declare the attribute itself:
attribute Permissions { type=Permission category=subjectCat id="permissions" }
With the Permisison attribute defined we can use it in the policy. We're going to remove the three role-based rules and replace with a single permission-based access rule which verifies that the user has the permission to take action on the specified resource.
rule permissionBasedAccess {
permit
target clause Resource == Permissions.Resource
condition Permissions[Oasis.Attributes.Resource == Resource].Action == Action
}
Lets dissect the syntax around the usage of the structured attribute here. In ALFA, all attributes are bags, and may contain multiple values. Structured Attributes follow the same rules, and so the Permissions
attribute may contain multiple Permission
values. Additionally, the fields within a Structured Attribute are also bags, and may each contain multiple values.
So, the statement target clause Resource = Permissions.Resource
is checking whether the user has any permission that relates to the requested resource.
Naturally, we want to check the user is allowed to take the requested Action
against the requested Resource
. If the statement was Resource == Permissions.Resource and Action == Permissions.Action
, this would be checking if the user can do any action against the given resource, and if the user can do the requested action against any resource. This is too permissive, and this is where the syntax in the condition
comes into play.
Using square braces immediately following a Structured Attribute defines a filter on that attribute. Permissions[Oasis.Attributes.Resource == Resource]
filters the Permissions
attribute down to only those values where the Resource
matches the requested resource. Note that because we have a structured field and an attribute with the same name, we need use the fully qualified name Oasis.Attributes.Resource
to disambiguate. The scope of attribute identifiers within a filter will always check the structured attribute for a match first.
Once we've filtered the structured attribute down to the matching resource, we can access the action property and verify that we have permission to do the requested action on that resource, eg. Permissions[Oasis.Attributes.Resource == Resource].Action == Action
Your ALFA policy should now contain only two rules, mustUse2FA
and permissionBasedAccess
.
Modifying the CMS controller
We have a new attribute being used in the policy now: Resource
. Lets map that now.
In the Edit
and Publish
endpoints, we have the documentId
parameter supplied in the route. This will be the Resource
. Add [PolicyAttributeValue(PolicyAttributeCategories.Resource, "Resource")]
to the documentId
parameter on each controller method.
As the Create
endpoint doesn't require a documentId
, we'll map a wildcard *
to the Resource
attribute. *
isn't anything special for Enforcer, we'll just use that value in our permission attribute value provider to represent new documents. Modify the EnforcerAuthorize
attribute on the Create
endpoint as follows: [EnforcerAuthorization(ResourceType = "document", Resource = "*", Action = "create")]
Providing values for Structured Attributes
Move over to UserRoleAttributeValueProvider
and UserRoleRecord
. These are no longer required, we will modify them to to use Structured Attributes.
Define a new class, alongside UserRoleRecord
that mirrors the shape of our Permissions attribute. In Enforcer our classes that implement structured attributes need to inherit from StructuredAttribute<T>
where T
is your structured attribute.
public class Permission : StructuredAttribute<Permission>
{
public string Action { get; set; }
public string Resource { get; set; }
}
Now modify UserRoleRecord
, update the name to UserPermissionRecord
and update the Role
property to a Permissions
and update the Enforcer attribute binding.
public class UserPermissionRecord
{
[PolicyAttributeValue(PolicyAttributeCategories.Subject, "permissions", Sensitivity = PolicyAttributeSensitivity.NonSensitive)]
public IEnumerable<Permission> Permissions { get; set; }
}
Since attributes are implicitly bags, we can declare Permissions
as any type that implements IEnumerable<T>
. It is possible for a user to have multiple permissions, each with different resources and actions. Note: We could instead declare the Action
field as IEnumerable<string>
, since structured attribute fields are also implictly bags. Both approaches are viable.
Rename the UserRoleAttributeValueProvider
to UserPermissionAttributeValueProvider
, and we will modify the dictionary of user roles to fine-grained permissions. Also update dependency injection in Startup.cs
with the updated class name.
private readonly Dictionary<string, List<Permission>> permissions = new()
{
{
"Alan", new List<Permission>()
},
{
"Bob", new List<Permission>
{
new()
{
Resource = "*",
Action = "create"
}
}
}
};
Enforcer creates a new RecordAttributeValueProvider
for each request. When UserPermissionAttributeValueProvider
is created, only Bob has the permission to create documents. Add a reference to the document store in this class' constructor, and then we can initialise the permissions for any other documents in the system.
private const string CreatePermission = "create";
private const string EditPermission = "edit";
private const string PublishPermission = "publish";
private const string Alan = "Alan";
private const string Bob = "Bob";
private IDocumentStore documentStore;
public UserPermissionAttributeValueProvider(IDocumentStore documentStore)
{
this.documentStore = documentStore;
foreach (var id in this.documentStore.GetDocumentIds())
{
AddDocumentPermissions(id);
}
}
private void AddDocumentPermissions(string newDocument)
{
// Grant users access to the document
permissions[Alan].Add(new Permission(){ Resource = newDocument, Action = PublishPermission});
permissions[Alan].Add(new Permission(){ Resource = newDocument, Action = EditPermission});
permissions[Bob].Add(new Permission(){ Resource = newDocument, Action = EditPermission});
}
It's quite easy to see how this model can support the concept of individual document ownership, a writer would own a document until it is ready to publish, at which point it could be shared with an editor, granting them access.
Modify the GetRecordValue
method so that the user's permissions are resolved and passed to the policy.
protected override async Task<UserPermissionRecord> GetRecordValue(IAttributeResolver attributeResolver, CancellationToken ct)
{
var userAttribute = await attributeResolver.Resolve<string>(
new PolicyAttribute("request.user", PolicyValueType.String, PolicyAttributeCategories.Subject),
ct);
var user = userAttribute.SingleOrDefault();
if (user == null || !permissions.TryGetValue(user, out var documentPermissions)) return null;
return new UserPermissionRecord
{
Permissions = documentPermissions
};
}
Run the project again, verify that the three endpoints behave as expected for both Alan and Bob. Edit
and Publish
requests to documents not yet created will result in 403 Forbidden
.
Conclusion
You have seen how we can use Structured Attributes in ALFA to take our policies to new levels, taking us us beyond simple roles into the realm of fine-grained permissions and 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.