diff options
author | Gordon Chung <chungg@ca.ibm.com> | 2014-01-21 11:47:52 -0500 |
---|---|---|
committer | Gordon Chung <chungg@ca.ibm.com> | 2014-02-03 10:51:32 -0500 |
commit | a6423817b6ca7d1746ae792e7b02889bd1f4814c (patch) | |
tree | 0701508285d0b04369d27cbb3cae48381bc5a512 /pycadf | |
parent | eb9a747e3fae746393085215e7098a3b2104e3de (diff) | |
download | pycadf-a6423817b6ca7d1746ae792e7b02889bd1f4814c.tar.gz |
adjust typeURI to capture target better
update to fix nova audits.
- change typeURI to record greater depth of api request.
- add custom_actions to allow special case actions based on api
path
Change-Id: I2b33cf8cff796bdb135dd832c23f0e2ffdf90780
blueprint: audit-all-apis
Diffstat (limited to 'pycadf')
-rw-r--r-- | pycadf/audit/api.py | 148 | ||||
-rw-r--r-- | pycadf/cadftaxonomy.py | 10 | ||||
-rw-r--r-- | pycadf/tests/audit/test_api.py | 104 |
3 files changed, 177 insertions, 85 deletions
diff --git a/pycadf/audit/api.py b/pycadf/audit/api.py index b058aab..62ce2d7 100644 --- a/pycadf/audit/api.py +++ b/pycadf/audit/api.py @@ -19,6 +19,7 @@ import ast import collections import os from oslo.config import cfg +import re from six.moves import configparser from six.moves.urllib import parse as urlparse @@ -46,6 +47,58 @@ opts = [ CONF.register_opts(opts, group='audit') +AuditMap = collections.namedtuple('AuditMap', + ['path_kw', + 'custom_actions', + 'service_endpoints', + 'default_target_endpoint_type']) + + +def _configure_audit_map(): + """Configure to recognize and map known api paths.""" + + path_kw = {} + custom_actions = {} + service_endpoints = {} + default_target_endpoint_type = None + + 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) + + 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: + service_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) + return AuditMap(path_kw=path_kw, custom_actions=custom_actions, + service_endpoints=service_endpoints, + default_target_endpoint_type=default_target_endpoint_type) + + class ClientResource(resource.Resource): def __init__(self, project_id=None, **kwargs): super(ClientResource, self).__init__(**kwargs) @@ -66,60 +119,18 @@ class PycadfAuditApiConfigError(Exception): class OpenStackAuditApi(object): - _api_paths = [] - _body_actions = {} - _service_endpoints = {} - _default_target_endpoint_type = None + _MAP = None Service = collections.namedtuple('Service', ['id', 'name', 'type', 'admin_endp', 'public_endp', 'private_endp']) - 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) - - 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') - try: - self._default_target_endpoint_type = \ - audit_map.get('DEFAULT', 'target_endpoint_type') - except configparser.NoOptionError: - pass - except configparser.NoSectionError: - pass - - try: - self._body_actions = dict(audit_map.items('body_actions')) - except configparser.Error: - pass - - try: - self._service_endpoints = \ - dict(audit_map.items('service_endpoints')) - except configparser.Error: - pass - except configparser.ParsingError as err: - raise PycadfAuditApiConfigError( - '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; + 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; @@ -129,24 +140,28 @@ class OpenStackAuditApi(object): if HEAD, assume read action. """ - path = urlparse.urlparse(req.url).path - path = path[:-1] if path.endswith('/') else path - + path = req.path[:-1] if req.path.endswith('/') else req.path + url_ending = path[path.rfind('/') + 1:] method = req.method - if method == 'POST': - if path[path.rfind('/') + 1:] == 'action': + + 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': if req.json: body_action = list(req.json.keys())[0] - action = self._body_actions.get(body_action, - taxonomy.ACTION_CREATE) + action = taxonomy.ACTION_UPDATE + '/' + body_action else: action = taxonomy.ACTION_CREATE - elif path[path.rfind('/') + 1:] not in self._api_paths: + elif url_ending not in self._MAP.path_kw: action = taxonomy.ACTION_UPDATE else: action = taxonomy.ACTION_CREATE elif method == 'GET': - if path[path.rfind('/') + 1:] in self._api_paths: + if url_ending in self._MAP.path_kw: action = taxonomy.ACTION_LIST else: action = taxonomy.ACTION_READ @@ -163,7 +178,7 @@ class OpenStackAuditApi(object): def _get_service_info(self, endp): service = self.Service( - type=self._service_endpoints.get( + type=self._MAP.service_endpoints.get( endp['type'], taxonomy.UNKNOWN), name=endp['name'], @@ -180,7 +195,21 @@ class OpenStackAuditApi(object): return service + def _build_typeURI(self, req, service_type): + type_uri = '' + prev_key = None + for key in re.split('/', req.path_qs): + 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 create_event(self, req, correlation_id): + if not self._MAP: + self._MAP = _configure_audit_map() + action = self._get_action(req) initiator_host = host.Host(address=req.client_addr, agent=req.user_agent) @@ -202,8 +231,8 @@ class OpenStackAuditApi(object): or req_url.netloc == public_urlparse.netloc): service_info = self._get_service_info(endp) break - elif (self._default_target_endpoint_type - and endp['type'] == self._default_target_endpoint_type): + elif (self._MAP.default_target_endpoint_type + and endp['type'] == self._MAP.default_target_endpoint_type): default_endpoint = endp else: if default_endpoint: @@ -218,7 +247,10 @@ class OpenStackAuditApi(object): 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 = resource.Resource(typeURI=service_info.type, + target_typeURI = (self._build_typeURI(req, service_info.type) + if service_info.type != taxonomy.UNKNOWN + else service_info.type) + target = resource.Resource(typeURI=target_typeURI, id=service_info.id, name=service_info.name) if service_info.admin_endp: diff --git a/pycadf/cadftaxonomy.py b/pycadf/cadftaxonomy.py index 52770aa..d21d54a 100644 --- a/pycadf/cadftaxonomy.py +++ b/pycadf/cadftaxonomy.py @@ -64,7 +64,10 @@ ACTION_TAXONOMY = frozenset([ # TODO(mrutkows): validate absolute URIs as well def is_valid_action(value): - return value in ACTION_TAXONOMY + for type in ACTION_TAXONOMY: + if value.startswith(type): + return True + return False TYPE_URI_OUTCOME = cadftype.CADF_VERSION_1_0_0 + 'outcome' @@ -176,4 +179,7 @@ RESOURCE_TAXONOMY = frozenset([ # TODO(mrutkows): validate absolute URIs as well def is_valid_resource(value): - return value in RESOURCE_TAXONOMY + for type in RESOURCE_TAXONOMY: + if value.startswith(type): + return True + return False diff --git a/pycadf/tests/audit/test_api.py b/pycadf/tests/audit/test_api.py index 71b3781..d5cbbad 100644 --- a/pycadf/tests/audit/test_api.py +++ b/pycadf/tests/audit/test_api.py @@ -26,12 +26,12 @@ class TestAuditApi(base.TestCase): ENV_HEADERS = {'HTTP_X_SERVICE_CATALOG': '''[{"endpoints_links": [], "endpoints": [{"adminURL": - "http://host:8774/v2/admin", + "http://admin_host:8774", "region": "RegionOne", "publicURL": - "http://host:8774/v2/public", + "http://public_host:8775", "internalURL": - "http://host:8774/v2/internal", + "http://internal_host:8776", "id": "resource_id"}], "type": "compute", "name": "nova"},]''', @@ -61,7 +61,8 @@ class TestAuditApi(base.TestCase): return req def test_get_list(self): - req = self.api_request('GET', 'http://host:8774/v2/public/servers') + req = self.api_request('GET', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers') payload = req.environ['CADF_EVENT'] self.assertEqual(payload['action'], 'read/list') self.assertEqual(payload['typeURI'], @@ -70,11 +71,12 @@ class TestAuditApi(base.TestCase): self.assertEqual(payload['eventType'], 'activity') self.assertEqual(payload['target']['name'], 'nova') self.assertEqual(payload['target']['id'], 'openstack:resource_id') - self.assertEqual(payload['target']['typeURI'], 'service/compute') + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers') self.assertEqual(len(payload['target']['addresses']), 3) self.assertEqual(payload['target']['addresses'][0]['name'], 'admin') self.assertEqual(payload['target']['addresses'][0]['url'], - 'http://host:8774/v2/admin') + 'http://admin_host:8774') self.assertEqual(payload['initiator']['id'], 'openstack:user_id') self.assertEqual(payload['initiator']['name'], 'user_name') self.assertEqual(payload['initiator']['project_id'], @@ -92,15 +94,19 @@ class TestAuditApi(base.TestCase): def test_get_read(self): req = self.api_request('GET', - 'http://host:8774/v2/public/servers/' + - str(uuid.uuid4())) + 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers/' + + str(uuid.uuid4())) payload = req.environ['CADF_EVENT'] + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers/server') self.assertEqual(payload['action'], 'read') self.assertEqual(payload['outcome'], 'pending') def test_get_unknown_endpoint(self): req = self.api_request('GET', - 'http://unknown:8774/v2/public/servers/') + 'http://unknown:8774/v2/' + + str(uuid.uuid4()) + '/servers') payload = req.environ['CADF_EVENT'] self.assertEqual(payload['action'], 'read/list') self.assertEqual(payload['outcome'], 'pending') @@ -113,68 +119,114 @@ class TestAuditApi(base.TestCase): with open(tmpfile, "w") as f: f.write("[DEFAULT]\n") f.write("target_endpoint_type = compute \n") - f.write("api_paths = servers\n\n") + f.write("[path_keywords]\n") + f.write("servers = server\n\n") f.write("[service_endpoints]\n") f.write("compute = service/compute") cfg.CONF.set_override('api_audit_map', tmpfile, group='audit') self.audit_api = api.OpenStackAuditApi() - self.assertEqual(self.audit_api._default_target_endpoint_type, - 'compute') req = self.api_request('GET', - 'http://unknown:8774/v2/public/servers/') + 'http://unknown:8774/v2/' + + str(uuid.uuid4()) + '/servers') + self.assertEqual(self.audit_api._MAP.default_target_endpoint_type, + 'compute') payload = req.environ['CADF_EVENT'] self.assertEqual(payload['action'], 'read/list') self.assertEqual(payload['outcome'], 'pending') self.assertEqual(payload['target']['name'], 'nova') self.assertEqual(payload['target']['id'], 'openstack:resource_id') - self.assertEqual(payload['target']['typeURI'], 'service/compute') + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers') def test_put(self): - req = self.api_request('PUT', 'http://host:8774/v2/public/servers') + req = self.api_request('PUT', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers') payload = req.environ['CADF_EVENT'] + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers') self.assertEqual(payload['action'], 'update') self.assertEqual(payload['outcome'], 'pending') def test_delete(self): - req = self.api_request('DELETE', 'http://host:8774/v2/public/servers') + req = self.api_request('DELETE', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers') payload = req.environ['CADF_EVENT'] + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers') self.assertEqual(payload['action'], 'delete') self.assertEqual(payload['outcome'], 'pending') def test_head(self): - req = self.api_request('HEAD', 'http://host:8774/v2/public/servers') + req = self.api_request('HEAD', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers') payload = req.environ['CADF_EVENT'] + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers') self.assertEqual(payload['action'], 'read') self.assertEqual(payload['outcome'], 'pending') def test_post_update(self): req = self.api_request('POST', - 'http://host:8774/v2/public/servers/' + - str(uuid.uuid4())) + 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers/' + + str(uuid.uuid4())) payload = req.environ['CADF_EVENT'] + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers/server') self.assertEqual(payload['action'], 'update') self.assertEqual(payload['outcome'], 'pending') def test_post_create(self): - req = self.api_request('POST', 'http://host:8774/v2/public/servers') + req = self.api_request('POST', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers') payload = req.environ['CADF_EVENT'] + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers') 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', + req = webob.Request.blank('http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers/action', environ=self.ENV_HEADERS) req.body = b'{"createImage" : {"name" : "new-image","metadata": ' \ b'{"ImageType": "Gold","ImageVersion": "2.0"}}}' self.audit_api.append_audit_event(req) payload = req.environ['CADF_EVENT'] - self.assertEqual(payload['action'], 'create') + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers/action') + self.assertEqual(payload['action'], 'update/createImage') + self.assertEqual(payload['outcome'], 'pending') + + def test_custom_action(self): + req = self.api_request('GET', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/os-hosts/' + + str(uuid.uuid4()) + '/reboot') + payload = req.environ['CADF_EVENT'] + self.assertEqual(payload['target']['typeURI'], + 'service/compute/os-hosts/host/reboot') + self.assertEqual(payload['action'], 'start/reboot') self.assertEqual(payload['outcome'], 'pending') + def test_custom_action_complex(self): + req = self.api_request('GET', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/os-migrations') + payload = req.environ['CADF_EVENT'] + self.assertEqual(payload['target']['typeURI'], + 'service/compute/os-migrations') + self.assertEqual(payload['action'], 'read') + req = self.api_request('POST', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/os-migrations') + payload = req.environ['CADF_EVENT'] + self.assertEqual(payload['target']['typeURI'], + 'service/compute/os-migrations') + self.assertEqual(payload['action'], 'create') + def test_response_mod_msg(self): - req = self.api_request('GET', 'http://host:8774/v2/public/servers') + req = self.api_request('GET', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers') payload = req.environ['CADF_EVENT'] self.audit_api.mod_audit_event(req, webob.Response()) payload2 = req.environ['CADF_EVENT'] @@ -189,7 +241,8 @@ class TestAuditApi(base.TestCase): 'target') def test_no_response(self): - req = self.api_request('GET', 'http://host:8774/v2/public/servers') + req = self.api_request('GET', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers') payload = req.environ['CADF_EVENT'] self.audit_api.mod_audit_event(req, None) payload2 = req.environ['CADF_EVENT'] @@ -204,7 +257,8 @@ class TestAuditApi(base.TestCase): def test_missing_req(self): self.ENV_HEADERS['REQUEST_METHOD'] = 'GET' - req = webob.Request.blank('http://host:8774/v2/public/servers', + req = webob.Request.blank('http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers', environ=self.ENV_HEADERS) self.assertNotIn('CADF_EVENT', req.environ) self.audit_api.mod_audit_event(req, webob.Response()) |