Răsfoiți Sursa

HUE-2844 [desktop] Add django-axes to limit login attempts

For backwards compatibility, this does not lock out multiple
attempts by default.
Erick Tryzelaar 10 ani în urmă
părinte
comite
d378e37
45 a modificat fișierele cu 2427 adăugiri și 2 ștergeri
  1. 15 0
      desktop/conf.dist/hue.ini
  2. 15 0
      desktop/conf/pseudo-distributed.ini.tmpl
  3. 22 0
      desktop/core/ext-py/django-axes-1.4.0/.travis.yml
  4. 222 0
      desktop/core/ext-py/django-axes-1.4.0/CHANGES.txt
  5. 21 0
      desktop/core/ext-py/django-axes-1.4.0/LICENSE
  6. 8 0
      desktop/core/ext-py/django-axes-1.4.0/MANIFEST.in
  7. 13 0
      desktop/core/ext-py/django-axes-1.4.0/Makefile
  8. 415 0
      desktop/core/ext-py/django-axes-1.4.0/PKG-INFO
  9. 167 0
      desktop/core/ext-py/django-axes-1.4.0/README.rst
  10. 10 0
      desktop/core/ext-py/django-axes-1.4.0/axes/__init__.py
  11. 84 0
      desktop/core/ext-py/django-axes-1.4.0/axes/admin.py
  12. 491 0
      desktop/core/ext-py/django-axes-1.4.0/axes/decorators.py
  13. 0 0
      desktop/core/ext-py/django-axes-1.4.0/axes/management/__init__.py
  14. 0 0
      desktop/core/ext-py/django-axes-1.4.0/axes/management/commands/__init__.py
  15. 13 0
      desktop/core/ext-py/django-axes-1.4.0/axes/management/commands/axes_list_attempts.py
  16. 23 0
      desktop/core/ext-py/django-axes-1.4.0/axes/management/commands/axes_reset.py
  17. 37 0
      desktop/core/ext-py/django-axes-1.4.0/axes/middleware.py
  18. 72 0
      desktop/core/ext-py/django-axes-1.4.0/axes/models.py
  19. 34 0
      desktop/core/ext-py/django-axes-1.4.0/axes/signals.py
  20. 37 0
      desktop/core/ext-py/django-axes-1.4.0/axes/test_settings.py
  21. 6 0
      desktop/core/ext-py/django-axes-1.4.0/axes/test_urls.py
  22. 185 0
      desktop/core/ext-py/django-axes-1.4.0/axes/tests.py
  23. 20 0
      desktop/core/ext-py/django-axes-1.4.0/axes/utils.py
  24. 1 0
      desktop/core/ext-py/django-axes-1.4.0/axes/views.py
  25. 31 0
      desktop/core/ext-py/django-axes-1.4.0/examples/README.rst
  26. 0 0
      desktop/core/ext-py/django-axes-1.4.0/examples/example/__init__.py
  27. 0 0
      desktop/core/ext-py/django-axes-1.4.0/examples/example/foo/__init__.py
  28. 0 0
      desktop/core/ext-py/django-axes-1.4.0/examples/example/foo/management/__init__.py
  29. 0 0
      desktop/core/ext-py/django-axes-1.4.0/examples/example/foo/management/commands/__init__.py
  30. 41 0
      desktop/core/ext-py/django-axes-1.4.0/examples/example/foo/management/commands/axes_create_test_data.py
  31. 0 0
      desktop/core/ext-py/django-axes-1.4.0/examples/example/foo/models.py
  32. 26 0
      desktop/core/ext-py/django-axes-1.4.0/examples/example/local_settings.example
  33. 10 0
      desktop/core/ext-py/django-axes-1.4.0/examples/example/manage.py
  34. 242 0
      desktop/core/ext-py/django-axes-1.4.0/examples/example/settings.py
  35. 17 0
      desktop/core/ext-py/django-axes-1.4.0/examples/example/urls.py
  36. 32 0
      desktop/core/ext-py/django-axes-1.4.0/examples/example/wsgi.py
  37. 7 0
      desktop/core/ext-py/django-axes-1.4.0/examples/install.sh
  38. 3 0
      desktop/core/ext-py/django-axes-1.4.0/examples/reinstall.sh
  39. 4 0
      desktop/core/ext-py/django-axes-1.4.0/examples/test.sh
  40. 5 0
      desktop/core/ext-py/django-axes-1.4.0/examples/uninstall.sh
  41. 5 0
      desktop/core/ext-py/django-axes-1.4.0/setup.cfg
  42. 43 0
      desktop/core/ext-py/django-axes-1.4.0/setup.py
  43. 2 0
      desktop/core/src/desktop/auth/views.py
  44. 36 1
      desktop/core/src/desktop/conf.py
  45. 12 1
      desktop/core/src/desktop/settings.py

+ 15 - 0
desktop/conf.dist/hue.ini

@@ -247,6 +247,21 @@
     # Force users to change password on first login with desktop.auth.backend.AllowFirstUserDjangoBackend
     ## change_default_password=false
 
+    # Number of login attempts allowed before a record is created for failed logins
+    ## login_failure_limit=3
+
+    # After number of allowed login attempts are exceeded, do we lock out this IP and optionally user agent?
+    ## login_lock_out_at_failure=false
+
+    # If set, defines period of inactivity in seconds after which failed logins will be forgotten
+    ## login_cooloff_time=60
+
+    # If True, lock out based on IP and browser user agent
+    ## login_lock_out_by_combination_browser_user_agent_and_ip=false
+
+    # If True, lock out based on IP and user
+    ## login_lock_out_by_combination_user_and_ip=false
+
   # Configuration options for connecting to LDAP and Active Directory
   # -------------------------------------------------------------------
   [[ldap]]

+ 15 - 0
desktop/conf/pseudo-distributed.ini.tmpl

@@ -256,6 +256,21 @@
     # Force users to change password on first login with desktop.auth.backend.AllowFirstUserDjangoBackend
     ## change_default_password=false
 
+    # Number of login attempts allowed before a record is created for failed logins
+    ## login_failure_limit=3
+
+    # After number of allowed login attempts are exceeded, do we lock out this IP and optionally user agent?
+    ## login_lock_out_at_failure=false
+
+    # If set, defines period of inactivity in seconds after which failed logins will be forgotten
+    ## login_cooloff_time=60
+
+    # If True, lock out based on IP and browser user agent
+    ## login_lock_out_by_combination_browser_user_agent_and_ip=false
+
+    # If True, lock out based on IP and user
+    ## login_lock_out_by_combination_user_and_ip=false
+
   # Configuration options for connecting to LDAP and Active Directory
   # -------------------------------------------------------------------
   [[ldap]]

+ 22 - 0
desktop/core/ext-py/django-axes-1.4.0/.travis.yml

@@ -0,0 +1,22 @@
+language: python
+
+python:
+  - 2.7
+  - 3.3
+
+env:
+  - DJANGO=Django==1.4.15
+  - DJANGO=Django==1.5.10
+  - DJANGO=Django==1.6.7
+  - DJANGO=Django==1.7
+
+install:
+  - pip install -q $DJANGO
+
+script:
+  - PYTHONPATH=$PYTHONPATH:$PWD django-admin.py test axes --settings=axes.test_settings
+
+matrix:
+    exclude:
+        - python: 3.3
+          env: DJANGO=Django==1.4.15

+ 222 - 0
desktop/core/ext-py/django-axes-1.4.0/CHANGES.txt

