summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2014-12-01 23:47:15 +0000
committerGerrit Code Review <review@openstack.org>2014-12-01 23:47:15 +0000
commit6ae5b9580197d63dfdcd52f22f667b993d0adb81 (patch)
tree89d774b2461f8ab74a9c34a151c08224bfdab800
parent4d7f610f891224ac43204d2a1c0fb5393344698c (diff)
parent6d3f3605832fdfe7c9e66c1cd323a337b53a9cd7 (diff)
downloadkeystonemiddleware-6ae5b9580197d63dfdcd52f22f667b993d0adb81.tar.gz
Merge "Adding audit middleware to keystonemiddleware"
-rw-r--r--keystonemiddleware/audit.py162
-rw-r--r--keystonemiddleware/tests/test_audit_middleware.py195
-rw-r--r--requirements.txt1
-rw-r--r--test-requirements.txt1
4 files changed, 359 insertions, 0 deletions
diff --git a/keystonemiddleware/audit.py b/keystonemiddleware/audit.py
new file mode 100644
index 0000000..eb03175
--- /dev/null
+++ b/keystonemiddleware/audit.py
@@ -0,0 +1,162 @@
+#
+# 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."""
+
+ @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 = AuditMiddleware._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
diff --git a/keystonemiddleware/tests/test_audit_middleware.py b/keystonemiddleware/tests/test_audit_middleware.py
new file mode 100644
index 0000000..651a7d6
--- /dev/null
+++ b/keystonemiddleware/tests/test_audit_middleware.py
@@ -0,0 +1,195 @@
+#
+# 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.
+
+import os
+import tempfile
+
+import mock
+from oslo.config import cfg
+import testtools
+from testtools import matchers
+import webob
+
+from keystonemiddleware import audit
+
+
+class FakeApp(object):
+ def __call__(self, env, start_response):
+ body = 'Some response'
+ start_response('200 OK', [
+ ('Content-Type', 'text/plain'),
+ ('Content-Length', str(sum(map(len, body))))
+ ])
+ return [body]
+
+
+class FakeFailingApp(object):
+ def __call__(self, env, start_response):
+ raise Exception('It happens!')
+
+
+@mock.patch('oslo.messaging.get_transport', mock.MagicMock())
+class AuditMiddlewareTest(testtools.TestCase):
+
+ def setUp(self):
+ super(AuditMiddlewareTest, self).setUp()
+ (self.fd, self.audit_map) = tempfile.mkstemp()
+ cfg.CONF([], project='keystonemiddleware')
+
+ self.addCleanup(lambda: os.close(self.fd))
+ self.addCleanup(cfg.CONF.reset)
+
+ @staticmethod
+ def _get_environ_header(req_type):
+ env_headers = {'HTTP_X_SERVICE_CATALOG':
+ '''[{"endpoints_links": [],
+ "endpoints": [{"adminURL":
+ "http://host:8774/v2/admin",
+ "region": "RegionOne",
+ "publicURL":
+ "http://host:8774/v2/public",
+ "internalURL":
+ "http://host:8774/v2/internal",
+ "id": "resource_id"}],
+ "type": "compute",
+ "name": "nova"},]''',
+ 'HTTP_X_USER_ID': 'user_id',
+ 'HTTP_X_USER_NAME': 'user_name',
+ 'HTTP_X_AUTH_TOKEN': 'token',
+ 'HTTP_X_PROJECT_ID': 'tenant_id',
+ 'HTTP_X_IDENTITY_STATUS': 'Confirmed'}
+ env_headers['REQUEST_METHOD'] = req_type
+ return env_headers
+
+ def test_api_request(self):
+ middleware = audit.AuditMiddleware(
+ FakeApp(),
+ audit_map_file=self.audit_map,
+ service_name='pycadf')
+ req = webob.Request.blank('/foo/bar',
+ environ=self._get_environ_header('GET'))
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ middleware(req)
+ # Check first notification with only 'request'
+ call_args = notify.call_args_list[0][0]
+ self.assertEqual('audit.http.request', call_args[1])
+ self.assertEqual('/foo/bar', call_args[2]['requestPath'])
+ self.assertEqual('pending', call_args[2]['outcome'])
+ self.assertNotIn('reason', call_args[2])
+ self.assertNotIn('reporterchain', call_args[2])
+
+ # Check second notification with request + response
+ call_args = notify.call_args_list[1][0]
+ self.assertEqual('audit.http.response', call_args[1])
+ self.assertEqual('/foo/bar', call_args[2]['requestPath'])
+ self.assertEqual('success', call_args[2]['outcome'])
+ self.assertIn('reason', call_args[2])
+ self.assertIn('reporterchain', call_args[2])
+
+ def test_api_request_failure(self):
+ middleware = audit.AuditMiddleware(
+ FakeFailingApp(),
+ audit_map_file=self.audit_map,
+ service_name='pycadf')
+ req = webob.Request.blank('/foo/bar',
+ environ=self._get_environ_header('GET'))
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ try:
+ middleware(req)
+ self.fail('Application exception has not been re-raised')
+ except Exception:
+ pass
+ # Check first notification with only 'request'
+ call_args = notify.call_args_list[0][0]
+ self.assertEqual('audit.http.request', call_args[1])
+ self.assertEqual('/foo/bar', call_args[2]['requestPath'])
+ self.assertEqual('pending', call_args[2]['outcome'])
+ self.assertNotIn('reporterchain', call_args[2])
+
+ # Check second notification with request + response
+ call_args = notify.call_args_list[1][0]
+ self.assertEqual('audit.http.response', call_args[1])
+ self.assertEqual('/foo/bar', call_args[2]['requestPath'])
+ self.assertEqual('unknown', call_args[2]['outcome'])
+ self.assertIn('reporterchain', call_args[2])
+
+ def test_process_request_fail(self):
+ middleware = audit.AuditMiddleware(
+ FakeApp(),
+ audit_map_file=self.audit_map,
+ service_name='pycadf')
+ req = webob.Request.blank('/foo/bar',
+ environ=self._get_environ_header('GET'))
+ with mock.patch('oslo.messaging.Notifier.info',
+ side_effect=Exception('error')) as notify:
+ middleware.process_request(req)
+ self.assertTrue(notify.called)
+
+ def test_process_response_fail(self):
+ middleware = audit.AuditMiddleware(
+ FakeApp(),
+ audit_map_file=self.audit_map,
+ service_name='pycadf')
+ req = webob.Request.blank('/foo/bar',
+ environ=self._get_environ_header('GET'))
+ with mock.patch('oslo.messaging.Notifier.info',
+ side_effect=Exception('error')) as notify:
+ middleware.process_response(req, webob.response.Response())
+ self.assertTrue(notify.called)
+
+ def test_ignore_req_opt(self):
+ middleware = audit.AuditMiddleware(FakeApp(),
+ audit_map_file=self.audit_map,
+ ignore_req_list='get, PUT')
+ req = webob.Request.blank('/skip/foo',
+ environ=self._get_environ_header('GET'))
+ req1 = webob.Request.blank('/skip/foo',
+ environ=self._get_environ_header('PUT'))
+ req2 = webob.Request.blank('/accept/foo',
+ environ=self._get_environ_header('POST'))
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ # Check GET/PUT request does not send notification
+ middleware(req)
+ middleware(req1)
+ self.assertEqual([], notify.call_args_list)
+
+ # Check non-GET/PUT request does send notification
+ middleware(req2)
+ self.assertThat(notify.call_args_list, matchers.HasLength(2))
+ call_args = notify.call_args_list[0][0]
+ self.assertEqual('audit.http.request', call_args[1])
+ self.assertEqual('/accept/foo', call_args[2]['requestPath'])
+
+ call_args = notify.call_args_list[1][0]
+ self.assertEqual('audit.http.response', call_args[1])
+ self.assertEqual('/accept/foo', call_args[2]['requestPath'])
+
+ def test_api_request_no_messaging(self):
+ middleware = audit.AuditMiddleware(
+ FakeApp(),
+ audit_map_file=self.audit_map,
+ service_name='pycadf')
+ req = webob.Request.blank('/foo/bar',
+ environ=self._get_environ_header('GET'))
+ with mock.patch('keystonemiddleware.audit.messaging', None):
+ with mock.patch('keystonemiddleware.audit.LOG.info') as log:
+ middleware(req)
+ # Check first notification with only 'request'
+ call_args = log.call_args_list[0][0]
+ self.assertEqual('audit.http.request',
+ call_args[1]['event_type'])
+
+ # Check second notification with request + response
+ call_args = log.call_args_list[1][0]
+ self.assertEqual('audit.http.response',
+ call_args[1]['event_type'])
diff --git a/requirements.txt b/requirements.txt
index 16a1c41..a19244b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,6 +9,7 @@ oslo.i18n>=1.0.0 # Apache-2.0
oslo.serialization>=1.0.0 # Apache-2.0
oslo.utils>=1.0.0 # Apache-2.0
pbr>=0.6,!=0.7,<1.0
+pycadf>=0.6.0
python-keystoneclient>=0.11.1
requests>=2.2.0,!=2.4.0
six>=1.7.0
diff --git a/test-requirements.txt b/test-requirements.txt
index e900725..35d489d 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -10,6 +10,7 @@ mock>=1.0
pycrypto>=2.6
oslosphinx>=2.2.0 # Apache-2.0
oslotest>=1.2.0 # Apache-2.0
+oslo.messaging>=1.4.0
requests-mock>=0.5.1 # Apache-2.0
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
testrepository>=0.0.18