authorizationopaopen-policy-agentrego

OPA authorization policies with scopes and roles


I'm using Open Policy Agent as an authorization component together with OIDC enabled apps.

I have input from the apps in the format:

{
    "token": {
        "scopes": [
            "read:books",
            "write:books"
        ]
    },
    "principal": {
        "roles": [
            "user",
            "moderator"
        ]
    },
    "context": {
        "action": "read",
        "resource": "books"
    }
}

Then I have data with access mapping in the format:

{
    "user": [
        "read:books"
    ],
    "moderator": [
        "read:books",
        "write:books"
    ],
    "administrator": [
        "read:books",
        "write:books",
        "read:store",
        "write:store"
    ]
}

And the policy currently looks like this:

package whatever.authz

context_scope := concat(":", [input.context.action, input.context.resource])

default allow = false

allow {
    token_has_context_scope
    principal_has_resource_access
}

token_has_context_scope {
    context_scope == input.token.scopes[_]
}

principal_has_resource_access {
    principal_role := input.principal.roles[_]
    context_scope == data[principal_role][_]
}

This produces the following error:

2 errors occurred:
policy.rego:16: rego_recursion_error: rule principal_has_resource_access is recursive: principal_has_resource_access -> principal_has_resource_access
policy.rego:7: rego_recursion_error: rule allow is recursive: allow -> principal_has_resource_access -> allow

It is the recursive lookup in the principal_has_resource_access function that is causing the error.

I need to check if one of the roles of the principal is allowed to access the resource as specified by the context. Since roles is an array i need to find the union of all access scopes in the data and see if one of them matches the context scope. What am I doing wrong in the policy?

The snippet can be found in the Rego Playground https://play.openpolicyagent.org/p/KhovLRgMup


Solution

  • OPA stores all data under the data path, including policy and rules. There's no way for the compiler to know that the input you're providing isn't referencing the policy itself (i.e. data["whatever"]) which would be recursive. The easiest way to work around this is to simply use a top level attribute for your data which differs from your policy (i.e package name), like this:

    {
        "attributes": {
            "user": [
                "read:books"
            ],
            "moderator": [
                "read:books",
                "write:books"
            ],
            "administrator": [
                "read:books",
                "write:books",
                "read:store",
                "write:store"
            ]
        }
    }
    

    And update your policy to reference this:

    context_scope == data["attributes"][principal_role][_]
    

    Since data.attributes != data.whatever.authz there is no risk of recursion, and the compiler won't complain. You might want a better name than "attributes", but I'll leave that to you :)