@@ -0,0 +1,222 @@
+Changes
+=======
+
+1.4.0 (2015-08-09)
+------------------
+
+- Send the user_locked_out signal. Fixes #94.
+  [toabi]
+
+1.3.9 (2015-02-11)
+------------------
+
+- Python 3 fix (#104)
+
+1.3.8 (2014-10-07)
+------------------
+
+- Rename GitHub organization from django-security to django-pci to emphasize focus on providing assistance with building PCI compliant websites with Django.
+  [aclark4life]
+
+1.3.7 (2014-10-05)
+------------------
+
+- Explain common issues where Axes fails silently
+  [cericoda]
+
+- Allow for user-defined username field for lookup in POST data
+  [SteveByerly]
+
+- Log out only if user was logged in
+  [zoten]
+
+- Support for floats in cooloff time (i.e: 0.1 == 6 minutes)
+  [marianov]
+
+- Limit amount of POST data logged (#73). Limiting the length of value is not enough, as there could be arbitrary number of them, or very long key names.
+  [peterkuma]
+
+- Improve get_ip to try for real ip address
+  [7wonders]
+
+- Change IPAddressField to GenericIPAddressField. When using a PostgreSQL database and the client does not pass an IP address you get an inet error. This is a known problem with PostgreSQL and the IPAddressField. https://code.djangoproject.com/ticket/5622. It can be fixed by using a GenericIPAddressField instead.
+  [polvoblanco]
+
+- Get first X-Forwarded-For IP
+  [tutumcloud]
+
+- White listing IP addresses behind reverse proxy. Allowing some IP addresses to have direct access to the app even if they are behind a reverse proxy. Those IP addresses must still be on a white list.
+  [ericbulloch]
+
+- Reduce logging of reverse proxy IP lookup and use configured logger. Fixes #76. Instead of logging the notice that django.axes looks for a HTTP header set by a reverse proxy on each attempt, just log it one-time on first module import. Also use the configured logger (by default axes.watch_login) for the message to be more consistent in logging.
+  [eht16]
+
+- Limit the length of the values logged into the database. Refs #73
+  [camilonova]
+
+- Refactored tests to be more stable and faster
+  [camilonova]
+
+- Clean client references
+  [camilonova]
+
+- Fixed admin login url
+  [camilonova]
+
+- Added django 1.7 for testing
+  [camilonova]
+
+- Travis file cleanup
+  [camilonova]
+
+- Remove hardcoded url path
+  [camilonova]
+
+- Fixing tests for django 1.7
+  [Andrew-Crosio]
+
+- Fix for django 1.7 exception not existing
+  [Andrew-Crosio]
+
+- Removed python 2.6 from testing
+  [camilonova]
+
+- Use django built-in six version
+  [camilonova]
+
+- Added six as requirement
+  [camilonova]
+
+- Added python 2.6 for travis testing
+  [camilonova]
+
+- Replaced u string literal prefixes with six.u() calls
+  [amrhassan]
+
+- Fixes object type issue, response is not an string
+  [camilonova]
+    
+- Python 3 compatibility fix for db_reset
+  [nicois]
+
+- Added example project and helper scripts
+  [barseghyanartur]
+
+- Admin command to list login attemps
+  [marianov]
+
+- Replaced six imports with django.utils.six ones
+  [amrhassan]
+
+- Replaced u string literal prefixes with six.u() calls to make it compatible with Python 3.2
+  [amrhassan]
+
+- Replaced `assertIn`s and `assertNotIn`s with `assertContains` and `assertNotContains`
+  [fcurella]
+
+- Added py3k to travis
+  [fcurella]
+
+- Update test cases to be python3 compatible
+  [nicois]
+
+- Python 3 compatibility fix for db_reset
+  [nicois]
+
+- Removed trash from example urls
+  [barseghyanartur]
+
+- Added django installer
+  [barseghyanartur]
+
+- Added example project and helper scripts
+  [barseghyanartur]
+
+
+1.3.6 (2013-11-23)
+------------------
+
+- Added AttributeError in case get_profile doesn't exist [camilonova]
+- Improved axes_reset command [camilonova]
+
+
+1.3.5 (2013-11-01)
+------------------
+
+- Fix an issue with __version__ loading the wrong version [graingert]
+
+
+1.3.4 (2013-11-01)
+------------------
+
+- Update README.rst for PyPI [marty] [camilonova] [graingert]
+- Add cooloff period [visualspace]
+
+
+1.3.3 (2013-07-05)
+------------------
+
+- Added 'username' field to the Admin table [bkvirendra]
+- Removed fallback logging creation since logging cames by default on django 1.4 or later, if you don't have it is because you explicitly wanted. Fixes #45 [camilonova]
+
+
+1.3.2 (2013-03-28)
+------------------
+
+- Fix an issue when a user logout [camilonova]
+- Match pypi version [camilonova]
+- Better User model import method [camilonova]
+- Use only one place to get the version number [camilonova]
+- Fixed an issue when a user on django 1.4 logout [camilonova]
+- Handle exception if there is not user profile model set [camilonova]
+- Made some cleanup and remove a pokemon exception handling [camilonova]
+- Improved tests so it really looks for the rabbit in the hole [camilonova]
+- Match pypi version [camilonova]
+
+
+1.3.1 (2013-03-19)
+------------------
+
+- Add support for Django 1.5 [camilonova]
+
+
+1.3.0 (2013-02-27)
+------------------
+
+- Bug fix: get_version() format string [csghormley]
+
+
+1.2.9 (2013-02-20)
+------------------
+
+- Add to and improve test cases [camilonova]
+
+
+1.2.8 (2013-01-23)
+------------------
+
+- Increased http accept header length [jslatts]
+
+
+1.2.7 (2013-01-17)
+------------------
+
+- Reverse proxy support [rmagee]
+- Clean up README [martey]
+
+
+1.2.6 (2012-12-04)
+------------------
+
+- Remove unused import [aclark4life]
+
+
+1.2.5 (2012-11-28)
+------------------
+
+- Fix setup.py [aclark4life]
+- Added ability to flag user accounts as unlockable. [kencochrane]
+- Added ipaddress as a param to the user_locked_out signal. [kencochrane]
+- Added a signal receiver for user_logged_out. [kencochrane]
+- Added a signal for when a user gets locked out. [kencochrane]
+- Added AccessLog model to log all access attempts. [kencochrane]

+ 21 - 0
desktop/core/ext-py/django-axes-1.4.0/LICENSE

@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2008 Josh VanderLinden, 2009 Philip Neustrom <philipn@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 8 - 0
desktop/core/ext-py/django-axes-1.4.0/MANIFEST.in

@@ -0,0 +1,8 @@
+include LICENSE README.rst CHANGES.txt
+recursive-include axes *.py
+include .travis.yml
+recursive-include examples *.example
+recursive-include examples *.py
+recursive-include examples *.rst
+recursive-include examples *.sh
+include Makefile

+ 13 - 0
desktop/core/ext-py/django-axes-1.4.0/Makefile

@@ -0,0 +1,13 @@
+all: clean check-manifest pyroma viewdoc
+clean:
+	if [ -e .long-description.html ]; then rm .long-description.html ; fi
+check-manifest:
+	check-manifest
+viewdoc:
+	viewdoc
+pyroma:
+	pyroma .
+releasetest:
+	python setup.py sdist --format=zip upload -r test
+release:
+	python setup.py sdist --format=zip upload

+ 415 - 0
desktop/core/ext-py/django-axes-1.4.0/PKG-INFO

@@ -0,0 +1,415 @@
+Metadata-Version: 1.1
+Name: django-axes
+Version: 1.4.0
+Summary: Keep track of failed login attempts in Django-powered sites.
+Home-page: https://github.com/django-pci/django-axes
+Author: Alex Clark
+Author-email: aclark@aclark.net
+License: MIT
+Description: Django Axes
+        ===========
+        
+        .. image:: https://secure.travis-ci.org/django-pci/django-axes.png?branch=master
+            :alt: Build Status
+            :target: http://travis-ci.org/django-pci/django-axes
+        
+        ``django-axes`` is a very simple way for you to keep track of failed login
+        attempts, both for the Django admin and for the rest of your site.  The name is
+        sort of a geeky pun, since ``axes`` can be read interpreted as:
+        
+        * "access", as in monitoring access attempts
+        * "axes", as in tools you can use hack (generally on wood).  In this case,
+          however, the "hacking" part of it can be taken a bit further: ``django-axes``
+          is intended to help you *stop* people from hacking (popular media
+          definition) your website.  Hilarious, right?  That's what I thought too!
+        
+        
+        Requirements
+        ============
+        
+        ``django-axes`` requires Django 1.5 or later. The application is intended to
+        work around the Django admin and the regular ``django.contrib.auth``
+        login-powered pages.
+        
+        
+        Installation
+        ============
+        
+        You can install the latest stable package running this command::
+        
+            $ pip install django-axes
+        
+        Also you can install the development version running this command::
+        
+            $ pip install -e git+http://github.com/django-pci/django-axes.git#egg=django_axes-dev
+        
+        Development
+        ===========
+        
+        You can contribute to this project forking it from github and sending pull requests.
+        
+        Running tests
+        -------------
+        
+        Tests can be run, after you clone the repository and having django installed, like::
+        
+            $ PYTHONPATH=$PYTHONPATH:$PWD django-admin.py test axes --settings=axes.test_settings
+        
+        
+        Configuration
+        =============
+        
+        First of all, you must add this project to your list of ``INSTALLED_APPS`` in
+        ``settings.py``::
+        
+            INSTALLED_APPS = (
+                'django.contrib.admin',
+                'django.contrib.auth',
+                'django.contrib.contenttypes',
+                'django.contrib.sessions',
+                'django.contrib.sites',
+                ...
+                'axes',
+                ...
+            )
+        
+        Next, install the ``FailedLoginMiddleware`` middleware::
+        
+            MIDDLEWARE_CLASSES = (
+                'django.middleware.common.CommonMiddleware',
+                'django.contrib.sessions.middleware.SessionMiddleware',
+                'django.contrib.auth.middleware.AuthenticationMiddleware',
+                'axes.middleware.FailedLoginMiddleware'
+            )
+        
+        Run ``python manage.py syncdb``.  This creates the appropriate tables in your database
+        that are necessary for operation.
+        
+        Customizing Axes
+        ----------------
+        
+        You have a couple options available to you to customize ``django-axes`` a bit.
+        These should be defined in your ``settings.py`` file.
+        
+        * ``AXES_LOGIN_FAILURE_LIMIT``: The number of login attempts allowed before a
+          record is created for the failed logins.  Default: ``3``
+        * ``AXES_LOCK_OUT_AT_FAILURE``: After the number of allowed login attempts
+          are exceeded, should we lock out this IP (and optional user agent)?
+          Default: ``True``
+        * ``AXES_USE_USER_AGENT``: If ``True``, lock out / log based on an IP address
+          AND a user agent.  This means requests from different user agents but from
+          the same IP are treated differently.  Default: ``False``
+        * ``AXES_COOLOFF_TIME``: If set, defines a period of inactivity after which
+          old failed login attempts will be forgotten. Can be set to a python
+          timedelta object or an integer. If an integer, will be interpreted as a
+          number of hours.  Default: ``None``
+        * ``AXES_LOGGER``: If set, specifies a logging mechanism for axes to use.
+          Default: ``'axes.watch_login'``
+        * ``AXES_LOCKOUT_TEMPLATE``: If set, specifies a template to render when a
+          user is locked out. Template receives cooloff_time and failure_limit as
+          context variables. Default: ``None``
+        * ``AXES_LOCKOUT_URL``: If set, specifies a URL to redirect to on lockout. If
+          both AXES_LOCKOUT_TEMPLATE and AXES_LOCKOUT_URL are set, the template will
+          be used. Default: ``None``
+        * ``AXES_VERBOSE``: If ``True``, you'll see slightly more logging for Axes.
+          Default: ``True``
+        * ``AXES_USERNAME_FORM_FIELD``: the name of the form field that contains your
+          users usernames. Default: ``username``
+        
+        * ``AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP``: If ``True`` prevents to login
+          from IP under particular user if attempts limit exceed, otherwise lock out
+          based on IP.
+          Default: ``False``
+        
+        
+        Usage
+        =====
+        
+        Using ``django-axes`` is extremely simple.  Once you install the application
+        and the middleware, all you need to do is periodically check the Access
+        Attempts section of the admin.
+        
+        By default, django-axes will lock out repeated attempts from the same IP
+        address.  You can allow this IP to attempt again by deleting the relevant
+        ``AccessAttempt`` records in the admin.
+        
+        You can also use the ``axes_reset`` management command using Django's
+        ``manage.py``.
+        
+        * ``manage.py axes_reset`` will reset all lockouts and access records.
+        * ``manage.py axes_reset ip`` will clear lockout/records for ip
+        
+        In your code, you can use ``from axes.utils import reset``.
+        
+        * ``reset()`` will reset all lockouts and access records.
+        * ``reset(ip=ip)`` will clear lockout/records for ip
+        * ``reset(username=username)`` will clear lockout/records for username
+        
+        Issues
+        ======
+        
+        Not being locked out after failed attempts
+        ------------------------------------------
+        
+        You may find that Axes is not capturing your failed login attempts. It may be that you need to manually add watch_login to your login url.
+        For example, in your urls.py::
+        
+            ...
+            from django.contrib.auth.views import login, logout, password_change
+            from axes.decorators import watch_login
+            ...
+            urlpatterns = patterns('',
+                (r'^login/$', watch_login(login), {'template_name': 'auth/login.html'}),
+            ...
+        
+        
+        Locked out without reason
+        -------------------------
+        
+        It may happen that you have suddenly become locked out without a single failed
+        attempt. One possible reason is that you are using some custom login form and the
+        username field is named something different than "username", e.g. "email". This
+        leads to all users attempts being lumped together. To fix this add the following
+        to your settings:
+        
+            AXES_USERNAME_FORM_FIELD = "email"
+        
+        Changes
+        =======
+        
+        1.4.0 (2015-08-09)
+        ------------------
+        
+        - Send the user_locked_out signal. Fixes #94.
+          [toabi]
+        
+        1.3.9 (2015-02-11)
+        ------------------
+        
+        - Python 3 fix (#104)
+        
+        1.3.8 (2014-10-07)
+        ------------------
+        
+        - Rename GitHub organization from django-security to django-pci to emphasize focus on providing assistance with building PCI compliant websites with Django.
+          [aclark4life]
+        
+        1.3.7 (2014-10-05)
+        ------------------
+        
+        - Explain common issues where Axes fails silently
+          [cericoda]
+        
+        - Allow for user-defined username field for lookup in POST data
+          [SteveByerly]
+        
+        - Log out only if user was logged in
+          [zoten]
+        
+        - Support for floats in cooloff time (i.e: 0.1 == 6 minutes)
+          [marianov]
+        
+        - Limit amount of POST data logged (#73). Limiting the length of value is not enough, as there could be arbitrary number of them, or very long key names.
+          [peterkuma]
+        
+        - Improve get_ip to try for real ip address
+          [7wonders]
+        
+        - Change IPAddressField to GenericIPAddressField. When using a PostgreSQL database and the client does not pass an IP address you get an inet error. This is a known problem with PostgreSQL and the IPAddressField. https://code.djangoproject.com/ticket/5622. It can be fixed by using a GenericIPAddressField instead.
+          [polvoblanco]
+        
+        - Get first X-Forwarded-For IP
+          [tutumcloud]
+        
+        - White listing IP addresses behind reverse proxy. Allowing some IP addresses to have direct access to the app even if they are behind a reverse proxy. Those IP addresses must still be on a white list.
+          [ericbulloch]
+        
+        - Reduce logging of reverse proxy IP lookup and use configured logger. Fixes #76. Instead of logging the notice that django.axes looks for a HTTP header set by a reverse proxy on each attempt, just log it one-time on first module import. Also use the configured logger (by default axes.watch_login) for the message to be more consistent in logging.
+          [eht16]
+        
+        - Limit the length of the values logged into the database. Refs #73
+          [camilonova]
+        
+        - Refactored tests to be more stable and faster
+          [camilonova]
+        
+        - Clean client references
+          [camilonova]
+        
+        - Fixed admin login url
+          [camilonova]
+        
+        - Added django 1.7 for testing
+          [camilonova]
+        
+        - Travis file cleanup
+          [camilonova]
+        
+        - Remove hardcoded url path
+          [camilonova]
+        
+        - Fixing tests for django 1.7
+          [Andrew-Crosio]
+        
+        - Fix for django 1.7 exception not existing
+          [Andrew-Crosio]
+        
+        - Removed python 2.6 from testing
+          [camilonova]
+        
+        - Use django built-in six version
+          [camilonova]
+        
+        - Added six as requirement
+          [camilonova]
+        
+        - Added python 2.6 for travis testing
+          [camilonova]
+        
+        - Replaced u string literal prefixes with six.u() calls
+          [amrhassan]
+        
+        - Fixes object type issue, response is not an string
+          [camilonova]
+            
+        - Python 3 compatibility fix for db_reset
+          [nicois]
+        
+        - Added example project and helper scripts
+          [barseghyanartur]
+        
+        - Admin command to list login attemps
+          [marianov]
+        
+        - Replaced six imports with django.utils.six ones
+          [amrhassan]
+        
+        - Replaced u string literal prefixes with six.u() calls to make it compatible with Python 3.2
+          [amrhassan]
+        
+        - Replaced `assertIn`s and `assertNotIn`s with `assertContains` and `assertNotContains`
+          [fcurella]
+        
+        - Added py3k to travis
+          [fcurella]
+        
+        - Update test cases to be python3 compatible
+          [nicois]
+        
+        - Python 3 compatibility fix for db_reset
+          [nicois]
+        
+        - Removed trash from example urls
+          [barseghyanartur]
+        
+        - Added django installer
+          [barseghyanartur]
+        
+        - Added example project and helper scripts
+          [barseghyanartur]
+        
+        
+        1.3.6 (2013-11-23)
+        ------------------
+        
+        - Added AttributeError in case get_profile doesn't exist [camilonova]
+        - Improved axes_reset command [camilonova]
+        
+        
+        1.3.5 (2013-11-01)
+        ------------------
+        
+        - Fix an issue with __version__ loading the wrong version [graingert]
+        
+        
+        1.3.4 (2013-11-01)
+        ------------------
+        
+        - Update README.rst for PyPI [marty] [camilonova] [graingert]
+        - Add cooloff period [visualspace]
+        
+        
+        1.3.3 (2013-07-05)
+        ------------------
+        
+        - Added 'username' field to the Admin table [bkvirendra]
+        - Removed fallback logging creation since logging cames by default on django 1.4 or later, if you don't have it is because you explicitly wanted. Fixes #45 [camilonova]
+        
+        
+        1.3.2 (2013-03-28)
+        ------------------
+        
+        - Fix an issue when a user logout [camilonova]
+        - Match pypi version [camilonova]
+        - Better User model import method [camilonova]
+        - Use only one place to get the version number [camilonova]
+        - Fixed an issue when a user on django 1.4 logout [camilonova]
+        - Handle exception if there is not user profile model set [camilonova]
+        - Made some cleanup and remove a pokemon exception handling [camilonova]
+        - Improved tests so it really looks for the rabbit in the hole [camilonova]
+        - Match pypi version [camilonova]
+        
+        
+        1.3.1 (2013-03-19)
+        ------------------
+        
+        - Add support for Django 1.5 [camilonova]
+        
+        
+        1.3.0 (2013-02-27)
+        ------------------
+        
+        - Bug fix: get_version() format string [csghormley]
+        
+        
+        1.2.9 (2013-02-20)
+        ------------------
+        
+        - Add to and improve test cases [camilonova]
+        
+        
+        1.2.8 (2013-01-23)
+        ------------------
+        
+        - Increased http accept header length [jslatts]
+        
+        
+        1.2.7 (2013-01-17)
+        ------------------
+        
+        - Reverse proxy support [rmagee]
+        - Clean up README [martey]
+        
+        
+        1.2.6 (2012-12-04)
+        ------------------
+        
+        - Remove unused import [aclark4life]
+        
+        
+        1.2.5 (2012-11-28)
+        ------------------
+        
+        - Fix setup.py [aclark4life]
+        - Added ability to flag user accounts as unlockable. [kencochrane]
+        - Added ipaddress as a param to the user_locked_out signal. [kencochrane]
+        - Added a signal receiver for user_logged_out. [kencochrane]
+        - Added a signal for when a user gets locked out. [kencochrane]
+        - Added AccessLog model to log all access attempts. [kencochrane]
+        
+Keywords: authentication,django,pci,security
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Framework :: Django
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: System Administrators
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Internet :: Log Analysis
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
+Classifier: Topic :: Security
+Classifier: Topic :: System :: Logging

+ 167 - 0
desktop/core/ext-py/django-axes-1.4.0/README.rst

@@ -0,0 +1,167 @@
+Django Axes
+===========
+
+.. image:: https://secure.travis-ci.org/django-pci/django-axes.png?branch=master
+    :alt: Build Status
+    :target: http://travis-ci.org/django-pci/django-axes
+
+``django-axes`` is a very simple way for you to keep track of failed login
+attempts, both for the Django admin and for the rest of your site.  The name is
+sort of a geeky pun, since ``axes`` can be read interpreted as:
+
+* "access", as in monitoring access attempts
+* "axes", as in tools you can use hack (generally on wood).  In this case,
+  however, the "hacking" part of it can be taken a bit further: ``django-axes``
+  is intended to help you *stop* people from hacking (popular media
+  definition) your website.  Hilarious, right?  That's what I thought too!
+
+
+Requirements
+============
+
+``django-axes`` requires Django 1.5 or later. The application is intended to
+work around the Django admin and the regular ``django.contrib.auth``
+login-powered pages.
+
+
+Installation
+============
+
+You can install the latest stable package running this command::
+
+    $ pip install django-axes
+
+Also you can install the development version running this command::
+
+    $ pip install -e git+http://github.com/django-pci/django-axes.git#egg=django_axes-dev
+
+Development
+===========
+
+You can contribute to this project forking it from github and sending pull requests.
+
+Running tests
+-------------
+
+Tests can be run, after you clone the repository and having django installed, like::
+
+    $ PYTHONPATH=$PYTHONPATH:$PWD django-admin.py test axes --settings=axes.test_settings
+
+
+Configuration
+=============
+
+First of all, you must add this project to your list of ``INSTALLED_APPS`` in
+``settings.py``::
+
+    INSTALLED_APPS = (
+        'django.contrib.admin',
+        'django.contrib.auth',
+        'django.contrib.contenttypes',
+        'django.contrib.sessions',
+        'django.contrib.sites',
+        ...
+        'axes',
+        ...
+    )
+
+Next, install the ``FailedLoginMiddleware`` middleware::
+
+    MIDDLEWARE_CLASSES = (
+        'django.middleware.common.CommonMiddleware',
+        'django.contrib.sessions.middleware.SessionMiddleware',
+        'django.contrib.auth.middleware.AuthenticationMiddleware',
+        'axes.middleware.FailedLoginMiddleware'
+    )
+
+Run ``python manage.py syncdb``.  This creates the appropriate tables in your database
+that are necessary for operation.
+
+Customizing Axes
+----------------
+
+You have a couple options available to you to customize ``django-axes`` a bit.
+These should be defined in your ``settings.py`` file.
+
+* ``AXES_LOGIN_FAILURE_LIMIT``: The number of login attempts allowed before a
+  record is created for the failed logins.  Default: ``3``
+* ``AXES_LOCK_OUT_AT_FAILURE``: After the number of allowed login attempts
+  are exceeded, should we lock out this IP (and optional user agent)?
+  Default: ``True``
+* ``AXES_USE_USER_AGENT``: If ``True``, lock out / log based on an IP address
+  AND a user agent.  This means requests from different user agents but from
+  the same IP are treated differently.  Default: ``False``
+* ``AXES_COOLOFF_TIME``: If set, defines a period of inactivity after which
+  old failed login attempts will be forgotten. Can be set to a python
+  timedelta object or an integer. If an integer, will be interpreted as a
+  number of hours.  Default: ``None``
+* ``AXES_LOGGER``: If set, specifies a logging mechanism for axes to use.
+  Default: ``'axes.watch_login'``
+* ``AXES_LOCKOUT_TEMPLATE``: If set, specifies a template to render when a
+  user is locked out. Template receives cooloff_time and failure_limit as
+  context variables. Default: ``None``
+* ``AXES_LOCKOUT_URL``: If set, specifies a URL to redirect to on lockout. If
+  both AXES_LOCKOUT_TEMPLATE and AXES_LOCKOUT_URL are set, the template will
+  be used. Default: ``None``
+* ``AXES_VERBOSE``: If ``True``, you'll see slightly more logging for Axes.
+  Default: ``True``
+* ``AXES_USERNAME_FORM_FIELD``: the name of the form field that contains your
+  users usernames. Default: ``username``
+
+* ``AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP``: If ``True`` prevents to login
+  from IP under particular user if attempts limit exceed, otherwise lock out
+  based on IP.
+  Default: ``False``
+
+
+Usage
+=====
+
+Using ``django-axes`` is extremely simple.  Once you install the application
+and the middleware, all you need to do is periodically check the Access
+Attempts section of the admin.
+
+By default, django-axes will lock out repeated attempts from the same IP
+address.  You can allow this IP to attempt again by deleting the relevant
+``AccessAttempt`` records in the admin.
+
+You can also use the ``axes_reset`` management command using Django's
+``manage.py``.
+
+* ``manage.py axes_reset`` will reset all lockouts and access records.
+* ``manage.py axes_reset ip`` will clear lockout/records for ip
+
+In your code, you can use ``from axes.utils import reset``.
+
+* ``reset()`` will reset all lockouts and access records.
+* ``reset(ip=ip)`` will clear lockout/records for ip
+* ``reset(username=username)`` will clear lockout/records for username
+
+Issues
+======
+
+Not being locked out after failed attempts
+------------------------------------------
+
+You may find that Axes is not capturing your failed login attempts. It may be that you need to manually add watch_login to your login url.
+For example, in your urls.py::
+
+    ...
+    from django.contrib.auth.views import login, logout, password_change
+    from axes.decorators import watch_login
+    ...
+    urlpatterns = patterns('',
+        (r'^login/$', watch_login(login), {'template_name': 'auth/login.html'}),
+    ...
+
+
+Locked out without reason
+-------------------------
+
+It may happen that you have suddenly become locked out without a single failed
+attempt. One possible reason is that you are using some custom login form and the
+username field is named something different than "username", e.g. "email". This
+leads to all users attempts being lumped together. To fix this add the following
+to your settings:
+
+    AXES_USERNAME_FORM_FIELD = "email"

+ 10 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/__init__.py

@@ -0,0 +1,10 @@
+try:
+    __version__ = __import__('pkg_resources').get_distribution(
+        'django-axes'
+    ).version
+except:
+    __version__ = ''
+
+
+def get_version():
+    return __version__

+ 84 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/admin.py

@@ -0,0 +1,84 @@
+from django.contrib import admin
+
+from axes.models import AccessLog
+from axes.models import AccessAttempt
+
+
+class AccessAttemptAdmin(admin.ModelAdmin):
+    list_display = (
+        'attempt_time',
+        'ip_address',
+        'user_agent',
+        'username',
+        'path_info',
+        'failures_since_start',
+    )
+
+    list_filter = [
+        'attempt_time',
+        'ip_address',
+        'username',
+        'path_info',
+    ]
+
+    search_fields = [
+        'ip_address',
+        'username',
+        'user_agent',
+        'path_info',
+    ]
+
+    date_hierarchy = 'attempt_time'
+
+    fieldsets = (
+        (None, {
+            'fields': ('path_info', 'failures_since_start')
+        }),
+        ('Form Data', {
+            'fields': ('get_data', 'post_data')
+        }),
+        ('Meta Data', {
+            'fields': ('user_agent', 'ip_address', 'http_accept')
+        })
+    )
+
+admin.site.register(AccessAttempt, AccessAttemptAdmin)
+
+
+class AccessLogAdmin(admin.ModelAdmin):
+    list_display = (
+        'attempt_time',
+        'logout_time',
+        'ip_address',
+        'username',
+        'user_agent',
+        'path_info',
+    )
+
+    list_filter = [
+        'attempt_time',
+        'logout_time',
+        'ip_address',
+        'username',
+        'path_info',
+    ]
+
+    search_fields = [
+        'ip_address',
+        'user_agent',
+        'username',
+        'path_info',
+    ]
+
+    date_hierarchy = 'attempt_time'
+
+    fieldsets = (
+        (None, {
+            'fields': ('path_info',)
+        }),
+        ('Meta Data', {
+            'fields': ('user_agent', 'ip_address', 'http_accept')
+        })
+    )
+
+admin.site.register(AccessLog, AccessLogAdmin)

+ 491 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/decorators.py

@@ -0,0 +1,491 @@
+import logging
+import socket
+
+from datetime import timedelta
+
+from django.conf import settings
+from django.contrib.auth import logout
+from django.core.exceptions import ObjectDoesNotExist
+from django.http import HttpResponse
+from django.http import HttpResponseRedirect
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.utils import timezone as datetime
+from django.utils.translation import ugettext_lazy
+
+try:
+    from django.contrib.auth import get_user_model
+except ImportError:  # django < 1.5
+    from django.contrib.auth.models import User
+else:
+    User = get_user_model()
+
+try:
+    from django.contrib.auth.models import SiteProfileNotAvailable
+except ImportError: # django >= 1.7
+    SiteProfileNotAvailable = type('SiteProfileNotAvailable', (Exception,), {})
+
+from axes.models import AccessLog
+from axes.models import AccessAttempt
+from axes.signals import user_locked_out
+import axes
+from django.utils import six
+
+
+# see if the user has overridden the failure limit
+FAILURE_LIMIT = getattr(settings, 'AXES_LOGIN_FAILURE_LIMIT', 3)
+
+# see if the user has set axes to lock out logins after failure limit
+LOCK_OUT_AT_FAILURE = getattr(settings, 'AXES_LOCK_OUT_AT_FAILURE', True)
+
+USE_USER_AGENT = getattr(settings, 'AXES_USE_USER_AGENT', False)
+
+# use a specific username field to retrieve from login POST data
+USERNAME_FORM_FIELD = getattr(settings, 'AXES_USERNAME_FORM_FIELD', 'username')
+
+# use a specific password field to retrieve from login POST data
+PASSWORD_FORM_FIELD = getattr(settings, 'AXES_PASSWORD_FORM_FIELD', 'password')
+
+# see if the django app is sitting behind a reverse proxy
+BEHIND_REVERSE_PROXY = getattr(settings, 'AXES_BEHIND_REVERSE_PROXY', False)
+
+# see if the django app is sitting behind a reverse proxy but can be accessed directly
+BEHIND_REVERSE_PROXY_WITH_DIRECT_ACCESS = getattr(settings, 'AXES_BEHIND_REVERSE_PROXY_WITH_DIRECT_ACCESS', False)
+
+# if the django app is behind a reverse proxy, look for the ip address using this HTTP header value
+REVERSE_PROXY_HEADER = getattr(settings, 'AXES_REVERSE_PROXY_HEADER', 'HTTP_X_FORWARDED_FOR')
+
+# lock out user from particular IP based on combination USER+IP
+LOCK_OUT_BY_COMBINATION_USER_AND_IP = getattr(settings, 'AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP', False)
+
+COOLOFF_TIME = getattr(settings, 'AXES_COOLOFF_TIME', None)
+if (isinstance(COOLOFF_TIME, int) or isinstance(COOLOFF_TIME, float) ):
+    COOLOFF_TIME = timedelta(hours=COOLOFF_TIME)
+
+LOGGER = getattr(settings, 'AXES_LOGGER', 'axes.watch_login')
+
+LOCKOUT_TEMPLATE = getattr(settings, 'AXES_LOCKOUT_TEMPLATE', None)
+VERBOSE = getattr(settings, 'AXES_VERBOSE', True)
+
+# whitelist and blacklist
+# todo: convert the strings to IPv4 on startup to avoid type conversion during processing
+ONLY_WHITELIST = getattr(settings, 'AXES_ONLY_ALLOW_WHITELIST', False)
+IP_WHITELIST = getattr(settings, 'AXES_IP_WHITELIST', None)
+IP_BLACKLIST = getattr(settings, 'AXES_IP_BLACKLIST', None)
+
+ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. "
+                              "Note that both fields are case-sensitive.")
+
+
+log = logging.getLogger(LOGGER)
+if VERBOSE:
+    log.info('AXES: BEGIN LOG')
+    log.info('Using django-axes ' + axes.get_version())
+
+
+if BEHIND_REVERSE_PROXY:
+    log.debug('Axes is configured to be behind reverse proxy...looking for header value %s', REVERSE_PROXY_HEADER)
+
+
+def is_valid_ip(ip_address):
+    """ Check Validity of an IP address """
+    valid = True
+    try:
+        socket.inet_aton(ip_address.strip())
+    except:
+        valid = False
+    return valid
+
+
+def get_ip_address_from_request(request):
+    """ Makes the best attempt to get the client's real IP or return the loopback """
+    PRIVATE_IPS_PREFIX = ('10.', '172.', '192.', '127.')
+    ip_address = ''
+    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '')
+    if x_forwarded_for and ',' not in x_forwarded_for:
+        if not x_forwarded_for.startswith(PRIVATE_IPS_PREFIX) and is_valid_ip(x_forwarded_for):
+            ip_address = x_forwarded_for.strip()
+    else:
+        ips = [ip.strip() for ip in x_forwarded_for.split(',')]
+        for ip in ips:
+            if ip.startswith(PRIVATE_IPS_PREFIX):
+                continue
+            elif not is_valid_ip(ip):
+                continue
+            else:
+                ip_address = ip
+                break
+    if not ip_address:
+        x_real_ip = request.META.get('HTTP_X_REAL_IP', '')
+        if x_real_ip:
+            if not x_real_ip.startswith(PRIVATE_IPS_PREFIX) and is_valid_ip(x_real_ip):
+                ip_address = x_real_ip.strip()
+    if not ip_address:
+        remote_addr = request.META.get('REMOTE_ADDR', '')
+        if remote_addr:
+            if not remote_addr.startswith(PRIVATE_IPS_PREFIX) and is_valid_ip(remote_addr):
+                ip_address = remote_addr.strip()
+            if remote_addr.startswith(PRIVATE_IPS_PREFIX) and is_valid_ip(remote_addr):
+                ip_address = remote_addr.strip()
+    if not ip_address:
+            ip_address = '127.0.0.1'
+    return ip_address
+
+
+def get_ip(request):
+    if not BEHIND_REVERSE_PROXY:
+        ip = get_ip_address_from_request(request)
+    else:
+        ip = request.META.get(REVERSE_PROXY_HEADER, '')
+        ip = ip.split(",", 1)[0].strip()
+        if ip == '':
+            if not BEHIND_REVERSE_PROXY_WITH_DIRECT_ACCESS:
+                raise Warning('Axes is configured for operation behind a reverse proxy but could not find '\
+                    'an HTTP header value {0}. Check your proxy server settings '\
+                    'to make sure this header value is being passed.'.format(REVERSE_PROXY_HEADER))
+            else:
+                ip = request.META.get('REMOTE_ADDR', '')
+                if ip not in IP_WHITELIST:
+                    raise Warning('Axes is configured for operation behind a reverse proxy and to allow some'\
+                        'IP addresses to have direct access. {0} is not on the white list'.format(ip))
+    return ip
+
+
+def get_lockout_url():
+    return getattr(settings, 'AXES_LOCKOUT_URL', None)
+
+
+def query2str(items, max_length=1024):
+    """Turns a dictionary into an easy-to-read list of key-value pairs.
+
+    If there's a field called "password" it will be excluded from the output.
+
+    The length of the output is limited to max_length to avoid a DoS attack.
+    """
+
+    kvs = []
+    for k, v in items:
+        if k != PASSWORD_FORM_FIELD:
+            kvs.append(six.u('%s=%s') % (k, v))
+
+    return '\n'.join(kvs)[:max_length]
+
+
+def ip_in_whitelist(ip):
+    if IP_WHITELIST is not None:
+        return ip in IP_WHITELIST
+
+    return False
+
+
+def ip_in_blacklist(ip):
+    if IP_BLACKLIST is not None:
+        return ip in IP_BLACKLIST
+
+    return False
+
+
+def is_user_lockable(request):
+    """Check if the user has a profile with nolockout
+    If so, then return the value to see if this user is special
+    and doesn't get their account locked out
+    """
+    try:
+        field = getattr(User, 'USERNAME_FIELD', 'username')
+        kwargs = {
+            field: request.POST.get(USERNAME_FORM_FIELD)
+        }
+        user = User.objects.get(**kwargs)
+    except User.DoesNotExist:
+        # not a valid user
+        return True
+
+    if hasattr(user, 'nolockout'):
+        # need to revert since we need to return
+        # false for users that can't be blocked
+        return not user.nolockout
+
+    elif hasattr(settings, 'AUTH_PROFILE_MODULE'):
+        try:
+            profile = user.get_profile()
+            if hasattr(profile, 'nolockout'):
+                # need to revert since we need to return
+                # false for users that can't be blocked
+                return not profile.nolockout
+
+        except (SiteProfileNotAvailable, ObjectDoesNotExist, AttributeError):
+            # no profile
+            return True
+
+    # Default behavior for a user to be lockable
+    return True
+
+def _get_user_attempts(request):
+    """Returns access attempt record if it exists.
+    Otherwise return None.
+    """
+    ip = get_ip(request)
+
+    username = request.POST.get(USERNAME_FORM_FIELD, None)
+
+    if USE_USER_AGENT:
+        ua = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
+        attempts = AccessAttempt.objects.filter(
+            user_agent=ua, ip_address=ip, username=username, trusted=True
+        )
+    else:
+        attempts = AccessAttempt.objects.filter(
+            ip_address=ip, username=username, trusted=True
+        )
+
+    if not attempts and not LOCK_OUT_BY_COMBINATION_USER_AND_IP:
+        params = {'ip_address': ip, 'trusted': False}
+        if USE_USER_AGENT:
+            params['user_agent'] = ua
+
+        attempts = AccessAttempt.objects.filter(**params)
+
+    return attempts
+
+def get_user_attempts(request):
+    objects_deleted = False
+    attempts = _get_user_attempts(request)
+
+    if COOLOFF_TIME:
+        for attempt in attempts:
+            if attempt.attempt_time + COOLOFF_TIME < datetime.now():
+                if attempt.trusted:
+                    attempt.failures_since_start = 0
+                    attempt.save()
+                else:
+                    attempt.delete()
+                    objects_deleted = True
+
+    # If objects were deleted, we need to update the queryset to reflect this,
+    # so force a reload.
+    if objects_deleted:
+        attempts = _get_user_attempts(request)
+
+    return attempts
+
+
+def watch_login(func):
+    """
+    Used to decorate the django.contrib.admin.site.login method.
+    """
+
+    def decorated_login(request, *args, **kwargs):
+        # share some useful information
+        if func.__name__ != 'decorated_login' and VERBOSE:
+            log.info('AXES: Calling decorated function: %s' % func.__name__)
+            if args:
+                log.info('args: %s' % str(args))
+            if kwargs:
+                log.info('kwargs: %s' % kwargs)
+
+        # TODO: create a class to hold the attempts records and perform checks
+        # with its methods? or just store attempts=get_user_attempts here and
+        # pass it to the functions
+        # also no need to keep accessing these:
+        # ip = request.META.get('REMOTE_ADDR', '')
+        # ua = request.META.get('HTTP_USER_AGENT', '<unknown>')
+        # username = request.POST.get(USERNAME_FORM_FIELD, None)
+
+        # if the request is currently under lockout, do not proceed to the
+        # login function, go directly to lockout url, do not pass go, do not
+        # collect messages about this login attempt
+        if is_already_locked(request):
+            return lockout_response(request)
+
+        # call the login function
+        response = func(request, *args, **kwargs)
+
+        if func.__name__ == 'decorated_login':
+            # if we're dealing with this function itself, don't bother checking
+            # for invalid login attempts.  I suppose there's a bunch of
+            # recursion going on here that used to cause one failed login
+            # attempt to generate 10+ failed access attempt records (with 3
+            # failed attempts each supposedly)
+            return response
+
+        if request.method == 'POST':
+            # see if the login was successful
+
+            login_unsuccessful = (
+                response and
+                not response.has_header('location') and
+                response.status_code != 302
+            )
+
+            access_log = AccessLog.objects.create(
+                user_agent=request.META.get('HTTP_USER_AGENT', '<unknown>')[:255],
+                ip_address=get_ip(request),
+                username=request.POST.get(USERNAME_FORM_FIELD, None),
+                http_accept=request.META.get('HTTP_ACCEPT', '<unknown>'),
+                path_info=request.META.get('PATH_INFO', '<unknown>'),
+                trusted=not login_unsuccessful,
+            )
+            if check_request(request, login_unsuccessful):
+                return response
+
+            return lockout_response(request)
+
+        return response
+
+    return decorated_login
+
+
+def lockout_response(request):
+    if LOCKOUT_TEMPLATE:
+        context = {
+            'cooloff_time': COOLOFF_TIME,
+            'failure_limit': FAILURE_LIMIT,
+            'username': request.POST.get(USERNAME_FORM_FIELD, '')
+        }
+        return render_to_response(LOCKOUT_TEMPLATE, context,
+                                  context_instance=RequestContext(request))
+
+    LOCKOUT_URL = get_lockout_url()
+    if LOCKOUT_URL:
+        return HttpResponseRedirect(LOCKOUT_URL)
+
+    if COOLOFF_TIME:
+        return HttpResponse("Account locked: too many login attempts.  "
+                            "Please try again later.")
+    else:
+        return HttpResponse("Account locked: too many login attempts.  "
+                            "Contact an admin to unlock your account.")
+
+
+def is_already_locked(request):
+    ip = get_ip(request)
+
+    if ONLY_WHITELIST:
+        if not ip_in_whitelist(ip):
+            return True
+
+    if ip_in_blacklist(ip):
+        return True
+
+    attempts = get_user_attempts(request)
+    user_lockable = is_user_lockable(request)
+    for attempt in attempts:
+        if attempt.failures_since_start >= FAILURE_LIMIT and LOCK_OUT_AT_FAILURE and user_lockable:
+            return True
+
+    return False
+
+
+def check_request(request, login_unsuccessful):
+    ip_address = get_ip(request)
+    username = request.POST.get(USERNAME_FORM_FIELD, None)
+    failures = 0
+    attempts = get_user_attempts(request)
+
+    for attempt in attempts:
+        failures = max(failures, attempt.failures_since_start)
+
+    if login_unsuccessful:
+        # add a failed attempt for this user
+        failures += 1
+
+        # Create an AccessAttempt record if the login wasn't successful
+        # has already attempted, update the info
+        if len(attempts):
+            for attempt in attempts:
+                attempt.get_data = '%s\n---------\n%s' % (
+                    attempt.get_data,
+                    query2str(request.GET.items()),
+                )
+                attempt.post_data = '%s\n---------\n%s' % (
+                    attempt.post_data,
+                    query2str(request.POST.items())
+                )
+                attempt.http_accept = request.META.get('HTTP_ACCEPT', '<unknown>')
+                attempt.path_info = request.META.get('PATH_INFO', '<unknown>')
+                attempt.failures_since_start = failures
+                attempt.attempt_time = datetime.now()
+                attempt.save()
+                log.info('AXES: Repeated login failure by %s. Updating access '
+                         'record. Count = %s' %
+                         (attempt.ip_address, failures))
+        else:
+            create_new_failure_records(request, failures)
+    else:
+        # user logged in -- forget the failed attempts
+        failures = 0
+        trusted_record_exists = False
+        for attempt in attempts:
+            if not attempt.trusted:
+                attempt.delete()
+            else:
+                trusted_record_exists = True
+                attempt.failures_since_start = 0
+                attempt.save()
+
+        if trusted_record_exists is False:
+            create_new_trusted_record(request)
+
+    user_lockable = is_user_lockable(request)
+    # no matter what, we want to lock them out if they're past the number of
+    # attempts allowed, unless the user is set to notlockable
+    if failures >= FAILURE_LIMIT and LOCK_OUT_AT_FAILURE and user_lockable:
+        # We log them out in case they actually managed to enter the correct
+        # password
+        if hasattr(request, 'user') and request.user.is_authenticated():
+            logout(request)
+        log.warn('AXES: locked out %s after repeated login attempts.' %
+                 (ip_address,))
+        # send signal when someone is locked out.
+        user_locked_out.send("axes", request=request, username=username, ip_address=ip_address)
+
+        # if a trusted login has violated lockout, revoke trust
+        for attempt in [a for a in attempts if a.trusted]:
+            attempt.delete()
+            create_new_failure_records(request, failures)
+
+        return False
+
+    return True
+
+
+def create_new_failure_records(request, failures):
+    ip = get_ip(request)
+    ua = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
+    username = request.POST.get(USERNAME_FORM_FIELD, None)
+
+    params = {
+        'user_agent': ua,
+        'ip_address': ip,
+        'username': username,
+        'get_data': query2str(request.GET.items()),
+        'post_data': query2str(request.POST.items()),
+        'http_accept': request.META.get('HTTP_ACCEPT', '<unknown>'),
+        'path_info': request.META.get('PATH_INFO', '<unknown>'),
+        'failures_since_start': failures,
+    }
+
+    AccessAttempt.objects.create(**params)
+
+    log.info('AXES: New login failure by %s. Creating access record.' % (ip,))
+
+
+def create_new_trusted_record(request):
+    ip = get_ip(request)
+    ua = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
+    username = request.POST.get(USERNAME_FORM_FIELD, None)
+
+    if not username:
+        return False
+
+    AccessAttempt.objects.create(
+        user_agent=ua,
+        ip_address=ip,
+        username=username,
+        get_data=query2str(request.GET.items()),
+        post_data=query2str(request.POST.items()),
+        http_accept=request.META.get('HTTP_ACCEPT', '<unknown>'),
+        path_info=request.META.get('PATH_INFO', '<unknown>'),
+        failures_since_start=0,
+        trusted=True
+    )

+ 0 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/management/__init__.py


+ 0 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/management/commands/__init__.py


+ 13 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/management/commands/axes_list_attempts.py

@@ -0,0 +1,13 @@
+from django.core.management.base import BaseCommand
+from django.core.management.base import CommandError
+
+from axes.models import AccessAttempt
+
+class Command(BaseCommand):
+    args = ''
+    help = ("List login attempts")
+
+    def handle(self, *args, **kwargs):
+        for at in  AccessAttempt.objects.all():
+            print ("%s %s %s" % (at.ip_address,  at.username, at.failures))
+

+ 23 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/management/commands/axes_reset.py

@@ -0,0 +1,23 @@
+from django.core.management.base import BaseCommand
+from django.core.management.base import CommandError
+
+from axes.utils import reset
+
+
+class Command(BaseCommand):
+    args = ''
+    help = ("resets any lockouts or failed login records. If called with an "
+            "IP, resets only for that IP")
+
+    def handle(self, *args, **kwargs):
+        count = 0
+        if args:
+            for ip in args:
+                count += reset(ip=ip)
+        else:
+            count = reset()
+
+        if count:
+            print('{0} attempts removed.'.format(count))
+        else:
+            print('No attempts found.')

+ 37 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/middleware.py

@@ -0,0 +1,37 @@
+from django.conf import settings
+from django.contrib.auth import views as auth_views
+
+from axes.decorators import watch_login
+
+
+class FailedLoginMiddleware(object):
+    def __init__(self, *args, **kwargs):
+        super(FailedLoginMiddleware, self).__init__(*args, **kwargs)
+
+        # watch the auth login
+        auth_views.login = watch_login(auth_views.login)
+
+
+class ViewDecoratorMiddleware(object):
+    """
+    When the django_axes middleware is installed, by default it watches the
+    django.auth.views.login.
+
+    This middleware allows adding protection to other views without the need
+    to change any urls or dectorate them manually.
+
+    Add this middleware to your MIDDLEWARE settings after
+    `axes.middleware.FailedLoginMiddleware` and before the django
+    flatpages middleware.
+    """
+    watched_logins = getattr(
+        settings, 'AXES_PROTECTED_LOGINS', (
+            '/accounts/login/',
+        )
+    )
+
+    def process_view(self, request, view_func, view_args, view_kwargs):
+        if request.path in self.watched_logins:
+            return watch_login(view_func)(request, *view_args, **view_kwargs)
+
+        return None

+ 72 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/models.py

@@ -0,0 +1,72 @@
+from django.db import models
+from django.utils import six
+
+class CommonAccess(models.Model):
+    user_agent = models.CharField(
+        max_length=255,
+    )
+
+    ip_address = models.GenericIPAddressField(
+        verbose_name='IP Address',
+        null=True,
+    )
+
+    username = models.CharField(
+        max_length=255,
+        null=True,
+    )
+
+    # Once a user logs in from an ip, that combination is trusted and not
+    # locked out in case of a distributed attack
+    trusted = models.BooleanField(
+        default=False,
+    )
+
+    http_accept = models.CharField(
+        verbose_name='HTTP Accept',
+        max_length=1025,
+    )
+
+    path_info = models.CharField(
+        verbose_name='Path',
+        max_length=255,
+    )
+
+    attempt_time = models.DateTimeField(
+        auto_now_add=True,
+    )
+
+    class Meta:
+        abstract = True
+        ordering = ['-attempt_time']
+
+
+class AccessAttempt(CommonAccess):
+    get_data = models.TextField(
+        verbose_name='GET Data',
+    )
+
+    post_data = models.TextField(
+        verbose_name='POST Data',
+    )
+
+    failures_since_start = models.PositiveIntegerField(
+        verbose_name='Failed Logins',
+    )
+
+    @property
+    def failures(self):
+        return self.failures_since_start
+
+    def __unicode__(self):
+        return six.u('Attempted Access: %s') % self.attempt_time
+
+
+class AccessLog(CommonAccess):
+    logout_time = models.DateTimeField(
+        null=True,
+        blank=True,
+    )
+
+    def __unicode__(self):
+        return six.u('Access Log for %s @ %s') % (self.username, self.attempt_time)

+ 34 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/signals.py

@@ -0,0 +1,34 @@
+from django.dispatch import receiver
+from django.dispatch import Signal
+from django.utils.timezone import now
+from django.contrib.auth.signals import user_logged_out
+from django.core.exceptions import ObjectDoesNotExist
+
+from axes.models import AccessLog
+
+
+user_locked_out = Signal(providing_args=['request', 'username', 'ip_address'])
+
+
+@receiver(user_logged_out)
+def log_user_lockout(sender, request, user, signal, *args, **kwargs):
+    """ When a user logs out, update the access log
+    """
+    if not user:
+        return
+
+    try:
+        username = user.get_username()
+    except AttributeError:
+        # Django < 1.5
+        username = user.username
+
+    access_logs = AccessLog.objects.filter(
+        username=username,
+        logout_time__isnull=True,
+    ).order_by('-attempt_time')
+
+    if access_logs:
+        access_log = access_logs[0]
+        access_log.logout_time = now()
+        access_log.save()

+ 37 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/test_settings.py

@@ -0,0 +1,37 @@
+from datetime import timedelta
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': ':memory:',
+    }
+}
+
+SITE_ID = 1
+
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'axes.middleware.FailedLoginMiddleware'
+)
+
+ROOT_URLCONF = 'axes.test_urls'
+
+INSTALLED_APPS = (
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'django.contrib.messages',
+    'django.contrib.admin',
+
+    'axes',
+)
+
+SECRET_KEY = 'too-secret-for-test'
+
+LOGIN_REDIRECT_URL = '/admin/'
+
+AXES_LOGIN_FAILURE_LIMIT = 10
+AXES_COOLOFF_TIME = timedelta(seconds=2)

+ 6 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/test_urls.py

@@ -0,0 +1,6 @@
+from django.conf.urls import patterns, include
+from django.contrib import admin
+
+urlpatterns = patterns('',
+    (r'^admin/', include(admin.site.urls)),
+)

+ 185 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/tests.py

@@ -0,0 +1,185 @@
+import random
+import string
+import time
+
+from django.test import TestCase
+from django.contrib.auth.models import User
+from django.core.urlresolvers import NoReverseMatch
+from django.core.urlresolvers import reverse
+
+from axes.decorators import COOLOFF_TIME
+from axes.decorators import FAILURE_LIMIT
+from axes.models import AccessLog
+from axes.signals import user_locked_out
+from axes.utils import reset
+
+
+class AccessAttemptTest(TestCase):
+    """Test case using custom settings for testing
+    """
+    VALID_PASSWORD = 'valid-password'
+    LOCKED_MESSAGE = 'Account locked: too many login attempts.'
+    LOGIN_FORM_KEY = '<input type="submit" value="Log in" />'
+
+    def _login(self, is_valid=False, user_agent='test-browser'):
+        """Login a user. A valid credential is used when is_valid is True,
+        otherwise it will use a random string to make a failed login.
+        """
+        try:
+            admin_login = reverse('admin:login')
+        except NoReverseMatch:
+            admin_login = reverse('admin:index')
+
+        if is_valid:
+            # Use a valid username
+            username = self.user.username
+        else:
+            # Generate a wrong random username
+            chars = string.ascii_uppercase + string.digits
+            username = ''.join(random.choice(chars) for x in range(10))
+
+        response = self.client.post(admin_login, {
+            'username': username,
+            'password': self.VALID_PASSWORD,
+            'this_is_the_login_form': 1,
+        }, HTTP_USER_AGENT=user_agent)
+
+        return response
+
+    def setUp(self):
+        """Create a valid user for login
+        """
+        self.user = User.objects.create_superuser(
+            username='valid-username',
+            email='test@example.com',
+            password=self.VALID_PASSWORD,
+        )
+
+    def test_failure_limit_once(self):
+        """Tests the login lock trying to login one more time
+        than failure limit
+        """
+        for i in range(1, FAILURE_LIMIT):  # test until one try before the limit
+            response = self._login()
+            # Check if we are in the same login page
+            self.assertContains(response, self.LOGIN_FORM_KEY)
+
+        # So, we shouldn't have gotten a lock-out yet.
+        # But we should get one now
+        response = self._login()
+        self.assertContains(response, self.LOCKED_MESSAGE)
+
+    def test_failure_limit_many(self):
+        """Tests the login lock trying to login a lot of times more
+        than failure limit
+        """
+        for i in range(1, FAILURE_LIMIT):
+            response = self._login()
+            # Check if we are in the same login page
+            self.assertContains(response, self.LOGIN_FORM_KEY)
+
+        # So, we shouldn't have gotten a lock-out yet.
+        # We should get a locked message each time we try again
+        for i in range(0, random.randrange(1, FAILURE_LIMIT)):
+            response = self._login()
+            self.assertContains(response, self.LOCKED_MESSAGE)
+
+    def test_valid_login(self):
+        """Tests a valid login for a real username
+        """
+        response = self._login(is_valid=True)
+        self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=302)
+
+    def test_valid_logout(self):
+        """Tests a valid logout and make sure the logout_time is updated
+        """
+        response = self._login(is_valid=True)
+        self.assertEquals(AccessLog.objects.latest('id').logout_time, None)
+
+        response = self.client.get(reverse('admin:logout'))
+        self.assertNotEquals(AccessLog.objects.latest('id').logout_time, None)
+        self.assertContains(response, 'Logged out')
+
+    def test_cooling_off(self):
+        """Tests if the cooling time allows a user to login
+        """
+        self.test_failure_limit_once()
+
+        # Wait for the cooling off period
+        time.sleep(COOLOFF_TIME.total_seconds())
+
+        # It should be possible to login again, make sure it is.
+        self.test_valid_login()
+
+    def test_cooling_off_for_trusted_user(self):
+        """Test the cooling time for a trusted user
+        """
+        # Test successful login-logout, this makes the user trusted.
+        self.test_valid_logout()
+
+        # Try the cooling off time
+        self.test_cooling_off()
+
+    def test_long_user_agent_valid(self):
+        """Tests if can handle a long user agent
+        """
+        long_user_agent = 'ie6' * 1024
+        response = self._login(is_valid=True, user_agent=long_user_agent)
+        self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=302)
+
+    def test_long_user_agent_not_valid(self):
+        """Tests if can handle a long user agent with failure
+        """
+        long_user_agent = 'ie6' * 1024
+        for i in range(0, FAILURE_LIMIT + 1):
+            response = self._login(user_agent=long_user_agent)
+
+        self.assertContains(response, self.LOCKED_MESSAGE)
+
+    def test_reset_ip(self):
+        """Tests if can reset an ip address
+        """
+        # Make a lockout
+        self.test_failure_limit_once()
+
+        # Reset the ip so we can try again
+        reset(ip='127.0.0.1')
+
+        # Make a login attempt again
+        self.test_valid_login()
+
+    def test_reset_all(self):
+        """Tests if can reset all attempts
+        """
+        # Make a lockout
+        self.test_failure_limit_once()
+
+        # Reset all attempts so we can try again
+        reset()
+
+        # Make a login attempt again
+        self.test_valid_login()
+
+    def test_send_lockout_signal(self):
+        """Test if the lockout signal is emitted
+        """
+        class Scope(object): pass  # this "hack" is needed so we don't have to use global variables or python3 features
+        scope = Scope()
+        scope.signal_received = 0
+
+        def signal_handler(request, username, ip_address, *args, **kwargs):
+            scope.signal_received += 1
+            self.assertIsNotNone(request)
+
+        # Connect signal handler
+        user_locked_out.connect(signal_handler)
+
+        # Make a lockout
+        self.test_failure_limit_once()
+        self.assertEquals(scope.signal_received, 1)
+
+        reset()
+
+        # Make another lockout
+        self.test_failure_limit_once()
+        self.assertEquals(scope.signal_received, 2)

