170 lines
5.9 KiB
Python
170 lines
5.9 KiB
Python
#
|
|
# 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 functools
|
|
import logging
|
|
import os.path
|
|
import sys
|
|
|
|
from oslo.config import cfg
|
|
try:
|
|
import oslo.messaging
|
|
messaging = True
|
|
except ImportError:
|
|
messaging = False
|
|
import pycadf
|
|
from pycadf.audit import api
|
|
import webob.dec
|
|
|
|
from keystonemiddleware.i18n import _LE, _LI
|
|
from keystonemiddleware.openstack.common import context
|
|
|
|
|
|
_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
|
|
|
|
|
|
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 = api.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})
|
|
|
|
@_log_and_ignore_error
|
|
def _process_request(self, request):
|
|
correlation_id = pycadf.identifier.generate_uuid()
|
|
self._event = self._cadf_audit.create_event(request, correlation_id)
|
|
|
|
self._emit_audit(context.get_admin_context().to_dict(),
|
|
'audit.http.request', self._event.as_dict())
|
|
|
|
@_log_and_ignore_error
|
|
def _process_response(self, request, response=None):
|
|
if not hasattr(self, 'event'):
|
|
# NOTE(gordc): handle case where error processing request
|
|
correlation_id = pycadf.identifier.generate_uuid()
|
|
self._event = self._cadf_audit.create_event(request,
|
|
correlation_id)
|
|
|
|
if response:
|
|
if response.status_int >= 200 and response.status_int < 400:
|
|
result = pycadf.cadftaxonomy.OUTCOME_SUCCESS
|
|
else:
|
|
result = pycadf.cadftaxonomy.OUTCOME_FAILURE
|
|
self._event.reason = pycadf.reason.Reason(
|
|
reasonType='HTTP', reasonCode=str(response.status_int))
|
|
else:
|
|
result = pycadf.cadftaxonomy.UNKNOWN
|
|
|
|
self._event.outcome = result
|
|
self._event.add_reporterstep(
|
|
pycadf.reporterstep.Reporterstep(
|
|
role=pycadf.cadftype.REPORTER_ROLE_MODIFIER,
|
|
reporter=pycadf.resource.Resource(id='target'),
|
|
reporterTime=pycadf.timestamp.get_utc_now()))
|
|
|
|
self._emit_audit(context.get_admin_context().to_dict(),
|
|
'audit.http.response', self._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
|