diff options
Diffstat (limited to 'keystonemiddleware-moon/keystonemiddleware/audit.py')
-rw-r--r-- | keystonemiddleware-moon/keystonemiddleware/audit.py | 449 |
1 files changed, 0 insertions, 449 deletions
diff --git a/keystonemiddleware-moon/keystonemiddleware/audit.py b/keystonemiddleware-moon/keystonemiddleware/audit.py deleted file mode 100644 index e3536092..00000000 --- a/keystonemiddleware-moon/keystonemiddleware/audit.py +++ /dev/null @@ -1,449 +0,0 @@ -# -# 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 -import six -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']) - - -# NOTE(blk-u): Compatibility for Python 2. SafeConfigParser and -# SafeConfigParser.readfp are deprecated in Python 3. Remove this when we drop -# support for Python 2. -if six.PY2: - class _ConfigParser(configparser.SafeConfigParser): - read_file = configparser.SafeConfigParser.readfp -else: - _ConfigParser = configparser.ConfigParser - - -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() - map_conf.read_file(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].get('adminURL', taxonomy.UNKNOWN)), - private_endp=endpoint.Endpoint( - name='private', - url=endp['endpoints'][0].get('internalURL', taxonomy.UNKNOWN)), - public_endp=endpoint.Endpoint( - name='public', - url=endp['endpoints'][0].get('publicURL', taxonomy.UNKNOWN))) - - 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: - endpoint_urls = endp['endpoints'][0] - admin_urlparse = urlparse.urlparse( - endpoint_urls.get('adminURL', '')) - public_urlparse = urlparse.urlparse( - endpoint_urls.get('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 |