Ozznotes

This is a blog with random OpenShift, Kubernetes, OpenStack and Linux related notes so I don't forget things. If you find something inaccurate or that could be fixed, please file a bug report here.

View on GitHub

Back to home

24 September 2018

Oslo Policy Deep Dive (part 1)

by Juan Antonio Osorio Robles

In the upcoming OpenStack Summit in Berlin we have submitted a talk to teach folks about oslo.policy. The name is of the talk is OpenStack Policy 101, and its purpose is:

The purpose of this post is to write a comprehensive set of notes to deliver for the talk, as well as have me review all the material :D. But I hope this is useful for other folks that are interested in this topic as well.

What is oslo.policy - overview

It’s a python library that OpenStack services use in order to enable RBAC (Role Based Access Control) policy. This policy determines which user can access which objects or resources, in which way. The library contains its own implementation of a policy language that the service creator will use in order to create appropriate defaults on what is allowed by each endpoint of the service. Operators can then overwrite these defaults in order to customize the policy for the specific service.

The policy language is based on either yaml or json, however given that these implementations are quite similar, here we’ll focus on only one of these. We assume it’s fairly trivial to use both, since a json-writen policy will also be correctly parsed as yaml.

Where are the policies defined?

Given that each services has different purposes, endpoints, and different needs, each service has its own policy, and the responsibility of defining it.

In the past, it used to be the case that you would find the policy for a given service as a yaml or json file itself. This had the advantage that you would then see the whole policy in a single file.

Recently though, OpenStack moved the default policy to be in-code rather than in a yaml file shipped with the projects. This change was mostly targeted at giving a better experience to operators, with the following reasons:

This doesn’t mean that the usage of a full-fledged policy.yaml is no longer available as folks can still generate these files from the OpenStack project’s codebase with the tooling that was created as part of this work (I’ll tell you how to do this later). So you also don’t need to dig into the project’s code base to get the default policy, just use the tooling and it’ll all be fine :).

How do I write policies?

Whether you’re a service creator or an operator, it is quite useful to know how to write policies if you want proper RBAC to work with your service, or if you want to modify it. So lets give it a go!

Each policy rule is defined as follows:

"< target >": "< rule >"

Simple as that!

The targets could be either aliases or actions. Lets leave aliases for later. Actions, represent API calls or operations. For Keystone, for instance, it could be something like “create user” or “list users”. For Barbican it could be “create secret” or “list secrets”. It is whatever operation your service is capable of doing.

The target (as an action) will typically typically look as follows: secrets:get. In the aforementioned case, that target refers to the “list secrets for a specific project” action. Typically, the service creators define these names, and the only way to know what action name refers to what operation is to either refer to the project’s documentation, or to dig into the project’s code-base.

The “rule” section defines what needs to be fulfilled in order to allow the operation.

Here’s what rules can be:

