summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorZiad Sawalha <github@highbridgellc.com>2012-01-06 16:47:08 -0600
committerZiad Sawalha <github@highbridgellc.com>2012-01-23 19:34:35 -0600
commit28dac4595cbdaab82d108992f43321efbb925199 (patch)
tree308280a5a40fe1a0215fed2e9d4a046d432462a9
parent88b88a9e6f515fe617da724993a327b5bbc12aaa (diff)
downloadkeystone-28dac4595cbdaab82d108992f43321efbb925199.tar.gz
keystone-28dac4595cbdaab82d108992f43321efbb925199.tar.xz
keystone-28dac4595cbdaab82d108992f43321efbb925199.zip
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
-rw-r--r--doc/source/extensions.rst10
-rw-r--r--etc/keystone.conf2
-rw-r--r--keystone/common/bufferedhttp.py2
-rw-r--r--keystone/common/crypt.py74
-rw-r--r--keystone/content/admin/OS-KSVALIDATE-admin.wadl192
-rw-r--r--keystone/content/common/samples/extensions.json5
-rw-r--r--keystone/contrib/extensions/admin/hpidm/__init__.py10
-rw-r--r--keystone/contrib/extensions/admin/osksvalidate/__init__.py39
-rw-r--r--keystone/contrib/extensions/admin/osksvalidate/extension.json21
-rw-r--r--keystone/contrib/extensions/admin/osksvalidate/extension.xml15
-rw-r--r--keystone/contrib/extensions/admin/osksvalidate/handler.py47
-rw-r--r--keystone/controllers/token.py40
-rw-r--r--keystone/middleware/auth_token.py153
-rw-r--r--keystone/middleware/crypt.py74
-rwxr-xr-xkeystone/routers/admin.py6
15 files changed, 636 insertions, 54 deletions
diff --git a/doc/source/extensions.rst b/doc/source/extensions.rst
index fa6be0df..bde6e404 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 d7c0e036..62643eea 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 c6e3c2ec..db64175d 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 00000000..bb25620d
--- /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 00000000..c477131d
--- /dev/null
+++ b/keystone/content/admin/OS-KSVALIDATE-admin.wadl
@@ -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>
diff --git a/keystone/content/common/samples/extensions.json b/keystone/content/common/samples/extensions.json
index 75b0f7e8..ca46f941 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 59086ef7..ca314826 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 00000000..77c1ebe8
--- /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 00000000..b1f0c65b
--- /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 00000000..6643484a
--- /dev/null
+++ b/keystone/contrib/extensions/admin/osksvalidate/extension.xml
@@ -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>
diff --git a/keystone/contrib/extensions/admin/osksvalidate/handler.py b/keystone/contrib/extensions/admin/osksvalidate/handler.py
new file mode 100644
index 00000000..9132c474
--- /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 2386f6e2..44eff4aa 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 daedc08c..d3ccc3a5 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 00000000..bb25620d
--- /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 c46584bc..1a0db7ee 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)