From 28dac4595cbdaab82d108992f43321efbb925199 Mon Sep 17 00:00:00 2001 From: Ziad Sawalha Date: Fri, 6 Jan 2012 16:47:08 -0600 Subject: [PATCH] Implement Secure Token Auth - Added OS-KSVALIDATE extension that supports /tokens calls with the token supplied in the headers. Using X-Subject-Token instead of in the URL. - Addresses bug 861854 Change-Id: Ib6798a98683111c8ce7dfd70a99603ddf1f85248 --- doc/source/extensions.rst | 10 + etc/keystone.conf | 2 +- keystone/common/bufferedhttp.py | 2 +- keystone/common/crypt.py | 74 +++++++ .../content/admin/OS-KSVALIDATE-admin.wadl | 192 ++++++++++++++++++ .../content/common/samples/extensions.json | 5 +- .../extensions/admin/hpidm/__init__.py | 10 +- .../extensions/admin/osksvalidate/__init__.py | 39 ++++ .../admin/osksvalidate/extension.json | 21 ++ .../admin/osksvalidate/extension.xml | 15 ++ .../extensions/admin/osksvalidate/handler.py | 47 +++++ keystone/controllers/token.py | 40 ++-- keystone/middleware/auth_token.py | 153 +++++++++++--- keystone/middleware/crypt.py | 74 +++++++ keystone/routers/admin.py | 6 +- 15 files changed, 636 insertions(+), 54 deletions(-) create mode 100644 keystone/common/crypt.py create mode 100644 keystone/content/admin/OS-KSVALIDATE-admin.wadl create mode 100644 keystone/contrib/extensions/admin/osksvalidate/__init__.py create mode 100644 keystone/contrib/extensions/admin/osksvalidate/extension.json create mode 100644 keystone/contrib/extensions/admin/osksvalidate/extension.xml create mode 100644 keystone/contrib/extensions/admin/osksvalidate/handler.py create mode 100644 keystone/middleware/crypt.py diff --git a/doc/source/extensions.rst b/doc/source/extensions.rst index fa6be0dfed..bde6e40480 100644 --- a/doc/source/extensions.rst +++ b/doc/source/extensions.rst @@ -85,6 +85,16 @@ HP-IDM The included extensions are in the process of being rewritten. Currently only osksadm and oskscatalog work with this new extensions design. +OS-KSVALIDATE + + This extensions supports admin calls to /tokens without having to specify + the token ID in the URL. Instead, the ID is supplied in a header called + X-Subject-Token. This is provided as an alternative to address any security + concerns that arise when token IDs are passed as part of the URL which is + often (and by default) logged to insecure media. + + This is an Admin API extension only. + Enabling & Disabling Extensions ------------------------------- diff --git a/etc/keystone.conf b/etc/keystone.conf index d7c0e036db..62643eea0b 100644 --- a/etc/keystone.conf +++ b/etc/keystone.conf @@ -28,7 +28,7 @@ service-header-mappings = { #List of extensions currently loaded. #Refer docs for list of supported extensions. -extensions= osksadm, oskscatalog, hpidm +extensions= osksadm, oskscatalog, hpidm, osksvalidate # Address to bind the API server # TODO Properties defined within app not available via pipeline. diff --git a/keystone/common/bufferedhttp.py b/keystone/common/bufferedhttp.py index c6e3c2ec93..db64175d1b 100644 --- a/keystone/common/bufferedhttp.py +++ b/keystone/common/bufferedhttp.py @@ -36,7 +36,7 @@ from eventlet.green.httplib import CONTINUE, HTTPConnection, HTTPMessage, \ DEFAULT_TIMEOUT = 30 -logger = logging.getLogger(__name__) +logger = logging.getLogger(__name__) # pylint: disable=C0103 # pylint: disable=R0902 diff --git a/keystone/common/crypt.py b/keystone/common/crypt.py new file mode 100644 index 0000000000..bb25620dab --- /dev/null +++ b/keystone/common/crypt.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +""" +Routines for URL-safe encrypting/decrypting + +Keep this file in sync with all copies: +- glance/common/crypt.py +- keystone/middleware/crypt.py +- keystone/common/crypt.py + +""" + +import base64 + +from Crypto.Cipher import AES +from Crypto import Random +from Crypto.Random import random + + +def urlsafe_encrypt(key, plaintext, blocksize=16): + """ + Encrypts plaintext. Resulting ciphertext will contain URL-safe characters + :param key: AES secret key + :param plaintext: Input text to be encrypted + :param blocksize: Non-zero integer multiple of AES blocksize in bytes (16) + + :returns : Resulting ciphertext + """ + def pad(text): + """ + Pads text to be encrypted + """ + pad_length = (blocksize - len(text) % blocksize) + sr = random.StrongRandom() + pad = ''.join(chr(sr.randint(1, 0xFF)) for i in range(pad_length - 1)) + # We use chr(0) as a delimiter between text and padding + return text + chr(0) + pad + + # random initial 16 bytes for CBC + init_vector = Random.get_random_bytes(16) + cypher = AES.new(key, AES.MODE_CBC, init_vector) + padded = cypher.encrypt(pad(str(plaintext))) + return base64.urlsafe_b64encode(init_vector + padded) + + +def urlsafe_decrypt(key, ciphertext): + """ + Decrypts URL-safe base64 encoded ciphertext + :param key: AES secret key + :param ciphertext: The encrypted text to decrypt + + :returns : Resulting plaintext + """ + # Cast from unicode + ciphertext = base64.urlsafe_b64decode(str(ciphertext)) + cypher = AES.new(key, AES.MODE_CBC, ciphertext[:16]) + padded = cypher.decrypt(ciphertext[16:]) + return padded[:padded.rfind(chr(0))] diff --git a/keystone/content/admin/OS-KSVALIDATE-admin.wadl b/keystone/content/admin/OS-KSVALIDATE-admin.wadl new file mode 100644 index 0000000000..c477131d68 --- /dev/null +++ b/keystone/content/admin/OS-KSVALIDATE-admin.wadl @@ -0,0 +1,192 @@ + + + + + + + + + + %common; +]> + + + + + + + + + + + + + + + + + + + + You need a valid admin token for access. + + + You need to supply a token to validate. + + + + + + + + + + You need a valid admin token for access. + + + You need to supply a token to validate. + + + + + + + + + + + + + + + + + +

+ Check that a token is valid and that it belongs to a supplied tenant + and services and return the permissions relevant to a particular client. +

+

+ Behaviour is similar to /tokens/{tokenId}. In + other words, a user should expect an + itemNotFound (404) fault for an + invalid token. +

+

+ 'X-Subject-Token' is encrypted, but can still be used for + caching. This extension will basically decrypt this header and + internally call Keystone's normal validation, passing along all + headers and query parameters. It should therefore support + all exsting calls on /tokens/{tokenId}, including + extensions such as HP-IDM. +

+
+ + + +

+ Validates a token has the supplied tenant in scope. +

+
+ + + +

+ If provided, filter the roles to be returned by the given service IDs. +

+
+ +
+ + + + + + + + + + + + + &commonFaults; + &getFaults; +
+ + +

+ Check that a token is valid and that it belongs to a particular + tenant and services (For performance). +

+

+ Behaviour is similar to /tokens/{tokenId}. In + other words, a user should expect an + itemNotFound (404) fault for an + invalid token. +

+

+ 'X-Subject-Token' is encrypted, but can still be used for + caching. This extension will basically decrypt this header and + internally call Keystone's normal validation, passing along all + headers and query parameters. It should therefore support + all exsting calls on /tokens/{tokenId}, including + extensions such as HP-IDM. +

+

+ No response body is returned for this method. +

+
+ + + +

+ Validates a token has the supplied tenant in scope. (for performance). +

+
+ + + +

+ Check the roles against the given service IDs. +

+
+ +
+ + &commonFaults; + &getFaults; +
+ + +

+ Returns a list of endpoints associated with a specific token. +

+
+ + + + + + + + + + + + + &commonFaults; + &getFaults; +
+ +
diff --git a/keystone/content/common/samples/extensions.json b/keystone/content/common/samples/extensions.json index 75b0f7e8c3..ca46f94101 100644 --- a/keystone/content/common/samples/extensions.json +++ b/keystone/content/common/samples/extensions.json @@ -1,5 +1,6 @@ { - "extensions": [ + "extensions": { + "values": [ { "name": "Reset Password Extension", "namespace": "http://docs.rackspacecloud.com/identity/api/ext/rpe/v2.0", @@ -38,6 +39,6 @@ } ] } - ], + ]}, "extensions_links": [] } diff --git a/keystone/contrib/extensions/admin/hpidm/__init__.py b/keystone/contrib/extensions/admin/hpidm/__init__.py index 59086ef72f..ca31482646 100644 --- a/keystone/contrib/extensions/admin/hpidm/__init__.py +++ b/keystone/contrib/extensions/admin/hpidm/__init__.py @@ -22,12 +22,4 @@ from keystone.controllers.token import TokenController class ExtensionHandler(BaseExtensionHandler): def map_extension_methods(self, mapper, options): - token_controller = TokenController(options) - - # Token Operations - mapper.connect("/tokens/{token_id}", controller=token_controller, - action="validate_token", - conditions=dict(method=["GET"])) - mapper.connect("/tokens/{tenant_id}", - controller=token_controller, - action="check_token", conditions=dict(method=["HEAD"])) + pass diff --git a/keystone/contrib/extensions/admin/osksvalidate/__init__.py b/keystone/contrib/extensions/admin/osksvalidate/__init__.py new file mode 100644 index 0000000000..77c1ebe872 --- /dev/null +++ b/keystone/contrib/extensions/admin/osksvalidate/__init__.py @@ -0,0 +1,39 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# 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 routes.route import Route + +from keystone.contrib.extensions.admin.extension import BaseExtensionHandler +from keystone.contrib.extensions.admin.osksvalidate import handler + + +class ExtensionHandler(BaseExtensionHandler): + def map_extension_methods(self, mapper, options): + extension_controller = handler.SecureValidationController(options) + + # Token Operations + mapper.connect("/OS-KSVALIDATE/token/validate", + controller=extension_controller, + action="handle_validate_request", + conditions=dict(method=["GET"])) + + mapper.connect("/OS-KSVALIDATE/token/endpoints", + controller=extension_controller, + action="handle_endpoints_request", + conditions=dict(method=["GET"])) + # TODO(zns): make this handle all routes by using the mapper diff --git a/keystone/contrib/extensions/admin/osksvalidate/extension.json b/keystone/contrib/extensions/admin/osksvalidate/extension.json new file mode 100644 index 0000000000..b1f0c65bc1 --- /dev/null +++ b/keystone/contrib/extensions/admin/osksvalidate/extension.json @@ -0,0 +1,21 @@ +{ + "extension": { + "name": "Openstack Keystone Admin", + "namespace": "http://docs.openstack.org/identity/api/ext/OS-KSVALIDATE/v1.0", + "alias": "OS-KSVALIDATE", + "updated": "2012-01-12T12:17:00-06:00", + "description": "Openstack extensions to Keystone v2.0 API for Secure Token Validation.", + "links": [ + { + "rel": "describedby", + "type": "application/pdf", + "href": "https://github.com/openstack/keystone/raw/master/keystone/content/admin/OS-KSVALIDATE-admin-devguide.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "https://github.com/openstack/keystone/raw/master/keystone/content/admin/OS-KSVALIDATE-admin.wadl" + } + ] + } +} diff --git a/keystone/contrib/extensions/admin/osksvalidate/extension.xml b/keystone/contrib/extensions/admin/osksvalidate/extension.xml new file mode 100644 index 0000000000..6643484a15 --- /dev/null +++ b/keystone/contrib/extensions/admin/osksvalidate/extension.xml @@ -0,0 +1,15 @@ + + + + Openstack extensions to Keystone v2.0 + API for Secure Token Validation. + + + + diff --git a/keystone/contrib/extensions/admin/osksvalidate/handler.py b/keystone/contrib/extensions/admin/osksvalidate/handler.py new file mode 100644 index 0000000000..9132c474ff --- /dev/null +++ b/keystone/contrib/extensions/admin/osksvalidate/handler.py @@ -0,0 +1,47 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (c) 2010-2011 OpenStack, LLC. +# +# 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. + + +""" +Router & Controller for handling Secure Token Validation + +""" +import logging + +from keystone.common import wsgi +from keystone.controllers.token import TokenController + +logger = logging.getLogger(__name__) # pylint: disable=C0103 + + +class SecureValidationController(wsgi.Controller): + """Controller for Tenant related operations""" + + # pylint: disable=W0231 + def __init__(self, options): + self.options = options + self.token_controller = TokenController(options) + + logger.info("Initializing Secure Token Validation extension") + + def handle_validate_request(self, req): + token_id = req.headers.get("X-Subject-Token") + return self.token_controller.validate_token(req=req, token_id=token_id) + + def handle_endpoints_request(self, req): + token_id = req.headers.get("X-Subject-Token") + return self.token_controller.endpoints(req=req, token_id=token_id) diff --git a/keystone/controllers/token.py b/keystone/controllers/token.py index 2386f6e21a..44eff4aa4a 100644 --- a/keystone/controllers/token.py +++ b/keystone/controllers/token.py @@ -78,7 +78,7 @@ class TokenController(wsgi.Controller): """Undecorated EC2 handler""" creds = utils.get_normalized_request_content(auth.Ec2Credentials, req) return utils.send_result(200, req, - self.identity_service.authenticate_ec2(creds)) + self.identity_service.authenticate_ec2(creds)) def _validate_token(self, req, token_id): """Validates the token, and that it belongs to the specified tenant""" @@ -88,29 +88,41 @@ class TokenController(wsgi.Controller): # service IDs are only relevant if hpidm extension is enabled service_ids = req.GET.get('HP-IDM-serviceId') return self.identity_service.validate_token( - utils.get_auth_token(req), token_id, belongs_to, service_ids) + utils.get_auth_token(req), token_id, belongs_to, service_ids) @utils.wrap_error def validate_token(self, req, token_id): - result = self._validate_token(req, token_id) - return utils.send_result(200, req, result) + if self.options.get('disable_tokens_in_url'): + fault.ServiceUnavailableFault() + else: + result = self._validate_token(req, token_id) + return utils.send_result(200, req, result) @utils.wrap_error def check_token(self, req, token_id): """Validates the token, but only returns a status code (HEAD)""" - self._validate_token(req, token_id) - return utils.send_result(200, req) + if self.options.get('disable_tokens_in_url'): + fault.ServiceUnavailableFault() + else: + self._validate_token(req, token_id) + return utils.send_result(200, req) @utils.wrap_error def delete_token(self, req, token_id): - return utils.send_result(204, req, - self.identity_service.revoke_token(utils.get_auth_token(req), - token_id)) + if self.options.get('disable_tokens_in_url'): + fault.ServiceUnavailableFault() + else: + return utils.send_result(204, req, + self.identity_service.revoke_token( + utils.get_auth_token(req), token_id)) @utils.wrap_error def endpoints(self, req, token_id): - marker, limit, url = get_marker_limit_and_url(req) - return utils.send_result(200, req, - self.identity_service.get_endpoints_for_token( - utils.get_auth_token(req), - token_id, marker, limit, url)) + if self.options.get('disable_tokens_in_url'): + fault.ServiceUnavailableFault() + else: + marker, limit, url = get_marker_limit_and_url(req) + return utils.send_result(200, req, + self.identity_service.get_endpoints_for_token( + utils.get_auth_token(req), + token_id, marker, limit, url)) diff --git a/keystone/middleware/auth_token.py b/keystone/middleware/auth_token.py index daedc08c8f..d3ccc3a52d 100644 --- a/keystone/middleware/auth_token.py +++ b/keystone/middleware/auth_token.py @@ -98,8 +98,10 @@ HTTP_X_ROLES from datetime import datetime from dateutil import parser +import errno import eventlet from eventlet import wsgi +from httplib import HTTPException import json # memcache is imported in __init__ if memcache caching is configured import logging @@ -111,7 +113,6 @@ from urlparse import urlparse from webob.exc import HTTPUnauthorized from webob.exc import Request, Response -import keystone.tools.tracer # @UnusedImport # module runs on import from keystone.common.bufferedhttp import http_connect_raw as http_connect logger = logging.getLogger(__name__) # pylint: disable=C0103 @@ -130,9 +131,14 @@ class TokenExpired(Exception): pass +class KeystoneUnreachable(Exception): + pass + + class AuthProtocol(object): """Auth Middleware that handles authenticating client calls""" + # pylint: disable=W0613 def _init_protocol_common(self, app, conf): """ Common initialization code @@ -151,9 +157,9 @@ class AuthProtocol(object): self.service_host = conf.get('service_host') service_port = conf.get('service_port') service_ids = conf.get('service_ids') - self.serviceId_qs = '' + self.service_id_querystring = '' if service_ids: - self.serviceId_qs = '?HP-IDM-serviceId=%s' % \ + self.service_id_querystring = '?HP-IDM-serviceId=%s' % \ (urllib.quote(service_ids)) if service_port: self.service_port = int(service_port) @@ -175,7 +181,7 @@ class AuthProtocol(object): self.auth_host = conf.get('auth_host') self.auth_port = int(conf.get('auth_port')) self.auth_protocol = conf.get('auth_protocol', 'https') - self.auth_timeout = conf.get('auth_timeout', 30) + self.auth_timeout = float(conf.get('auth_timeout', 30)) # where to tell clients to find the auth service (default to url # constructed based on endpoint we have for the service to use) @@ -198,6 +204,9 @@ class AuthProtocol(object): if self.memcache_hosts: if self.cache is None: self.cache = "keystone.cache" + self.tested_for_osksvalidate = False + self.last_test_for_osksvalidate = None + self.osksvalidate = self._supports_osksvalidate() def __init__(self, app, conf): """ Common initialization code """ @@ -226,6 +235,10 @@ class AuthProtocol(object): self.service_protocol = None self.service_timeout = None self.service_url = None + self.service_id_querystring = None + self.osksvalidate = None + self.tested_for_osksvalidate = None + self.last_test_for_osksvalidate = None self.cache = None self.memcache_hosts = None self._init_protocol_common(app, conf) # Applies to all protocols @@ -243,6 +256,18 @@ class AuthProtocol(object): memcache_client = memcache.Client([self.memcache_hosts]) env[self.cache] = memcache_client + # Check if we're set up to use OS-KSVALIDATE periodically if not on + if self.tested_for_osksvalidate != True: + if self.last_test_for_osksvalidate is None or \ + (time.time() - self.last_test_for_osksvalidate) > 60: + # Try test again every 60 seconds if failed + # this also handles if middleware was started before + # the keystone server + try: + self.osksvalidate = self._supports_osksvalidate() + except (HTTPException, StandardError): + pass + #Prep headers to forward request to local or remote downstream service proxy_headers = env.copy() for header in proxy_headers.iterkeys(): @@ -322,11 +347,13 @@ class AuthProtocol(object): """ Convert datetime to unix timestamp for caching """ return time.mktime(parser.parse(date).utctimetuple()) + # pylint: disable=W0613 @staticmethod def _protect_claims(token, claims): """ encrypt or mac claims if necessary """ return claims + # pylint: disable=W0613 @staticmethod def _unprotect_claims(token, pclaims): """ decrypt or demac claims if necessary """ @@ -346,8 +373,7 @@ class AuthProtocol(object): else: # normal memcache client expires = self._convert_date(claims['expires']) - delta = expires - time.time() - timeout = delta.seconds + timeout = expires - time.time() if timeout > MAX_CACHE_TIME or not valid: # Limit cache to one day (and cache bad tokens for a day) timeout = MAX_CACHE_TIME @@ -429,21 +455,44 @@ class AuthProtocol(object): headers = {"Content-type": "application/json", "Accept": "application/json", "X-Auth-Token": self.admin_token} - ##TODO(ziad):we need to figure out how to auth to keystone - #since validate_token is a priviledged call - #Khaled's version uses creds to get a token - # "X-Auth-Token": admin_token} - # we're using a test token from the ini file for now - logger.debug("Connecting to %s://%s:%s to check claims" % ( - self.auth_protocol, self.auth_host, self.auth_port)) - conn = http_connect(self.auth_host, self.auth_port, 'GET', - '/v2.0/tokens/%s%s' % (claims, self.serviceId_qs), - headers=headers, - ssl=(self.auth_protocol == 'https'), - key_file=self.key_file, cert_file=self.cert_file, - timeout=self.auth_timeout) - resp = conn.getresponse() - data = resp.read() + if self.osksvalidate: + headers['X-Subject-Token'] = claims + path = '/v2.0/OS-KSVALIDATE/token/validate/%s' % \ + self.service_id_querystring + logger.debug("Connecting to %s://%s:%s to check claims using the" + "OS-KSVALIDATE extension" % (self.auth_protocol, + self.auth_host, self.auth_port)) + else: + path = '/v2.0/tokens/%s%s' % (claims, self.service_id_querystring) + logger.debug("Connecting to %s://%s:%s to check claims" % ( + self.auth_protocol, self.auth_host, self.auth_port)) + + ##TODO(ziad):we need to figure out how to auth to keystone + #since validate_token is a priviledged call + #Khaled's version uses creds to get a token + # "X-Auth-Token": admin_token} + # we're using a test token from the ini file for now + try: + conn = http_connect(self.auth_host, self.auth_port, 'GET', + path, + headers=headers, + ssl=(self.auth_protocol == 'https'), + key_file=self.key_file, + cert_file=self.cert_file, + timeout=self.auth_timeout) + resp = conn.getresponse() + data = resp.read() + except EnvironmentError as exc: + if exc.errno == errno.ECONNREFUSED: + logger.error("Keystone server not responding on %s://%s:%s " + "to check claims" % (self.auth_protocol, + self.auth_host, + self.auth_port)) + raise KeystoneUnreachable("Unable to connect to authentication" + " server") + else: + logger.exception(exc) + raise logger.debug("Response received: %s" % resp.status) if not str(resp.status).startswith('20'): @@ -532,6 +581,7 @@ class AuthProtocol(object): req = Request(proxy_headers) parsed = urlparse(req.url) + # pylint: disable=E1101 conn = http_connect(self.service_host, self.service_port, req.method, @@ -557,6 +607,50 @@ class AuthProtocol(object): return Response(status=resp.status, body=data)(env, start_response) + def _supports_osksvalidate(self): + """Check if target Keystone server supports OS-KSVALIDATE.""" + if self.tested_for_osksvalidate: + return self.osksvalidate + + headers = {"Accept": "application/json"} + logger.debug("Connecting to %s://%s:%s to check extensions" % ( + self.auth_protocol, self.auth_host, self.auth_port)) + try: + self.last_test_for_osksvalidate = time.time() + conn = http_connect(self.auth_host, self.auth_port, 'GET', + '/v2.0/extensions/', + headers=headers, + ssl=(self.auth_protocol == 'https'), + key_file=self.key_file, + cert_file=self.cert_file, + timeout=self.auth_timeout) + resp = conn.getresponse() + data = resp.read() + + logger.debug("Response received: %s" % resp.status) + if not str(resp.status).startswith('20'): + logger.debug("Failed to detect extensions. " + "Falling back to core API") + return False + except EnvironmentError as exc: + if exc.errno == errno.ECONNREFUSED: + logger.warning("Keystone server not responding. Extension " + "detection will be retried later.") + else: + logger.exception("Unexpected error trying to detect " + "extensions.") + logger.debug("Falling back to core API behavior (using tokens in " + "URL)") + return False + except HTTPException as exc: + logger.exception("Error trying to detect extensions.") + logger.debug("Falling back to core API behavior (using tokens in " + "URL)") + return False + + self.tested_for_osksvalidate = True + return "OS-KSVALIDATE" in data + def filter_factory(global_conf, **local_conf): """Returns a WSGI filter app for use with paste.deploy.""" @@ -573,11 +667,20 @@ def app_factory(global_conf, **local_conf): conf.update(local_conf) return AuthProtocol(None, conf) -if __name__ == "__main__": - wsgiapp = loadapp("config:" + \ - os.path.join(os.path.abspath(os.path.dirname(__file__)), + +def main(): + """Called when the middleware is started up separately (as in a remote + proxy configuration) + """ + config_file = os.path.join(os.path.abspath(os.path.dirname(__file__)), os.pardir, os.pardir, - "examples/paste/auth_token.ini"), + "examples/paste/auth_token.ini") + logger.debug("Initializing with config file: %s" % config_file) + wsgiapp = loadapp("config:%s" % config_file, global_conf={"log_name": "auth_token.log"}) wsgi.server(eventlet.listen(('', 8090)), wsgiapp) + + +if __name__ == "__main__": + main() diff --git a/keystone/middleware/crypt.py b/keystone/middleware/crypt.py new file mode 100644 index 0000000000..bb25620dab --- /dev/null +++ b/keystone/middleware/crypt.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +""" +Routines for URL-safe encrypting/decrypting + +Keep this file in sync with all copies: +- glance/common/crypt.py +- keystone/middleware/crypt.py +- keystone/common/crypt.py + +""" + +import base64 + +from Crypto.Cipher import AES +from Crypto import Random +from Crypto.Random import random + + +def urlsafe_encrypt(key, plaintext, blocksize=16): + """ + Encrypts plaintext. Resulting ciphertext will contain URL-safe characters + :param key: AES secret key + :param plaintext: Input text to be encrypted + :param blocksize: Non-zero integer multiple of AES blocksize in bytes (16) + + :returns : Resulting ciphertext + """ + def pad(text): + """ + Pads text to be encrypted + """ + pad_length = (blocksize - len(text) % blocksize) + sr = random.StrongRandom() + pad = ''.join(chr(sr.randint(1, 0xFF)) for i in range(pad_length - 1)) + # We use chr(0) as a delimiter between text and padding + return text + chr(0) + pad + + # random initial 16 bytes for CBC + init_vector = Random.get_random_bytes(16) + cypher = AES.new(key, AES.MODE_CBC, init_vector) + padded = cypher.encrypt(pad(str(plaintext))) + return base64.urlsafe_b64encode(init_vector + padded) + + +def urlsafe_decrypt(key, ciphertext): + """ + Decrypts URL-safe base64 encoded ciphertext + :param key: AES secret key + :param ciphertext: The encrypted text to decrypt + + :returns : Resulting plaintext + """ + # Cast from unicode + ciphertext = base64.urlsafe_b64decode(str(ciphertext)) + cypher = AES.new(key, AES.MODE_CBC, ciphertext[:16]) + padded = cypher.decrypt(ciphertext[16:]) + return padded[:padded.rfind(chr(0))] diff --git a/keystone/routers/admin.py b/keystone/routers/admin.py index c46584bc52..1a0db7eefe 100755 --- a/keystone/routers/admin.py +++ b/keystone/routers/admin.py @@ -39,6 +39,9 @@ class AdminApi(wsgi.Router): logger.debug("Init with options=%s" % options) mapper = routes.Mapper() + # Load extensions first so they can override core if they need to + extension.get_extension_configurer().configure(mapper, options) + # Token Operations auth_controller = TokenController(options) mapper.connect("/tokens", controller=auth_controller, @@ -50,7 +53,7 @@ class AdminApi(wsgi.Router): mapper.connect("/tokens/{token_id}", controller=auth_controller, action="check_token", conditions=dict(method=["HEAD"])) - # Do we need this.API doesn't have delete token. + # Do we need this. API doesn't have delete token. mapper.connect("/tokens/{token_id}", controller=auth_controller, action="delete_token", conditions=dict(method=["DELETE"])) @@ -141,5 +144,4 @@ class AdminApi(wsgi.Router): action="get_static_file", root="content/common/", path="samples/", conditions=dict(method=["GET"])) - extension.get_extension_configurer().configure(mapper, options) super(AdminApi, self).__init__(mapper)