+ 20 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/utils.py

@@ -0,0 +1,20 @@
+from axes.models import AccessAttempt
+
+
+def reset(ip=None, username=None):
+    """Reset records that match ip or username, and
+    return the count of removed attempts.
+    """
+    count = 0
+
+    attempts = AccessAttempt.objects.all()
+    if ip:
+        attempts = attempts.filter(ip_address=ip)
+    if username:
+        attempts = attempts.filter(username=username)
+
+    if attempts:
+        count = attempts.count()
+        attempts.delete()
+
+    return count

+ 1 - 0
desktop/core/ext-py/django-axes-1.4.0/axes/views.py

@@ -0,0 +1 @@
+# Create your views here.

+ 31 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/README.rst

@@ -0,0 +1,31 @@
+================================
+Example project for django-axes
+================================
+
+Installation
+================================
+1. Run the install.sh script:
+
+    $ ./install.sh
+
+2. Run the server:
+
+    $ ./manage.py runserver
+
+3. Try the app:
+
+There are two admin accounts created:
+
+- admin:test
+- test:test
+
+Open the http://localhost:8000/admin/axes/accessattempt/ URL and log in using admin:admin.
+
+In another browser open http://localhost:8000/admin/ URL and try to log in using test:1 (wrong
+password). After your 3-rd wrong login attempt, your account would be locked out.
+
+Testing
+================================
+To test the app in an easy way do as follows:
+
+    $ ./test.sh

+ 0 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/example/__init__.py


+ 0 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/example/foo/__init__.py


+ 0 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/example/foo/management/__init__.py


+ 0 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/example/foo/management/commands/__init__.py


+ 41 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/example/foo/management/commands/axes_create_test_data.py

@@ -0,0 +1,41 @@
+from __future__ import print_function
+
+from django.core.management.base import BaseCommand
+
+from django.contrib.auth.models import User
+
+def create_admin_user(username, password):
+    """
+    Create a user for testing the admin.
+
+    :param string username:
+    :param strring password:
+    """
+    u = User()
+    u.username = username
+    u.email = '{0}@dev.mail.example.com'.format(username)
+    u.is_superuser = True
+    u.is_staff = True
+    u.set_password(password)
+
+    try:
+        u.save()
+        print("Created user {0} with password {1}.".format(username, password))
+    except Exception as e:
+        #print("Failed to create user {0} with password {1}. Reason: {2}".format(username, password, str(e)))
+        pass
+
+class Command(BaseCommand):
+    def handle(self, *args, **options):
+        """
+        Creates test data.
+        """
+        try:
+            create_admin_user('admin', 'test')
+        except Exception as e:
+            pass
+
+        try:
+            create_admin_user('test', 'test')
+        except Exception as e:
+            pass

+ 0 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/example/foo/models.py


+ 26 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/example/local_settings.example

@@ -0,0 +1,26 @@
+import os
+PROJECT_DIR = lambda base : os.path.abspath(os.path.join(os.path.dirname(__file__), base).replace('\\','/'))
+
+DEBUG = True
+DEBUG_TOOLBAR = not True
+TEMPLATE_DEBUG = DEBUG
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
+        'NAME': PROJECT_DIR('../db/example.db'),                      # Or path to database file if using sqlite3.
+
+        # The following settings are not used with sqlite3:
+        'USER': '',
+        'PASSWORD': '',
+        'HOST': '',                      # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP.
+        'PORT': '',                      # Set to empty string for default.
+    }
+}
+
+INTERNAL_IPS = ('127.0.0.1',)
+
+EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
+EMAIL_FILE_PATH = PROJECT_DIR('../tmp')
+
+DEFAULT_FROM_EMAIL = '<no-reply@example.com>'

+ 10 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/example/manage.py

@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
+
+    from django.core.management import execute_from_command_line
+
+    execute_from_command_line(sys.argv)

+ 242 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/example/settings.py

@@ -0,0 +1,242 @@
+# Django settings for example project.
+import os
+PROJECT_DIR = lambda base : os.path.abspath(os.path.join(os.path.dirname(__file__), base).replace('\\','/'))
+gettext = lambda s: s
+
+DEBUG = False
+DEBUG_TOOLBAR = False
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+    # ('Your Name', 'your_email@example.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
+        'NAME': PROJECT_DIR('../db/example.db'),                      # Or path to database file if using sqlite3.
+        # The following settings are not used with sqlite3:
+        'USER': '',
+        'PASSWORD': '',
+        'HOST': '',                      # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP.
+        'PORT': '',                      # Set to empty string for default.
+    }
+}
+
+# Hosts/domain names that are valid for this site; required if DEBUG is False
+# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
+ALLOWED_HOSTS = []
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# In a Windows environment this must be set to your system time zone.
+TIME_ZONE = 'America/Chicago'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+#LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# If you set this to False, Django will not format dates, numbers and
+# calendars according to the current locale.
+USE_L10N = True
+
+# If you set this to False, Django will not use timezone-aware datetimes.
+USE_TZ = True
+
+# Absolute filesystem path to the directory that will hold user-uploaded files.
+# Example: "/var/www/example.com/media/"
+MEDIA_ROOT = PROJECT_DIR(os.path.join('..', 'media'))
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash.
+# Examples: "http://example.com/media/", "http://media.example.com/"
+MEDIA_URL = '/media/'
+
+# Absolute path to the directory static files should be collected to.
+# Don't put anything in this directory yourself; store your static files
+# in apps' "static/" subdirectories and in STATICFILES_DIRS.
+# Example: "/var/www/example.com/static/"
+STATIC_ROOT = PROJECT_DIR(os.path.join('..', 'static'))
+
+# URL prefix for static files.
+# Example: "http://example.com/static/", "http://static.example.com/"
+STATIC_URL = '/static/'
+
+# Additional locations of static files
+STATICFILES_DIRS = (
+    # Put strings here, like "/home/html/static" or "C:/www/django/static".
+    # Always use forward slashes, even on Windows.
+    # Don't forget to use absolute paths, not relative paths.
+    PROJECT_DIR(os.path.join('..', 'media', 'static')),
+)
+
+# List of finder classes that know how to find static files in
+# various locations.
+STATICFILES_FINDERS = (
+    'django.contrib.staticfiles.finders.FileSystemFinder',
+    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+#    'django.contrib.staticfiles.finders.DefaultStorageFinder',
+)
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = '6sf18c*w971i8a-m^1coasrmur2k6+q5_kyn*)s@(*_dk5q3&r'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.Loader',
+    'django.template.loaders.app_directories.Loader',
+    'django.template.loaders.eggs.Loader',
+)
+
+MIDDLEWARE_CLASSES = (
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    # Uncomment the next line for simple clickjacking protection:
+    # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'axes.middleware.FailedLoginMiddleware'
+)
+
+ROOT_URLCONF = 'urls'
+
+# Python dotted path to the WSGI application used by Django's runserver.
+WSGI_APPLICATION = 'wsgi.application'
+
+TEMPLATE_CONTEXT_PROCESSORS = (
+    "django.contrib.auth.context_processors.auth",
+    "django.core.context_processors.debug",
+    "django.core.context_processors.i18n",
+    "django.core.context_processors.media",
+    "django.core.context_processors.static",
+    "django.core.context_processors.tz",
+    "django.contrib.messages.context_processors.messages",
+    "django.core.context_processors.request"
+)
+
+TEMPLATE_DIRS = (
+    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+    # Always use forward slashes, even on Windows.
+    # Don't forget to use absolute paths, not relative paths.
+    PROJECT_DIR('templates')
+)
+
+INSTALLED_APPS = (
+    # Django core and contrib apps
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'django.contrib.admin',
+    'django.contrib.sitemaps',
+
+    'axes',
+
+    # Test app
+    'foo',
+)
+
+LOGIN_REDIRECT_URL = '/admin'
+
+# ******************** django-axes settings *********************
+# Max number of login attemts within the ``AXES_COOLOFF_TIME``
+AXES_LOGIN_FAILURE_LIMIT = 3
+
+from datetime import timedelta
+AXES_COOLOFF_TIME=timedelta(seconds = 200)
+# ******************** /django-axes settings *********************
+
+# A sample logging configuration. The only tangible logging
+# performed by this configuration is to send an email to
+# the site admins on every HTTP 500 error when DEBUG=False.
+# See http://docs.djangoproject.com/en/dev/topics/logging for
+# more details on how to customize your logging configuration.
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'filters': {
+        'require_debug_false': {
+            '()': 'django.utils.log.RequireDebugFalse'
+        }
+    },
+    'formatters': {
+        'verbose': {
+            'format': '%(levelname)s %(asctime)s [%(pathname)s:%(lineno)s] %(message)s'
+        },
+        'simple': {
+            'format': '%(levelname)s %(message)s'
+        },
+    },
+    'handlers': {
+        'mail_admins': {
+            'level': 'ERROR',
+            'filters': ['require_debug_false'],
+            'class': 'django.utils.log.AdminEmailHandler'
+        },
+        'console': {
+            'level': 'DEBUG',
+            'class': 'logging.StreamHandler',
+            'formatter': 'verbose'
+        },
+        'django_log': {
+            'level':'DEBUG',
+            'class':'logging.handlers.RotatingFileHandler',
+            'filename': PROJECT_DIR("../logs/django.log"),
+            'maxBytes': 1048576,
+            'backupCount': 99,
+            'formatter': 'verbose',
+        },
+        'axes_log': {
+            'level':'DEBUG',
+            'class':'logging.handlers.RotatingFileHandler',
+            'filename': PROJECT_DIR("../logs/axes.log"),
+            'maxBytes': 1048576,
+            'backupCount': 99,
+            'formatter': 'verbose',
+        },
+    },
+    'loggers': {
+        'django': {
+            'handlers': ['django_log'],
+            'level': 'ERROR',
+            'propagate': True,
+        },
+        'axes': {
+            'handlers': ['console', 'axes_log'],
+            'level': 'DEBUG',
+            'propagate': True,
+        },
+    },
+}
+
+# Do not put any settings below this line
+try:
+    from local_settings import *
+except:
+    pass
+
+if DEBUG and DEBUG_TOOLBAR:
+    # debug_toolbar
+    MIDDLEWARE_CLASSES += (
+        'debug_toolbar.middleware.DebugToolbarMiddleware',
+	)
+
+    INSTALLED_APPS += (
+        'debug_toolbar',
+    )
+
+    DEBUG_TOOLBAR_CONFIG = {
+        'INTERCEPT_REDIRECTS': False,
+    }

+ 17 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/example/urls.py

@@ -0,0 +1,17 @@
+from django.conf.urls import patterns, include, url
+
+from django.conf import settings
+from django.contrib import admin
+from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+from django.conf.urls.static import static
+
+admin.autodiscover()
+
+urlpatterns = patterns('',
+    # Uncomment the next line to enable the admin:
+    url(r'^admin/', include(admin.site.urls)),
+)
+
+if settings.DEBUG:
+    urlpatterns += staticfiles_urlpatterns()
+    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

+ 32 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/example/wsgi.py

@@ -0,0 +1,32 @@
+"""
+WSGI config for example project.
+
+This module contains the WSGI application used by Django's development server
+and any production WSGI deployments. It should expose a module-level variable
+named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
+this application via the ``WSGI_APPLICATION`` setting.
+
+Usually you will have the standard Django WSGI application here, but it also
+might make sense to replace the whole Django WSGI application with a custom one
+that later delegates to the Django one. For example, you could introduce WSGI
+middleware here, or combine a Django application with an application of another
+framework.
+
+"""
+import os
+
+# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
+# if running multiple sites in the same mod_wsgi process. To fix this, use
+# mod_wsgi daemon mode with each site in its own daemon process, or use
+# os.environ["DJANGO_SETTINGS_MODULE"] = "example.settings"
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
+
+# This application object is used by any WSGI server configured to use this
+# file. This includes Django's development server, if the WSGI_APPLICATION
+# setting points here.
+from django.core.wsgi import get_wsgi_application
+application = get_wsgi_application()
+
+# Apply WSGI middleware here.
+# from helloworld.wsgi import HelloWorldApplication
+# application = HelloWorldApplication(application)

