diff --git a/doc/source/api.rst b/doc/source/api.rst index 4c7801b..0c3ced6 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -239,9 +239,9 @@ described:: # special group indicating an account admin and # .reseller_admin indicating a reseller admin. ], - "auth": "plaintext:" + "auth": ":" # The auth-type and key for the user; currently only - # plaintext is implemented. + # plaintext and sha1 are implemented as auth types. } For example:: diff --git a/doc/source/authtypes.rst b/doc/source/authtypes.rst new file mode 100644 index 0000000..e5109ea --- /dev/null +++ b/doc/source/authtypes.rst @@ -0,0 +1,10 @@ +.. _swauth_middleware_module: + +swauth.authtypes +================= + +.. automodule:: swauth.authtypes + :members: + :undoc-members: + :show-inheritance: + :noindex: diff --git a/doc/source/details.rst b/doc/source/details.rst index 11ad2bd..34074ea 100644 --- a/doc/source/details.rst +++ b/doc/source/details.rst @@ -31,8 +31,18 @@ objects contain a JSON dictionary of the format:: {"auth": ":", "groups": } -The `` can only be `plaintext` at this time, and the `` -is the plain text password itself. +The `` specifies how the user key is encoded. The default is `plaintext`, +which saves the user's key in plaintext in the `` field. +The value `sha1` is supported as well, which stores the user's key as a salted +SHA1 hash. The `` 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_salt = + +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 `` contains at least two groups. The first is a unique group identifying that user and it's name is of the format `:`. The diff --git a/doc/source/index.rst b/doc/source/index.rst index 34a61cb..9f0c40c 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -79,6 +79,7 @@ Contents swauth middleware api + authtypes Indices and tables ------------------ diff --git a/swauth/authtypes.py b/swauth/authtypes.py new file mode 100644 index 0000000..c3037bb --- /dev/null +++ b/swauth/authtypes.py @@ -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:" +- 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 diff --git a/swauth/middleware.py b/swauth/middleware.py index 573d398..88f39ab 100644 --- a/swauth/middleware.py +++ b/swauth/middleware.py @@ -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): """ diff --git a/test_swauth/unit/test_authtypes.py b/test_swauth/unit/test_authtypes.py new file mode 100644 index 0000000..eda1de4 --- /dev/null +++ b/test_swauth/unit/test_authtypes.py @@ -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() diff --git a/test_swauth/unit/test_middleware.py b/test_swauth/unit/test_middleware.py index c17dde7..9820631 100644 --- a/test_swauth/unit/test_middleware.py +++ b/test_swauth/unit/test_middleware.py @@ -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({