bottle-cork-0.12.0/ 0000755 0001750 0001750 00000000000 12617467123 013660 5 ustar fede fede 0000000 0000000 bottle-cork-0.12.0/PKG-INFO 0000644 0001750 0001750 00000002242 12617467123 014755 0 ustar fede fede 0000000 0000000 Metadata-Version: 1.1
Name: bottle-cork
Version: 0.12.0
Summary: Authentication/Authorization library for Bottle
Home-page: http://cork.firelet.net/
Author: Federico Ceratto
Author-email: federico.ceratto@gmail.com
License: LGPLv3+
Description: Cork is a simple Authentication/Authorization libraryfor the Bottle and Flask web frameworks.
Platform: Linux
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Bottle
Classifier: Framework :: Flask
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
Classifier: Natural Language :: English
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.4
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Libraries :: Python Modules
bottle-cork-0.12.0/README.rst 0000644 0001750 0001750 00000001740 12512453671 015346 0 ustar fede fede 0000000 0000000 Cork - Authentication for the Bottle web framework
.. image:: https://secure.travis-ci.org/FedericoCeratto/bottle-cork.png?branch=master
:target: http://travis-ci.org/FedericoCeratto/bottle-cork
:alt: Build status
.. image:: https://coveralls.io/repos/FedericoCeratto/bottle-cork/badge.png?branch=master
:target: https://coveralls.io/r/FedericoCeratto/bottle-cork?branch=master
:alt: Coverage
.. image:: https://pypip.in/download/bottle-cork/badge.png
:target: https://pypi.python.org/pypi//bottle-cork/
:alt: Downloads
.. image:: https://pypip.in/version/bottle-cork/badge.png
:target: https://pypi.python.org/pypi/bottle-cork/
:alt: Latest Version
.. image:: https://pypip.in/license/bottle-cork/badge.png
:target: https://pypi.python.org/pypi/bottle-cork/
:alt: License
Cork provides a simple set of methods to implement Authentication and Authorization in web applications based on Bottle.
Website and Documentation:
http://cork.firelet.net
bottle-cork-0.12.0/setup.cfg 0000644 0001750 0001750 00000000073 12617467123 015501 0 ustar fede fede 0000000 0000000 [egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
bottle-cork-0.12.0/tests/ 0000755 0001750 0001750 00000000000 12617467123 015022 5 ustar fede fede 0000000 0000000 bottle-cork-0.12.0/tests/simple_webapp_decorated.py 0000777 0001750 0001750 00000000000 12512453671 031473 2../examples/simple_webapp_decorated.py ustar fede fede 0000000 0000000 bottle-cork-0.12.0/tests/test_flask.py 0000644 0001750 0001750 00000100555 12512453671 017536 0 ustar fede fede 0000000 0000000 # Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Unit testing
#
from base64 import b64encode, b64decode
from nose import SkipTest
from nose.tools import assert_raises, raises, with_setup
from time import time
import mock
import os
import shutil
from cork import Cork, JsonBackend, AAAException, AuthException
from cork import Mailer
from cork.base_backend import BackendIOException
import testutils
testdir = None # Test directory
aaa = None # global Cork instance
cookie_name = None # global variable to track cookie status
tmproot = testutils.pick_temp_directory()
class RoAttrDict(dict):
"""Read-only attribute-accessed dictionary.
Used to mock beaker's session objects
"""
def __getattr__(self, name):
return self[name]
class MockedAdminCork(Cork):
"""Mocked module where the current user is always 'admin'"""
@property
def _beaker_session(self):
return RoAttrDict(username='admin')
def _setup_cookie(self, username):
global cookie_name
cookie_name = username
class MockedUnauthenticatedCork(Cork):
"""Mocked module where the current user not set"""
@property
def _beaker_session(self):
return RoAttrDict()
def _setup_cookie(self, username):
global cookie_name
cookie_name = username
def setup_empty_dir():
"""Setup test directory without JSON files"""
global testdir
tstamp = "%f" % time()
testdir = "%s/fl_%s" % (tmproot, tstamp)
os.mkdir(testdir)
os.mkdir(testdir + '/view')
print("setup done in %s" % testdir)
def setup_dir():
"""Setup test directory with valid JSON files"""
global testdir
tstamp = "%f" % time()
testdir = "%s/fl_%s" % (tmproot, tstamp)
os.mkdir(testdir)
os.mkdir(testdir + '/views')
with open("%s/users.json" % testdir, 'w') as f:
f.write("""{"admin": {"email_addr": null, "desc": null, "role": "admin", "hash": "69f75f38ac3bfd6ac813794f3d8c47acc867adb10b806e8979316ddbf6113999b6052efe4ba95c0fa9f6a568bddf60e8e5572d9254dbf3d533085e9153265623", "creation_date": "2012-04-09 14:22:27.075596"}}""")
with open("%s/roles.json" % testdir, 'w') as f:
f.write("""{"special": 200, "admin": 100, "user": 50}""")
with open("%s/register.json" % testdir, 'w') as f:
f.write("""{}""")
with open("%s/views/registration_email.tpl" % testdir, 'w') as f:
f.write("""Username:{{username}} Email:{{email_addr}} Code:{{registration_code}}""")
with open("%s/views/password_reset_email.tpl" % testdir, 'w') as f:
f.write("""Username:{{username}} Email:{{email_addr}} Code:{{reset_code}}""")
print("setup done in %s" % testdir)
def setup_mockedadmin():
"""Setup test directory and a MockedAdminCork instance"""
global aaa
global cookie_name
setup_dir()
aaa = MockedAdminCork(testdir, smtp_server='localhost', email_sender='test@localhost')
cookie_name = None
def setup_mocked_unauthenticated():
"""Setup test directory and a MockedAdminCork instance"""
global aaa
global cookie_name
setup_dir()
aaa = MockedUnauthenticatedCork(testdir)
cookie_name = None
def teardown_dir():
global cookie_name
global testdir
if testdir:
shutil.rmtree(testdir)
testdir = None
cookie_name = None
@with_setup(setup_dir, teardown_dir)
def test_init():
Cork(testdir)
@with_setup(setup_dir, teardown_dir)
def test_initialize_storage():
jb = JsonBackend(testdir, initialize=True)
Cork(backend=jb)
with open("%s/users.json" % testdir) as f:
assert f.readlines() == ['{}']
with open("%s/roles.json" % testdir) as f:
assert f.readlines() == ['{}']
with open("%s/register.json" % testdir) as f:
assert f.readlines() == ['{}']
with open("%s/views/registration_email.tpl" % testdir) as f:
assert f.readlines() == [
'Username:{{username}} Email:{{email_addr}} Code:{{registration_code}}']
with open("%s/views/password_reset_email.tpl" % testdir) as f:
assert f.readlines() == [
'Username:{{username}} Email:{{email_addr}} Code:{{reset_code}}']
@raises(BackendIOException)
@with_setup(setup_dir, teardown_dir)
def test_unable_to_save():
bogus_dir = '/___inexisting_directory___'
Cork(bogus_dir, initialize=True)
@with_setup(setup_mockedadmin, teardown_dir)
def test_mockedadmin():
assert len(aaa._store.users) == 1, repr(aaa._store.users)
assert 'admin' in aaa._store.users, repr(aaa._store.users)
@raises(BackendIOException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_loadjson_missing_file():
aaa._store._loadjson('nonexistent_file', {})
@raises(BackendIOException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_loadjson_broken_file():
with open(testdir + '/broken_file.json', 'w') as f:
f.write('-----')
aaa._store._loadjson('broken_file', {})
@with_setup(setup_mockedadmin, teardown_dir)
def test_loadjson_unchanged():
# By running _refresh with unchanged files the files should not be reloaded
mtimes = aaa._store._mtimes
aaa._store._refresh()
# The test simply ensures that no mtimes have been updated
assert mtimes == aaa._store._mtimes
# Test PBKDF2-based password hashing
@with_setup(setup_mockedadmin, teardown_dir)
def test_password_hashing_PBKDF2():
shash = aaa._hash('user_foo', 'bogus_pwd')
assert len(shash) == 88, "hash length should be 88 and is %d" % len(shash)
assert shash.endswith('='), "hash should end with '='"
assert aaa._verify_password('user_foo', 'bogus_pwd', shash) == True, \
"Hashing verification should succeed"
@with_setup(setup_mockedadmin, teardown_dir)
def test_password_hashing_PBKDF2_known_hash():
salt = 's' * 32
shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt)
assert shash == 'cHNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzax44AxQgK6uD9q1YWxLos1ispCe1Z7T7pOFK1PwdWEs='
@with_setup(setup_mockedadmin, teardown_dir)
def test_password_hashing_PBKDF2_known_hash_2():
salt = '\0' * 32
shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt)
assert shash == 'cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8Uh4pyEOHoRz4j0lDzAmqb7Dvmo8GpeXwiKTDsuYFw='
@with_setup(setup_mockedadmin, teardown_dir)
def test_password_hashing_PBKDF2_known_hash_3():
salt = 'x' * 32
shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt)
assert shash == 'cHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4MEaIU5Op97lmvwX5NpVSTBP8jg8OlrN7c2K8K8tnNks='
@raises(AssertionError)
@with_setup(setup_mockedadmin, teardown_dir)
def test_password_hashing_PBKDF2_incorrect_hash_len():
salt = 'x' * 31 # Incorrect length
shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt)
@with_setup(setup_mockedadmin, teardown_dir)
def test_password_hashing_PBKDF2_incorrect_hash_value():
shash = aaa._hash('user_foo', 'bogus_pwd')
assert len(shash) == 88, "hash length should be 88 and is %d" % len(shash)
assert shash.endswith('='), "hash should end with '='"
assert aaa._verify_password('user_foo', '####', shash) == False, \
"Hashing verification should fail"
assert aaa._verify_password('###', 'bogus_pwd', shash) == False, \
"Hashing verification should fail"
@with_setup(setup_mockedadmin, teardown_dir)
def test_password_hashing_PBKDF2_collision():
salt = 'S' * 32
hash1 = aaa._hash('user_foo', 'bogus_pwd', salt=salt)
hash2 = aaa._hash('user_foobogus', '_pwd', salt=salt)
assert hash1 != hash2, "Hash collision"
# Test password hashing for inexistent algorithms
@raises(RuntimeError)
@with_setup(setup_mockedadmin, teardown_dir)
def test_password_hashing_bogus_algo():
aaa._hash('user_foo', 'bogus_pwd', algo='bogus_algo')
@raises(RuntimeError)
@with_setup(setup_mockedadmin, teardown_dir)
def test_password_hashing_bogus_algo_during_verify():
# Incorrect hash type (starts with "X")
shash = b64encode('X' + 'bogusstring')
aaa._verify_password('user_foo', 'bogus_pwd', shash)
# End of password hashing tests
@with_setup(setup_mockedadmin, teardown_dir)
def test_unauth_create_role():
aaa._store.roles['admin'] = 10 # lower admin level
assert_raises(AuthException, aaa.create_role, 'user', 33)
@with_setup(setup_mockedadmin, teardown_dir)
def test_create_existing_role():
assert_raises(AAAException, aaa.create_role, 'user', 33)
@raises(AAAException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_create_role_with_incorrect_level():
aaa.create_role('new_user', 'not_a_number')
@with_setup(setup_mockedadmin, teardown_dir)
def test_create_role():
assert len(aaa._store.roles) == 3, repr(aaa._store.roles)
aaa.create_role('user33', 33)
assert len(aaa._store.roles) == 4, repr(aaa._store.roles)
fname = "%s/%s.json" % (aaa._store._directory, aaa._store._roles_fname)
with open(fname) as f:
data = f.read()
assert 'user33' in data, repr(data)
@SkipTest
@raises(AAAException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_create_empty_role():
aaa.create_role('', 42)
@with_setup(setup_mockedadmin, teardown_dir)
def test_unauth_delete_role():
aaa._store.roles['admin'] = 10 # lower admin level
assert_raises(AuthException, aaa.delete_role, 'user')
@with_setup(setup_mockedadmin, teardown_dir)
def test_delete_nonexistent_role():
assert_raises(AAAException, aaa.delete_role, 'user123')
@with_setup(setup_mockedadmin, teardown_dir)
def test_create_delete_role():
assert len(aaa._store.roles) == 3, repr(aaa._store.roles)
aaa.create_role('user33', 33)
assert len(aaa._store.roles) == 4, repr(aaa._store.roles)
fname = "%s/%s.json" % (aaa._store._directory, aaa._store._roles_fname)
with open(fname) as f:
data = f.read()
assert 'user33' in data, repr(data)
assert aaa._store.roles['user33'] == 33
aaa.delete_role('user33')
assert len(aaa._store.roles) == 3, repr(aaa._store.roles)
@with_setup(setup_mockedadmin, teardown_dir)
def test_list_roles():
roles = list(aaa.list_roles())
assert len(roles) == 3, "Incorrect. Users are: %s" % repr(aaa._store.roles)
@with_setup(setup_mockedadmin, teardown_dir)
def test_unauth_create_user():
aaa._store.roles['admin'] = 10 # lower admin level
assert_raises(AuthException, aaa.create_user, 'phil', 'user', 'hunter123')
@with_setup(setup_mockedadmin, teardown_dir)
def test_create_existing_user():
assert_raises(AAAException, aaa.create_user, 'admin', 'admin', 'bogus')
@raises(AAAException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_create_user_with_wrong_role():
aaa.create_user('admin2', 'nonexistent_role', 'bogus')
@with_setup(setup_mockedadmin, teardown_dir)
def test_create_user():
assert len(aaa._store.users) == 1, repr(aaa._store.users)
aaa.create_user('phil', 'user', 'user')
assert len(aaa._store.users) == 2, repr(aaa._store.users)
fname = "%s/%s.json" % (aaa._store._directory, aaa._store._users_fname)
with open(fname) as f:
data = f.read()
assert 'phil' in data, repr(data)
@with_setup(setup_mockedadmin, teardown_dir)
def test_unauth_delete_user():
aaa._store.roles['admin'] = 10 # lower admin level
assert_raises(AuthException, aaa.delete_user, 'phil')
@with_setup(setup_mockedadmin, teardown_dir)
def test_delete_nonexistent_user():
assert_raises(AAAException, aaa.delete_user, 'not_an_user')
@with_setup(setup_mockedadmin, teardown_dir)
def test_delete_user():
assert len(aaa._store.users) == 1, repr(aaa._store.users)
aaa.delete_user('admin')
assert len(aaa._store.users) == 0, repr(aaa._store.users)
fname = "%s/%s.json" % (aaa._store._directory, aaa._store._users_fname)
with open(fname) as f:
data = f.read()
assert 'admin' not in data, "'admin' must not be in %s" % repr(data)
@with_setup(setup_mockedadmin, teardown_dir)
def test_list_users():
users = list(aaa.list_users())
assert len(users) == 1, "Incorrect. Users are: %s" % repr(aaa._store.users)
@with_setup(setup_mockedadmin, teardown_dir)
def test_failing_login():
login = aaa.login('phil', 'hunter123')
assert login == False, "Login must fail"
global cookie_name
assert cookie_name == None
@with_setup(setup_mockedadmin, teardown_dir)
def test_login_nonexistent_user_empty_password():
login = aaa.login('IAmNotHome', '')
assert login == False, "Login must fail"
global cookie_name
assert cookie_name == None
@with_setup(setup_mockedadmin, teardown_dir)
def test_login_existing_user_empty_password():
aaa.create_user('phil', 'user', 'hunter123')
assert 'phil' in aaa._store.users
assert aaa._store.users['phil']['role'] == 'user'
login = aaa.login('phil', '')
assert login == False, "Login must fail"
global cookie_name
assert cookie_name == None
@with_setup(setup_mockedadmin, teardown_dir)
def test_create_and_validate_user():
aaa.create_user('phil', 'user', 'hunter123')
assert 'phil' in aaa._store.users
assert aaa._store.users['phil']['role'] == 'user'
login = aaa.login('phil', 'hunter123')
assert login == True, "Login must succeed"
global cookie_name
assert cookie_name == 'phil'
@with_setup(setup_mockedadmin, teardown_dir)
def test_require_failing_username():
# The user exists, but I'm 'admin'
aaa.create_user('phil', 'user', 'hunter123')
assert_raises(AuthException, aaa.require, username='phil')
@with_setup(setup_mockedadmin, teardown_dir)
def test_require_nonexistent_username():
assert_raises(AAAException, aaa.require, username='no_such_user')
@with_setup(setup_mockedadmin, teardown_dir)
def test_require_failing_role_fixed():
assert_raises(AuthException, aaa.require, role='user', fixed_role=True)
@raises(AAAException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_require_missing_parameter():
aaa.require(fixed_role=True)
@with_setup(setup_mockedadmin, teardown_dir)
def test_require_nonexistent_role():
assert_raises(AAAException, aaa.require, role='clown')
@with_setup(setup_mockedadmin, teardown_dir)
def test_require_failing_role():
# Requesting level >= 100
assert_raises(AuthException, aaa.require, role='special')
@with_setup(setup_mockedadmin, teardown_dir)
def test_successful_require_role():
aaa.require(username='admin')
aaa.require(username='admin', role='admin')
aaa.require(username='admin', role='admin', fixed_role=True)
aaa.require(username='admin', role='user')
@with_setup(setup_mockedadmin, teardown_dir)
def test_authenticated_is_not__anonymous():
assert not aaa.user_is_anonymous
@with_setup(setup_mockedadmin, teardown_dir)
def test_update_nonexistent_role():
assert_raises(AAAException, aaa.current_user.update, role='clown')
@raises(AAAException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_update_nonexistent_user():
aaa._store.users.pop('admin')
aaa.current_user.update(role='user')
@with_setup(setup_mockedadmin, teardown_dir)
def test_update_role():
aaa.current_user.update(role='user')
assert aaa._store.users['admin']['role'] == 'user'
@with_setup(setup_mockedadmin, teardown_dir)
def test_update_pwd():
aaa.current_user.update(pwd='meow')
@with_setup(setup_mockedadmin, teardown_dir)
def test_update_email():
aaa.current_user.update(email_addr='foo')
assert aaa._store.users['admin']['email_addr'] == 'foo'
@raises(AAAException)
@with_setup(setup_mocked_unauthenticated, teardown_dir)
def test_get_current_user_unauth():
aaa.current_user['username']
@with_setup(setup_mocked_unauthenticated, teardown_dir)
def test_unauth_is_anonymous():
assert aaa.user_is_anonymous
@raises(AuthException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_get_current_user_nonexistent():
# The current user 'admin' is not in the user table
aaa._store.users.pop('admin')
aaa.current_user
@with_setup(setup_mockedadmin, teardown_dir)
def test_get_nonexistent_user():
assert aaa.user('nonexistent_user') is None
@with_setup(setup_mockedadmin, teardown_dir)
def test_get_user_description_field():
admin = aaa.user('admin')
for field in ['description', 'email_addr']:
assert field in admin.__dict__
@with_setup(setup_mockedadmin, teardown_dir)
def test_register_no_user():
assert_raises(AssertionError, aaa.register, None, 'pwd', 'a@a.a')
@with_setup(setup_mockedadmin, teardown_dir)
def test_register_no_pwd():
assert_raises(AssertionError, aaa.register, 'foo', None, 'a@a.a')
@with_setup(setup_mockedadmin, teardown_dir)
def test_register_no_email():
assert_raises(AssertionError, aaa.register, 'foo', 'pwd', None)
@with_setup(setup_mockedadmin, teardown_dir)
def test_register_already_existing():
assert_raises(AAAException, aaa.register, 'admin', 'pwd', 'a@a.a')
@with_setup(setup_mockedadmin, teardown_dir)
def test_register_no_role():
assert_raises(AAAException, aaa.register, 'foo', 'pwd', 'a@a.a', role='clown')
@with_setup(setup_mockedadmin, teardown_dir)
def test_register_role_too_high():
assert_raises(AAAException, aaa.register, 'foo', 'pwd', 'a@a.a', role='admin')
# Patch the mailer _send() method to prevent network interactions
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch.object(Mailer, '_send')
def test_register(mocked):
old_dir = os.getcwd()
os.chdir(testdir)
aaa.register('foo', 'pwd', 'a@a.a')
os.chdir(old_dir)
assert len(aaa._store.pending_registrations) == 1, repr(aaa._store.pending_registrations)
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_1():
c = aaa.mailer._parse_smtp_url('')
assert c['proto'] == 'smtp'
assert c['user'] == None
assert c['pass'] == None
assert c['fqdn'] == ''
assert c['port'] == 25
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_2():
c = aaa.mailer._parse_smtp_url('starttls://foo')
assert c['proto'] == 'starttls'
assert c['user'] == None
assert c['pass'] == None
assert c['fqdn'] == 'foo'
assert c['port'] == 25
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_3():
c = aaa.mailer._parse_smtp_url('foo:443')
assert c['proto'] == 'smtp'
assert c['user'] == None
assert c['pass'] == None
assert c['fqdn'] == 'foo'
assert c['port'] == 443
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_4():
c = aaa.mailer._parse_smtp_url('ssl://user:pass@foo:443/')
assert c['proto'] == 'ssl'
assert c['user'] == 'user'
assert c['pass'] == 'pass'
assert c['fqdn'] == 'foo'
assert c['port'] == 443
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_5():
c = aaa.mailer._parse_smtp_url('smtp://smtp.magnet.ie')
assert c['proto'] == 'smtp'
assert c['user'] == None
assert c['pass'] == None
assert c['fqdn'] == 'smtp.magnet.ie'
assert c['port'] == 25
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_email_as_username_no_password():
# the username contains an at sign '@'
c = aaa.mailer._parse_smtp_url('ssl://us.er@somewhere.net@foo:443/')
assert c['proto'] == 'ssl'
assert c['user'] == 'us.er@somewhere.net', \
"Username is incorrectly parsed as '%s'" % c['user']
assert c['pass'] == None
assert c['fqdn'] == 'foo'
assert c['port'] == 443
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_email_as_username():
# the username contains an at sign '@'
c = aaa.mailer._parse_smtp_url('ssl://us.er@somewhere.net:pass@foo:443/')
assert c['proto'] == 'ssl'
assert c['user'] == 'us.er@somewhere.net', \
"Username is incorrectly parsed as '%s'" % c['user']
assert c['pass'] == 'pass'
assert c['fqdn'] == 'foo'
assert c['port'] == 443
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_at_sign_in_password():
# the password contains at signs '@'
c = aaa.mailer._parse_smtp_url('ssl://username:pass@w@rd@foo:443/')
assert c['proto'] == 'ssl'
assert c['user'] == 'username', \
"Username is incorrectly parsed as '%s'" % c['user']
assert c['pass'] == 'pass@w@rd', \
"Password is incorrectly parsed as '%s'" % c['pass']
assert c['fqdn'] == 'foo'
assert c['port'] == 443
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_email_as_username_2():
# both the username and the password contains an at sign '@'
c = aaa.mailer._parse_smtp_url('ssl://us.er@somewhere.net:pass@word@foo:443/')
assert c['proto'] == 'ssl'
assert c['user'] == 'us.er@somewhere.net', \
"Username is incorrectly parsed as '%s'" % c['user']
assert c['pass'] == 'pass@word', \
"Password is incorrectly parsed as '%s'" % c['pass']
assert c['fqdn'] == 'foo'
assert c['port'] == 443
@raises(RuntimeError)
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_incorrect_URL_port():
c = aaa.mailer._parse_smtp_url(':99999')
@raises(RuntimeError)
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_incorrect_URL_port_len():
c = aaa.mailer._parse_smtp_url(':123456')
@raises(RuntimeError)
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_incorrect_URL_len():
c = aaa.mailer._parse_smtp_url('a' * 256)
@raises(RuntimeError)
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_incorrect_URL_syntax():
c = aaa.mailer._parse_smtp_url('::')
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_IPv4():
c = aaa.mailer._parse_smtp_url('127.0.0.1')
assert c['fqdn'] == '127.0.0.1'
@with_setup(setup_mockedadmin, teardown_dir)
def test_smtp_url_parsing_IPv6():
c = aaa.mailer._parse_smtp_url('[2001:0:0123:4567:89ab:cdef]')
assert c['fqdn'] == '[2001:0:0123:4567:89ab:cdef]'
# Patch the SMTP class to prevent network interactions
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch('cork.cork.SMTP')
def test_send_email_SMTP(SMTP):
SMTP.return_value = msession = mock.Mock() # session instance
aaa.mailer.send_email('address', ' sbj', 'text')
aaa.mailer.join()
SMTP.assert_called_once_with('localhost', 25)
assert msession.sendmail.call_count == 1
assert msession.quit.call_count == 1
assert len(msession.method_calls) == 2
# Patch the SMTP_SSL class to prevent network interactions
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch('cork.cork.SMTP_SSL')
def test_send_email_SMTP_SSL(SMTP_SSL):
SMTP_SSL.return_value = msession = mock.Mock() # session instance
aaa.mailer._conf['proto'] = 'ssl'
aaa.mailer.send_email('address', ' sbj', 'text')
aaa.mailer.join()
SMTP_SSL.assert_called_once_with('localhost', 25)
assert msession.sendmail.call_count == 1
assert msession.quit.call_count == 1
assert len(msession.method_calls) == 2
# Patch the SMTP_SSL class to prevent network interactions
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch('cork.cork.SMTP_SSL')
def test_send_email_SMTP_SSL_with_login(SMTP_SSL):
SMTP_SSL.return_value = msession = mock.Mock() # session instance
aaa.mailer._conf['proto'] = 'ssl'
aaa.mailer._conf['user'] = 'username'
aaa.mailer.send_email('address', ' sbj', 'text')
aaa.mailer.join()
SMTP_SSL.assert_called_once_with('localhost', 25)
assert msession.login.call_count == 1
assert msession.sendmail.call_count == 1
assert msession.quit.call_count == 1
assert len(msession.method_calls) == 3
# Patch the SMTP_SSL class to prevent network interactions
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch('cork.cork.SMTP')
def test_send_email_SMTP_STARTTLS(SMTP):
SMTP.return_value = msession = mock.Mock() # session instance
aaa.mailer._conf['proto'] = 'starttls'
aaa.mailer.send_email('address', ' sbj', 'text')
aaa.mailer.join()
SMTP.assert_called_once_with('localhost', 25)
assert msession.ehlo.call_count == 2
assert msession.starttls.call_count == 1
assert msession.sendmail.call_count == 1
assert msession.quit.call_count == 1
assert len(msession.method_calls) == 5
@raises(AAAException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_do_not_send_email():
aaa.mailer._conf['fqdn'] = None # disable email delivery
aaa.mailer.send_email('address', 'sbj', 'text')
aaa.mailer.join()
@with_setup(setup_mockedadmin, teardown_dir)
def test_validate_registration_no_code():
assert_raises(AAAException, aaa.validate_registration, 'not_a_valid_code')
# Patch the mailer _send() method to prevent network interactions
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch.object(Mailer, '_send')
def test_validate_registration(mocked):
# create registration
old_dir = os.getcwd()
os.chdir(testdir)
aaa.register('user_foo', 'pwd', 'a@a.a')
os.chdir(old_dir)
assert len(aaa._store.pending_registrations) == 1, repr(aaa._store.pending_registrations)
# get the registration code, and run validate_registration
code = aaa._store.pending_registrations.keys()[0]
user_data = aaa._store.pending_registrations[code]
aaa.validate_registration(code)
assert user_data['username'] in aaa._store.users, "Account should have been added"
# test login
login = aaa.login('user_foo', 'pwd')
assert login == True, "Login must succeed"
# The registration should have been removed
assert len(aaa._store.pending_registrations) == 0, repr(aaa._store.pending_registrations)
# Patch the mailer _send() method to prevent network interactions
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch.object(Mailer, '_send')
def test_purge_expired_registration(mocked):
old_dir = os.getcwd()
os.chdir(testdir)
aaa.register('foo', 'pwd', 'a@a.a')
os.chdir(old_dir)
assert len(aaa._store.pending_registrations) == 1, "The registration should" \
" be present"
aaa._purge_expired_registrations()
assert len(aaa._store.pending_registrations) == 1, "The registration should " \
"be still there"
aaa._purge_expired_registrations(exp_time=0)
assert len(aaa._store.pending_registrations) == 0, "The registration should " \
"have been removed"
# Patch the mailer _send() method to prevent network interactions
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch.object(Mailer, '_send')
def test_prevent_double_registration(mocked):
# Create two registration requests, then validate them.
# The first should succeed, the second one fail as the account has been created.
# create first registration
old_dir = os.getcwd()
os.chdir(testdir)
aaa.register('user_foo', 'first_pwd', 'a@a.a')
assert len(aaa._store.pending_registrations) == 1, repr(aaa._store.pending_registrations)
first_registration_code = aaa._store.pending_registrations.keys()[0]
# create second registration
aaa.register('user_foo', 'second_pwd', 'b@b.b')
os.chdir(old_dir)
assert len(aaa._store.pending_registrations) == 2, repr(aaa._store.pending_registrations)
registration_codes = aaa._store.pending_registrations.keys()
if first_registration_code == registration_codes[0]:
second_registration_code = registration_codes[1]
else:
second_registration_code = registration_codes[0]
# Only the 'admin' account exists
assert len(aaa._store.users) == 1
# Run validate_registration with the first registration
aaa.validate_registration(first_registration_code)
assert 'user_foo' in aaa._store.users, "Account should have been added"
assert len(aaa._store.users) == 2
# After the first registration only one pending registration should be left
# The registration having 'a@a.a' email address should be gone
assert len(aaa._store.pending_registrations) == 1, repr(aaa._store.pending_registrations)
pr_code, pr_data = aaa._store.pending_registrations.items()[0]
assert pr_data['email_addr'] == 'b@b.b', "Incorrect registration in the datastore"
# Logging in using the first login should succeed
login = aaa.login('user_foo', 'first_pwd')
assert login == True, "Login must succed"
assert len(aaa._store.pending_registrations) == 1, repr(aaa._store.pending_registrations)
# Run validate_registration with the second registration code
# The second registration should fail as the user account exists
assert_raises(AAAException, aaa.validate_registration, second_registration_code)
# test login
login = aaa.login('user_foo', 'second_pwd')
assert login == False, "Login must fail"
@raises(AAAException)
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch.object(Mailer, '_send')
def test_send_password_reset_email_no_params(mocked):
aaa.send_password_reset_email()
@raises(AAAException)
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch.object(Mailer, '_send')
def test_send_password_reset_email_incorrect_addr(mocked):
aaa.send_password_reset_email(email_addr='incorrect_addr')
@raises(AAAException)
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch.object(Mailer, '_send')
def test_send_password_reset_email_incorrect_user(mocked):
aaa.send_password_reset_email(username='bogus_name')
@raises(AAAException)
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch.object(Mailer, '_send')
def test_send_password_reset_email_missing_email_addr(mocked):
aaa.send_password_reset_email(username='admin')
@raises(AuthException)
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch.object(Mailer, '_send')
def test_send_password_reset_email_incorrect_pair(mocked):
aaa.send_password_reset_email(username='admin', email_addr='incorrect_addr')
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch.object(Mailer, '_send')
def test_send_password_reset_email_by_email_addr(mocked):
aaa._store.users['admin']['email_addr'] = 'admin@localhost.local'
old_dir = os.getcwd()
os.chdir(testdir)
aaa.send_password_reset_email(email_addr='admin@localhost.local')
os.chdir(old_dir)
#TODO: add UT
@with_setup(setup_mockedadmin, teardown_dir)
@mock.patch.object(Mailer, '_send')
def test_send_password_reset_email_by_username(mocked):
old_dir = os.getcwd()
os.chdir(testdir)
aaa._store.users['admin']['email_addr'] = 'admin@localhost.local'
assert not mocked.called
aaa.send_password_reset_email(username='admin')
aaa.mailer.join()
os.chdir(old_dir)
assert mocked.called
assert mocked.call_args[0][0] == 'admin@localhost.local'
@raises(AuthException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_perform_password_reset_invalid():
aaa.reset_password('bogus', 'newpassword')
@raises(AuthException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_perform_password_reset_timed_out():
aaa.password_reset_timeout = 0
token = aaa._reset_code('admin', 'admin@localhost.local')
aaa.reset_password(token, 'newpassword')
@raises(AAAException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_perform_password_reset_nonexistent_user():
token = aaa._reset_code('admin_bogus', 'admin@localhost.local')
aaa.reset_password(token, 'newpassword')
# The following test should fail
# an user can change the password reset timestamp by b64-decoding the token,
# editing the field and b64-encoding it
@SkipTest
@raises(AuthException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_perform_password_reset_mangled_timestamp():
token = aaa._reset_code('admin', 'admin@localhost.local')
username, email_addr, tstamp, h = b64decode(token).split(':', 3)
tstamp = str(int(tstamp) + 100)
mangled_token = ':'.join((username, email_addr, tstamp, h))
mangled_token = b64encode(mangled_token)
aaa.reset_password(mangled_token, 'newpassword')
@raises(AuthException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_perform_password_reset_mangled_username():
token = aaa._reset_code('admin', 'admin@localhost.local')
username, email_addr, tstamp, h = b64decode(token).split(':', 3)
username += "mangled_username"
mangled_token = ':'.join((username, email_addr, tstamp, h))
mangled_token = b64encode(mangled_token)
aaa.reset_password(mangled_token, 'newpassword')
@raises(AuthException)
@with_setup(setup_mockedadmin, teardown_dir)
def test_perform_password_reset_mangled_email():
token = aaa._reset_code('admin', 'admin@localhost.local')
username, email_addr, tstamp, h = b64decode(token).split(':', 3)
email_addr += "mangled_email"
mangled_token = ':'.join((username, email_addr, tstamp, h))
mangled_token = b64encode(mangled_token)
aaa.reset_password(mangled_token, 'newpassword')
@with_setup(setup_mockedadmin, teardown_dir)
def test_perform_password_reset():
old_dir = os.getcwd()
os.chdir(testdir)
token = aaa._reset_code('admin', 'admin@localhost.local')
aaa.reset_password(token, 'newpassword')
os.chdir(old_dir)
bottle-cork-0.12.0/tests/testutils.py 0000644 0001750 0001750 00000026077 12512453671 017445 0 ustar fede fede 0000000 0000000 # -*- coding: utf-8 -*
# Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Unit testing - utility functions.
#
import bottle
import os
import shutil
import sys
import json
import mock
import tempfile
from base64 import b64encode, b64decode
from pytest import raises
import pytest
from datetime import datetime
from cork import Cork, AAAException, AuthException
SkipTest = pytest.mark.skipif(True, reason='skipped')
cookie_name = None
def pick_temp_directory():
"""Select a temporary directory for the test files.
Set the tmproot global variable.
"""
if os.environ.get('TRAVIS', False):
return tempfile.mkdtemp()
if sys.platform == 'linux2':
# In-memory filesystem allows faster testing.
return tempfile.mkdtemp(dir='/dev/shm')
return tempfile.mkdtemp()
def purge_temp_directory(test_dir):
"""Remove the test directory"""
assert test_dir
shutil.rmtree(test_dir)
REDIR = 302
class WebFunctional(object):
def __init__(self):
self._tmpdir = None
self._app = None
self._starting_dir = os.getcwd()
def populate_conf_directory(self):
"""Populate a directory with valid configuration files, to be run just once
The files are not modified by each test
"""
self._tmpdir = os.path.join(self._tmproot, "cork_functional_test_source")
# only do this once, as advertised
if os.path.exists(self._tmpdir):
return
os.mkdir(self._tmpdir)
os.mkdir(self._tmpdir + "/example_conf")
cork = Cork(os.path.join(self._tmpdir, "example_conf"), initialize=True)
cork._store.roles['admin'] = 100
cork._store.roles['editor'] = 60
cork._store.roles['user'] = 50
cork._store.save_roles()
tstamp = str(datetime.utcnow())
username = password = 'admin'
cork._store.users[username] = {
'role': 'admin',
'hash': cork._hash(username, password),
'email_addr': username + '@localhost.local',
'desc': username + ' test user',
'creation_date': tstamp
}
username = password = ''
cork._store.users[username] = {
'role': 'user',
'hash': cork._hash(username, password),
'email_addr': username + '@localhost.local',
'desc': username + ' test user',
'creation_date': tstamp
}
cork._store.save_users()
def populate_temp_dir(self):
"""populate the temporary test dir"""
assert self._tmpdir is not None
# copy the needed files
shutil.copytree(
os.path.join(self._starting_dir, 'tests/example_conf'),
os.path.join(self._tmpdir, 'example_conf')
)
shutil.copytree(
os.path.join(self._starting_dir, 'tests/views'),
os.path.join(self._tmpdir, 'views')
)
print("Test directory set up")
def remove_temp_dir(self):
p = os.path.join(self._tmproot, 'cork_functional_test_wd')
for f in glob.glob('%s*' % p):
#shutil.rmtree(f)
pass
def teardown(self):
print("Doing teardown")
try:
self._app.post('/logout')
except:
pass
# drop the cookie
self._app.reset()
assert 'beaker.session.id' not in self._app.cookies, "Unexpected cookie found"
# drop the cookie
self._app.reset()
#assert self._app.get('/admin').status != '200 OK'
os.chdir(self._starting_dir)
self._app.app.options['timeout'] = self._default_timeout
self._app = None
shutil.rmtree(self._tmpdir)
self._tmpdir = None
print("Teardown done")
def setup(self):
# create test dir and populate it using the example files
# save the directory where the unit testing has been run
if self._starting_dir is None:
self._starting_dir = os.getcwd()
# create json files to be used by Cork
self._tmpdir = pick_temp_directory()
assert self._tmpdir is not None
self.populate_temp_dir()
# change to the temporary test directory
# cork relies on this being the current directory
os.chdir(self._tmpdir)
self.create_app_instance()
self._app.reset()
print("Reset done")
self._default_timeout = self._app.app.options['timeout']
print("Setup completed")
# Utility functions
def assert_200(self, path, match):
"""Assert that a page returns 200"""
p = self._app.get(path)
assert p.status_int == 200, "Status: %d, Location: %s" % \
(p.status_int, p.location)
if match is not None:
assert match in p.body, "'%s' not found in body: '%s'" % (match, p.body)
return p
def assert_redirect(self, page, redir_page, post=None):
"""Assert that a page redirects to another one"""
# perform GET or POST
if post is None:
p = self._app.get(page, status=302)
else:
assert isinstance(post, dict)
p = self._app.post(page, post, status=302)
dest = p.location.split(':80/')[-1]
dest = "/%s" % dest
assert dest == redir_page, "%s redirects to %s instead of %s" % \
(page, dest, redir_page)
return p
# Tests
def test_functional_login(self):
assert self._app
self._app.get('/admin', status=302)
self._app.get('/my_role', status=302)
self.login_as_admin()
# fetch a page successfully
r = self._app.get('/admin')
assert r.status_int == 200, repr(r)
def test_login_existing_user_none_password(self):
p = self._app.post('/login', {'username': 'admin', 'password': None})
assert p.status_int == REDIR, "Redirect expected"
assert p.location == 'http://localhost:80/login', \
"Incorrect redirect to %s" % p.location
def test_login_nonexistent_user_none_password(self):
p = self._app.post('/login', {'username': 'IAmNotHere', 'password': None})
assert p.status_int == REDIR, "Redirect expected"
assert p.location == 'http://localhost:80/login', \
"Incorrect redirect to %s" % p.location
def test_login_existing_user_empty_password(self):
p = self._app.post('/login', {'username': 'admin', 'password': ''})
assert p.status_int == REDIR, "Redirect expected"
assert p.location == 'http://localhost:80/login', \
"Incorrect redirect to %s" % p.location
def test_login_nonexistent_user_empty_password(self):
p = self._app.post('/login', {'username': 'IAmNotHere', 'password': ''})
assert p.status_int == REDIR, "Redirect expected"
assert p.location == 'http://localhost:80/login', \
"Incorrect redirect to %s" % p.location
def test_login_existing_user_wrong_password(self):
p = self._app.post('/login', {'username': 'admin', 'password': 'BogusPassword'})
assert p.status_int == REDIR, "Redirect expected"
assert p.location == 'http://localhost:80/login', \
"Incorrect redirect to %s" % p.location
def test_functional_login_logout(self):
# Incorrect login
p = self._app.post('/login', {'username': 'admin', 'password': 'BogusPassword'})
assert p.status_int == REDIR
assert p.location == 'http://localhost:80/login', \
"Incorrect redirect to %s" % p.location
# log in and get a cookie
p = self._app.post('/login', {'username': 'admin', 'password': 'admin'})
assert p.status_int == REDIR
assert p.location == 'http://localhost:80/', \
"Incorrect redirect to %s" % p.location
self.assert_200('/my_role', 'admin')
# fetch a page successfully
assert self._app.get('/admin').status_int == 200, "Admin page should be served"
# log out
assert self._app.get('/logout').status_int == REDIR
# drop the cookie
self._app.reset()
assert self._app.cookies == {}, "The cookie should be gone"
# fetch the same page, unsuccessfully
assert self._app.get('/admin').status_int == REDIR
def test_functional_user_creation_login_deletion(self):
assert self._app.cookies == {}, "The cookie should be not set"
# Log in as Admin
p = self._app.post('/login', {'username': 'admin', 'password': 'admin'})
assert p.status_int == REDIR
assert p.location == 'http://localhost:80/', \
"Incorrect redirect to %s" % p.location
self.assert_200('/my_role', 'admin')
username = 'BrandNewUser'
# Delete the user
ret = self._app.post('/delete_user', {
'username': username,
})
# Create new user
password = '42IsTheAnswer'
ret = self._app.post('/create_user', {
'username': username,
'password': password,
'role': 'user'
})
retj = json.loads(ret.body)
assert 'ok' in retj and retj['ok'] == True, "Failed user creation: %s" % \
ret.body
# log out
assert self._app.get('/logout').status_int == REDIR
self._app.reset()
assert self._app.cookies == {}, "The cookie should be gone"
# Log in as user
p = self._app.post('/login', {'username': username, 'password': password})
assert p.status_int == REDIR and p.location == 'http://localhost:80/', \
"Failed user login"
# log out
assert self._app.get('/logout').status_int == REDIR
self._app.reset()
assert self._app.cookies == {}, "The cookie should be gone"
# Log in as user with empty password
p = self._app.post('/login', {'username': username, 'password': ''})
assert p.status_int == REDIR and p.location == 'http://localhost:80/login', \
"User login should fail"
assert self._app.cookies == {}, "The cookie should not be set"
# Log in as Admin, again
p = self._app.post('/login', {'username': 'admin', 'password': 'admin'})
assert p.status_int == REDIR
assert p.location == 'http://localhost:80/', \
"Incorrect redirect to %s" % p.location
self.assert_200('/my_role', 'admin')
# Delete the user
ret = self._app.post('/delete_user', {
'username': username,
})
retj = json.loads(ret.body)
assert 'ok' in retj and retj['ok'] == True, "Failed user deletion: %s" % \
ret.body
#def test_functional_user_registration(self):
# assert self._app.cookies == {}, "The cookie should be not set"
#
# # Register new user
# username = 'BrandNewUser'
# password = '42IsTheAnswer'
# ret = self._app.post('/register', {
# 'username': username,
# 'password': password,
# 'email_address': 'test@localhost.local'
# })
def test_functional_expiration(self):
raise NotImplementedError
bottle-cork-0.12.0/tests/test_foo.py 0000644 0001750 0001750 00000000546 12506334640 017215 0 ustar fede fede 0000000 0000000
import pytest
@pytest.fixture(params=["merlinux.eu", "mail.python.org"])
def backend(request):
print 'called with ', request.param
return request.param
@pytest.fixture
def fixa(backend):
return backend
@pytest.fixture
def fixb(backend):
return backend + 'b'
def test_1(fixa):
assert not fixa
def test_2(fixb):
assert not fixb
bottle-cork-0.12.0/tests/test_webtest_flask.py 0000644 0001750 0001750 00000010036 12512453671 021265 0 ustar fede fede 0000000 0000000 # Cork - Authentication module for the Flask web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Functional test using Json backend
#
# Requires WebTest http://webtest.pythonpaste.org/
#
# Run as: nosetests functional_test.py
#
from nose import SkipTest
from webtest import TestApp
from datetime import timedelta
import shutil
import os
import testutils
from cork import FlaskCork
REDIR = 302
class Test(testutils.WebFunctional):
def create_app_instance(self):
"""create TestApp instance"""
assert self._app is None
import simple_webapp_flask
self._bottle_app = simple_webapp_flask.app
self._app = TestApp(self._bottle_app)
#simple_webapp_flask.flask.session.secret_key = 'bogus'
simple_webapp_flask.SECRET_KEY = 'bogus'
print("Test App created")
def teardown(self):
print("Doing teardown")
try:
self._app.post('/logout')
except:
pass
# drop the cookie
self._app.reset()
assert 'beaker.session.id' not in self._app.cookies, "Unexpected cookie found"
# drop the cookie
self._app.reset()
#assert self._app.get('/admin').status != '200 OK'
os.chdir(self._starting_dir)
self._app.app.options['timeout'] = self._default_timeout
self._app = None
shutil.rmtree(self._tmpdir)
self._tmpdir = None
print("Teardown done")
def setup(self):
# create test dir and populate it using the example files
# save the directory where the unit testing has been run
if self._starting_dir is None:
self._starting_dir = os.getcwd()
# create json files to be used by Cork
self._tmpdir = testutils.pick_temp_directory()
assert self._tmpdir is not None
self.populate_temp_dir()
# change to the temporary test directory
# cork relies on this being the current directory
os.chdir(self._tmpdir)
self.create_app_instance()
self._app.reset()
print("Reset done")
#self._default_timeout = self._app.app.options['timeout']
self._default_timeout = 30
#FIXME: reset
print("Setup completed")
def login_as_admin(self):
"""perform log in"""
assert self._app is not None
assert 'session' not in self._app.cookies, "Unexpected cookie found"
self.assert_200('/login', 'Please insert your credentials')
assert 'session' not in self._app.cookies, "Unexpected cookie found"
self.assert_redirect('/admin', '/sorry_page')
self.assert_200('/user_is_anonymous', 'True')
assert 'session' not in self._app.cookies, "Unexpected cookie found"
post = {'username': 'admin', 'password': 'admin'}
self.assert_redirect('/login', '/', post=post)
assert 'session' in self._app.cookies, "Cookie not found"
import bottle
session = bottle.request.environ.get('beaker.session')
print("Session from func. test", repr(session))
self.assert_200('/login', 'Please insert your credentials')
p = self._app.get('/admin')
assert 'Welcome' in p.body, repr(p)
p = self._app.get('/my_role', status=200)
assert p.status_int == 200
assert p.body == 'admin', "Sta"
print("Login performed")
def test_functional_expiration(self):
self.login_as_admin()
r = self._app.get('/admin')
assert r.status_int == 200, repr(r)
# change the cookie expiration in order to expire it
saved = self._bottle_app.permanent_session_lifetime
try:
self._bottle_app.permanent_session_lifetime = timedelta(seconds=-1)
# change the cookie expiration in order to expire it
self._app.app.options['timeout'] = 0
assert self._app.get('/admin').status_int == REDIR, "The cookie should have expired"
finally:
self._bottle_app.permanent_session_lifetime = saved
bottle-cork-0.12.0/tests/conftest.py 0000644 0001750 0001750 00000005546 12517131104 017215 0 ustar fede fede 0000000 0000000
import bottle
import os
import pytest
from cork import Cork
@pytest.fixture
def chdir_to_tmpdir(tmpdir):
# Chdir into the current tmpdir (used by Cork to find email templates)
tmpdir.chdir()
assert tmpdir.strpath == os.getcwd()
@pytest.fixture
def templates_dir(tmpdir, chdir_to_tmpdir):
# Setup email templates
tmpdir.mkdir('views')
tmpdir.join('views/registration_email.tpl').write("""Username:{{username}} Email:{{email_addr}} Code:{{registration_code}}""")
tmpdir.join('views/password_reset_email.tpl').write("""Username:{{username}} Email:{{email_addr}} Code:{{reset_code}}""")
assert tmpdir.join('views/password_reset_email.tpl').exists()
tmpdir.mkdir('examples')
tmpdir.mkdir('examples/views')
tmpdir.join('examples/views/password_reset_email.tpl').write(
"""Hello {{username}},
You are receiving this email because you requested a password reset on Cork Demo Webapp.
If you wish to complete the password reset please click on:
Confirm
""")
return tmpdir
@pytest.fixture
def mytmpdir(tmpdir):
"""Setup tmp directory with test files
"""
tmpdir.mkdir('views')
tmpdir.join('users.json').write("""{"admin": {"email_addr": null, "desc": null, "role": "admin", "hash": "69f75f38ac3bfd6ac813794f3d8c47acc867adb10b806e8979316ddbf6113999b6052efe4ba95c0fa9f6a568bddf60e8e5572d9254dbf3d533085e9153265623", "creation_date": "2012-04-09 14:22:27.075596"}}""")
tmpdir.join('roles.json').write("""{"special": 200, "admin": 100, "user": 50}""")
tmpdir.join('register.json').write("""{}""")
tmpdir.join('registration_email.tpl').write("""Username:{{username}} Email:{{email_addr}} Code:{{registration_code}}""")
tmpdir.join('password_reset_email.tpl').write("""Username:{{username}} Email:{{email_addr}} Code:{{reset_code}}""")
return tmpdir
# used by test_scrypt.py
@pytest.fixture
def aaa(mytmpdir):
aaa = Cork(mytmpdir, smtp_server='localhost', email_sender='test@localhost')
return aaa
# mb = self.setup_test_db()
# self.aaa = MockedUnauthenticatedCork(backend=mb,
# smtp_server='localhost', email_sender='test@localhost')
# cookie_name = None
# if hasattr(self, 'purge_test_db'):
# self.purge_test_db()
#
# del(self.aaa)
# cookie_name = None
def assert_is_redirect(e, path):
"""Check if an HTTPResponse is a redirect.
:param path: relative path without leading slash.
:type path: str
"""
assert isinstance(e, bottle.HTTPResponse), "Incorrect exception type passed to assert_is_redirect"
assert e.status_code == 302, "HTTPResponse status should be 302 but is '%s'" % e.status
redir_location = e.headers['Location'].rsplit('/', 1)[1]
assert redir_location == path, "Redirected to %s instead of %s" % (redir_location, path)
bottle-cork-0.12.0/tests/test_pbkdf2.py 0000644 0001750 0001750 00000007532 12515203757 017610 0 ustar fede fede 0000000 0000000
import hashlib
import binascii
import json
from cork import Cork
import base64
from base64 import b64encode
import pytest
from beaker import crypto
def test_hash_basic_sha1():
dk = hashlib.pbkdf2_hmac('sha1', b'password', b'salt', 100000)
assert binascii.hexlify(dk) == b'f5496bb1328184f5228eff393ab4be9ae8fe69e7'
def test_hash_basic_sha1_b():
dk = hashlib.pbkdf2_hmac('sha1', b'password', b'salt', 10)
assert binascii.hexlify(dk) == b'ae3fe5f5707e07f3e7c117fb885cd052a6fcd77a'
def test_hash_basic_sha1_c():
cleartext = b'x' * 32
salt = b'y' * 32
dk = hashlib.pbkdf2_hmac('sha1', b'password', salt, 10)
h_old = crypto.generateCryptoKeys(cleartext, salt, 10)
assert binascii.hexlify(dk) == b'b10d37a81afb93afa5db8b522dc5cf8d8164f469'
assert binascii.hexlify(h_old) == b'b10d37a81afb93afa5db8b522dc5cf8d8164f469'
def test_hash():
cleartext = b'x' * 32
salt = b'y' * 32
print(repr(cleartext))
h = hashlib.pbkdf2_hmac('sha1', cleartext, salt, 10)
h = binascii.hexlify(h)
assert h == b'916c7f92b887a3eace655172bc3ed21fcecd82ef'
def test_hash_32():
cleartext = b'x' * 32
salt = b'y' * 32
h = hashlib.pbkdf2_hmac('sha1', cleartext, salt, 10, dklen=32)
h = binascii.hexlify(h)
assert h == b'916c7f92b887a3eace655172bc3ed21fcecd82ef4b28c12826cd5cada361013f'
def test_compare_pbkdf2():
cleartext = b'x' * 32
salt = b'y' * 32
old_h = crypto.generateCryptoKeys(cleartext, salt, 10)
h = hashlib.pbkdf2_hmac('sha1', cleartext, salt, 10, dklen=32)
assert len(h) == len(old_h)
assert h == old_h
# Test PBKDF2-based password hashing
_hash = Cork._hash_pbkdf2
def _oldhash(username, pwd, salt=None):
username = username.encode('utf-8')
pwd = pwd.encode('utf-8')
cleartext = username + b'\0' + pwd
h = crypto.generateCryptoKeys(cleartext, salt, 10)
hashed = b'p' + salt + h
return b64encode(hashed)
def test_password_hashing_PBKDF2():
shash = _hash(u'user_foo', u'bogus_pwd')
shash_old = _oldhash(u'user_foo', u'bogus_pwd')
assert isinstance(shash, bytes)
assert len(shash) == 88, "hash length should be 88 and is %d" % len(shash)
assert shash.endswith(b'='), "hash should end with '='"
assert shash == shash_old
def test_password_hashing_PBKDF2_known_hash():
salt = b's' * 32
shash = _hash(u'user_foo', u'bogus_pwd', salt=salt)
shash_old = _oldhash(u'user_foo', u'bogus_pwd', salt=salt)
assert shash == 'cHNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzax44AxQgK6uD9q1YWxLos1ispCe1Z7T7pOFK1PwdWEs='
assert shash == shash_old
def test_password_hashing_PBKDF2_known_hash_2():
salt = b'\0' * 32
shash = _hash(u'user_foo', u'bogus_pwd', salt=salt)
shash_old = _oldhash(u'user_foo', u'bogus_pwd', salt=salt)
assert shash == 'cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8Uh4pyEOHoRz4j0lDzAmqb7Dvmo8GpeXwiKTDsuYFw='
assert shash == shash_old
def test_password_hashing_PBKDF2_known_hash_3():
salt = b'x' * 32
shash = _hash(u'user_foo', u'bogus_pwd', salt=salt)
shash_old = _oldhash(u'user_foo', u'bogus_pwd', salt=salt)
assert shash == b'cHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4MEaIU5Op97lmvwX5NpVSTBP8jg8OlrN7c2K8K8tnNks='
assert shash == shash_old
def test_password_hashing_PBKDF2_incorrect_hash_len():
salt = b'x' * 31 # Incorrect length
with pytest.raises(AssertionError):
shash = _hash(u'user_foo', u'bogus_pwd', salt=salt)
def test_password_hashing_PBKDF2_incorrect_hash_value():
shash = _hash(u'user_foo', u'bogus_pwd')
assert len(shash) == 88, "hash length should be 88 and is %d" % len(shash)
assert shash.endswith(b'='), "hash should end with '='"
def test_password_hashing_PBKDF2_collision():
salt = b'S' * 32
hash1 = _hash(u'user_foo', u'bogus_pwd', salt=salt)
hash2 = _hash(u'user_foobogus', u'_pwd', salt=salt)
assert hash1 != hash2, "Hash collision"
bottle-cork-0.12.0/tests/test_sqla.py 0000644 0001750 0001750 00000007713 12515736435 017405 0 ustar fede fede 0000000 0000000
from sqlalchemy import create_engine, delete, select, \
Column, ForeignKey, Integer, MetaData, String, Table, Unicode
from cork.backends import SqlAlchemyBackend
import pytest
import os
from cork import Cork
class MockedSession(object):
"""Mock Beaker session
"""
def __init__(self, username=None):
self.__username = username
self.__saved = False
def get(self, k, default):
assert k in ('username')
if self.__username is None:
return default
return self.__username
def __getitem__(self, k):
assert k in ('username')
if self.__username is None:
raise KeyError()
return self.__username
def __setitem__(self, k, v):
assert k in ('username')
self.__username = v
self.__saved = False
def delete(self):
"""Used during logout to delete the current session"""
self.__username = None
def save(self):
self.__saved = True
#TODO: implement tests around MockedSession __saved
class MockedSessionCork(Cork):
"""Mocked Cork instance where the session is replaced with
MockedSession
"""
@property
def _beaker_session(self):
return self._mocked_beaker_session
def setup_postgresql_db(request):
if os.environ.get('TRAVIS', False):
# Using Travis-CI - https://travis-ci.org/
db_name = 'myapp_test'
else:
db_name = 'cork_functional_test'
uri = "postgresql+psycopg2://postgres:@/%s" % db_name
mb = SqlAlchemyBackend(uri, initialize=True)
# Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
@pytest.fixture
def backend(tmpdir, request):
return setup_postgresql_db(request)
raise Exception()
@pytest.fixture
def aaa_admin(templates_dir, backend):
# Session with an admin user
aaa = MockedSessionCork(
templates_dir,
backend=backend,
email_sender='test@localhost',
smtp_server='localhost',
)
aaa._mocked_beaker_session = MockedSession(username='admin')
return aaa
def test_write_user_hash_bytes(aaa_admin):
username = 'huh'
h = b'1234'
tstamp = "just a string"
assert isinstance(h, type(b''))
aaa_admin._store.users[username] = {
'role': "user",
'hash': h,
'email_addr': "bar",
'desc': "foo",
'creation_date': tstamp,
'last_login': tstamp
}
#eng = aaa_admin._store._engine
#print(eng.execute("SELECT * FROM users").fetchall()[1])
fetched_h = aaa_admin._store.users[username]['hash']
assert isinstance(fetched_h, type(b''))
assert fetched_h == b'1234'
def test_write_user_hash_unicode(aaa_admin):
username = 'huh'
h = u'1234'
tstamp = "just a string"
aaa_admin._store.users[username] = {
'role': "user",
'hash': h,
'email_addr': "bar",
'desc': "foo",
'creation_date': tstamp,
'last_login': tstamp
}
fetched_h = aaa_admin._store.users[username]['hash']
eng = aaa_admin._store._engine
print(eng.execute("SELECT * FROM users").fetchall()[1])
assert fetched_h == u'1234'
bottle-cork-0.12.0/tests/test_set_pass.py 0000644 0001750 0001750 00000025165 12520700252 020247 0 ustar fede fede 0000000 0000000 # -*- coding: utf-8 -*
# Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Functional testing - test the Cork module against diffent database backends
from base64 import b64encode, b64decode
from pytest import raises
import bottle
import mock
import os
import pytest
import time
from cork import Cork, AAAException, AuthException
from cork.backends import JsonBackend
from cork.backends import MongoDBBackend
from cork.backends import SQLiteBackend
from cork.backends import SqlAlchemyBackend
from conftest import assert_is_redirect
try:
import pymongo
pymongo_available = True
except ImportError:
pymongo_available = False
try:
import MySQLdb
MySQLdb_available = True
except ImportError:
MySQLdb_available = False
### Mocked classes
class MockedSession(object):
"""Mock Beaker session
"""
def __init__(self, username=None):
self.__username = username
self.__saved = False
def get(self, k, default):
assert k in ('username')
if self.__username is None:
return default
return self.__username
def __getitem__(self, k):
assert k in ('username')
if self.__username is None:
raise KeyError()
return self.__username
def __setitem__(self, k, v):
assert k in ('username')
self.__username = v
self.__saved = False
def delete(self):
"""Used during logout to delete the current session"""
self.__username = None
def save(self):
self.__saved = True
#TODO: implement tests around MockedSession __saved
class MockedSessionCork(Cork):
"""Mocked Cork instance where the session is replaced with
MockedSession
"""
@property
def _beaker_session(self):
return self._mocked_beaker_session
### Fixtures and helpers
## Backends
def setup_sqlite_db(request):
# in-memory SQLite DB using the SQLiteBackend backend module.
b = SQLiteBackend(':memory:', initialize=True)
b.connection.executescript("""
INSERT INTO users (username, email_addr, desc, role, hash, creation_date) VALUES
(
'admin',
'admin@localhost.local',
'admin test user',
'admin',
'cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=',
'2012-10-28 20:50:26.286723'
);
INSERT INTO roles (role, level) VALUES ('special', 200);
INSERT INTO roles (role, level) VALUES ('admin', 100);
INSERT INTO roles (role, level) VALUES ('editor', 60);
INSERT INTO roles (role, level) VALUES ('user', 50);
""")
return b
def setup_json_db(request, tmpdir):
# Setup test directory with valid JSON files and return JsonBackend instance
tmpdir.join('users.json').write("""{"admin": {"email_addr": "admin@localhost.local", "desc": null, "role": "admin", "hash": "69f75f38ac3bfd6ac813794f3d8c47acc867adb10b806e8979316ddbf6113999b6052efe4ba95c0fa9f6a568bddf60e8e5572d9254dbf3d533085e9153265623", "creation_date": "2012-04-09 14:22:27.075596", "last_login": "2012-10-28 20:50:26.286723"}}""")
tmpdir.join('roles.json').write("""{"special": 200, "admin": 100, "user": 50, "editor": 60}""")
tmpdir.join('register.json').write("""{}""")
return JsonBackend(tmpdir)
def setup_sqlalchemy_with_sqlite_in_memory_db(request):
# Setup an SqlAlchemyBackend backend using an in-memory SQLite DB
mb = SqlAlchemyBackend('sqlite:///:memory:', initialize=True)
## Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
#def purge_test_db(self):
# # Purge DB
# mb = connect_to_test_db()
# mb._drop_all_tables()
def setup_mongo_db(request):
# FIXME no last_login?
t0 = time.time()
def timer(s, max_time=None):
delta = time.time() - t0
print("%s %f" % (s, delta))
if max_time is not None:
assert delta < max_time
mb = MongoDBBackend(db_name='cork-functional-test', initialize=True)
timer('connect + init')
# Purge DB
mb.users._coll.drop()
mb.roles._coll.drop()
mb.pending_registrations._coll.drop()
timer('purge')
# Create admin
mb.users._coll.insert({
"login": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723"
})
timer('create')
# Create users
mb.roles._coll.insert({'role': 'special', 'val': 200})
mb.roles._coll.insert({'role': 'admin', 'val': 100})
mb.roles._coll.insert({'role': 'editor', 'val': 60})
mb.roles._coll.insert({'role': 'user', 'val': 50})
timer('create users')
def fin():
mb.users._coll.drop()
mb.roles._coll.drop()
request.addfinalizer(fin)
timer('mongo setup', 8)
return mb
def setup_mysql_db(request):
if os.environ.get('TRAVIS', False):
# Using Travis-CI - https://travis-ci.org/
password = ''
db_name = 'myapp_test'
else:
password = ''
db_name = 'cork_functional_test'
uri = "mysql://root:%s@localhost/%s" % (password, db_name)
mb = SqlAlchemyBackend(uri, initialize=True)
## Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
return # TODO: fix
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
def setup_postgresql_db(request):
if os.environ.get('TRAVIS', False):
# Using Travis-CI - https://travis-ci.org/
db_name = 'myapp_test'
else:
db_name = 'cork_functional_test'
uri = "postgresql+psycopg2://postgres:@/%s" % db_name
mb = SqlAlchemyBackend(uri, initialize=True)
# Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
## General fixtures
@pytest.fixture(params=[
'json',
'mongodb',
'mysql',
'postgresql',
'sqlalchemy',
'sqlite',
])
def backend(tmpdir, request):
# Create backend instances
backend_type = request.param
if backend_type == 'json':
return setup_json_db(request, tmpdir)
if backend_type == 'sqlite':
return setup_sqlite_db(request)
if backend_type == 'sqlalchemy':
return setup_sqlalchemy_with_sqlite_in_memory_db(request)
if backend_type == 'mongodb':
if not pymongo_available:
pytest.skip()
return setup_mongo_db(request)
if backend_type == 'mysql':
if not MySQLdb_available:
pytest.skip()
return setup_mysql_db(request)
if backend_type == 'postgresql':
return setup_postgresql_db(request)
raise Exception()
@pytest.fixture
def aaa_unauth(templates_dir, backend):
# Session without any authenticated user
aaa = MockedSessionCork(
templates_dir,
backend=backend,
smtp_server='localhost',
email_sender='test@localhost',
)
aaa._mocked_beaker_session = MockedSession()
return aaa
@pytest.fixture
def aaa_admin(templates_dir, backend):
# Session with an admin user
aaa = MockedSessionCork(
templates_dir,
backend=backend,
email_sender='test@localhost',
smtp_server='localhost',
)
aaa._mocked_beaker_session = MockedSession(username='admin')
return aaa
### Tests
def test_verify_password(aaa_admin):
salted_hash = b'cFc8Yx4vLF7qNr2mqeH5PEoFX8T4+LJhEcJTxWhTVYLbIa6vI6DiGmYeP6PZ5iCyucnCJKEd5QOBGyDlmWN82b0='
v = aaa_admin._verify_password(u'admin', u'newpwd', salted_hash)
assert v is True
def test_set_password_directly(aaa_admin):
#assert aaa_admin.login(u'admin', u'newpwd') == False
user = aaa_admin.user(u'admin')
assert user
user.update(pwd='newpwd')
assert aaa_admin.login(u'admin', u'newpwd')
def test_perform_password_reset(aaa_admin):
token = aaa_admin._reset_code(u'admin', u'admin@localhost.local')
aaa_admin.reset_password(token, u'newpassword')
login = aaa_admin.login(u'admin', u'newpassword')
assert login == True, "Login must succeed"
bottle-cork-0.12.0/tests/test_mysql.py 0000644 0001750 0001750 00000027522 12516510331 017574 0 ustar fede fede 0000000 0000000 # -*- coding: utf-8 -*
# Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Functional testing - test the Cork module against diffent database backends
from base64 import b64encode, b64decode
from pytest import raises
import bottle
import mock
import os
import pytest
import time
from cork import Cork, AAAException, AuthException
from cork.backends import JsonBackend
from cork.backends import MongoDBBackend
from cork.backends import SQLiteBackend
from cork.backends import SqlAlchemyBackend
from conftest import assert_is_redirect
try:
import pymongo
pymongo_available = True
except ImportError:
pymongo_available = False
try:
import MySQLdb
MySQLdb_available = True
except ImportError:
MySQLdb_available = False
### Mocked classes
class MockedSession(object):
"""Mock Beaker session
"""
def __init__(self, username=None):
self.__username = username
self.__saved = False
def get(self, k, default):
assert k in ('username')
if self.__username is None:
return default
return self.__username
def __getitem__(self, k):
assert k in ('username')
if self.__username is None:
raise KeyError()
return self.__username
def __setitem__(self, k, v):
assert k in ('username')
self.__username = v
self.__saved = False
def delete(self):
"""Used during logout to delete the current session"""
self.__username = None
def save(self):
self.__saved = True
#TODO: implement tests around MockedSession __saved
class MockedSessionCork(Cork):
"""Mocked Cork instance where the session is replaced with
MockedSession
"""
@property
def _beaker_session(self):
return self._mocked_beaker_session
### Fixtures and helpers
## Backends
def setup_sqlite_db(request):
# in-memory SQLite DB using the SQLiteBackend backend module.
b = SQLiteBackend(':memory:', initialize=True)
b.connection.executescript("""
INSERT INTO users (username, email_addr, desc, role, hash, creation_date) VALUES
(
'admin',
'admin@localhost.local',
'admin test user',
'admin',
'cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=',
'2012-10-28 20:50:26.286723'
);
INSERT INTO roles (role, level) VALUES ('special', 200);
INSERT INTO roles (role, level) VALUES ('admin', 100);
INSERT INTO roles (role, level) VALUES ('editor', 60);
INSERT INTO roles (role, level) VALUES ('user', 50);
""")
return b
def setup_json_db(request, tmpdir):
# Setup test directory with valid JSON files and return JsonBackend instance
tmpdir.join('users.json').write("""{"admin": {"email_addr": "admin@localhost.local", "desc": null, "role": "admin", "hash": "69f75f38ac3bfd6ac813794f3d8c47acc867adb10b806e8979316ddbf6113999b6052efe4ba95c0fa9f6a568bddf60e8e5572d9254dbf3d533085e9153265623", "creation_date": "2012-04-09 14:22:27.075596", "last_login": "2012-10-28 20:50:26.286723"}}""")
tmpdir.join('roles.json').write("""{"special": 200, "admin": 100, "user": 50, "editor": 60}""")
tmpdir.join('register.json').write("""{}""")
return JsonBackend(tmpdir)
def setup_sqlalchemy_with_sqlite_in_memory_db(request):
# Setup an SqlAlchemyBackend backend using an in-memory SQLite DB
mb = SqlAlchemyBackend('sqlite:///:memory:', initialize=True)
## Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
#def purge_test_db(self):
# # Purge DB
# mb = connect_to_test_db()
# mb._drop_all_tables()
def setup_mongo_db(request):
# FIXME no last_login?
t0 = time.time()
def timer(s, max_time=None):
delta = time.time() - t0
print("%s %f" % (s, delta))
if max_time is not None:
assert delta < max_time
mb = MongoDBBackend(db_name='cork-functional-test', initialize=True)
timer('connect + init')
# Purge DB
mb.users._coll.drop()
mb.roles._coll.drop()
mb.pending_registrations._coll.drop()
timer('purge')
# Create admin
mb.users._coll.insert({
"login": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723"
})
timer('create')
# Create users
mb.roles._coll.insert({'role': 'special', 'val': 200})
mb.roles._coll.insert({'role': 'admin', 'val': 100})
mb.roles._coll.insert({'role': 'editor', 'val': 60})
mb.roles._coll.insert({'role': 'user', 'val': 50})
timer('create users')
def fin():
mb.users._coll.drop()
mb.roles._coll.drop()
request.addfinalizer(fin)
timer('mongo setup', 8)
return mb
def setup_mysql_db(request):
if os.environ.get('TRAVIS', False):
# Using Travis-CI - https://travis-ci.org/
password = ''
db_name = 'myapp_test'
else:
password = ''
db_name = 'cork_functional_test'
uri = "mysql://root:%s@localhost/%s" % (password, db_name)
mb = SqlAlchemyBackend(uri, initialize=True)
## Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
return # TODO: fix
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
def setup_postgresql_db(request):
if os.environ.get('TRAVIS', False):
# Using Travis-CI - https://travis-ci.org/
db_name = 'myapp_test'
else:
db_name = 'cork_functional_test'
uri = "postgresql+psycopg2://postgres:@/%s" % db_name
mb = SqlAlchemyBackend(uri, initialize=True)
# Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
## General fixtures
@pytest.fixture(params=[
'json',
'mongodb',
'mysql',
'postgresql',
'sqlalchemy',
'sqlite',
])
def backend(tmpdir, request):
# Create backend instances
backend_type = request.param
if backend_type == 'json':
return setup_json_db(request, tmpdir)
if backend_type == 'sqlite':
return setup_sqlite_db(request)
if backend_type == 'sqlalchemy':
return setup_sqlalchemy_with_sqlite_in_memory_db(request)
if backend_type == 'mongodb':
pytest.skip()
if not pymongo_available:
pytest.skip()
return setup_mongo_db(request)
if backend_type == 'mysql':
if not MySQLdb_available:
pytest.skip()
return setup_mysql_db(request)
if backend_type == 'postgresql':
return setup_postgresql_db(request)
raise Exception()
@pytest.fixture
def aaa_unauth(templates_dir, backend):
# Session without any authenticated user
aaa = MockedSessionCork(
templates_dir,
backend=backend,
smtp_server='localhost',
email_sender='test@localhost',
)
aaa._mocked_beaker_session = MockedSession()
return aaa
@pytest.fixture
def aaa_admin(templates_dir, backend):
# Session with an admin user
aaa = MockedSessionCork(
templates_dir,
backend=backend,
email_sender='test@localhost',
smtp_server='localhost',
)
aaa._mocked_beaker_session = MockedSession(username='admin')
return aaa
def test_create_and_validate_user(aaa_admin):
assert len(aaa_admin._store.users) == 1, "Only the admin user should be present"
aaa_admin.create_user(u'phil', 'user', u'hunter123')
assert len(aaa_admin._store.users) == 2, "Two users should be present"
assert 'phil' in aaa_admin._store.users
assert aaa_admin._store.users['phil']['role'] == 'user'
assert u'phil' in aaa_admin._store.users
if hasattr(aaa_admin._store, '_engine'):
eng = aaa_admin._store._engine
for row in eng.execute("SELECT * FROM users").fetchall():
name = row[0]
# py2: unicode with mysql, str with others
print(repr(row))
login = aaa_admin.login('phil', 'hunter123')
assert login == True, "Login must succeed"
assert aaa_admin._beaker_session['username'] == 'phil'
def test_create_and_validate_user_unicode(aaa_admin):
assert len(aaa_admin._store.users) == 1, "Only the admin user should be present"
if False and hasattr(aaa_admin._store, '_engine'):
from sqlalchemy import create_engine, delete, select
eng = aaa_admin._store._engine
query = select(['users'], aaa_admin._store.users._key_col == 'foo')
eng.execute(query).fetchall()
if u"hiॐ" in aaa_admin._store.users:
assert False
aaa_admin.create_user(u'phil_åöॐ', 'user', u'neko_猫')
if hasattr(aaa_admin._store, '_engine'):
eng = aaa_admin._store._engine
for row in eng.execute("SELECT * FROM users").fetchall():
name = row[0]
# py2: unicode with mysql, str with others
assert len(aaa_admin._store.users) == 2, "Two users should be present"
assert u'phil_åöॐ' in aaa_admin._store.users
assert aaa_admin._store.users[u'phil_åöॐ']['role'] == 'user'
login = aaa_admin.login(u'phil_åöॐ', u'neko_猫')
assert login == True, "Login must succeed"
assert aaa_admin._beaker_session['username'] == u'phil_åöॐ'
bottle-cork-0.12.0/tests/test_functional.py 0000644 0001750 0001750 00000073106 12520652355 020600 0 ustar fede fede 0000000 0000000 # -*- coding: utf-8 -*
# Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Functional testing - test the Cork module against diffent database backends
from base64 import b64encode, b64decode
from pytest import raises
import bottle
import mock
import os
import pytest
import time
from cork import Cork, AAAException, AuthException
from cork.backends import JsonBackend
from cork.backends import MongoDBBackend
from cork.backends import SQLiteBackend
from cork.backends import SqlAlchemyBackend
from conftest import assert_is_redirect
try:
import pymongo
pymongo_available = True
except ImportError:
pymongo_available = False
try:
import MySQLdb
MySQLdb_available = True
except ImportError:
MySQLdb_available = False
### Mocked classes
class MockedSession(object):
"""Mock Beaker session
"""
def __init__(self, username=None):
self.__username = username
self.__saved = False
def get(self, k, default):
assert k in ('username')
if self.__username is None:
return default
return self.__username
def __getitem__(self, k):
assert k in ('username')
if self.__username is None:
raise KeyError()
return self.__username
def __setitem__(self, k, v):
assert k in ('username')
self.__username = v
self.__saved = False
def delete(self):
"""Used during logout to delete the current session"""
self.__username = None
def save(self):
self.__saved = True
#TODO: implement tests around MockedSession __saved
class MockedSessionCork(Cork):
"""Mocked Cork instance where the session is replaced with
MockedSession
"""
@property
def _beaker_session(self):
return self._mocked_beaker_session
### Fixtures and helpers
## Backends
def setup_sqlite_db(request):
# in-memory SQLite DB using the SQLiteBackend backend module.
b = SQLiteBackend(':memory:', initialize=True)
b.connection.executescript("""
INSERT INTO users (username, email_addr, desc, role, hash, creation_date) VALUES
(
'admin',
'admin@localhost.local',
'admin test user',
'admin',
'cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=',
'2012-10-28 20:50:26.286723'
);
INSERT INTO roles (role, level) VALUES ('special', 200);
INSERT INTO roles (role, level) VALUES ('admin', 100);
INSERT INTO roles (role, level) VALUES ('editor', 60);
INSERT INTO roles (role, level) VALUES ('user', 50);
""")
return b
def setup_json_db(request, tmpdir):
# Setup test directory with valid JSON files and return JsonBackend instance
tmpdir.join('users.json').write("""{"admin": {"email_addr": "admin@localhost.local", "desc": null, "role": "admin", "hash": "69f75f38ac3bfd6ac813794f3d8c47acc867adb10b806e8979316ddbf6113999b6052efe4ba95c0fa9f6a568bddf60e8e5572d9254dbf3d533085e9153265623", "creation_date": "2012-04-09 14:22:27.075596", "last_login": "2012-10-28 20:50:26.286723"}}""")
tmpdir.join('roles.json').write("""{"special": 200, "admin": 100, "user": 50, "editor": 60}""")
tmpdir.join('register.json').write("""{}""")
return JsonBackend(tmpdir)
def setup_sqlalchemy_with_sqlite_in_memory_db(request):
# Setup an SqlAlchemyBackend backend using an in-memory SQLite DB
mb = SqlAlchemyBackend('sqlite:///:memory:', initialize=True)
## Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
#def purge_test_db(self):
# # Purge DB
# mb = connect_to_test_db()
# mb._drop_all_tables()
def setup_mongo_db(request):
# FIXME no last_login?
t0 = time.time()
def timer(s, max_time=None):
delta = time.time() - t0
print("%s %f" % (s, delta))
if max_time is not None:
assert delta < max_time
mb = MongoDBBackend(db_name='cork-functional-test', initialize=True)
timer('connect + init')
# Purge DB
mb.users._coll.drop()
mb.roles._coll.drop()
mb.pending_registrations._coll.drop()
timer('purge')
# Create admin
mb.users._coll.insert({
"login": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723"
})
timer('create')
# Create users
mb.roles._coll.insert({'role': 'special', 'val': 200})
mb.roles._coll.insert({'role': 'admin', 'val': 100})
mb.roles._coll.insert({'role': 'editor', 'val': 60})
mb.roles._coll.insert({'role': 'user', 'val': 50})
timer('create users')
def fin():
mb.users._coll.drop()
mb.roles._coll.drop()
request.addfinalizer(fin)
timer('mongo setup', 8)
return mb
def setup_mysql_db(request):
if os.environ.get('TRAVIS', False):
# Using Travis-CI - https://travis-ci.org/
password = ''
db_name = 'myapp_test'
else:
password = ''
db_name = 'cork_functional_test'
uri = "mysql://root:%s@localhost/%s" % (password, db_name)
mb = SqlAlchemyBackend(uri, initialize=True)
## Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
return # TODO: fix
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
def setup_postgresql_db(request):
if os.environ.get('TRAVIS', False):
# Using Travis-CI - https://travis-ci.org/
db_name = 'myapp_test'
else:
db_name = 'cork_functional_test'
uri = "postgresql+psycopg2://postgres:@/%s" % db_name
mb = SqlAlchemyBackend(uri, initialize=True)
# Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
## General fixtures
@pytest.fixture(params=[
'json',
'mongodb',
'mysql',
'postgresql',
'sqlalchemy',
'sqlite',
])
def backend(tmpdir, request):
# Create backend instances
backend_type = request.param
if backend_type == 'json':
return setup_json_db(request, tmpdir)
if backend_type == 'sqlite':
return setup_sqlite_db(request)
if backend_type == 'sqlalchemy':
return setup_sqlalchemy_with_sqlite_in_memory_db(request)
if backend_type == 'mongodb':
if not pymongo_available:
pytest.skip()
return setup_mongo_db(request)
if backend_type == 'mysql':
if not MySQLdb_available:
pytest.skip()
return setup_mysql_db(request)
if backend_type == 'postgresql':
return setup_postgresql_db(request)
raise Exception()
@pytest.fixture
def aaa_unauth(templates_dir, backend):
# Session without any authenticated user
aaa = MockedSessionCork(
templates_dir,
backend=backend,
smtp_server='localhost',
email_sender='test@localhost',
)
aaa._mocked_beaker_session = MockedSession()
return aaa
@pytest.fixture
def aaa_admin(templates_dir, backend):
# Session with an admin user
aaa = MockedSessionCork(
templates_dir,
backend=backend,
email_sender='test@localhost',
smtp_server='localhost',
)
aaa._mocked_beaker_session = MockedSession(username='admin')
return aaa
### Tests
## Unauthenticated user
def test_unauth_basic(aaa_unauth):
assert aaa_unauth._beaker_session.get('username', None) == None
def test_get_current_user_unauth(aaa_unauth):
with raises(AAAException):
aaa_unauth.current_user['username']
def test_unauth_is_anonymous(aaa_unauth):
assert aaa_unauth.user_is_anonymous
def test_failing_login_unauth(aaa_unauth):
login = aaa_unauth.login('phil', 'hunter123')
assert login == False, "Login must fail"
assert aaa_unauth._beaker_session.get('username', None) == None
## Logged in as admin
def test_mockedadmin(aaa_admin):
assert len(aaa_admin._store.users) == 1, len(aaa_admin._store.users)
assert 'admin' in aaa_admin._store.users, repr(aaa_admin._store.users)
def test_password_hashing(aaa_admin):
shash = aaa_admin._hash('user_foo', 'bogus_pwd')
assert len(shash) == 88, "hash length should be 88 and is %d" % len(shash)
assert shash.endswith(b'='), "hash should end with '='"
assert aaa_admin._verify_password('user_foo', 'bogus_pwd', shash) == True, \
"Hashing verification should succeed"
def test_incorrect_password_hashing(aaa_admin):
shash = aaa_admin._hash('user_foo', 'bogus_pwd')
assert len(shash) == 88, "hash length should be 88 and is %d" % len(shash)
assert shash.endswith(b'='), "hash should end with '='"
assert aaa_admin._verify_password('user_foo', '####', shash) == False, \
"Hashing verification should fail"
assert aaa_admin._verify_password('###', 'bogus_pwd', shash) == False, \
"Hashing verification should fail"
def test_password_hashing_collision(aaa_admin):
salt = b'S' * 32
hash1 = aaa_admin._hash(u'user_foo', u'bogus_pwd', salt=salt)
hash2 = aaa_admin._hash(u'user_foobogus', u'_pwd', salt=salt)
assert hash1 != hash2, "Hash collision"
def test_unauth_create_role(aaa_admin):
assert len(aaa_admin._store.users) == 1, "Only the admin user should be present"
aaa_admin._store.roles['admin'] = 10 # lower admin level
assert aaa_admin._store.roles['admin'] == 10, aaa_admin._store.roles['admin']
assert len(aaa_admin._store.users) == 1, "Only the admin user should be present"
with raises(AuthException):
aaa_admin.create_role('user', 33)
def assert_raises(ex, f, *a, **kw):
with raises(ex):
f(*a, **kw)
def test_create_existing_role(aaa_admin):
assert_raises(AAAException, aaa_admin.create_role, 'user', 33)
def test_access_nonexisting_role(aaa_admin):
with raises(KeyError):
aaa_admin._store.roles['NotThere']
def test_create_role_with_incorrect_level(aaa_admin):
with raises(AAAException):
aaa_admin.create_role('new_user', 'not_a_number')
def test_create_role(aaa_admin):
assert len(aaa_admin._store.roles) == 4, repr(aaa_admin._store.roles)
aaa_admin.create_role('user33', 33)
assert len(aaa_admin._store.roles) == 5, repr(aaa_admin._store.roles)
def test_unauth_delete_role(aaa_admin):
aaa_admin._store.roles['admin'] = 10 # lower admin level
assert_raises(AuthException, aaa_admin.delete_role, 'user')
def test_delete_nonexistent_role(aaa_admin):
assert_raises(AAAException, aaa_admin.delete_role, 'user123')
def test_create_delete_role(aaa_admin):
assert len(aaa_admin._store.roles) == 4, repr(aaa_admin._store.roles)
aaa_admin.create_role('user33', 33)
assert len(aaa_admin._store.roles) == 5, repr(aaa_admin._store.roles)
assert aaa_admin._store.roles['user33'] == 33
aaa_admin.delete_role('user33')
assert len(aaa_admin._store.roles) == 4, repr(aaa_admin._store.roles)
def test_list_roles(aaa_admin):
roles = list(aaa_admin.list_roles())
assert len(roles) == 4, "Incorrect. Users are: %s" % repr(aaa_admin._store.roles)
def test_unauth_create_user(aaa_admin):
aaa_admin._store.roles['admin'] = 10 # lower admin level
assert_raises(AuthException, aaa_admin.create_user, 'phil', 'user', 'hunter123')
def test_create_existing_user(aaa_admin):
assert_raises(AAAException, aaa_admin.create_user, 'admin', 'admin', 'bogus')
def test_create_user_with_wrong_role(aaa_admin):
with raises(AAAException):
aaa_admin.create_user('admin2', 'nonexistent_role', 'bogus')
def test_create_user(aaa_admin):
assert len(aaa_admin._store.users) == 1, repr(aaa_admin._store.users)
aaa_admin.create_user('phil', 'user', 'user')
assert len(aaa_admin._store.users) == 2, repr(aaa_admin._store.users)
assert 'phil' in aaa_admin._store.users
@pytest.fixture
def disable_os_urandom(monkeypatch):
monkeypatch.setattr('os.urandom', lambda n: b'9' * n)
def test_check_hashing(aaa_admin, disable_os_urandom):
h1 = aaa_admin._hash(u'user', u'pwd')
assert h1 == b'cDk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5ihNBRY2RYEuI8BWPKndJzD0BTxFOV+hv4Ih9WvRk9Dg='
def test_create_user_check_hashing(aaa_admin, disable_os_urandom):
assert len(aaa_admin._store.users) == 1, repr(aaa_admin._store.users)
aaa_admin.create_user(u'phil', 'user', u'pwd')
assert len(aaa_admin._store.users) == 2, repr(aaa_admin._store.users)
assert 'phil' in aaa_admin._store.users
h = aaa_admin._store.users['phil']['hash'].encode('ascii')
assert h == b'cDk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5REycUvi9EWiRY7kAUwU4vnGD84a0hstdqigKOaNmqBM='
assert h == aaa_admin._hash(u'phil', u'pwd')
def test_unauth_delete_user(aaa_admin):
aaa_admin._store.roles['admin'] = 10 # lower admin level
assert_raises(AuthException, aaa_admin.delete_user, 'phil')
def test_delete_nonexistent_user(aaa_admin):
assert_raises(AAAException, aaa_admin.delete_user, 'not_an_user')
def test_delete_user(aaa_admin):
assert len(aaa_admin._store.users) == 1, repr(aaa_admin._store.users)
aaa_admin.delete_user(u'admin')
assert len(aaa_admin._store.users) == 0, repr(aaa_admin._store.users)
assert 'admin' not in aaa_admin._store.users
def test_list_users(aaa_admin):
users = list(aaa_admin.list_users())
assert len(users) == 1, "Incorrect. Users are: %s" % repr(aaa_admin._store.users)
def test_iteritems_on_users(aaa_admin):
expected_dkeys = set(('hash', 'email_addr', 'role', 'creation_date',
'desc', 'last_login'))
if isinstance(aaa_admin._store, MongoDBBackend):
expected_dkeys.discard('last_login')
if hasattr(aaa_admin._store.users, 'iteritems'):
items = aaa_admin._store.users.iteritems()
else:
items = iter(aaa_admin._store.users.items())
for k, v in items:
dkeys = set(v.keys())
extra = dkeys - expected_dkeys
assert not extra, "Unexpected extra keys: %s" % repr(extra)
missing = expected_dkeys - dkeys
assert not missing, "Missing keys: %s" % repr(missing)
def test_failing_login(aaa_admin):
login = aaa_admin.login('phil', 'hunter123')
assert login == False, "Login must fail"
assert aaa_admin._beaker_session.get('username', None) == 'admin'
def test_login_nonexistent_user_empty_password(aaa_admin):
login = aaa_admin.login('IAmNotHome', '')
assert login == False, "Login must fail"
assert aaa_admin._beaker_session.get('username', None) == 'admin'
def test_login_existing_user_empty_password(aaa_admin):
aaa_admin.create_user('phil', 'user', 'hunter123')
assert 'phil' in aaa_admin._store.users
assert aaa_admin._store.users['phil']['role'] == 'user'
login = aaa_admin.login('phil', '')
assert login == False, "Login must fail"
assert aaa_admin._beaker_session.get('username', None) == 'admin'
def test_create_and_validate_user(aaa_admin):
assert len(aaa_admin._store.users) == 1, "Only the admin user should be present"
aaa_admin.create_user('phil', 'user', 'hunter123')
assert len(aaa_admin._store.users) == 2, "Two users should be present"
assert 'phil' in aaa_admin._store.users
assert aaa_admin._store.users['phil']['role'] == 'user'
login = aaa_admin.login('phil', 'hunter123')
assert login == True, "Login must succeed"
assert aaa_admin._beaker_session['username'] == 'phil'
def test_create_and_validate_user_unicode(aaa_admin, backend):
# FIXME, see #4
if hasattr(backend, '_engine'):
url = backend._engine.url
if str(url).startswith('mysql'):
pytest.xfail()
assert len(aaa_admin._store.users) == 1, "Only the admin user should be present"
aaa_admin.create_user(u'phil_åöॐ', 'user', u'neko_猫')
assert len(aaa_admin._store.users) == 2, "Two users should be present"
assert u'phil_åöॐ' in aaa_admin._store.users
assert aaa_admin._store.users[u'phil_åöॐ']['role'] == 'user'
login = aaa_admin.login(u'phil_åöॐ', u'neko_猫')
assert login == True, "Login must succeed"
assert aaa_admin._beaker_session['username'] == u'phil_åöॐ'
def test_create_user_login_logout(aaa_admin):
assert 'phil' not in aaa_admin._store.users
aaa_admin.create_user('phil', 'user', 'hunter123')
assert 'phil' in aaa_admin._store.users
login = aaa_admin.login('phil', 'hunter123')
assert login == True, "Login must succeed"
assert aaa_admin._beaker_session['username'] == 'phil'
try:
aaa_admin.logout(fail_redirect='/failed_logout')
except bottle.HTTPResponse as e:
assert_is_redirect(e, 'login')
assert aaa_admin._beaker_session.get('username', None) == None
def test_modify_user_using_overwrite(aaa_admin):
aaa_admin.create_user('phil', 'user', 'hunter123')
assert 'phil' in aaa_admin._store.users
u = aaa_admin._store.users['phil']
u.update(role='editor')
aaa_admin._store.users['phil'] = u
assert aaa_admin._store.users['phil']['role'] == 'editor'
def test_modify_user(aaa_admin):
aaa_admin.create_user('phil', 'user', 'hunter123')
aaa_admin._store.users['phil']['role'] = 'editor'
assert aaa_admin._store.users['phil']['role'] == 'editor', aaa_admin._store.users['phil']
def test_modify_user_using_local_change(aaa_admin):
aaa_admin.create_user('phil', 'user', 'hunter123')
u = aaa_admin._store.users['phil']
u['role'] = 'editor'
assert u['role'] == 'editor', repr(u)
assert aaa_admin._store.users['phil']['role'] == 'editor'
def test_write_user_hash_bytes(aaa_admin, backend):
username = 'huh'
h = b'1234'
tstamp = "just a string"
h = h.decode('ascii')
assert isinstance(h, type(u''))
aaa_admin._store.users[username] = {
'role': "user",
'hash': h,
'email_addr': "bar",
'desc': "foo",
'creation_date': tstamp,
'last_login': tstamp
}
if hasattr(backend, '_engine'):
h_from_db = backend._engine.execute("SELECT * FROM users").fetchall()[1][2]
assert h_from_db == '1234'
fetched_h = aaa_admin._store.users[username]['hash']
fetched_h = fetched_h.encode('ascii')
assert isinstance(fetched_h, type(b''))
assert fetched_h == b'1234'
def test_write_user_hash_unicode(aaa_admin):
username = 'huh'
h = u'1234'
tstamp = "just a string"
aaa_admin._store.users[username] = {
'role': "user",
'hash': h,
'email_addr': "bar",
'desc': "foo",
'creation_date': tstamp,
'last_login': tstamp
}
if hasattr(backend, '_engine'):
h_from_db = backend._engine.execute("SELECT * FROM users").fetchall()[1][2]
print("H %r" % h_from_db)
assert h_from_db == '1234'
fetched_h = aaa_admin._store.users[username]['hash']
assert fetched_h == u'1234'
def test_require_failing_username(aaa_admin):
# The user exists, but I'm 'admin'
aaa_admin.create_user('phil', 'user', 'hunter123')
assert_raises(AuthException, aaa_admin.require, username='phil')
def test_require_nonexistent_username(aaa_admin):
assert_raises(AAAException, aaa_admin.require, username='no_such_user')
def test_require_failing_role_fixed(aaa_admin):
assert_raises(AuthException, aaa_admin.require, role='user', fixed_role=True)
def test_require_missing_parameter(aaa_admin):
with raises(AAAException):
aaa_admin.require(fixed_role=True)
def test_require_nonexistent_role(aaa_admin):
assert_raises(AAAException, aaa_admin.require, role='clown')
def test_require_failing_role(aaa_admin):
# Requesting level >= 100
assert_raises(AuthException, aaa_admin.require, role='special')
def test_successful_require_role(aaa_admin):
aaa_admin.require(username='admin')
aaa_admin.require(username='admin', role='admin')
aaa_admin.require(username='admin', role='admin', fixed_role=True)
aaa_admin.require(username='admin', role='user')
def test_authenticated_is_not_anonymous(aaa_admin):
assert not aaa_admin.user_is_anonymous
def test_update_nonexistent_role(aaa_admin):
assert_raises(AAAException, aaa_admin.current_user.update, role='clown')
def test_update_nonexistent_user(aaa_admin):
with raises(AAAException):
aaa_admin._store.users.pop(u'admin')
aaa_admin.current_user.update(role='user')
def test_update_role(aaa_admin):
aaa_admin.current_user.update(role='user')
assert aaa_admin._store.users['admin']['role'] == 'user'
def test_update_pwd(aaa_admin):
aaa_admin.current_user.update(pwd='meow')
def test_update_email(aaa_admin):
aaa_admin.current_user.update(email_addr='foo')
assert aaa_admin._store.users['admin']['email_addr'] == 'foo', aaa_admin._store.users['admin']
def test_get_current_user_nonexistent(aaa_admin):
# The current user 'admin' is not in the user table
with raises(AuthException):
aaa_admin._store.users.pop(u'admin')
aaa_admin.current_user
def test_get_nonexistent_user(aaa_admin):
assert aaa_admin.user('nonexistent_user') is None
def test_get_user_description_field(aaa_admin):
admin = aaa_admin.user(u'admin')
for field in ['description', 'email_addr']:
assert field in admin.__dict__
def test_register_no_user(aaa_admin):
assert_raises(AssertionError, aaa_admin.register, None, 'pwd', 'a@a.a')
def test_register_no_pwd(aaa_admin):
assert_raises(AssertionError, aaa_admin.register, 'foo', None, 'a@a.a')
def test_register_no_email(aaa_admin):
assert_raises(AssertionError, aaa_admin.register, 'foo', 'pwd', None)
def test_register_already_existing(aaa_admin):
assert_raises(AAAException, aaa_admin.register, 'admin', 'pwd', 'a@a.a')
def test_register_no_role(aaa_admin):
assert_raises(AAAException, aaa_admin.register, 'foo', 'pwd', 'a@a.a', role='clown')
def test_register_role_too_high(aaa_admin):
assert_raises(AAAException, aaa_admin.register, 'foo', 'pwd', 'a@a.a', role='admin')
def test_register_valid(aaa_admin, templates_dir):
aaa_admin.mailer.send_email = mock.Mock()
aaa_admin.register('foo', 'pwd', 'email@email.org', role='user',
email_template='views/registration_email.tpl'
)
assert aaa_admin.mailer.send_email.called
r = aaa_admin._store.pending_registrations
assert len(r) == 1
reg_code = list(r)[0]
assert r[reg_code]['username'] == 'foo'
assert r[reg_code]['email_addr'] == 'email@email.org'
assert r[reg_code]['role'] == 'user'
def test_validate_registration_no_code(aaa_admin):
assert_raises(AAAException, aaa_admin.validate_registration, 'not_a_valid_code')
def test_validate_registration(aaa_admin, templates_dir):
aaa_admin.mailer.send_email = mock.Mock()
aaa_admin.register('foo', 'pwd', 'email@email.org', role='user',
email_template='views/registration_email.tpl'
)
r = aaa_admin._store.pending_registrations
reg_code = list(r)[0]
assert len(aaa_admin._store.users) == 1, "Only the admin user should be present"
aaa_admin.validate_registration(reg_code)
assert len(aaa_admin._store.users) == 2, "The new user should be present"
assert len(aaa_admin._store.pending_registrations) == 0, \
"The registration entry should be removed"
def test_send_password_reset_email_no_data(aaa_admin):
with raises(AAAException):
aaa_admin.send_password_reset_email()
def test_send_password_reset_email_incorrect_data(aaa_admin):
with raises(AAAException):
aaa_admin.send_password_reset_email(username='NotThere', email_addr='NoEmail')
def test_send_password_reset_email_incorrect_data2(aaa_admin):
with raises(AAAException):
# The username is valid but the email address is not matching
aaa_admin.send_password_reset_email(username='admin', email_addr='NoEmail')
def test_send_password_reset_email_only_incorrect_email(aaa_admin):
with raises(AAAException):
aaa_admin.send_password_reset_email(email_addr='NoEmail')
def test_send_password_reset_email_only_incorrect_username(aaa_admin):
with raises(AAAException):
aaa_admin.send_password_reset_email(username='NotThere')
def test_send_password_reset_email_only_email(aaa_admin, templates_dir):
aaa_admin.mailer.send_email = mock.Mock()
aaa_admin.send_password_reset_email(email_addr='admin@localhost.local',
email_template='views/password_reset_email')
def test_send_password_reset_email_only_username(aaa_admin, tmpdir, templates_dir):
aaa_admin.mailer.send_email = mock.Mock()
aaa_admin.send_password_reset_email(username='admin',
email_template='views/password_reset_email')
def test_perform_password_reset_invalid(aaa_admin):
with raises(AuthException):
aaa_admin.reset_password(u'bogus', u'newpassword')
def test_perform_password_reset_timed_out(aaa_admin):
aaa_admin.password_reset_timeout = 0
token = aaa_admin._reset_code(u'admin', u'admin@localhost.local')
with raises(AuthException):
aaa_admin.reset_password(token, 'newpassword')
def test_perform_password_reset_nonexistent_user(aaa_admin):
token = aaa_admin._reset_code(u'admin_bogus', u'admin@localhost.local')
with raises(AAAException):
aaa_admin.reset_password(token, u'newpassword')
# The following test should fail
# an user can change the password reset timestamp by b64-decoding the token,
# editing the field and b64-encoding it
@pytest.mark.xfail # FIXME
def test_perform_password_reset_mangled_timestamp(aaa_admin):
token = aaa_admin._reset_code(u'admin', 'admin@localhost.local')
reset_code = b64decode(token).decode('utf-8')
username, email_addr, tstamp, h = reset_code.split(':', 3)
tstamp = str(int(tstamp) + 100)
mangled_token = ':'.join((username, email_addr, tstamp, h))
mangled_token = b64encode(mangled_token.encode('utf-8'))
with raises(AuthException):
aaa_admin.reset_password(mangled_token, u'newpassword')
def test_perform_password_reset_mangled_username(aaa_admin):
token = aaa_admin._reset_code(u'admin', u'admin@localhost.local')
reset_code = b64decode(token).decode('utf-8')
username, email_addr, tstamp, h = reset_code.split(':', 3)
username += "mangled_username"
mangled_token = ':'.join((username, email_addr, tstamp, h))
mangled_token = b64encode(mangled_token.encode('utf-8'))
with raises(AuthException):
aaa_admin.reset_password(mangled_token, u'newpassword')
def test_perform_password_reset_mangled_email(aaa_admin):
token = aaa_admin._reset_code(u'admin', u'admin@localhost.local')
reset_code = b64decode(token).decode('utf-8')
username, email_addr, tstamp, h = reset_code.split(':', 3)
email_addr += "mangled_email"
mangled_token = ':'.join((username, email_addr, tstamp, h))
mangled_token = b64encode(mangled_token.encode('utf-8'))
with raises(AuthException):
aaa_admin.reset_password(mangled_token, u'newpassword')
def test_set_password_directly(aaa_admin):
#assert aaa_admin.login(u'admin', u'newpwd') == False
user = aaa_admin.user(u'admin')
assert user
user.update(pwd='newpwd')
assert aaa_admin.login(u'admin', u'newpwd')
def test_perform_password_reset(aaa_admin):
token = aaa_admin._reset_code(u'admin', u'admin@localhost.local')
aaa_admin.reset_password(token, u'newpassword')
login = aaa_admin.login(u'admin', u'newpassword')
assert login == True, "Login must succeed"
bottle-cork-0.12.0/tests/test_webtest_decorated.py 0000644 0001750 0001750 00000004741 12512453671 022125 0 ustar fede fede 0000000 0000000 # Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Functional test for decorators-based webapp using Json backend
#
# Requires WebTest http://webtest.pythonpaste.org/
#
# Run as: nosetests tests/test_functional_decorated.py
#
from nose import SkipTest
from webtest import TestApp
import shutil
import os
import testutils
from cork import Cork
REDIR = 302
class Test(testutils.WebFunctional):
def create_app_instance(self):
"""create TestApp instance"""
assert self._app is None
import simple_webapp_decorated
self._bottle_app = simple_webapp_decorated.app
env = {'REMOTE_ADDR': '127.0.0.1'}
self._app = TestApp(self._bottle_app, extra_environ=env)
print("Test App created")
def login_as_admin(self):
"""perform log in"""
assert self._app is not None
assert 'beaker.session.id' not in self._app.cookies, "Unexpected cookie found"
self.assert_200('/login', 'Please insert your credentials')
assert 'beaker.session.id' not in self._app.cookies, "Unexpected cookie found"
self.assert_redirect('/admin', '/sorry_page')
self.assert_200('/user_is_anonymous', 'True')
assert 'beaker.session.id' not in self._app.cookies, "Unexpected cookie found"
post = {'username': 'admin', 'password': 'admin'}
self.assert_redirect('/login', '/', post=post)
assert 'beaker.session.id' in self._app.cookies, "Cookie not found"
self.assert_200('/my_role', 'admin')
assert 'beaker.session.id' in self._app.cookies, "Cookie not found"
import bottle
session = bottle.request.environ.get('beaker.session')
print("Session from func. test", repr(session))
self.assert_200('/login', 'Please insert your credentials')
p = self._app.get('/admin')
assert 'Welcome' in p.body, repr(p)
p = self._app.get('/my_role', status=200)
assert p.status_int == 200
assert p.body == 'admin', "Sta"
print("Login performed")
def test_functional_expiration(self):
self.login_as_admin()
r = self._app.get('/admin')
assert r.status == '200 OK', repr(r)
# change the cookie expiration in order to expire it
self._app.app.options['timeout'] = 0
assert self._app.get('/admin').status_int == REDIR, "The cookie should have expired"
bottle-cork-0.12.0/tests/simple_webapp.py 0000777 0001750 0001750 00000000000 12512453671 025447 2../examples/simple_webapp.py ustar fede fede 0000000 0000000 bottle-cork-0.12.0/tests/test_sqlalchemy.py 0000644 0001750 0001750 00000032601 12515755510 020574 0 ustar fede fede 0000000 0000000 # -*- coding: utf-8 -*
# Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Functional testing - test the Cork module against diffent database backends
from base64 import b64encode, b64decode
from pytest import raises
import bottle
import mock
import os
import pytest
import time
from cork import Cork, AAAException, AuthException
from cork.backends import JsonBackend
from cork.backends import MongoDBBackend
from cork.backends import SQLiteBackend
from cork.backends import SqlAlchemyBackend
from conftest import assert_is_redirect
import MySQLdb
### Mocked classes
class MockedSession(object):
"""Mock Beaker session
"""
def __init__(self, username=None):
self.__username = username
self.__saved = False
def get(self, k, default):
assert k in ('username')
if self.__username is None:
return default
return self.__username
def __getitem__(self, k):
assert k in ('username')
if self.__username is None:
raise KeyError()
return self.__username
def __setitem__(self, k, v):
assert k in ('username')
self.__username = v
self.__saved = False
def delete(self):
"""Used during logout to delete the current session"""
self.__username = None
def save(self):
self.__saved = True
#TODO: implement tests around MockedSession __saved
class MockedSessionCork(Cork):
"""Mocked Cork instance where the session is replaced with
MockedSession
"""
@property
def _beaker_session(self):
return self._mocked_beaker_session
### Fixtures and helpers
## Backends
def setup_sqlite_db(request):
# in-memory SQLite DB using the SQLiteBackend backend module.
b = SQLiteBackend(':memory:', initialize=True)
b.connection.executescript("""
INSERT INTO users (username, email_addr, desc, role, hash, creation_date) VALUES
(
'admin',
'admin@localhost.local',
'admin test user',
'admin',
'cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=',
'2012-10-28 20:50:26.286723'
);
INSERT INTO roles (role, level) VALUES ('special', 200);
INSERT INTO roles (role, level) VALUES ('admin', 100);
INSERT INTO roles (role, level) VALUES ('editor', 60);
INSERT INTO roles (role, level) VALUES ('user', 50);
""")
return b
def setup_json_db(request, tmpdir):
# Setup test directory with valid JSON files and return JsonBackend instance
tmpdir.join('users.json').write("""{"admin": {"email_addr": "admin@localhost.local", "desc": null, "role": "admin", "hash": "69f75f38ac3bfd6ac813794f3d8c47acc867adb10b806e8979316ddbf6113999b6052efe4ba95c0fa9f6a568bddf60e8e5572d9254dbf3d533085e9153265623", "creation_date": "2012-04-09 14:22:27.075596", "last_login": "2012-10-28 20:50:26.286723"}}""")
tmpdir.join('roles.json').write("""{"special": 200, "admin": 100, "user": 50, "editor": 60}""")
tmpdir.join('register.json').write("""{}""")
return JsonBackend(tmpdir)
def setup_sqlalchemy_with_sqlite_in_memory_db(request):
# Setup an SqlAlchemyBackend backend using an in-memory SQLite DB
mb = SqlAlchemyBackend('sqlite:///:memory:', initialize=True)
## Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
#def purge_test_db(self):
# # Purge DB
# mb = connect_to_test_db()
# mb._drop_all_tables()
def setup_mongo_db(request):
# FIXME no last_login?
t0 = time.time()
def timer(s, max_time=None):
delta = time.time() - t0
print("%s %f" % (s, delta))
if max_time is not None:
assert delta < max_time
mb = MongoDBBackend(db_name='cork-functional-test', initialize=True)
timer('connect + init')
# Purge DB
mb.users._coll.drop()
mb.roles._coll.drop()
mb.pending_registrations._coll.drop()
timer('purge')
# Create admin
mb.users._coll.insert({
"login": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723"
})
timer('create')
# Create users
mb.roles._coll.insert({'role': 'special', 'val': 200})
mb.roles._coll.insert({'role': 'admin', 'val': 100})
mb.roles._coll.insert({'role': 'editor', 'val': 60})
mb.roles._coll.insert({'role': 'user', 'val': 50})
timer('create users')
def fin():
mb.users._coll.drop()
mb.roles._coll.drop()
request.addfinalizer(fin)
timer('mongo setup', 8)
return mb
def setup_mysql_db(request):
if os.environ.get('TRAVIS', False):
# Using Travis-CI - https://travis-ci.org/
password = ''
db_name = 'myapp_test'
else:
password = ''
db_name = 'cork_functional_test'
uri = "mysql://root:%s@localhost/%s" % (password, db_name)
mb = SqlAlchemyBackend(uri, initialize=True)
## Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
return # TODO: fix
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
def setup_postgresql_db(request):
if os.environ.get('TRAVIS', False):
# Using Travis-CI - https://travis-ci.org/
db_name = 'myapp_test'
else:
db_name = 'cork_functional_test'
uri = "postgresql+psycopg2://postgres:@/%s" % db_name
mb = SqlAlchemyBackend(uri, initialize=True)
# Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
## General fixtures
@pytest.fixture(params=[
'json',
'mysql',
'postgresql',
'sqlalchemy',
'sqlite',
])
def backend(tmpdir, request):
# Create backend instances
backend_type = request.param
if backend_type == 'json':
return setup_json_db(request, tmpdir)
if backend_type == 'sqlite':
return setup_sqlite_db(request)
if backend_type == 'sqlalchemy':
return setup_sqlalchemy_with_sqlite_in_memory_db(request)
if backend_type == 'mysql':
return setup_mysql_db(request)
if backend_type == 'postgresql':
return setup_postgresql_db(request)
raise Exception()
@pytest.fixture
def aaa_unauth(templates_dir, backend):
# Session without any authenticated user
aaa = MockedSessionCork(
templates_dir,
backend=backend,
smtp_server='localhost',
email_sender='test@localhost',
)
aaa._mocked_beaker_session = MockedSession()
return aaa
@pytest.fixture
def aaa_admin(templates_dir, backend):
# Session with an admin user
aaa = MockedSessionCork(
templates_dir,
backend=backend,
email_sender='test@localhost',
smtp_server='localhost',
)
aaa._mocked_beaker_session = MockedSession(username='admin')
return aaa
### Tests
def test_password_hashing(aaa_admin):
shash = aaa_admin._hash('user_foo', 'bogus_pwd')
assert len(shash) == 88, "hash length should be 88 and is %d" % len(shash)
assert shash.endswith(b'='), "hash should end with '='"
assert aaa_admin._verify_password('user_foo', 'bogus_pwd', shash) == True, \
"Hashing verification should succeed"
def test_incorrect_password_hashing(aaa_admin):
shash = aaa_admin._hash('user_foo', 'bogus_pwd')
assert len(shash) == 88, "hash length should be 88 and is %d" % len(shash)
assert shash.endswith(b'='), "hash should end with '='"
assert aaa_admin._verify_password('user_foo', '####', shash) == False, \
"Hashing verification should fail"
assert aaa_admin._verify_password('###', 'bogus_pwd', shash) == False, \
"Hashing verification should fail"
def test_password_hashing_collision(aaa_admin):
salt = b'S' * 32
hash1 = aaa_admin._hash(u'user_foo', u'bogus_pwd', salt=salt)
hash2 = aaa_admin._hash(u'user_foobogus', u'_pwd', salt=salt)
assert hash1 != hash2, "Hash collision"
@pytest.fixture
def disable_os_urandom(monkeypatch):
monkeypatch.setattr('os.urandom', lambda n: b'9' * n)
def test_check_hashing(aaa_admin, disable_os_urandom):
h1 = aaa_admin._hash(u'user', u'pwd')
assert h1 == b'cDk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5ihNBRY2RYEuI8BWPKndJzD0BTxFOV+hv4Ih9WvRk9Dg='
def test_create_user_check_hashing(aaa_admin, disable_os_urandom):
assert len(aaa_admin._store.users) == 1, repr(aaa_admin._store.users)
aaa_admin.create_user(u'phil', 'user', u'pwd')
assert len(aaa_admin._store.users) == 2, repr(aaa_admin._store.users)
assert 'phil' in aaa_admin._store.users
h = aaa_admin._store.users['phil']['hash'].encode('ascii')
assert h == b'cDk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5REycUvi9EWiRY7kAUwU4vnGD84a0hstdqigKOaNmqBM='
assert h == aaa_admin._hash(u'phil', u'pwd')
def test_write_user_hash_bytes(aaa_admin, backend):
username = 'huh'
h = b'1234'
tstamp = "just a string"
h = h.decode('ascii')
assert isinstance(h, type(u''))
aaa_admin._store.users[username] = {
'role': "user",
'hash': h,
'email_addr': "bar",
'desc': "foo",
'creation_date': tstamp,
'last_login': tstamp
}
if hasattr(backend, '_engine'):
h_from_db = backend._engine.execute("SELECT * FROM users").fetchall()[1][2]
print("H %r" % h_from_db)
assert h_from_db == '1234'
fetched_h = aaa_admin._store.users[username]['hash']
fetched_h = fetched_h.encode('ascii')
assert isinstance(fetched_h, type(b''))
assert fetched_h == b'1234'
def test_write_user_hash_unicode(aaa_admin):
username = 'huh'
h = u'1234'
tstamp = "just a string"
aaa_admin._store.users[username] = {
'role': "user",
'hash': h,
'email_addr': "bar",
'desc': "foo",
'creation_date': tstamp,
'last_login': tstamp
}
if hasattr(backend, '_engine'):
h_from_db = backend._engine.execute("SELECT * FROM users").fetchall()[1][2]
print("H %r" % h_from_db)
assert h_from_db == '1234'
fetched_h = aaa_admin._store.users[username]['hash']
assert fetched_h == u'1234'
def test_iteritems_on_users(aaa_admin):
expected_dkeys = set(('hash', 'email_addr', 'role', 'creation_date',
'desc', 'last_login'))
if isinstance(aaa_admin._store, MongoDBBackend):
expected_dkeys.discard('last_login')
if hasattr(aaa_admin._store.users, 'iteritems'):
items = aaa_admin._store.users.iteritems()
else:
items = iter(aaa_admin._store.users.items())
for k, v in items:
dkeys = set(v.keys())
extra = dkeys - expected_dkeys
assert not extra, "Unexpected extra keys: %s" % repr(extra)
missing = expected_dkeys - dkeys
assert not missing, "Missing keys: %s" % repr(missing)
bottle-cork-0.12.0/tests/test_pymongo.py 0000644 0001750 0001750 00000024500 12517521253 020116 0 ustar fede fede 0000000 0000000 # -*- coding: utf-8 -*
# Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Functional testing - test the Cork module against diffent database backends
from base64 import b64encode, b64decode
from pytest import raises
import bottle
import mock
import os
import pytest
import time
from cork import Cork, AAAException, AuthException
from cork.backends import JsonBackend
from cork.backends import MongoDBBackend
from cork.backends import SQLiteBackend
from cork.backends import SqlAlchemyBackend
from conftest import assert_is_redirect
try:
import pymongo
pymongo_available = True
except ImportError:
pymongo_available = False
try:
import MySQLdb
MySQLdb_available = True
except ImportError:
MySQLdb_available = False
### Mocked classes
class MockedSession(object):
"""Mock Beaker session
"""
def __init__(self, username=None):
self.__username = username
self.__saved = False
def get(self, k, default):
assert k in ('username')
if self.__username is None:
return default
return self.__username
def __getitem__(self, k):
assert k in ('username')
if self.__username is None:
raise KeyError()
return self.__username
def __setitem__(self, k, v):
assert k in ('username')
self.__username = v
self.__saved = False
def delete(self):
"""Used during logout to delete the current session"""
self.__username = None
def save(self):
self.__saved = True
#TODO: implement tests around MockedSession __saved
class MockedSessionCork(Cork):
"""Mocked Cork instance where the session is replaced with
MockedSession
"""
@property
def _beaker_session(self):
return self._mocked_beaker_session
### Fixtures and helpers
## Backends
def setup_sqlite_db(request):
# in-memory SQLite DB using the SQLiteBackend backend module.
b = SQLiteBackend(':memory:', initialize=True)
b.connection.executescript("""
INSERT INTO users (username, email_addr, desc, role, hash, creation_date) VALUES
(
'admin',
'admin@localhost.local',
'admin test user',
'admin',
'cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=',
'2012-10-28 20:50:26.286723'
);
INSERT INTO roles (role, level) VALUES ('special', 200);
INSERT INTO roles (role, level) VALUES ('admin', 100);
INSERT INTO roles (role, level) VALUES ('editor', 60);
INSERT INTO roles (role, level) VALUES ('user', 50);
""")
return b
def setup_json_db(request, tmpdir):
# Setup test directory with valid JSON files and return JsonBackend instance
tmpdir.join('users.json').write("""{"admin": {"email_addr": "admin@localhost.local", "desc": null, "role": "admin", "hash": "69f75f38ac3bfd6ac813794f3d8c47acc867adb10b806e8979316ddbf6113999b6052efe4ba95c0fa9f6a568bddf60e8e5572d9254dbf3d533085e9153265623", "creation_date": "2012-04-09 14:22:27.075596", "last_login": "2012-10-28 20:50:26.286723"}}""")
tmpdir.join('roles.json').write("""{"special": 200, "admin": 100, "user": 50, "editor": 60}""")
tmpdir.join('register.json').write("""{}""")
return JsonBackend(tmpdir)
def setup_sqlalchemy_with_sqlite_in_memory_db(request):
# Setup an SqlAlchemyBackend backend using an in-memory SQLite DB
mb = SqlAlchemyBackend('sqlite:///:memory:', initialize=True)
## Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
#def purge_test_db(self):
# # Purge DB
# mb = connect_to_test_db()
# mb._drop_all_tables()
def setup_mongo_db(request):
# FIXME no last_login?
t0 = time.time()
def timer(s, max_time=None):
delta = time.time() - t0
print("%s %f" % (s, delta))
if max_time is not None:
assert delta < max_time
mb = MongoDBBackend(db_name='cork-functional-test', initialize=True)
timer('connect + init')
# Purge DB
mb.users._coll.drop()
mb.roles._coll.drop()
mb.pending_registrations._coll.drop()
timer('purge')
# Create admin
mb.users._coll.insert({
"login": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723"
})
timer('create')
# Create users
mb.roles._coll.insert({'role': 'special', 'val': 200})
mb.roles._coll.insert({'role': 'admin', 'val': 100})
mb.roles._coll.insert({'role': 'editor', 'val': 60})
mb.roles._coll.insert({'role': 'user', 'val': 50})
timer('create users')
def fin():
mb.users._coll.drop()
mb.roles._coll.drop()
request.addfinalizer(fin)
timer('mongo setup', 8)
return mb
def setup_mysql_db(request):
if os.environ.get('TRAVIS', False):
# Using Travis-CI - https://travis-ci.org/
password = ''
db_name = 'myapp_test'
else:
password = ''
db_name = 'cork_functional_test'
uri = "mysql://root:%s@localhost/%s" % (password, db_name)
mb = SqlAlchemyBackend(uri, initialize=True)
## Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
return # TODO: fix
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
def setup_postgresql_db(request):
if os.environ.get('TRAVIS', False):
# Using Travis-CI - https://travis-ci.org/
db_name = 'myapp_test'
else:
db_name = 'cork_functional_test'
uri = "postgresql+psycopg2://postgres:@/%s" % db_name
mb = SqlAlchemyBackend(uri, initialize=True)
# Purge DB
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
# Create roles
mb.roles.insert({'role': 'special', 'level': 200})
mb.roles.insert({'role': 'admin', 'level': 100})
mb.roles.insert({'role': 'editor', 'level': 60})
mb.roles.insert({'role': 'user', 'level': 50})
# Create admin
mb.users.insert({
"username": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723",
"last_login": "2012-10-28 20:50:26.286723"
})
assert len(mb.roles) == 4
assert len(mb.users) == 1
def fin():
mb._drop_all_tables()
assert len(mb.roles) == 0
assert len(mb.users) == 0
request.addfinalizer(fin)
return mb
## General fixtures
@pytest.fixture(params=[
'json',
'mongodb',
'mysql',
'postgresql',
'sqlalchemy',
'sqlite',
])
def backend(tmpdir, request):
# Create backend instances
backend_type = request.param
if backend_type == 'json':
return setup_json_db(request, tmpdir)
if backend_type == 'sqlite':
return setup_sqlite_db(request)
if backend_type == 'sqlalchemy':
return setup_sqlalchemy_with_sqlite_in_memory_db(request)
if backend_type == 'mongodb':
if not pymongo_available:
pytest.skip()
return setup_mongo_db(request)
if backend_type == 'mysql':
if not MySQLdb_available:
pytest.skip()
return setup_mysql_db(request)
if backend_type == 'postgresql':
pytest.skip()
return setup_postgresql_db(request)
raise Exception()
@pytest.fixture
def aaa_unauth(templates_dir, backend):
# Session without any authenticated user
aaa = MockedSessionCork(
templates_dir,
backend=backend,
smtp_server='localhost',
email_sender='test@localhost',
)
aaa._mocked_beaker_session = MockedSession()
return aaa
@pytest.fixture
def aaa_admin(templates_dir, backend):
# Session with an admin user
aaa = MockedSessionCork(
templates_dir,
backend=backend,
email_sender='test@localhost',
smtp_server='localhost',
)
aaa._mocked_beaker_session = MockedSession(username='admin')
return aaa
### Tests
def test_update_role(aaa_admin):
aaa_admin.current_user.update(role='user')
assert aaa_admin._store.users['admin']['role'] == 'user'
def test_update_pwd(aaa_admin):
aaa_admin.current_user.update(pwd='meow')
def test_update_email(aaa_admin):
aaa_admin.current_user.update(email_addr='foo')
assert aaa_admin._store.users['admin']['email_addr'] == 'foo', aaa_admin._store.users['admin']
bottle-cork-0.12.0/tests/test_crypto.py 0000644 0001750 0001750 00000001175 12514234502 017744 0 ustar fede fede 0000000 0000000
import pytest
from beaker import crypto
import hashlib
def test_beaker_gen_keys():
cleartext = u'user_foo\x00bogus_pwd'
salt = u'x' * 32
h = crypto.generateCryptoKeys(cleartext, salt, 10)
cl2 = str(cleartext).encode('ascii', 'strict')
print('ct %r' % cleartext)
print('ct str %r' % str(cleartext))
print('cl2 %r' % cl2)
sa2 = str(salt).encode('ascii', 'strict')
h2=hashlib.pbkdf2_hmac(hashlib.sha1().name, cl2, sa2, 10, None)
print('hlib convert')
print(h2)
assert h == b'0F\x88S\x93\xa9\xf7\xb9f\xbf\x05\xf96\x95RL\x13\xfc\x8e\x0f\x0e\x96\xb3{sb\xbc+\xcbg6K'
def test_2():
pass
bottle-cork-0.12.0/tests/test.py 0000644 0001750 0001750 00000042616 12517131104 016346 0 ustar fede fede 0000000 0000000 # Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Unit testing
#
from base64 import b64encode
import mock
import pytest
import smtplib
import cork.cork
from cork import Cork, JsonBackend, AAAException, AuthException
from cork.base_backend import BackendIOException
class RoAttrDict(dict):
"""Read-only attribute-accessed dictionary.
Used to mock beaker's session objects
"""
def __getattr__(self, name):
return self[name]
class MockedAdminCork(Cork):
"""Mocked module where the current user is always 'admin'"""
@property
def _beaker_session(self):
return RoAttrDict(username=u'admin')
def _setup_cookie(self, username):
global cookie_name
cookie_name = username
class MockedUnauthenticatedCork(Cork):
"""Mocked module where the current user not set"""
@property
def _beaker_session(self):
return RoAttrDict()
def _setup_cookie(self, username):
global cookie_name
cookie_name = username
@pytest.fixture
def json_db_dir(tmpdir, templates_dir):
"""Setup test directory with valid JSON files"""
tmpdir.join('users.json').write("""{"admin": {"email_addr": null, "desc": null, "role": "admin", "hash": "69f75f38ac3bfd6ac813794f3d8c47acc867adb10b806e8979316ddbf6113999b6052efe4ba95c0fa9f6a568bddf60e8e5572d9254dbf3d533085e9153265623", "creation_date": "2012-04-09 14:22:27.075596"}}""")
tmpdir.join('roles.json').write("""{"special": 200, "admin": 100, "user": 50}""")
tmpdir.join('register.json').write("""{}""")
return tmpdir
@pytest.fixture
def aaa(json_db_dir):
"""Setup a MockedAdminCork instance"""
aaa = MockedAdminCork(json_db_dir.strpath, smtp_server='localhost', email_sender='test@localhost')
aaa.mailer.use_threads = False
return aaa
@pytest.fixture
def aaa_unauth(json_db_dir):
"""Setup test directory and a MockedAdminCork instance"""
aaa = MockedUnauthenticatedCork(json_db_dir.strpath)
aaa.mailer.use_threads = False
return aaa
# Patch SMTP / SMTP_SSL to prevent network interaction
@pytest.fixture(autouse=True)
def mock_smtp(monkeypatch):
m = mock.Mock()
monkeypatch.setattr('cork.cork.SMTP', m)
return m
@pytest.fixture(autouse=True)
def mock_smtp_ssl(monkeypatch):
m = mock.Mock()
m.return_value = mock.Mock()
monkeypatch.setattr('cork.cork.SMTP_SSL', m)
return m
@pytest.fixture
def mock_send(monkeypatch):
m = mock.Mock()
m.return_value = Mock()
monkeypatch.setattr(Mailer, '_send', m)
return m
def raises(f, *e):
def wrapper(*a, **kw):
return f(*a, **kw)
return wrapper
# Tests
def test_init(json_db_dir):
Cork(json_db_dir.strpath)
def test_initialize_storage(json_db_dir):
jb = JsonBackend(json_db_dir.strpath, initialize=True)
Cork(backend=jb)
assert json_db_dir.join('users.json').read() == '{}'
assert json_db_dir.join('roles.json').read() == '{}'
assert json_db_dir.join('register.json').read() == '{}'
return
with open("%s/views/registration_email.tpl" % testdir) as f:
assert f.readlines() == [
'Username:{{username}} Email:{{email_addr}} Code:{{registration_code}}']
with open("%s/views/password_reset_email.tpl" % testdir) as f:
assert f.readlines() == [
'Username:{{username}} Email:{{email_addr}} Code:{{reset_code}}']
def test_unable_to_save(json_db_dir):
bogus_dir = '/___inexisting_directory___'
with pytest.raises(BackendIOException):
Cork(bogus_dir, initialize=True)
def test_loadjson_missing_file(aaa):
with pytest.raises(BackendIOException):
aaa._store._loadjson('nonexistent_file', {})
def test_loadjson_broken_file(aaa, json_db_dir):
json_db_dir.join('broken_file.json').write('-----')
with pytest.raises(BackendIOException):
aaa._store._loadjson('broken_file', {})
def test_loadjson_unchanged(aaa):
# By running _refresh with unchanged files the files should not be reloaded
mtimes = aaa._store._mtimes
aaa._store._refresh()
# The test simply ensures that no mtimes have been updated
assert mtimes == aaa._store._mtimes
# Test PBKDF2-based password hashing
def test_password_hashing_PBKDF2(aaa):
shash = aaa._hash(u'user_foo', u'bogus_pwd')
assert isinstance(shash, bytes)
assert len(shash) == 88, "hash length should be 88 and is %d" % len(shash)
assert shash.endswith(b'='), "hash should end with '='"
assert aaa._verify_password('user_foo', 'bogus_pwd', shash) == True, \
"Hashing verification should succeed"
def test_hashlib_pbk():
# Hashlib works under py2 and py3 producing the same output.
# With iterations = 10 and dklen = 32 the output is also consistent with
# beaker under py2 as in the previous versions of Cork
import hashlib
cleartext = b'hello'
salt = b'hi'
h = hashlib.pbkdf2_hmac('sha1', cleartext, salt, 10, dklen=32)
assert b64encode(h) == b'QTH8vcCFLLqLhxCTnkz6sq+Un3B4RQgWjMPpRC9hfEY='
def test_password_hashing_PBKDF2_known_hash(aaa):
assert aaa.preferred_hashing_algorithm == 'PBKDF2'
salt = b's' * 32
shash = aaa._hash(u'user_foo', u'bogus_pwd', salt=salt)
assert shash == b'cHNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzax44AxQgK6uD9q1YWxLos1ispCe1Z7T7pOFK1PwdWEs='
def test_password_hashing_PBKDF2_known_hash_2(aaa):
assert aaa.preferred_hashing_algorithm == 'PBKDF2'
salt = b'\0' * 32
shash = aaa._hash(u'user_foo', u'bogus_pwd', salt=salt)
assert shash == b'cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8Uh4pyEOHoRz4j0lDzAmqb7Dvmo8GpeXwiKTDsuYFw='
def test_password_hashing_PBKDF2_known_hash_3(aaa):
assert aaa.preferred_hashing_algorithm == 'PBKDF2'
salt = b'x' * 32
shash = aaa._hash(u'user_foo', u'bogus_pwd', salt=salt)
assert shash == b'cHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4MEaIU5Op97lmvwX5NpVSTBP8jg8OlrN7c2K8K8tnNks='
def test_password_hashing_PBKDF2_incorrect_hash_len(aaa):
salt = b'x' * 31 # Incorrect length
with pytest.raises(AssertionError):
shash = aaa._hash(u'user_foo', u'bogus_pwd', salt=salt)
def test_password_hashing_PBKDF2_incorrect_hash_value(aaa):
shash = aaa._hash(u'user_foo', u'bogus_pwd')
assert len(shash) == 88, "hash length should be 88 and is %d" % len(shash)
assert shash.endswith(b'='), "hash should end with '='"
assert aaa._verify_password(u'user_foo', u'####', shash) == False, \
"Hashing verification should fail"
assert aaa._verify_password(u'###', u'bogus_pwd', shash) == False, \
"Hashing verification should fail"
def test_password_hashing_PBKDF2_collision(aaa):
salt = b'S' * 32
hash1 = aaa._hash(u'user_foo', u'bogus_pwd', salt=salt)
hash2 = aaa._hash(u'user_foobogus', u'_pwd', salt=salt)
assert hash1 != hash2, "Hash collision"
# Test password hashing for inexistent algorithms
def test_password_hashing_bogus_algo(aaa):
with pytest.raises(RuntimeError):
aaa._hash('user_foo', 'bogus_pwd', algo='bogus_algo')
def test_password_hashing_bogus_algo_during_verify(aaa):
# Incorrect hash type (starts with "X")
shash = b64encode(b'X' + b'bogusstring')
with pytest.raises(RuntimeError):
aaa._verify_password(u'user_foo', u'bogus_pwd', shash)
# End of password hashing tests
@pytest.mark.xfail
def test_create_empty_role(aaa):
# TODO: implement empty role check
with pytest.raises(AAAException):
aaa.create_role('', 42)
def test_authenticated_is_not_anonymous(aaa):
assert not aaa.user_is_anonymous
def test_register(aaa, json_db_dir):
aaa.register(u'foo', u'pwd', u'a@a.a')
assert len(aaa._store.pending_registrations) == 1, repr(aaa._store.pending_registrations)
def test_smtp_url_parsing_1(aaa):
c = aaa.mailer._parse_smtp_url('')
assert c['proto'] == 'smtp'
assert c['user'] == None
assert c['pass'] == None
assert c['fqdn'] == ''
assert c['port'] == 25
def test_smtp_url_parsing_2(aaa):
c = aaa.mailer._parse_smtp_url('starttls://foo')
assert c['proto'] == 'starttls'
assert c['user'] == None
assert c['pass'] == None
assert c['fqdn'] == 'foo'
assert c['port'] == 25
def test_smtp_url_parsing_3(aaa):
c = aaa.mailer._parse_smtp_url('foo:443')
assert c['proto'] == 'smtp'
assert c['user'] == None
assert c['pass'] == None
assert c['fqdn'] == 'foo'
assert c['port'] == 443
def test_smtp_url_parsing_4(aaa):
c = aaa.mailer._parse_smtp_url('ssl://user:pass@foo:443/')
assert c['proto'] == 'ssl'
assert c['user'] == 'user'
assert c['pass'] == 'pass'
assert c['fqdn'] == 'foo'
assert c['port'] == 443
def test_smtp_url_parsing_5(aaa):
c = aaa.mailer._parse_smtp_url('smtp://smtp.magnet.ie')
assert c['proto'] == 'smtp'
assert c['user'] == None
assert c['pass'] == None
assert c['fqdn'] == 'smtp.magnet.ie'
assert c['port'] == 25
def test_smtp_url_parsing_email_as_username_no_password(aaa):
# the username contains an at sign '@'
c = aaa.mailer._parse_smtp_url('ssl://us.er@somewhere.net@foo:443/')
assert c['proto'] == 'ssl'
assert c['user'] == u'us.er@somewhere.net', \
"Username is incorrectly parsed as '%s'" % c['user']
assert c['pass'] == None
assert c['fqdn'] == 'foo'
assert c['port'] == 443
def test_smtp_url_parsing_email_as_username(aaa):
# the username contains an at sign '@'
c = aaa.mailer._parse_smtp_url('ssl://us.er@somewhere.net:pass@foo:443/')
assert c['proto'] == 'ssl'
assert c['user'] == u'us.er@somewhere.net', \
"Username is incorrectly parsed as '%s'" % c['user']
assert c['pass'] == 'pass'
assert c['fqdn'] == 'foo'
assert c['port'] == 443
def test_smtp_url_parsing_at_sign_in_password(aaa):
# the password contains at signs '@'
c = aaa.mailer._parse_smtp_url('ssl://username:pass@w@rd@foo:443/')
assert c['proto'] == 'ssl'
assert c['user'] == 'username', \
"Username is incorrectly parsed as '%s'" % c['user']
assert c['pass'] == 'pass@w@rd', \
"Password is incorrectly parsed as '%s'" % c['pass']
assert c['fqdn'] == 'foo'
assert c['port'] == 443
def test_smtp_url_parsing_email_as_username_2(aaa):
# both the username and the password contains an at sign '@'
c = aaa.mailer._parse_smtp_url('ssl://us.er@somewhere.net:pass@word@foo:443/')
assert c['proto'] == 'ssl'
assert c['user'] == u'us.er@somewhere.net', \
"Username is incorrectly parsed as '%s'" % c['user']
assert c['pass'] == u'pass@word', \
"Password is incorrectly parsed as '%s'" % c['pass']
assert c['fqdn'] == 'foo'
assert c['port'] == 443
def test_smtp_url_parsing_incorrect_URL_port(aaa):
with pytest.raises(RuntimeError):
c = aaa.mailer._parse_smtp_url(':99999')
def test_smtp_url_parsing_incorrect_URL_port_len(aaa):
with pytest.raises(RuntimeError):
c = aaa.mailer._parse_smtp_url(':123456')
def test_smtp_url_parsing_incorrect_URL_len(aaa):
with pytest.raises(RuntimeError):
c = aaa.mailer._parse_smtp_url('a' * 256)
def test_smtp_url_parsing_incorrect_URL_syntax(aaa):
with pytest.raises(RuntimeError):
c = aaa.mailer._parse_smtp_url('::')
def test_smtp_url_parsing_IPv4(aaa):
c = aaa.mailer._parse_smtp_url('127.0.0.1')
assert c['fqdn'] == '127.0.0.1'
def test_smtp_url_parsing_IPv6(aaa):
c = aaa.mailer._parse_smtp_url('[2001:0:0123:4567:89ab:cdef]')
assert c['fqdn'] == '[2001:0:0123:4567:89ab:cdef]'
def test_send_email_SMTP(aaa, mock_smtp):
aaa.mailer.send_email(u'address', u' sbj', u'text')
aaa.mailer.join()
mock_smtp.assert_called_once_with('localhost', 25)
session = mock_smtp.return_value
assert session.sendmail.call_count == 1
assert session.quit.call_count == 1
def test_send_email_SMTP_SSL(aaa, mock_smtp_ssl):
aaa.mailer._conf['proto'] = 'ssl'
aaa.mailer.send_email('address', ' sbj', 'text')
aaa.mailer.join()
mock_smtp_ssl.assert_called_once_with('localhost', 25)
session = mock_smtp_ssl.return_value
assert session.sendmail.call_count == 1
assert session.quit.call_count == 1
assert len(session.method_calls) == 2
def test_send_email_SMTP_SSL_with_login(aaa, mock_smtp_ssl):
aaa.mailer._conf['proto'] = 'ssl'
aaa.mailer._conf['user'] = u'username'
aaa.mailer.send_email('address', ' sbj', 'text')
aaa.mailer.join()
mock_smtp_ssl.assert_called_once_with('localhost', 25)
session = mock_smtp_ssl.return_value
assert session.login.call_count == 1
assert session.sendmail.call_count == 1
assert session.quit.call_count == 1
assert len(session.method_calls) == 3
def test_send_email_SMTP_STARTTLS(aaa, mock_smtp):
aaa.mailer._conf['proto'] = 'starttls'
aaa.mailer.send_email(u'address', u' sbj', u'text')
aaa.mailer.join()
mock_smtp.assert_called_once_with('localhost', 25)
session = mock_smtp.return_value
assert session.ehlo.call_count == 2
assert session.starttls.call_count == 1
assert session.sendmail.call_count == 1
assert session.quit.call_count == 1
assert len(session.method_calls) == 5
def test_do_not_send_email(aaa):
aaa.mailer._conf['fqdn'] = None # disable email delivery
with pytest.raises(AAAException):
aaa.mailer.send_email(u'address', u'sbj', u'text')
aaa.mailer.join()
def test_purge_expired_registration(aaa, json_db_dir):
aaa.register(u'foo', u'pwd', u'a@a.a')
assert len(aaa._store.pending_registrations) == 1, "The registration should" \
" be present"
aaa._purge_expired_registrations()
assert len(aaa._store.pending_registrations) == 1, "The registration should " \
"be still there"
aaa._purge_expired_registrations(exp_time=0)
assert len(aaa._store.pending_registrations) == 0, "The registration should " \
"have been removed"
def test_prevent_double_registration(aaa, json_db_dir):
# Create two registration requests, then validate them.
# The first should succeed, the second one fail as the account has been created.
# create first registration
aaa.register(u'user_foo', u'first_pwd', u'a@a.a')
assert len(aaa._store.pending_registrations) == 1, repr(aaa._store.pending_registrations)
for first_registration_code in aaa._store.pending_registrations:
break
# create second registration
aaa.register(u'user_foo', u'second_pwd', u'b@b.b')
assert len(aaa._store.pending_registrations) == 2, repr(aaa._store.pending_registrations)
registration_codes = list(aaa._store.pending_registrations)
if first_registration_code == registration_codes[0]:
second_registration_code = registration_codes[1]
else:
second_registration_code = registration_codes[0]
# Only the 'admin' account exists
assert len(aaa._store.users) == 1
# Run validate_registration with the first registration
aaa.validate_registration(first_registration_code)
assert 'user_foo' in aaa._store.users, "Account should have been added"
assert len(aaa._store.users) == 2
# After the first registration only one pending registration should be left
# The registration having 'a@a.a' email address should be gone
assert len(aaa._store.pending_registrations) == 1, repr(aaa._store.pending_registrations)
for pr_code, pr_data in aaa._store.pending_registrations.items():
break
assert pr_data['email_addr'] == u'b@b.b', "Incorrect registration in the datastore"
# Logging in using the first login should succeed
login = aaa.login('user_foo', 'first_pwd')
assert login == True, "Login must succed"
assert len(aaa._store.pending_registrations) == 1, repr(aaa._store.pending_registrations)
# Run validate_registration with the second registration code
# The second registration should fail as the user account exists
with pytest.raises(AAAException):
aaa.validate_registration(second_registration_code)
# test login
login = aaa.login('user_foo', 'second_pwd')
assert login == False, "Login must fail"
def test_send_password_reset_email_no_params(aaa):
with pytest.raises(AAAException):
aaa.send_password_reset_email()
def test_send_password_reset_email_incorrect_addr(aaa):
with pytest.raises(AAAException):
aaa.send_password_reset_email(email_addr=u'incorrect_addr')
def test_send_password_reset_email_incorrect_user(aaa):
with pytest.raises(AAAException):
aaa.send_password_reset_email(username=u'bogus_name')
def test_send_password_reset_email_missing_email_addr(aaa):
with pytest.raises(AAAException):
aaa.send_password_reset_email(username=u'admin')
def test_send_password_reset_email_incorrect_pair(aaa):
with pytest.raises(AuthException):
aaa.send_password_reset_email(username=u'admin', email_addr=u'incorrect_addr')
def test_send_password_reset_email_by_email_addr(aaa, json_db_dir):
aaa._store.users['admin']['email_addr'] = u'admin@localhost.local'
aaa.send_password_reset_email(email_addr=u'admin@localhost.local')
def test_send_password_reset_email_by_username(aaa, json_db_dir, mock_smtp):
aaa._store.users['admin']['email_addr'] = u'admin@localhost.local'
assert aaa._store.users['admin']['email_addr'] == u'admin@localhost.local'
assert not mock_smtp.called
aaa.send_password_reset_email(username='admin')
aaa.mailer.join()
assert mock_smtp.called
session = mock_smtp.return_value
assert session.sendmail.called
assert session.sendmail.call_args[0][1] == u'admin@localhost.local'
assert session.quit.called
bottle-cork-0.12.0/tests/test_webtest_bottle.py 0000644 0001750 0001750 00000004353 12512453671 021463 0 ustar fede fede 0000000 0000000 # Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Functional test using Json backend
#
# Requires WebTest http://webtest.pythonpaste.org/
#
# Run as: nosetests functional_test.py
#
from nose import SkipTest
from webtest import TestApp
import shutil
import os
import testutils
from cork import Cork
REDIR = 302
class Test(testutils.WebFunctional):
def create_app_instance(self):
"""create TestApp instance"""
assert self._app is None
import simple_webapp
self._bottle_app = simple_webapp.app
self._app = TestApp(self._bottle_app)
print("Test App created")
def login_as_admin(self):
"""perform log in"""
assert self._app is not None
assert 'beaker.session.id' not in self._app.cookies, "Unexpected cookie found"
self.assert_200('/login', 'Please insert your credentials')
assert 'beaker.session.id' not in self._app.cookies, "Unexpected cookie found"
self.assert_redirect('/admin', '/sorry_page')
self.assert_200('/user_is_anonymous', 'True')
assert 'beaker.session.id' not in self._app.cookies, "Unexpected cookie found"
post = {'username': 'admin', 'password': 'admin'}
self.assert_redirect('/login', '/', post=post)
assert 'beaker.session.id' in self._app.cookies, "Cookie not found"
import bottle
session = bottle.request.environ.get('beaker.session')
print("Session from func. test", repr(session))
self.assert_200('/login', 'Please insert your credentials')
p = self._app.get('/admin')
assert 'Welcome' in p.body, repr(p)
p = self._app.get('/my_role', status=200)
assert p.status_int == 200
assert p.body == 'admin', "Sta"
print("Login performed")
def test_functional_expiration(self):
self.login_as_admin()
r = self._app.get('/admin')
assert r.status == '200 OK', repr(r)
# change the cookie expiration in order to expire it
self._app.app.options['timeout'] = 0
assert self._app.get('/admin').status_int == REDIR, "The cookie should have expired"
bottle-cork-0.12.0/tests/test_scrypt.py 0000644 0001750 0001750 00000010255 12512453671 017757 0 ustar fede fede 0000000 0000000
# Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Unit testing
# Test scrypt-based password hashing
#
from pytest import raises
from time import time
import os
import shutil
from cork import Cork, JsonBackend, AuthException
import testutils
testdir = None # Test directory
aaa = None # global Cork instance
cookie_name = None # global variable to track cookie status
tmproot = testutils.pick_temp_directory()
def setup_dir():
"""Setup test directory with valid JSON files"""
global testdir
tstamp = "%f" % time()
testdir = "%s/fl_%s" % (tmproot, tstamp)
os.mkdir(testdir)
os.mkdir(testdir + '/views')
with open("%s/users.json" % testdir, 'w') as f:
f.write("""{"admin": {"email_addr": null, "desc": null, "role": "admin", "hash": "69f75f38ac3bfd6ac813794f3d8c47acc867adb10b806e8979316ddbf6113999b6052efe4ba95c0fa9f6a568bddf60e8e5572d9254dbf3d533085e9153265623", "creation_date": "2012-04-09 14:22:27.075596"}}""")
with open("%s/roles.json" % testdir, 'w') as f:
f.write("""{"special": 200, "admin": 100, "user": 50}""")
with open("%s/register.json" % testdir, 'w') as f:
f.write("""{}""")
with open("%s/views/registration_email.tpl" % testdir, 'w') as f:
f.write("""Username:{{username}} Email:{{email_addr}} Code:{{registration_code}}""")
with open("%s/views/password_reset_email.tpl" % testdir, 'w') as f:
f.write("""Username:{{username}} Email:{{email_addr}} Code:{{reset_code}}""")
print("setup done in %s" % testdir)
def setUp():
global aaa
setup_dir()
aaa = Cork(testdir, smtp_server='localhost', email_sender='test@localhost')
def teardown_dir():
global cookie_name
global testdir
if testdir:
shutil.rmtree(testdir)
testdir = None
cookie_name = None
def tearDown():
global aaa
aaa = None
teardown_dir()
def test_password_hashing_scrypt(aaa):
shash = aaa._hash('user_foo', 'bogus_pwd', algo='scrypt')
assert len(shash) == 132, "hash length should be 132 and is %d" % len(shash)
assert shash.endswith(b'='), "hash should end with '=': %r" % shash
assert aaa._verify_password('user_foo', 'bogus_pwd', shash) == True, \
"Hashing verification should succeed"
def test_password_hashing_scrypt_known_hash(aaa):
salt = b's' * 32
shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt, algo='scrypt')
assert shash == b'c3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NzeLt/2Ta8vJOVqimNpN9G1WWxN1hxlUOJDPgH+0wqPpG20XQHFHLlksDIUo2BL4P8BMLBZj7F+cq6UP6pc304LQ==', repr(shash)
def test_password_hashing_scrypt_known_hash_2(aaa):
salt = b'\0' * 32
shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt, algo='scrypt')
assert shash == b'cwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmu5jQskr2/yX13Yxmc4TYL0MIuSxwo41SVJwn/QueiDdLGkNaEsxlKL37i98YofXxs8xJJAJlC3Xj/9Nx0RNBw=='
def test_password_hashing_scrypt_known_hash_3(aaa):
salt = b'x' * 32
shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt, algo='scrypt')
assert shash == b'c3h4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4yKuT1e8lovFZnaaOctivIvYBPkLoKDXX72kf5/nRuGIgyyhiKxxKE4LVYFKFCeVNPQM5m/+LulQkWhO0aB89lA=='
def test_password_hashing_scrypt_incorrect_hash_len(aaa):
salt = b'x' * 31 # Incorrect length
with raises(AssertionError):
shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt, algo='scrypt')
def test_password_hashing_scrypt_incorrect_hash_value(aaa):
shash = aaa._hash('user_foo', 'bogus_pwd', algo='scrypt')
assert len(shash) == 132, "hash length should be 132 and is %d" % len(shash)
assert shash.endswith(b'='), "hash should end with '='"
assert aaa._verify_password('user_foo', '####', shash) == False, \
"Hashing verification should fail"
assert aaa._verify_password('###', 'bogus_pwd', shash) == False, \
"Hashing verification should fail"
def test_password_hashing_scrypt_collision(aaa):
salt = b'S' * 32
hash1 = aaa._hash('user_foo', 'bogus_pwd', salt=salt, algo='scrypt')
hash2 = aaa._hash('user_foobogus', '_pwd', salt=salt, algo='scrypt')
assert hash1 != hash2, "Hash collision"
bottle-cork-0.12.0/tests/simple_webapp_flask.py 0000755 0001750 0001750 00000012670 12512453671 021411 0 ustar fede fede 0000000 0000000 #!/usr/bin/env python
#
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Cork example web application
#
# The following users are already available:
# admin/admin, demo/demo
import flask
from beaker.middleware import SessionMiddleware
from cork import FlaskCork
import logging
import os
logging.basicConfig(format='localhost - - [%(asctime)s] %(message)s', level=logging.DEBUG)
log = logging.getLogger(__name__)
# Use users.json and roles.json in the local example_conf directory
aaa = FlaskCork('example_conf', email_sender='federico.ceratto@gmail.com', smtp_url='smtp://smtp.magnet.ie')
app = flask.Flask(__name__)
app.debug = True
app.options = {} #FIXME
from flask import jsonify
#session_opts = {
# 'session.cookie_expires': True,
# 'session.encrypt_key': 'please use a random key and keep it secret!',
# 'session.httponly': True,
# 'session.timeout': 3600 * 24, # 1 day
# 'session.type': 'cookie',
# 'session.validate_key': True,
#}
#app = SessionMiddleware(app, session_opts)
# # Bottle methods # #
def post_get(name, default=''):
v = flask.request.form.get(name, default).strip()
return str(v)
from cork import Redirect
@app.errorhandler(Redirect)
def redirect_exception_handler(e):
return flask.redirect(e.message)
@app.route('/login', methods=['POST'])
def login():
"""Authenticate users"""
username = post_get('username')
password = post_get('password')
aaa.login(username, password, success_redirect='/', fail_redirect='/login')
@app.route('/user_is_anonymous')
def user_is_anonymous():
if aaa.user_is_anonymous:
return 'True'
return 'False'
@app.route('/logout')
def logout():
aaa.logout(success_redirect='/login')
@app.route('/register', methods=['POST'])
def register():
"""Send out registration email"""
aaa.register(post_get('username'), post_get('password'), post_get('email_address'))
return 'Please check your mailbox.'
@app.route('/validate_registration/:registration_code')
def validate_registration(registration_code):
"""Validate registration, create user account"""
aaa.validate_registration(registration_code)
return 'Thanks. Go to login'
@app.route('/reset_password', methods=['POST'])
def send_password_reset_email():
"""Send out password reset email"""
aaa.send_password_reset_email(
username=post_get('username'),
email_addr=post_get('email_address')
)
return 'Please check your mailbox.'
@app.route('/change_password/:reset_code')
def change_password(reset_code):
"""Show password change form"""
return flask.render_template('password_change_form',
reset_code=reset_code)
@app.route('/change_password', methods=['POST'])
def do_change_password():
"""Change password"""
aaa.reset_password(post_get('reset_code'), post_get('password'))
return 'Thanks. Go to login'
@app.route('/')
def index():
"""Only authenticated users can see this"""
aaa.require(fail_redirect='/login')
return 'Welcome! Admin page Logout'
@app.route('/restricted_download')
def restricted_download():
"""Only authenticated users can download this file"""
aaa.require(fail_redirect='/login')
return flask.static_file('static_file', root='.')
@app.route('/my_role')
def show_current_user_role():
"""Show current user role"""
session = flask.request.environ.get('beaker.session')
print "Session from simple_webapp", repr(session)
aaa.require(fail_redirect='/login')
return aaa.current_user.role
# Admin-only pages
@app.route('/admin')
def admin():
"""Only admin users can see this"""
aaa.require(role='admin', fail_redirect='/sorry_page')
return flask.render_template('admin_page.html',
current_user=aaa.current_user,
users=aaa.list_users(),
roles=aaa.list_roles()
)
@app.route('/create_user', methods=['POST'])
def create_user():
try:
aaa.create_user(post_get('username'), post_get('role'),
post_get('password'))
return jsonify(ok=True, msg='')
except Exception, e:
return jsonify(ok=False, msg=e.message)
@app.route('/delete_user', methods=['POST'])
def delete_user():
try:
aaa.delete_user(post_get('username'))
return jsonify(ok=True, msg='')
except Exception, e:
print repr(e)
return jsonify(ok=False, msg=e.message)
@app.route('/create_role', methods=['POST'])
def create_role():
try:
aaa.create_role(post_get('role'), post_get('level'))
return jsonify(ok=True, msg='')
except Exception, e:
return jsonify(ok=False, msg=e.message)
@app.route('/delete_role', methods=['POST'])
def delete_role():
try:
aaa.delete_role(post_get('role'))
return jsonify(ok=True, msg='')
except Exception, e:
return jsonify(ok=False, msg=e.message)
# Static pages
@app.route('/login')
def login_form():
"""Serve login form"""
return flask.render_template('login_form.html')
@app.route('/sorry_page')
def sorry_page():
"""Serve sorry page"""
return '
Sorry, you are not authorized to perform this action
'
# # Web application main # #
app.secret_key = os.urandom(24) #FIXME: why
def main():
# Start the Bottle webapp
#bottle.debug(True)
app.secret_key = os.urandom(24)
app.run(debug=True, use_reloader=True)
if __name__ == "__main__":
main()
bottle-cork-0.12.0/tests/test_webtest_parallel.py 0000644 0001750 0001750 00000020041 12314374426 021756 0 ustar fede fede 0000000 0000000 # Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Functional test using Json backend
#
# Requires WebTest http://webtest.pythonpaste.org/
#
# Run as: nosetests functional_test.py
#
from nose import SkipTest
from time import time
from datetime import datetime
from webtest import TestApp
import glob
import json
import os
import shutil
import testutils
from cork import Cork
REDIR = 302
class Test(testutils.WebFunctional):
def __init__(self):
self._tmpdir = None
self._tmproot = None
self._app = None
self._starting_dir = os.getcwd()
def populate_conf_directory(self):
"""Populate a directory with valid configuration files, to be run just once
The files are not modified by each test
"""
self._tmpdir = os.path.join(self._tmproot, "cork_functional_test_source")
# only do this once, as advertised
if os.path.exists(self._tmpdir):
return
os.mkdir(self._tmpdir)
os.mkdir(self._tmpdir + "/example_conf")
cork = Cork(os.path.join(self._tmpdir, "example_conf"), initialize=True)
cork._store.roles['admin'] = 100
cork._store.roles['editor'] = 60
cork._store.roles['user'] = 50
cork._store.save_roles()
tstamp = str(datetime.utcnow())
username = password = 'admin'
cork._store.users[username] = {
'role': 'admin',
'hash': cork._hash(username, password),
'email_addr': username + '@localhost.local',
'desc': username + ' test user',
'creation_date': tstamp
}
username = password = ''
cork._store.users[username] = {
'role': 'user',
'hash': cork._hash(username, password),
'email_addr': username + '@localhost.local',
'desc': username + ' test user',
'creation_date': tstamp
}
cork._store.save_users()
def remove_temp_dir(self):
p = os.path.join(self._tmproot, 'cork_functional_test_wd')
for f in glob.glob('%s*' % p):
#shutil.rmtree(f)
pass
@classmethod
def setUpClass(cls):
print("Setup class")
def setup(self):
# create test dir and populate it using the example files
# save the directory where the unit testing has been run
if self._starting_dir is None:
self._starting_dir = os.getcwd()
# create json files to be used by Cork
self._tmproot = testutils.pick_temp_directory()
assert self._tmproot is not None
# purge the temporary test directory
self.remove_temp_dir()
self.populate_temp_dir()
self.create_app_instance()
self._app.reset()
print("Reset done")
print("Setup completed")
def populate_temp_dir(self):
"""populate the temporary test dir"""
assert self._tmproot is not None
assert self._tmpdir is None
tstamp = str(time())[5:]
self._tmpdir = os.path.join(self._tmproot, "cork_functional_test_wd_%s" % tstamp)
try:
os.mkdir(self._tmpdir)
except OSError:
# The directory is already there, purge it
print("Deleting %s" % self._tmpdir)
shutil.rmtree(self._tmpdir)
os.mkdir(self._tmpdir)
#p = os.path.join(self._tmproot, 'cork_functional_test_wd')
#for f in glob.glob('%s*' % p):
# shutil.rmtree(f)
# copy the needed files
shutil.copytree(
os.path.join(self._starting_dir, 'tests/example_conf'),
os.path.join(self._tmpdir, 'example_conf')
)
shutil.copytree(
os.path.join(self._starting_dir, 'tests/views'),
os.path.join(self._tmpdir, 'views')
)
# change to the temporary test directory
# cork relies on this being the current directory
os.chdir(self._tmpdir)
print("Test directory set up")
def create_app_instance(self):
"""create TestApp instance"""
assert self._app is None
import simple_webapp
self._bottle_app = simple_webapp.app
self._app = TestApp(self._bottle_app)
print("Test App created")
def teardown(self):
print("Doing teardown")
try:
self._app.post('/logout')
except:
pass
# drop the cookie
self._app.reset()
assert 'beaker.session.id' not in self._app.cookies, "Unexpected cookie found"
# drop the cookie
self._app.reset()
#assert self._app.get('/admin').status != '200 OK'
os.chdir(self._starting_dir)
#if self._tmproot is not None:
# testutils.purge_temp_directory(self._tmproot)
self._app.app.options['timeout'] = self._default_timeout
self._app = None
self._tmproot = None
self._tmpdir = None
print("Teardown done")
def setup(self):
# create test dir and populate it using the example files
# save the directory where the unit testing has been run
if self._starting_dir is None:
self._starting_dir = os.getcwd()
# create json files to be used by Cork
self._tmproot = testutils.pick_temp_directory()
assert self._tmproot is not None
# purge the temporary test directory
self.remove_temp_dir()
self.populate_temp_dir()
self.create_app_instance()
self._app.reset()
print("Reset done")
self._default_timeout = self._app.app.options['timeout']
print("Setup completed")
def assert_200(self, path, match):
"""Assert that a page returns 200"""
p = self._app.get(path)
assert p.status_int == 200, "Status: %d, Location: %s" % \
(p.status_int, p.location)
if match is not None:
assert match in p.body, "'%s' not found in body: '%s'" % (match, p.body)
return p
def assert_redirect(self, page, redir_page, post=None):
"""Assert that a page redirects to another one"""
# perform GET or POST
if post is None:
p = self._app.get(page, status=302)
else:
assert isinstance(post, dict)
p = self._app.post(page, post, status=302)
dest = p.location.split(':80/')[-1]
dest = "/%s" % dest
assert dest == redir_page, "%s redirects to %s instead of %s" % \
(page, dest, redir_page)
return p
def login_as_admin(self):
"""perform log in"""
assert self._app is not None
assert 'beaker.session.id' not in self._app.cookies, "Unexpected cookie found"
self.assert_200('/login', 'Please insert your credentials')
assert 'beaker.session.id' not in self._app.cookies, "Unexpected cookie found"
self.assert_redirect('/admin', '/sorry_page')
self.assert_200('/user_is_anonymous', 'True')
assert 'beaker.session.id' not in self._app.cookies, "Unexpected cookie found"
post = {'username': 'admin', 'password': 'admin'}
self.assert_redirect('/login', '/', post=post)
assert 'beaker.session.id' in self._app.cookies, "Cookie not found"
import bottle
session = bottle.request.environ.get('beaker.session')
print("Session from func. test", repr(session))
self.assert_200('/login', 'Please insert your credentials')
p = self._app.get('/admin')
assert 'Welcome' in p.body, repr(p)
p = self._app.get('/my_role', status=200)
assert p.status == '200 OK'
assert p.body == 'admin', "Sta"
print("Login performed")
def test_functional_expiration(self):
self.login_as_admin()
r = self._app.get('/admin')
assert r.status == '200 OK', repr(r)
# change the cookie expiration in order to expire it
self._app.app.options['timeout'] = 0
assert self._app.get('/admin').status_int == REDIR, "The cookie should have expired"
bottle-cork-0.12.0/tests/multiparam.py 0000644 0001750 0001750 00000000645 12453054133 017543 0 ustar fede fede 0000000 0000000
# content of conftest.py
import pytest
class A(object):
pass
@pytest.fixture
def base():
print 'INST A'
return A()
@pytest.fixture(params=["merlinux.eu", "mail.python.org"])
def smtp(request, base):
print request.param
return base
@pytest.fixture(params=['user', 'admin', None])
def auth_status(request, base):
return base
def test_nyan(smtp, auth_status):
assert smtp == auth_status
bottle-cork-0.12.0/cork/ 0000755 0001750 0001750 00000000000 12617467123 014616 5 ustar fede fede 0000000 0000000 bottle-cork-0.12.0/cork/base_backend.py 0000644 0001750 0001750 00000001360 12512453671 017546 0 ustar fede fede 0000000 0000000 # Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
"""
.. module:: backend.py
:synopsis: Base Backend.
"""
class BackendIOException(Exception):
"""Generic Backend I/O Exception"""
pass
def ni(*args, **kwargs):
raise NotImplementedError
class Backend(object):
"""Base Backend class - to be subclassed by real backends."""
save_users = ni
save_roles = ni
save_pending_registrations = ni
class Table(object):
"""Base Table class - to be subclassed by real backends."""
__len__ = ni
__contains__ = ni
__setitem__ = ni
__getitem__ = ni
__iter__ = ni
iteritems = ni
bottle-cork-0.12.0/cork/mongodb_backend.py 0000644 0001750 0001750 00000012440 12520166131 020251 0 ustar fede fede 0000000 0000000 # Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
"""
.. module:: mongodb_backend
:synopsis: MongoDB storage backend.
"""
from logging import getLogger
log = getLogger(__name__)
from .base_backend import Backend, Table
try:
import pymongo
is_pymongo_2 = (pymongo.version_tuple[0] == 2)
except ImportError: # pragma: no cover
pass
class MongoTable(Table):
"""Abstract MongoDB Table.
Allow dictionary-like access.
"""
def __init__(self, name, key_name, collection):
self._name = name
self._key_name = key_name
self._coll = collection
def create_index(self):
"""Create collection index."""
self._coll.create_index(
self._key_name,
drop_dups=True,
unique=True,
)
def __len__(self):
return self._coll.count()
def __contains__(self, value):
r = self._coll.find_one({self._key_name: value})
return r is not None
def __iter__(self):
"""Iter on dictionary keys"""
if is_pymongo_2:
r = self._coll.find(fields=[self._key_name,])
else:
r = self._coll.find(projection=[self._key_name,])
return (i[self._key_name] for i in r)
def iteritems(self):
"""Iter on dictionary items.
:returns: generator of (key, value) tuples
"""
r = self._coll.find()
for i in r:
d = i.copy()
d.pop(self._key_name)
d.pop('_id')
yield (i[self._key_name], d)
def pop(self, key_val):
"""Remove a dictionary item"""
r = self[key_val]
self._coll.remove({self._key_name: key_val}, w=1)
return r
class MongoSingleValueTable(MongoTable):
"""MongoDB table accessible as a simple key -> value dictionary.
Used to store roles.
"""
# Values are stored in a MongoDB "column" named "val"
def __init__(self, *args, **kw):
super(MongoSingleValueTable, self).__init__(*args, **kw)
def __setitem__(self, key_val, data):
assert not isinstance(data, dict)
spec = {self._key_name: key_val}
data = {self._key_name: key_val, 'val': data}
if is_pymongo_2:
self._coll.update(spec, {'$set': data}, upsert=True, w=1)
else:
self._coll.update_one(spec, {'$set': data}, upsert=True)
def __getitem__(self, key_val):
r = self._coll.find_one({self._key_name: key_val})
if r is None:
raise KeyError(key_val)
return r['val']
class MongoMutableDict(dict):
"""Represent an item from a Table. Acts as a dictionary.
"""
def __init__(self, parent, root_key, d):
"""Create a MongoMutableDict instance.
:param parent: Table instance
:type parent: :class:`MongoTable`
"""
super(MongoMutableDict, self).__init__(d)
self._parent = parent
self._root_key = root_key
def __setitem__(self, k, v):
super(MongoMutableDict, self).__setitem__(k, v)
spec = {self._parent._key_name: self._root_key}
if is_pymongo_2:
r = self._parent._coll.update(spec, {'$set': {k: v}}, upsert=True)
else:
r = self._parent._coll.update_one(spec, {'$set': {k: v}}, upsert=True)
class MongoMultiValueTable(MongoTable):
"""MongoDB table accessible as a dictionary.
"""
def __init__(self, *args, **kw):
super(MongoMultiValueTable, self).__init__(*args, **kw)
def __setitem__(self, key_val, data):
assert isinstance(data, dict)
key_name = self._key_name
if key_name in data:
assert data[key_name] == key_val
else:
data[key_name] = key_val
spec = {key_name: key_val}
if u'_id' in data:
del(data[u'_id'])
if is_pymongo_2:
self._coll.update(spec, {'$set': data}, upsert=True, w=1)
else:
self._coll.update_one(spec, {'$set': data}, upsert=True)
def __getitem__(self, key_val):
r = self._coll.find_one({self._key_name: key_val})
if r is None:
raise KeyError(key_val)
return MongoMutableDict(self, key_val, r)
class MongoDBBackend(Backend):
def __init__(self, db_name='cork', hostname='localhost', port=27017, initialize=False, username=None, password=None):
"""Initialize MongoDB Backend"""
connection = pymongo.MongoClient(host=hostname, port=port)
db = connection[db_name]
if username and password:
db.authenticate(username, password)
self.users = MongoMultiValueTable('users', 'login', db.users)
self.pending_registrations = MongoMultiValueTable(
'pending_registrations',
'pending_registration',
db.pending_registrations
)
self.roles = MongoSingleValueTable('roles', 'role', db.roles)
if initialize:
self._initialize_storage()
def _initialize_storage(self):
"""Create MongoDB indexes."""
for c in (self.users, self.roles, self.pending_registrations):
c.create_index()
def save_users(self):
pass
def save_roles(self):
pass
def save_pending_registrations(self):
pass
bottle-cork-0.12.0/cork/json_backend.py 0000644 0001750 0001750 00000010536 12517131104 017577 0 ustar fede fede 0000000 0000000 # Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
"""
.. module:: json_backend
:synopsis: JSON file-based storage backend.
"""
from logging import getLogger
import os
import shutil
import sys
try:
import json
except ImportError: # pragma: no cover
import simplejson as json
from .base_backend import BackendIOException
is_py3 = (sys.version_info.major == 3)
log = getLogger(__name__)
try:
dict.iteritems
py23dict = dict
except AttributeError:
class py23dict(dict):
iteritems = dict.items
class BytesEncoder(json.JSONEncoder):
def default(self, obj):
if is_py3 and isinstance(obj, bytes):
return obj.decode()
return json.JSONEncoder.default(self, obj)
class JsonBackend(object):
"""JSON file-based storage backend."""
def __init__(self, directory, users_fname='users',
roles_fname='roles', pending_reg_fname='register', initialize=False):
"""Data storage class. Handles JSON files
:param users_fname: users file name (without .json)
:type users_fname: str.
:param roles_fname: roles file name (without .json)
:type roles_fname: str.
:param pending_reg_fname: pending registrations file name (without .json)
:type pending_reg_fname: str.
:param initialize: create empty JSON files (defaults to False)
:type initialize: bool.
"""
assert directory, "Directory name must be valid"
self._directory = directory
self.users = py23dict()
self._users_fname = users_fname
self.roles = py23dict()
self._roles_fname = roles_fname
self._mtimes = py23dict()
self._pending_reg_fname = pending_reg_fname
self.pending_registrations = py23dict()
if initialize:
self._initialize_storage()
self._refresh() # load users and roles
def _initialize_storage(self):
"""Create empty JSON files"""
self._savejson(self._users_fname, {})
self._savejson(self._roles_fname, {})
self._savejson(self._pending_reg_fname, {})
def _refresh(self):
"""Load users and roles from JSON files, if needed"""
self._loadjson(self._users_fname, self.users)
self._loadjson(self._roles_fname, self.roles)
self._loadjson(self._pending_reg_fname, self.pending_registrations)
def _loadjson(self, fname, dest):
"""Load JSON file located under self._directory, if needed
:param fname: short file name (without path and .json)
:type fname: str.
:param dest: destination
:type dest: dict
"""
try:
fname = "%s/%s.json" % (self._directory, fname)
mtime = os.stat(fname).st_mtime
if self._mtimes.get(fname, 0) == mtime:
# no need to reload the file: the mtime has not been changed
return
with open(fname) as f:
json_data = f.read()
except Exception as e:
raise BackendIOException("Unable to read json file %s: %s" % (fname, e))
try:
json_obj = json.loads(json_data)
dest.clear()
dest.update(json_obj)
self._mtimes[fname] = os.stat(fname).st_mtime
except Exception as e:
raise BackendIOException("Unable to parse JSON data from %s: %s" \
% (fname, e))
def _savejson(self, fname, obj):
"""Save obj in JSON format in a file in self._directory"""
fname = "%s/%s.json" % (self._directory, fname)
try:
with open("%s.tmp" % fname, 'w') as f:
json.dump(obj, f, cls=BytesEncoder)
f.flush()
shutil.move("%s.tmp" % fname, fname)
except Exception as e:
raise BackendIOException("Unable to save JSON file %s: %s" \
% (fname, e))
def save_users(self):
"""Save users in a JSON file"""
self._savejson(self._users_fname, self.users)
def save_roles(self):
"""Save roles in a JSON file"""
self._savejson(self._roles_fname, self.roles)
def save_pending_registrations(self):
"""Save pending registrations in a JSON file"""
self._savejson(self._pending_reg_fname, self.pending_registrations)
bottle-cork-0.12.0/cork/cork.py 0000644 0001750 0001750 00000101626 12617465100 016125 0 ustar fede fede 0000000 0000000 #!/usr/bin/env python
#
# Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
#
# This package is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This package is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from base64 import b64encode, b64decode
from datetime import datetime, timedelta
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from logging import getLogger
from smtplib import SMTP, SMTP_SSL
from threading import Thread
from time import time
import bottle
import hashlib
import os
import re
import sys
import uuid
if sys.version_info[0:3] < (2, 7, 8):
print("Python >= 2.7.8 is required")
sys.exit(1)
try:
import scrypt
scrypt_available = True
except ImportError: # pragma: no cover
scrypt_available = False
try:
basestring
except NameError:
basestring = str
from .backends import JsonBackend
is_py3 = (sys.version_info.major == 3)
log = getLogger(__name__)
class AAAException(Exception):
"""Generic Authentication/Authorization Exception"""
pass
class AuthException(AAAException):
"""Authentication Exception: incorrect username/password pair"""
pass
class BaseCork(object):
"""Abstract class"""
def __init__(self, directory=None, backend=None, email_sender=None,
initialize=False, session_domain=None, smtp_server=None,
smtp_url='localhost', session_key_name=None):
"""Auth/Authorization/Accounting class
:param directory: configuration directory
:type directory: str.
:param users_fname: users filename (without .json), defaults to 'users'
:type users_fname: str.
:param roles_fname: roles filename (without .json), defaults to 'roles'
:type roles_fname: str.
"""
if smtp_server:
smtp_url = smtp_server
self.mailer = Mailer(email_sender, smtp_url)
self.password_reset_timeout = 3600 * 24
self.session_domain = session_domain
self.session_key_name = session_key_name or 'beaker.session'
self.preferred_hashing_algorithm = 'PBKDF2'
# Setup JsonBackend by default for backward compatibility.
if backend is None:
self._store = JsonBackend(
directory, users_fname='users',
roles_fname='roles', pending_reg_fname='register',
initialize=initialize
)
else:
self._store = backend
def login(self, username, password, success_redirect=None,
fail_redirect=None):
"""Check login credentials for an existing user.
Optionally redirect the user to another page (typically /login)
:param username: username
:type username: str or unicode.
:param password: cleartext password
:type password: str.or unicode
:param success_redirect: redirect authorized users (optional)
:type success_redirect: str.
:param fail_redirect: redirect unauthorized users (optional)
:type fail_redirect: str.
:returns: True for successful logins, else False
"""
# assert isinstance(username, type(u'')), "username must be a string"
# assert isinstance(password, type(u'')), "password must be a string"
if username in self._store.users:
salted_hash = self._store.users[username]['hash']
if hasattr(salted_hash, 'encode'):
salted_hash = salted_hash.encode('ascii')
authenticated = self._verify_password(
username,
password,
salted_hash,
)
if authenticated:
# Setup session data
self._setup_cookie(username)
self._store.users[username]['last_login'] = str(
datetime.utcnow())
self._store.save_users()
if success_redirect:
self._redirect(success_redirect)
return True
if fail_redirect:
self._redirect(fail_redirect)
return False
def logout(self, success_redirect='/login', fail_redirect='/login'):
"""Log the user out, remove cookie
:param success_redirect: redirect the user after logging out
:type success_redirect: str.
:param fail_redirect: redirect the user if it is not logged in
:type fail_redirect: str.
"""
try:
session = self._beaker_session
session.delete()
except Exception as e:
log.debug("Exception %s while logging out." % repr(e))
self._redirect(fail_redirect)
self._redirect(success_redirect)
def require(self, username=None, role=None, fixed_role=False,
fail_redirect=None):
"""Ensure the user is logged in has the required role (or higher).
Optionally redirect the user to another page (typically /login)
If both `username` and `role` are specified, both conditions need to be
satisfied.
If none is specified, any authenticated user will be authorized.
By default, any role with higher level than `role` will be authorized;
set fixed_role=True to prevent this.
:param username: username (optional)
:type username: str.
:param role: role
:type role: str.
:param fixed_role: require user role to match `role` strictly
:type fixed_role: bool.
:param redirect: redirect unauthorized users (optional)
:type redirect: str.
"""
# Parameter validation
if username is not None:
if username not in self._store.users:
raise AAAException("Nonexistent user")
if fixed_role and role is None:
raise AAAException(
"""A role must be specified if fixed_role has been set""")
if role is not None and role not in self._store.roles:
raise AAAException("Role not found")
# Authentication
try:
cu = self.current_user
except AAAException:
if fail_redirect is None:
raise AuthException("Unauthenticated user")
else:
self._redirect(fail_redirect)
# Authorization
if cu.role not in self._store.roles:
raise AAAException("Role not found for the current user")
if username is not None:
# A specific user is required
if username == self.current_user.username:
return
if fail_redirect is None:
raise AuthException("Unauthorized access: incorrect"
" username")
self._redirect(fail_redirect)
if fixed_role:
# A specific role is required
if role == self.current_user.role:
return
if fail_redirect is None:
raise AuthException("Unauthorized access: incorrect role")
self._redirect(fail_redirect)
if role is not None:
# Any role with higher level is allowed
current_lvl = self._store.roles[self.current_user.role]
threshold_lvl = self._store.roles[role]
if current_lvl >= threshold_lvl:
return
if fail_redirect is None:
raise AuthException("Unauthorized access: ")
self._redirect(fail_redirect)
return # success
def create_role(self, role, level):
"""Create a new role.
:param role: role name
:type role: str.
:param level: role level (0=lowest, 100=admin)
:type level: int.
:raises: AuthException on errors
"""
if self.current_user.level < 100:
raise AuthException("The current user is not authorized to ")
if role in self._store.roles:
raise AAAException("The role is already existing")
try:
int(level)
except ValueError:
raise AAAException("The level must be numeric.")
self._store.roles[role] = level
self._store.save_roles()
def delete_role(self, role):
"""Deleta a role.
:param role: role name
:type role: str.
:raises: AuthException on errors
"""
if self.current_user.level < 100:
raise AuthException("The current user is not authorized to ")
if role not in self._store.roles:
raise AAAException("Nonexistent role.")
self._store.roles.pop(role)
self._store.save_roles()
def list_roles(self):
"""List roles.
:returns: (role, role_level) generator (sorted by role)
"""
for role in sorted(self._store.roles):
yield (role, self._store.roles[role])
def create_user(self, username, role, password, email_addr=None,
description=None):
"""Create a new user account.
This method is available to users with level>=100
:param username: username
:type username: str.
:param role: role
:type role: str.
:param password: cleartext password
:type password: str.
:param email_addr: email address (optional)
:type email_addr: str.
:param description: description (free form)
:type description: str.
:raises: AuthException on errors
"""
assert username, "Username must be provided."
if self.current_user.level < 100:
raise AuthException("The current user is not authorized"
" to create users.")
if username in self._store.users:
raise AAAException("User is already existing.")
if role not in self._store.roles:
raise AAAException("Nonexistent user role.")
tstamp = str(datetime.utcnow())
h = self._hash(username, password)
h = h.decode('ascii')
self._store.users[username] = {
'role': role,
'hash': h,
'email_addr': email_addr,
'desc': description,
'creation_date': tstamp,
'last_login': tstamp
}
self._store.save_users()
def delete_user(self, username):
"""Delete a user account.
This method is available to users with level>=100
:param username: username
:type username: str.
:raises: Exceptions on errors
"""
if self.current_user.level < 100:
raise AuthException("The current user is not authorized to ")
if username not in self._store.users:
raise AAAException("Nonexistent user.")
self.user(username).delete()
def list_users(self):
"""List users.
:return: (username, role, email_addr, description) generator (sorted by
username)
"""
for un in sorted(self._store.users):
d = self._store.users[un]
yield (un, d['role'], d['email_addr'], d['desc'])
@property
def current_user(self):
"""Current autenticated user
:returns: User() instance, if authenticated
:raises: AuthException otherwise
"""
session = self._beaker_session
username = session.get('username', None)
if username is None:
raise AuthException("Unauthenticated user")
if username is not None and username in self._store.users:
return User(username, self, session=session)
raise AuthException("Unknown user: %s" % username)
@property
def user_is_anonymous(self):
"""Check if the current user is anonymous.
:returns: True if the user is anonymous, False otherwise
:raises: AuthException if the session username is unknown
"""
try:
username = self._beaker_session['username']
except KeyError:
return True
if username not in self._store.users:
raise AuthException("Unknown user: %s" % username)
return False
def user(self, username):
"""Existing user
:returns: User() instance if the user exist, None otherwise
"""
if username is not None and username in self._store.users:
return User(username, self)
return None
def register(self, username, password, email_addr, role='user',
max_level=50, subject="Signup confirmation",
email_template='views/registration_email.tpl',
description=None, **kwargs):
"""Register a new user account. An email with a registration validation
is sent to the user.
WARNING: this method is available to unauthenticated users
:param username: username
:type username: str.
:param password: cleartext password
:type password: str.
:param role: role (optional), defaults to 'user'
:type role: str.
:param max_level: maximum role level (optional), defaults to 50
:type max_level: int.
:param email_addr: email address
:type email_addr: str.
:param subject: email subject
:type subject: str.
:param email_template: email template filename
:type email_template: str.
:param description: description (free form)
:type description: str.
:raises: AssertError or AAAException on errors
"""
assert username, "Username must be provided."
assert password, "A password must be provided."
assert email_addr, "An email address must be provided."
if username in self._store.users:
raise AAAException("User is already existing.")
if role not in self._store.roles:
raise AAAException("Nonexistent role")
if self._store.roles[role] > max_level:
raise AAAException("Unauthorized role")
registration_code = uuid.uuid4().hex
creation_date = str(datetime.utcnow())
# send registration email
email_text = bottle.template(
email_template,
username=username,
email_addr=email_addr,
role=role,
creation_date=creation_date,
registration_code=registration_code,
**kwargs
)
self.mailer.send_email(email_addr, subject, email_text)
# store pending registration
h = self._hash(username, password)
h = h.decode('ascii')
self._store.pending_registrations[registration_code] = {
'username': username,
'role': role,
'hash': h,
'email_addr': email_addr,
'desc': description,
'creation_date': creation_date,
}
self._store.save_pending_registrations()
def validate_registration(self, registration_code):
"""Validate pending account registration, create a new account if
successful.
:param registration_code: registration code
:type registration_code: str.
"""
try:
data = self._store.pending_registrations.pop(registration_code)
except KeyError:
raise AuthException("Invalid registration code.")
username = data['username']
if username in self._store.users:
raise AAAException("User is already existing.")
# the user data is moved from pending_registrations to _users
self._store.users[username] = {
'role': data['role'],
'hash': data['hash'],
'email_addr': data['email_addr'],
'desc': data['desc'],
'creation_date': data['creation_date'],
'last_login': str(datetime.utcnow())
}
self._store.save_users()
def send_password_reset_email(self, username=None, email_addr=None,
subject="Password reset confirmation",
email_template='views/password_reset_email',
**kwargs):
"""Email the user with a link to reset his/her password
If only one parameter is passed, fetch the other from the users
database. If both are passed they will be matched against the users
database as a security check.
:param username: username
:type username: str.
:param email_addr: email address
:type email_addr: str.
:param subject: email subject
:type subject: str.
:param email_template: email template filename
:type email_template: str.
:raises: AAAException on missing username or email_addr,
AuthException on incorrect username/email_addr pair
"""
if not username:
if not email_addr:
raise AAAException("At least `username` or `email_addr` must"
" be specified.")
# only email_addr is specified: fetch the username
for k, v in self._store.users.iteritems():
if v['email_addr'] == email_addr:
username = k
break
else:
raise AAAException("Email address not found.")
else: # username is provided
if username not in self._store.users:
raise AAAException("Nonexistent user.")
if not email_addr:
email_addr = self._store.users[username].get('email_addr', None)
if not email_addr:
raise AAAException("Email address not available.")
else:
# both username and email_addr are provided: check them
stored_email_addr = self._store.users[username]['email_addr']
if email_addr != stored_email_addr:
raise AuthException(
"Username/email address pair not found.")
# generate a reset_code token
reset_code = self._reset_code(username, email_addr)
# send reset email
email_text = bottle.template(
email_template,
username=username,
email_addr=email_addr,
reset_code=reset_code,
**kwargs
)
self.mailer.send_email(email_addr, subject, email_text)
def reset_password(self, reset_code, password):
"""Validate reset_code and update the account password
The username is extracted from the reset_code token
:param reset_code: reset token
:type reset_code: str.
:param password: new password
:type password: str.
:raises: AuthException for invalid reset tokens, AAAException
"""
try:
reset_code = b64decode(reset_code).decode()
username, email_addr, tstamp, h = reset_code.split(':', 3)
tstamp = int(tstamp)
assert isinstance(username, type(u''))
assert isinstance(email_addr, type(u''))
if not isinstance(h, type(b'')):
h = h.encode('utf-8')
except (TypeError, ValueError):
raise AuthException("Invalid reset code.")
if time() - tstamp > self.password_reset_timeout:
raise AuthException("Expired reset code.")
assert isinstance(h, type(b''))
if not self._verify_password(username, email_addr, h):
raise AuthException("Invalid reset code.")
user = self.user(username)
if user is None:
raise AAAException("Nonexistent user.")
user.update(pwd=password)
def make_auth_decorator(self, username=None, role=None, fixed_role=False,
fail_redirect='/login'):
'''
Create a decorator to be used for authentication and authorization
:param username: A resource can be protected for a specific user
:param role: Minimum role level required for authorization
:param fixed_role: Only this role gets authorized
:param fail_redirect: The URL to redirect to if a login is required.
'''
session_manager = self
def auth_require(username=username, role=role, fixed_role=fixed_role,
fail_redirect=fail_redirect):
def decorator(func):
import functools
@functools.wraps(func)
def wrapper(*a, **ka):
session_manager.require(
username=username, role=role, fixed_role=fixed_role,
fail_redirect=fail_redirect)
return func(*a, **ka)
return wrapper
return decorator
return(auth_require)
## Private methods
def _setup_cookie(self, username):
"""Setup cookie for a user that just logged in"""
session = self._beaker_session
session['username'] = username
if self.session_domain is not None:
session.domain = self.session_domain
self._save_session()
def _hash(self, username, pwd, salt=None, algo=None):
"""Hash username and password, generating salt value if required
"""
if algo is None:
algo = self.preferred_hashing_algorithm
if algo == 'PBKDF2':
return self._hash_pbkdf2(username, pwd, salt=salt)
if algo == 'scrypt':
return self._hash_scrypt(username, pwd, salt=salt)
raise RuntimeError("Unknown hashing algorithm requested: %s" % algo)
@staticmethod
def _hash_scrypt(username, pwd, salt=None):
"""Hash username and password, generating salt value if required
Use scrypt.
:returns: base-64 encoded str.
"""
if not scrypt_available:
raise Exception("scrypt.hash required."
" Please install the scrypt library.")
if salt is None:
salt = os.urandom(32)
assert len(salt) == 32, "Incorrect salt length"
cleartext = "%s\0%s" % (username, pwd)
h = scrypt.hash(cleartext, salt)
# 's' for scrypt
hashed = b's' + salt + h
return b64encode(hashed)
@staticmethod
def _hash_pbkdf2(username, pwd, salt=None):
"""Hash username and password, generating salt value if required
Use PBKDF2 from Beaker
:returns: base-64 encoded str.
"""
if salt is None:
salt = os.urandom(32)
assert isinstance(salt, bytes)
assert len(salt) == 32, "Incorrect salt length"
username = username.encode('utf-8')
assert isinstance(username, bytes)
pwd = pwd.encode('utf-8')
assert isinstance(pwd, bytes)
cleartext = username + b'\0' + pwd
h = hashlib.pbkdf2_hmac('sha1', cleartext, salt, 10, dklen=32)
# 'p' for PBKDF2
hashed = b'p' + salt + h
return b64encode(hashed)
def _verify_password(self, username, pwd, salted_hash):
"""Verity username/password pair against a salted hash
:returns: bool
"""
assert isinstance(salted_hash, type(b''))
decoded = b64decode(salted_hash)
hash_type = decoded[0]
if isinstance(hash_type, int):
hash_type = chr(hash_type)
salt = decoded[1:33]
if hash_type == 'p': # PBKDF2
h = self._hash_pbkdf2(username, pwd, salt)
return salted_hash == h
if hash_type == 's': # scrypt
h = self._hash_scrypt(username, pwd, salt)
return salted_hash == h
raise RuntimeError("Unknown hashing algorithm in hash: %r" % decoded)
def _purge_expired_registrations(self, exp_time=96):
"""Purge expired registration requests.
:param exp_time: expiration time (hours)
:type exp_time: float.
"""
pending = self._store.pending_registrations.items()
if is_py3:
pending = list(pending)
for uuid_code, data in pending:
creation = datetime.strptime(data['creation_date'],
"%Y-%m-%d %H:%M:%S.%f")
now = datetime.utcnow()
maxdelta = timedelta(hours=exp_time)
if now - creation > maxdelta:
self._store.pending_registrations.pop(uuid_code)
def _reset_code(self, username, email_addr):
"""generate a reset_code token
:param username: username
:type username: str.
:param email_addr: email address
:type email_addr: str.
:returns: Base-64 encoded token
"""
h = self._hash(username, email_addr)
t = "%d" % time()
t = t.encode('utf-8')
reset_code = b':'.join((username.encode('utf-8'),
email_addr.encode('utf-8'), t, h))
return b64encode(reset_code)
class User(object):
def __init__(self, username, cork_obj, session=None):
"""Represent an authenticated user, exposing useful attributes:
username, role, level, description, email_addr, session_creation_time,
session_accessed_time, session_id. The session-related attributes are
available for the current user only.
:param username: username
:type username: str.
:param cork_obj: instance of :class:`Cork`
"""
self._cork = cork_obj
assert username in self._cork._store.users, "Unknown user"
self.username = username
user_data = self._cork._store.users[username]
self.role = user_data['role']
self.description = user_data['desc']
self.email_addr = user_data['email_addr']
self.level = self._cork._store.roles[self.role]
if session is not None:
try:
self.session_creation_time = session['_creation_time']
self.session_accessed_time = session['_accessed_time']
self.session_id = session['_id']
except:
pass
def update(self, role=None, pwd=None, email_addr=None):
"""Update an user account data
:param role: change user role, if specified
:type role: str.
:param pwd: change user password, if specified
:type pwd: str.
:param email_addr: change user email address, if specified
:type email_addr: str.
:raises: AAAException on nonexistent user or role.
"""
username = self.username
if username not in self._cork._store.users:
raise AAAException("User does not exist.")
if role is not None:
if role not in self._cork._store.roles:
raise AAAException("Nonexistent role.")
self._cork._store.users[username]['role'] = role
if pwd is not None:
self._cork._store.users[username]['hash'] = self._cork._hash(
username, pwd).decode()
if email_addr is not None:
self._cork._store.users[username]['email_addr'] = email_addr
self._cork._store.save_users()
def delete(self):
"""Delete user account
:raises: AAAException on nonexistent user.
"""
try:
self._cork._store.users.pop(self.username)
except KeyError:
raise AAAException("Nonexistent user.")
self._cork._store.save_users()
class Redirect(Exception):
pass
def raise_redirect(path):
raise Redirect(path)
class Cork(BaseCork):
@staticmethod
def _redirect(location):
bottle.redirect(location)
@property
def _beaker_session(self):
"""Get session"""
return bottle.request.environ.get(self.session_key_name)
def _save_session(self):
self._beaker_session.save()
class FlaskCork(BaseCork):
@staticmethod
def _redirect(location):
raise_redirect(location)
@property
def _beaker_session(self):
"""Get session"""
import flask
return flask.session
def _save_session(self):
pass
class Mailer(object):
def __init__(self, sender, smtp_url, join_timeout=5, use_threads=True):
"""Send emails asyncronously
:param sender: Sender email address
:type sender: str.
:param smtp_server: SMTP server
:type smtp_server: str.
"""
self.sender = sender
self.join_timeout = join_timeout
self.use_threads = use_threads
self._threads = []
self._conf = self._parse_smtp_url(smtp_url)
def _parse_smtp_url(self, url):
"""Parse SMTP URL"""
match = re.match(r"""
( # Optional protocol
(?Psmtp|starttls|ssl) # Protocol name
://
)?
( # Optional user:pass@
(?P[^:]*) # Match every char except ':'
(: (?P.*) )? @ # Optional :pass
)?
(?P # Required FQDN on IP address
()| # Empty string
( # FQDN
[a-zA-Z_\-] # First character cannot be a number
[a-zA-Z0-9_\-\.]{,254}
)
|( # IPv4
([0-9]{1,3}\.){3}
[0-9]{1,3}
)
|( # IPv6
\[ # Square brackets
([0-9a-f]{,4}:){1,8}
[0-9a-f]{,4}
\]
)
)
( # Optional :port
:
(?P[0-9]{,5}) # Up to 5-digits port
)?
[/]?
$
""", url, re.VERBOSE)
if not match:
raise RuntimeError("SMTP URL seems incorrect")
d = match.groupdict()
if d['proto'] is None:
d['proto'] = 'smtp'
if d['port'] is None:
d['port'] = 25
else:
d['port'] = int(d['port'])
if not 0 < d['port'] < 65536:
raise RuntimeError("Incorrect SMTP port")
return d
def send_email(self, email_addr, subject, email_text):
"""Send an email
:param email_addr: email address
:type email_addr: str.
:param subject: subject
:type subject: str.
:param email_text: email text
:type email_text: str.
:raises: AAAException if smtp_server and/or sender are not set
"""
if not (self._conf['fqdn'] and self.sender):
raise AAAException("SMTP server or sender not set")
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = self.sender
msg['To'] = email_addr
if isinstance(email_text, bytes):
email_text = email_text.encode('utf-8')
part = MIMEText(email_text, 'html')
msg.attach(part)
msg = msg.as_string()
log.debug("Sending email using %s" % self._conf['fqdn'])
if self.use_threads:
thread = Thread(target=self._send, args=(email_addr, msg))
thread.start()
self._threads.append(thread)
else:
self._send(email_addr, msg)
def _send(self, email_addr, msg):
"""Deliver an email using SMTP
:param email_addr: recipient
:type email_addr: str.
:param msg: email text
:type msg: str.
"""
proto = self._conf['proto']
assert proto in ('smtp', 'starttls', 'ssl'), \
"Incorrect protocol: %s" % proto
try:
if proto == 'ssl':
log.debug("Setting up SSL")
session = SMTP_SSL(self._conf['fqdn'], self._conf['port'])
else:
session = SMTP(self._conf['fqdn'], self._conf['port'])
if proto == 'starttls':
log.debug('Sending EHLO and STARTTLS')
session.ehlo()
session.starttls()
session.ehlo()
if self._conf['user'] is not None:
log.debug('Performing login')
session.login(self._conf['user'], self._conf['pass'])
log.debug('Sending')
session.sendmail(self.sender, email_addr, msg)
session.quit()
log.info('Email sent')
except Exception as e: # pragma: no cover
log.error("Error sending email: %s" % e, exc_info=True)
def join(self):
"""Flush email queue by waiting the completion of the existing threads
:returns: None
"""
return [t.join(self.join_timeout) for t in self._threads]
def __del__(self):
"""Class destructor: wait for threads to terminate within a timeout"""
try:
self.join()
except TypeError:
pass
bottle-cork-0.12.0/cork/sqlite_backend.py 0000644 0001750 0001750 00000016666 12552271756 020162 0 ustar fede fede 0000000 0000000 # Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
"""
.. module:: sqlite_backend
:synopsis: SQLite storage backend.
"""
from . import base_backend
from logging import getLogger
log = getLogger(__name__)
class SqlRowProxy(dict):
def __init__(self, table, key, row):
li = ((k, v) for (k, ktype), v in zip(table._columns[1:], row[1:]))
dict.__init__(self, li)
self._table = table
self._key = key
def __setitem__(self, key, value):
dict.__setitem__(self, key, value)
self._table[self._key] = self
class Table(base_backend.Table):
"""Provides dictionary-like access to an SQL table."""
def __init__(self, backend, table_name):
self._backend = backend
self._engine = backend.connection
self._table_name = table_name
self._column_names = [n for n, t in self._columns]
self._key_col_num = 0
self._key_col_name = self._column_names[self._key_col_num]
self._key_col = self._column_names[self._key_col_num]
def _row_to_value(self, key, row):
assert isinstance(row, tuple)
row_key = row[self._key_col_num]
row_value = SqlRowProxy(self, key, row)
return row_key, row_value
def __len__(self):
query = "SELECT count() FROM %s" % self._table_name
ret = self._backend.run_query(query)
return ret.fetchone()[0]
def __contains__(self, key):
#FIXME: count()
query = "SELECT * FROM %s WHERE %s='%s'" % \
(self._table_name, self._key_col, key)
row = self._backend.fetch_one(query)
return row is not None
def __setitem__(self, key, value):
"""Create or update a row"""
assert isinstance(value, dict)
v, cn = set(value), set(self._column_names[1:])
assert not v - cn, repr(v - cn)
assert not cn - v, repr(cn - v)
assert set(value) == set(self._column_names[1:]), "%s %s" % \
(repr(set(value)), repr(set(self._column_names[1:])))
col_values = [key] + [value[k] for k in self._column_names[1:]]
col_names = ', '.join(self._column_names)
question_marks = ', '.join('?' for x in col_values)
query = "INSERT OR REPLACE INTO %s (%s) VALUES (%s)" % \
(self._table_name, col_names, question_marks)
ret = self._backend.run_query_using_conversion(query, col_values)
def __getitem__(self, key):
query = "SELECT * FROM %s WHERE %s='%s'" % \
(self._table_name, self._key_col, key)
row = self._backend.fetch_one(query)
if row is None:
raise KeyError(key)
return self._row_to_value(key, row)[1]
#return dict(zip(self._column_names, row))
def __iter__(self):
"""Iterate over table index key values"""
query = "SELECT %s FROM %s" % (self._key_col, self._table_name)
result = self._backend.run_query(query)
for row in result:
yield row[0]
def iteritems(self):
"""Iterate over table rows"""
query = "SELECT * FROM %s" % self._table_name
result = self._backend.run_query(query)
for row in result:
d = dict(zip(self._column_names, row))
d.pop(self._key_col)
yield (self._key_col, d)
def pop(self, key):
d = self.__getitem__(key)
query = "DELETE FROM %s WHERE %s='%s'" % \
(self._table_name, self._key_col, key)
self._backend.fetch_one(query)
#FIXME: check deletion
return d
def insert(self, d):
raise NotImplementedError
def empty_table(self):
raise NotImplementedError
def create_table(self):
"""Issue table creation"""
cc = []
for col_name, col_type in self._columns:
if col_type == int:
col_type = 'INTEGER'
elif col_type == str:
col_type = 'TEXT'
if col_name == self._key_col:
extras = 'PRIMARY KEY ASC'
else:
extras = ''
cc.append("%s %s %s" % (col_name, col_type, extras))
cc = ','.join(cc)
query = "CREATE TABLE %s (%s)" % (self._table_name, cc)
self._backend.run_query(query)
class SingleValueTable(Table):
def __init__(self, *args):
super(SingleValueTable, self).__init__(*args)
self._value_col = self._column_names[1]
def __setitem__(self, key, value):
"""Create or update a row"""
assert not isinstance(value, dict)
query = "INSERT OR REPLACE INTO %s (%s, %s) VALUES (?, ?)" % \
(self._table_name, self._key_col, self._value_col)
col_values = (key, value)
ret = self._backend.run_query_using_conversion(query, col_values)
def __getitem__(self, key):
query = "SELECT %s FROM %s WHERE %s='%s'" % \
(self._value_col, self._table_name, self._key_col, key)
row = self._backend.fetch_one(query)
if row is None:
raise KeyError(key)
return row[0]
class UsersTable(Table):
def __init__(self, *args, **kwargs):
self._columns = (
('username', str),
('role', str),
('hash', str),
('email_addr', str),
('desc', str),
('creation_date', str),
('last_login', str)
)
super(UsersTable, self).__init__(*args, **kwargs)
class RolesTable(SingleValueTable):
def __init__(self, *args, **kwargs):
self._columns = (
('role', str),
('level', int)
)
super(RolesTable, self).__init__(*args, **kwargs)
class PendingRegistrationsTable(Table):
def __init__(self, *args, **kwargs):
self._columns = (
('code', str),
('username', str),
('role', str),
('hash', str),
('email_addr', str),
('desc', str),
('creation_date', str)
)
super(PendingRegistrationsTable, self).__init__(*args, **kwargs)
class SQLiteBackend(base_backend.Backend):
def __init__(self, filename, users_tname='users', roles_tname='roles',
pending_reg_tname='register', initialize=False):
self._filename = filename
self.users = UsersTable(self, users_tname)
self.roles = RolesTable(self, roles_tname)
self.pending_registrations = PendingRegistrationsTable(self, pending_reg_tname)
if initialize:
self.users.create_table()
self.roles.create_table()
self.pending_registrations.create_table()
log.debug("Tables created")
@property
def connection(self):
try:
return self._connection
except AttributeError:
import sqlite3
self._connection = sqlite3.connect(self._filename, isolation_level=None)
return self._connection
def run_query(self, query):
return self._connection.execute(query)
def run_query_using_conversion(self, query, args):
return self._connection.execute(query, args)
def fetch_one(self, query):
return self._connection.execute(query).fetchone()
def _initialize_storage(self, db_name):
raise NotImplementedError
def _drop_all_tables(self):
raise NotImplementedError
def save_users(self): pass
def save_roles(self): pass
def save_pending_registrations(self): pass
bottle-cork-0.12.0/cork/sqlalchemy_backend.py 0000644 0001750 0001750 00000015351 12617446061 021004 0 ustar fede fede 0000000 0000000 # Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
"""
.. module:: sqlalchemy_backend
:synopsis: SQLAlchemy storage backend.
"""
import sys
from logging import getLogger
from . import base_backend
log = getLogger(__name__)
is_py3 = (sys.version_info.major == 3)
try:
from sqlalchemy import create_engine, delete, select, \
Column, ForeignKey, Integer, MetaData, String, Table, Unicode
sqlalchemy_available = True
except ImportError: # pragma: no cover
sqlalchemy_available = False
class SqlRowProxy(dict):
def __init__(self, sql_dict, key, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
self.sql_dict = sql_dict
self.key = key
def __setitem__(self, key, value):
dict.__setitem__(self, key, value)
if self.sql_dict is not None:
self.sql_dict[self.key] = {key: value}
class SqlTable(base_backend.Table):
"""Provides dictionary-like access to an SQL table."""
def __init__(self, engine, table, key_col_name):
self._engine = engine
self._table = table
self._key_col = table.c[key_col_name]
def _row_to_value(self, row):
row_key = row[self._key_col]
row_value = SqlRowProxy(self, row_key,
((k, row[k]) for k in row.keys() if k != self._key_col.name))
return row_key, row_value
def __len__(self):
query = self._table.count()
c = self._engine.execute(query).scalar()
return int(c)
def __contains__(self, key):
query = select([self._key_col], self._key_col == key)
row = self._engine.execute(query).fetchone()
return row is not None
def __setitem__(self, key, value):
if key in self:
values = value
query = self._table.update().where(self._key_col == key)
else:
values = {self._key_col.name: key}
values.update(value)
query = self._table.insert()
self._engine.execute(query.values(**values))
def __getitem__(self, key):
query = select([self._table], self._key_col == key)
row = self._engine.execute(query).fetchone()
if row is None:
raise KeyError(key)
return self._row_to_value(row)[1]
def __iter__(self):
"""Iterate over table index key values"""
query = select([self._key_col])
result = self._engine.execute(query)
for row in result:
key = row[0]
yield key
def iteritems(self):
"""Iterate over table rows"""
query = select([self._table])
result = self._engine.execute(query)
for row in result:
key = row[0]
d = self._row_to_value(row)[1]
yield (key, d)
def pop(self, key):
query = select([self._table], self._key_col == key)
row = self._engine.execute(query).fetchone()
if row is None:
raise KeyError
query = delete(self._table, self._key_col == key)
self._engine.execute(query)
return row
def insert(self, d):
query = self._table.insert(d)
self._engine.execute(query)
log.debug("%s inserted" % repr(d))
def empty_table(self):
query = self._table.delete()
self._engine.execute(query)
log.info("Table purged")
class SqlSingleValueTable(SqlTable):
def __init__(self, engine, table, key_col_name, col_name):
SqlTable.__init__(self, engine, table, key_col_name)
self._col_name = col_name
def _row_to_value(self, row):
return row[self._key_col], row[self._col_name]
def __setitem__(self, key, value):
SqlTable.__setitem__(self, key, {self._col_name: value})
class SqlAlchemyBackend(base_backend.Backend):
def __init__(self, db_full_url, users_tname='users', roles_tname='roles',
pending_reg_tname='register', initialize=False, **kwargs):
if not sqlalchemy_available:
raise RuntimeError("The SQLAlchemy library is not available.")
self._metadata = MetaData()
if initialize:
# Create new database if needed.
db_url, db_name = db_full_url.rsplit('/', 1)
if is_py3 and db_url.startswith('mysql'):
print("WARNING: MySQL is not supported under Python3")
self._engine = create_engine(db_url, encoding='utf-8', **kwargs)
try:
self._engine.execute("CREATE DATABASE %s" % db_name)
except Exception as e:
log.info("Failed DB creation: %s" % e)
# SQLite in-memory database URL: "sqlite://:memory:"
if db_name != ':memory:' and not db_url.startswith('postgresql'):
self._engine.execute("USE %s" % db_name)
else:
self._engine = create_engine(db_full_url, encoding='utf-8', **kwargs)
self._users = Table(users_tname, self._metadata,
Column('username', Unicode(128), primary_key=True),
Column('role', ForeignKey(roles_tname + '.role')),
Column('hash', String(256), nullable=False),
Column('email_addr', String(128)),
Column('desc', String(128)),
Column('creation_date', String(128), nullable=False),
Column('last_login', String(128), nullable=False)
)
self._roles = Table(roles_tname, self._metadata,
Column('role', String(128), primary_key=True),
Column('level', Integer, nullable=False)
)
self._pending_reg = Table(pending_reg_tname, self._metadata,
Column('code', String(128), primary_key=True),
Column('username', Unicode(128), nullable=False),
Column('role', ForeignKey(roles_tname + '.role')),
Column('hash', String(256), nullable=False),
Column('email_addr', String(128)),
Column('desc', String(128)),
Column('creation_date', String(128), nullable=False)
)
self.users = SqlTable(self._engine, self._users, 'username')
self.roles = SqlSingleValueTable(self._engine, self._roles, 'role', 'level')
self.pending_registrations = SqlTable(self._engine, self._pending_reg, 'code')
if initialize:
self._initialize_storage(db_name)
log.debug("Tables created")
def _initialize_storage(self, db_name):
self._metadata.create_all(self._engine)
def _drop_all_tables(self):
for table in reversed(self._metadata.sorted_tables):
log.info("Dropping table %s" % repr(table.name))
self._engine.execute(table.delete())
def save_users(self): pass
def save_roles(self): pass
def save_pending_registrations(self): pass
bottle-cork-0.12.0/cork/__init__.py 0000644 0001750 0001750 00000000523 12617462665 016735 0 ustar fede fede 0000000 0000000 # Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Backends API - used to make backends available for importing
#
from .cork import Cork, JsonBackend, AAAException, AuthException, Mailer, FlaskCork, Redirect
bottle-cork-0.12.0/cork/backends.py 0000644 0001750 0001750 00000000712 12517131104 016724 0 ustar fede fede 0000000 0000000 # Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
"""
.. module:: backends
:synopsis: Backends API - used to make backends available for importing
"""
from .json_backend import JsonBackend
from .mongodb_backend import MongoDBBackend
from .sqlalchemy_backend import SqlAlchemyBackend
from .sqlite_backend import SQLiteBackend
bottle-cork-0.12.0/setup.py 0000644 0001750 0001750 00000002514 12617466063 015376 0 ustar fede fede 0000000 0000000 #!/usr/bin/env python
from setuptools import setup
__version__ = '0.12.0'
CLASSIFIERS = map(str.strip,
"""Development Status :: 4 - Beta
Environment :: Web Environment
Framework :: Bottle
Framework :: Flask
Intended Audience :: Developers
License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
Natural Language :: English
Operating System :: POSIX :: Linux
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3.4
Topic :: Internet :: WWW/HTTP :: WSGI
Topic :: Internet :: WWW/HTTP :: WSGI
Topic :: Security
Topic :: Software Development :: Libraries :: Python Modules
Topic :: Software Development :: Libraries :: Python Modules
""".splitlines())
setup(
name="bottle-cork",
version=__version__,
author="Federico Ceratto",
author_email="federico.ceratto@gmail.com",
description="Authentication/Authorization library for Bottle",
license="LGPLv3+",
url="http://cork.firelet.net/",
long_description="Cork is a simple Authentication/Authorization library"
"for the Bottle and Flask web frameworks.",
classifiers=CLASSIFIERS,
install_requires=[
'Bottle',
'pycrypto',
],
extras_require={
'scrypt': ["scrypt>=0.6.1"],
},
packages=['cork'],
platforms=['Linux'],
)
bottle-cork-0.12.0/MANIFEST.in 0000644 0001750 0001750 00000000102 12166632355 015407 0 ustar fede fede 0000000 0000000 include tests/*.py
include setup.py
recursive-include examples *
bottle-cork-0.12.0/examples/ 0000755 0001750 0001750 00000000000 12617467123 015476 5 ustar fede fede 0000000 0000000 bottle-cork-0.12.0/examples/simple_webapp_decorated.py 0000755 0001750 0001750 00000014577 12312136477 022727 0 ustar fede fede 0000000 0000000 #!/usr/bin/env python
#
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
#
# Cork example web application
#
# The following users are already available:
# admin/admin, demo/demo
import bottle
from beaker.middleware import SessionMiddleware
from cork import Cork
import logging
logging.basicConfig(format='localhost - - [%(asctime)s] %(message)s', level=logging.DEBUG)
log = logging.getLogger(__name__)
bottle.debug(True)
# Use users.json and roles.json in the local example_conf directory
aaa = Cork('example_conf', email_sender='federico.ceratto@gmail.com', smtp_url='smtp://smtp.magnet.ie')
# alias the authorization decorator with defaults
authorize = aaa.make_auth_decorator(fail_redirect="/login", role="user")
import datetime
app = bottle.app()
session_opts = {
'session.cookie_expires': True,
'session.encrypt_key': 'please use a random key and keep it secret!',
'session.httponly': True,
'session.timeout': 3600 * 24, # 1 day
'session.type': 'cookie',
'session.validate_key': True,
}
app = SessionMiddleware(app, session_opts)
# # Bottle methods # #
def postd():
return bottle.request.forms
def post_get(name, default=''):
return bottle.request.POST.get(name, default).strip()
@bottle.post('/login')
def login():
"""Authenticate users"""
username = post_get('username')
password = post_get('password')
aaa.login(username, password, success_redirect='/', fail_redirect='/login')
@bottle.route('/user_is_anonymous')
def user_is_anonymous():
if aaa.user_is_anonymous:
return 'True'
return 'False'
@bottle.route('/logout')
def logout():
aaa.logout(success_redirect='/login')
@bottle.post('/register')
def register():
"""Send out registration email"""
aaa.register(post_get('username'), post_get('password'), post_get('email_address'))
return 'Please check your mailbox.'
@bottle.route('/validate_registration/:registration_code')
def validate_registration(registration_code):
"""Validate registration, create user account"""
aaa.validate_registration(registration_code)
return 'Thanks. Go to login'
@bottle.post('/reset_password')
def send_password_reset_email():
"""Send out password reset email"""
aaa.send_password_reset_email(
username=post_get('username'),
email_addr=post_get('email_address')
)
return 'Please check your mailbox.'
@bottle.route('/change_password/:reset_code')
@bottle.view('password_change_form')
def change_password(reset_code):
"""Show password change form"""
return dict(reset_code=reset_code)
@bottle.post('/change_password')
def change_password():
"""Change password"""
aaa.reset_password(post_get('reset_code'), post_get('password'))
return 'Thanks. Go to login'
@bottle.route('/')
@authorize()
def index():
"""Only authenticated users can see this"""
#session = bottle.request.environ.get('beaker.session')
#aaa.require(fail_redirect='/login')
return 'Welcome! Admin page Logout'
# Resources used by tests designed to test decorators specifically
@bottle.route('/for_kings_only')
@authorize(role="king")
def page_for_kings():
"""
This resource is used to test a non-existing role.
Only kings or higher (e.g. gods) can see this
"""
return 'Welcome! Admin page Logout'
@bottle.route('/page_for_specific_user_admin')
@authorize(username="admin")
def page_for_username_admin():
"""Only a user named 'admin' can see this"""
return 'Welcome! Admin page Logout'
@bottle.route('/page_for_specific_user_fred_who_doesnt_exist')
@authorize(username="fred")
def page_for_user_fred():
"""Only authenticated users by the name of 'fred' can see this"""
return 'Welcome! Admin page Logout'
@bottle.route('/page_for_admins')
@authorize(role="admin")
def page_for_role_admin():
"""Only authenticated users (role=user or role=admin) can see this"""
return 'Welcome! Admin page Logout'
@bottle.route('/restricted_download')
@authorize()
def restricted_download():
"""Only authenticated users can download this file"""
#aaa.require(fail_redirect='/login')
return bottle.static_file('static_file', root='.')
@bottle.route('/my_role')
def show_current_user_role():
"""Show current user role"""
session = bottle.request.environ.get('beaker.session')
print "Session from simple_webapp", repr(session)
aaa.require(fail_redirect='/login')
return aaa.current_user.role
# Admin-only pages
@bottle.route('/admin')
@authorize(role="admin", fail_redirect='/sorry_page')
@bottle.view('admin_page')
def admin():
"""Only admin users can see this"""
#aaa.require(role='admin', fail_redirect='/sorry_page')
return dict(
current_user = aaa.current_user,
users = aaa.list_users(),
roles = aaa.list_roles()
)
@bottle.post('/create_user')
def create_user():
try:
aaa.create_user(postd().username, postd().role, postd().password)
return dict(ok=True, msg='')
except Exception, e:
return dict(ok=False, msg=e.message)
@bottle.post('/delete_user')
def delete_user():
try:
aaa.delete_user(post_get('username'))
return dict(ok=True, msg='')
except Exception, e:
print repr(e)
return dict(ok=False, msg=e.message)
@bottle.post('/create_role')
def create_role():
try:
aaa.create_role(post_get('role'), post_get('level'))
return dict(ok=True, msg='')
except Exception, e:
return dict(ok=False, msg=e.message)
@bottle.post('/delete_role')
def delete_role():
try:
aaa.delete_role(post_get('role'))
return dict(ok=True, msg='')
except Exception, e:
return dict(ok=False, msg=e.message)
# Static pages
@bottle.route('/login')
@bottle.view('login_form')
def login_form():
"""Serve login form"""
return {}
@bottle.route('/sorry_page')
def sorry_page():
"""Serve sorry page"""
return 'Sorry, you are not authorized to perform this action
'
# # Web application main # #
def main():
# Start the Bottle webapp
bottle.debug(True)
bottle.run(app=app, quiet=False, reloader=True)
if __name__ == "__main__":
main()
bottle-cork-0.12.0/examples/recreate_example_conf.py 0000755 0001750 0001750 00000001753 12312136477 022370 0 ustar fede fede 0000000 0000000 #!/usr/bin/env python
#
#
# Regenerate files in example_conf
from datetime import datetime
from cork import Cork
def populate_conf_directory():
cork = Cork('example_conf', initialize=True)
cork._store.roles['admin'] = 100
cork._store.roles['editor'] = 60
cork._store.roles['user'] = 50
cork._store.save_roles()
tstamp = str(datetime.utcnow())
username = password = 'admin'
cork._store.users[username] = {
'role': 'admin',
'hash': cork._hash(username, password),
'email_addr': username + '@localhost.local',
'desc': username + ' test user',
'creation_date': tstamp
}
username = password = ''
cork._store.users[username] = {
'role': 'user',
'hash': cork._hash(username, password),
'email_addr': username + '@localhost.local',
'desc': username + ' test user',
'creation_date': tstamp
}
cork._store.save_users()
if __name__ == '__main__':
populate_conf_directory()
bottle-cork-0.12.0/examples/simple_webapp_using_sqlite_mytest.py 0000755 0001750 0001750 00000013671 12312136477 025102 0 ustar fede fede 0000000 0000000 #!/usr/bin/env python
#
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under GPLv3+ license, see LICENSE.txt
#
# Cork example web application
#
# The following users are already available:
# admin/admin, demo/demo
import bottle
from beaker.middleware import SessionMiddleware
from cork import Cork
from cork.backends import SQLiteBackend
import logging
logging.basicConfig(format='localhost - - [%(asctime)s] %(message)s', level=logging.DEBUG)
log = logging.getLogger(__name__)
bottle.debug(True)
# Use users.json and roles.json in the local example_conf directory
aaa = Cork('example_conf', email_sender='federico.ceratto@gmail.com', smtp_url='smtp://smtp.magnet.ie')
def populate_backend():
b = SQLiteBackend('example2.db', initialize=True)
b.connection.executescript("""
INSERT INTO users (username, email_addr, desc, role, hash, creation_date) VALUES
(
'admin',
'admin@localhost.local',
'admin test user',
'admin',
'cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=',
'2012-10-28 20:50:26.286723'
);
INSERT INTO roles (role, level) VALUES ('special', 200);
INSERT INTO roles (role, level) VALUES ('admin', 100);
INSERT INTO roles (role, level) VALUES ('editor', 60);
INSERT INTO roles (role, level) VALUES ('user', 50);
""")
return b
b = populate_backend()
aaa = Cork(backend=b, email_sender='federico.ceratto@gmail.com', smtp_url='smtp://smtp.magnet.ie')
app = bottle.app()
session_opts = {
'session.cookie_expires': True,
'session.encrypt_key': 'please use a random key and keep it secret!',
'session.httponly': True,
'session.timeout': 3600 * 24, # 1 day
'session.type': 'cookie',
'session.validate_key': True,
}
app = SessionMiddleware(app, session_opts)
# # Bottle methods # #
def postd():
return bottle.request.forms
def post_get(name, default=''):
return bottle.request.POST.get(name, default).strip()
@bottle.post('/login')
def login():
"""Authenticate users"""
username = post_get('username')
password = post_get('password')
aaa.login(username, password, success_redirect='/', fail_redirect='/login')
@bottle.route('/user_is_anonymous')
def user_is_anonymous():
if aaa.user_is_anonymous:
return 'True'
return 'False'
@bottle.route('/logout')
def logout():
aaa.logout(success_redirect='/login')
@bottle.post('/register')
def register():
"""Send out registration email"""
aaa.register(post_get('username'), post_get('password'), post_get('email_address'))
return 'Please check your mailbox.'
@bottle.route('/validate_registration/:registration_code')
def validate_registration(registration_code):
"""Validate registration, create user account"""
aaa.validate_registration(registration_code)
return 'Thanks. Go to login'
@bottle.post('/reset_password')
def send_password_reset_email():
"""Send out password reset email"""
aaa.send_password_reset_email(
username=post_get('username'),
email_addr=post_get('email_address')
)
return 'Please check your mailbox.'
@bottle.route('/change_password/:reset_code')
@bottle.view('password_change_form')
def change_password(reset_code):
"""Show password change form"""
return dict(reset_code=reset_code)
@bottle.post('/change_password')
def change_password():
"""Change password"""
aaa.reset_password(post_get('reset_code'), post_get('password'))
return 'Thanks. Go to login'
@bottle.route('/')
def index():
"""Only authenticated users can see this"""
aaa.require(fail_redirect='/login')
return 'Welcome! Admin page Logout'
@bottle.route('/restricted_download')
def restricted_download():
"""Only authenticated users can download this file"""
aaa.require(fail_redirect='/login')
return bottle.static_file('static_file', root='.')
@bottle.route('/my_role')
def show_current_user_role():
"""Show current user role"""
session = bottle.request.environ.get('beaker.session')
print "Session from simple_webapp", repr(session)
aaa.require(fail_redirect='/login')
return aaa.current_user.role
# Admin-only pages
@bottle.route('/admin')
@bottle.view('admin_page')
def admin():
"""Only admin users can see this"""
aaa.require(role='admin', fail_redirect='/sorry_page')
return dict(
current_user=aaa.current_user,
users=aaa.list_users(),
roles=aaa.list_roles()
)
@bottle.post('/create_user')
def create_user():
try:
aaa.create_user(postd().username, postd().role, postd().password)
return dict(ok=True, msg='')
except Exception, e:
return dict(ok=False, msg=e.message)
@bottle.post('/delete_user')
def delete_user():
try:
aaa.delete_user(post_get('username'))
return dict(ok=True, msg='')
except Exception, e:
print repr(e)
return dict(ok=False, msg=e.message)
@bottle.post('/create_role')
def create_role():
try:
aaa.create_role(post_get('role'), post_get('level'))
return dict(ok=True, msg='')
except Exception, e:
return dict(ok=False, msg=e.message)
@bottle.post('/delete_role')
def delete_role():
try:
aaa.delete_role(post_get('role'))
return dict(ok=True, msg='')
except Exception, e:
return dict(ok=False, msg=e.message)
# Static pages
@bottle.route('/login')
@bottle.view('login_form')
def login_form():
"""Serve login form"""
return {}
@bottle.route('/sorry_page')
def sorry_page():
"""Serve sorry page"""
return 'Sorry, you are not authorized to perform this action
'
# # Web application main # #
def main():
# Start the Bottle webapp
bottle.debug(True)
bottle.run(app=app, port=8877, quiet=False, reloader=False,server="paste")
if __name__ == "__main__":
main()
bottle-cork-0.12.0/examples/simple_webapp_using_mongodb.py 0000755 0001750 0001750 00000012770 12312136477 023620 0 ustar fede fede 0000000 0000000 #!/usr/bin/env python
#
#
# Cork example web application
#
# The following users are already available:
# admin/admin, demo/demo
import bottle
from beaker.middleware import SessionMiddleware
from cork import Cork
from cork.backends import MongoDBBackend
import logging
logging.basicConfig(format='localhost - - [%(asctime)s] %(message)s', level=logging.DEBUG)
log = logging.getLogger(__name__)
bottle.debug(True)
def populate_mongodb_backend():
mb = MongoDBBackend(db_name='cork-example', initialize=True)
mb.users._coll.insert({
"login": "admin",
"email_addr": "admin@localhost.local",
"desc": "admin test user",
"role": "admin",
"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=",
"creation_date": "2012-10-28 20:50:26.286723"
})
mb.roles._coll.insert({'role': 'admin', 'val': 100})
mb.roles._coll.insert({'role': 'editor', 'val': 60})
mb.roles._coll.insert({'role': 'user', 'val': 50})
return mb
mb = populate_mongodb_backend()
aaa = Cork(backend=mb, email_sender='federico.ceratto@gmail.com', smtp_url='smtp://smtp.magnet.ie')
app = bottle.app()
session_opts = {
'session.cookie_expires': True,
'session.encrypt_key': 'please use a random key and keep it secret!',
'session.httponly': True,
'session.timeout': 3600 * 24, # 1 day
'session.type': 'cookie',
'session.validate_key': True,
}
app = SessionMiddleware(app, session_opts)
# # Bottle methods # #
def postd():
return bottle.request.forms
def post_get(name, default=''):
return bottle.request.POST.get(name, default).strip()
@bottle.post('/login')
def login():
"""Authenticate users"""
username = post_get('username')
password = post_get('password')
aaa.login(username, password, success_redirect='/', fail_redirect='/login')
@bottle.route('/user_is_anonymous')
def user_is_anonymous():
if aaa.user_is_anonymous:
return 'True'
return 'False'
@bottle.route('/logout')
def logout():
aaa.logout(success_redirect='/login')
@bottle.post('/register')
def register():
"""Send out registration email"""
aaa.register(post_get('username'), post_get('password'), post_get('email_address'))
return 'Please check your mailbox.'
@bottle.route('/validate_registration/:registration_code')
def validate_registration(registration_code):
"""Validate registration, create user account"""
aaa.validate_registration(registration_code)
return 'Thanks. Go to login'
@bottle.post('/reset_password')
def send_password_reset_email():
"""Send out password reset email"""
aaa.send_password_reset_email(
username=post_get('username'),
email_addr=post_get('email_address')
)
return 'Please check your mailbox.'
@bottle.route('/change_password/:reset_code')
@bottle.view('password_change_form')
def change_password(reset_code):
"""Show password change form"""
return dict(reset_code=reset_code)
@bottle.post('/change_password')
def change_password():
"""Change password"""
aaa.reset_password(post_get('reset_code'), post_get('password'))
return 'Thanks. Go to login'
@bottle.route('/')
def index():
"""Only authenticated users can see this"""
aaa.require(fail_redirect='/login')
return 'Welcome! Admin page Logout'
@bottle.route('/restricted_download')
def restricted_download():
"""Only authenticated users can download this file"""
aaa.require(fail_redirect='/login')
return bottle.static_file('static_file', root='.')
@bottle.route('/my_role')
def show_current_user_role():
"""Show current user role"""
session = bottle.request.environ.get('beaker.session')
print "Session from simple_webapp", repr(session)
aaa.require(fail_redirect='/login')
return aaa.current_user.role
# Admin-only pages
@bottle.route('/admin')
@bottle.view('admin_page')
def admin():
"""Only admin users can see this"""
aaa.require(role='admin', fail_redirect='/sorry_page')
return dict(
current_user=aaa.current_user,
users=aaa.list_users(),
roles=aaa.list_roles()
)
@bottle.post('/create_user')
def create_user():
try:
aaa.create_user(postd().username, postd().role, postd().password)
return dict(ok=True, msg='')
except Exception, e:
return dict(ok=False, msg=e.message)
@bottle.post('/delete_user')
def delete_user():
try:
aaa.delete_user(post_get('username'))
return dict(ok=True, msg='')
except Exception, e:
print repr(e)
return dict(ok=False, msg=e.message)
@bottle.post('/create_role')
def create_role():
try:
aaa.create_role(post_get('role'), post_get('level'))
return dict(ok=True, msg='')
except Exception, e:
return dict(ok=False, msg=e.message)
@bottle.post('/delete_role')
def delete_role():
try:
aaa.delete_role(post_get('role'))
return dict(ok=True, msg='')
except Exception, e:
return dict(ok=False, msg=e.message)
# Static pages
@bottle.route('/login')
@bottle.view('login_form')
def login_form():
"""Serve login form"""
return {}
@bottle.route('/sorry_page')
def sorry_page():
"""Serve sorry page"""
return 'Sorry, you are not authorized to perform this action
'
# # Web application main # #
def main():
# Start the Bottle webapp
bottle.debug(True)
bottle.run(app=app, quiet=False, reloader=True)
if __name__ == "__main__":
main()
bottle-cork-0.12.0/examples/static_file 0000644 0001750 0001750 00000000003 12161350444 017667 0 ustar fede fede 0000000 0000000 hi
bottle-cork-0.12.0/examples/cork.py 0000644 0001750 0001750 00000074665 12312136477 017025 0 ustar fede fede 0000000 0000000 #!/usr/bin/env python
#
# Cork - Authentication module for the Bottle web framework
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
#
# This package is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This package is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from base64 import b64encode, b64decode
from beaker import crypto
from datetime import datetime, timedelta
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from logging import getLogger
from smtplib import SMTP, SMTP_SSL
from threading import Thread
from time import time
import bottle
import os
import re
import uuid
try:
import scrypt
scrypt_available = True
except ImportError: # pragma: no cover
scrypt_available = False
from backends import JsonBackend
log = getLogger(__name__)
class AAAException(Exception):
"""Generic Authentication/Authorization Exception"""
pass
class AuthException(AAAException):
"""Authentication Exception: incorrect username/password pair"""
pass
class Cork(object):
def __init__(self, directory=None, backend=None, email_sender=None,
initialize=False, session_domain=None, smtp_server=None,
smtp_url='localhost'):
"""Auth/Authorization/Accounting class
:param directory: configuration directory
:type directory: str.
:param users_fname: users filename (without .json), defaults to 'users'
:type users_fname: str.
:param roles_fname: roles filename (without .json), defaults to 'roles'
:type roles_fname: str.
"""
if smtp_server:
smtp_url = smtp_server
self.mailer = Mailer(email_sender, smtp_url)
self.password_reset_timeout = 3600 * 24
self.session_domain = session_domain
self.preferred_hashing_algorithm = 'PBKDF2'
# Setup JsonBackend by default for backward compatibility.
if backend is None:
self._store = JsonBackend(directory, users_fname='users',
roles_fname='roles', pending_reg_fname='register',
initialize=initialize)
else:
self._store = backend
def login(self, username, password, success_redirect=None,
fail_redirect=None):
"""Check login credentials for an existing user.
Optionally redirect the user to another page (typically /login)
:param username: username
:type username: str.
:param password: cleartext password
:type password: str.
:param success_redirect: redirect authorized users (optional)
:type success_redirect: str.
:param fail_redirect: redirect unauthorized users (optional)
:type fail_redirect: str.
:returns: True for successful logins, else False
"""
assert isinstance(username, str), "the username must be a string"
assert isinstance(password, str), "the password must be a string"
if username in self._store.users:
if self._verify_password(username, password,
self._store.users[username]['hash']):
# Setup session data
self._setup_cookie(username)
self._store.users[username]['last_login'] = str(datetime.utcnow())
self._store.save_users()
if success_redirect:
bottle.redirect(success_redirect)
return True
if fail_redirect:
bottle.redirect(fail_redirect)
return False
def logout(self, success_redirect='/login', fail_redirect='/login'):
"""Log the user out, remove cookie
:param success_redirect: redirect the user after logging out
:type success_redirect: str.
:param fail_redirect: redirect the user if it is not logged in
:type fail_redirect: str.
"""
try:
session = self._beaker_session
session.delete()
except Exception, e:
log.debug("Exception %s while logging out." % repr(e))
bottle.redirect(fail_redirect)
bottle.redirect(success_redirect)
def require(self, username=None, role=None, fixed_role=False,
fail_redirect=None):
"""Ensure the user is logged in has the required role (or higher).
Optionally redirect the user to another page (typically /login)
If both `username` and `role` are specified, both conditions need to be
satisfied.
If none is specified, any authenticated user will be authorized.
By default, any role with higher level than `role` will be authorized;
set fixed_role=True to prevent this.
:param username: username (optional)
:type username: str.
:param role: role
:type role: str.
:param fixed_role: require user role to match `role` strictly
:type fixed_role: bool.
:param redirect: redirect unauthorized users (optional)
:type redirect: str.
"""
# Parameter validation
if username is not None:
if username not in self._store.users:
raise AAAException("Nonexistent user")
if fixed_role and role is None:
raise AAAException(
"""A role must be specified if fixed_role has been set""")
if role is not None and role not in self._store.roles:
raise AAAException("Role not found")
# Authentication
try:
cu = self.current_user
except AAAException:
if fail_redirect is None:
raise AuthException("Unauthenticated user")
else:
bottle.redirect(fail_redirect)
# Authorization
if cu.role not in self._store.roles:
raise AAAException("Role not found for the current user")
if username is not None:
if username != self.current_user.username:
if fail_redirect is None:
raise AuthException("Unauthorized access: incorrect"
" username")
else:
bottle.redirect(fail_redirect)
if fixed_role:
if role == self.current_user.role:
return
if fail_redirect is None:
raise AuthException("Unauthorized access: incorrect role")
else:
bottle.redirect(fail_redirect)
else:
if role is not None:
# Any role with higher level is allowed
current_lvl = self._store.roles[self.current_user.role]
threshold_lvl = self._store.roles[role]
if current_lvl >= threshold_lvl:
return
if fail_redirect is None:
raise AuthException("Unauthorized access: ")
else:
bottle.redirect(fail_redirect)
return
def create_role(self, role, level):
"""Create a new role.
:param role: role name
:type role: str.
:param level: role level (0=lowest, 100=admin)
:type level: int.
:raises: AuthException on errors
"""
if self.current_user.level < 100:
raise AuthException("The current user is not authorized to ")
if role in self._store.roles:
raise AAAException("The role is already existing")
try:
int(level)
except ValueError:
raise AAAException("The level must be numeric.")
self._store.roles[role] = level
self._store.save_roles()
def delete_role(self, role):
"""Deleta a role.
:param role: role name
:type role: str.
:raises: AuthException on errors
"""
if self.current_user.level < 100:
raise AuthException("The current user is not authorized to ")
if role not in self._store.roles:
raise AAAException("Nonexistent role.")
self._store.roles.pop(role)
self._store.save_roles()
def list_roles(self):
"""List roles.
:returns: (role, role_level) generator (sorted by role)
"""
for role in sorted(self._store.roles):
yield (role, self._store.roles[role])
def create_user(self, username, role, password, email_addr=None,
description=None):
"""Create a new user account.
This method is available to users with level>=100
:param username: username
:type username: str.
:param role: role
:type role: str.
:param password: cleartext password
:type password: str.
:param email_addr: email address (optional)
:type email_addr: str.
:param description: description (free form)
:type description: str.
:raises: AuthException on errors
"""
assert username, "Username must be provided."
if self.current_user.level < 100:
raise AuthException("The current user is not authorized" \
" to create users.")
if username in self._store.users:
raise AAAException("User is already existing.")
if role not in self._store.roles:
raise AAAException("Nonexistent user role.")
tstamp = str(datetime.utcnow())
self._store.users[username] = {
'role': role,
'hash': self._hash(username, password),
'email_addr': email_addr,
'desc': description,
'creation_date': tstamp,
'last_login': tstamp
}
self._store.save_users()
def delete_user(self, username):
"""Delete a user account.
This method is available to users with level>=100
:param username: username
:type username: str.
:raises: Exceptions on errors
"""
if self.current_user.level < 100:
raise AuthException("The current user is not authorized to ")
if username not in self._store.users:
raise AAAException("Nonexistent user.")
self.user(username).delete()
def list_users(self):
"""List users.
:return: (username, role, email_addr, description) generator (sorted by
username)
"""
for un in sorted(self._store.users):
d = self._store.users[un]
yield (un, d['role'], d['email_addr'], d['desc'])
@property
def current_user(self):
"""Current autenticated user
:returns: User() instance, if authenticated
:raises: AuthException otherwise
"""
session = self._beaker_session
username = session.get('username', None)
if username is None:
raise AuthException("Unauthenticated user")
if username is not None and username in self._store.users:
return User(username, self, session=session)
raise AuthException("Unknown user: %s" % username)
@property
def user_is_anonymous(self):
"""Check if the current user is anonymous.
:returns: True if the user is anonymous, False otherwise
:raises: AuthException if the session username is unknown
"""
try:
username = self._beaker_session['username']
except KeyError:
return True
if username not in self._store.users:
raise AuthException("Unknown user: %s" % username)
return False
def user(self, username):
"""Existing user
:returns: User() instance if the user exist, None otherwise
"""
if username is not None and username in self._store.users:
return User(username, self)
return None
def register(self, username, password, email_addr, role='user',
max_level=50, subject="Signup confirmation",
email_template='views/registration_email.tpl',
description=None):
"""Register a new user account. An email with a registration validation
is sent to the user.
WARNING: this method is available to unauthenticated users
:param username: username
:type username: str.
:param password: cleartext password
:type password: str.
:param role: role (optional), defaults to 'user'
:type role: str.
:param max_level: maximum role level (optional), defaults to 50
:type max_level: int.
:param email_addr: email address
:type email_addr: str.
:param subject: email subject
:type subject: str.
:param email_template: email template filename
:type email_template: str.
:param description: description (free form)
:type description: str.
:raises: AssertError or AAAException on errors
"""
assert username, "Username must be provided."
assert password, "A password must be provided."
assert email_addr, "An email address must be provided."
if username in self._store.users:
raise AAAException("User is already existing.")
if role not in self._store.roles:
raise AAAException("Nonexistent role")
if self._store.roles[role] > max_level:
raise AAAException("Unauthorized role")
registration_code = uuid.uuid4().hex
creation_date = str(datetime.utcnow())
# send registration email
email_text = bottle.template(email_template,
username=username,
email_addr=email_addr,
role=role,
creation_date=creation_date,
registration_code=registration_code
)
self.mailer.send_email(email_addr, subject, email_text)
# store pending registration
self._store.pending_registrations[registration_code] = {
'username': username,
'role': role,
'hash': self._hash(username, password),
'email_addr': email_addr,
'desc': description,
'creation_date': creation_date,
}
self._store.save_pending_registrations()
def validate_registration(self, registration_code):
"""Validate pending account registration, create a new account if
successful.
:param registration_code: registration code
:type registration_code: str.
"""
try:
data = self._store.pending_registrations.pop(registration_code)
except KeyError:
raise AuthException("Invalid registration code.")
username = data['username']
if username in self._store.users:
raise AAAException("User is already existing.")
# the user data is moved from pending_registrations to _users
self._store.users[username] = {
'role': data['role'],
'hash': data['hash'],
'email_addr': data['email_addr'],
'desc': data['desc'],
'creation_date': data['creation_date'],
'last_login': str(datetime.utcnow())
}
self._store.save_users()
def send_password_reset_email(self, username=None, email_addr=None,
subject="Password reset confirmation",
email_template='views/password_reset_email'):
"""Email the user with a link to reset his/her password
If only one parameter is passed, fetch the other from the users
database. If both are passed they will be matched against the users
database as a security check.
:param username: username
:type username: str.
:param email_addr: email address
:type email_addr: str.
:param subject: email subject
:type subject: str.
:param email_template: email template filename
:type email_template: str.
:raises: AAAException on missing username or email_addr,
AuthException on incorrect username/email_addr pair
"""
if username is None:
if email_addr is None:
raise AAAException("At least `username` or `email_addr` must" \
" be specified.")
# only email_addr is specified: fetch the username
for k, v in self._store.users.iteritems():
if v['email_addr'] == email_addr:
username = k
break
else:
raise AAAException("Email address not found.")
else: # username is provided
if username not in self._store.users:
raise AAAException("Nonexistent user.")
if email_addr is None:
email_addr = self._store.users[username].get('email_addr', None)
if not email_addr:
raise AAAException("Email address not available.")
else:
# both username and email_addr are provided: check them
stored_email_addr = self._store.users[username]['email_addr']
if email_addr != stored_email_addr:
raise AuthException("Username/email address pair not found.")
# generate a reset_code token
reset_code = self._reset_code(username, email_addr)
# send reset email
email_text = bottle.template(email_template,
username=username,
email_addr=email_addr,
reset_code=reset_code
)
self.mailer.send_email(email_addr, subject, email_text)
def reset_password(self, reset_code, password):
"""Validate reset_code and update the account password
The username is extracted from the reset_code token
:param reset_code: reset token
:type reset_code: str.
:param password: new password
:type password: str.
:raises: AuthException for invalid reset tokens, AAAException
"""
try:
reset_code = b64decode(reset_code)
username, email_addr, tstamp, h = reset_code.split(':', 3)
tstamp = int(tstamp)
except (TypeError, ValueError):
raise AuthException("Invalid reset code.")
if time() - tstamp > self.password_reset_timeout:
raise AuthException("Expired reset code.")
if not self._verify_password(username, email_addr, h):
raise AuthException("Invalid reset code.")
user = self.user(username)
if user is None:
raise AAAException("Nonexistent user.")
user.update(pwd=password)
def make_auth_decorator(self, username=None, role=None, fixed_role=False, fail_redirect='/login'):
'''
Create a decorator to be used for authentication and authorization
:param username: A resource can be protected for a specific user
:param role: Minimum role level required for authorization
:param fixed_role: Only this role gets authorized
:param fail_redirect: The URL to redirect to if a login is required.
'''
session_manager = self
def auth_require(username=username, role=role, fixed_role=fixed_role,
fail_redirect=fail_redirect):
def decorator(func):
import functools
@functools.wraps(func)
def wrapper(*a, **ka):
session_manager.require(username=username, role=role, fixed_role=fixed_role,
fail_redirect=fail_redirect)
return func(*a, **ka)
return wrapper
return decorator
return(auth_require)
## Private methods
@property
def _beaker_session(self):
"""Get Beaker session"""
return bottle.request.environ.get('beaker.session')
def _setup_cookie(self, username):
"""Setup cookie for a user that just logged in"""
session = self._beaker_session
session['username'] = username
if self.session_domain is not None:
session.domain = self.session_domain
session.save()
def _hash(self, username, pwd, salt=None, algo=None):
"""Hash username and password, generating salt value if required
"""
if algo is None:
algo = self.preferred_hashing_algorithm
if algo == 'PBKDF2':
return self._hash_pbkdf2(username, pwd, salt=salt)
if algo == 'scrypt':
return self._hash_scrypt(username, pwd, salt=salt)
raise RuntimeError("Unknown hashing algorithm requested: %s" % algo)
@staticmethod
def _hash_scrypt(username, pwd, salt=None):
"""Hash username and password, generating salt value if required
Use scrypt.
:returns: base-64 encoded str.
"""
if not scrypt_available:
raise Exception("scrypt.hash required."
" Please install the scrypt library.")
if salt is None:
salt = os.urandom(32)
assert len(salt) == 32, "Incorrect salt length"
cleartext = "%s\0%s" % (username, pwd)
h = scrypt.hash(cleartext, salt)
# 's' for scrypt
return b64encode('s' + salt + h)
@staticmethod
def _hash_pbkdf2(username, pwd, salt=None):
"""Hash username and password, generating salt value if required
Use PBKDF2 from Beaker
:returns: base-64 encoded str.
"""
if salt is None:
salt = os.urandom(32)
assert len(salt) == 32, "Incorrect salt length"
cleartext = "%s\0%s" % (username, pwd)
h = crypto.generateCryptoKeys(cleartext, salt, 10)
if len(h) != 32:
raise RuntimeError("The PBKDF2 hash is %d bytes long instead"
"of 32. The pycrypto library might be missing." % len(h))
# 'p' for PBKDF2
return b64encode('p' + salt + h)
def _verify_password(self, username, pwd, salted_hash):
"""Verity username/password pair against a salted hash
:returns: bool
"""
decoded = b64decode(salted_hash)
hash_type = decoded[0]
salt = decoded[1:33]
if hash_type == 'p': # PBKDF2
h = self._hash_pbkdf2(username, pwd, salt)
return salted_hash == h
if hash_type == 's': # scrypt
h = self._hash_scrypt(username, pwd, salt)
return salted_hash == h
raise RuntimeError("Unknown hashing algorithm: %s" % hash_type)
def _purge_expired_registrations(self, exp_time=96):
"""Purge expired registration requests.
:param exp_time: expiration time (hours)
:type exp_time: float.
"""
for uuid, data in self._store.pending_registrations.items():
creation = datetime.strptime(data['creation_date'],
"%Y-%m-%d %H:%M:%S.%f")
now = datetime.utcnow()
maxdelta = timedelta(hours=exp_time)
if now - creation > maxdelta:
self._store.pending_registrations.pop(uuid)
def _reset_code(self, username, email_addr):
"""generate a reset_code token
:param username: username
:type username: str.
:param email_addr: email address
:type email_addr: str.
:returns: Base-64 encoded token
"""
h = self._hash(username, email_addr)
t = "%d" % time()
reset_code = ':'.join((username, email_addr, t, h))
return b64encode(reset_code)
class User(object):
def __init__(self, username, cork_obj, session=None):
"""Represent an authenticated user, exposing useful attributes:
username, role, level, description, email_addr, session_creation_time,
session_accessed_time, session_id. The session-related attributes are
available for the current user only.
:param username: username
:type username: str.
:param cork_obj: instance of :class:`Cork`
"""
self._cork = cork_obj
assert username in self._cork._store.users, "Unknown user"
self.username = username
user_data = self._cork._store.users[username]
self.role = user_data['role']
self.description = user_data['desc']
self.email_addr = user_data['email_addr']
self.level = self._cork._store.roles[self.role]
if session is not None:
try:
self.session_creation_time = session['_creation_time']
self.session_accessed_time = session['_accessed_time']
self.session_id = session['_id']
except:
pass
def update(self, role=None, pwd=None, email_addr=None):
"""Update an user account data
:param role: change user role, if specified
:type role: str.
:param pwd: change user password, if specified
:type pwd: str.
:param email_addr: change user email address, if specified
:type email_addr: str.
:raises: AAAException on nonexistent user or role.
"""
username = self.username
if username not in self._cork._store.users:
raise AAAException("User does not exist.")
if role is not None:
if role not in self._cork._store.roles:
raise AAAException("Nonexistent role.")
self._cork._store.users[username]['role'] = role
if pwd is not None:
self._cork._store.users[username]['hash'] = self._cork._hash(
username, pwd)
if email_addr is not None:
self._cork._store.users[username]['email_addr'] = email_addr
self._cork._store.save_users()
def delete(self):
"""Delete user account
:raises: AAAException on nonexistent user.
"""
try:
self._cork._store.users.pop(self.username)
except KeyError:
raise AAAException("Nonexistent user.")
self._cork._store.save_users()
class Mailer(object):
def __init__(self, sender, smtp_url, join_timeout=5):
"""Send emails asyncronously
:param sender: Sender email address
:type sender: str.
:param smtp_server: SMTP server
:type smtp_server: str.
"""
self.sender = sender
self.join_timeout = join_timeout
self._threads = []
self._conf = self._parse_smtp_url(smtp_url)
def _parse_smtp_url(self, url):
"""Parse SMTP URL"""
match = re.match(r"""
( # Optional protocol
(?Psmtp|starttls|ssl) # Protocol name
://
)?
( # Optional user:pass@
(?P[^:]*) # Match every char except ':'
(: (?P.*) )? @ # Optional :pass
)?
(?P # Required FQDN on IP address
()| # Empty string
( # FQDN
[a-zA-Z_\-] # First character cannot be a number
[a-zA-Z0-9_\-\.]{,254}
)
|( # IPv4
([0-9]{1,3}\.){3}
[0-9]{1,3}
)
|( # IPv6
\[ # Square brackets
([0-9a-f]{,4}:){1,8}
[0-9a-f]{,4}
\]
)
)
( # Optional :port
:
(?P[0-9]{,5}) # Up to 5-digits port
)?
[/]?
$
""", url, re.VERBOSE)
if not match:
raise RuntimeError("SMTP URL seems incorrect")
d = match.groupdict()
if d['proto'] is None:
d['proto'] = 'smtp'
if d['port'] is None:
d['port'] = 25
else:
d['port'] = int(d['port'])
if not 0 < d['port'] < 65536:
raise RuntimeError("Incorrect SMTP port")
return d
def send_email(self, email_addr, subject, email_text):
"""Send an email
:param email_addr: email address
:type email_addr: str.
:param subject: subject
:type subject: str.
:param email_text: email text
:type email_text: str.
:raises: AAAException if smtp_server and/or sender are not set
"""
if not (self._conf['fqdn'] and self.sender):
raise AAAException("SMTP server or sender not set")
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = self.sender
msg['To'] = email_addr
part = MIMEText(email_text, 'html')
msg.attach(part)
log.debug("Sending email using %s" % self._conf['fqdn'])
thread = Thread(target=self._send, args=(email_addr, msg.as_string()))
thread.start()
self._threads.append(thread)
def _send(self, email_addr, msg):
"""Deliver an email using SMTP
:param email_addr: recipient
:type email_addr: str.
:param msg: email text
:type msg: str.
"""
proto = self._conf['proto']
assert proto in ('smtp', 'starttls', 'ssl'), \
"Incorrect protocol: %s" % proto
try:
if proto == 'ssl':
log.debug("Setting up SSL")
session = SMTP_SSL(self._conf['fqdn'], self._conf['port'])
else:
session = SMTP(self._conf['fqdn'], self._conf['port'])
if proto == 'starttls':
log.debug('Sending EHLO and STARTTLS')
session.ehlo()
session.starttls()
session.ehlo()
if self._conf['user'] is not None:
log.debug('Performing login')
session.login(self._conf['user'], self._conf['pass'])
log.debug('Sending')
session.sendmail(self.sender, email_addr, msg)
session.quit()
log.info('Email sent')
except Exception as e: # pragma: no cover
log.error("Error sending email: %s" % e, exc_info=True)
def join(self):
"""Flush email queue by waiting the completion of the existing threads
:returns: None
"""
return [t.join(self.join_timeout) for t in self._threads]
def __del__(self):
"""Class destructor: wait for threads to terminate within a timeout"""
self.join()
bottle-cork-0.12.0/examples/templates/ 0000755 0001750 0001750 00000000000 12617467123 017474 5 ustar fede fede 0000000 0000000 bottle-cork-0.12.0/examples/templates/password_change_form.html 0000644 0001750 0001750 00000001470 12161350444 024545 0 ustar fede fede 0000000 0000000
Password change
Please insert your new password:
bottle-cork-0.12.0/examples/templates/login_form.html 0000644 0001750 0001750 00000003571 12166632355 022523 0 ustar fede fede 0000000 0000000
Login
Please insert your credentials:
Signup
Please insert your credentials:
Password reset
Please insert your credentials:
bottle-cork-0.12.0/examples/templates/password_reset_email.html 0000644 0001750 0001750 00000000424 12161350444 024564 0 ustar fede fede 0000000 0000000 Hello {{username}},
You are receiving this email because you requested a password reset on Cork Demo Webapp.
If you wish to complete the password reset please click on:
Confirm
bottle-cork-0.12.0/examples/templates/registration_email.html 0000644 0001750 0001750 00000000561 12161350444 024234 0 ustar fede fede 0000000 0000000 Hello {{username}},
You are receiving this email because you registered on Cork Demo Webapp on {{creation_date}}.
If you wish to complete the registration please click on:
Confirm
Your email address is: {{email_addr}}
Your role will be: {{role}}.
bottle-cork-0.12.0/examples/templates/admin_page.html 0000644 0001750 0001750 00000007650 12302755331 022446 0 ustar fede fede 0000000 0000000
Cork - Administration page
Welcome {{current_user.username}}, your role is: {{current_user.role}},
access time: {{current_user.session_accessed_time}}
Create new user:
Delete user:
Create new role:
Delete role:
Username | Role | Email | Description |
{% for u in users: %}
{{u[0]}} | {{u[1]}} | {{u[2]}} | {{u[2]}} |
{% endfor %}
Role | Level |
{% for r in roles: %}
{{r[0]}} | {{r[1]}} |
{% endfor %}
(Reload page to refresh)
bottle-cork-0.12.0/examples/test_real_ssl_smtp.py 0000644 0001750 0001750 00000000601 12312136477 021750 0 ustar fede fede 0000000 0000000 from cork import Cork
import logging
logging.basicConfig(format='localhost - - [%(asctime)s] %(message)s', level=logging.DEBUG)
log = logging.getLogger(__name__)
aaa = Cork('example_conf',
email_sender='federico.ceratto@gmail.com',
smtp_url='ssl://federico.ceratto@gmail.com:jckgtwesxgfrrqfi@smtp.gmail.com:587'
)
aaa.register('user', 'pass', 'federico.ceratto@gmail.com')
bottle-cork-0.12.0/examples/example.db 0000644 0001750 0001750 00000016000 12461704342 017427 0 ustar fede fede 0000000 0000000 SQLite format 3 @ -
.| #tableregisterregisterCREATE TABLE register (code TEXT PRIMARY KEY ASC,username TEXT ,role TEXT ,hash TEXT ,email_addr TEXT ,desc TEXT ,creation_date TEXT )/C indexsqlite_autoindex_register_1registerTtablerolesrolesCREATE TABLE roles (role TEXT PRIMARY KEY ASC,level INTEGER ))= indexsqlite_autoindex_roles_1roles tableusersusersCREATE TABLE users (username TEXT PRIMARY KEY ASC,role TEXT ,hash TEXT ,email_addr TEXT ,desc TEXT ,creation_date TEXT ,last_login TEXT ))= indexsqlite_autoindex_users_1users
T T ) =7+A adminadmincLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=admin@localhost.localadmin test user2012-10-28 20:50:26.286723
admin
user2
editor< admindspecial
user
editor admin
special
bottle-cork-0.12.0/examples/simple_webapp_using_sqlite.py 0000755 0001750 0001750 00000013361 12314377543 023474 0 ustar fede fede 0000000 0000000 #!/usr/bin/env python
#
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
# Released under GPLv3+ license, see LICENSE.txt
#
# Cork example web application
#
# The following users are already available:
# admin/admin, demo/demo
import bottle
from beaker.middleware import SessionMiddleware
from cork import Cork
from cork.backends import SQLiteBackend
import logging
logging.basicConfig(format='localhost - - [%(asctime)s] %(message)s', level=logging.DEBUG)
log = logging.getLogger(__name__)
bottle.debug(True)
def populate_backend():
b = SQLiteBackend('example.db', initialize=True)
b.connection.executescript("""
INSERT INTO users (username, email_addr, desc, role, hash, creation_date) VALUES
(
'admin',
'admin@localhost.local',
'admin test user',
'admin',
'cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=',
'2012-10-28 20:50:26.286723'
);
INSERT INTO roles (role, level) VALUES ('special', 200);
INSERT INTO roles (role, level) VALUES ('admin', 100);
INSERT INTO roles (role, level) VALUES ('editor', 60);
INSERT INTO roles (role, level) VALUES ('user', 50);
""")
return b
b = populate_backend()
aaa = Cork(backend=b, email_sender='federico.ceratto@gmail.com', smtp_url='smtp://smtp.magnet.ie')
app = bottle.app()
session_opts = {
'session.cookie_expires': True,
'session.encrypt_key': 'please use a random key and keep it secret!',
'session.httponly': True,
'session.timeout': 3600 * 24, # 1 day
'session.type': 'cookie',
'session.validate_key': True,
}
app = SessionMiddleware(app, session_opts)
# # Bottle methods # #
def postd():
return bottle.request.forms
def post_get(name, default=''):
return bottle.request.POST.get(name, default).strip()
@bottle.post('/login')
def login():
"""Authenticate users"""
username = post_get('username')
password = post_get('password')
aaa.login(username, password, success_redirect='/', fail_redirect='/login')
@bottle.route('/user_is_anonymous')
def user_is_anonymous():
if aaa.user_is_anonymous:
return 'True'
return 'False'
@bottle.route('/logout')
def logout():
aaa.logout(success_redirect='/login')
@bottle.post('/register')
def register():
"""Send out registration email"""
aaa.register(post_get('username'), post_get('password'), post_get('email_address'))
return 'Please check your mailbox.'
@bottle.route('/validate_registration/:registration_code')
def validate_registration(registration_code):
"""Validate registration, create user account"""
aaa.validate_registration(registration_code)
return 'Thanks. Go to login'
@bottle.post('/reset_password')
def send_password_reset_email():
"""Send out password reset email"""
aaa.send_password_reset_email(
username=post_get('username'),
email_addr=post_get('email_address')
)
return 'Please check your mailbox.'
@bottle.route('/change_password/:reset_code')
@bottle.view('password_change_form')
def change_password(reset_code):
"""Show password change form"""
return dict(reset_code=reset_code)
@bottle.post('/change_password')
def change_password():
"""Change password"""
aaa.reset_password(post_get('reset_code'), post_get('password'))
return 'Thanks. Go to login'
@bottle.route('/')
def index():
"""Only authenticated users can see this"""
aaa.require(fail_redirect='/login')
return 'Welcome! Admin page Logout'
@bottle.route('/restricted_download')
def restricted_download():
"""Only authenticated users can download this file"""
aaa.require(fail_redirect='/login')
return bottle.static_file('static_file', root='.')
@bottle.route('/my_role')
def show_current_user_role():
"""Show current user role"""
session = bottle.request.environ.get('beaker.session')
print "Session from simple_webapp", repr(session)
aaa.require(fail_redirect='/login')
return aaa.current_user.role
# Admin-only pages
@bottle.route('/admin')
@bottle.view('admin_page')
def admin():
"""Only admin users can see this"""
aaa.require(role='admin', fail_redirect='/sorry_page')
return dict(
current_user=aaa.current_user,
users=aaa.list_users(),
roles=aaa.list_roles()
)
@bottle.post('/create_user')
def create_user():
try:
aaa.create_user(postd().username, postd().role, postd().password)
return dict(ok=True, msg='')
except Exception, e:
return dict(ok=False, msg=e.message)
@bottle.post('/delete_user')
def delete_user():
try:
aaa.delete_user(post_get('username'))
return dict(ok=True, msg='')
except Exception, e:
print repr(e)
return dict(ok=False, msg=e.message)
@bottle.post('/create_role')
def create_role():
try:
aaa.create_role(post_get('role'), post_get('level'))
return dict(ok=True, msg='')
except Exception, e:
return dict(ok=False, msg=e.message)
@bottle.post('/delete_role')
def delete_role():
try:
aaa.delete_role(post_get('role'))
return dict(ok=True, msg='')
except Exception, e:
return dict(ok=False, msg=e.message)
# Static pages
@bottle.route('/login')
@bottle.view('login_form')
def login_form():
"""Serve login form"""
return {}
@bottle.route('/sorry_page')
def sorry_page():
"""Serve sorry page"""
return 'Sorry, you are not authorized to perform this action
'
# # Web application main # #
def main():
# Start the Bottle webapp
bottle.debug(True)
bottle.run(app=app, quiet=False, reloader=False)
if __name__ == "__main__":
main()
bottle-cork-0.12.0/examples/example_conf/ 0000755 0001750 0001750 00000000000 12617467123 020136 5 ustar fede fede 0000000 0000000 bottle-cork-0.12.0/examples/example_conf/register.json 0000644 0001750 0001750 00000000002 12166632355 022645 0 ustar fede fede 0000000 0000000 {} bottle-cork-0.12.0/examples/example_conf/users.json 0000644 0001750 0001750 00000000722 12512453671 022170 0 ustar fede fede 0000000 0000000 {"admin": {"hash": "cLzRnzbEwehP6ZzTREh3A4MXJyNo+TV8Hs4//EEbPbiDoo+dmNg22f2RJC282aSwgyWv/O6s3h42qrA6iHx8yfw=", "email_addr": "admin@localhost.local", "role": "admin", "creation_date": "2012-10-28 20:50:26.286723", "desc": "admin test user"}, "": {"hash": "cFFusHJ5BZ07G3KhrkeVjB6pIyIGoMP6+BtK0JBtpU/EjhY6v3nvnOZgLqoniJQ9OXn2KFV+SJmZSyPH4Og0HiQ=", "email_addr": "@localhost.local", "role": "user", "creation_date": "2012-10-28 20:50:26.286723", "desc": " test user"}} bottle-cork-0.12.0/examples/example_conf/roles.json 0000644 0001750 0001750 00000000050 12302756620 022142 0 ustar fede fede 0000000 0000000 {"admin": 100, "editor": 60, "user": 50} bottle-cork-0.12.0/examples/cork/ 0000755 0001750 0001750 00000000000 12617467123 016434 5 ustar fede fede 0000000 0000000 bottle-cork-0.12.0/examples/cork/base_backend.py 0000644 0001750 0001750 00000000000 12512453671 030203 1bottle-cork-0.12.0/cork/base_backend.py ustar fede fede 0000000 0000000 bottle-cork-0.12.0/examples/cork/SUMMARY 0000644 0001750 0001750 00000000112 12312671642 017501 0 ustar fede fede 0000000 0000000 2 3
test.py k
mongo k
mysql k
sqlalch k
paralle k
sqlite k
bottle-cork-0.12.0/examples/cork/mongodb_backend.py 0000644 0001750 0001750 00000000000 12520166131 031420 1bottle-cork-0.12.0/cork/mongodb_backend.py ustar fede fede 0000000 0000000 bottle-cork-0.12.0/examples/cork/__pycache__/ 0000755 0001750 0001750 00000000000 12617467123 020644 5 ustar fede fede 0000000 0000000 bottle-cork-0.12.0/examples/cork/__pycache__/__init__.cpython-34.pyc 0000644 0001750 0001750 00000000477 12617463330 025033 0 ustar fede fede 0000000 0000000
e>VS @ s8 d d l m Z m Z m Z m Z m Z m Z m Z d S) )CorkJsonBackendAAAException
AuthExceptionMailer FlaskCorkRedirectN)corkr r r r r r r r
r
>/home/fede/newhome/projects/bottle-cork/tests/cork/__init__.py s bottle-cork-0.12.0/examples/cork/__pycache__/base_backend.cpython-34.pyc 0000644 0001750 0001750 00000002261 12516462076 025652 0 ustar fede fede 0000000 0000000
W*U @ sX d Z Gd d d e Z d d Z Gd d d e Z Gd d d e Z d S)
z4
.. module:: backend.py
:synopsis: Base Backend.
c @ s e Z d Z d Z d S)BackendIOExceptionzGeneric Backend I/O ExceptionN)__name__
__module____qualname____doc__ r r B/home/fede/newhome/projects/bottle-cork/tests/cork/base_backend.pyr
s r c O s
t d S)N)NotImplementedError)argskwargsr r r ni s r c @ s( e Z d Z d Z e Z e Z e Z d S)Backendz7Base Backend class - to be subclassed by real backends.N)r r r r r
save_users
save_rolessave_pending_registrationsr r r r r s r c @ s: e Z d Z d Z e Z e Z e Z e Z e Z e Z
d S)Tablez5Base Table class - to be subclassed by real backends.N)r r r r r __len____contains____setitem____getitem____iter__ iteritemsr r r r r s r N)r Exceptionr r objectr r r r r r s bottle-cork-0.12.0/examples/cork/__pycache__/json_backend.cpython-34.pyc 0000644 0001750 0001750 00000011506 12520666144 025710 0 ustar fede fede 0000000 0000000
D