+ 7 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/install.sh

@@ -0,0 +1,7 @@
+pip install django==1.5.5
+pip install django-axes
+mkdir -p logs db media media/static
+cp example/local_settings.example example/local_settings.py
+python example/manage.py collectstatic --noinput
+python example/manage.py syncdb --noinput
+python example/manage.py axes_create_test_data

+ 3 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/reinstall.sh

@@ -0,0 +1,3 @@
+reset
+./uninstall.sh
+./install.sh

+ 4 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/test.sh

@@ -0,0 +1,4 @@
+reset
+./uninstall.sh
+./install.sh
+python example/manage.py test axes --traceback

+ 5 - 0
desktop/core/ext-py/django-axes-1.4.0/examples/uninstall.sh

@@ -0,0 +1,5 @@
+pip uninstall django-axes -y
+rm build -rf
+rm dist -rf
+rm django_axes.egg-info -rf
+rm django-axes.egg-info -rf

+ 5 - 0
desktop/core/ext-py/django-axes-1.4.0/setup.cfg

@@ -0,0 +1,5 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+

+ 43 - 0
desktop/core/ext-py/django-axes-1.4.0/setup.py

@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+from setuptools import setup, find_packages
+
+VERSION = '1.4.0'
+
+setup(
+    name='django-axes',
+    version=VERSION,
+    description="Keep track of failed login attempts in Django-powered sites.",
+    long_description=(
+        open("README.rst").read() + '\n' +
+        open("CHANGES.txt").read()),
+    keywords='authentication, django, pci, security',
+    author='Josh VanderLinden, Philip Neustrom, Michael Blume, Camilo Nova',
+    author_email='codekoala@gmail.com',
+    maintainer='Alex Clark',
+    maintainer_email='aclark@aclark.net',
+    url='https://github.com/django-pci/django-axes',
+    license='MIT',
+    package_dir={'axes': 'axes'},
+    include_package_data=True,
+    packages=find_packages(),
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'Environment :: Web Environment',
+        'Framework :: Django',
+        'Intended Audience :: Developers',
+        'Intended Audience :: System Administrators',
+        'License :: OSI Approved :: MIT License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Topic :: Internet :: Log Analysis',
+        'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware',
+        'Topic :: Security',
+        'Topic :: System :: Logging',
+    ],
+    zip_safe=False,
+)

