summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--etc/pycadf/api_audit_map.conf104
-rw-r--r--pycadf/audit/api.py148
-rw-r--r--pycadf/cadftaxonomy.py10
-rw-r--r--pycadf/tests/audit/test_api.py104
4 files changed, 237 insertions, 129 deletions
diff --git a/etc/pycadf/api_audit_map.conf b/etc/pycadf/api_audit_map.conf
index 93794f6..4003f7a 100644
--- a/etc/pycadf/api_audit_map.conf
+++ b/etc/pycadf/api_audit_map.conf
@@ -3,52 +3,68 @@
# should match the endpoint type defined in service catalog
target_endpoint_type = None
+[custom_actions]
+enable = enable
+disable = disable
+delete = delete
+startup = start/startup
+shutdown = stop/shutdown
+reboot = start/reboot
+os-migrations/get = read
+os-server-password/post = update
+
# possible end path of api requests
-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
+[path_keywords]
+add = None
+action = None
+enable = None
+disable = None
+configure-project = None
+defaults = None
+delete = None
+detail = None
+diagnostics = None
+entries = entry
+extensions = alias
+flavors = flavor
+images = image
+ips = label
+limits = None
+metadata = key
+os-agents = os-agent
+os-aggregates = os-aggregate
+os-certificates = None
+os-cloudpipe = None
+os-fixed-ips = ip
+os-extra_specs = key
+os-flavor-access = None
+os-floating-ip-dns = domain
+os-floating-ips-bulk = host
+os-floating-ip-pools = None
+os-floating-ips = floating-ip
+os-hosts = host
+os-hypervisors = hypervisor
+os-instance-actions = instance-action
+os-keypairs = keypair
+os-migrations = None
+os-networks = network
+os-quota-sets = None
+os-security-groups = security-group
+os-security-group-rules = rule
+os-server-password = None
+os-services = None
+os-simple-tenant-usage = None
+os-virtual-interfaces = None
+os-volume_attachments = attachment
+os-volumes = volume
+os-volume-types = volume-type
+os-snapshots = snapshot
+reboot = None
+servers = server
+shutdown = None
+startup = None
+statistics = None
-[body_actions]
-changePassword = update
-reboot = update
-rebuild = update
-resize = update
-confirmResize = read
-revertResize = update
-createImage = create
# map endpoint type defined in service catalog to CADF typeURI
[service_endpoints]
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())