Adds keystone auth-n/auth-z for Swift S3 API.

This capability has three parts:

a) a keystone patch to handle OS-KSS3-s3Credentials
   when received in a POST to /tokens.
b) a new keystone middleware s3_token.py for swift.
c) a swift patch to use token and endpoints from keystone via b).

This patch contains a) and b).

Note: modified by zns to get it in by E3.

See:
blueprint s3token
bug #874280

Change-Id: I9021de064177db358ea6d727c570f4e3bcd6e56c
This commit is contained in:
Akira YOSHIYAMA 2012-01-15 23:20:07 +09:00 committed by Ziad Sawalha
parent 5b8682f2aa
commit 027782a95f
15 changed files with 929 additions and 3 deletions

View File

@ -0,0 +1,41 @@
[DEFAULT]
bind_ip = 0.0.0.0
bind_port = 3333
user = root
[pipeline:main]
#pipeline = healthcheck cache swift3 keystone proxy-server
pipeline = healthcheck cache s3token swift3 keystone proxy-server
[app:proxy-server]
use = egg:swift#proxy
account_autocreate = true
allow_account_management = true
[filter:swift3]
use = egg:swift#swift3
[filter:s3token]
use = egg:keystone#s3token
auth_protocol = http
auth_host = 127.0.0.1
auth_port = 5000
admin_token = 999888777666
[filter:keystone]
use = egg:keystone#tokenauth
auth_protocol = http
auth_host = 127.0.0.1
auth_port = 35357
admin_token = 999888777666
delay_auth_decision = 0
service_protocol = http
service_host = 127.0.0.1
service_port = 3333
service_pass = pass
[filter:healthcheck]
use = egg:swift#healthcheck
[filter:cache]
use = egg:swift#memcache

View File

