Policy Enforcement in OpenStack

How can I delegate the ability to delegate?

Keystone’s Roles are the primary indicator of authority in an Open Stack system; the roles a user has determine what operations they can perform. The primary function of Keystone is to map a user to a role in a project. In a system with millions of users, one person, or even a small subset of people, cannot be responsible for assigning all roles to all people. I want to be able to delegate the authority to assign people to roles.

The following article walks through the process of assigning roles to users, and adjusting policy to perform more specific checks in an Open Stack service. To work through this example, all you will need is a working Keystone server.

Third of three articles: Examples. More Examples.

So I’m going to create a role called project_manager that, when assigned to a user in a Keystone project, can do just that. I don’t want them to be able to assign to any project, however. In order to assign to a project, the user needs the project_manager role in that same project? In order words:

A user with the role ‘project_manager’ in the project named ‘demo’ can assign other users to roles in the project named ‘demo.’

Lets see how this works. First of all, we need to find our project. I’ll start by working with the admin_token as that puts Keystone into God-Mode (in video game terms) so I can affect any change. Since the output is going to be long, I’ll use a tool called jq to match for patterns in the output:

To find the demo project,

export TOKEN=freeipa4all
curl  -H"X-Auth-Token:$TOKEN" http://localhost:35357/v3/projects |  jq '.projects[] | {id, name}  '

Which gives me:

{
  "name": "invisible_to_admin",
  "id": "26954cb53fb341f183e669408da4c4a9"
}
{
  "name": "Demonstration",
  "id": "51ad0a84075a4edab7723b0069212d38"
}
{
  "name": "service",
  "id": "9d707f40f66644329115417f33744bb3"
}
{
  "name": "admin",
  "id": "e0a4094cb09a4e528139d076244d0bde"
}
{
  "name": "demo",
  "id": "e15bab932d9349f7b2cbe6f1ae62cc8c"
}

To list all roles for all users in the demo project use the query tool for role_assignments.

curl  -H"X-Auth-Token:$TOKEN" http://localhost:35357/v3/role_assignments?scope.project.id=e15bab932d9349f7b2cbe6f1ae62cc8c  |  jq '.role_assignments[] | {user, role} '
{
  "role": {                                                                                                          
    "id": "a350468e26874be6bf4e528a5e75af28"
  },                                                                                                                 
  "user": {                                                                                                          
    "id": "35bbda9f4b8f4fb0b4bfce2097f8618c"
  }
}
{
  "role": {
    "id": "4525a8e3f7ae465bab98c37f59abea8d"
  },
  "user": {
    "id": "35bbda9f4b8f4fb0b4bfce2097f8618c"
  }
}
{
  "role": {
    "id": "2ff650cc0120400db42cc39bf9bf9c32"
  },
  "user": {
    "id": "77f5d1f4bf6e4a2ca7abad04d4c40dca"
  }
}
{
  "role": {
    "id": "9fe2ff9ee4384b1894a90878d3e92bab"
  },
  "user": {
    "id": "d36f803edcc74fae99428efe696c431d"
  }
}

Say you know the name of a role, but not the id. jq can help. For example, to find the id for the role usermanager

curl -s  -H"X-Auth-Token:$TOKEN" http://localhost:35357/v3/roles | jq '.roles[] | select( contains({name: "usermanager"})) | {id}[]  '

Gets me

"03e35fc0723e443d9adb9dee9e5cd1e5"

Note that I use the -s switch to remove any reporting, and the closing square brackets to treat it as an array element, dropping the curly braces.

I want to add assign the role usermanage to user who’s name is ayoung in he project Demonstration to

