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:
parent
88b88a9e6f
commit
28dac4595c
|
@ -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
|
||||
-------------------------------
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))]
|
|
@ -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>
|
|
@ -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": []
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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)
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))]
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue