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. 6
      ordr2/scripts/initializedb.py
  3. 12
      ordr2/security.py
  4. 46
      tests/models/users.py

32
ordr2/models/users.py

@ -3,6 +3,7 @@
import enum import enum
from datetime import datetime from datetime import datetime
from passlib.context import CryptContext
from sqlalchemy import ( from sqlalchemy import (
Column, Column,
Date, Date,
@ -14,6 +15,12 @@ from sqlalchemy import (
from .meta import Base 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): class Role(enum.Enum):
''' roles of user accounts ''' ''' roles of user accounts '''
@ -87,6 +94,31 @@ class User(Base):
''' is true if the user has an active role ''' ''' is true if the user has an active role '''
return self.role in (Role.USER, Role.PURCHASER, Role.ADMIN) 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): def __str__(self):
''' string representation ''' ''' string representation '''
return str(self.username) return str(self.username)

6
ordr2/scripts/initializedb.py

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

12
ordr2/security.py

@ -1,15 +1,10 @@
''' User Authentication and Authorization ''' ''' User Authentication and Authorization '''
from passlib.context import CryptContext
from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.security import Authenticated, Everyone from pyramid.security import Authenticated, Everyone
from .models import User from ordr2.models.users import User, passlib_context
#: create a crypt context for password hashes, configured in :func:`includeme()`
passlib_context = CryptContext()
class AuthenticationPolicy(AuthTktAuthenticationPolicy): class AuthenticationPolicy(AuthTktAuthenticationPolicy):
@ -54,7 +49,7 @@ def get_user(request):
def includeme(config): 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')``. Activate this setup using ``config.include('ordr2.security')``.
''' '''
@ -70,5 +65,6 @@ def includeme(config):
) )
config.set_authentication_policy(authn_policy) config.set_authentication_policy(authn_policy)
config.set_authorization_policy(ACLAuthorizationPolicy()) 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):
assert user.is_active == 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(): def test_user_string_representation():
''' test the string representation of the user ''' ''' test the string representation of the user '''
from ordr2.models.users import User, Role from ordr2.models.users import User, Role