diff options
5 files changed, 414 insertions, 0 deletions
diff --git a/keystonemiddleware-moon/keystonemiddleware/moon_agent.py b/keystonemiddleware-moon/keystonemiddleware/moon_agent.py new file mode 100644 index 00000000..de11e3e5 --- /dev/null +++ b/keystonemiddleware-moon/keystonemiddleware/moon_agent.py @@ -0,0 +1,308 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +import webob +import logging +import json +import re +import httplib + +from cStringIO import StringIO +from oslo_config import cfg +from keystonemiddleware.i18n import _, _LC, _LE, _LI, _LW + + +_OPTS = [ + cfg.StrOpt('auth_uri', + default="http://127.0.0.1:35357/v3", + help='Complete public Identity API endpoint.'), + cfg.StrOpt('auth_version', + default=None, + help='API version of the admin Identity API endpoint.'), + cfg.StrOpt('keystonemiddleware_agent_logfile', # TODO: update in the paste.ini + default="/tmp/moon_keystonemiddleware_agent.log", + help='File where logs goes.'), + ] + +_MOON_KEYSTONEMIDDLEWARE_AGENT_GROUP = 'moon_keystonemiddleware_agent' +CONF = cfg.CONF +CONF.register_opts(_OPTS, group=_MOON_KEYSTONEMIDDLEWARE_AGENT_GROUP) +CONF.debug = True + +# from http://developer.openstack.org/api-ref-objectstorage-v1.html +SWIFT_API = ( + ("^/v1/(?P<account>[\w_-]+)$", "GET", "get_account_details"), + ("^/v1/(?P<account>[\w_-]+)$", "POST", "modify_account"), + ("^/v1/(?P<account>[\w_-]+)$", "HEAD", "get_account"), + ("^/v1/(?P<account>[\w_-]+)/(?P<container>[\w-]+)$", "GET", "get_container"), + ("^/v1/(?P<account>[\w_-]+)/(?P<container>[\w-]+)$", "PUT", "create_container"), + ("^/v1/(?P<account>[\w_-]+)/(?P<container>[\w-]+)$", "POST", "update_container_metadata"), + ("^/v1/(?P<account>[\w_-]+)/(?P<container>[\w-]+)$", "DELETE", "delete_container"), + ("^/v1/(?P<account>[\w_-]+)/(?P<container>[\w-]+)$", "HEAD", "get_container_metadata"), + ("^/v1/(?P<account>[\w_-]+)/(?P<container>[\w-]+)/(?P<object>.+)$", "GET", "get_object"), + ("^/v1/(?P<account>[\w_-]+)/(?P<container>[\w-]+)/(?P<object>.+)$", "PUT", "create_object"), + ("^/v1/(?P<account>[\w_-]+)/(?P<container>[\w-]+)/(?P<object>.+)$", "COPY", "copy_object"), + ("^/v1/(?P<account>[\w_-]+)/(?P<container>[\w-]+)/(?P<object>.+)$", "POST", "update_object_metadata"), + ("^/v1/(?P<account>[\w_-]+)/(?P<container>[\w-]+)/(?P<object>.+)$", "DELETE", "delete_object"), + ("^/v1/(?P<account>[\w_-]+)/(?P<container>[\w-]+)/(?P<object>.+)$", "HEAD", "get_object_metadata"), +) + + +class MoonAgentKeystoneMiddleware(object): + """Moon's agent for KeystoneMiddleware to interact calls.""" + + post_data = { + "auth": { + "identity": { + "methods": [ + "password" + ], + "password": { + "user": { + "domain": { + "id": "Default" + }, + "name": "admin", + "password": "nomoresecrete" + } + } + }, + "scope": { + "project": { + "domain": { + "id": "Default" + }, + "name": "demo" + } + } + } + } + + def __init__(self, app, conf): + self.conf = conf + self._LOG = logging.getLogger(conf.get('log_name', __name__)) + # FIXME: events are duplicated in log file + moon_agent_fh = logging.FileHandler(CONF.moon_keystonemiddleware_agent["keystonemiddleware_agent_logfile"]) + self._LOG.setLevel(logging.DEBUG) + self._LOG.addHandler(moon_agent_fh) + self._LOG.info(_LI('Starting Moon KeystoneMiddleware Agent')) + self._conf = conf + self._app = app + + # Auth + self.auth_host = conf.get('auth_host', "127.0.0.1") + self.auth_port = int(conf.get('auth_port', 35357)) + auth_protocol = conf.get('auth_protocol', 'http') + self._request_uri = '%s://%s:%s' % (auth_protocol, self.auth_host, # TODO: ??? for auth or authz + self.auth_port) + + # SSL + insecure = conf.get('insecure', False) + cert_file = conf.get('certfile') + key_file = conf.get('keyfile') + + if insecure: + self._verify = False + elif cert_file and key_file: + self._verify = (cert_file, key_file) + elif cert_file: + self._verify = cert_file + else: + self._verify = None + + # Moon registered mgrs + self.local_registered_mgr_dict = dict() # TODO: load from the sql backend + + def __set_token(self): + data = self.get_url("/v3/auth/tokens", post_data=self.post_data) + if "token" not in data: + raise Exception("Authentication problem ({})".format(data)) + self.token = data["token"] + + def __unset_token(self): + data = self.get_url("/v3/auth/tokens", method="DELETE", authtoken=True) + if "content" in data and len(data["content"]) > 0: + self._LOG.error("Error while unsetting token {}".format(data["content"])) + self.token = None + + def get_url(self, url, post_data=None, delete_data=None, method="GET", authtoken=None): + if post_data: + method = "POST" + if delete_data: + method = "DELETE" + self._LOG.debug("\033[32m{} {}\033[m".format(method, url)) + conn = httplib.HTTPConnection(self.auth_host, self.auth_port) + headers = { + "Content-type": "application/x-www-form-urlencoded", + "Accept": "text/plain,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + } + if authtoken: + if self.x_subject_token: + if method == "DELETE": + headers["X-Subject-Token"] = self.x_subject_token + headers["X-Auth-Token"] = self.x_subject_token + else: + headers["X-Auth-Token"] = self.x_subject_token + if post_data: + method = "POST" + headers["Content-type"] = "application/json" + post_data = json.dumps(post_data) + conn.request(method, url, post_data, headers=headers) + elif delete_data: + method = "DELETE" + conn.request(method, url, json.dumps(delete_data), headers=headers) + else: + conn.request(method, url, headers=headers) + resp = conn.getresponse() + headers = resp.getheaders() + try: + self.x_subject_token = dict(headers)["x-subject-token"] + except KeyError: + pass + content = resp.read() + conn.close() + try: + return json.loads(content) + except ValueError: + return {"content": content} + + def _find_openstack_component(self, env): + if "nova.context" in env.keys(): + return "nova" + elif "swift.authorize" in env.keys(): + return "swift" + else: + self._LOG.debug(env.keys()) + return "unknown" + + def _get_action(self, env, component): + """ Find and return the action of the request + Actually, find only Nova action (start, destroy, pause, unpause, ...) + + :param env: the request + :return: the action or "" + """ + action = "" + self.input = "" + if component == "nova": + length = int(env.get('CONTENT_LENGTH', '0')) + # TODO (dthom): compute for Nova, Cinder, Neutron, ... + action = "" + if length > 0: + try: + sub_action_object = env['wsgi.input'].read(length) + self.input = sub_action_object + action = json.loads(sub_action_object).keys()[0] + body = StringIO(sub_action_object) + env['wsgi.input'] = body + except ValueError: + self._LOG.error("Error in decoding sub-action") + except Exception as e: + self._LOG.error(str(e)) + if not action or len(action) == 0 and "servers/detail" in env["PATH_INFO"]: + return "list" + if component == "swift": + path = env["PATH_INFO"] + method = env["REQUEST_METHOD"] + for api in SWIFT_API: + if re.match(api[0], path) and method == api[1]: + action = api[2] + length = int(env.get('CONTENT_LENGTH', '0')) + # TODO (dthom): compute for Nova, Cinder, Neutron, ... + _action = "" + if length > 0: + try: + sub_action_object = env['wsgi.input'].read(length) + self.input = sub_action_object + _action = json.loads(sub_action_object).keys()[0] + body = StringIO(sub_action_object) + env['wsgi.input'] = body + self._LOG.debug("wsgi.input={}".format(_action)) + except ValueError: + self._LOG.error("Error in decoding sub-action") + except Exception as e: + self._LOG.error(str(e)) + return action + + @staticmethod + def _get_resource(env, component): + if component == "nova": + # http://developer.openstack.org/api-ref-compute-v2.1.html + # nova URLs: + # /<tenant_id>/servers/<server_id> + # list details for server_id + # /<tenant_id>/servers/<server_id>/action + # execute action to server_id + # /<tenant_id>/servers/<server_id>/metadata + # show metadata from server_id + # /<tenant_id>/servers/details + # list servers + url = env.get("PATH_INFO").split("/") + if url[-1] == "detail": + return "servers" + try: + return url[3] + except IndexError: + return + elif component == "swift": + # remove the "/v1/" part of the URL + return env.get("PATH_INFO").split("/", 2)[-1].replace("/", "-").replace(".", "-") + return "unknown" + + def __call__(self, env, start_response): + req = webob.Request(env) + agent_data = dict() + + agent_data['user_id'] = env.get("HTTP_X_USER_ID") + if not agent_data['user_id']: + self._LOG.warning("No user_id found for {}".format(env.get("PATH_INFO"))) + return self._app(env, start_response) + + agent_data['tenant_id'] = env.get("HTTP_X_TENANT_ID") + if not agent_data['tenant_id']: + self._LOG.warning("No tenant_id found for {}".format(env.get("PATH_INFO"))) + return self._app(env, start_response) + + agent_data['OS_component'] = self._find_openstack_component(env) + + agent_data['action_id'] = self._get_action(env, agent_data['OS_component']) + if not agent_data['action_id']: + self._LOG.warning("No action_id found for {}".format(env.get("PATH_INFO"))) + # If action is not found, we can't raise an exception because a lots of action is missing + # in function self._get_action, it is not possible to get them all. + return self._app(env, start_response) + + agent_data['resource_id'] = self._get_resource(env, agent_data['OS_component']) + if not agent_data['resource_id'] : + self._LOG.warning("No resource_id found for {}".format(env.get("PATH_INFO"))) + return self._app(env, start_response) + else: + self._LOG.debug("resource_id={}".format(agent_data['resource_id'])) + + self.__set_token() + for _mgr in self.local_registered_mgr_dict: # TODO: update from the sql backend + self.local_registered_mgr_dict[_mgr]['response_content'] = \ + json.loads(self.local_registered_mgr_dict[_mgr].treat_request(self.x_subject_token, agent_data).content) + self.__unset_token() + + aggregate_result = 1 + for _mgr in self.local_registered_mgr_dict: + if not self.local_registered_mgr_dict[_mgr]['response_content']: + aggregate_result = 0 + + if aggregate_result: + return self._app(env, start_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 moon_agent_filter(app): + return MoonAgentKeystoneMiddleware(app, conf) + return moon_agent_filter + + diff --git a/keystonemiddleware-moon/keystonemiddleware/moon_mgrs/__init__.py b/keystonemiddleware-moon/keystonemiddleware/moon_mgrs/__init__.py new file mode 100644 index 00000000..10d80bc9 --- /dev/null +++ b/keystonemiddleware-moon/keystonemiddleware/moon_mgrs/__init__.py @@ -0,0 +1 @@ +__author__ = 'wukong' diff --git a/keystonemiddleware-moon/keystonemiddleware/moon_mgrs/abe_mgr/__init__.py b/keystonemiddleware-moon/keystonemiddleware/moon_mgrs/abe_mgr/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/keystonemiddleware-moon/keystonemiddleware/moon_mgrs/abe_mgr/__init__.py diff --git a/keystonemiddleware-moon/keystonemiddleware/moon_mgrs/authz_mgr/__init__.py b/keystonemiddleware-moon/keystonemiddleware/moon_mgrs/authz_mgr/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/keystonemiddleware-moon/keystonemiddleware/moon_mgrs/authz_mgr/__init__.py diff --git a/keystonemiddleware-moon/keystonemiddleware/moon_mgrs/authz_mgr/authz_mgr.py b/keystonemiddleware-moon/keystonemiddleware/moon_mgrs/authz_mgr/authz_mgr.py new file mode 100644 index 00000000..af519225 --- /dev/null +++ b/keystonemiddleware-moon/keystonemiddleware/moon_mgrs/authz_mgr/authz_mgr.py @@ -0,0 +1,105 @@ +import logging +import requests +import six +import webob +import json + +from keystone import exception +from keystonemiddleware.i18n import _, _LC, _LE, _LI, _LW +from oslo_config import cfg + + +_OPTS = [ + cfg.StrOpt('authz_mgr_login', + default="admin", + help='Name of the administrator who will connect to the Keystone Moon backends.'), + cfg.StrOpt('authz_mgr_password', + default="nomoresecrete", + help='Password of the administrator who will connect to the Keystone Moon backends.'), + cfg.StrOpt('authz_mgr_logfile', + default="/tmp/moon_authz_mgr.log", # TODO: update in paste.init + help='File where logs goes.'), + ] + +_MOON_AUTHZ_MGR_GROUP = 'moon_authz_mgr' +CONF = cfg.CONF +CONF.register_opts(_OPTS, group=_MOON_AUTHZ_MGR_GROUP) +CONF.debug = True + + +class ServiceError(Exception): + pass + + +class AuthzMgr(object): + + def __init__(self, conf): + self._LOG = logging.getLogger(conf.get('log_name', __name__)) + authz_mgr_fh = logging.FileHandler(CONF.moon_authz_mgr["authz_mgr_logfile"]) + self._LOG.setLevel(logging.DEBUG) + self._LOG.addHandler(authz_mgr_fh) + + def _deny_request(self, code): + error_table = { + 'AccessDenied': (401, 'Access denied'), + 'InvalidURI': (400, 'Could not parse the specified URI'), + 'NotFound': (404, 'URI not found'), + 'Error': (500, 'Server error'), + } + resp = webob.Response(content_type='text/xml') + resp.status = error_table[code][0] + error_msg = ('<?xml version="1.0" encoding="UTF-8"?>\r\n' + '<Error>\r\n <Code>%s</Code>\r\n ' + '<Message>%s</Message>\r\n</Error>\r\n' % + (code, error_table[code][1])) + if six.PY3: + error_msg = error_msg.encode() + resp.body = error_msg + return resp + + + def treat_request(self, auth_token, agent_data): + if not agent_data['resource_id']: + agent_data['resource_id'] = "servers" + + headers = {'X-Auth-Token': auth_token} + self._LOG.debug('X-Auth-Token={}'.format(auth_token)) + try: + _url ='{}/v3/OS-MOON/authz/{}/{}/{}/{}'.format( + self._request_uri, + agent_data['tenant_id'], + agent_data['user_id'], + agent_data['resource_id'], + agent_data['action_id']) + self._LOG.info(_url) + response = requests.get(_url, + headers=headers, + verify=self._verify) + except requests.exceptions.RequestException as e: + self._LOG.error(_LI('HTTP connection exception: %s'), e) + resp = self._deny_request('InvalidURI') + raise ServiceError(resp) + + if response.status_code < 200 or response.status_code >= 300: + self._LOG.debug('Keystone reply error: status=%s reason=%s', + response.status_code, response.reason) + if response.status_code == 404: + resp = self._deny_request('NotFound') + elif response.status_code == 401: + resp = self._deny_request('AccessDenied') + else: + resp = self._deny_request('Error') + raise ServiceError(resp) + + elif response.status_code == 200: + answer = json.loads(response.content) + self._LOG.debug("action_id={}/{}".format(agent_data['OS_component'] , agent_data['action_id'])) + self._LOG.debug(answer) + if "authz" in answer and answer["authz"]: + return response + self._LOG.error("You are not authorized to do that! ({})".format(unicode(answer["comment"]))) + raise exception.Unauthorized(message="You are not authorized to do that! ({})".format(unicode(answer["comment"]))) + else: + self._LOG.error("Unable to request Moon ({}: {})".format(response.status_code, response.reason)) + + return response |