@ -0,0 +1,212 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- (C) 2011 OpenStack LLC., All Rights Reserved -->
<!--*******************************************************-->
<!-- Import Common XML Entities -->
<!-- -->
<!-- You can resolve the entites with xmllint -->
<!-- -->
<!-- xmllint -noent OS-KSEC2-admin.wadl -->
<!--*******************************************************-->
<!DOCTYPE application [
<!ENTITY % common SYSTEM "../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-KSEC2="http://docs.openstack.org/identity/api/ext/OS-KSEC2/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
http://docs.openstack.org/identity/api/ext/OS-KSEC2/v1.0 ../common/xsd/OS-KSEC2-credentials.xsd
">
<grammars>
<include href="../common/xsd/api.xsd"/>
<include href="../common/xsd/api-common.xsd"/>
<include href="../common/xsd/OS-KSEC2-credentials.xsd" />
</grammars>
<!--*******************************************************-->
<!-- All Resoruces -->
<!--*******************************************************-->
<!-- We should use SSL in production -->
<resources base="http://localhost:35357">
<resource id="version" path="v2.0">
<param name="X-Auth-Token" style="header" type="xsd:string" required="true">
<doc>You need a valid admin token for access.</doc>
</param>
<resource id="users" path="users">
<resource id="userById" path="{userId}">
<param name="userId" style="template" required="true" type="xsd:string"/>
<resource id="user-OS-KSADM" path="OS-KSADM">
<resource id="userCredentials" path="credentials">
<method href="#addUserCredential"/>
<method href="#listCredentials"/>
<resource id="userCredentialsByType" path="OS-KSEC2:s3Credentials">
<method href="#updateUserCredential"/>
<method href="#deleteUserCredential"/>
<method href="#getUserCredential"/>
</resource>
</resource>
</resource>
</resource>
</resource>
</resource>
</resources>
<!--*******************************************************-->
<!-- All Methods -->
<!--*******************************************************-->
<!-- User Credentials-->
<method name="POST" id="addUserCredential">
<doc xml:lang="EN" title="Add user Credential.">
<p xmlns="http://www.w3.org/1999/xhtml">Adds a credential to a user.</p>
</doc>
<request>
<representation mediaType="application/xml" element="OS-KSS3:s3credentials">
<doc xml:lang="EN">
<xsdxt:code href="../common/samples/s3Credentials.xml"/>
</doc>
</representation>
<representation mediaType="application/json">
<doc xml:lang="EN">
<xsdxt:code href="../common/samples/s3Credentials.json"/>
</doc>
</representation>
</request>
<response status="201">
<representation mediaType="application/xml" element="OS-KSS3:s3credentials">
<doc xml:lang="EN">
<xsdxt:code href="../common/samples/s3Credentials.xml"/>
</doc>
</representation>
<representation mediaType="application/json">
<doc xml:lang="EN">
<xsdxt:code href="../common/samples/s3Credentials.json"/>
</doc>
</representation>
</response>
&commonFaults;
&postPutFaults;
&getFaults;
</method>
<method name="GET" id="listCredentials">
<doc xml:lang="EN" title="List Credentials">
<p xmlns="http://www.w3.org/1999/xhtml">List credentials.</p>
</doc>
<request>
<param name="marker" style="query" required="false" type="xsd:string"/>
<param name="limit" style="query" required="false" type="xsd:int"/>
</request>
<response status="200 203">
<representation mediaType="application/xml" element="identity:credentials">
<doc xml:lang="EN">
<xsdxt:code href="../common/samples/credentialswiths3.xml"/>
</doc>
</representation>
<representation mediaType="application/json">
<doc xml:lang="EN">
<xsdxt:code href="../common/samples/credentialswiths3.json"/>
</doc>
</representation>
</response>
&commonFaults;
&getFaults;
</method>
<method name="GET" id="listCredentialsByType">
<doc xml:lang="EN" title="List Credentials by type">
<p xmlns="http://www.w3.org/1999/xhtml">List credentials by type.</p>
</doc>
<request>
<param name="marker" style="query" required="false" type="xsd:string"/>
<param name="limit" style="query" required="false" type="xsd:int"/>
</request>
<response status="200 203">
<representation mediaType="application/xml" element="identity:credentials">
<doc xml:lang="EN">
<xsdxt:code href="../common/samples/credentials.xml"/>
</doc>
</representation>
<representation mediaType="application/json">
<doc xml:lang="EN">
<xsdxt:code href="../common/samples/credentials.json"/>
</doc>
</representation>
</response>
&commonFaults;
&getFaults;
</method>
<method name="POST" id="updateUserCredential">
<doc xml:lang="EN" title="Update user credential">
<p xmlns="http://www.w3.org/1999/xhtml">Update credentials.</p>
</doc>
<request>
<representation mediaType="application/xml" element="OS-KSS3:s3credentials">
<doc xml:lang="EN">
<xsdxt:code href="../common/samples/s3Credentials.xml"/>
</doc>
</representation>
<representation mediaType="application/json">
<doc xml:lang="EN">
<xsdxt:code href="../common/samples/s3Credentials.json"/>
</doc>
</representation>
</request>
<response status="200">
<representation mediaType="application/xml" element="OS-KSS3:s3credentials">
<doc xml:lang="EN">
<xsdxt:code href="../common/samples/s3Credentials.xml"/>
</doc>
</representation>
<representation mediaType="application/json">
<doc xml:lang="EN">
<xsdxt:code href="../common/samples/s3Credentials.json"/>
</doc>
</representation>
</response>
&commonFaults;
&postPutFaults;
&getFaults;
</method>
<method name="DELETE" id="deleteUserCredential">
<doc xml:lang="EN" title="Delete user credential">
<p xmlns="http://www.w3.org/1999/xhtml">Delete User credentials.</p>
</doc>
<response status="204"/>
&commonFaults;
&postPutFaults;
&getFaults;
</method>
<method name="GET" id="getUserCredential">
<doc xml:lang="EN" title="Get user Credentials">
<p xmlns="http://www.w3.org/1999/xhtml">Get user credentials.</p>
</doc>
<response status="200 203">
<representation mediaType="application/xml" element="OS-KSS3:s3credentials">
<doc xml:lang="EN">
<xsdxt:code href="../common/samples/s3Credentials.xml"/>
</doc>
</representation>
<representation mediaType="application/json">
<doc xml:lang="EN">
<xsdxt:code href="../common/samples/s3Credentials.json"/>
</doc>
</representation>
</response>
&commonFaults;
&getFaults;
</method>
</application>

View File

@ -0,0 +1,17 @@
{
"credentials":[{
"passwordCredentials":{
"username":"test_user",
"password":"mypass"
}
},
{
"OS-KSS3-s3Credentials":{
"username":"test_user",
"secret":"aaaaa",
"signature":"bbb"
}
}
],
"credentials_links":[]
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<credentials xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://docs.openstack.org/identity/api/v2.0">
<passwordCredentials username="test_user" password="test"/>
<s3Credentials xmlns="http://docs.openstack.org/identity/api/ext/OS-KSS3/v1.0"
username="testuser" key="aaaaa" signature="bbbbb"/>
</credentials>

View File

@ -0,0 +1,7 @@
{
"OS-KSS3-s3Credentials":{
"username":"test_user",
"secret":"aaaaa",
"signature":"bbb"
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<s3Credentials
xmlns="http://docs.openstack.org/identity/api/ext/OS-KSS3/v1.0"
username="testuser"
key="aaaaa"
signature="bbbbb"/>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="../xslt/schema.xslt"?>
<schema
elementFormDefault="qualified"
attributeFormDefault="unqualified"
xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:identity="http://docs.openstack.org/identity/api/v2.0"
xmlns:OS-KSEC2="http://docs.openstack.org/identity/api/ext/OS-KSEC2/v1.0"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"
xmlns:xsdxt="http://docs.rackspacecloud.com/xsd-ext/v1.0"
xmlns:atom="http://www.w3.org/2005/Atom"
targetNamespace="http://docs.openstack.org/identity/api/ext/OS-KSEC2/v1.0"
>
<!--Import schema we are extending -->
<import namespace="http://docs.openstack.org/identity/api/v2.0"
schemaLocation="credentials.xsd"/>
<!-- Elements -->
<element name="s3Credentials" type="OS-KSEC2:s3CredentialsType" substitutionGroup="identity:credential"/>
<!-- Complex Types -->
<complexType name="s3CredentialsType">
<complexContent>
<extension base="identity:CredentialType">
<attribute name="username" type="xsd:string" use="required" ></attribute>
<attribute name="key" type="xsd:string" use="required" ></attribute>
<attribute name="signature" type="xsd:string" use="required" ></attribute>
</extension>
</complexContent>
</complexType>
</schema>

View File

@ -62,6 +62,8 @@ class TokenController(BaseController):
return utils.send_result(200, req, result)
elif credential_type in ["ec2Credentials", "OS-KSEC2-ec2Credentials"]:
return self._authenticate_ec2(req)
elif credential_type == "OS-KSS3-s3Credentials":
return self._authenticate_s3(req)
else:
raise fault.BadRequestFault("Invalid credentials %s" %
credential_type)
@ -76,6 +78,16 @@ class TokenController(BaseController):
return utils.send_result(200, req,
self.identity_service.authenticate_ec2(creds))
@utils.wrap_error
def authenticate_s3(self, req):
return self._authenticate_s3(req)
def _authenticate_s3(self, req):
"""Undecorated S3 handler"""
creds = utils.get_normalized_request_content(auth.S3Credentials, req)
return utils.send_result(200, req,
self.identity_service.authenticate_s3(creds))
def _validate_token(self, req, token_id):
"""Validates the token, and that it belongs to the specified tenant"""
belongs_to = req.GET.get('belongsTo')

View File

@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# pylint: disable=C0302
# pylint: disable=C0302,W0603,W0602
from datetime import datetime, timedelta
import functools
@ -77,6 +77,7 @@ def service_admin_token_validator(fnc):
return _wrapper
# pylint: disable=R0902
class IdentityService(object):
"""Implements the Identity service
@ -191,6 +192,25 @@ class IdentityService(object):
return self._authenticate(validate, creds.user_id,
creds.tenant_id)
def authenticate_s3(self, credentials):
# Check credentials
if not isinstance(credentials, auth.S3Credentials):
raise fault.BadRequestFault("Expecting S3 Credentials!")
creds = self.credential_manager.get_by_access(credentials.access)
if not creds:
raise fault.UnauthorizedFault("No credentials found for %s"
% credentials.access)
def validate(duser): # pylint: disable=W0613
signer = Signer(creds.secret)
signature = signer.generate(credentials, s3=True)
if signature == credentials.signature:
return True
return False
return self._authenticate(validate, creds.user_id, creds.tenant_id)
def _authenticate(self, validate, user_id, tenant_id=None):
LOG.debug("Authenticating user %s (tenant: %s)" % (user_id, tenant_id))
if tenant_id:
@ -648,6 +668,7 @@ class IdentityService(object):
dtenant.enabled = tenant.enabled
return self.tenant_manager.create(dtenant)
# pylint: disable=R0914
def get_tenants(self, admin_token, marker, limit, url,
is_service_operation=False):
"""Fetch tenants for either an admin or service operation."""

View File

@ -63,8 +63,15 @@ class Signer(object):
if hashlib.sha256:
self.hmac_256 = hmac.new(secret_key, digestmod=hashlib.sha256)
def generate(self, credentials):
def generate(self, credentials, s3=False):
"""Generate auth string according to what SignatureVersion is given."""
if s3:
return self._calc_signature_s3(credentials.verb,
credentials.expire,
credentials.path,
credentials.content_type,
credentials.content_md5,
credentials.xheaders)
if credentials.params['SignatureVersion'] == '0':
return self._calc_signature_0(credentials.params)
if credentials.params['SignatureVersion'] == '1':
@ -130,6 +137,27 @@ class Signer(object):
LOG.debug('base64 encoded digest: %s', b64)
return b64
# pylint: disable=R0913
def _calc_signature_s3(self, verb, expire, path, content_type, content_md5,
xheaders):
"""Generate AWS signature for S3 string."""
LOG.debug('using _calc_signature_s3')
string_to_sign = '%s\n%s\n%s\n%s\n' % (
verb, content_md5, content_type, expire)
if xheaders:
xheader_keys = xheaders.keys()
xheader_keys.sort()
for key in xheader_keys:
string_to_sign += '%s:%s\n' % (key, xheaders[key])
string_to_sign += path
current_hmac = self.hmac
LOG.debug('string_to_sign: %s', string_to_sign)
current_hmac.update(string_to_sign)
b64 = base64.b64encode(current_hmac.digest())
LOG.debug('len(b64)=%d', len(b64))
LOG.debug('base64 encoded digest: %s', b64)
return b64
if __name__ == '__main__':
# pylint: disable=E1121

View File

@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=C0103
# pylint: disable=C0103,R0912,R0913,R0914
import json
from lxml import etree
@ -273,6 +273,137 @@ class Ec2Credentials(object):
str(e))
# pylint: disable=R0902
class S3Credentials(object):
"""Credentials based on username, access_key, signature and data.
@type access: str
@param access: Access key for user in the form of access:project.
@type signature: str
@param signature: Signature of the request.
@type verb: str
@param verb: Web request verb ('GET' or 'POST').
@type host: expire
@param host: Web request expire time.
@type path: str
@param path: Web request path.
@type expire: str
@param expire: Web request expire.
@type content_type: str
@param content_type: Web request content contenttype.
@type content_md5: str
@param content_md5: Web request content contentmd5.
@type xheaders: str
@param xheaders: Web request content extended headers.
"""
def __init__(self, access, signature, verb, path, expire, content_type,
content_md5, xheaders):
self.access = access
self.signature = signature
self.verb = verb
self.path = path
self.expire = expire
self.content_type = content_type
self.content_md5 = content_md5
self.xheaders = xheaders
@staticmethod
def from_xml(xml_str):
try:
dom = etree.Element("root")
dom.append(etree.fromstring(xml_str))
root = dom.find("{http://docs.openstack.org/identity/api/v2.0}"
"auth")
xmlns = "http://docs.openstack.org/identity/api/ext/OS-KSS3/v1.0"
if root is None:
root = dom.find("{%s}s3Credentials" % xmlns)
else:
root = root.find("{%s}s3Credentials" % xmlns)
if root is None:
raise fault.BadRequestFault("Expecting s3Credentials")
access = root.get("access")
if access == None:
raise fault.BadRequestFault("Expecting an access key")
signature = root.get("signature")
if signature == None:
raise fault.BadRequestFault("Expecting a signature")
verb = root.get("verb")
if verb == None:
raise fault.BadRequestFault("Expecting a verb")
path = root.get("path")
if path == None:
raise fault.BadRequestFault("Expecting a path")
expire = root.get("expire")
if expire == None:
raise fault.BadRequestFault("Expecting a expire")
content_type = root.get("content_type", '')
content_md5 = root.get("content_md5", '')
xheaders = root.get("xheaders", None)
return S3Credentials(access, signature, verb, path, expire,
content_type, content_md5, xheaders)
except etree.LxmlError as e:
raise fault.BadRequestFault("Cannot parse password credentials",
str(e))
@staticmethod
def from_json(json_str):
try:
root = json.loads(json_str)
if "auth" in root:
obj = root['auth']
else:
obj = root
if "OS-KSS3-s3Credentials" in obj:
cred = obj["OS-KSS3-s3Credentials"]
elif "s3Credentials" in obj:
cred = obj["s3Credentials"]
else:
raise fault.BadRequestFault("Expecting s3Credentials")
# Check that fields are valid
invalid = [key for key in cred if key not in\
['username', 'access', 'signature', 'verb', 'expire',
'path', 'content_type', 'content_md5', 'xheaders']]
if invalid != []:
raise fault.BadRequestFault("Invalid attribute(s): %s"
% invalid)
if not "access" in cred:
raise fault.BadRequestFault("Expecting an access key")
access = cred["access"]
if not "signature" in cred:
raise fault.BadRequestFault("Expecting a signature")
signature = cred["signature"]
if not "verb" in cred:
raise fault.BadRequestFault("Expecting a verb")
verb = cred["verb"]
if not "path" in cred:
raise fault.BadRequestFault("Expecting a path")
path = cred["path"]
if not "expire" in cred:
raise fault.BadRequestFault("Expecting a expire")
expire = cred["expire"]
content_type = cred.get("content_type", '')
content_md5 = cred.get("content_md5", '')
xheaders = cred.get("xheaders", None)
return S3Credentials(access, signature, verb, path, expire,
content_type, content_md5, xheaders)
except (ValueError, TypeError) as e:
raise fault.BadRequestFault("Cannot parse password credentials",
str(e))
class Tenant(object):
"""Provides the scope of a token"""

View File

@ -0,0 +1,161 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# Copyright 2011,2012 Akira YOSHIYAMA <akirayoshiyama@gmail.com>
# 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.
# This source code is based ./auth_token.py and ./ec2_token.py.
# See them for their copyright.
"""
Starting point for routing S3 requests.
"""
import httplib
import json
from webob.dec import wsgify
from urlparse import urlparse
PROTOCOL_NAME = "S3 Token Authentication"
class S3Token(object):
"""Auth Middleware that handles S3 authenticating client calls"""
def _init_protocol_common(self, app, conf):
""" Common initialization code"""
print "Starting the %s component" % PROTOCOL_NAME
self.conf = conf
self.app = app
#if app is set, then we are in a WSGI pipeline and requests get passed
# on to app. If it is not set, this component should forward requests
def _init_protocol(self, conf):
""" Protocol specific initialization """
# where to find the auth service (we use this to validate tokens)
self.auth_host = conf.get('auth_host')
self.auth_port = int(conf.get('auth_port'))
self.auth_protocol = conf.get('auth_protocol', 'https')
# where to tell clients to find the auth service (default to url
# constructed based on endpoint we have for the service to use)
self.auth_location = conf.get('auth_uri',
"%s://%s:%s" % (self.auth_protocol,
self.auth_host,
self.auth_port))
# Credentials used to verify this component with the Auth service since
# validating tokens is a privileged call
self.admin_token = conf.get('admin_token')
def __init__(self, app, conf):
""" Common initialization code """
#TODO(ziad): maybe we refactor this into a superclass
self._init_protocol_common(app, conf) # Applies to all protocols
self._init_protocol(conf) # Specific to this protocol
self.app = None
self.auth_port = None
self.auth_protocol = None
self.auth_location = None
self.auth_host = None
self.admin_token = None
self.conf = None
#@webob.dec.wsgify(RequestClass=webob.exc.Request)
# pylint: disable=R0914
@wsgify
def __call__(self, req):
""" Handle incoming request. Authenticate. And send downstream. """
# Read request signature and access id.
if not 'Authorization' in req.headers:
return self.app
try:
account, signature = \
req.headers['Authorization'].split(' ')[-1].rsplit(':', 1)
#del(req.headers['Authorization'])
except StandardError:
return self.app
#try:
# account, tenant = access.split(':')
#except Exception:
# account = access
# Authenticate the request.
creds = {'OS-KSS3-s3Credentials': {'access': account,
'signature': signature,
'verb': req.method,
'path': req.path,
'expire': req.headers['Date'],
}}
if req.headers.get('Content-Type'):
creds['s3Credentials']['content_type'] = \
req.headers['Content-Type']
if req.headers.get('Content-MD5'):
creds['s3Credentials']['content_md5'] = req.headers['Content-MD5']
xheaders = {}
for key, value in req.headers.iteritems():
if key.startswith('X-Amz'):
xheaders[key.lower()] = value
if xheaders:
creds['s3Credentials']['xheaders'] = xheaders
creds_json = json.dumps(creds)
headers = {'Content-Type': 'application/json'}
if self.auth_protocol == 'http':
conn = httplib.HTTPConnection(self.auth_host, self.auth_port)
else:
conn = httplib.HTTPSConnection(self.auth_host, self.auth_port)
conn.request('POST', '/v2.0/tokens', body=creds_json, headers=headers)
response = conn.getresponse().read()
conn.close()
# NOTE(vish): We could save a call to keystone by
# having keystone return token, tenant,
# user, and roles from this call.
result = json.loads(response)
endpoint_path = ''
try:
token_id = str(result['access']['token']['id'])
for endpoint in result['access']['serviceCatalog']:
if endpoint['type'] == 'Swift Service':
ep = urlparse(endpoint['endpoints'][0]['internalURL'])
endpoint_path = str(ep.path) # pylint: disable=E1101
break
except KeyError:
return self.app
# Authenticated!
req.headers['X-Auth-Token'] = token_id
req.headers['X-Endpoint-Path'] = endpoint_path
return self.app
def filter_factory(global_conf, **local_conf):
"""Returns a WSGI filter app for use with paste.deploy."""
conf = global_conf.copy()
conf.update(local_conf)
def auth_filter(app):
return S3Token(app, conf)
return auth_filter

View File

@ -43,6 +43,7 @@ class ServiceApi(wsgi.Router):
mapper.connect("/ec2tokens", controller=auth_controller,
action="authenticate_ec2",
conditions=dict(method=["POST"]))
tenant_controller = TenantController(True)
mapper.connect("/tenants",
controller=tenant_controller,

View File

@ -0,0 +1,245 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 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.
import json
import logging
import unittest2 as unittest
import base
from keystone.test.unit.decorators import jsonify
from keystone.logic import signer
from keystone.logic.types import auth
LOGGER = logging.getLogger('test.unit.test_s3_authn')
class S3AuthnMethods(base.ServiceAPITest):
@jsonify
def test_valid_authn_s3_success_json(self):
"""Tests correct syntax with {"auth":} wrapper and extension """
url = "/tokens"
access = "xpd285.access"
secret = "345fgi.secret"
kwargs = {
"user_name": self.auth_user['name'],
"tenant_id": self.auth_user['tenant_id'],
"type": "EC2",
"key": access,
"secret": secret,
}
self.fixture_create_credentials(**kwargs)
req = self.get_request('POST', url)
params = {
"x-amz-acl": "public-read-write",
"x-amz-server-side-encryption": "AES256",
}
credentials = {
"access": access,
"verb": "PUT",
"path": "/test.txt",
"expire": 0,
"content_type": "text/plain",
"content_md5": "1234567890abcdef",
"xheaders": params,
"signature": None,
}
sign = signer.Signer(secret)
obj_creds = auth.S3Credentials(**credentials)
credentials['signature'] = sign.generate(obj_creds, s3=True)
body = {
"auth": {
"OS-KSS3-s3Credentials": credentials,
}
}
req.body = json.dumps(body)
self.get_response()
expected = {
u'access': {
u'token': {
u'id': self.auth_token_id,
u'expires': self.expires.strftime("%Y-%m-%dT%H:%M:%S.%f")},
u'user': {
u'id': unicode(self.auth_user['id']),
u'name': self.auth_user['name'],
u'roles': [{u'description': u'regular role',
u'id': u'0',
u'name': u'regular_role'}]}}}
self.assert_dict_equal(expected, json.loads(self.res.body))
self.status_ok()
@jsonify
def test_authn_s3_success_json(self):
"""Tests correct syntax with {"auth":} wrapper """
self._auth_to_url(url="/tokens")
def _auth_to_url(self, url):
"""
Test that good s3 credentials returns a 200 OK
"""
access = "xpd285.access"
secret = "345fgi.secret"
kwargs = {
"user_name": self.auth_user['name'],
"tenant_id": self.auth_user['tenant_id'],
"type": "EC2",
"key": access,
"secret": secret,
}
self.fixture_create_credentials(**kwargs)
req = self.get_request('POST', url)
params = {
"x-amz-acl": "public-read-write",
"x-amz-server-side-encryption": "AES256",
}
credentials = {
"access": access,
"verb": "PUT",
"path": "/test.txt",
"expire": 0,
"content_type": "text/plain",
"content_md5": "1234567890abcdef",
"xheaders": params,
"signature": None,
}
sign = signer.Signer(secret)
obj_creds = auth.S3Credentials(**credentials)
credentials['signature'] = sign.generate(obj_creds, s3=True)
body = {
"auth": {
"OS-KSS3-s3Credentials": credentials,
}
}
req.body = json.dumps(body)
self.get_response()
expected = {
u'access': {
u'token': {
u'id': self.auth_token_id,
u'expires': self.expires.strftime("%Y-%m-%dT%H:%M:%S.%f")},
u'user': {
u'id': unicode(self.auth_user['id']),
u'name': self.auth_user['name'],
u'roles': [{u'description': u'regular role',
u'id': u'0',
u'name': u'regular_role'}]}}}
self.assert_dict_equal(expected, json.loads(self.res.body))
self.status_ok()
@jsonify
def test_authn_s3_success_json_bad_user(self):
"""
Test that bad credentials returns a 401
"""
access = "xpd285.access"
secret = "345fgi.secret"
url = "/tokens"
req = self.get_request('POST', url)
params = {
"x-amz-acl": "public-read-write",
"x-amz-server-side-encryption": "AES256",
}
credentials = {
"access": access,
"verb": "PUT",
"path": "/test.txt",
"expire": 0,
"content_type": "text/plain",
"content_md5": "1234567890abcdef",
"xheaders": params,
"signature": None,
}
sign = signer.Signer(secret)
obj_creds = auth.S3Credentials(**credentials)
credentials['signature'] = sign.generate(obj_creds, s3=True)
body = {
"auth": {
"OS-KSS3-s3Credentials": credentials,
}
}
req.body = json.dumps(body)
self.get_response()
expected = {
u'unauthorized': {
u'code': u'401',
u'message': u'No credentials found for %s' % access,
}
}
self.assert_dict_equal(expected, json.loads(self.res.body))
self.assertEqual(self.res.status_int, 401)
@jsonify
def test_authn_s3_success_json_bad_tenant(self):
"""
Test that bad credentials returns a 401
"""
# Create dummy tenant (or adding creds will fail)
self.fixture_create_tenant(id='bad', name='bad')
access = "xpd285.access"
secret = "345fgi.secret"
kwargs = {
"user_name": self.auth_user['name'],
"tenant_id": 'bad',
"type": "EC2",
"key": access,
"secret": secret,
}
self.fixture_create_credentials(**kwargs)
# Delete the 'bad' tenant, orphaning the creds
self.get_request('DELETE', '/tenants/bad')
url = "/tokens"
req = self.get_request('POST', url)
params = {
"x-amz-acl": "public-read-write",
"x-amz-server-side-encryption": "AES256",
}
credentials = {
"access": access,
"verb": "PUT",
"path": "/test.txt",
"expire": 0,
"content_type": "text/plain",
"content_md5": "1234567890abcdef",
"xheaders": params,
"signature": None,
}
sign = signer.Signer(secret)
obj_creds = auth.S3Credentials(**credentials)
credentials['signature'] = sign.generate(obj_creds, s3=True)
body = {
"auth": {
"OS-KSS3-s3Credentials": credentials,
}
}
req.body = json.dumps(body)
self.get_response()
expected = {
u'unauthorized': {
u'code': u'401',
u'message': u'Unauthorized on this tenant',
}
}
self.assert_dict_equal(expected, json.loads(self.res.body))
self.assertEqual(self.res.status_int, 401)
if __name__ == '__main__':
unittest.main()

View File

@ -69,6 +69,7 @@ setup(
'remoteauth=keystone.middleware.remoteauth:remoteauth_factory',
'tokenauth=keystone.middleware.auth_token:filter_factory',
'swiftauth=keystone.middleware.swift_auth:filter_factory',
's3token=keystone.middleware.s3_token:filter_factory',
],
},
)