export USERMANAGER_ID=$(curl -s  -H"X-Auth-Token:$TOKEN" http://localhost:35357/v3/roles | jq '.roles[] | select( contains({name: "usermanager"})) | {id}[]  '  | sed 's!\"!!g' )
export AYOUNG_ID=$( curl -s  -H"X-Auth-Token:$TOKEN" -H "Content-type: application/json" http://localhost:35357/v3/users | jq '.users[] | select( contains ({name: "ayoung"})) | {id}[]  '| sed 's!\"!!g' )
export DEMONSTRATION_ID=$( curl -s  -H"X-Auth-Token:$TOKEN" -H "Content-type: application/json" http://localhost:35357/v3/projects | jq '.projects[] | select( contains({name: "Demonstration"})) | {id}[]  '     | sed 's!\"!!g'  )

Sorry about the sed. I am sure there is some way to drop the quotes, but I got impatient.

Now to assign that role to the user on the specified project:

 curl  -X PUT  -H"X-Auth-Token:$TOKEN" -H "Content-type: application/json" http://localhost:35357/v3/projects/$DEMONSTRATION_ID/users/$AYOUNG_ID/roles/$USERMANAGER_ID

Which provides no output, but we can check with the equivalent GET

 curl  -H"X-Auth-Token:$TOKEN" -H "Content-type: application/json" http://localhost:35357/v3/projects/$DEMONSTRATION_ID/users/$AYOUNG_ID/roles | jq '.'
{
  "roles": [
    {
      "name": "usermanager",
      "links": {
        "self": "http://127.0.0.1:5000/v3/roles/03e35fc0723e443d9adb9dee9e5cd1e5"
      },
      "id": "03e35fc0723e443d9adb9dee9e5cd1e5"
    }
  ],
  "links": {
    "next": null,
    "previous": null,
    "self": "http://127.0.0.1:5000/v3/projects/51ad0a84075a4edab7723b0069212d38/users/d36f803edcc74fae99428efe696c431d/roles"
  }
}

Now, using the token request from a previous exercise, I become the ayoung user:

export TOKEN=`curl -si -d @token-request.json -H "Content-type: application/json" http://localhost:35357/v3/auth/tokens | awk '/X-Subject-Token/ {print $2}'`

and executing that same command:

curl  -H"X-Auth-Token:$TOKEN" -H "Content-type: application/json" http://localhost:35357/v3/projects/$DEMONSTRATION_ID/users/$AYOUNG_ID/roles | jq '.'

Fails:

{
  "error": {
    "title": "Forbidden",
    "code": 403,
    "message": "You are not authorized to perform the requested action, identity:list_grants."
  }
}

Each application has a policy file that manages the rules that provide access to the APIs. For Keystone, you can usually find it in /etc/keystone/policy.json. Here is the sample that ships with Keystone. The identity:list_grants API is controlled by the rule:

"identity:list_grants": "rule:admin_required"

and we see the definition of “admin_required” at the top of the file:

"admin_required": "role:admin or is_admin:1",

So the user either needs the role:admin or they have to have the admin_token set (that last part is majik.)

Note that I can perform:

curl  -H"X-Auth-Token:$TOKEN" -H "Content-type: application/json" http://localhost:35357/v3/users/$AYOUNG_ID/projects | jq '.'

To list my roles, as that is protected by the rule:

"identity:list_user_projects": "rule:admin_or_owner",

Which is defined as

"owner" : "user_id:%(user_id)s",
"admin_or_owner": "rule:admin_required or rule:owner",

We could probably apply this rule to list_grants and get it to work, since it is just going to match the user id. However, we want to match the role assignment. So I change the rule to:

 "identity:list_grants": "role:usermanager",

Another gotcha: when I request a token, I need to request a token with that role in it; I only have that role on the project, so I use token-request-demonstration.json:

 diff token-request.json token-request-demostration.json 
16c16
<                 "id": "e15bab932d9349f7b2cbe6f1ae62cc8c"
---
>                 "id": "51ad0a84075a4edab7723b0069212d38"

THe UUID that starts with 51 was found above, the id for the Demonstration project.

And I can perform this operation. Ideally, I would also check the project id. I can do that:

"identity:list_grants": "role:usermanager and project_id:%(project_id)s",

Which works:

$ curl  -H"X-Auth-Token:$TOKEN" -H "Content-type: application/json" http://localhost:35357/v3/projects/$DEMONSTRATION_ID/users/$AYOUNG_ID/roles | jq '.'
{
  "roles": [
    {
      "name": "usermanager",
      "links": {
        "self": "http://127.0.0.1:5000/v3/roles/03e35fc0723e443d9adb9dee9e5cd1e5"
      },
      "id": "03e35fc0723e443d9adb9dee9e5cd1e5"
    }
  ],
  "links": {
    "next": null,
    "previous": null,
    "self": "http://127.0.0.1:5000/v3/projects/51ad0a84075a4edab7723b0069212d38/users/d36f803edcc74fae99428efe696c431d/roles"
  }
}

Which means that I shouldn’t be able to execute that same command on a different project:

$ curl  -H"X-Auth-Token:$TOKEN" -H "Content-type: application/json" http://localhost:35357/v3/projects/e0a4094cb09a4e528139d076244d0bde/users/$AYOUNG_ID/roles | jq '.'
{
  "error": {
    "title": "Forbidden",
    "code": 403,
    "message": "You are not authorized to perform the requested action, identity:list_grants."
  }
}

Note that I cannot yet assign a role to someone else in this project:

$ curl  -X PUT  -H"X-Auth-Token:$TOKEN" -H "Content-type: application/json" http://localhost:35357/v3/projects/$DEMONSTRATION_ID/users/$AYOUNG_ID/roles/1b6b831bc1cf484e8050f0514d9b0ee2
{"error": {"message": "You are not authorized to perform the requested action, identity:create_grant.", "code": 403, "title": "Forbidden"}}

However, changing the rule in the policy file to:

"identity:create_grant": "role:usermanager and project_id:%(project_id)s",

restart the keystone server and I can now add the role.

$ curl  -X PUT  -H"X-Auth-Token:$TOKEN" -H "Content-type: application/json" http://localhost:35357/v3/projects/$DEMONSTRATION_ID/users/$AYOUNG_ID/roles/1b6b831bc1cf484e8050f0514d9b0ee2
$ curl   -H"X-Auth-Token:$TOKEN" -H "Content-type: application/json" http://localhost:35357/v3/projects/$DEMONSTRATION_ID/users/$AYOUNG_ID/roles | jq '.'
{
  "roles": [
    {
      "name": "usermanager",
      "links": {
        "self": "http://127.0.0.1:5000/v3/roles/03e35fc0723e443d9adb9dee9e5cd1e5"
      },
      "id": "03e35fc0723e443d9adb9dee9e5cd1e5"
    },
    {
      "name": "testRole",
      "links": {
        "self": "http://127.0.0.1:5000/v3/roles/1b6b831bc1cf484e8050f0514d9b0ee2"
      },
      "id": "1b6b831bc1cf484e8050f0514d9b0ee2"
    }
  ],
  "links": {
    "next": null,
    "previous": null,
    "self": "http://127.0.0.1:5000/v3/projects/51ad0a84075a4edab7723b0069212d38/users/d36f803edcc74fae99428efe696c431d/roles"
  }
}

This should give you a sense of how policy enforces the Role Based access control, and how to write your own policy, as well as define your own rules.

UPDATE:

Cleaned up some of the examples…did this late at night and made a few mistakes in transcription from my Bash prompt. let me know if you find more.

6 thoughts on “Policy Enforcement in OpenStack

  1. In this example, what will stop anyone with the “usermanager” role from granting someone the “admin” role on the tenant? The problem is that the admin role in any tenant still makes you a global admin in other components like Nova.

  2. Marc, policy engine do allow us to enforce rules based on the values that comes as part of the API call
    “identity:list_grants”: “role:usermanager and project_id:%(project_id)s” and not ‘admin’:%(target.role.name)s”.

    These are known as contextual attributes. These are the attributes of objects referenced (or targeted) by an API call; i.e. it can be any object whose id is present in the api call. For e.g, if you assign a role on a project to a user, all attributes related to the role, the project and the user are available to the policy engine, through this target keyword.

  3. Helpful hint:
    One thing I wondered was how to write a rule that made this call (which list all roles for all users in a specific project):
    http://localhost:35357/v3/role_assignments?scope.project.id=e15bab932d9349f7b2cbe6f1ae62cc8c
    be allowed by for global admins or user managers that in the specified project.

    This is the answer:
    “identity:list_role_assignments”: “rule:admin_required or (role:usermanager and project_id:%(scope.project.id)s)”

  4. Is there any documentation on what contextual attributes different rules have? Or for that matter any documentation on the syntax of policy.json files in general?

  5. I would like to restrict flaoting ip creation(all floating ip operations) by any one in openstack. is it possible through policy.json file ?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.