It is also possible to use operators on these rules. The available operators are the following (in order of precedence:

Lets dig in through each case:

Always true

So, lets say that you want to write a policy where anyone can list the compose instances. Here’s what you can do:

"compute:get_all": ""
"compute:get_all": []
"compute:get_all": "@"

Any of the three aforementioned values will get you the same result, which is to allow anybody to list the compute instances.

Always false

If you want to be very restrictive, and not allow anybody to do such an operation. You use the following:

"compute:get_all": "!"

This will deny the operation for everyone.

Special checks

Role checks

Lets say that you only want to allow users with the role “lister” to list instances. You can do so with the following rule:

"compute:get_all": "role:lister"

These roles tie in directly with Keystone roles, so when using such a policy, you need to make sure that the relevant users have the appropriate roles in Keystone. For some services, this tends to cause confusion. Such as is the case for Barbican. In Barbican, the default policy makes reference to several rules that are non-standard in OpenStack:

So, it is necessary to get your users access to these rules if you want them to have access to Barbican without being admin.

Rule aliases

Remember in the beginning where I mentioned that rule definitions could be either aliases or actions? Well, here are the alises!

In order to re-use rules, it is possible to create rule aliases and subsequently use these aliases in other rules. This comes in handy when your rules start to get longer and you take operators into use. For this example, lets use the “or” operator, and create a rule that allows users with the “admin” role or the “creator” role to list compute instances:

"admin_or_creator": "role:admin or role:creator"
"compute:get_all": "rule:admin_or_creator"

As you can see, the compute:get_all rule is a reference to the admin_or_creator rule that we defined in the line above it. We can even take that rule into use for another target. For instance, to create servers:

"compute:create": "rule:admin_or_creator"

External check

It is also possible to use an external engine in order to evaluate individual policies. The syntax is fairly simple, as one only needs to use the URL of the external decision endpoint.

So, lets say that we have written a service that does this, and we’ll use it to evaluate if a certain user can list the compute instances. We would write the rule as follows:

"compute:create": "http://my-external-service.example.com/path/to/resource"

Or better yet:

"compute:create": "https://my-external-service.example.com/path/to/resource"

The external resource then needs to answer exactly “True”. Any other response is considered negative.

The external resource is passed the same enforcement data as oslo.policy gets: Rule, target and credentials (I’ll talk about these later).

There are also several ways that one can configure the interaction with this external engine, and this is done through oslo.config. In the service configuration and under the oslo_policy section, one can set the following:

Note that it is possible to create custom checks, but we’ll cover this topic in a subsequent blog post.

Comparisons

In certain cases where checking the user’s role isn’t enough, we can also do comparisons between several things. Here’s the available objects we can use:

Constants

If you would like to base your policy decision by comparing a certain attribute to a constant, it’s possible to do so as follows:

"compute:get_all": "<variable>:'xpto2035abc'"
"compute:create": "'myproject':<variable>"

API attributes

We typically derive these from the request’s context. These would normally be:

While most projects have tried to keep these attributes constant, it is important to note that not all of the projects use the exact names. This is because the way these are passed is dependent on how the oslo.policy library is called. There are, however, efforts to standardize this. Hopefully in the near future (as this gets standardized), the available API attributes will be the same ones as what’s available from oslo.context.

Target object attributes

This refers to the objects that the policies are working on.

Lets take barbican as an example. We want to make sure that the incoming user’s project ID matches the secret’s project ID. So, for this, we created the following rule:

"secret_project_match": "project:%(target.secret.project_id)s",

Here project refers to the user’s project ID, while target.secret.project_id refers to the secret that is target of this operation.

It is important to note that how these “targets” are passed is highly project specific, and you would typically need to dig into the project’s code to figure out how these attributes are passed.

Checks recap

The olso.policy code documentation contains a very nice table that sums the aforementioned cases quite nicely:

TYPE SYNTAX
User’s Role role:admin
   
Rules already defined on policy rule:admin_required
   
Against URLs http://my-url.org/check
   
User attributes project_id:%(target.project.id)s
   
Strings - <variable>:'xpto2035abc'
  - 'myproject':<variable>
   
  - project_id:xpto2035abc
Literals - domain_id:20
  - True:%(user.enabled)s

Where do API attributes and target objects come from?

As I mentioned in previous sections, these parameters are dependant on how the library is called, and it varies from project to project. Lets see how this works.

oslo.policy enforces policy using an object called Enforcer. You’ll typically create it like this:

from oslo_config import cfg
CONF = cfg.CONF
enforcer = policy.Enforcer(CONF, policy_file=_POLICY_PATH)

Once you have this Enforcer object created, every time you need policy to be evaluated, you need to call the enforce or [authorize][ authorize-method] methods for that object:

enforce(rule, target, creds, do_raise=False, exc=None, *args, **kwargs)

enforce and authorize take the same arguments.

Lets look at the relevant parameters:

Unfortunately, if you ever need to change the policy and decipher what information is passed as the API attributes and the target, you’ll need to dig into the project’s codebase and look for where the enforce or authorize calls are made for the relevant policy rule you’re looking for.

Barbican example

Lets take Barbican as an example. If we look at the code-base, we can see that barbican enforces policy as part of a common decorator. This decorator ends up calling the _do_enforce_rbac function which, at the time of writing this, looks as follows:

def _do_enforce_rbac(inst, req, action_name, ctx, **kwargs):
    """Enforce RBAC based on 'request' information."""
    if action_name and ctx:

        # Prepare credentials information.
        credentials = {
            'roles': ctx.roles,
            'user': ctx.user,
            'project': ctx.project_id
        }

        # Enforce special case: secret GET decryption
        if 'secret:get' == action_name and not is_json_request_accept(req):
            action_name = 'secret:decrypt'  # Override to perform special rules

        target_name, target_data = inst.get_acl_tuple(req, **kwargs)
        policy_dict = {}
        if target_name and target_data:
            policy_dict['target'] = {target_name: target_data}

        policy_dict.update(kwargs)
        # Enforce access controls.
        if ctx.policy_enforcer:
            ctx.policy_enforcer.enforce(action_name, flatten(policy_dict), credentials, do_raise=True)

Here we can see that the credentials are derived from the oslo.context object; which contains information about the user that’s making the request.

Preferrably, barbican should instead pass the oslo.context object and let oslo.policy derive the needed parameters from that object. Although for this, the default policy will need to be adjusted to match the names: e.g. project_id instead of just project.

Subsequently, depending on the class that calls this, we’ll get the appropriate information about the target object from the get_acl_tuple function.

If we’re dealing with secrets, we can see how the target is filled up:

class SecretController(controllers.ACLMixin):
    """Handles Secret retrieval and deletion requests."""

    def __init__(self, secret):
        LOG.debug('=== Creating SecretController ===')
        self.secret = secret
        self.transport_key_repo = repo.get_transport_key_repository()

    def get_acl_tuple(self, req, **kwargs):
        d = self.get_acl_dict_for_user(req, self.secret.secret_acls)
        d['project_id'] = self.secret.project.external_id
        d['creator_id'] = self.secret.creator_id
        return 'secret', d

Note that the target’s project_id and creator_id is derived from the secret that the user is trying to access. We also query the ACL’s to get extra information about the access that the user might have on the secret.

So, for barbican, the target map will look as follows:

{
    "target": {
        "<entity>": {
            # actual target data
            ...
        }
    }
}

For secrets, it would be:

{
    "target": {
        "secret": {
            "project_id": "<some id>",
            "creator_id": "<some other id>",
            ...
        }
    }
}

I would like to point out once more that the target map structure is highly dependant on the project. So, if you’re dealing with another project, you’ll need to dig into that code to know how the project fills it up. But at least now you know what to look for :) .

Conclusion

Here we learned what oslo.policy is, how to write policies with it, and how to get the relevant information on how the policy is called for specific projects.

In the next blog post, we’ll learn how to do modifications to policies and how to reflect them on a running service.

tags: openstack - policy

Back to home