← Back to team overview

nova team mailing list archive

Thoughts on RBAC implementation

 

Hi all (especially Vish and Devin)!

I've been going through the code in /nova/auth/* and have a few
comments/suggestions on the implementation of authorization (not
authentication) in Nova.  Love to get some feedback from y'all.

Currently, Nova's authorization is handled via role-based access
control (RBAC).  Nothing wrong with this at all; it's very common and
well-understood.

However, the current RBAC implementation, while quite elegant, isn't
very flexible and is not configurable because:

1) All the roles are hard-coded strings (netadmin, projectmanager, etc..)
2) All the permissions are hard-coded using the decorators in /nova/rbac.py.

As an example, here is the cloud controller's get_console_output() method:

    @rbac.allow('projectmanager', 'sysadmin')
    def get_console_output(self, context, instance_id, **kwargs):
        # instance_id is passed in as a list of instances
        instance = self._get_instance(context, instance_id[0])
        return rpc.call('%s.%s' % (FLAGS.compute_topic, instance['node_name']),
            {"method": "get_console_output",
             "args" : {"instance_id": instance_id[0]}})

The rbac.allow decorator establishes if the request context's user is
in either the "projectmanager" or "sysadmin" roles.  This decorator
pattern is very elegant and declarative.

Unfortunately, what this implementation means is that a user
installing Nova has no say in:

1) The names and number of roles
2) The access permissions for the hard-coded roles

without modifying the Nova source code, which of course isn't
particularly friendly! :)

I propose changing the authorization code to allow a flexible RBAC
configuration file (possibly XML?) that would allow installing users
to implement their own security groups and permissions.  Of course,
Nova would ship with a configuration file that matches the current
hard-coded names and permissions...

Consider this possible configuration file:

?xml version="1.0"?>
<rbac>
<roles>
  <role id="all" desc="Anyone" />
  <role id="projectmanager" desc="Project Manager" />
  <role id="sysadmin" desc="Sys Admin" />
  <role id="netadmin" desc="Network Admin" />
  ....
</roles>
<resourcegroups>
  <resourcegroup id="cloud">
    <resource id="create_key_pair" allow="all" />
    <resource id="delete_key_pair" allow="all" />
    <resource id="describe_security_groups" allow="all" />
    <resource id="create_security_group" allow="netadmin" />
    <resource id="delete_security_group" allow="netadmin" />
  ...
  </resourcegroup>
</resources>
</rbac>

On controller initialization, the controller would call some interface
that would parse this configuration file into an in-memory
representation.  Imagine something like this:

// in /nova/auth/acl.py
class ACL:
  def __init__(self):
    # Read config file and populate a dict of roles and resources...
  def allowed_roles(self, resource_id):
    # Takes a resource id and returns a set of allowed roles...

The elegant decorator pattern could, of course, still be used, but in
a slightly different manner.  Something like this might work:

class CloudController(object):
  ...
  @rbac.check
  def create_security_group(self, context, group_name, **kwargs):
     ...

// in /nova/auth/rbac.py:
from nova.auth import acl

acl = acl.ACL()

def check():
    def wrap(f):
        def wrapped_f(self, context, *args, **kwargs):
            if context.user.is_superuser():
                return f(self, context, *args, **kwargs)
            f_name = f.__name__
            roles = acl.get_allowed_roles(f_name)
            for role in roles:
                if __matches_role(context, role):
                    return f(self, context, *args, **kwargs)
            raise exception.NotAuthorized()
        return wrapped_f
    return wrap

def __matches_role(context, role):
    if role == 'all':
        return True
    if role == 'none':
        return False
    return context.project.has_role(context.user.id, role)

Thoughts?

-jay



Follow ups