diff options
author | WuKong <rebirthmonkey@gmail.com> | 2015-07-01 08:54:55 +0200 |
---|---|---|
committer | WuKong <rebirthmonkey@gmail.com> | 2015-07-01 08:54:55 +0200 |
commit | 03bf0c32a0c656d4b91bebedc87a005e6d7563bb (patch) | |
tree | 7ab486ea98c8255bd28b345e9fd5b54d1b31c802 /keystonemiddleware-moon/keystonemiddleware/audit.py | |
parent | 53d12675bc07feb552492df2d01fcd298167c363 (diff) |
migrate openstack hook to opnfv
Change-Id: I1e828dae38820fdff93966e57691b344af01140f
Signed-off-by: WuKong <rebirthmonkey@gmail.com>
Diffstat (limited to 'keystonemiddleware-moon/keystonemiddleware/audit.py')
-rw-r--r-- | keystonemiddleware-moon/keystonemiddleware/audit.py | 430 |
1 files changed, 430 insertions, 0 deletions
diff --git a/keystonemiddleware-moon/keystonemiddleware/audit.py b/keystonemiddleware-moon/keystonemiddleware/audit.py new file mode 100644 index 00000000..f44da80d --- /dev/null +++ b/keystonemiddleware-moon/keystonemiddleware/audit.py @@ -0,0 +1,430 @@ +# +# 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. + +""" +Build open standard audit information based on incoming requests + +AuditMiddleware filter should be placed after keystonemiddleware.auth_token +in the pipeline so that it can utilise the information the Identity server +provides. +""" + +import ast +import collections +import functools +import logging +import os.path +import re +import sys + +from oslo_config import cfg +from oslo_context import context +try: + import oslo.messaging + messaging = True +except ImportError: + messaging = False +from pycadf import cadftaxonomy as taxonomy +from pycadf import cadftype +from pycadf import credential +from pycadf import endpoint +from pycadf import eventfactory as factory +from pycadf import host +from pycadf import identifier +from pycadf import reason +from pycadf import reporterstep +from pycadf import resource +from pycadf import tag +from pycadf import timestamp +from six.moves import configparser +from six.moves.urllib import parse as urlparse +import webob.dec + +from keystonemiddleware.i18n import _LE, _LI + + +_LOG = None + + +def _log_and_ignore_error(fn): + @functools.wraps(fn) + def wrapper(*args, **kwargs): + try: + return fn(*args, **kwargs) + except Exception as e: + _LOG.exception(_LE('An exception occurred processing ' + 'the API call: %s '), e) + return wrapper + + +Service = collections.namedtuple('Service', + ['id', 'name', 'type', 'admin_endp', + 'public_endp', 'private_endp']) + + +AuditMap = collections.namedtuple('AuditMap', + ['path_kw', + 'custom_actions', + 'service_endpoints', + 'default_target_endpoint_type']) + + +class OpenStackAuditApi(object): + + def __init__(self, cfg_file): + """Configure to recognize and map known api paths.""" + path_kw = {} + custom_actions = {} + endpoints = {} + default_target_endpoint_type = None + + if cfg_file: + try: + map_conf = configparser.SafeConfigParser() + map_conf.readfp(open(cfg_file)) + + try: + default_target_endpoint_type = map_conf.get( + 'DEFAULT', 'target_endpoint_type') + except configparser.NoOptionError: + pass + + try: + custom_actions = dict(map_conf.items('custom_actions')) + except configparser.Error: + pass + + try: + path_kw = dict(map_conf.items('path_keywords')) + except configparser.Error: + pass + + try: + endpoints = dict(map_conf.items('service_endpoints')) + except configparser.Error: + pass + except configparser.ParsingError as err: + raise PycadfAuditApiConfigError( + 'Error parsing audit map file: %s' % err) + self._MAP = AuditMap( + path_kw=path_kw, custom_actions=custom_actions, + service_endpoints=endpoints, + default_target_endpoint_type=default_target_endpoint_type) + + @staticmethod + def _clean_path(value): + """Clean path if path has json suffix.""" + return value[:-5] if value.endswith('.json') else value + + def get_action(self, req): + """Take a given Request, parse url path to calculate action type. + + Depending on req.method: + if POST: path ends with 'action', read the body and use as action; + path ends with known custom_action, take action from config; + request ends with known path, assume is create action; + request ends with unknown path, assume is update action. + if GET: request ends with known path, assume is list action; + request ends with unknown path, assume is read action. + if PUT, assume update action. + if DELETE, assume delete action. + if HEAD, assume read action. + + """ + path = req.path[:-1] if req.path.endswith('/') else req.path + url_ending = self._clean_path(path[path.rfind('/') + 1:]) + method = req.method + + if url_ending + '/' + method.lower() in self._MAP.custom_actions: + action = self._MAP.custom_actions[url_ending + '/' + + method.lower()] + elif url_ending in self._MAP.custom_actions: + action = self._MAP.custom_actions[url_ending] + elif method == 'POST': + if url_ending == 'action': + try: + if req.json: + body_action = list(req.json.keys())[0] + action = taxonomy.ACTION_UPDATE + '/' + body_action + else: + action = taxonomy.ACTION_CREATE + except ValueError: + action = taxonomy.ACTION_CREATE + elif url_ending not in self._MAP.path_kw: + action = taxonomy.ACTION_UPDATE + else: + action = taxonomy.ACTION_CREATE + elif method == 'GET': + if url_ending in self._MAP.path_kw: + action = taxonomy.ACTION_LIST + else: + action = taxonomy.ACTION_READ + elif method == 'PUT' or method == 'PATCH': + action = taxonomy.ACTION_UPDATE + elif method == 'DELETE': + action = taxonomy.ACTION_DELETE + elif method == 'HEAD': + action = taxonomy.ACTION_READ + else: + action = taxonomy.UNKNOWN + + return action + + def _get_service_info(self, endp): + service = Service( + type=self._MAP.service_endpoints.get( + endp['type'], + taxonomy.UNKNOWN), + name=endp['name'], + id=identifier.norm_ns(endp['endpoints'][0].get('id', + endp['name'])), + admin_endp=endpoint.Endpoint( + name='admin', + url=endp['endpoints'][0]['adminURL']), + private_endp=endpoint.Endpoint( + name='private', + url=endp['endpoints'][0]['internalURL']), + public_endp=endpoint.Endpoint( + name='public', + url=endp['endpoints'][0]['publicURL'])) + + return service + + def _build_typeURI(self, req, service_type): + """Build typeURI of target + + Combines service type and corresponding path for greater detail. + """ + type_uri = '' + prev_key = None + for key in re.split('/', req.path): + key = self._clean_path(key) + if key in self._MAP.path_kw: + type_uri += '/' + key + elif prev_key in self._MAP.path_kw: + type_uri += '/' + self._MAP.path_kw[prev_key] + prev_key = key + return service_type + type_uri + + def _build_target(self, req, service): + """Build target resource.""" + target_typeURI = ( + self._build_typeURI(req, service.type) + if service.type != taxonomy.UNKNOWN else service.type) + target = resource.Resource(typeURI=target_typeURI, + id=service.id, name=service.name) + if service.admin_endp: + target.add_address(service.admin_endp) + if service.private_endp: + target.add_address(service.private_endp) + if service.public_endp: + target.add_address(service.public_endp) + return target + + def get_target_resource(self, req): + """Retrieve target information + + If discovery is enabled, target will attempt to retrieve information + from service catalog. If not, the information will be taken from + given config file. + """ + service_info = Service(type=taxonomy.UNKNOWN, name=taxonomy.UNKNOWN, + id=taxonomy.UNKNOWN, admin_endp=None, + private_endp=None, public_endp=None) + try: + catalog = ast.literal_eval( + req.environ['HTTP_X_SERVICE_CATALOG']) + except KeyError: + raise PycadfAuditApiConfigError( + 'Service catalog is missing. ' + 'Cannot discover target information') + + default_endpoint = None + for endp in catalog: + admin_urlparse = urlparse.urlparse( + endp['endpoints'][0]['adminURL']) + public_urlparse = urlparse.urlparse( + endp['endpoints'][0]['publicURL']) + req_url = urlparse.urlparse(req.host_url) + if (req_url.netloc == admin_urlparse.netloc + or req_url.netloc == public_urlparse.netloc): + service_info = self._get_service_info(endp) + break + elif (self._MAP.default_target_endpoint_type and + endp['type'] == self._MAP.default_target_endpoint_type): + default_endpoint = endp + else: + if default_endpoint: + service_info = self._get_service_info(default_endpoint) + return self._build_target(req, service_info) + + +class ClientResource(resource.Resource): + def __init__(self, project_id=None, **kwargs): + super(ClientResource, self).__init__(**kwargs) + if project_id is not None: + self.project_id = project_id + + +class KeystoneCredential(credential.Credential): + def __init__(self, identity_status=None, **kwargs): + super(KeystoneCredential, self).__init__(**kwargs) + if identity_status is not None: + self.identity_status = identity_status + + +class PycadfAuditApiConfigError(Exception): + """Error raised when pyCADF fails to configure correctly.""" + + +class AuditMiddleware(object): + """Create an audit event based on request/response. + + The audit middleware takes in various configuration options such as the + ability to skip audit of certain requests. The full list of options can + be discovered here: + http://docs.openstack.org/developer/keystonemiddleware/audit.html + """ + + @staticmethod + def _get_aliases(proj): + aliases = {} + if proj: + # Aliases to support backward compatibility + aliases = { + '%s.openstack.common.rpc.impl_kombu' % proj: 'rabbit', + '%s.openstack.common.rpc.impl_qpid' % proj: 'qpid', + '%s.openstack.common.rpc.impl_zmq' % proj: 'zmq', + '%s.rpc.impl_kombu' % proj: 'rabbit', + '%s.rpc.impl_qpid' % proj: 'qpid', + '%s.rpc.impl_zmq' % proj: 'zmq', + } + return aliases + + def __init__(self, app, **conf): + self._application = app + global _LOG + _LOG = logging.getLogger(conf.get('log_name', __name__)) + self._service_name = conf.get('service_name') + self._ignore_req_list = [x.upper().strip() for x in + conf.get('ignore_req_list', '').split(',')] + self._cadf_audit = OpenStackAuditApi(conf.get('audit_map_file')) + + transport_aliases = self._get_aliases(cfg.CONF.project) + if messaging: + self._notifier = oslo.messaging.Notifier( + oslo.messaging.get_transport(cfg.CONF, + aliases=transport_aliases), + os.path.basename(sys.argv[0])) + + def _emit_audit(self, context, event_type, payload): + """Emit audit notification + + if oslo.messaging enabled, send notification. if not, log event. + """ + + if messaging: + self._notifier.info(context, event_type, payload) + else: + _LOG.info(_LI('Event type: %(event_type)s, Context: %(context)s, ' + 'Payload: %(payload)s'), {'context': context, + 'event_type': event_type, + 'payload': payload}) + + def _create_event(self, req): + correlation_id = identifier.generate_uuid() + action = self._cadf_audit.get_action(req) + + initiator = ClientResource( + typeURI=taxonomy.ACCOUNT_USER, + id=identifier.norm_ns(str(req.environ['HTTP_X_USER_ID'])), + name=req.environ['HTTP_X_USER_NAME'], + host=host.Host(address=req.client_addr, agent=req.user_agent), + credential=KeystoneCredential( + token=req.environ['HTTP_X_AUTH_TOKEN'], + identity_status=req.environ['HTTP_X_IDENTITY_STATUS']), + project_id=identifier.norm_ns(req.environ['HTTP_X_PROJECT_ID'])) + target = self._cadf_audit.get_target_resource(req) + + event = factory.EventFactory().new_event( + eventType=cadftype.EVENTTYPE_ACTIVITY, + outcome=taxonomy.OUTCOME_PENDING, + action=action, + initiator=initiator, + target=target, + observer=resource.Resource(id='target')) + event.requestPath = req.path_qs + event.add_tag(tag.generate_name_value_tag('correlation_id', + correlation_id)) + # cache model in request to allow tracking of transistive steps. + req.environ['cadf_event'] = event + return event + + @_log_and_ignore_error + def _process_request(self, request): + event = self._create_event(request) + + self._emit_audit(context.get_admin_context().to_dict(), + 'audit.http.request', event.as_dict()) + + @_log_and_ignore_error + def _process_response(self, request, response=None): + # NOTE(gordc): handle case where error processing request + if 'cadf_event' not in request.environ: + self._create_event(request) + event = request.environ['cadf_event'] + + if response: + if response.status_int >= 200 and response.status_int < 400: + result = taxonomy.OUTCOME_SUCCESS + else: + result = taxonomy.OUTCOME_FAILURE + event.reason = reason.Reason( + reasonType='HTTP', reasonCode=str(response.status_int)) + else: + result = taxonomy.UNKNOWN + + event.outcome = result + event.add_reporterstep( + reporterstep.Reporterstep( + role=cadftype.REPORTER_ROLE_MODIFIER, + reporter=resource.Resource(id='target'), + reporterTime=timestamp.get_utc_now())) + + self._emit_audit(context.get_admin_context().to_dict(), + 'audit.http.response', event.as_dict()) + + @webob.dec.wsgify + def __call__(self, req): + if req.method in self._ignore_req_list: + return req.get_response(self._application) + + self._process_request(req) + try: + response = req.get_response(self._application) + except Exception: + self._process_response(req) + raise + else: + self._process_response(req, response) + return response + + +def filter_factory(global_conf, **local_conf): + """Returns a WSGI filter app for use with paste.deploy.""" + conf = global_conf.copy() + conf.update(local_conf) + + def audit_filter(app): + return AuditMiddleware(app, **conf) + return audit_filter |