+ 2 - 0
desktop/core/src/desktop/auth/views.py

@@ -26,6 +26,7 @@ import logging
 import urllib
 
 import django.contrib.auth.views
+from axes.decorators import watch_login
 from django.core import urlresolvers
 from django.core.exceptions import SuspiciousOperation
 from django.contrib.auth import login, get_backends, authenticate
@@ -81,6 +82,7 @@ def get_backend_names():
 
 
 @login_notrequired
+@watch_login
 def dt_login(request):
   redirect_to = request.REQUEST.get('next', '/')
   is_first_login_ever = first_login_ever()

+ 36 - 1
desktop/core/src/desktop/conf.py

@@ -15,6 +15,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import datetime
 import logging
 import os
 import socket
@@ -64,6 +65,10 @@ def coerce_password_from_script(script):
   return stdout.strip('\n')
 
 
+def coerce_timedelta(value):
+  return datetime.timedelta(seconds=int(value))
+
+
 HTTP_HOST = Config(
   key="http_host",
   help=_("HTTP host to bind to."),
@@ -605,7 +610,37 @@ AUTH = ConfigSection(
                                    "their password upon first login.  The default is false."),
                             type=coerce_bool,
                             default=False,
-    )
+    ),
+    LOGIN_FAILURE_LIMIT = Config(
+      key="login_failure_limit",
+      help=_("Number of login attempts allowed before a record is created for failed logins"),
+      type=int,
+      default=3,
+    ),
+    LOGIN_LOCK_OUT_AT_FAILURE = Config(
+      key="login_lock_out_at_failure",
+      help=_("After number of allowed login attempts are exceeded, do we lock out this IP and optionally user agent?"),
+      type=coerce_bool,
+      default=False,
+    ),
+    LOGIN_COOLOFF_TIME = Config(
+      key="login_cooloff_time",
+      help=_("If set, defines period of inactivity in seconds after which failed logins will be forgotten"),
+      type=coerce_timedelta,
+      default=None,
+    ),
+    LOGIN_LOCK_OUT_BY_COMBINATION_BROWSER_USER_AGENT_AND_IP = Config(
+      key="login_lock_out_by_combination_browser_user_agent_and_ip",
+      help=_("If True, lock out based on IP and browser user agent"),
+      type=coerce_bool,
+      default=False,
+    ),
+    LOGIN_LOCK_OUT_BY_COMBINATION_USER_AND_IP = Config(
+      key="login_lock_out_by_combination_user_and_ip",
+      help=_("If True, lock out based on IP and user"),
+      type=coerce_bool,
+      default=False,
+    ),
 ))
 
 LDAP = ConfigSection(

+ 12 - 1
desktop/core/src/desktop/settings.py

@@ -146,6 +146,7 @@ MIDDLEWARE_CLASSES = [
     'django.middleware.csrf.CsrfViewMiddleware',
 
     'django.middleware.http.ConditionalGetMiddleware',
+    'axes.middleware.FailedLoginMiddleware',
 ]
 
 if os.environ.get(ENV_DESKTOP_DEBUG):
@@ -179,7 +180,10 @@ INSTALLED_APPS = [
     'babeldjango',
 
     # Desktop injects all the other installed apps into here magically.
-    'desktop'
+    'desktop',
+
+    # App that keeps track of failed logins.
+    'axes',
 ]
 
 LOCALE_PATHS = [
@@ -362,6 +366,13 @@ else:
   import uuid
   SECRET_KEY = str(uuid.uuid4())
 
+# Axes
+AXES_LOGIN_FAILURE_LIMIT = desktop.conf.AUTH.LOGIN_FAILURE_LIMIT.get()
+AXES_LOCK_OUT_AT_FAILURE = desktop.conf.AUTH.LOGIN_LOCK_OUT_AT_FAILURE.get()
+AXES_COOLOFF_TIME = desktop.conf.AUTH.LOGIN_COOLOFF_TIME.get()
+AXES_USE_USER_AGENT = desktop.conf.AUTH.LOGIN_LOCK_OUT_BY_COMBINATION_BROWSER_USER_AGENT_AND_IP.get()
+AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP = desktop.conf.AUTH.LOGIN_LOCK_OUT_BY_COMBINATION_USER_AND_IP.get()
+
 # SAML
 SAML_AUTHENTICATION = 'libsaml.backend.SAML2Backend' in AUTHENTICATION_BACKENDS
 if SAML_AUTHENTICATION: