فهرست منبع

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 سال پیش
والد
کامیت
d378e37
45فایلهای تغییر یافته به همراه2427 افزوده شده و 2 حذف شده
  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: