Share Rules Across Resources

Some applications have many resources that should have similar authorization rules applied to them. This is a common scenario in workflow driven applications that have different user types and a large number of resources.

A common set of rules will apply to many resources, with some exceptions. In this guide, we will cover various ways for modeling this scenario with Oso.

Setup

In this example, we will consider a hypothetical EMR (electronic medical records) application. We’ll discuss a few resources:

  • Order: A record of an action that medical staff will perform on a patient
  • Test: A diagnostic test that will be performed on a patient.
  • Lab: A lab test that will be performed on a patient.

These resources are all examples of different types of patient data.

Basic Policy

Let’s start by considering a basic policy controlling access to these three resources:

01-polar.polar
allow(actor: User, "read", resource: Order) if
    actor.role = "medical_staff" and
    actor.treated(resource.patient);

allow(actor: User, "read", resource: Test) if
    actor.role = "medical_staff" and
    actor.treated(resource.patient);

allow(actor: User, "read", resource: Lab) if
    actor.role = "medical_staff" and
    actor.treated(resource.patient);

Let’s take a look at the first rule in the policy. This allow rule permits an actor to perform the "read" action on an Order if:

  1. The actor’s role property is equal to "medical_staff".
  2. The actor has treated the patient associated with the Order in question, which is verified by calling the actor’s treated() method.

Note the head of the rule. Each argument uses a type specializer to ensure this rule only applies to certain types of resources and actors. This rule indicates that the actor argument must be an instance of the User class and the resource argument must be an instance of the Order class.

This policy meets our goal above. We have expressed the same rule for the three types of patient data, but it is a bit repetitive. Let’s try to improve it.

Using a Rule to Express Common Behavior

Our policy doesn’t just need to contain allow rules. We can write any rules we’d like and compose them as needed to express our policy!

02-nested-rule.polar
can_read_patient_data(actor, "read", resource) if
    actor.role = "medical_staff" and
    actor.treated(resource.patient);

allow(actor: User, "read", resource) if
    can_read_patient_data(actor, "read", resource);

Now, we’ve taken the repeated logic and expressed it as the can_read_patient_data rule. When the allow rule is evaluated, Oso will check if the can_read_patient_data is satisfied. The policy is much shorter!

Unfortunately, we’ve lost one property of our last policy: the specializers. This rule would be evaluated for any type of resource — not just our three examples of patient data above. That’s not what we want.

Bringing Back Specializers

We can combine this idea with our first policy to make sure only our three patient data resources use the can_read_patient_data rule.

03-specializer.polar
allow(actor: User, "read", resource: Order) if
    can_read_patient_data(actor, "read", resource);

allow(actor: User, "read", resource: Test) if
    can_read_patient_data(actor, "read", resource);

allow(actor: User, "read", resource: Lab) if
    can_read_patient_data(actor, "read", resource);

Now, we still have three rules, but the body isn’t repeated anymore.

One Rule to Rule Them All

We haven’t talked about the application side of this yet. So far, we’ve assumed Order, Lab, and Test are application classes.

Here’s how they might be implemented:

inheritance_external.rb
class PatientData
  attr_accessor :patient

  def initialize(patient:)
    @patient = patient
  end
end
OSO.register_class(PatientData)

class Lab < PatientData; end
OSO.register_class(Lab)

class Order < PatientData; end
OSO.register_class(Order)

class Test < PatientData; end
OSO.register_class(Test)

We used inheritance to capture some of the common functionality needed (storing the patient). In a real application these would probably be ORM models.

We can use the same idea to shorten our policy even further!

04-one-specializer.polar
allow(actor: User, "read", resource: PatientData) if
    actor.role = "medical_staff" and
    actor.treated(resource.patient);

Now, this allow rule will be evaluated for any instance that is a subclass of PatientData. Polar understands the class inheritance structure when selecting rules to evaluate!

Summary

In this guide, we saw an example of an application policy that could result in significant repetition. We tried out a few strategies for representing common policy across many resource types. First, we wrote a custom rule that moved duplicated logic into one place. Then we used specializers and application types to condense our policy even further.

Connect with us on Slack

If you have any questions, or just want to talk something through, jump into Slack. An Oso engineer or one of the thousands of developers in the growing community will be happy to help.