Going Backstage with OPA

8 min read

The huge ecosystem of integrations has over time become a true differentiator for Open Policy Agent (OPA), and an embodiment of the project’s promise to provide policy across the “whole cloud native stack”. Integrating OPA into a new tech stack also tends to be a rewarding experience for developers, which might help explain why so many integrations have been provided by external contributors over the years.

Today we’ll take a look at one of the newest integrations in the OPA ecosystem, the OPA Backstage plugin. I say we here because — what better way for me (Anders) to present this than to invite the actual author of the integration (Peter) to co-author this blog with me. 

So what’s Backstage, and what role does OPA play here?

Backstage + OPA = ❤️

Backstage 

Backstage is a popular open source tool for building Internal Developer Portals (IDPs). Developer portals allow developer teams to create an inventory of the software shared across the development organization, called software catalogs. These software catalogs can then be used to analyze dependencies, ensure common code standards are used consistently, that new projects include all the required or recommended components from the start, and much more.

Since we’re both big believers in enforcing organizational standards, and of course, using policy to enforce these standards, you probably see where this is going. Just like OPA, Backstage is a project in the Cloud Native Computing Foundation (CNCF) family, making collaboration across both projects feel particularly close to our hearts. 

So, what would an OPA integration in Backstage look like? There are actually quite a few possibilities! OPA policies could for example be an integral part in deciding what resources in the catalog must look like, or be used to ensure new projects launched via Backstage’s templating feature adheres to policy-backed standards. We’ll revisit these other possibilities later, but the first thing we were keen to explore was authorization.

Why authorization in Backstage?

Does Backstage need authorization? The answer may seem like it’d be an obvious “yes”, but the truth is that many deployments of Backstage use neither authentication nor authorization! This isn’t necessarily the disaster it may sound like. Provided that Backstage is running behind corporate firewalls and inside internal systems only, many organizations consider Backstage as a “public” (to their employees) portal where any employee can get an overview of the software components in their environment. These components aren’t necessarily secret to anyone in the development organization, and since they’re commonly read-only, there might not be an acute need to track who’s been using the portal.

Some organizations, like those working within regulated industries, will however likely want to verify both identity and ensure proper access control. Even when parts of the catalog aren’t secret per se, authorization can also be used as a filter to display only parts of the catalog that are likely to be relevant to any given user, greatly reducing noise and clutter in the UI.

Why OPA for Backstage authorization?

What would the benefits of using OPA for authorization be when Backstage already provides this? The answer would be mostly the same as anywhere else OPA is used in favor of the “native” way of doing things. These benefits include:

  • Updates to policy can be done without “downtime” or re-deploying Backstage, as the policy system is entirely decoupled from Backstage.
  • A unified way of working (authoring, testing, reviewing…) with policy across the stack.
  • Unified auditing — what someone does in Backstage may be audited in the same location and in the same way as what someone does in another system. 
  • Updates to Backstage policy can be done by anyone familiar with Rego, i.e. without having to know Typescript that Backstage is built with. 

Alright! So assuming we want authorization for Backstage, and we want to use OPA for that purpose, what components of this system would we need to work with for that to happen?

Anders and Peter talk about using OPA for Backstage authorization, at the KubeCon / CloudNativeCon conference in Paris, March 2024.

Plugins

Most things in Backstage revolve around plugins. Plugins provide not only optional or additional features, but even core functionality. Examples include the catalog plugin for keeping track of and managing software components in an organization, or the software templates plugin to allow users to create new components — like GitHub repositories — based off of ready-made templates following the best practices and guidelines of the organization.

We won’t go deep into the architecture of Backstage in general, but this architecture has some interesting implications for the topic we’re keen to explore here — authorization and access control. Specifically, plugins are independent, decoupled components that tend to own the data under their control. This ownership presents an interesting challenge for authorization systems, as they’ll have to either do their best to cooperate with existing plugins, or to try and sidestep the architecture to some extent and claim their own ownership over data pertaining to permissions and authorization. 

