Commits

Andriy Kornatskyy committed f6167eb

Added authorize handler decorator; updated public demo to use it

Comments (0)

Files changed (22)

demos/public/content/templates/membership/business-only.html

+<%inherit file="/shared/master.html"/>
+<%block name="title">Business Accounts</%block>
+<div id="business-only">
+    <h2>
+        Business Accounts</h2>
+    <p>
+        This is restricted area.
+    </p>
+</div>

demos/public/content/templates/membership/members-only.html

+<%inherit file="/shared/master.html"/>
+<%block name="title">Members Only</%block>
+<div id="members-only">
+    <h2>
+        Members Only</h2>
+    <p>
+        This is restricted area.
+    </p>
+</div>

demos/public/content/templates/membership/signup.html

                 </p>
                 <p>
                 ${account.account_type.label('Account Type:')}
-                ${account.account_type.radio(choices=(('1', 'User'), ('2', 'Business')))}
+                ${account.account_type.radio(choices=account_types.model)}
                 ${account.account_type.error()}
                 </p>
                 <p>

demos/public/content/templates/public/about.html

         <li><a href="${path_for('internal_error')}">Internal Error</a></li>
         </%block>
     </ul>
+    <p><a href="${path_for('members_only')}">Members only</a> page is
+    protected by <i>@authorize</i> decorator for registered members.
+    <a href="${path_for('business_only')}">Business only</a> page is
+    protected by <i>@authorize(roles=['business'])</i> decorator
+    and accessable for business accounts only.
+    </p>
 </div>

demos/public/i18n/en/LC_MESSAGES/membership.po

 msgid "Favorite color"
 msgstr "Favorite color"
 
-#: src/membership/service/bridge.py:41 src/membership/service/bridge.py:42
+#: src/membership/service/bridge.py:37
+msgid "User"
+msgstr "User"
+
+#: src/membership/service/bridge.py:38
+msgid "Business"
+msgstr "Business"
+
+#: src/membership/service/bridge.py:47
 msgid "The username or password provided is incorrect."
 msgstr "The username or password provided is incorrect."
 
-#: src/membership/service/bridge.py:52 src/membership/service/bridge.py:55
+#: src/membership/service/bridge.py:61
 msgid "The user with such username is already registered. Please try another."
 msgstr "The user with such username is already registered. Please try another."
 
-#: src/membership/service/bridge.py:57 src/membership/service/bridge.py:60
+#: src/membership/service/bridge.py:66
 msgid ""
 "The system was unable to create an account for you. Please try again later."
 msgstr ""
 "The system was unable to create an account for you. Please try again later."
 
-#: src/membership/web/views.py:107
+#: src/membership/web/views.py:107 src/membership/web/views.py:112
 msgid ""
 "Your registration request has been queued. Please wait while your request "
 "will be processed. If your request fails please try again."

demos/public/i18n/membership.po

 msgid "Favorite color"
 msgstr "Favorite color"
 
-#: src/membership/service/bridge.py:41 src/membership/service/bridge.py:42
+#: src/membership/service/bridge.py:37
+msgid "User"
+msgstr "User"
+
+#: src/membership/service/bridge.py:38
+msgid "Business"
+msgstr "Business"
+
+#: src/membership/service/bridge.py:47
 msgid "The username or password provided is incorrect."
 msgstr "The username or password provided is incorrect."
 
-#: src/membership/service/bridge.py:52 src/membership/service/bridge.py:55
+#: src/membership/service/bridge.py:61
 msgid "The user with such username is already registered. Please try another."
 msgstr "The user with such username is already registered. Please try another."
 
-#: src/membership/service/bridge.py:57 src/membership/service/bridge.py:60
+#: src/membership/service/bridge.py:66
 msgid ""
 "The system was unable to create an account for you. Please try again later."
 msgstr ""
 "The system was unable to create an account for you. Please try again later."
 
-#: src/membership/web/views.py:107
+#: src/membership/web/views.py:107 src/membership/web/views.py:112
 msgid ""
 "Your registration request has been queued. Please wait while your request "
 "will be processed. If your request fails please try again."

