Merge pull request #14 from pllopis/authtypes

Authtypes, adds Sha1 auth type
This commit is contained in:
tlohg 2011-07-31 09:15:50 -07:00
commit bbdad0432e
8 changed files with 230 additions and 10 deletions

View File

@ -239,9 +239,9 @@ described::
# special group indicating an account admin and
# .reseller_admin indicating a reseller admin.
],
"auth": "plaintext:<key>"
"auth": "<auth-type>:<key>"
# The auth-type and key for the user; currently only
# plaintext is implemented.
# plaintext and sha1 are implemented as auth types.
}
For example::

10
doc/source/authtypes.rst Normal file
View File

@ -0,0 +1,10 @@
.. _swauth_middleware_module:
swauth.authtypes
=================
.. automodule:: swauth.authtypes
:members:
:undoc-members:
:show-inheritance:
:noindex:

View File

@ -31,8 +31,18 @@ objects contain a JSON dictionary of the format::
{"auth": "<auth_type>:<auth_value>", "groups": <groups_array>}
The `<auth_type>` can only be `plaintext` at this time, and the `<auth_value>`
is the plain text password itself.
The `<auth_type>` specifies how the user key is encoded. The default is `plaintext`,
which saves the user's key in plaintext in the `<auth_value>` field.
The value `sha1` is supported as well, which stores the user's key as a salted
SHA1 hash. The `<auth_type>` can be specified in the swauth section of the proxy server's
config file, along with the salt value in the following way::
auth_type = <auth_type>
auth_type_salt = <salt-value>
Both fields are optional. auth_type defaults to `plaintext` and auth_type_salt
defaults to "swauthsalt". Additional auth types can be implemented along with
existing ones in the authtypes.py module.
The `<groups_array>` contains at least two groups. The first is a unique group
identifying that user and it's name is of the format `<user>:<account>`. The

View File

@ -79,6 +79,7 @@ Contents
swauth
middleware
api
authtypes
Indices and tables
------------------

99
swauth/authtypes.py Normal file
View File

@ -0,0 +1,99 @@
# 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.
#
# Pablo Llopis 2011
"""
This module hosts available auth types for encoding and matching user keys.
For adding a new auth type, simply write a class that satisfies the following
conditions:
- For the class name, apitalize first letter only. This makes sure the user
can specify an all-lowercase config option such as "plaintext" or "sha1".
Swauth takes care of capitalizing the first letter before instantiating it.
- Write an encode(key) method that will take a single argument, the user's key,
and returns the encoded string. For plaintext, this would be
"plaintext:<key>"
- Write a match(key, creds) method that will take two arguments: the user's
key, and the user's retrieved credentials. Return a boolean value that
indicates whether the match is True or False.
Note that, since some of the encodings will be hashes, swauth supports the
notion of salts. Thus, self.salt will be set to either a user-specified salt
value or to a default value.
"""
import hashlib
class Plaintext(object):
"""
Provides a particular auth type for encoding format for encoding and
matching user keys.
This class must be all lowercase except for the first character, which
must be capitalized. encode and match methods must be provided and are
the only ones that will be used by swauth.
"""
def encode(self, key):
"""
Encodes a user key into a particular format. The result of this method
will be used by swauth for storing user credentials.
:param key: User's secret key
:returns: A string representing user credentials
"""
return "plaintext:%s" % key
def match(self, key, creds):
"""
Checks whether the user-provided key matches the user's credentials
:param key: User-supplied key
:param creds: User's stored credentials
:returns: True if the supplied key is valid, False otherwise
"""
return self.encode(key) == creds
class Sha1(object):
"""
Provides a particular auth type for encoding format for encoding and
matching user keys.
This class must be all lowercase except for the first character, which
must be capitalized. encode and match methods must be provided and are
the only ones that will be used by swauth.
"""
def encode(self, key):
"""
Encodes a user key into a particular format. The result of this method
will be used by swauth for storing user credentials.
:param key: User's secret key
:returns: A string representing user credentials
"""
enc_key = '%s%s' % (self.salt, key)
enc_val = hashlib.sha1(enc_key).hexdigest()
return "sha1:%s$%s" % (self.salt, enc_val)
def match(self, key, creds):
"""
Checks whether the user-provided key matches the user's credentials
:param key: User-supplied key
:param creds: User's stored credentials
:returns: True if the supplied key is valid, False otherwise
"""
return self.encode(key) == creds

View File

