Alen
Alen

Reputation: 63

Using opa for abac to check user claims agains defined policies

So I'm trying implement a fairly simple ABAC system for my application and came across open policy agent during my investigations. It seems to be a good fit for my needs but I just can't make it work for my use case where I have an user object that is read from jwt claims that looks something like this: { email: "[email protected]", role: "admin", location: "us" }. I want to check if that user has access rights to a specific path (which is provided as input, same as the user). So for example i want to give access rights to /admin/us if user.role == admin and user.location == us. I've created an example on the rego playground and it is working fine as long as the user has exactly the same claims as written in the policy, but fails if the user has any additional claims:

package play

default allow = false

allow {
    some p
    policy := data.policies[p]
    policy.request_path == input.request_path
    
    # check if all input.user[x] matches to a policy
    policy.user == input.user # works only if objects have the same keys and values
}

I was thinking I could use intersection to get the matching keys and compare that to the policy definition like this:

# check if all input.user[x] matches to a policy
count(intersection(input.user[data.user])) == count(policy.user)

but this isn't working, most likely because the syntax isn't correct.

I also tried to use comprehensions to filter out the keys from user, then comparing that with a full equals against all policies but couldn't get that to work either.

Could someone please push me in the right direction or provide some learning materials for opa/rego (the officials docs are a bit lacking).

Here's the full example on the rego playground: https://play.openpolicyagent.org/p/ijtOjxXRKk

Upvotes: 1

Views: 529

Answers (2)

Alen
Alen

Reputation: 63

I have found a way to do it using object.union():

patchedpolicy := object.union(input.user, policy.user)
patchedpolicy == input.user

https://play.openpolicyagent.org/p/7LKqnCz7Wr

It essentially copies all the missing keys from user to policy and then compares that to the input.

However, this won't work when the user doesn't have a key that was defined in a policy at all, something like:

# this will evaluate to true even though it is not desired
user = { "email": "[email protected]", location: "us" }
policy = { "path": "/admin", user: { role: "admin" }

This can be fixed with an additional rule to check the input.user keys against the patched policy:

some k
input.user[k] == patchedpolicy[k]

https://play.openpolicyagent.org/p/muHCb2TBv3

This last link works like a charm and passes all my tests but I feel like this can be much simpler. I'd be open to seeing simpler or better solutions.

Upvotes: 2

Devoops
Devoops

Reputation: 2315

What you've encountered here is Rego's lack of "for all", or "universal quantification". See the docs on the topic here.

As you have noted already there are still quite a few ways of doing this. One would be to use negation in a helper rule (i.e. some key/value in policy.user is not in input.user). Another one would be to use a comprehension and compare the count of all matches to the count of the required attributes:

package play

default allow = false

allow {
    some p
    policy := data.policies[p]
    policy.request_path == input.request_path
    
    required := count(policy.user)
    matches  := count([v |  v := policy.user[k]; v == input.user[k]])
    
    required == matches
}

Future versions of OPA are likely going to make this easier.

Upvotes: 1

Related Questions