Example 2: Multi-tenant access control and user-defined RBAC with OPA and Rego
This example uses OPA and Rego to demonstrate how access control can be implemented on an API for a multi-tenant application with custom roles defined by tenant users. It also demonstrates how access can be restricted based on a tenant. This model shows how OPA can make granular permission decisions based on information that is provided in a high-level role.
The roles for the tenants are stored in external data (RBAC data) that is used to make access decisions for OPA:
{ "roles": { "tenant_a": { "all_access_role": ["viewData", "updateData"] }, "tenant_b": { "update_data_role": ["updateData"], "view_data_role": ["viewData"] } } }
These roles, when defined by a tenant user, should be stored in an external data source or an identity provider (IdP) that can act as a source of truth when mapping tenant-defined roles to permissions and to the tenant itself.
This example uses two policies in OPA to make authorization decisions and to examine how these policies enforce tenant isolation. These policies use the RBAC data defined earlier.
default allowViewData = false allowViewData = true { input.method == "GET" input.path = ["viewData", tenant_id] input.tenant_id == tenant_id role_permissions := data.roles[input.tenant_id][input.role][_] contains(role_permissions, "viewData") }
To show how this rule will function, consider an OPA query that has the following input:
{ "tenant_id": "tenant_a", "role": "all_access_role", "path": ["viewData", "tenant_a"], "method": "GET" }
An authorization decision for this API call is made as follows, by combining the RBAC data, the OPA policies, and the OPA query input:
-
A user from
Tenant Amakes an API call to/viewData/tenant_a. -
The Data microservice receives the call and queries the
allowViewDatarule, passing the input shown in the OPA query input example. -
OPA uses the queried rule in OPA policies to evaluate the input provided. OPA also uses the data from RBAC data to evaluate the input. OPA does the following:
-
Verifies that the method used to make the API call is
GET. -
Verifies that the path requested is
viewData. -
Checks that the
tenant_idin the path is equal to theinput.tenant_idassociated with the user. This ensures that tenant isolation is maintained. Another tenant, even with an identical role, is unable to be authorized in making this API call. -
Pulls a list of role permissions from the roles' external data and assigns them to the variable
role_permissions. This list is retrieved by using the tenant-defined role that is associated with the user ininput.role. -
Checks
role_permissionsto see whether it contains the permissionviewData.
-
-
OPA returns the following decision to the Data microservice:
{ "allowViewData": true }
This process shows how RBAC and tenant awareness can contribute to making an
authorization decision with OPA. To further illustrate this point, consider an API
call to /viewData/tenant_b with the following query
input:
{ "tenant_id": "tenant_b", "role": "view_data_role", "path": ["viewData", "tenant_b"], "method": "GET" }
This rule would return the same output as OPA query input
although it is for a different tenant who has a different role. This is because this
call is for /tenant_b and the
view_data_role in RBAC data still has
the viewData permission associated with it. To enforce the same
type of access control for /updateData, you can use a similar
OPA rule:
default allowUpdateData = false allowUpdateData = true { input.method == "POST" input.path = ["updateData", tenant_id] input.tenant_id == tenant_id role_permissions := data.roles[input.tenant_id][input.role][_] contains(role_permissions, "updateData") }
This rule is functionally the same as the allowViewData rule,
but it verifies a different path and input method. The rule still ensures tenant
isolation and checks that the tenant-defined role grants the API caller permission.
To see how this might be enforced, examine the following query input for an API call
to /updateData/tenant_b:
{ "tenant_id": "tenant_b", "role": "view_data_role", "path": ["updateData", "tenant_b"], "method": "POST" }
This query input, when evaluated with the
allowUpdateData rule, returns the following
authorization decision:
{ "allowUpdateData": false }
This call will not be authorized. Although the API caller is associated with the
correct tenant_id and is calling the API by using an
approved method, the input.role is the tenant-defined
view_data_role. The view_data_role
doesn't have the updateData permission; therefore, the
call to /updateData is unauthorized. This call would have been
successful for a tenant_b user who has the
update_data_role.