Backstage policy

Policy enforcement in Backstage is provided through the permissions framework. This is an optional component that needs to be installed as an extra step, or rather several extra steps, after authentication has been configured. While the authentication component is also optional, we’ll need to know the identity of the user making a request in order to have something to base our authorization policies on.

Customizing the permissions framework — which you’ll need to do to e.g. add or modify a policy — is done by editing the Typescript code of Backstage itself. Since Typescript is a compiled language, any change must be compiled and Backstage redeployed in order for the change to go into effect. While the Backstage docs are great, at least a basic understanding of Typescript feels like a prerequisite for many of the administrative tasks, and the permissions framework is no exception.

A Backstage permissions framework policy takes a permission query and a user object and from that returns a decision. The user object predictably contains attributes retrieved from authentication, and commonly includes the username and any groups the user is a member of. The permission query contains only a string, which might look something like “catalog.entity.read” — meaning someone (identified in the user object) is trying to read an entity from the catalog plugin. But which entity? Authorization policy commonly includes at least three components in its input: a subject (user), the action performed (like “read” or “delete”) and a resource. Backstage policies are however not provided anything to identify the resource on which an action is about to be performed. Why? A comment in the code for the PolicyQuery object provides some motivation:

“Unlike other parts of the permission API, the policy does not accept a resource ref. This keeps the policy decoupled from the resource loading and condition applying logic.”
This is where the Backstage policy model diverges from what we might be used to in most projects integrating OPA for authorization. While OPA defers enforcement to the client asking for decisions, we still expect OPA to make the decisions. The Backstage permissions model allows this to some extent, as policies may either allow, deny, or return a conditional decision, but conditional decisions are often needed for more fine-grained access control. So what does it mean to return a conditional decision? In order to understand that, we’ll first need to understand what an unconditional decision is.

Unconditional decisions

Unconditional decisions may be returned in cases where OPA (or another authorization extension) may make a decision fully on its own. As we’ve previously noted, our authorization policy is handed a query (like “catalog.entity.delete”) and a user object. Based on this we may in some cases be able to make a final decision already — we could for example see that the user trying to delete something should not have access rights to delete any resource provided by the catalog plugin, and could therefore deny the request without having further details about the specific resource at our disposal.

Other examples of rules that may not need detailed knowledge of resource references or attributes could include those that decide based on the environment around them — like a rule denying creation of new resources by anyone outside of working hours, and so on.
More fine-grained decisions however often require knowledge about the resource being the target of an action. Since we don’t possess this knowledge ourselves, we’ll need to defer to someone who does. More specifically, we’ll need to provide the conditions for which we’ll allow the request to the system that makes the final decision. As most things Backstage, it is commonly a plugin responsible for the resource targeted by an action.

Conditional decisions

As we previously covered, plugins constitute the core component of Backstage. Plugins commonly own any data under the control of the plugin, and this ownership extends also to the permissions system. Plugins may define their own rules for conditional decisions, where the plugin eventually provides the data missing in the context of a Backstage policy, often via a database query. 

To provide a concrete example, a plugin like the catalog plugin may provide a conditional rule that when queried answers the question “is the user making the request the owner of the resource?”. A Backstage policy may then defer the decision to the plugin based on that rule: “the resource may be modified if the user is the owner of that resource”. Administrators who aren’t satisfied with the available number of rules a plugin provides may choose to inject their own custom rules to extend the permissions system of either their own, or existing plugins.

Example of a conditional decision response deferring to the IS_ENTITY_OWNER rule:

{
  "result": "CONDITIONAL",
  "pluginId": "catalog",
  "resourceType": "catalog-entity",
  "conditions": {
    "anyOf": [
      {
        "resourceType": "catalog-entity",
	  "rule": "IS_ENTITY_OWNER",
	  "params": {
           "claims": [
             "user:default/anders",
             "group:default/opa-maintainers"
           ]
         }
      }
    ]
  }
}

For those of you deeply familiar with OPA, you may recognize parts of this pattern as partial evaluation

OPA Plugins for Backstage

Knowing what we now know about the Backstage permissions system and conditional vs. unconditional decisions, integrating OPA as part of the permissions framework entails having parts of it replaced by code that calls out to OPA for decisions — whether conditional or not. To simplify this, Peter built the OPA Plugins for Backstage project. This repository provides integration points for OPA to be used as the authorizer, and requires minimal changes to Backstage code to do so. 

Follow the instructions provided in the repo to get the integration set up, and enjoy having a decoupled fine-grained authorization system for backstage where policy can be deployed at any time and without having to rebuild and redeploy Backstage itself! 

Rego Policy for Backstage

So what does a Backstage authorization policy look like in Rego? The following example demonstrates a simple policy that will unconditionally allow anyone to read entities from the catalog plugin. If any other type of action is requested, like to write or delete an entity, the policy will respond with a conditional decision that states that the request may only be allowed on the condition that it is sent by the entity owner. This is done via the IS_ENTITY_OWNER rule, which is included in the catalog plugin and enforced at the database level. Since OPA is not involved in the final decision (as it doesn’t know who the entity owner is), one will have to consult the database rather than OPA’s decision logs in order to see why decisions like these were made.

package backstage_policy

import rego.v1

default decision := {"result": "DENY"}

# Allow read requests from anyone
decision := {"result": "ALLOW"} if {
    permission == "catalog.entity.read"
}

# Allow other type of requests only from the entity owner
decision := conditional("catalog", "catalog-entity", {"anyOf": [{
    "resourceType": "catalog-entity",
    "rule": "IS_ENTITY_OWNER",
    "params": {"claims": input.identity.claims},
}]}) if {
    input.permission.name == "catalog.entity.delete"
}

# Helper function for constructing a conditional decision
conditional(plugin_id, resource_type, conditions) := {
    "result": "CONDITIONAL",
    "pluginId": plugin_id,
    "resourceType": resource_type,
    "conditions": conditions,
}

Next Steps

Using OPA for Backstage authorization was the first topic we wanted to explore, but as we mentioned previously, there are many potential use cases for OPA in Backstage. A few examples include:

  • Using OPA for the templating plugin, where OPA could help determine who can publish code from templates, and/or what that code should look like depending on the context from which it’s published.
  • Have OPA validate the schema of resources stored in Backstage, similarly to how OPA may be used to validate infrastructure components or Kubernetes resource manifests.
  • Authorization use cases where OPA sidesteps the components and takes ownership of authorization policy entirely.

It’s likely the OPA Plugins for Backstage project will explore some of these domains in the future, so make sure to follow it if it sounds interesting to you!

Wrapping Up

Is OPA the right tool for authorization in Backstage? As with anything else, it depends on your requirements. For a fairly static authorization policy unlikely to change over time, the built-in permissions framework might serve your needs well. But if you’re already familiar with OPA and want to unify authorization across your organization, or if you want to update policy without having to rebuild and redeploy Backstage, OPA offers a compelling alternative.

If you’re just starting out learning about OPA and Rego, check out the Styra Academy. And make sure to use Regal for your policy authoring sessions. Not only will it help you find bugs — it’ll be an excellent learning assistance on your journey!

If you want to learn more about using OPA for authorization in Backstage, or have questions to ask, you’ll find both me and Peter in the Styra Community Slack!

Credits

Special thanks to Vincenzo Scamporlino (@vinzscam) and Mike Lewis (@mtlewis) from the Backstage team for helping to guide us through the Backstage permissions framework, and providing us with alternative models for integrating OPA in Backstage.

Cloud native
Authorization

Entitlement Explosion Repair

Join Styra and PACLabs on April 11 for a webinar exploring how organizations are using Policy as Code for smarter Access Control.

Speak with an Engineer

Request time with our team to talk about how you can modernize your access management.