Commits

Peter Sagerson  committed 8d1d416

Fix for otp_agents.decorators.otp_required.

otp_required no longer ignores if_configured when accept_trusted_agent is True.

  • Participants
  • Parent commits 5b16152

Comments (0)

Files changed (13)

File django-otp-agents/CHANGES

+v0.1.4 -- August 20, 2013 - Fix in otp_required
+-----------------------------------------------
+
+- :func:`~otp_agents.decorators.otp_required` no longer ignores
+  ``if_configured`` when ``accept_trusted_agent`` is ``True``. The default
+  behavior for ``login_url`` is also now more explicit.
+
+
 v0.1.3 -- July 3, 2013 - Decorator improvement
 -----------------------------------------------
 
 ---------------------------------------------
 
 - Major unit test cleanup. Tests should pass or be skipped under all supported
-  versions of Django, with or without custom users and timzeone support.
+  versions of Django, with or without custom users and timezone support.
 
 
 v0.1.1 - October 8, 2012 - Django < 1.4

File django-otp-agents/docs/source/conf.py

 # The short X.Y version.
 version = '0.1'
 # The full version, including alpha/beta/rc tags.
-release = '0.1.3'
+release = '0.1.4'
 
 
 # The language for content autogenerated by Sphinx. Refer to documentation

File django-otp-agents/otp_agents/decorators.py

 from functools import wraps
 
+import django_agent_trust.conf
+import django_otp.conf
 from django_otp.decorators import otp_required as real_otp_required
-from django_agent_trust.decorators import trusted_agent_required
 
 
 def otp_required(view=None, redirect_field_name='next', login_url=None, if_configured=False, accept_trusted_agent=False):
     Similar to :func:`~django_otp.decorators.otp_required`, but with an extra
     argument.
 
+    The default value for ``login_url`` depends on the value of
+    ``accept_trusted_agent``. If ``True``, we'll use
+    :setting:`AGENT_LOGIN_URL`; otherwise, we'll use :setting:`OTP_LOGIN_URL`.
+
     :param bool accept_trusted_agent: If ``True``, we'll accept a trusted
         agent in lieu of OTP verification. Default is ``False``.
     """
     def decorator(view_func):
         @wraps(view_func)
         def _wrapped_view(request, *args, **kwargs):
-            if request.user.is_verified() or (accept_trusted_agent and request.agent.is_trusted):
-                x_required = lambda v: v
-            elif accept_trusted_agent:
-                x_required = trusted_agent_required(redirect_field_name=redirect_field_name, login_url=login_url)
+            if accept_trusted_agent and request.agent.is_trusted:
+                _decorator = lambda v: v
             else:
-                x_required = real_otp_required(redirect_field_name=redirect_field_name, login_url=login_url, if_configured=if_configured)
+                _login_url = login_url
+                if _login_url is None:
+                    _login_url = django_agent_trust.conf.settings.AGENT_LOGIN_URL if accept_trusted_agent else django_otp.conf.settings.OTP_LOGIN_URL
+                _decorator = real_otp_required(redirect_field_name=redirect_field_name, login_url=_login_url, if_configured=if_configured)
 
-            return x_required(view_func)(request, *args, **kwargs)
+            return _decorator(view_func)(request, *args, **kwargs)
 
         return _wrapped_view
 

File django-otp-agents/otp_agents/tests/__init__.py

 
         self.assertEquals(response.status_code, 200)
 
+    def test_otp_advised_unconfigured_2(self):
+        self.alice.staticdevice_set.all().delete()
+        self.login()
+        response = self.client.get('/otp_advised_2/')
+
+        self.assertEquals(response.status_code, 200)
+
     def test_otp_advised_authenticated(self):
         self.login()
         response = self.client.get('/otp_advised/')

File django-otp-agents/otp_agents/tests/urls.py

     url(r'^otp/$', views.otp_view),
     url(r'^otp2/$', views.otp_view_2),
     url(r'^otp_advised/$', views.otp_advised_view),
+    url(r'^otp_advised_2/$', views.otp_advised_view_2),
     url(r'^agent/$', views.agent_view),
 )

File django-otp-agents/otp_agents/tests/views.py

     return HttpResponse()
 
 
+@otp_required(if_configured=True, accept_trusted_agent=True)
+def otp_advised_view(request):
+    return HttpResponse()
+
+
 @otp_required(if_configured=True)
-def otp_advised_view(request):
+def otp_advised_view_2(request):
     return HttpResponse()
 
 

File django-otp-agents/setup.py

 
 setup(
     name='django-otp-agents',
-    version='0.1.3',
+    version='0.1.4',
     description="Integration of django-otp and django-agent-trust.",
     long_description=open('README').read(),
     author='Peter Sagerson',
     url='https://bitbucket.org/psagers/django-otp',
     license='BSD',
     install_requires=[
-        'django-otp >= 0.1.7',
+        'django-otp >= 0.1.8',
         'django-agent-trust',
     ],
     classifiers=[

File django-otp/CHANGES

+v0.1.8 -- August 20, 2013 - user_has_device API
+-----------------------------------------------
+
+- Add :func:`django_otp.user_has_device` to detect whether a user has any
+  devices configured. This change supports a fix in django-otp-agents 0.1.4.
+
+
 v0.1.7 -- July 3, 2013 - Decorator improvement
 -----------------------------------------------
 

File django-otp/django_otp/__init__.py

 
 def devices_for_user(user, confirmed=True):
     """