demos/public/i18n/ru/LC_MESSAGES/membership.po

 msgid "Favorite color"
 msgstr "Любимый цвет"
 
+#: src/membership/service/bridge.py:37
+msgid "User"
+msgstr "Пользователь"
+
+#: src/membership/service/bridge.py:38
+msgid "Business"
+msgstr "Бизнес"
+
 #: src/membership/service/bridge.py:40
 msgid "The username or password provided is incorrect."
 msgstr "Предоставленное имя пользователя или пароль неверные."

demos/public/src/config.py

         'http_errors': defaultdict(lambda: 'http500', {
             # HTTP status code: route name
             400: 'http400',
+            401: 'signin',
             403: 'http403',
             404: 'http404',
             500: 'http500',

demos/public/src/membership/models.py

     def __init__(self):
         self.email = u('')
         self.display_name = u('')
-        self.account_type = 1  # 1 - user, 2 - business
+        self.account_type = 'user'
 
 
 class Registration(object):

demos/public/src/membership/repository/caching.py

         # TODO:
         return self.inner.has_account(username)
 
+    def user_roles(self, username):
+        # TODO:
+        return self.inner.user_roles(username)
+
     def create_account(self, registration):
         # TODO:
         return self.inner.create_account(registration)

demos/public/src/membership/repository/contract.py

     def has_account(self, username):
         return False
 
+    def user_roles(self, username):
+        return tuple([])
+
     def create_account(self, registration):
         return False

demos/public/src/membership/repository/mock.py

 
 class MembershipRepository(IMembershipRepository):
     credentials = {
-            'demo': u('P@ssw0rd')
+            'demo': u('P@ssw0rd'),
+            'biz': u('P@ssw0rd')
+    }
+    roles = {
+            'demo': tuple(['user']),
+            'biz': tuple(['business'])
     }
 
     def authenticate(self, credential):
     def has_account(self, username):
         return username in self.credentials
 
+    def user_roles(self, username):
+        return self.roles.get(username, None)
+
     def create_account(self, registration):
         credential = registration.credential
         self.credentials[credential.username] = credential.password
+        self.roles[credential.username] = tuple(
+                [registration.account.account_type])
         return True

demos/public/src/membership/service/bridge.py

 
     @attribute
     def password_questions(self):
-        questions = {
+        return {
                 '1': self.gettext('Favorite number'),
                 '2': self.gettext('City of birth'),
                 '3': self.gettext('Favorite color')
         }
-        return questions
+
+    @attribute
+    def account_types(self):
+        return {
+                'user': self.gettext('User'),
+                'business': self.gettext('Business')
+        }
 
     def authenticate(self, credential):
         assert isinstance(credential, Credential)
             return False
         return True
 
+    def roles(self, username):
+        return self.repository.membership.user_roles(username)
+
     def create_account(self, registration):
         assert isinstance(registration, Registration)
         if not self.validate(registration, registration_validator):

demos/public/src/membership/service/contract.py

     def password_questions(self):
         return {'0': 'None'}
 
+    @attribute
+    def account_types(self):
+        return {'0': 'None'}
+
     def authenticate(self, credential):
         assert isinstance(credential, Credential)
         return False
 
+    def roles(self, username):
+        return tuple([])
+
     def create_account(self, registration):
         assert isinstance(registration, Registration)
         return False

demos/public/src/membership/validation.py

 from wheezy.validation import Validator
 from wheezy.validation.rules import compare
 from wheezy.validation.rules import length
-from wheezy.validation.rules import required
+from wheezy.validation.rules import one_of
 from wheezy.validation.rules import range
 from wheezy.validation.rules import relative_date
+from wheezy.validation.rules import required
 
 
 _ = lambda s: s
 account_validator = Validator({
     'email': [required, length(min=6, max=30)],
     'display_name': [required, length(max=30)],
-    'account_type': [required, range(min=1, max=2)]
+    'account_type': [required, one_of(('user', 'business'))]
 })
 
 registration_validator = Validator({
     'credential': credential_validator,
     'account': account_validator,
     'answer': [required, length(min=1, max=20)],
+    'questionid': [required, range(min=1, max=3)],
     'date_of_birth': [
         required,
         relative_date(

demos/public/src/membership/web/tests/test_views.py

         """
         errors = self.signup(
                 username='john',
-                display_name='John',
+                display_name='John Smith',
                 email='john@somewhere.com',
                 date_of_birth='1987/2/7',
                 password='P@ssw0rd',
         assert 200 == self.client.follow()
         assert AUTH_COOKIE in self.client.cookies
         assert RESUBMISSION_NAME not in self.client.cookies
-        assert 'Welcome <b>john' in self.client.content
+        assert 'Welcome <b>John Smith' in self.client.content
 
     def test_if_authenticated_redirect(self):
         """ If user is already authenticated redirect
         client.cookies[RESUBMISSION_NAME] = '100'
         page.signup()
         SignUpPage(client)
+
+
+class MembersOnlyTestCase(unittest.TestCase, SignInMixin):
+
+    def setUp(self):
+        self.client = WSGIClient(main)
+
+    def tearDown(self):
+        del self.client
+        self.client = None
+
+    def test_unauthorized(self):
+        """ Ensure unauthorized user is redirected to signin page.
+        """
+        self.client.get('/en/members-only')
+        assert 200 == self.client.follow()
+        SignInPage(self.client)
+
+    def test_authorized(self):
+        """ Ensure authorized user is able to see restricted page.
+        """
+        self.signin('demo', 'P@ssw0rd')
+        assert 200 == self.client.follow()
+        assert 200 == self.client.get('/en/members-only')
+        assert 'Members Only' in self.client.content
+
+
+class BusinessOnlyTestCase(unittest.TestCase, SignInMixin):
+
+    def setUp(self):
+        self.client = WSGIClient(main)
+
+    def tearDown(self):
+        del self.client
+        self.client = None
+
+    def test_unauthorized(self):
+        """ Ensure unauthorized user is redirected to signin page.
+        """
+        self.client.get('/en/business-only')
+        assert 200 == self.client.follow()
+        SignInPage(self.client)
+
+    def test_authorized_but_not_business(self):
+        """ Ensure authorized user that is not in business role
+            is not able to see restricted page.
+        """
+        self.signin('demo', 'P@ssw0rd')
+        assert 200 == self.client.follow()
+        assert 302 == self.client.get('/en/business-only')
+        assert '/en/signin' in self.client.headers['Location'][0]
+
+    def test_authorized_in_business_role(self):
+        """ Ensure authorized user in business role
+            is able to see restricted page.
+        """
+        self.signin('biz', 'P@ssw0rd')
+        assert 200 == self.client.follow()
+        assert 200 == self.client.get('/en/business-only')
+        assert 'Business Accounts' in self.client.content

demos/public/src/membership/web/urls.py

 
 from wheezy.routing import url
 
+from membership.web.views import BusinessOnlyHandler
+from membership.web.views import MembersOnlyHandler
 from membership.web.views import SignInHandler
 from membership.web.views import SignOutHandler
 from membership.web.views import SignUpHandler
     url('signin', SignInHandler, name='signin'),
     url('signout', SignOutHandler, name='signout'),
     url('signup', SignUpHandler, name='signup'),
+    url('members-only', MembersOnlyHandler, name='members_only'),
+    url('business-only', BusinessOnlyHandler, name='business_only')
 ]

demos/public/src/membership/web/views.py

 from wheezy.core.descriptors import attribute
 from wheezy.http import bad_request
 from wheezy.security import Principal
-from wheezy.web.caching import handler_cache
+from wheezy.web import authorize
+from wheezy.web import handler_cache
 from wheezy.web.handlers import BaseHandler
 
 from config import none_cache_profile
             return self.get(credential)
         self.principal = Principal(
                 id=credential.username,
-                alias=credential.username)
+                alias=credential.username,
+                roles=tuple(self.factory.membership.roles(
+                    credential.username)))
         del self.xsrf_token
         return self.redirect_for('default')
 
                 model=self.model,
                 questions=sorted(
                     self.factory.membership.password_questions.items(),
-                    key=itemgetter(1))
-                )
+                    key=itemgetter(1)),
+                account_types=sorted(
+                    self.factory.membership.account_types.items(),
+                    key=itemgetter(1)))
 
     def post(self):
         if not self.validate_resubmission():
             return self.get(registration)
         self.principal = Principal(
                 id=registration.credential.username,
-                alias=registration.credential.username)
+                alias=registration.account.display_name,
+                roles=tuple(self.factory.membership.roles(
+                    registration.credential.username)))
         del self.resubmission
         return self.redirect_for('default')
+
+
+class MembersOnlyHandler(BaseHandler):
+
+    @attribute
+    def translation(self):
+        return self.translations['membership']
+
+    @authorize
+    def get(self, registration=None):
+        return self.render_response('membership/members-only.html')
+
+
+class BusinessOnlyHandler(BaseHandler):
+
+    @attribute
+    def translation(self):
+        return self.translations['membership']
+
+    @authorize(roles=['business'])
+    def get(self, registration=None):
+        return self.render_response('membership/business-only.html')
 #sys.path.insert(0, os.path.abspath('.'))
 sys.path.extend([
     os.path.abspath(os.path.join('..', '..', 'wheezy.core', 'src')),
-    os.path.abspath(os.path.join('..', '..', 'wheezy.http', 'src')),    
-    os.path.abspath(os.path.join('..', '..', 'wheezy.caching', 'src')),    
+    os.path.abspath(os.path.join('..', '..', 'wheezy.http', 'src')),
+    os.path.abspath(os.path.join('..', '..', 'wheezy.caching', 'src')),
     os.path.abspath(os.path.join('..', '..', 'wheezy.html', 'src')),
     os.path.abspath(os.path.join('..', '..', 'wheezy.routing', 'src')),
     os.path.abspath(os.path.join('..', '..', 'wheezy.security', 'src')),
 # built documents.
 #
 # The short X.Y version.
-version = '0.1'
+version = '0.1.1'
 # The full version, including alpha/beta/rc tags.
-release = '0.1'
+release = '0.1.1'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
 
 setup(
     name='wheezy.web',
-    version='0.1',
+    version='0.1.1',
     description='A lightweight web library',
     long_description=README,
     url='https://bitbucket.org/akorn/wheezy.web',

src/wheezy/web/__init__.py

+
+""" ``web`` package.
+"""
+
+from wheezy.web.authorization import authorize
+from wheezy.web.caching import handler_cache

src/wheezy/web/authorization.py

+
+"""
+"""
+
+from wheezy.http import unauthorized
+
+
+def authorize(wrapped=None, roles=None):
+    """ Checks if user accessing protected resource is
+        authenticated and optionally in one of allowed ``roles``.
+
+        ``roles`` - a list of authorized roles.
+
+        Check if call is authenticated::
+
+            class MyHandler(BaseHandler):
+                @authorize
+                def get(self):
+                    return response
+
+        Check if principal in role::
+
+            class MyHandler(BaseHandler):
+                @authorize(roles=('operator', 'manager'))
+                def get(self):
+                    return response
+    """
+    def decorate(func):
+        if roles:
+            def check_roles(handler, *args, **kwargs):
+                principal = handler.principal
+                if principal:
+                    principal_roles = principal.roles
+                    if any(role in principal_roles for role in roles):
+                        return func(handler, *args, **kwargs)
+                    else:
+                        return unauthorized(handler.options)
+                else:
+                    return unauthorized(handler.options)
+            return check_roles
+        else:
+            def check_authenticated(handler, *args, **kwargs):
+                if handler.principal:
+                    return func(handler, *args, **kwargs)
+                else:
+                    return unauthorized(handler.options)
+            return check_authenticated
+    if wrapped is None:
+        return decorate
+    else:
+        return decorate(wrapped)