Merge pull request #14 from pllopis/authtypes
Authtypes, adds Sha1 auth type
This commit is contained in:
commit
bbdad0432e
@ -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
10
doc/source/authtypes.rst
Normal file
@ -0,0 +1,10 @@
|
||||
.. _swauth_middleware_module:
|
||||
|
||||
swauth.authtypes
|
||||
=================
|
||||
|
||||
.. automodule:: swauth.authtypes
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:noindex:
|
@ -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
|
||||
|
@ -79,6 +79,7 @@ Contents
|
||||
swauth
|
||||
middleware
|
||||
api
|
||||
authtypes
|
||||
|
||||
Indices and tables
|
||||
------------------
|
||||
|
99
swauth/authtypes.py
Normal file
99
swauth/authtypes.py
Normal 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
|
@ -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):
|
||||
"""
|
||||
|
64
test_swauth/unit/test_authtypes.py
Normal file
64
test_swauth/unit/test_authtypes.py
Normal 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()
|
@ -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({
|
||||
|
Loading…
x
Reference in New Issue
Block a user