@ -38,6 +38,8 @@ from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
from swift.common.utils import cache_from_env, get_logger, split_path, \
TRUE_VALUES, urlparse
import sys
import swauth.authtypes
# NOTE: This should be removed after some time when anyone upgrading Swauth
# should have also upgraded Swift.
@ -46,6 +48,7 @@ try:
# this.
from swift.common.utils import get_remote_client
except ImportError:
# Fall back to locally defined version.
def get_remote_client(req):
# remote host for zeus
@ -144,6 +147,14 @@ class Swauth(object):
self.allowed_sync_hosts = [h.strip()
for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',')
if h.strip()]
# Get an instance of our auth_type encoder for saving and checking the
# user's key
self.auth_type = conf.get('auth_type', 'Plaintext').title()
self.auth_encoder = getattr(swauth.authtypes, self.auth_type, None)
if self.auth_encoder is None:
raise Exception('Invalid auth_type in config file: %s'
% self.auth_type)
self.auth_encoder.salt = conf.get('auth_type_salt', 'swauthsalt')
def __call__(self, env, start_response):
"""
@ -1003,8 +1014,9 @@ class Swauth(object):
groups.append('.admin')
if reseller_admin:
groups.append('.reseller_admin')
auth_value = self.auth_encoder().encode(key)
resp = self.make_request(req.environ, 'PUT', path,
json.dumps({'auth': 'plaintext:%s' % key,
json.dumps({'auth': auth_value,
'groups': [{'name': g} for g in groups]}),
headers=headers).get_response(self.app)
if resp.status_int == 404:
@ -1206,8 +1218,8 @@ class Swauth(object):
# Record the token with the user info for future use.
path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
resp = self.make_request(req.environ, 'POST', path,
headers={'X-Object-Meta-Auth-Token': token}
).get_response(self.app)
headers={'X-Object-Meta-Auth-Token': token}).get_response(
self.app)
if resp.status_int // 100 != 2:
raise Exception('Could not save new token: %s %s' %
(path, resp.status))
@ -1369,14 +1381,15 @@ class Swauth(object):
def credentials_match(self, user_detail, key):
"""
Returns True if the key is valid for the user_detail. Currently, this
only supports plaintext key matching.
Returns True if the key is valid for the user_detail.
It will use self.auth_encoder to check for a key match.
:param user_detail: The dict for the user.
:param key: The key to validate for the user.
:returns: True if the key is valid for the user, False if not.
"""
return user_detail and user_detail.get('auth') == 'plaintext:%s' % key
return user_detail and self.auth_encoder().match(
key, user_detail.get('auth'))
def is_super_admin(self, req):
"""

View File

@ -0,0 +1,64 @@
# 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.
#
# Pablo Llopis 2011
import unittest
from contextlib import contextmanager
from swauth import authtypes
class TestPlaintext(unittest.TestCase):
def setUp(self):
self.auth_encoder = authtypes.Plaintext()
def test_plaintext_encode(self):
enc_key = self.auth_encoder.encode('keystring')
self.assertEquals('plaintext:keystring', enc_key)
def test_plaintext_valid_match(self):
creds = 'plaintext:keystring'
match = self.auth_encoder.match('keystring', creds)
self.assertEquals(match, True)
def test_plaintext_invalid_match(self):
creds = 'plaintext:other-keystring'
match = self.auth_encoder.match('keystring', creds)
self.assertEquals(match, False)
class TestSha1(unittest.TestCase):
def setUp(self):
self.auth_encoder = authtypes.Sha1()
self.auth_encoder.salt = 'salt'
def test_sha1_encode(self):
enc_key = self.auth_encoder.encode('keystring')
self.assertEquals('sha1:salt$d50dc700c296e23ce5b41f7431a0e01f69010f06',
enc_key)
def test_sha1_valid_match(self):
creds = 'sha1:salt$d50dc700c296e23ce5b41f7431a0e01f69010f06'
match = self.auth_encoder.match('keystring', creds)
self.assertEquals(match, True)
def test_sha1_invalid_match(self):
creds = 'sha1:salt$deadbabedeadbabedeadbabec0ffeebadc0ffeee'
match = self.auth_encoder.match('keystring', creds)
self.assertEquals(match, False)
if __name__ == '__main__':
unittest.main()

View File

@ -145,6 +145,29 @@ class TestAuth(unittest.TestCase):
'auth_prefix': 'test'})(app)
self.assertEquals(ath.auth_prefix, '/test/')
def test_no_auth_type_init(self):
app = FakeApp()
ath = auth.filter_factory({})(app)
self.assertEquals(ath.auth_type, 'Plaintext')
def test_valid_auth_type_init(self):
app = FakeApp()
ath = auth.filter_factory({'auth_type': 'sha1'})(app)
self.assertEquals(ath.auth_type, 'Sha1')
ath = auth.filter_factory({'auth_type': 'plaintext'})(app)
self.assertEquals(ath.auth_type, 'Plaintext')
def test_invalid_auth_type_init(self):
app = FakeApp()
exc = None
try:
auth.filter_factory({'auth_type': 'NONEXISTANT'})(app)
except Exception as err:
exc = err
self.assertEquals(str(exc),
'Invalid auth_type in config file: %s' %
'Nonexistant')
def test_default_swift_cluster_init(self):
app = FakeApp()
self.assertRaises(Exception, auth.filter_factory({