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
This commit is contained in:
Ziad Sawalha 2012-01-06 16:47:08 -06:00
parent 88b88a9e6f
commit 28dac4595c
15 changed files with 636 additions and 54 deletions

View File

@ -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
-------------------------------

View File

@ -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.

View File

@ -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

74
keystone/common/crypt.py Normal file
View File

@ -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))]

View File

@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--*******************************************************-->
<!-- Import Common XML Entities -->
<!-- -->
<!-- You can resolve the entites with xmllint -->
<!-- -->
<!-- xmllint -noent OS-KSVALIDATE-admin.wadl -->
<!--*******************************************************-->
<!DOCTYPE application [
<!ENTITY % common SYSTEM "https://raw.github.com/openstack/keystone/master/keystone/content/common/common.ent">
%common;
]>
<application xmlns="http://wadl.dev.java.net/2009/02"
xmlns:identity="http://docs.openstack.org/identity/api/v2.0"
xmlns:OS-KSVALIDATE="http://docs.openstack.org/identity/api/ext/OS-KSVALIDATE/v1.0"
xmlns:capi="http://docs.openstack.org/common/api/v1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsdxt="http://docs.rackspacecloud.com/xsd-ext/v1.0"
xsi:schemaLocation="http://docs.openstack.org/identity/api/v2.0 ../common/xsd/api.xsd
http://docs.openstack.org/common/api/v1.0 ../common/xsd/api-common.xsd
http://wadl.dev.java.net/2009/02 http://www.w3.org/Submission/wadl/wadl.xsd
">
<grammars>
<include href="https://raw.github.com/openstack/keystone/master/keystone/content/common/xsd/api.xsd"/>
<include href="https://raw.github.com/openstack/keystone/master/keystone/content/common/xsd/api-common.xsd"/>
</grammars>
<!--*******************************************************-->
<!-- All Resources -->
<!--*******************************************************-->
<!-- We should use SSL in production -->
<resources base="http://localhost:35357">
<resource id="version" path="v2.0">
<resource id="extension" path="OS-KSVALIDATE">
<resource id="token" path="token">
<resource id="validate" path="validate">
<param name="X-Auth-Token" style="header" type="xsd:string" required="true">
<doc>You need a valid admin token for access.</doc>
</param>
<param name="X-Subject-Token" style="header" type="xsd:string" required="true">
<doc>You need to supply a token to validate.</doc>
</param>
<param name="belongsTo" style="query" type="xsd:string" required="false"/>
<param name="HP-IDM-serviceId" style="query" type="xsd:string" required="false"/>
<method href="#validateToken"/>
<method href="#checkToken"/>
</resource>
<resource id="endpointsForToken" path="endpoints">
<param name="X-Auth-Token" style="header" type="xsd:string" required="true">
<doc>You need a valid admin token for access.</doc>
</param>
<param name="X-Subject-Token" style="header" type="xsd:string" required="true">
<doc>You need to supply a token to validate.</doc>
</param>
<param name="HP-IDM-serviceId" style="query" type="xsd:string" required="false"/>
<method href="#listEndpointsForToken"/>
</resource>
</resource>
</resource>
</resource>
</resources>
<!--*******************************************************-->
<!-- All Methods -->
<!--*******************************************************-->
<!-- Token Operations -->
<method name="GET" id="validateToken">
<doc xml:lang="EN" title="Validate Token">
<p xmlns="http://www.w3.org/1999/xhtml" class="shortdesc">
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.
</p>
<p xmlns="http://www.w3.org/1999/xhtml">
Behaviour is similar to <code>/tokens/{tokenId}</code>. In
other words, a user should expect an
itemNotFound (<code>404</code>) fault for an
invalid token.
</p>
<p xmlns="http://www.w3.org/1999/xhtml">
'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 <code>/tokens/{tokenId}</code>, including
extensions such as HP-IDM.
</p>
</doc>
<request>
<param name="belongsTo" style="query" required="false" type="xsd:string">
<doc xml:lang="EN">
<p xmlns="http://www.w3.org/1999/xhtml">
Validates a token has the supplied tenant in scope.
</p>
</doc>
</param>
<param name="OS-KSVALIDATE-serviceId" style="query" required="false" type="xsd:string">
<doc xml:lang="EN">
<p xmlns="http://www.w3.org/1999/xhtml">
If provided, filter the roles to be returned by the given service IDs.
</p>
</doc>
</param>
</request>
<response status="200 203">
<representation mediaType="application/xml" element="identity:access">
<doc>
<xsdxt:code href="../samples/validatetoken.xml"/>
</doc>
</representation>
<representation mediaType="application/json">
<doc>
<xsdxt:code href="../samples/validatetoken.json"/>
</doc>
</representation>
</response>
&commonFaults;
&getFaults;
</method>
<method name="HEAD" id="checkToken">
<doc xml:lang="EN" title="Check Token">
<p xmlns="http://www.w3.org/1999/xhtml" class="shortdesc">
Check that a token is valid and that it belongs to a particular
tenant and services (For performance).
</p>
<p xmlns="http://www.w3.org/1999/xhtml">
Behaviour is similar to <code>/tokens/{tokenId}</code>. In
other words, a user should expect an
itemNotFound (<code>404</code>) fault for an
invalid token.
</p>
<p xmlns="http://www.w3.org/1999/xhtml">
'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 <code>/tokens/{tokenId}</code>, including
extensions such as HP-IDM.
</p>
<p xmlns="http://www.w3.org/1999/xhtml">
No response body is returned for this method.
</p>
</doc>
<request>
<param name="belongsTo" style="query" required="false" type="xsd:string">
<doc xml:lang="EN">
<p xmlns="http://www.w3.org/1999/xhtml">
Validates a token has the supplied tenant in scope. (for performance).
</p>
</doc>
</param>
<param name="OS-KSVALIDATE-serviceId" style="query" required="false" type="xsd:string">
<doc xml:lang="EN">
<p xmlns="http://www.w3.org/1999/xhtml">
Check the roles against the given service IDs.
</p>
</doc>
</param>
</request>
<response status="200 203"/>
&commonFaults;
&getFaults;
</method>
<method name="GET" id="listEndpointsForToken">
<doc xml:lang="EN" title="List Endoints for a Token">
<p xmlns="http://www.w3.org/1999/xhtml">
Returns a list of endpoints associated with a specific token.
</p>
</doc>
<response status="200 203">
<representation mediaType="application/xml" element="identity:endpoints">
<doc>
<xsdxt:code href="../common/samples/endpoints.xml"/>
</doc>
</representation>
<representation mediaType="application/json">
<doc>
<xsdxt:code href="../common/samples/endpoints.json"/>
</doc>
</representation>
</response>
&commonFaults;
&getFaults;
</method>
</application>

View File

@ -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": []
}

View File

@ -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

View File

@ -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

View File

@ -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"
}
]
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension xmlns="http://docs.openstack.org/common/api/v1.0"
xmlns:atom="http://www.w3.org/2005/Atom"
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.
</description>
<atom:link rel="describedby" type="application/pdf"
href="https://github.com/openstack/keystone/raw/master/keystone/content/admin/OS-KSVALIDATE-admin-devguide.pdf"/>
<atom:link rel="describedby" type="application/pdf"
href="https://github.com/openstack/keystone/raw/master/keystone/content/admin/OS-KSVALIDATE-admin.wadl"/>
</extension>

View File

@ -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)

View File

@ -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))

View File

@ -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()

View File

@ -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))]

View File

@ -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)