Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modelling a roles to permissions relationship in Rego #903

Closed
dan6518 opened this issue Aug 27, 2018 · 4 comments
Closed

Modelling a roles to permissions relationship in Rego #903

dan6518 opened this issue Aug 27, 2018 · 4 comments
Labels

Comments

@dan6518
Copy link

dan6518 commented Aug 27, 2018

Hello, I don't know if this is the correct place to ask for usage help, so please let me know if I should post this somewhere else instead.

I have followed the HTTP API authorization guide, and it's along the lines of what I want, but I need a more complex authorization model and I'm having a hard time grokking Rego. My model looks more or less like this:

I have a bunch of users, each with a unique user ID and a list of roles. These roles are valid in some context, for example "user A is a Manager (role) in building B". Each role corresponds to a set of API permissions. The example user A should be able to call both "POST /building/B/openDoors" and "POST /building/B/closeDoors" because he is a manager of building B. However an example user C who is a Tenant of Apartment 123 in building B should only be able to call "POST /building/B/apartment/123/openDoors". A user can also have multiple of these roles, in various contexts, like be a manager for two buildings and a tenant in one.

Additionally I need users to be able to query OPA to get a list of their roles, and a list of their derived permissions based on the roles they have. Example user A should be able to query and get the response that he's "manager of building B", and also a query that gives the list of API endpoints he can access.

The request to OPA only contains the user's ID and the resource the user is trying to access.

So I think I need a list of my API permissions, my roles, and a mapping between them, sort of like this (pseudo-ish code):

openDoors = { "method": "POST", "resource": "openDoors" }
closeDoors = { "method": "POST", "resource": "closeDoors" }

# some sort of ruleset that maps manager to a set of the above values,
# and context of which building/etc it is valid for?

allow {
  # somehow using the result of the above mapping to allow or deny the request
}

And an array of users in my data.json file, which looks something like:

[
  {
    "id": "A",
    "roles": [
      "role": "manager",
      "context": {
         "building": "B"
      }
    ]
  },
  {
    "id": "C",
    "roles": [
      "role": "tenant",
      "context": {
         "building": "B",
         "apartment": "123"
      }
    ]
  }
]

This feels like something that should be possible or even simple to do in Rego, but so far I haven't been able to come up with a solution. That is mostly because of my lack of experience with declarative programming, but also because I have had a hard time finding examples to learn from.

Any help/guidance is appreciated.

@tsandall
Copy link
Member

tsandall commented Aug 27, 2018

@dan6518 there's an example of implementing a role-based access control (RBAC) policy on the OPA website here.

The example is a bit messy, but what it essentially does is:

  1. Finds roles that match the input.user
  2. Finds roles with permissions that match the input.action and input.object

We could refactor the policy to make it slightly clearer.

# Find roles that match user and required permission
allow {
    roles_for_user[r]
    roles_with_permission[r]
}

# Find roles for the input.user
roles_for_user[r] {
    r := user_roles[input.user][_]
}

# Find roles for the input.action and input.object
role_permissions[r] {
    p := role_perms[r][_]
    p.method == input.action
    p.path == input.object
}

You could follow a similar approach in your case. The main difference is that the role is not simply a string (or single value), it's multi-dimensional. For example, having role "manager" is not sufficient, the user must have role "manager" with building "B".

How do you supply the resource being accessed (e.g., is it a path like in your example)?

Regarding the second part of your question, about querying OPA for roles and derived permissions, it should be possible to write a query that finds relevant roles and permissions relatively easily. For example, assuming you have a role-permission mapping like this:

{
  "manager": [
    {"method": "POST", "resource": "closeDoors"},
    {"method": "POST", "resource": "openDoors"}
  ]
}

You can run a query for the roles & permissions that a user has, as follows:

user = data.users[_]
user.id = "bob" # alternatively, provided as input.user_id or similar
role = user.roles[_]
perm = data.permissions[role.role][_]

The result will contain sets of values for user, role, and perm that satisfy the query.

@dan6518
Copy link
Author

dan6518 commented Aug 28, 2018

Thanks for your reponse!

I was able to make a bit of progress when I realized modeling the mapping as part of the data is probably the way to go, like in your example role-permission mapping. I was able to map from user to permissions. I have some more questions though:

Let's say I have two sources for roles for my users. They can either be a property on the user directly, or the user can have a group property from which the user inherits a set of roles. In other words, both of these derivations are valid:

user -> groups -> roles -> permissions
user-> roles -> permissions

In imperative or functional languages I would make the roles->permissions mapping, since it's present in both derivations. I'd concatenate user.roles and user.groups.roles and pass that as the paremeter. Is there an equivalent in Rego to this sort of composition?

Or is the canonical way of doing this something more like

allow {
  # user -> groups -> roles -> permissions
  user = input.user
  groups = data.user[user].groups
  roles = data.groups[groups].roles
  perms = data.roles[roles].permissions
  perms = input.requested_permission
} {
  # user-> roles -> permissions
  user = input.user
  roles = data.user[user].roles
  perms = data.roles[roles].permissions
  perms = input.requested_permission
}

What about when I query for the user's roles (or permissions), how do I concatenate the result sets?

How do you supply the resource being accessed (e.g., is it a path like in your example)?

I call OPA from an AWS lambda authorizer, so I get to inspect the call made from the client and transform it however I want. It can be a path or anything else really. The important point is that it's split into two parts: The resource and the context. For example:

// access all microwaves in building B
"input": {
  "resource": "microwave",
  "context": {
     "building": "B"
  }
}
// access only the microwaves in storage unit 15 belonging to apartment 456 in building B
"input": {
  "resource": "microwave",
  "context": {
    "building": "B",
    "apartment": 456,
    "storage_unit": 15
  }
}

The reason for this is that I want thousands of dynamically created resources, without having to create/specify API permissions for all of them. Someone who has access to /buildings/B/microwaves should also have access to /buildings/B/apartment/456/storage_unit/15/microwaves. Basically if you have the "admin" role in the context of "building B", that implicitly grants you the "admin" role for all sub-contexts of "building B". There's a hierarchical structure to the contexts though, so it's not quite as bad as it sounds. But this is part of my domain's requirements, which I didn't want to bother you with too much so feel free to ignore this part. If I can get the earlier part of my question to work I can probably extrapolate what I need from there.

@tsandall
Copy link
Member

Your allow example is fine, the downside is that it's going to duplicate the logic to check the permissions. You could refactor it a bit like this:

allow {
  roles_for_user[role]
  # check if role has required permisisons
}

roles_for_user[role] {
  # find roles attached directly to user
}

roles_for_user[role] {
  # find roles attached to groups that the user is part of
}

When you create multiple rules with the same name in OPA, we say that the rule is defined incrementally. In the case of roles_for_user above, the rule is defining a set of roles. Each rule adds more elements to the set. The language guide on the website says more about this.

Thanks for providing more detail on the use case. Also, the lambda authorizer integration sounds really cool! This has been on our list to look at for a while.

@tsandall
Copy link
Member

Closing this for the time being. Feel free to reopen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants