summaryrefslogtreecommitdiff
path: root/keystonemiddleware/audit
diff options
context:
space:
mode:
authorJamie Lennox <jamielennox@gmail.com>2016-06-27 09:43:44 +1000
committerJamie Lennox <jamielennox@gmail.com>2016-06-27 09:45:57 +1000
commitaa2cde7f9f504a34fee1e9c3d4b952f2c127ea89 (patch)
treeeddb229397e92b9ba89f0b0ecabe17fe747cdfbd /keystonemiddleware/audit
parentdcb5b240a3a727120c5ccdd987ac797ed62249f4 (diff)
downloadkeystonemiddleware-aa2cde7f9f504a34fee1e9c3d4b952f2c127ea89.tar.gz
Break out the API piece into its own file
Refactor the API object out of the audit middleware into its own file. Change-Id: Iddeb91db48c718d749d878ebfbe09f6a3a143229
Diffstat (limited to 'keystonemiddleware/audit')
-rw-r--r--keystonemiddleware/audit/__init__.py256
-rw-r--r--keystonemiddleware/audit/_api.py263
2 files changed, 274 insertions, 245 deletions
diff --git a/keystonemiddleware/audit/__init__.py b/keystonemiddleware/audit/__init__.py
index 5206855..8a64a92 100644
--- a/keystonemiddleware/audit/__init__.py
+++ b/keystonemiddleware/audit/__init__.py
@@ -19,13 +19,10 @@ in the pipeline so that it can utilise the information the Identity server
provides.
"""
-import ast
-import collections
import copy
import functools
import logging
import os.path
-import re
import sys
from oslo_config import cfg
@@ -38,7 +35,6 @@ except ImportError:
from pycadf import cadftaxonomy as taxonomy
from pycadf import cadftype
from pycadf import credential
-from pycadf import endpoint
from pycadf import eventfactory as factory
from pycadf import host
from pycadf import identifier
@@ -47,13 +43,11 @@ from pycadf import reporterstep
from pycadf import resource
from pycadf import tag
from pycadf import timestamp
-import six
-from six.moves import configparser
-from six.moves.urllib import parse as urlparse
import webob.dec
from keystonemiddleware._common import config
-from keystonemiddleware.i18n import _LE, _LI, _LW
+from keystonemiddleware.audit import _api
+from keystonemiddleware.i18n import _LE, _LI
_LOG = None
@@ -90,236 +84,6 @@ def _log_and_ignore_error(fn):
return wrapper
-Service = collections.namedtuple('Service',
- ['id', 'name', 'type', 'admin_endp',
- 'public_endp', 'private_endp'])
-
-
-AuditMap = collections.namedtuple('AuditMap',
- ['path_kw',
- 'custom_actions',
- 'service_endpoints',
- 'default_target_endpoint_type'])
-
-
-# NOTE(blk-u): Compatibility for Python 2. SafeConfigParser and
-# SafeConfigParser.readfp are deprecated in Python 3. Remove this when we drop
-# support for Python 2.
-if six.PY2:
- class _ConfigParser(configparser.SafeConfigParser):
- read_file = configparser.SafeConfigParser.readfp
-else:
- _ConfigParser = configparser.ConfigParser
-
-
-class OpenStackAuditApi(object):
-
- def __init__(self, cfg_file):
- """Configure to recognize and map known api paths."""
- path_kw = {}
- custom_actions = {}
- endpoints = {}
- default_target_endpoint_type = None
-
- if cfg_file:
- try:
- map_conf = _ConfigParser()
- map_conf.read_file(open(cfg_file))
-
- try:
- default_target_endpoint_type = map_conf.get(
- 'DEFAULT', 'target_endpoint_type')
- except configparser.NoOptionError: # nosec
- # Ignore the undefined config option,
- # default_target_endpoint_type remains None which is valid.
- pass
-
- try:
- custom_actions = dict(map_conf.items('custom_actions'))
- except configparser.Error: # nosec
- # custom_actions remains {} which is valid.
- pass
-
- try:
- path_kw = dict(map_conf.items('path_keywords'))
- except configparser.Error: # nosec
- # path_kw remains {} which is valid.
- pass
-
- try:
- endpoints = dict(map_conf.items('service_endpoints'))
- except configparser.Error: # nosec
- # endpoints remains {} which is valid.
- pass
- except configparser.ParsingError as err:
- raise PycadfAuditApiConfigError(
- 'Error parsing audit map file: %s' % err)
- self._MAP = AuditMap(
- path_kw=path_kw, custom_actions=custom_actions,
- service_endpoints=endpoints,
- default_target_endpoint_type=default_target_endpoint_type)
-
- @staticmethod
- def _clean_path(value):
- """Clean path if path has json suffix."""
- return value[:-5] if value.endswith('.json') else value
-
- 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 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;
- - 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 = req.path[:-1] if req.path.endswith('/') else req.path
- url_ending = self._clean_path(path[path.rfind('/') + 1:])
- method = req.method
-
- 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':
- try:
- if req.json:
- body_action = list(req.json.keys())[0]
- action = taxonomy.ACTION_UPDATE + '/' + body_action
- else:
- action = taxonomy.ACTION_CREATE
- except ValueError:
- action = taxonomy.ACTION_CREATE
- elif url_ending not in self._MAP.path_kw:
- action = taxonomy.ACTION_UPDATE
- else:
- action = taxonomy.ACTION_CREATE
- elif method == 'GET':
- if url_ending in self._MAP.path_kw:
- action = taxonomy.ACTION_LIST
- else:
- action = taxonomy.ACTION_READ
- elif method == 'PUT' or method == 'PATCH':
- 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 _get_service_info(self, endp):
- service = Service(
- type=self._MAP.service_endpoints.get(
- endp['type'],
- taxonomy.UNKNOWN),
- name=endp['name'],
- id=endp['endpoints'][0].get('id', endp['name']),
- admin_endp=endpoint.Endpoint(
- name='admin',
- url=endp['endpoints'][0].get('adminURL', taxonomy.UNKNOWN)),
- private_endp=endpoint.Endpoint(
- name='private',
- url=endp['endpoints'][0].get('internalURL', taxonomy.UNKNOWN)),
- public_endp=endpoint.Endpoint(
- name='public',
- url=endp['endpoints'][0].get('publicURL', taxonomy.UNKNOWN)))
-
- return service
-
- def _build_typeURI(self, req, service_type):
- """Build typeURI of target.
-
- Combines service type and corresponding path for greater detail.
- """
- type_uri = ''
- prev_key = None
- for key in re.split('/', req.path):
- key = self._clean_path(key)
- 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 _build_target(self, req, service):
- """Build target resource."""
- target_typeURI = (
- self._build_typeURI(req, service.type)
- if service.type != taxonomy.UNKNOWN else service.type)
- target = resource.Resource(typeURI=target_typeURI,
- id=service.id, name=service.name)
- if service.admin_endp:
- target.add_address(service.admin_endp)
- if service.private_endp:
- target.add_address(service.private_endp)
- if service.public_endp:
- target.add_address(service.public_endp)
- return target
-
- def get_target_resource(self, req):
- """Retrieve target information.
-
- If discovery is enabled, target will attempt to retrieve information
- from service catalog. If not, the information will be taken from
- given config file.
- """
- service_info = Service(type=taxonomy.UNKNOWN, name=taxonomy.UNKNOWN,
- id=taxonomy.UNKNOWN, admin_endp=None,
- private_endp=None, public_endp=None)
-
- catalog = {}
- try:
- catalog = ast.literal_eval(
- req.environ['HTTP_X_SERVICE_CATALOG'])
- except KeyError:
- _LOG.warning(_LW('Unable to discover target information because '
- 'service catalog is missing. Either the incoming '
- 'request does not contain an auth token or auth '
- 'token does not contain a service catalog. For '
- 'the latter, please make sure the '
- '"include_service_catalog" property in '
- 'auth_token middleware is set to "True"'))
-
- default_endpoint = None
- for endp in catalog:
- endpoint_urls = endp['endpoints'][0]
- admin_urlparse = urlparse.urlparse(
- endpoint_urls.get('adminURL', ''))
- public_urlparse = urlparse.urlparse(
- endpoint_urls.get('publicURL', ''))
- req_url = urlparse.urlparse(req.host_url)
- if (req_url.netloc == admin_urlparse.netloc
- or req_url.netloc == public_urlparse.netloc):
- service_info = self._get_service_info(endp)
- break
- elif (self._MAP.default_target_endpoint_type and
- endp['type'] == self._MAP.default_target_endpoint_type):
- default_endpoint = endp
- else:
- if default_endpoint:
- service_info = self._get_service_info(default_endpoint)
- return self._build_target(req, service_info)
-
-
class ClientResource(resource.Resource):
def __init__(self, project_id=None, **kwargs):
super(ClientResource, self).__init__(**kwargs)
@@ -334,12 +98,6 @@ class KeystoneCredential(credential.Credential):
self.identity_status = identity_status
-class PycadfAuditApiConfigError(Exception):
- """Error raised when pyCADF fails to configure correctly."""
-
- pass
-
-
class AuditMiddleware(object):
"""Create an audit event based on request/response.
@@ -375,7 +133,8 @@ class AuditMiddleware(object):
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 = OpenStackAuditApi(conf.get('audit_map_file'))
+ self._cadf_audit = _api.OpenStackAuditApi(conf.get('audit_map_file'),
+ _LOG)
project = self._conf.project or taxonomy.UNKNOWN
transport_aliases = self._get_aliases(project)
@@ -512,3 +271,10 @@ def filter_factory(global_conf, **local_conf):
def audit_filter(app):
return AuditMiddleware(app, **conf)
return audit_filter
+
+
+# NOTE(jamielennox): Maintained here for public API compatibility.
+Service = _api.Service
+AuditMap = _api.AuditMap
+PycadfAuditApiConfigError = _api.PycadfAuditApiConfigError
+OpenStackAuditApi = _api.OpenStackAuditApi
diff --git a/keystonemiddleware/audit/_api.py b/keystonemiddleware/audit/_api.py
new file mode 100644
index 0000000..ddcff4e
--- /dev/null
+++ b/keystonemiddleware/audit/_api.py
@@ -0,0 +1,263 @@
+# 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 collections
+import re
+
+from pycadf import cadftaxonomy as taxonomy
+from pycadf import endpoint
+from pycadf import resource
+import six
+from six.moves import configparser
+from six.moves.urllib import parse as urlparse
+
+from keystonemiddleware.i18n import _LW
+
+
+# NOTE(blk-u): Compatibility for Python 2. SafeConfigParser and
+# SafeConfigParser.readfp are deprecated in Python 3. Remove this when we drop
+# support for Python 2.
+if six.PY2:
+ class _ConfigParser(configparser.SafeConfigParser):
+ read_file = configparser.SafeConfigParser.readfp
+else:
+ _ConfigParser = configparser.ConfigParser
+
+
+Service = collections.namedtuple('Service',
+ ['id', 'name', 'type', 'admin_endp',
+ 'public_endp', 'private_endp'])
+
+
+AuditMap = collections.namedtuple('AuditMap',
+ ['path_kw',
+ 'custom_actions',
+ 'service_endpoints',
+ 'default_target_endpoint_type'])
+
+
+class PycadfAuditApiConfigError(Exception):
+ """Error raised when pyCADF fails to configure correctly."""
+
+ pass
+
+
+class OpenStackAuditApi(object):
+
+ def __init__(self, cfg_file, log):
+ """Configure to recognize and map known api paths."""
+ path_kw = {}
+ custom_actions = {}
+ endpoints = {}
+ default_target_endpoint_type = None
+
+ if cfg_file:
+ try:
+ map_conf = _ConfigParser()
+ map_conf.read_file(open(cfg_file))
+
+ try:
+ default_target_endpoint_type = map_conf.get(
+ 'DEFAULT', 'target_endpoint_type')
+ except configparser.NoOptionError: # nosec
+ # Ignore the undefined config option,
+ # default_target_endpoint_type remains None which is valid.
+ pass
+
+ try:
+ custom_actions = dict(map_conf.items('custom_actions'))
+ except configparser.Error: # nosec
+ # custom_actions remains {} which is valid.
+ pass
+
+ try:
+ path_kw = dict(map_conf.items('path_keywords'))
+ except configparser.Error: # nosec
+ # path_kw remains {} which is valid.
+ pass
+
+ try:
+ endpoints = dict(map_conf.items('service_endpoints'))
+ except configparser.Error: # nosec
+ # endpoints remains {} which is valid.
+ pass
+ except configparser.ParsingError as err:
+ raise PycadfAuditApiConfigError(
+ 'Error parsing audit map file: %s' % err)
+
+ self._log = log
+ self._MAP = AuditMap(
+ path_kw=path_kw, custom_actions=custom_actions,
+ service_endpoints=endpoints,
+ default_target_endpoint_type=default_target_endpoint_type)
+
+ @staticmethod
+ def _clean_path(value):
+ """Clean path if path has json suffix."""
+ return value[:-5] if value.endswith('.json') else value
+
+ 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 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;
+ - 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 = req.path[:-1] if req.path.endswith('/') else req.path
+ url_ending = self._clean_path(path[path.rfind('/') + 1:])
+ method = req.method
+
+ 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':
+ try:
+ if req.json:
+ body_action = list(req.json.keys())[0]
+ action = taxonomy.ACTION_UPDATE + '/' + body_action
+ else:
+ action = taxonomy.ACTION_CREATE
+ except ValueError:
+ action = taxonomy.ACTION_CREATE
+ elif url_ending not in self._MAP.path_kw:
+ action = taxonomy.ACTION_UPDATE
+ else:
+ action = taxonomy.ACTION_CREATE
+ elif method == 'GET':
+ if url_ending in self._MAP.path_kw:
+ action = taxonomy.ACTION_LIST
+ else:
+ action = taxonomy.ACTION_READ
+ elif method == 'PUT' or method == 'PATCH':
+ 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 _get_service_info(self, endp):
+ service = Service(
+ type=self._MAP.service_endpoints.get(
+ endp['type'],
+ taxonomy.UNKNOWN),
+ name=endp['name'],
+ id=endp['endpoints'][0].get('id', endp['name']),
+ admin_endp=endpoint.Endpoint(
+ name='admin',
+ url=endp['endpoints'][0].get('adminURL', taxonomy.UNKNOWN)),
+ private_endp=endpoint.Endpoint(
+ name='private',
+ url=endp['endpoints'][0].get('internalURL', taxonomy.UNKNOWN)),
+ public_endp=endpoint.Endpoint(
+ name='public',
+ url=endp['endpoints'][0].get('publicURL', taxonomy.UNKNOWN)))
+
+ return service
+
+ def _build_typeURI(self, req, service_type):
+ """Build typeURI of target.
+
+ Combines service type and corresponding path for greater detail.
+ """
+ type_uri = ''
+ prev_key = None
+ for key in re.split('/', req.path):
+ key = self._clean_path(key)
+ 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 _build_target(self, req, service):
+ """Build target resource."""
+ target_typeURI = (
+ self._build_typeURI(req, service.type)
+ if service.type != taxonomy.UNKNOWN else service.type)
+ target = resource.Resource(typeURI=target_typeURI,
+ id=service.id, name=service.name)
+ if service.admin_endp:
+ target.add_address(service.admin_endp)
+ if service.private_endp:
+ target.add_address(service.private_endp)
+ if service.public_endp:
+ target.add_address(service.public_endp)
+ return target
+
+ def get_target_resource(self, req):
+ """Retrieve target information.
+
+ If discovery is enabled, target will attempt to retrieve information
+ from service catalog. If not, the information will be taken from
+ given config file.
+ """
+ service_info = Service(type=taxonomy.UNKNOWN, name=taxonomy.UNKNOWN,
+ id=taxonomy.UNKNOWN, admin_endp=None,
+ private_endp=None, public_endp=None)
+
+ catalog = {}
+ try:
+ catalog = ast.literal_eval(
+ req.environ['HTTP_X_SERVICE_CATALOG'])
+ except KeyError:
+ msg = _LW('Unable to discover target information because '
+ 'service catalog is missing. Either the incoming '
+ 'request does not contain an auth token or auth '
+ 'token does not contain a service catalog. For '
+ 'the latter, please make sure the '
+ '"include_service_catalog" property in '
+ 'auth_token middleware is set to "True"')
+ self._log.warning(msg)
+
+ default_endpoint = None
+ for endp in catalog:
+ endpoint_urls = endp['endpoints'][0]
+ admin_urlparse = urlparse.urlparse(
+ endpoint_urls.get('adminURL', ''))
+ public_urlparse = urlparse.urlparse(
+ endpoint_urls.get('publicURL', ''))
+ req_url = urlparse.urlparse(req.host_url)
+ if (req_url.netloc == admin_urlparse.netloc
+ or req_url.netloc == public_urlparse.netloc):
+ service_info = self._get_service_info(endp)
+ break
+ elif (self._MAP.default_target_endpoint_type and
+ endp['type'] == self._MAP.default_target_endpoint_type):
+ default_endpoint = endp
+ else:
+ if default_endpoint:
+ service_info = self._get_service_info(default_endpoint)
+ return self._build_target(req, service_info)