-    Returns an iterable of all devices registered to the given user.
+    Return an iterable of all devices registered to the given user.
 
-    :param user:
+    Returns an empty iterable for anonymous users.
+
+    :param user: standard or custom user object.
     :type user: :class:`~django.contrib.auth.models.User`
 
     :param confirmed: If ``None``, all matching devices are returned.
 
     :rtype: iterable
     """
+    if user.is_anonymous():
+        return
+
     for model in device_classes():
         for device in model.objects.devices_for_user(user, confirmed=confirmed):
             yield device
 
 
+def user_has_device(user, confirmed=True):
+    """
+    Return ``True`` if the user has at least one device.
+
+    Returns ``False`` for anonymous users.
+
+    :param user: standard or custom user object.
+    :type user: :class:`~django.contrib.auth.models.User`
+
+    :param confirmed: If ``None``, all matching devices are considered.
+        Otherwise, this can be any true or false value to limit the query
+        to confirmed or unconfirmed devices, respectively.
+    """
+    try:
+        next(devices_for_user(user, confirmed=confirmed))
+    except StopIteration:
+        has_device = False
+    else:
+        has_device = True
+
+    return has_device
+
+
 def device_classes():
     """
     Returns an iterable of all loaded device models.

File django-otp/django_otp/decorators.py

 from django.contrib.auth.decorators import user_passes_test
 
-from django_otp import devices_for_user
+from django_otp import devices_for_user, user_has_device
 from django_otp.conf import settings
 
 
     if login_url is None:
         login_url = settings.OTP_LOGIN_URL
 
-    if if_configured:
-        def test(user):
-            try:
-                if user.is_authenticated():
-                    next(devices_for_user(user, confirmed=True))
-            except StopIteration:
-                return True  # No devices
-            else:
-                return user.is_verified()
-    else:
-        test = lambda u: u.is_verified()
-
+    test = lambda user: user.is_verified() or (if_configured and user.is_authenticated() and not user_has_device(user))
     decorator = user_passes_test(test, login_url=login_url, redirect_field_name=redirect_field_name)
 
     return decorator if (view is None) else decorator(view)

File django-otp/docs/source/auth.rst

 .. autoclass:: django_otp.admin.OTPAdminAuthenticationForm
 
 See the Django :class:`~django.contrib.admin.AdminSite` documentation for more
-on installing custom admin sites.
+on installing custom admin sites. If you want to copy the default admin site
+into an :class:`~django_otp.admin.OTPAdminSite`, we find that the following
+works well. Note that it relies on a private property, so use this at your own
+risk::
+
+    otp_admin_site = OTPAdminSite(OTPAdminSite.name)
+    for model_cls, model_admin in admin.site._registry.iteritems():
+        otp_admin_site.register(model_cls, model_admin.__class__)
 
 
 The Token Form
 
 .. autofunction:: django_otp.devices_for_user
 
+.. autofunction:: django_otp.user_has_device
+
 .. autofunction:: django_otp.match_token
 
 .. autofunction:: django_otp.login

File django-otp/docs/source/conf.py

 # The short X.Y version.
 version = '0.1'
 # The full version, including alpha/beta/rc tags.
-release = '0.1.7'
+release = '0.1.8'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.

File django-otp/setup.py

 
 setup(
     name='django-otp',
-    version='0.1.7',
+    version='0.1.8',
     description='A pluggable framework for adding two-factor authentication to Django using one-time passwords.',
     long_description=open('README').read(),
     author='Peter Sagerson',