Anatomy of an AuthZen request
An AuthZen request is composed of four main components: Subject
, Action
Resource
and Context
. Subject
, Action
and Resource
are mandatory, Context
is optional and can provide additional contextual data to support the request.
The response to the request contains a decision, either true
or false
, which determines whether the action can proceed.
A minimal example of an AuthZen request, as will be sent by the Kong plugin, is below.
{
"subject": {
"type": "identity",
"id": "alice@acmecorp.com"
},
"resource": {
"type": "route",
"id": "/api/todos"
},
"action": {
"name": "GET",
}
}
Enforcer will map these values to Oasis attributes as follows:
Request element | Attribute ID | Attribute Category |
---|---|---|
Subject ID | id |
Subject urn:oasis:names:tc:xacml:1.0:subject-category:access-subject |
Resource ID | Resource |
Resource urn:oasis:names:tc:xacml:3.0:attribute-category:resource |
Action name | Action |
Action urn:oasis:names:tc:xacml:3.0:attribute-category:action |
Updating the policy
Update the the authorization policy to align with the requirements in the Getting started section:
- Administrators are able to perform all actions on the to-do list.
- Workers can view to-do items and mark them as complete.
- Guests can only view to-do items.
First, in Policies\policy.alfa
update the TodoServicePolicy
so that it will only be considered for requests to the to-do service, and by default will return a deny result. Remove the permitAll
rule as well. The policy should now look as follows:
namespace AcmeCorp {
import Oasis.Attributes.*
import Oasis.Functions
import Enforcer.Functions
policyset Global {
apply firstApplicable
policy TodoServicePolicy {
target clause StartsWith("/todolist/api/", Single(Resource))
apply denyUnlessPermit
}
}
}
The verb of the request to the to-do API will be mapped to the Action
attribute. In the to-do API, all read operations are performed with GET
, and completing a to-do item is done with PATCH
. Create, Update and Delete are done with POST
, PUT
and DELETE
respectively. Lets add some shared conditions to use in rules through the policy.
namespace AcmeCorp {
import Oasis.Attributes.*
import Oasis.Functions
import Enforcer.Functions
condition ReadTask Action == "GET"
condition CompleteTask Action == "PATCH"
condition UpdateTask Action == "PUT"
condition CreateTask Action == "POST"
condition DeleteTask Action == "DELETE"
policyset Global {
apply firstApplicable
policy TodoServicePolicy {
target clause StartsWith("/todolist/api/", Resource)
apply denyUnlessPermit
}
}
}
Finally we can add some rules that allow users to take relevant actions. We will use built-in the Role
attribute here, and map values to it in the next section.
namespace AcmeCorp {
import Oasis.Attributes.*
import Oasis.Functions
import Enforcer.Functions
condition ReadTask Action == "GET"
condition CompleteTask Action == "PATCH"
condition UpdateTask Action == "PUT"
condition CreateTask Action == "POST"
condition DeleteTask Action == "DELETE"
policyset Global {
apply firstApplicable
policy TodoServicePolicy {
target clause StartsWith("/todolist/api/", Resource)
apply denyUnlessPermit
rule allUsersCanReadTasks {
target clause ReadTask
condition Role == "Admin" or
Role == "Worker" or
Role == "Guest"
permit
}
rule adminAndWorkersCanCompleteTasks {
target clause CompleteTask
condition Role == "Admin" or
Role == "Worker"
permit
}
rule allowAdministrators {
target clause CreateTask or UpdateTask or DeleteTask
target clause Role == "Admin"
permit
}
}
}
}
Resolving user roles
We will add a custom attribute value provider which will populate the Role
attribute for various users. See the Custom Attribute Value Providers page for more detailed information.
We will have three users, Alice, Bob and Carol, with roles Admin, Worker and Guest respectively.
Add a new class to the Enforcer project: RoleAttributeValueProvider.cs
with the content as follows:
using Rsk.Enforcer.PIP;
using Rsk.Enforcer.PolicyModels;
namespace KongTutorialRemoteEnforcer.AttributeValueProviders;
public class UserRole
{
public UserRole(string role)
{
Role = role;
}
[PolicyAttributeValue(PolicyAttributeCategories.Subject, "role")]
public string Role { get; set; }
}
public class RoleAttributeValueProvider : RecordAttributeValueProvider<UserRole>
{
private readonly Dictionary<string, UserRole> _roles = new()
{
{ "Alice", new UserRole("Admin") },
{ "Bob", new UserRole("Worker") },
{ "Carol", new UserRole("Guest") }
};
protected override async Task<UserRole> GetRecordValue(IAttributeResolver attributeResolver, CancellationToken ct)
{
var subAttribute = await attributeResolver.Resolve<string>(Rsk.Enforcer.Oasis.Attributes.Subject.Identifier, ct);
var userName = subAttribute.SingleOrDefault();
if (userName == null) return null;
if (!_roles.TryGetValue(userName, out var role))
return null;
return role;
}
}
Add the RoleAttributeValueProvider
to the Enforcer services in Program.cs
.
builder.Services
.AddEnforcer("AcmeCorp.Global", options =>
{
options.Licensee = "DEMO";
options.LicenseKey = "Obtain a free demo license key from https://www.identityserver.com/products/enforcer";
})
.AddFileSystemPolicyStore("Policies")
.AddPolicyAttributeProvider<RoleAttributeValueProvider>();
Now run and test everything works as expected for the three users, Alice, Bob and Carol. Alice is an administrator and can perform all actions, Bob can view and complete tasks, Carol can only view tasks.
To make requests as each user, generate a JWT and put the user's name in the sub
property. Set an Authorization header for the request with value Bearer {token}
.
Conclusion
As you can see, we can integrate Enforcer with external services relatively easily using the AuthZen protocol. Aside from Kong, there are also plugins for other API gateways available on the AuthZen Github.
For more information about Enforcer, see the product documentation.