OpenStack Essex Nova Policy

2013/03/28 openstack

# License: (CC 3.0) BY-NC-SA

Start Point

if a request is authenticated by keystone, it still need to be checked via policy. so when i read the network extension, i meet the call of authorize(context), the authorize is a function pointer generated by:

extensions.extension_authorizer('compute', 'networks')

the extensions is imported via from nova.api.openstack import extensions, the prototype of entension_authorizer is:

def extension_authorizer(api_name, extension_name):

it just return a function pointer:

def authorize(context, target=None):

the authorize will invoke nova.policy.enforce(context, action, target), in this case, action is “compute_extension:networks”, target is {“project_id”: context.project_id, “user_id”: context.user_id}. in the nova.policy.enforce(), it will read policy file first via

utils.read_cached_file(_POLICY_PATH, _POLICY_CACHE,
                       reload_func=_set_brain)

and generate a tuple of match_list, which is (“rule:compute_extension:networks”,), and a dict of credentials which is context.to_dict(), then it invoke policy.enforce(match_list, target, credentials). the policy is imported by from nova.common import policy. the prototype of enforce is:

def enforce(match_list, target_dict, credentials_dict):

it will instance class Brain(object) and invoke it check method:

def check(self, match_list, target_dict, cred_dict):

since the match_list is in the format of “rule:xx”, it will invoke _check_rule(“compute_extension:networks”, target_dict, cred_dict), the _check_rule just read its rules list, if not exist such rule and default_rule is not None, use default_rule, then invoke self.check(new_match_list, target_dict, cred_dict). the new_match_list is the value of match rule in the self.rules[]. Note that, the _check_role() just check if the match role is in the cred_dict[‘roles’]. while the _check_generic() will use information from target_dict to complete match and compare the key’s value with cred_dict.

Then i read the /etc/nova/policy.json, it is a json file of a large dict. for instance, the compute_extension:networks is:

"compute_extension:networks": [["rule:admin_api"]],

so it need admin_api rule, which is:

"admin_api": [["role:admin"]],

which means it will invoke _check_role() and need cred_dict has roles key and the value must contains admin.

Nova WSGI Request Context

According to the /etc/nova/api-paste.ini, if using keystone, a request to nova osapi_compute will be firstly filtered by faultwrap then authtoken, and then keystonecontext.

The keystonecontext app is nova.api.auth:NovaKeystoneContext, this class is a child of nova.wsgi.Middleware, its call() function will extract attrs of X_USER_ID, X_ROLE, X_TENANT_ID, X_AUTH_TOKEN and an optional X-Forward-For to instance nova.context.RuquestContext(), finally, it assign this instance to req.environ['nova.context'].

the authtoken app is keystone.middleware.auth_token, this file contains a class AuthProtocol(object) which is Auth Middleware that handles authenticating client calls. It will get attrs from paste.ini and global conf, including: delay_auth_decision, auth_host, auth_port, auth_protocol, auth_uri, admin_token, admin_user, admin_password, admin_tenant_name, memcache_servers, token_cache_time and etc.

The main workflow of call is:

self._remove_auth_headers(env)
user_token = self._get_user_token_from_header(env)
token_info = self._validate_user_token(user_token)
user_headers = self._build_user_headers(token_info)
self._add_headers(env, user_headers)
return self.app(env, start_response)

Note that, call use admin_token to validate user_token, if admin_token is not in conf file (global and local), then it will send a request to get admin_token. The _build_user_headers() will:

return {
    'X-Identity-Status': 'Confirmed',
    'X-Tenant-Id': tenant_id,
    'X-Tenant-Name': tenant_name,
    'X-User-Id': user_id,
    'X-User-Name': user_name,
    'X-Roles': roles,
    # Deprecated
    'X-User': user_name,
    'X-Tenant': tenant_name,
    'X-Role': roles,
}

After other things the authtoken app has done, the req is passed to nova.api.openstack.compute:APIRouter. It is inherited from nova.api.openstack.APIRouter, which is inherited from nova.wsgi.Router. The Router’s call just return a middleware routes.middleware.RoutesMiddleware to dispatch req with the target controller, note that the middleware will extract target project_id from url.

In nova.api.openstack.wsgi.py, class Resource(wsgi.Application)’s call method will do the following things:

project_id = action_args.pop("project_id", None)
context = request.environ.get('nova.context')
if (context and project_id and (project_id != context.project_id)):
    msg = _("Malformed request url")
    return Fault(webob.exc.HTTPBadRequest(explanation=msg))

So, if you want to update a target project’s status, you need that project’s token, so the keystone will generate header of that project and NovaKeystoneContext will generate context of that project and this check can be passed.

Search

    Table of Contents