Introduction into OpenFGA

Welcome back to our deep dive into data security and privacy. In my previous post, “Fine-Grained Authorization: The Future of Data Security and Privacy,” we explored the evolving landscape of cybersecurity and the critical role of Fine-Grained Authorization (FGA) in safeguarding sensitive information. As we discussed, shifting from traditional Role-Based Access Control (RBAC) to more nuanced Relation-Based Access Control (ReBAC) is pivotal in meeting today’s complex security challenges.

Today, we’ll take a practical journey into OpenFGA, an innovative tool that epitomizes the implementation of FGA principles. Our focus will be on applying OpenFGA in a scenario that’s all too familiar and significant – healthcare data security. Specifically, we’ll address the original problem statement: ensuring that only the managing doctor has access to a patient’s medical records within a hospital setting.

To support us during our development work I suggest installing the OpenFga Extention for Visual Studio code.

Creating the authorization model

We begin by examining a basic RBAC (Role-Based Access Control) implementation, where access to medical data is permitted to anyone with the role of a doctor, while individuals in roles such as IT or Finance are restricted from such access. Roles like “Doctor” and “IT” would be clearly defined in a typical RBAC system. This serves as our starting point, however, in a ReBAC system, we should be thinking about authorization starting from the resources. So start by just putting down a few sentences on how our system should act by starting with the most important feature and iterate over the following loop:

Pick the most important feature

To help define our authorization model you must be able to answer the question “Why could user U perform an action A on object O”, and can be formatted in the sentence “A user {user} can perform action {action} to/on/in {object types} … IF {contitions}”. The rules for viewing, editing, and history would be more complicated but for this example, we use the following list of statements:

  • A user can view_in_detail a patient’s health_record if they are the attending physician of the health_record
  • A user can view_metadata a patient’s health_record if they are a physician in the hospital
  • A user can view all of a patient’s health_record if they are the attending physician of that patient

List the Object types

Make a list of types of objects, that can correlate closely to objects in your existing domain. Marked in blue. Include the second nouns that appear in the conditions as part of the expression “{first noun} of a/the {second noun}”. Marked in green

  • A user can view_in_detail a patient’s health_record if they are the attending physician of the health_record
  • A user can view_metadata a patient’s health_record if they are a physician in the hospital
  • A user can view all of a patient’s health_record if they are the attending physician of that patient

This results in health_record, hospital, emergency_response, and patient. Let’s model this using the OpenFGA Configuration Language:

List the relations for those types

Now we defined our types we need to establish the relationships between those types. To help us identify we can use: Relations for a type {type} will be all of these:

  • any noun that is the {noun} of a “{noun} of a/an/the {type}” expression. These are typically the Foreign Keys in a database. We’ll highlight these in green.
  • any verb or action that is the {action} of a “can {action} (in) a/an {type}” expression. These are typically the permissions for a type. We’ll highlight these in yellow.
  • A user can view_in_detail a patient’s health_record if they are the attending physician of the health_record
  • A user can view_metadata a patient’s health_record if they are a physician in the hospital
  • A user can view_all of a patient’s health_record if they are the attending physician of that patient

This results in:
health_record (view_in_detail, attending_physician, view_metadata, patient)
hospital (physician)
patient (view_in_detail, attending_physician).
Let’s add that to our model:

Define Relations

When defining relations it makes sense to start from a grouping level and work down here for example the type of hospital that contains physicians. A physician should be a user.

