developers

Continuous Authorization Testing: FGA, GitHub Actions, and CI/CD

Learn how to achieve continuous confidence in your authorization logic. This article will guide you on defining OpenFGA test files and integrating them into a robust CI/CD pipeline using GitHub Actions.

In modern, scalable applications, authorization logic is critical. As your authorization model gets more and more complex, manually checking every possible permission change can quickly become a bottleneck, leading to errors that expose data or prevent legitimate access. Think of an example like Github: testing all scenarios for every permission change can become unmanageable as the model scales.

To maintain continuous correctness as your application and its permissions evolve, you need a robust, automated CI/CD testing strategy. This article will guide you through defining tests for your OpenFGA model and automating their execution using a GitHub Actions workflow, ensuring that every code change is secure before it reaches production.

Understanding the Core Components

Before we automate the process, let's briefly review the components required for testing OpenFGA models.

OpenFGA

OpenFGA centralizes authorization decisions. It relies on three core components:

  1. Authorization Model: Defined in the OpenFGA DSL, this is the schema of your permissions (for example, user has admin relation on document).
  2. Relationship Tuples: The actual data (for example, user:alice is admin of document:report).
  3. Authorization Checks: Queries asking if a specific relationship is true (for example, Can user:alice view document:report?).

OpenFGA models are tested using a standardized YAML format that lives alongside a .fga or .fga.yaml model file. This file defines a scenario by including:

  • Model: The FGA DSL model being tested.
  • Tuples: The initial relationship data that sets up the scenario.
  • Tests: A list of permission checks with expected outcomes (true or false).

This format allows you to write isolated tests for your model logic, entirely separate from your application's code.

Designing the Authorization Model

Let’s use a well-known example, think of a git repository management application, similar to this OpenFGA example, where the permissions are defined as follows:

  • Users can be admins, maintainers, writers, triagers, or readers of repositories (each level inherits all access of the level lower than it, for example, admins inherit maintainer access and so forth)
  • Teams can have members
  • Organizations can have members
  • Organizations can own repositories
  • Users can have repository admin access on organizations, and thus have admin access to all repositories owned by that organization

Such an authorization model can be defined in a file: store.fga.yaml as follows:

store.fga.yaml

model
  schema 1.1

type user

