summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGordon Chung <chungg@ca.ibm.com>2013-08-06 15:24:50 -0400
committerGordon Chung <chungg@ca.ibm.com>2013-08-06 15:39:25 -0400
commit7bf0f20abe7e721f28d9638c32e05e3e40d95356 (patch)
treed8b8b14c667b46c448ba8277d481b83666f54b7f
parent7f76e5cf7bf560603829ffaa73458a86c384ecb7 (diff)
downloadpycadf-7bf0f20abe7e721f28d9638c32e05e3e40d95356.tar.gz
add support for audit api middlewarev0.1
build api audit event and append to a given message blueprint support-standard-audit-formats Change-Id: I620f49083c3db12bdad74b29f4d9b3019d977468
-rw-r--r--etc/pycadf/api_audit_map.conf55
-rw-r--r--pycadf/audit/__init__.py28
-rw-r--r--pycadf/audit/api.py224
-rw-r--r--pycadf/tests/audit/__init__.py0
-rw-r--r--pycadf/tests/audit/test_api.py168
-rw-r--r--pycadf/tests/base.py43
-rw-r--r--test-requirements.txt1
7 files changed, 519 insertions, 0 deletions
diff --git a/etc/pycadf/api_audit_map.conf b/etc/pycadf/api_audit_map.conf
new file mode 100644
index 0000000..a27e67a
--- /dev/null
+++ b/etc/pycadf/api_audit_map.conf
@@ -0,0 +1,55 @@
+[DEFAULT]
+api_paths =
+ add
+ entries
+ extensions
+ limits
+ servers
+ metadata
+ ips
+ images
+ flavors
+ os-agents
+ os-aggregates
+ os-cloudpipe
+ diagnostics
+ os-fixed-ips
+ os-extra_specs
+ os-flavor-access
+ os-floating-ip-dns
+ os-floating-ips-bulk
+ os-floating-ips
+ os-hosts
+ os-hypervisors
+ os-instance-actions
+ os-keypairs
+ os-networks
+ os-quota-sets
+ os-security-groups
+ os-security-group-rules
+ os-server-password
+ os-services
+ os-simple-tenant-usage
+ os-virtual-interfaces
+ os-volume_attachments
+ os-volumes
+ os-volume-types
+ os-snapshots
+
+[body_actions]
+changePassword = update
+reboot = update
+rebuild = update
+resize = update
+confirmResize = read
+revertResize = update
+createImage = create
+
+[service_endpoints]
+identity = service/security
+object-store = service/storage/object
+volume = service/storage/block
+image = service/storage/image
+network = service/network
+compute = service/compute
+metering = service/bss/metering \ No newline at end of file
diff --git a/pycadf/audit/__init__.py b/pycadf/audit/__init__.py
new file mode 100644
index 0000000..3a143f2
--- /dev/null
+++ b/pycadf/audit/__init__.py
@@ -0,0 +1,28 @@
+# -*- encoding: utf-8 -*-
+#
+# Copyright 2013 IBM Corp.
+#
+#
+# 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.
+
+from oslo.config import cfg
+
+CONF = cfg.CONF
+
+opts = [
+ cfg.StrOpt('api_audit_map',
+ default='/etc/pycadf/api_audit_map.conf',
+ help='File containing mapping for api paths and '
+ 'service endpoints'),
+]
+CONF.register_opts(opts, group='audit')
diff --git a/pycadf/audit/api.py b/pycadf/audit/api.py
new file mode 100644
index 0000000..3f252e4
--- /dev/null
+++ b/pycadf/audit/api.py
@@ -0,0 +1,224 @@
+# -*- encoding: utf-8 -*-
+#
+# Copyright 2013 IBM Corp.
+#
+#
+# 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 ast
+import ConfigParser
+import os
+from oslo.config import cfg
+import urlparse
+
+from pycadf import cadftaxonomy as taxonomy
+from pycadf import cadftype
+from pycadf import eventfactory as factory
+from pycadf.openstack.common import log as logging
+from pycadf import reason
+from pycadf import reporterstep
+from pycadf import resource
+from pycadf import tag
+from pycadf import timestamp
+
+cfg.CONF.import_opt('api_audit_map', 'pycadf.audit', group='audit')
+
+LOG = logging.getLogger(__name__)
+CONF = cfg.CONF
+
+
+class ServiceResource(resource.Resource):
+ def __init__(self, admin_url=None, private_url=None,
+ public_url=None, **kwargs):
+ super(ServiceResource, self).__init__(**kwargs)
+ if admin_url is not None:
+ self.adminURL = admin_url
+ if private_url is not None:
+ self.privateURL = private_url
+ if public_url is not None:
+ self.publicURL = public_url
+
+
+class ClientResource(resource.Resource):
+ def __init__(self, client_addr=None, user_agent=None,
+ token=None, tenant=None, status=None, **kwargs):
+ super(ClientResource, self).__init__(**kwargs)
+ if client_addr is not None:
+ self.client_addr = client_addr
+ if user_agent is not None:
+ self.user_agent = user_agent
+ if token is not None:
+ self.token = token
+ if tenant is not None:
+ self.tenant = tenant
+ if status is not None:
+ self.status = status
+
+
+class OpenStackAuditApi(object):
+
+ _API_PATHS = []
+ _BODY_ACTIONS = {}
+ _SERVICE_ENDPOINTS = {}
+
+ def __init__(self):
+ self._configure_audit_map()
+
+ def _configure_audit_map(self):
+ """Configure to recognize and map known api paths."""
+
+ cfg_file = CONF.audit.api_audit_map
+ if not os.path.exists(CONF.audit.api_audit_map):
+ cfg_file = cfg.CONF.find_file(CONF.audit.api_audit_map)
+ LOG.debug("API path config file: %s", cfg_file)
+
+ if cfg_file:
+ try:
+ audit_map = ConfigParser.SafeConfigParser()
+ audit_map.readfp(open(cfg_file))
+
+ try:
+ paths = audit_map.get('DEFAULT', 'api_paths')
+ self._API_PATHS = paths.lstrip().split('\n')
+ except ConfigParser.NoSectionError:
+ pass
+
+ try:
+ self._BODY_ACTIONS = dict(audit_map.items('body_actions'))
+ except ConfigParser.NoSectionError:
+ pass
+
+ try:
+ self._SERVICE_ENDPOINTS = \
+ dict(audit_map.items('service_endpoints'))
+ except ConfigParser.NoSectionError:
+ pass
+ except ConfigParser.ParsingError as err:
+ LOG.error('Error parsing audit map file: %s' % err)
+
+ 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 get action from map;
+ 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 = urlparse.urlparse(req.url).path
+ path = path[:-1] if path.endswith('/') else path
+
+ method = req.method
+ if method == 'POST':
+ if path[path.rfind('/') + 1:] == 'action':
+ if req.json:
+ body_action = req.json.keys()[0]
+ action = self._BODY_ACTIONS.get(body_action,
+ taxonomy.ACTION_CREATE)
+ else:
+ action = taxonomy.ACTION_CREATE
+ elif path[path.rfind('/') + 1:] not in self._API_PATHS:
+ action = taxonomy.ACTION_UPDATE
+ else:
+ action = taxonomy.ACTION_CREATE
+ elif method == 'GET':
+ if path[path.rfind('/') + 1:] in self._API_PATHS:
+ action = taxonomy.ACTION_LIST
+ else:
+ action = taxonomy.ACTION_READ
+ elif method == 'PUT':
+ 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 gen_event(self, req, correlation_id):
+ action = self._get_action(req)
+ catalog = ast.literal_eval(req.environ['HTTP_X_SERVICE_CATALOG'])
+ for endpoint in catalog:
+ admin_urlparse = urlparse.urlparse(
+ endpoint['endpoints'][0]['adminURL'])
+ public_urlparse = urlparse.urlparse(
+ endpoint['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_type = self._SERVICE_ENDPOINTS.get(endpoint['type'],
+ taxonomy.UNKNOWN)
+ service_name = endpoint['name']
+ admin_url = endpoint['endpoints'][0]['adminURL']
+ private_url = endpoint['endpoints'][0]['internalURL']
+ public_url = endpoint['endpoints'][0]['publicURL']
+ service_id = endpoint['endpoints'][0]['id']
+ break
+ else:
+ service_type = service_id = service_name = taxonomy.UNKNOWN
+ admin_url = private_url = public_url = None
+
+ event = factory.EventFactory().new_event(
+ eventType=cadftype.EVENTTYPE_ACTIVITY,
+ outcome=taxonomy.OUTCOME_PENDING,
+ action=action,
+ initiator=ClientResource(
+ typeURI=taxonomy.ACCOUNT_USER,
+ id=str(req.environ['HTTP_X_USER_ID']),
+ name=req.environ['HTTP_X_USER_NAME'],
+ client_addr=req.client_addr,
+ user_agent=req.user_agent,
+ token=req.environ['HTTP_X_AUTH_TOKEN'],
+ tenant=req.environ['HTTP_X_PROJECT_ID'],
+ status=req.environ['HTTP_X_IDENTITY_STATUS']),
+ target=ServiceResource(typeURI=service_type,
+ id=service_id,
+ name=service_name,
+ private_url=private_url,
+ public_url=public_url,
+ admin_url=admin_url))
+ event.add_tag(tag.generate_name_value_tag('correlation_id',
+ correlation_id))
+ return event
+
+ def append_audit_event(self, msg, req, correlation_id):
+ setattr(req, 'CADF_EVENT_CORRELATION_ID', correlation_id)
+ event = self.gen_event(req, correlation_id)
+ event.add_reporterstep(
+ reporterstep.Reporterstep(
+ role=cadftype.REPORTER_ROLE_OBSERVER,
+ reporter='target'))
+ msg['cadf_event'] = event
+
+ def mod_audit_event(self, msg, response, correlation_id):
+ if response.status_int >= 200 and response.status_int < 400:
+ result = taxonomy.OUTCOME_SUCCESS
+ else:
+ result = taxonomy.OUTCOME_FAILURE
+ if 'cadf_event' in msg:
+ msg['cadf_event'].outcome = result
+ msg['cadf_event'].reason = \
+ reason.Reason(reasonType='HTTP',
+ reasonCode=str(response.status_int))
+ msg['cadf_event'].add_reporterstep(
+ reporterstep.Reporterstep(
+ role=cadftype.REPORTER_ROLE_MODIFIER,
+ reporter='target',
+ reporterTime=timestamp.get_utc_now()))
diff --git a/pycadf/tests/audit/__init__.py b/pycadf/tests/audit/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pycadf/tests/audit/__init__.py
diff --git a/pycadf/tests/audit/test_api.py b/pycadf/tests/audit/test_api.py
new file mode 100644
index 0000000..0bf7bd2
--- /dev/null
+++ b/pycadf/tests/audit/test_api.py
@@ -0,0 +1,168 @@
+# -*- encoding: utf-8 -*-
+#
+# Copyright 2013 IBM Corp.
+#
+#
+# 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.
+from oslo.config import cfg
+import uuid
+import webob
+
+from pycadf.audit import api
+from pycadf import identifier
+from pycadf.tests import base
+
+
+class TestAuditApi(base.TestCase):
+ 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'}
+
+ def setUp(self):
+ super(TestAuditApi, self).setUp()
+ # set nova CONF.host value
+ # Set a default location for the api_audit_map config file
+ cfg.CONF.set_override(
+ 'api_audit_map',
+ self.path_get('etc/pycadf/api_audit_map.conf'),
+ group='audit'
+ )
+ self.audit_api = api.OpenStackAuditApi()
+
+ def api_request(self, method, url):
+ self.ENV_HEADERS['REQUEST_METHOD'] = method
+ req = webob.Request.blank(url, environ=self.ENV_HEADERS)
+ msg = {}
+ self.audit_api.append_audit_event(msg, req,
+ identifier.generate_uuid())
+ return msg
+
+ def test_get_list(self):
+ msg = self.api_request('GET', 'http://host:8774/v2/public/servers')
+ payload = msg['cadf_event'].as_dict()
+ self.assertEqual(payload['action'], 'list')
+ self.assertEqual(payload['typeURI'],
+ 'http://schemas.dmtf.org/cloud/audit/1.0/event')
+ self.assertEqual(payload['outcome'], 'pending')
+ self.assertEqual(payload['eventType'], 'activity')
+ self.assertEqual(payload['target']['publicURL'],
+ 'http://host:8774/v2/public')
+ self.assertEqual(payload['target']['privateURL'],
+ 'http://host:8774/v2/internal')
+ self.assertEqual(payload['target']['adminURL'],
+ 'http://host:8774/v2/admin')
+ self.assertEqual(payload['target']['name'], 'nova')
+ self.assertEqual(payload['target']['id'], 'resource_id')
+ self.assertEqual(payload['target']['typeURI'], 'service/compute')
+ self.assertEqual(payload['initiator']['id'], 'user_id')
+ self.assertEqual(payload['initiator']['name'], 'user_name')
+ self.assertEqual(payload['initiator']['token'], 'token')
+ self.assertEqual(payload['initiator']['tenant'], 'tenant_id')
+ self.assertEqual(payload['initiator']['typeURI'],
+ 'service/security/account/user')
+ self.assertNotIn('reason', payload)
+ self.assertEqual(len(payload['reporterchain']), 1)
+ self.assertEqual(payload['reporterchain'][0]['role'], 'observer')
+ self.assertEqual(payload['reporterchain'][0]['reporter'], 'target')
+
+ def test_get_read(self):
+ msg = self.api_request('GET',
+ 'http://host:8774/v2/public/servers/' +
+ str(uuid.uuid4()))
+ payload = msg['cadf_event'].as_dict()
+ self.assertEqual(payload['action'], 'read')
+ self.assertEqual(payload['outcome'], 'pending')
+
+ def test_get_unknown_endpoint(self):
+ msg = self.api_request('GET',
+ 'http://unknown:8774/v2/public/servers/')
+ payload = msg['cadf_event'].as_dict()
+ self.assertEqual(payload['action'], 'list')
+ self.assertEqual(payload['outcome'], 'pending')
+ self.assertEqual(payload['target']['name'], 'unknown')
+ self.assertEqual(payload['target']['id'], 'unknown')
+ self.assertEqual(payload['target']['typeURI'], 'unknown')
+
+ def test_put(self):
+ msg = self.api_request('PUT', 'http://host:8774/v2/public/servers')
+ payload = msg['cadf_event'].as_dict()
+ self.assertEqual(payload['action'], 'update')
+ self.assertEqual(payload['outcome'], 'pending')
+
+ def test_delete(self):
+ msg = self.api_request('DELETE', 'http://host:8774/v2/public/servers')
+ payload = msg['cadf_event'].as_dict()
+ self.assertEqual(payload['action'], 'delete')
+ self.assertEqual(payload['outcome'], 'pending')
+
+ def test_head(self):
+ msg = self.api_request('HEAD', 'http://host:8774/v2/public/servers')
+ payload = msg['cadf_event'].as_dict()
+ self.assertEqual(payload['action'], 'read')
+ self.assertEqual(payload['outcome'], 'pending')
+
+ def test_post_update(self):
+ msg = self.api_request('POST',
+ 'http://host:8774/v2/public/servers/' +
+ str(uuid.uuid4()))
+ payload = msg['cadf_event'].as_dict()
+ self.assertEqual(payload['action'], 'update')
+ self.assertEqual(payload['outcome'], 'pending')
+
+ def test_post_create(self):
+ msg = self.api_request('POST', 'http://host:8774/v2/public/servers')
+ payload = msg['cadf_event'].as_dict()
+ self.assertEqual(payload['action'], 'create')
+ self.assertEqual(payload['outcome'], 'pending')
+
+ def test_post_action(self):
+ self.ENV_HEADERS['REQUEST_METHOD'] = 'POST'
+ req = webob.Request.blank('http://host:8774/v2/public/servers/action',
+ environ=self.ENV_HEADERS)
+ req.body = '{"createImage" : {"name" : "new-image","metadata": ' \
+ '{"ImageType": "Gold","ImageVersion": "2.0"}}}'
+ msg = {}
+ self.audit_api.append_audit_event(msg, req,
+ identifier.generate_uuid())
+ payload = msg['cadf_event'].as_dict()
+ self.assertEqual(payload['action'], 'create')
+ self.assertEqual(payload['outcome'], 'pending')
+
+ def test_response_mod_msg(self):
+ msg = self.api_request('GET', 'http://host:8774/v2/public/servers')
+ payload = msg['cadf_event'].as_dict()
+ self.audit_api.mod_audit_event(msg, webob.Response(),
+ identifier.generate_uuid())
+ payload2 = msg['cadf_event'].as_dict()
+ self.assertEqual(payload['id'], payload2['id'])
+ self.assertEqual(payload['tags'], payload2['tags'])
+ self.assertEqual(payload2['outcome'], 'success')
+ self.assertEqual(payload2['reason']['reasonType'], 'HTTP')
+ self.assertEqual(payload2['reason']['reasonCode'], '200')
+ self.assertEqual(len(payload2['reporterchain']), 2)
+ self.assertEqual(payload2['reporterchain'][1]['role'], 'modifier')
+ self.assertEqual(payload2['reporterchain'][1]['reporter'], 'target')
diff --git a/pycadf/tests/base.py b/pycadf/tests/base.py
new file mode 100644
index 0000000..729886a
--- /dev/null
+++ b/pycadf/tests/base.py
@@ -0,0 +1,43 @@
+# -*- encoding: utf-8 -*-
+#
+# Copyright 2013 IBM Corp.
+#
+# 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.
+
+"""Test base classes.
+"""
+import os.path
+from oslo.config import cfg
+import testtools
+
+
+class TestCase(testtools.TestCase):
+
+ def setUp(self):
+ super(TestCase, self).setUp()
+ cfg.CONF([], project='pycadf')
+
+ def path_get(self, project_file=None):
+ root = os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '..',
+ '..',
+ )
+ )
+ if project_file:
+ return os.path.join(root, project_file)
+ else:
+ return root
+
+ def tearDown(self):
+ cfg.CONF.reset()
+ super(TestCase, self).tearDown()
diff --git a/test-requirements.txt b/test-requirements.txt
index cdd85f5..e4e171b 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -10,6 +10,7 @@ python-subunit
testrepository>=0.0.17
testscenarios<0.5
testtools>=0.9.29
+WebOb>=1.2.3,<1.3
# when we can require tox>= 1.4, this can go into tox.ini:
# [testenv:cover]