Array.map for Rego or how to combine RBAC with api routes

412 Views Asked by At

I would like to define permissions in JSON data such as:

"permissions": [
 {
  "resource": ["users", ":uid", "salary"],
  "action": "GET"
 }
]

Now when evaluating, I want to replace :uid with input.subject . How would I go about this? Is there something like Array.prototype.map() in Rego?

PS: I know I can do this, for example.

allow {
    input.action = ["GET", "POST"][_]
    input.resource = ["users", uid, "salary"]
    input.subject = uid
}

But instead of spelling out each path in the policy, I would like to use RBAC (roles + permissions) so that I can pass those API endpoint permissions as JSON data. Is it possible?

1

There are 1 best solutions below

1
On BEST ANSWER

You can certainly write a policy that scans over all of the permissions and checks if there's a match. Here's a simple (but complete) example:

package play

permissions = [
    {
        "resource": "/users/:uid/salary",
        "action": "GET"
    },
    {
        "resource": "/metrics",
        "action": "GET"
    }
]

default allow = false

allow {
    some p
    matching_permission[p]
}

matching_permission[p] {
    some p
    matching_permission_action[p]
    matching_permission_resource[p]
}

matching_permission_action[p] {
    some p
    permissions[p].action == input.action
}

matching_permission_resource[p] {
    some p
    path := replace(permissions[p].resource, ":uid", input.subject)
    path == input.resource
}

The downside of this approach is that each evaluation has to, in the worse case, scan over all permissions. As more permissions are added, evaluation will take longer. Depending on how large the permission set can get, this might not satisfy the latency requirements.

The typical answer to this is to use partial evaluation to pre-evaluate the permissions data and generate a rule set that can be evaluated in constant-time due to rule indexing. This approach is covered on the Policy Performance page. For example, if you run partial evaluation on this policy, this is the output:

$ opa eval -d play.rego -f pretty 'data.play.allow' -p --disable-inlining data.play.allow
+-----------+-------------------------------------------------------------------------+
| Query 1   | data.partial.play.allow                                                 |
+-----------+-------------------------------------------------------------------------+
| Support 1 | package partial.play                                                    |
|           |                                                                         |
|           | allow {                                                                 |
|           |   "GET" = input.action                                                  |
|           |                                                                         |
|           |   replace("/users/:uid/salary", ":uid", input.subject) = input.resource |
|           | }                                                                       |
|           |                                                                         |
|           | allow {                                                                 |
|           |   "POST" = input.action                                                 |
|           |                                                                         |
|           |   replace("/metrics", ":uid", input.subject) = input.resource           |
|           | }                                                                       |
+-----------+-------------------------------------------------------------------------+

In this case, the equality statements would be recognized by the rule indexer. However, the indexer will not be able to efficiently index the ... = input.resource statements due to the replace() call.

Part of the challenge is that this policy is not pure RBAC...it's an attribute-based policy that encodes an equality check (between a path segment and the subject) into the permission data. If we restructure the permission data a little bit, we can workaround this:

package play2

permissions = [
    {
        "owner": "subject",
        "resource": "salary",
        "action": "GET"
    },
    {
        "resource": "metrics",
        "action": "GET"
    },
]

allow {
    some p
    matching_permission[p]
}

matching_permission[p] {
    some p
    matching_permission_action[p]
    matching_permission_resource[p]
    matching_permission_owner[p]
}

matching_permission_action[p] {
    some p
    permissions[p].action == input.action
}

matching_permission_resource[p] {
    some p
    permissions[p].resource == input.resource
}

matching_permission_owner[p] {
    some p
    permissions[p]
    not permissions[p].owner
}

matching_permission_owner[p] {
    some p
    owner := permissions[p].owner
    input.owner = input[owner]
}

This version is quite similar except we have explicitly encoded ownership into the permission model. The "owner" field indicates the resource owner (provided in the input document under the "owner" key) must be equal to the specified input value (in this example, input.subject).

Running partial evaluation on this version yields the following output:

$ opa eval -d play2.rego -f pretty 'data.play2.allow' -p --disable-inlining data.play2.allow
+-----------+-------------------------------+
| Query 1   | data.partial.play2.allow      |
+-----------+-------------------------------+
| Support 1 | package partial.play2         |
|           |                               |
|           | allow {                       |
|           |   "GET" = input.action        |
|           |                               |
|           |   "salary" = input.resource   |
|           |                               |
|           |   input.owner = input.subject |
|           | }                             |
|           |                               |
|           | allow {                       |
|           |   "GET" = input.action        |
|           |                               |
|           |   "metrics" = input.resource  |
|           | }                             |
+-----------+-------------------------------+

All of the conditions on the rule bodies are now recognized by the rule indexer and evaluation latency will scale w/ the number of rules that could potentially match the input. The tradeoff here of course is that whenever the permissions change, partial evaluation has to be re-executed.