diff --git a/ordr2/security.py b/ordr2/security.py new file mode 100644 index 0000000..3fe16aa --- /dev/null +++ b/ordr2/security.py @@ -0,0 +1,55 @@ +''' User Authentication and Authorization ''' + +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.security import Authenticated, Everyone + +from .models import User + + +class AuthenticationPolicy(AuthTktAuthenticationPolicy): + ''' How to authenticate users ''' + + def authenticated_userid(self, request): + ''' returns the id of an authenticated user + + heavy lifting done in get_user() attached to request + ''' + user = request.user + if user is not None: + return user.id + + def effective_principals(self, request): + ''' returns a list of principals for the user ''' + principals = [Everyone] + user = request.user + if user is not None: + principals.append(Authenticated) + principals.append(user.principal) + principals.extend(user.role_principals) + return principals + + +def get_user(request): + ''' retrieves the user object by the unauthenticated user id ''' + user_id = request.unauthenticated_userid + if user_id is not None: + user = request.dbsession.query(User).filter_by(id=user_id).first() + if user and user.is_active: + return user + return None + + +def includeme(config): + ''' initializing authentication and authorization for the Pyramid app + + Activate this setup using ``config.include('ordr2.security')``. + ''' + settings = config.get_settings() + authn_policy = AuthenticationPolicy( + settings['auth.secret'], + hashalg='sha512', + ) + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(ACLAuthorizationPolicy()) + config.add_request_method(get_user, 'user', reify=True) diff --git a/tests/__init__.py b/tests/__init__.py index 2cf08d1..c8b2d0d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,74 @@ ''' Test package for ordr2. ''' + +import pytest +import transaction + +from pyramid import testing + + +APP_SETTINGS = { + 'sqlalchemy.url': 'sqlite:///:memory:', + 'auth.secret': 'not-very-secure', + 'session.secret': 'not-very-secure', + 'session.auto_csrf': True + } + + +# helpers + +def create_users(db): + ''' set up some well known example users ''' + from ordr2.models import Role, User + stubs = [ + ('Graham', 'Chapman', Role.UNVALIDATED), + ('John', 'Cleese', Role.NEW), + ('Terry', 'Gilliam', Role.USER), + ('Eric', 'Idle', Role.PURCHASER), + ('Terry', 'Jones', Role.ADMIN), + ('Michael', 'Palin', Role.INACTIVE) + ] + for i, stub in enumerate(stubs): + first_name, last_name, role = stub + user = User( + id=i+1, + username=first_name + last_name, + first_name = first_name, + last_name = last_name, + email = last_name.lower() + '@example.com', + role=role, + password_hash = first_name.lower() + ) + db.add(user) + + +# fixtures + +@pytest.fixture(scope='session') +def app_config(): + ''' fixture for tests requiring a pyramid.testing setup ''' + with testing.testConfig(settings=APP_SETTINGS) as config: + config.include('pyramid_jinja2') + #config.include('pyramid_mailer.testing') + yield config + + +@pytest.fixture(scope='function') +def dbsession(app_config): + ''' fixture for testing with database connection ''' + from ordr2.models.meta import Base + from ordr2.models import ( + get_engine, + get_session_factory, + get_tm_session + ) + + settings = app_config.get_settings() + engine = get_engine(settings) + session_factory = get_session_factory(engine) + session = get_tm_session(session_factory, transaction.manager) + Base.metadata.create_all(engine) + + yield session + + transaction.abort() + Base.metadata.drop_all(engine) diff --git a/tests/security.py b/tests/security.py new file mode 100644 index 0000000..000358e --- /dev/null +++ b/tests/security.py @@ -0,0 +1,105 @@ +''' Tests for ordr2.security ''' + +import pytest + +from . import app_config, dbsession, create_users + + +# tests for ordr2.security.AuthenticationPolicy + +def test_authenticated_userid_no_user(): + ''' test if authenticated user id is None if no active user present ''' + from pyramid.testing import DummyRequest + from ordr2.security import AuthenticationPolicy + + request = DummyRequest(user=None) + policy = AuthenticationPolicy(secret='') + + assert policy.authenticated_userid(request) is None + + +def test_authenticated_userid_with_user(): + ''' test if authenticated user id is the id of the user ''' + from pyramid.testing import DummyRequest + from ordr2.models import User + from ordr2.security import AuthenticationPolicy + + user = User(id=3) + request = DummyRequest(user=user) + policy = AuthenticationPolicy(secret='') + + assert policy.authenticated_userid(request) == 3 + + +def test_effective_principals_no_user(): + ''' test the effective principals if no user is authenticated ''' + from pyramid.testing import DummyRequest + from pyramid.security import Everyone + from ordr2.security import AuthenticationPolicy + + request = DummyRequest(user=None) + policy = AuthenticationPolicy(secret='') + + assert policy.effective_principals(request) == [Everyone] + + +@pytest.mark.parametrize( + 'role_name, role_principals', [ + ('UNVALIDATED', ['role:unvalidated']), + ('NEW', ['role:new']), + ('USER', ['role:user']), + ('PURCHASER', ['role:purchaser', 'role:user']), + ('ADMIN', ['role:admin', 'role:purchaser', 'role:user']), + ('INACTIVE', ['role:inactive']) + ] + ) +def test_effective_principals_with_user(role_name, role_principals): + ''' test the effective principals if a user is authenticated ''' + from pyramid.testing import DummyRequest + from pyramid.security import Authenticated, Everyone + from ordr2.models import User, Role + from ordr2.security import AuthenticationPolicy + + role = Role[role_name] + user = User(id=3, role=role) + request = DummyRequest(user=user) + policy = AuthenticationPolicy(secret='') + + expected = [Everyone, Authenticated, 'user:3'] + expected.extend(role_principals) + assert policy.effective_principals(request) == expected + + +# tests for the get_user function + +def test_get_user_no_unauthenticated_user_id(): + ''' get_user() should return None if unauthenticated_userid is None ''' + from pyramid.testing import DummyRequest + from ordr2.security import get_user + + request = DummyRequest(unauthenticated_userid=None) + + assert get_user(request) is None + + +@pytest.mark.parametrize( + 'user_id', [ + 3, # active user, must work + pytest.mark.xfail(1), # inactive user, must fail + pytest.mark.xfail(1969), # unknown user id, must fail + ] + ) +def test_get_user_no_unauthenticated_user_id(user_id, dbsession): + ''' get_user() should return None if unauthenticated_userid is None ''' + from collections import namedtuple + from ordr2.models import User, Role + from ordr2.security import get_user + + create_users(dbsession) + # pyramid.testing.DummyRequest can't be used, since the parameter + # unauthenticated_userid cannot be set. A named tuple is used instead + Request = namedtuple('Request', 'dbsession, unauthenticated_userid') + request = Request(dbsession=dbsession, unauthenticated_userid=user_id) + user = get_user(request) + + assert isinstance(user, User)