How to validate application authorizations in a hierarchical RBAC/ABAC policy definition with Open Policy Agent?
Asked Answered
H

3

4

We are building a cloud based application, using C# as our main language and running on Microsoft Azure. One of the key pieces of the architecture is to have fine grained authorization rights implemented for business logic in the application.

Open Policy Agent

We are looking at Open Policy Agent, as that seems to be a promising technology for these purposes. The example scenario/rules are described below. But it boils down to the scenario in something like a SharePoint library, or a Windows folder on the file system.

You have a hierarchy and you want to assign users with specific permissions to places in the hierarchy, where rights get inherited, but can be overridden deeper in the tree.

The platform we have will frequently update the permissions of users in the tree, and will constantly evaluate the policy agent to ask if a user has rights to perform a specific action on our platform.

Question

The question we have:

  • Does someone has a good example in OpenPolicyAgent that implements something like a hierarchical/tree like permission policy?
  • Are there other alternatives to OPA that are better suited for this?

Authorization rules

We have a tenant hierarchy that lists devices, defined like a tree structure:

+- Plant01
   +- Line01
      +- Device01
    - Line02
      +- Device02
       - Device03
       - Device04
+- Plant02
   +- Line03
      +- Device05
       - Device06
       - Device07

We also have people, defined in groups/users that we want to assign a specific role on a place in that hierarchy above.

Example:

  • User01 can stop all devices of Plant01-Line01
  • User01 can only view devices of Plant01-Line02
  • User00 can stop all devices of Plant01
  • User00 must not stop Plant01-Line02-Device04
Huehuebner answered 16/2, 2022 at 13:19 Comment(0)
E
3

Are there other alternatives to OPA that are better suited for this?

ReBAC solutions (such as those based on Google's Zanzibar paper) might offer a good alternative for your use-case.

Some of the current offerings that are based of the paper and offer Fine Grained Authorization Services include:

They all allow you to define a policy and represent the system state in order to make a decision on whether someone is authorized to access a resource.

For example in Auth0 FGA, you might define an authorization model like so:

type plant
  relations
    define parent as self
    define viewer as self
    define stopper as self
    define can_stop as stopper
type line
  relations
    define parent as self
    define viewer as self or viewer from parent
    define stopper as self or stopper from parent
    define can_stop as stopper
type device
  relations
    define parent as self
    define viewer as self or viewer from parent
    define stopper as self or stopper from parent 
    define can_stop as stopper but not can_not_stop
    define can_not_stop as self

Then you can add the following relationship tuples to reflect your current state:

write({ "user": "plant:01", "relation": "parent_plant", "object": "line:01" })
write({ "user": "plant:01", "relation": "parent_plant", "object": "line:02" })
write({ "user": "line:01", "relation": "parent_line", "object": "device:01" })
write({ "user": "line:02", "relation": "parent_line", "object": "device:02" })
write({ "user": "line:02", "relation": "parent_line", "object": "device:03" })
write({ "user": "line:02", "relation": "parent_line", "object": "device:04" })
write({ "user": "plant:02", "relation": "parent_plant", "object": "line:03" })
write({ "user": "line:03", "relation": "parent_line", "object": "device:05" })
write({ "user": "line:03", "relation": "parent_line", "object": "device:06" })
write({ "user": "line:03", "relation": "parent_line", "object": "device:07" })
write({ "user": "user01", "relation": "stopper", "object": "line:01" })
write({ "user": "user01", "relation": "viewer", "object": "line:02" })
write({ "user": "user00", "relation": "stopper", "object": "plant:01" })
write({ "user": "user00", "relation": "can_not_stop", "object": "device:04" })

And then you can ask questions like "is user1 related to X as can_stop?":

check("user01", "can_stop", "device:01") // { "allowed": true }
check("user01", "can_stop", "line:01") // { "allowed": true }
check("user01", "can_stop", "device:02") // { "allowed": false }

You can try this out in the Playground: https://play.fga.dev/stores/create/?id=01GGQYNRHK8EM07146HB8SSHXD

Disclaimer: I'm currently part of the Auth0 FGA team.

Earthnut answered 18/2, 2022 at 18:46 Comment(0)
C
1

I believe OPA is an excellent choice for solving this kind of problem.

I don't know if this is a good example, but it gets the job done on your particular example, I think.

Given the input:

{
    "user": "User00",
    "action": "stop",
    "device": ["Plant01", "Line02", "Device04"]
}

and the data:

{
    "roles": {
        "User00": ["Role00"],
        "User01": ["Role01"],
        "User99": ["Role00", "Role01"]
    },
    "grants": {
        "Role00": [
            {
                "action": "stop",
                "path": ["Plant01"]
            },
            {
                "allow": false,
                "action": "stop",
                "path": ["Plant01", "Line02", "Device04"]
            }
        ],
        "Role01": [
            {
                "action": "stop",
                "path": ["Plant01", "Line01"]
            },
            {
                "action": "view",
                "path": ["Plant01", "Line02"]
            }
        ]
    }
}

, this policy verifies that the input is allowed given the data:

package play

import future.keywords.in

default allow = false

allow {
    # Check that the 'grantResults' is a set containing only 'true'.
    grantResults == {true}
}

# This creates a set of all matching grant results. In the end, we want only one 'true' entry.
grantResults[granted] {
    some grant
    grants_for_user[grant]
    
    grant.action == input.action
    
    array.slice(input.device, 0, count(grant.path)) == grant.path
    
    granted = isAllowed(grant)
}

grants_for_user[grant] {
    some role in data.roles[input.user]
    some grant in data.grants[role]
}

# Undefined allow is implicitly true
isAllowed(grant) = allowed {
    allowed = grant.allow
} else = true

You can check out a live example here: https://play.openpolicyagent.org/p/YGRvm90KRU

The above policy is naïve, and will evaluate all applicable paths in the tree for the given input. With some thinking, an early exit strategy can probably also be made.

Capacious answered 16/2, 2022 at 15:26 Comment(1)
I am trying to learn OPA, I tried out the playground and tried multiple inputs, now I want understand, how can make API request while running OPA as webserver to evaluate against the policy?Lewanna
S
0

One alternative to OPA for fine-grained authZ is Styra (which is actually built by the creators of OPA). Another one was build.security, but this was acquired by Elastic. When I spoke to Elastic (they're a vendor we use at the company I work for), it seemed like they had various plans for build.security, but fine-grained authZ was a lower priority - not sure where this is at now...

FWIW we started building a bespoke entitlements layer (that uses OPA under-the-hood as the fine-grained authZ policy engine), but I'd be keen to migrate to more of a dedicated COTS offering at some stage...

Somnambulate answered 19/2, 2022 at 0:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.