For the type of patient, the privilege view_in_detail is not directly assignable and is derived from an assigned role to the patient (attending_physician). To allow the can_view_metadata for all physicians in the hospital we need to pass it through the patient and add the hospital as assignable to the patient. The notation for assigning [user, hospital#physician] to view_metadata is fairly common. They are used to express that relationships “to the object with that relation” (eg. “users” of the type user or “physician of hospital{id}”) can be assigned by your system and that only users that have that relation are those with a direct relationship.

It is not the responsibility of the authorization model to verify that there is just one attending_physician, if that needs to be so the application should safeguard that.

The last type is the health_record which has a direct relation to the patient and the most privileges to view. We should assign it to the patient but we can also assign an attending physician to have direct access to just this health_record. the rest of the privileges are derived from other relations

Test the model

General authorization check: “Can user U, perform action A on an object O
OpenFga (ReBAC) authorization check: ” Can user U have relation R with object O

What we want to validate is that given the current context of our model and some relationship tuples, we get the expected outcome. The outcome can only be in 2 forms

  1. user U has a relation R with object O
  2. user U does not have a relation R with object O

Start by writing our relationship tuples.

The given examples are there to help you easily understand the relationships. in a real-world implementation identifiers like Jane, Karen, Treatment1, Treatment2, and VU would be replaced by Guids
System ActionRelationship Tuple
Joe is a physician at the VU hospital{ user:"user:joe", relation:"physician", object:"hospital:VU" }
Karen is a physician of the VU hospital{ user:"user:karen", relation:"physician", object:"hospital:VU" }
All physicians at the VU should have metadata access to the patient Jane{ user:"hospital:VU#physician", relation:"view_metadata", object:"patient:jane" }
Jane has a health_record of the first treatment{ user:"patient:jane", relation:"patient", object:"health_record:treatement1" }
Jane has a health_record of the second treatment{ user:"patient:jane", relation:"patient", object:"health_record:treatement2" }
Karen is the attending_physician of Jane{ user:"user:karen", relation:"attending_physician", object:"patient:jane" }
Joe is the attending_physician of the first treatment{ user:"user:joe", relation:"attending_physician", object:"health_record:treatement1" }

To verify our model in the context of our data we want to run the following tests.

  • “Joe can view the metadata for Jane because he is a physician at the hospital”
  • “Karen can view all patient data for Jane because he is the attending_physician of Jane”
  • “Joe can view all for treatment1 because he is the attending_physician of the health record”
  • “Joe can view the metadata for treatment2 because he is a physician of the hospital”
  • “Jane has 2 health_records”
  • “Karen sees 2 health_records”
  • “Joe sees 1 health_record”

The checks: can be written using the following structure

ObjectDescription
userThe user type and user id you are checking for access
objectThe object type and object id related to the user
contextA set of tests for contextual parameters used to evaluate conditions (not in this example)
assertionsA list of relation:expected-result pairs
<relation>: <true or false>The name of the relation you want to verify and the expected result

You can also run a validation on the expected outcome for a list_objects: following the structure

ObjectDescription
userThe user type and user id you are checking for access
typeThe type you want to fetch the objects for
contextA set of tests for contextual parameters used to evaluate conditions (not in this example)
assertionsA list of relation:expected-result pairs
<relation>: [<expected object>]The name of the relation you want to verify and the expected objects
The full model with tests can be found in UnclesLibrary here 

If you set your local environment with CLI as described in the next paragraph you can now run fga model test --tests model.fga.yaml and get the following result

Setting up your local environment

There are different ways we can interact with OpenFGA, we will use the CLI and just a local docker. To install the CLI go here and download and install the binaries. If you want to use the API it would be easy to generate a Postman collection. However, we want to leverage the test capabilities of the CLI that’s why we are using it.

Conclusion

Exploring OpenFGA showed us a smarter way to handle data security, moving from broad access controls to a more detailed, relationship-based system. This approach is key for protecting sensitive information effectively.

Our journey with OpenFGA, from building to testing an authorization model, proves its worth beyond healthcare, offering a blueprint for data privacy across industries. OpenFGA represents a step towards a future where data security is both sophisticated and user-centric, ensuring that sensitive information remains safe and accessible only to those who need it.

Next up

Having a look at the playground or adding conditions and context to our model. Not yet sure, keep posted 😉

Share

You may also like...

Leave a Reply