# Copyright (c) 2011 OpenStack, LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Policy Engine For Nova""" import datetime import httplib import json import logging import os import stat import time import urllib import webob.exc import os.path import traceback from oslo_config import cfg import os.path from nova import exception from nova.openstack.common import policy from nova import utils from oslo_serialization import jsonutils from oslo_utils import timeutils LOG = logging.getLogger(__name__) opts = [ cfg.StrOpt('auth_admin_prefix', default=''), cfg.StrOpt('keystone_auth_host', default='127.0.0.1'), cfg.IntOpt('keystone_auth_port', default=35357), cfg.StrOpt('sios_auth_host', default='127.0.0.1'), cfg.IntOpt('sios_auth_port', default=5253), cfg.StrOpt('auth_protocol', default='http'), cfg.StrOpt('auth_version', default=None), cfg.BoolOpt('delay_auth_decision', default=False), cfg.BoolOpt('http_connect_timeout', default=None), cfg.StrOpt('http_handler', default=None), cfg.StrOpt('admin_token', secret=True), cfg.StrOpt('admin_user'), cfg.StrOpt('admin_password', secret=True), cfg.StrOpt('admin_tenant_name', default='admin'), cfg.StrOpt('certfile'), cfg.StrOpt('keyfile'), cfg.IntOpt('token_cache_time', default=300), cfg.StrOpt('memcache_security_strategy', default=None), cfg.StrOpt('memcache_secret_key', default=None, secret=True) ] CONF = cfg.CONF CONF.register_opts(opts, group='authtoken') _ENFORCER = None sios_auth_host = CONF.authtoken['sios_auth_host'] sios_auth_port = CONF.authtoken['sios_auth_port'] def reset(): global _ENFORCER if _ENFORCER: _ENFORCER.clear() _ENFORCER = None def init(policy_file=None, rules=None, default_rule=None, use_conf=True): """Init an Enforcer class. :param policy_file: Custom policy file to use, if none is specified, `CONF.policy_file` will be used. :param rules: Default dictionary / Rules to use. It will be considered just in the first instantiation. :param default_rule: Default rule to use, CONF.default_rule will be used if none is specified. :param use_conf: Whether to load rules from config file. """ global _ENFORCER if not _ENFORCER: _ENFORCER = policy.Enforcer(policy_file=policy_file, rules=rules, default_rule=default_rule, use_conf=use_conf) def set_rules(rules, overwrite=True, use_conf=False): """Set rules based on the provided dict of rules. :param rules: New rules to use. It should be an instance of dict. :param overwrite: Whether to overwrite current rules or update them with the new rules. :param use_conf: Whether to reload rules from config file. """ init(use_conf=False) _ENFORCER.set_rules(rules, overwrite, use_conf) def get_rules(): if _ENFORCER: return _ENFORCER.rules def check_is_admin(context): """ Whether or not roles contains 'admin' role according to policy setting. """ init() #the target is user-self credentials = context.to_dict() target = credentials return _ENFORCER.enforce('context_is_admin', target, credentials) @policy.register('is_admin') class IsAdminCheck(policy.Check): """An explicit check for is_admin.""" def __init__(self, kind, match): """Initialize the check.""" self.expected = (match.lower() == 'true') super(IsAdminCheck, self).__init__(kind, str(self.expected)) def __call__(self, target, creds): """Determine whether is_admin matches the requested value.""" return creds['is_admin'] == self.expected def enforce(context, action, target, do_raise=True): """Verifies that the action is valid on the target in this context. :param context: Nova request context :param action: String representing the action to be checked :param object: Dictionary representing the object of the action. :raises: `nova.common.exception.PolicyNotAuthorized` :returns: A non-False value if access is allowed. """ headers = {'X-Auth-Token': context.auth_token, 'X-Action': action, 'X-Target': target} req = RESTConnect() response, data = req._json_request(sios_auth_host, sios_auth_port, 'POST', '/v1/pdp/enforce_nova', additional_headers=headers) if (data == False): raise exception.PolicyNotAuthorized else: return data class RESTConnect(object): def __init__(self): # where to find the auth service (we use this to validate tokens) self.keystone_auth_host = self._conf_get('keystone_auth_host') self.keystone_auth_port = int(self._conf_get('keystone_auth_port')) self.sios_auth_host = self._conf_get('sios_auth_host') self.sios_auth_port = int(self._conf_get('sios_auth_port')) self.auth_protocol = self._conf_get('auth_protocol') if not self._conf_get('http_handler'): if self.auth_protocol == 'http': self.http_client_class = httplib.HTTPConnection else: self.http_client_class = httplib.HTTPSConnection else: # Really only used for unit testing, since we need to # have a fake handler set up before we issue an http # request to get the list of versions supported by the # server at the end of this initialization self.http_client_class = self._conf_get('http_handler') self.auth_admin_prefix = self._conf_get('auth_admin_prefix') # SSL self.cert_file = self._conf_get('certfile') self.key_file = self._conf_get('keyfile') # Credentials used to verify this component with the Auth service since # validating tokens is a privileged call self.admin_token = self._conf_get('admin_token') self.admin_token_expiry = None self.admin_user = self._conf_get('admin_user') self.admin_password = self._conf_get('admin_password') self.admin_tenant_name = self._conf_get('admin_tenant_name') http_connect_timeout_cfg = self._conf_get('http_connect_timeout') self.http_connect_timeout = (http_connect_timeout_cfg and int(http_connect_timeout_cfg)) self.auth_version = None self.admin_token=None self.admin_user='admin' self.admin_password='admin' self.admin_tenant_name='admin' self.admin_token_expiry = None self.key_file = None self.cert_file = None if self.auth_protocol == 'http': self.http_client_class = httplib.HTTPConnection else: self.http_client_class = httplib.HTTPSConnection def _conf_get(self, name): return CONF.authtoken[name] def _request_admin_token(self): """Retrieve new token as admin user from keystone. :return token id upon success :raises ServerError when unable to communicate with keystone Irrespective of the auth version we are going to use for the user token, for simplicity we always use a v2 admin token to validate the user token. """ params = { 'auth': { 'passwordCredentials': { 'username': self.admin_user, 'password': self.admin_password, }, 'tenantName': self.admin_tenant_name, } } response, data = self._json_request(self.keystone_auth_host, self.keystone_auth_port, 'POST', '/v2.0/tokens', body=params) try: token = data['access']['token']['id'] expiry = data['access']['token']['expires'] assert token assert expiry datetime_expiry = timeutils.parse_isotime(expiry) return (token, timeutils.normalize_time(datetime_expiry)) except (AssertionError, KeyError): LOG.warn( "Unexpected response from keystone service: %s", data) raise ServiceError('invalid json response') except (ValueError): LOG.warn( "Unable to parse expiration time from token: %s", data) raise ServiceError('invalid json response') def get_admin_token(self): """Return admin token, possibly fetching a new one. if self.admin_token_expiry is set from fetching an admin token, check it for expiration, and request a new token is the existing token is about to expire. :return admin token id :raise ServiceError when unable to retrieve token from keystone """ if self.admin_token_expiry: if will_expire_soon(self.admin_token_expiry): self.admin_token = None if not self.admin_token: (self.admin_token, self.admin_token_expiry) = self._request_admin_token() return self.admin_token def _get_http_connection(self, auth_host, auth_port): if self.auth_protocol == 'http': return self.http_client_class(auth_host, auth_port, timeout=self.http_connect_timeout) else: return self.http_client_class(auth_host, auth_port, self.key_file, self.cert_file, timeout=self.http_connect_timeout) def _http_request(self, auth_host, auth_port, method, path, **kwargs): """HTTP request helper used to make unspecified content type requests. :param method: http method :param path: relative request url :return (http response object, response body) :raise ServerError when unable to communicate with keystone """ conn = self._get_http_connection(auth_host, auth_port) RETRIES = 3 retry = 0 while True: try: conn.request(method, path, **kwargs) response = conn.getresponse() body = response.read() break except Exception as e: if retry == RETRIES: LOG.error('HTTP connection exception: %s' % e) raise ServiceError('Unable to communicate with keystone') # NOTE(vish): sleep 0.5, 1, 2 LOG.warn('Retrying on HTTP connection exception: %s' % e) time.sleep(2.0 ** retry / 2) retry += 1 finally: conn.close() return response, body def _json_request(self, auth_host, auth_port, method, path, body=None, additional_headers=None): """HTTP request helper used to make json requests. :param method: http method :param path: relative request url :param body: dict to encode to json as request body. Optional. :param additional_headers: dict of additional headers to send with http request. Optional. :return (http response object, response body parsed as json) :raise ServerError when unable to communicate with keystone """ kwargs = { 'headers': { 'Content-type': 'application/json', 'Accept': 'application/json', }, } if additional_headers: kwargs['headers'].update(additional_headers) if body: kwargs['body'] = jsonutils.dumps(body) path = self.auth_admin_prefix + path response, body = self._http_request(auth_host, auth_port, method, path, **kwargs) try: data = jsonutils.loads(body) except ValueError: LOG.debug('Keystone did not return json-encoded body') data = {} return response, data