type team
  relations
    define member: [user, team#member]

type repo
  relations
    define admin: [user, team#member] or repo_admin from owner
    define maintainer: [user, team#member] or admin
    define owner: [organization]
    define reader: [user, team#member] or triager or repo_reader from owner
    define triager: [user, team#member] or writer
    define writer: [user, team#member] or maintainer or repo_writer from owner

type organization
  relations
    define member: [user] or owner
    define owner: [user]
    define repo_admin: [user, organization#member]
    define repo_reader: [user, organization#member]
    define repo_writer: [user, organization#member]

Defining Tuples for Testing

Next, you’re going to define some tuples that will serve as data to test your model. When creating these tuples, it’s important to try to represent all of the relations and edge cases presented in your model. Consider the following criteria:

Focus on the edge cases

Define tuples that test the limits of your model's logic. Create tuples that use the following relation types:

  • Direct Access: A tuple granting the most basic, explicit access (user:alice is owner of organization:acme).
  • Non-existent Access: A user/object pair where no relationship exists, ensuring your model correctly denies permission (for example, check if a non-member can view the organization).
  • Implicit/Inherited Access: Tuples that trigger a computed relationship, such as a user who is a member of an organization, where the organization is an admin of a document (e.g., user:bob is member of team:devs, team:devs#member is admin of repo:x).

Cover all relation types (Role, Permission, and Userset)

Ensure you have at least one tuple for every distinct relation defined in your model's DSL. This includes:

  • Role Assignments (if applicable): user:carla has role:name on object:id.
  • Object Hierarchies: document:child is parent of document:parent (which establishes inheritance).
  • Conditional Tuples: If you use FGA Conditions, create tuples where the condition is explicitly True and another where it's explicitly False

Validate recursion and intersections

Create tuples that specifically test paths involving multiple hops or userset intersections

  • For Hierarchies (from clause): Set up a three-level hierarchy (for example, a grandparent, parent, and child object) and test if a user with permission on the grandparent correctly inherits access to the child.
  • For Intersection (and): Create a user that satisfies both required relationships and another user that satisfies only one to ensure the and logic is strict.

Simulate real-world scenarios

Design tuple sets for common, high-value business use cases to use as regression tests. For example:

  • A set of tuples that correctly configures a user:admin and verifies they can perform all management actions (create, delete, invite).
  • Tuples that assign an outside user:guest read-only access to a single specific document without making them a member of the parent organization.
  • This is best done in End-to-End tests.

With the above in mind, let’s update the store.fga.yaml file to have the following structure:

model: 
    model
  schema 1.1

type user

type team
  relations
    define member: [user, team#member]

type repo
  relations
    define admin: [user, team#member] or repo_admin from owner
    define maintainer: [user, team#member] or admin
    define owner: [organization]
    define reader: [user, team#member] or triager or repo_reader from owner
    define triager: [user, team#member] or writer
    define writer: [user, team#member] or maintainer or repo_writer from owner

type organization
  relations
    define member: [user] or owner
    define owner: [user]
    define repo_admin: [user, organization#member]
    define repo_reader: [user, organization#member]
    define repo_writer: [user, organization#member]
tuples:
  # The OpenFGA organization is the owner of the openfga/openfga repository
  - user: organization:openfga
    relation: owner
    object: repo:openfga/openfga
  # Members of the OpenFGA organization have a repository admin base permission on the organization
  - user: organization:openfga#member
    relation: repo_admin
    object: organization:openfga
  # Erik is a member of the OpenFGA organization
  - user: user:erik
    relation: member
    object: organization:openfga
  # The openfga/core team members are admins on the openfga/openfga repository
  - user: team:openfga/core#member
    relation: admin
    object: repo:openfga/openfga
  # Anne is a reader on the openfga/openfga repository
  - user: user:anne
    relation: reader
    object: repo:openfga/openfga
  # Beth is a writer on the openfga/openfga repository
  - user: user:beth
    relation: writer
    object: repo:openfga/openfga
  # Charles is a member of the openfga/core team
  - user: user:charles
    relation: member
    object: team:openfga/core
  # Members of the openfga/backend team are members of the openfga/core team
  - user: team:openfga/backend#member
    relation: member
    object: team:openfga/core
  # Diane is a member of the openfga/backend team
  - user: user:diane
    relation: member
    object: team:openfga/backend

Creating Tests

Defining tests is one of the most important steps, so let’s take a look at some things to keep in mind before creating the tests:

  • Test for Completeness: For every defined permission, create corresponding assertions that validate both the allowed and denied scenarios.
  • Validate Complex Rules with Boundary Cases: Focus test cases specifically on logic that involves userset rewrites, rules using the from keyword or logical operators. For instance, when testing a hierarchy, ensure the user inherits the permission across multiple levels. When testing an and rule, create a test that satisfies all conditions and another that satisfies only one.
  • Make Tests Atomic and Context-Specific: For any test requiring a complex or unique arrangement of relationships, use the dedicated tuples array within that individual test block (such as this example). This practice ensures the test is completely self-contained, easily readable, and its result is independent of other unrelated data in the test file, simplifying maintenance and debugging.

With this in mind, let’s define and add the following tests to the end of the, adding them to the end of the store.fga.yaml file as follows:

# .... 
tests:
  - name: Test individual user permissions on the openfga/openfga repo
    check:
      - user: user:anne
        object: repo:openfga/openfga
        assertions:
          reader: true
          triager: false
      - user: user:beth
        object: repo:openfga/openfga
        assertions:
          admin: false
      - user: user:charles
        object: repo:openfga/openfga
        assertions:
          writer: true
      - user: user:diane
        object: repo:openfga/openfga
        assertions:
          admin: true
      - user: user:erik
        object: repo:openfga/openfga
        assertions:
          reader: true

  - name: Test who are readers of the openfga/openfga repo
    list_users:
    - object: repo:openfga/openfga
      user_filter:
        - type: user
      assertions:
        reader:
          users: 
            - user:diane
            - user:charles
            - user:beth
            - user:anne
            - user:erik

  - name: Test which repos can Diane read
    list_objects:
      - user: user:diane
        type: repo
        assertions:
          reader:
            - repo:openfga/openfga

  - name: Check if the right users have access to the right repositories
    list_users:
      - object: repo:openfga/openfga
        user_filter: 
          - type: user
        assertions:
          writer:
            users: 
              - user:charles
              - user:beth
              - user:diane
              - user:erik

      - object: repo:openfga/openfga
        user_filter: 
          - type: team
            relation: member
        assertions:
          writer:
            users: 
              - team:openfga/backend#member
              - team:openfga/core#member

Testing the model locally with the FGA CLI

Before integrating with GitHub Actions, developers should be able to run these tests locally. You can find installation instructions of the FGA CLI on the OpenFGA documentation page.

Once installed, you can execute the following command:

$ fga model test --test store.fga.yaml 

This will produce the following output:

# Test Summary #
Tests 4/4 passing
Checks 6/6 passing
ListObjects 1/1 passing
ListUsers 3/3 passing

Automating FGA model tests with GitHub Actions

The goal is to run the tests whenever the model or test files are updated, ensuring we prevent deployment of a broken authorization model within the CI/CD pipeline.The OpenFGA team created a GitHub action action-openfga-test for this exact purpose.

To learn more about GitHub Actions Workflows, refer to the docs

In your project’s root folder, create a new file .github/workflows/fga-tests.yaml

name: Test FGA Model
run-name: ${{ github.actor }} is testing the FGA model
on:
  push:
    paths:
      - fga/**

jobs:
  test:
    name: Run tests
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Test FGA Model
        uses: openfga/action-openfga-test@v0.1.1
        with:
          test_path: fga/store.fga.yaml

This GitHub Action will execute on every push involving the store.fga.yaml file. It will run on Ubuntu and execute two steps:

  1. Checkout the code’s repo using GitHub’s Checkout Action
  2. Run the tests using the OpenFGA Test Action

When you push a change to your repository, under the Actions tab you should see your action running, and if successful, you should see the exact same output provided by the FGA CLI earlier.

GitHub Actions screen running OpenFGA Test Action

FGA and CI/CD: Continuous Validation for Authorization

By adopting this automated testing strategy, you move authorization testing from a tedious manual step to a reliable, repeatable process integrated into your CI/CD pipeline. FGA provides you with the declarative power to define complex permissions, and GitHub Actions enables you to deploy them safely within your CI/CD process with speed and confidence. Your application's authorization logic is now continuously validated, allowing you to focus on building features rather than debugging access control.