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:
parent
5b8682f2aa
commit
027782a95f
|
@ -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
|
|
@ -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>
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"credentials":[{
|
||||
"passwordCredentials":{
|
||||
"username":"test_user",
|
||||
"password":"mypass"
|
||||
}
|
||||
},
|
||||
{
|
||||
"OS-KSS3-s3Credentials":{
|
||||
"username":"test_user",
|
||||
"secret":"aaaaa",
|
||||
"signature":"bbb"
|
||||
}
|
||||
}
|
||||
],
|
||||
"credentials_links":[]
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"OS-KSS3-s3Credentials":{
|
||||
"username":"test_user",
|
||||
"secret":"aaaaa",
|
||||
"signature":"bbb"
|
||||
}
|
||||
}
|
|
@ -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"/>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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')
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue