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
|
The included extensions are in the process of being rewritten. Currently
|
||||||
only osksadm and oskscatalog work with this new extensions design.
|
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
|
Enabling & Disabling Extensions
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ service-header-mappings = {
|
||||||
|
|
||||||
#List of extensions currently loaded.
|
#List of extensions currently loaded.
|
||||||
#Refer docs for list of supported extensions.
|
#Refer docs for list of supported extensions.
|
||||||
extensions= osksadm, oskscatalog, hpidm
|
extensions= osksadm, oskscatalog, hpidm, osksvalidate
|
||||||
|
|
||||||
# Address to bind the API server
|
# Address to bind the API server
|
||||||
# TODO Properties defined within app not available via pipeline.
|
# TODO Properties defined within app not available via pipeline.
|
||||||
|
|
|
@ -36,7 +36,7 @@ from eventlet.green.httplib import CONTINUE, HTTPConnection, HTTPMessage, \
|
||||||
|
|
||||||
DEFAULT_TIMEOUT = 30
|
DEFAULT_TIMEOUT = 30
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__) # pylint: disable=C0103
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=R0902
|
# 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",
|
"name": "Reset Password Extension",
|
||||||
"namespace": "http://docs.rackspacecloud.com/identity/api/ext/rpe/v2.0",
|
"namespace": "http://docs.rackspacecloud.com/identity/api/ext/rpe/v2.0",
|
||||||
|
@ -38,6 +39,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
]},
|
||||||
"extensions_links": []
|
"extensions_links": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,4 @@ from keystone.controllers.token import TokenController
|
||||||
|
|
||||||
class ExtensionHandler(BaseExtensionHandler):
|
class ExtensionHandler(BaseExtensionHandler):
|
||||||
def map_extension_methods(self, mapper, options):
|
def map_extension_methods(self, mapper, options):
|
||||||
token_controller = TokenController(options)
|
pass
|
||||||
|
|
||||||
# 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"]))
|
|
||||||
|
|
|
@ -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"""
|
"""Undecorated EC2 handler"""
|
||||||
creds = utils.get_normalized_request_content(auth.Ec2Credentials, req)
|
creds = utils.get_normalized_request_content(auth.Ec2Credentials, req)
|
||||||
return utils.send_result(200, 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):
|
def _validate_token(self, req, token_id):
|
||||||
"""Validates the token, and that it belongs to the specified tenant"""
|
"""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 are only relevant if hpidm extension is enabled
|
||||||
service_ids = req.GET.get('HP-IDM-serviceId')
|
service_ids = req.GET.get('HP-IDM-serviceId')
|
||||||
return self.identity_service.validate_token(
|
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
|
@utils.wrap_error
|
||||||
def validate_token(self, req, token_id):
|
def validate_token(self, req, token_id):
|
||||||
result = self._validate_token(req, token_id)
|
if self.options.get('disable_tokens_in_url'):
|
||||||
return utils.send_result(200, req, result)
|
fault.ServiceUnavailableFault()
|
||||||
|
else:
|
||||||
|
result = self._validate_token(req, token_id)
|
||||||
|
return utils.send_result(200, req, result)
|
||||||
|
|
||||||
@utils.wrap_error
|
@utils.wrap_error
|
||||||
def check_token(self, req, token_id):
|
def check_token(self, req, token_id):
|
||||||
"""Validates the token, but only returns a status code (HEAD)"""
|
"""Validates the token, but only returns a status code (HEAD)"""
|
||||||
self._validate_token(req, token_id)
|
if self.options.get('disable_tokens_in_url'):
|
||||||
return utils.send_result(200, req)
|
fault.ServiceUnavailableFault()
|
||||||
|
else:
|
||||||
|
self._validate_token(req, token_id)
|
||||||
|
return utils.send_result(200, req)
|
||||||
|
|
||||||
@utils.wrap_error
|
@utils.wrap_error
|
||||||
def delete_token(self, req, token_id):
|
def delete_token(self, req, token_id):
|
||||||
return utils.send_result(204, req,
|
if self.options.get('disable_tokens_in_url'):
|
||||||
self.identity_service.revoke_token(utils.get_auth_token(req),
|
fault.ServiceUnavailableFault()
|
||||||
token_id))
|
else:
|
||||||
|
return utils.send_result(204, req,
|
||||||
|
self.identity_service.revoke_token(
|
||||||
|
utils.get_auth_token(req), token_id))
|
||||||
|
|
||||||
@utils.wrap_error
|
@utils.wrap_error
|
||||||
def endpoints(self, req, token_id):
|
def endpoints(self, req, token_id):
|
||||||
marker, limit, url = get_marker_limit_and_url(req)
|
if self.options.get('disable_tokens_in_url'):
|
||||||
return utils.send_result(200, req,
|
fault.ServiceUnavailableFault()
|
||||||
self.identity_service.get_endpoints_for_token(
|
else:
|
||||||
utils.get_auth_token(req),
|
marker, limit, url = get_marker_limit_and_url(req)
|
||||||
token_id, marker, limit, url))
|
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 datetime import datetime
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
import errno
|
||||||
import eventlet
|
import eventlet
|
||||||
from eventlet import wsgi
|
from eventlet import wsgi
|
||||||
|
from httplib import HTTPException
|
||||||
import json
|
import json
|
||||||
# memcache is imported in __init__ if memcache caching is configured
|
# memcache is imported in __init__ if memcache caching is configured
|
||||||
import logging
|
import logging
|
||||||
|
@ -111,7 +113,6 @@ from urlparse import urlparse
|
||||||
from webob.exc import HTTPUnauthorized
|
from webob.exc import HTTPUnauthorized
|
||||||
from webob.exc import Request, Response
|
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
|
from keystone.common.bufferedhttp import http_connect_raw as http_connect
|
||||||
|
|
||||||
logger = logging.getLogger(__name__) # pylint: disable=C0103
|
logger = logging.getLogger(__name__) # pylint: disable=C0103
|
||||||
|
@ -130,9 +131,14 @@ class TokenExpired(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class KeystoneUnreachable(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AuthProtocol(object):
|
class AuthProtocol(object):
|
||||||
"""Auth Middleware that handles authenticating client calls"""
|
"""Auth Middleware that handles authenticating client calls"""
|
||||||
|
|
||||||
|
# pylint: disable=W0613
|
||||||
def _init_protocol_common(self, app, conf):
|
def _init_protocol_common(self, app, conf):
|
||||||
""" Common initialization code
|
""" Common initialization code
|
||||||
|
|
||||||
|
@ -151,9 +157,9 @@ class AuthProtocol(object):
|
||||||
self.service_host = conf.get('service_host')
|
self.service_host = conf.get('service_host')
|
||||||
service_port = conf.get('service_port')
|
service_port = conf.get('service_port')
|
||||||
service_ids = conf.get('service_ids')
|
service_ids = conf.get('service_ids')
|
||||||
self.serviceId_qs = ''
|
self.service_id_querystring = ''
|
||||||
if service_ids:
|
if service_ids:
|
||||||
self.serviceId_qs = '?HP-IDM-serviceId=%s' % \
|
self.service_id_querystring = '?HP-IDM-serviceId=%s' % \
|
||||||
(urllib.quote(service_ids))
|
(urllib.quote(service_ids))
|
||||||
if service_port:
|
if service_port:
|
||||||
self.service_port = int(service_port)
|
self.service_port = int(service_port)
|
||||||
|
@ -175,7 +181,7 @@ class AuthProtocol(object):
|
||||||
self.auth_host = conf.get('auth_host')
|
self.auth_host = conf.get('auth_host')
|
||||||
self.auth_port = int(conf.get('auth_port'))
|
self.auth_port = int(conf.get('auth_port'))
|
||||||
self.auth_protocol = conf.get('auth_protocol', 'https')
|
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
|
# where to tell clients to find the auth service (default to url
|
||||||
# constructed based on endpoint we have for the service to use)
|
# constructed based on endpoint we have for the service to use)
|
||||||
|
@ -198,6 +204,9 @@ class AuthProtocol(object):
|
||||||
if self.memcache_hosts:
|
if self.memcache_hosts:
|
||||||
if self.cache is None:
|
if self.cache is None:
|
||||||
self.cache = "keystone.cache"
|
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):
|
def __init__(self, app, conf):
|
||||||
""" Common initialization code """
|
""" Common initialization code """
|
||||||
|
@ -226,6 +235,10 @@ class AuthProtocol(object):
|
||||||
self.service_protocol = None
|
self.service_protocol = None
|
||||||
self.service_timeout = None
|
self.service_timeout = None
|
||||||
self.service_url = 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.cache = None
|
||||||
self.memcache_hosts = None
|
self.memcache_hosts = None
|
||||||
self._init_protocol_common(app, conf) # Applies to all protocols
|
self._init_protocol_common(app, conf) # Applies to all protocols
|
||||||
|
@ -243,6 +256,18 @@ class AuthProtocol(object):
|
||||||
memcache_client = memcache.Client([self.memcache_hosts])
|
memcache_client = memcache.Client([self.memcache_hosts])
|
||||||
env[self.cache] = memcache_client
|
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
|
#Prep headers to forward request to local or remote downstream service
|
||||||
proxy_headers = env.copy()
|
proxy_headers = env.copy()
|
||||||
for header in proxy_headers.iterkeys():
|
for header in proxy_headers.iterkeys():
|
||||||
|
@ -322,11 +347,13 @@ class AuthProtocol(object):
|
||||||
""" Convert datetime to unix timestamp for caching """
|
""" Convert datetime to unix timestamp for caching """
|
||||||
return time.mktime(parser.parse(date).utctimetuple())
|
return time.mktime(parser.parse(date).utctimetuple())
|
||||||
|
|
||||||
|
# pylint: disable=W0613
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _protect_claims(token, claims):
|
def _protect_claims(token, claims):
|
||||||
""" encrypt or mac claims if necessary """
|
""" encrypt or mac claims if necessary """
|
||||||
return claims
|
return claims
|
||||||
|
|
||||||
|
# pylint: disable=W0613
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _unprotect_claims(token, pclaims):
|
def _unprotect_claims(token, pclaims):
|
||||||
""" decrypt or demac claims if necessary """
|
""" decrypt or demac claims if necessary """
|
||||||
|
@ -346,8 +373,7 @@ class AuthProtocol(object):
|
||||||
else:
|
else:
|
||||||
# normal memcache client
|
# normal memcache client
|
||||||
expires = self._convert_date(claims['expires'])
|
expires = self._convert_date(claims['expires'])
|
||||||
delta = expires - time.time()
|
timeout = expires - time.time()
|
||||||
timeout = delta.seconds
|
|
||||||
if timeout > MAX_CACHE_TIME or not valid:
|
if timeout > MAX_CACHE_TIME or not valid:
|
||||||
# Limit cache to one day (and cache bad tokens for a day)
|
# Limit cache to one day (and cache bad tokens for a day)
|
||||||
timeout = MAX_CACHE_TIME
|
timeout = MAX_CACHE_TIME
|
||||||
|
@ -429,21 +455,44 @@ class AuthProtocol(object):
|
||||||
headers = {"Content-type": "application/json",
|
headers = {"Content-type": "application/json",
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
"X-Auth-Token": self.admin_token}
|
"X-Auth-Token": self.admin_token}
|
||||||
##TODO(ziad):we need to figure out how to auth to keystone
|
if self.osksvalidate:
|
||||||
#since validate_token is a priviledged call
|
headers['X-Subject-Token'] = claims
|
||||||
#Khaled's version uses creds to get a token
|
path = '/v2.0/OS-KSVALIDATE/token/validate/%s' % \
|
||||||
# "X-Auth-Token": admin_token}
|
self.service_id_querystring
|
||||||
# we're using a test token from the ini file for now
|
logger.debug("Connecting to %s://%s:%s to check claims using the"
|
||||||
logger.debug("Connecting to %s://%s:%s to check claims" % (
|
"OS-KSVALIDATE extension" % (self.auth_protocol,
|
||||||
self.auth_protocol, self.auth_host, self.auth_port))
|
self.auth_host, self.auth_port))
|
||||||
conn = http_connect(self.auth_host, self.auth_port, 'GET',
|
else:
|
||||||
'/v2.0/tokens/%s%s' % (claims, self.serviceId_qs),
|
path = '/v2.0/tokens/%s%s' % (claims, self.service_id_querystring)
|
||||||
headers=headers,
|
logger.debug("Connecting to %s://%s:%s to check claims" % (
|
||||||
ssl=(self.auth_protocol == 'https'),
|
self.auth_protocol, self.auth_host, self.auth_port))
|
||||||
key_file=self.key_file, cert_file=self.cert_file,
|
|
||||||
timeout=self.auth_timeout)
|
##TODO(ziad):we need to figure out how to auth to keystone
|
||||||
resp = conn.getresponse()
|
#since validate_token is a priviledged call
|
||||||
data = resp.read()
|
#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)
|
logger.debug("Response received: %s" % resp.status)
|
||||||
if not str(resp.status).startswith('20'):
|
if not str(resp.status).startswith('20'):
|
||||||
|
@ -532,6 +581,7 @@ class AuthProtocol(object):
|
||||||
req = Request(proxy_headers)
|
req = Request(proxy_headers)
|
||||||
parsed = urlparse(req.url)
|
parsed = urlparse(req.url)
|
||||||
|
|
||||||
|
# pylint: disable=E1101
|
||||||
conn = http_connect(self.service_host,
|
conn = http_connect(self.service_host,
|
||||||
self.service_port,
|
self.service_port,
|
||||||
req.method,
|
req.method,
|
||||||
|
@ -557,6 +607,50 @@ class AuthProtocol(object):
|
||||||
return Response(status=resp.status, body=data)(env,
|
return Response(status=resp.status, body=data)(env,
|
||||||
start_response)
|
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):
|
def filter_factory(global_conf, **local_conf):
|
||||||
"""Returns a WSGI filter app for use with paste.deploy."""
|
"""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)
|
conf.update(local_conf)
|
||||||
return AuthProtocol(None, conf)
|
return AuthProtocol(None, conf)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
wsgiapp = loadapp("config:" + \
|
def main():
|
||||||
os.path.join(os.path.abspath(os.path.dirname(__file__)),
|
"""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,
|
||||||
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"})
|
global_conf={"log_name": "auth_token.log"})
|
||||||
wsgi.server(eventlet.listen(('', 8090)), wsgiapp)
|
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)
|
logger.debug("Init with options=%s" % options)
|
||||||
mapper = routes.Mapper()
|
mapper = routes.Mapper()
|
||||||
|
|
||||||
|
# Load extensions first so they can override core if they need to
|
||||||
|
extension.get_extension_configurer().configure(mapper, options)
|
||||||
|
|
||||||
# Token Operations
|
# Token Operations
|
||||||
auth_controller = TokenController(options)
|
auth_controller = TokenController(options)
|
||||||
mapper.connect("/tokens", controller=auth_controller,
|
mapper.connect("/tokens", controller=auth_controller,
|
||||||
|
@ -50,7 +53,7 @@ class AdminApi(wsgi.Router):
|
||||||
mapper.connect("/tokens/{token_id}", controller=auth_controller,
|
mapper.connect("/tokens/{token_id}", controller=auth_controller,
|
||||||
action="check_token",
|
action="check_token",
|
||||||
conditions=dict(method=["HEAD"]))
|
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,
|
mapper.connect("/tokens/{token_id}", controller=auth_controller,
|
||||||
action="delete_token",
|
action="delete_token",
|
||||||
conditions=dict(method=["DELETE"]))
|
conditions=dict(method=["DELETE"]))
|
||||||
|
@ -141,5 +144,4 @@ class AdminApi(wsgi.Router):
|
||||||
action="get_static_file",
|
action="get_static_file",
|
||||||
root="content/common/", path="samples/",
|
root="content/common/", path="samples/",
|
||||||
conditions=dict(method=["GET"]))
|
conditions=dict(method=["GET"]))
|
||||||
extension.get_extension_configurer().configure(mapper, options)
|
|
||||||
super(AdminApi, self).__init__(mapper)
|
super(AdminApi, self).__init__(mapper)
|
||||||
|
|
Loading…
Reference in New Issue