Browse Source

added password hashing and checking to models.User

master
Holger Frey 7 years ago
parent
commit
9e6b0a43d4
  1. 32
      ordr2/models/users.py
  2. 4
      ordr2/scripts/initializedb.py
  3. 12
      ordr2/security.py
  4. 46
      tests/models/users.py

32
ordr2/models/users.py

@ -3,6 +3,7 @@ @@ -3,6 +3,7 @@
import enum
from datetime import datetime
from passlib.context import CryptContext
from sqlalchemy import (
Column,
Date,
@ -14,6 +15,12 @@ from sqlalchemy import ( @@ -14,6 +15,12 @@ from sqlalchemy import (
from .meta import Base
#: create a crypt context for password hashes
#: configured in :mod:`ordr2.security.includeme()`
#: this is not in :mod:`ordr2.security` to avoid circular imports
passlib_context = CryptContext()
class Role(enum.Enum):
''' roles of user accounts '''
@ -87,6 +94,31 @@ class User(Base): @@ -87,6 +94,31 @@ class User(Base):
''' is true if the user has an active role '''
return self.role in (Role.USER, Role.PURCHASER, Role.ADMIN)
def set_password(self, password):
''' hashes a password using :mod:`ordr2.security.passlib_context` '''
self.password_hash = passlib_context.hash(password)
def check_password(self, password):
''' checks a password against a stored password hash
if the password algorithm is considered deprecated, the stored hash
will be updated using the current algorithm
'''
ok, new_hash = passlib_context.verify_and_update(
password,
self.password_hash
)
if not ok:
# password does not match, return False
return False
elif new_hash:
# algorithm is deprecated, update hash with new algorithm
self.password_hash = new_hash
# password match, return True
return True
def __str__(self):
''' string representation '''
return str(self.username)

4
ordr2/scripts/initializedb.py

@ -43,5 +43,5 @@ def main(argv=sys.argv): @@ -43,5 +43,5 @@ def main(argv=sys.argv):
with transaction.manager:
dbsession = get_tm_session(session_factory, transaction.manager)
model = MyModel(name='one', value=1)
dbsession.add(model)
# model = MyModel(name='one', value=1)
# dbsession.add(model)

12
ordr2/security.py

@ -1,15 +1,10 @@ @@ -1,15 +1,10 @@
''' User Authentication and Authorization '''
from passlib.context import CryptContext
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.security import Authenticated, Everyone
from .models import User
#: create a crypt context for password hashes, configured in :func:`includeme()`
passlib_context = CryptContext()
from ordr2.models.users import User, passlib_context
class AuthenticationPolicy(AuthTktAuthenticationPolicy):
@ -54,7 +49,7 @@ def get_user(request): @@ -54,7 +49,7 @@ def get_user(request):
def includeme(config):
''' initializing authentication and authorization for the Pyramid app
''' initializing authentication, authorization and password hash settings
Activate this setup using ``config.include('ordr2.security')``.
'''
@ -70,5 +65,6 @@ def includeme(config): @@ -70,5 +65,6 @@ def includeme(config):
)
config.set_authentication_policy(authn_policy)
config.set_authorization_policy(ACLAuthorizationPolicy())
config.add_request_method(get_user, 'user', reify=True)
# attach the get_user function returned by get_user_closure()
config.add_request_method(get_user, 'user', reify=True)

46
tests/models/users.py

@ -80,6 +80,52 @@ def test_user_is_active(role_name, is_active): @@ -80,6 +80,52 @@ def test_user_is_active(role_name, is_active):
assert user.is_active == is_active
def test_user_set_password():
''' test password hash generation '''
from ordr2.models.users import User, passlib_context
passlib_context.update(schemes=['argon2', 'bcrypt'])
user = User(password_hash=None)
password = 'Fish Slapping Dance'
user.set_password(password)
assert user.password_hash.startswith('$argon2')
assert password not in user.password_hash
@pytest.mark.parametrize(
'password', [
'Fish Slapping Dance',
pytest.mark.xfail('Argument Clinic')
]
)
def test_user_check_password_ok(password):
''' test password check '''
from ordr2.models.users import User, passlib_context
passlib_context.update(schemes=['argon2', 'bcrypt'], deprecated='auto')
user = User(password_hash=None)
user.set_password('Fish Slapping Dance')
assert user.check_password(password)
def test_user_check_password_deprecated_hash():
''' test password check updates deprecated hash with new algorithm '''
from ordr2.models.users import User
from ordr2.security import passlib_context
passlib_context.update(schemes=['argon2', 'bcrypt'], deprecated='auto')
password = 'Fish Slapping Dance'
bcrypt_hash = passlib_context.hash(password, scheme='bcrypt')
user = User(password_hash=bcrypt_hash)
assert user.check_password(password)
assert user.password_hash != bcrypt_hash
assert user.password_hash.startswith('$argon2')
def test_user_string_representation():
''' test the string representation of the user '''
from ordr2.models.users import User, Role