Browse Source

Upgrading django-axes version for Django1

ayush.goyal 5 years ago
parent
commit
18d886f8e7
74 changed files with 5175 additions and 1555 deletions
  1. 0 16
      desktop/core/ext-py/django-axes-2.2.0/.travis.yml
  2. 0 216
      desktop/core/ext-py/django-axes-2.2.0/Makefile
  3. 0 212
      desktop/core/ext-py/django-axes-2.2.0/README.rst
  4. 0 11
      desktop/core/ext-py/django-axes-2.2.0/axes/apps.py
  5. 0 486
      desktop/core/ext-py/django-axes-2.2.0/axes/decorators.py
  6. 0 0
      desktop/core/ext-py/django-axes-2.2.0/axes/migrations/__init__.py
  7. 0 27
      desktop/core/ext-py/django-axes-2.2.0/axes/signals.py
  8. 0 16
      desktop/core/ext-py/django-axes-2.2.0/axes/test_urls.py
  9. 0 299
      desktop/core/ext-py/django-axes-2.2.0/axes/tests.py
  10. 0 41
      desktop/core/ext-py/django-axes-2.2.0/axes/utils.py
  11. 0 15
      desktop/core/ext-py/django-axes-2.2.0/runtests.py
  12. 42 0
      desktop/core/ext-py/django-axes-4.5.4/.travis.yml
  13. 315 0
      desktop/core/ext-py/django-axes-4.5.4/CHANGES.txt
  14. 0 0
      desktop/core/ext-py/django-axes-4.5.4/LICENSE
  15. 1 1
      desktop/core/ext-py/django-axes-4.5.4/MANIFEST.in
  16. 345 167
      desktop/core/ext-py/django-axes-4.5.4/PKG-INFO
  17. 65 0
      desktop/core/ext-py/django-axes-4.5.4/README.rst
  18. 3 1
      desktop/core/ext-py/django-axes-4.5.4/axes/__init__.py
  19. 37 5
      desktop/core/ext-py/django-axes-4.5.4/axes/admin.py
  20. 29 0
      desktop/core/ext-py/django-axes-4.5.4/axes/apps.py
  21. 213 0
      desktop/core/ext-py/django-axes-4.5.4/axes/attempts.py
  22. 49 0
      desktop/core/ext-py/django-axes-4.5.4/axes/backends.py
  23. 84 0
      desktop/core/ext-py/django-axes-4.5.4/axes/conf.py
  24. 80 0
      desktop/core/ext-py/django-axes-4.5.4/axes/decorators.py
  25. BIN
      desktop/core/ext-py/django-axes-4.5.4/axes/locale/de/LC_MESSAGES/django.mo
  26. 97 0
      desktop/core/ext-py/django-axes-4.5.4/axes/locale/de/LC_MESSAGES/django.po
  27. BIN
      desktop/core/ext-py/django-axes-4.5.4/axes/locale/ru/LC_MESSAGES/django.mo
  28. 97 0
      desktop/core/ext-py/django-axes-4.5.4/axes/locale/ru/LC_MESSAGES/django.po
  29. BIN
      desktop/core/ext-py/django-axes-4.5.4/axes/locale/tr/LC_MESSAGES/django.mo
  30. 96 0
      desktop/core/ext-py/django-axes-4.5.4/axes/locale/tr/LC_MESSAGES/django.po
  31. 1 0
      desktop/core/ext-py/django-axes-4.5.4/axes/management/__init__.py
  32. 1 0
      desktop/core/ext-py/django-axes-4.5.4/axes/management/commands/__init__.py
  33. 4 2
      desktop/core/ext-py/django-axes-4.5.4/axes/management/commands/axes_list_attempts.py
  34. 10 7
      desktop/core/ext-py/django-axes-4.5.4/axes/management/commands/axes_reset.py
  35. 22 0
      desktop/core/ext-py/django-axes-4.5.4/axes/management/commands/axes_reset_user.py
  36. 0 0
      desktop/core/ext-py/django-axes-4.5.4/axes/migrations/0001_initial.py
  37. 0 0
      desktop/core/ext-py/django-axes-4.5.4/axes/migrations/0002_auto_20151217_2044.py
  38. 0 0
      desktop/core/ext-py/django-axes-4.5.4/axes/migrations/0003_auto_20160322_0929.py
  39. 58 0
      desktop/core/ext-py/django-axes-4.5.4/axes/migrations/0004_auto_20181024_1538.py
  40. 17 0
      desktop/core/ext-py/django-axes-4.5.4/axes/migrations/0005_remove_accessattempt_trusted.py
  41. 0 0
      desktop/core/ext-py/django-axes-4.5.4/axes/migrations/__init__.py
  42. 33 21
      desktop/core/ext-py/django-axes-4.5.4/axes/models.py
  43. 184 0
      desktop/core/ext-py/django-axes-4.5.4/axes/signals.py
  44. 33 6
      desktop/core/ext-py/django-axes-4.5.4/axes/test_settings.py
  45. 11 0
      desktop/core/ext-py/django-axes-4.5.4/axes/test_settings_cache.py
  46. 8 0
      desktop/core/ext-py/django-axes-4.5.4/axes/test_urls.py
  47. 1 0
      desktop/core/ext-py/django-axes-4.5.4/axes/tests/__init__.py
  48. 6 0
      desktop/core/ext-py/django-axes-4.5.4/axes/tests/compatibility.py
  49. 476 0
      desktop/core/ext-py/django-axes-4.5.4/axes/tests/test_access_attempt.py
  50. 435 0
      desktop/core/ext-py/django-axes-4.5.4/axes/tests/test_access_attempt_config.py
  51. 27 0
      desktop/core/ext-py/django-axes-4.5.4/axes/tests/test_models.py
  52. 239 0
      desktop/core/ext-py/django-axes-4.5.4/axes/tests/test_utils.py
  53. 167 0
      desktop/core/ext-py/django-axes-4.5.4/axes/utils.py
  54. 0 1
      desktop/core/ext-py/django-axes-4.5.4/setup.cfg
  55. 22 4
      desktop/core/ext-py/django-axes-4.5.4/setup.py
  56. 22 0
      desktop/core/ext-py/django-ipware-2.1.0/PKG-INFO
  57. 224 0
      desktop/core/ext-py/django-ipware-2.1.0/README.md
  58. 8 0
      desktop/core/ext-py/django-ipware-2.1.0/ipware/__init__.py
  59. 10 0
      desktop/core/ext-py/django-ipware-2.1.0/ipware/apps.py
  60. 78 0
      desktop/core/ext-py/django-ipware-2.1.0/ipware/defaults.py
  61. 60 0
      desktop/core/ext-py/django-ipware-2.1.0/ipware/ip.py
  62. 60 0
      desktop/core/ext-py/django-ipware-2.1.0/ipware/ip2.py
  63. 0 0
      desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/__init__.py
  64. 84 0
      desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/tests_common.py
  65. 253 0
      desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/tests_ipv4.py
  66. 173 0
      desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/tests_ipv6.py
  67. 1 0
      desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/tests_v1/__init__.py
  68. 400 0
      desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/tests_v1/tests_ipv4.py
  69. 276 0
      desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/tests_v1/tests_ipv6.py
  70. 12 0
      desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/testsettings.py
  71. 115 0
      desktop/core/ext-py/django-ipware-2.1.0/ipware/utils.py
  72. 4 0
      desktop/core/ext-py/django-ipware-2.1.0/setup.cfg
  73. 86 0
      desktop/core/ext-py/django-ipware-2.1.0/setup.py
  74. 1 1
      desktop/core/requirements.txt

+ 0 - 16
desktop/core/ext-py/django-axes-2.2.0/.travis.yml

@@ -1,16 +0,0 @@
-language: python
-
-python:
-  - 2.7
-  - 3.5
-
-env:
-  - DJANGO="Django>=1.8,<1.9"
-  - DJANGO="Django>=1.9,<1.10"
-  - DJANGO="Django --pre"
-
-install:
-  - pip install -q $DJANGO
-
-script:
-  - ./runtests.py

+ 0 - 216
desktop/core/ext-py/django-axes-2.2.0/Makefile

@@ -1,216 +0,0 @@
-# https://github.com/aclark4life/project-makefile
-#
-# The MIT License (MIT)
-#
-# Copyright (c) 2016 Alex Clark
-#
-# 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.
-
-.DEFAULT_GOAL=git-commit-edit-push
-
-APP=app
-MESSAGE="Update"
-PROJECT=project
-TMP:=$(shell echo `tmp`)
-
-commit: git-commit-auto-push
-co: git-checkout-branches
-db: django-migrate django-su
-db-clean: django-db-clean-postgres
-django-start: django-init
-fe-init: npm-init npm-install grunt-init grunt-serve
-fe: npm-install grunt-serve
-freeze: python-pip-freeze
-heroku: heroku-push
-install: python3.5-virtualenv python-pip-install
-lint: python-flake python-yapf python-wc
-migrate: django-migrate
-push: git-push
-plone-start: plone-init
-readme: python-package-readme-test
-release: python-package-release
-releasetest: python-package-release-test
-serve: python-serve
-sphinx-start: sphinx-init
-static: django-static
-test: django-test
-vm: vagrant-up
-vm-down: vagrant-suspend
-
-
-# Django
-django-db-clean-postgres:
-	-dropdb $(PROJECT)-$(APP)
-	-createdb $(PROJECT)-$(APP)
-django-db-clean-sqlite:
-	-rm -f $(PROJECT)-$(APP).sqlite3
-django-init:
-	-mkdir -p $(PROJECT)/$(APP)
-	-django-admin startproject $(PROJECT) .
-	-django-admin startapp $(APP) $(PROJECT)/$(APP)
-django-install:
-	$(MAKE) python-virtualenv
-	bin/pip install Django
-django-migrate:
-	python manage.py migrate
-django-migrations:
-	python manage.py makemigrations $(APP)
-django-migrations-clean:
-	rm -rf $(PROJECT)/$(APP)/migrations
-	$(MAKE) django-migrations
-django-serve:
-	python manage.py runserver
-django-test:
-	python manage.py test
-django-shell:
-	python manage.py shell
-django-static:
-	python manage.py collectstatic --noinput
-django-su:
-	python manage.py createsuperuser
-
-# Git
-REMOTE_BRANCHES=`git branch -a |\
-	grep remote |\
-	grep -v HEAD |\
-	grep -v master`
-git-checkout-branches:
-	-for i in $(REMOTE_BRANCHES) ; do \
-        git checkout -t $$i ; done
-git-commit-auto-push:
-	git commit -a -m $(MESSAGE)
-	$(MAKE) git-push
-git-commit-edit-push:
-	git commit -a
-	$(MAKE) git-push
-git-push:
-	git push
-
-# Heroku
-heroku-debug-on:
-	heroku config:set DEBUG=1
-heroku-debug-off:
-	heroku config:unset DEBUG
-heroku-push:
-	git push heroku
-heroku-shell:
-	heroku run bash
-
-# Misc
-help:
-	@echo "\nPlease run \`make\` with one of these targets:\n"
-	@$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F:\
-        '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}'\
-        | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | xargs | tr ' ' '\n' | awk\
-        '{print "    - "$$0}'
-	@echo "\n"
-review:
-	open -a "Sublime Text 2" `find $(PROJECT) -name \*.py | grep -v __init__.py`\
-        `find $(PROJECT) -name \*.html`
-
-# Node
-npm-init:
-	npm init
-npm-install:
-	npm install
-grunt-init:
-	npm install grunt
-	grunt-init Gruntfile
-grunt-serve:
-	grunt serve
-
-# Plone
-plone-heroku:
-	-@createuser -s plone > /dev/null 2>&1
-	-@createdb -U plone plone > /dev/null 2>&1
-	@export PORT=8080 && \
-		export USERNAME=admin && \
-		export PASSWORD=admin && \
-		bin/buildout -c heroku.cfg
-plone-init:
-	plock --force --no-cache --no-virtualenv .
-plone-install:
-	$(MAKE) install
-	bin/buildout
-plone-db-sync:
-	bin/buildout -c database.cfg
-plone-serve:
-	@echo "Zope about to handle requests here:\n\n\thttp://localhost:8080\n"
-	@bin/plone fg
-
-# Python
-python-clean-pyc:
-	find . -name \*.pyc | xargs rm -v
-python-flake:
-	-flake8 *.py
-	-flake8 $(PROJECT)/*.py
-	-flake8 $(PROJECT)/$(APP)/*.py
-python-package-check:
-	check-manifest
-	pyroma .
-python-package-readme-test:
-	rst2html.py README.rst > readme.html; open readme.html
-python-package-release:
-	python setup.py sdist --format=gztar,zip upload
-python-package-release-test:
-	python setup.py sdist --format=gztar,zip upload -r test
-python-package-test:
-	python setup.py test
-python-pip-freeze:
-	bin/pip freeze | sort > $(TMP)/requirements.txt
-	mv -f $(TMP)/requirements.txt .
-python-pip-install:
-	bin/pip install -r requirements.txt
-python-serve:
-	@echo "\n\tServing HTTP on http://0.0.0.0:8000\n"
-	python -m SimpleHTTPServer
-python-virtualenv:
-	virtualenv .
-python-yapf:
-	-yapf -i *.py
-	-yapf -i -e $(PROJECT)/urls.py $(PROJECT)/*.py
-	-yapf -i $(PROJECT)/$(APP)/*.py
-python-wc:
-	-wc -l *.py
-	-wc -l $(PROJECT)/*.py
-	-wc -l $(PROJECT)/$(APP)/*.py
-
-# Sphinx
-sphinx-init:
-	sphinx-quickstart -q -p "Python Project" -a "Alex Clark" -v 0.0.1 doc
-sphinx-serve:
-	@echo "\nServing HTTP on http://0.0.0.0:8085 ...\n"
-	pushd _build/html; python -m SimpleHTTPServer 8085; popd
-
-# Vagrant
-vagrant-box-update:
-	vagrant box update
-vagrant-clean:
-	vagrant destroy
-vagrant-down:
-	vagrant suspend
-vagrant-init:
-	vagrant init ubuntu/trusty64
-	vagrant up --provider virtualbox
-vagrant-up:
-	vagrant up --provision
-
-# Django-axes
-python3.5-virtualenv:
-	virtualenv-3.5 .

+ 0 - 212
desktop/core/ext-py/django-axes-2.2.0/README.rst

@@ -1,212 +0,0 @@
-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 a supported Django version. The application is
-intended to work around the Django admin and the regular
-``django.contrib.auth`` login-powered pages.
-Look at https://www.djangoproject.com/download/ to check if your version
-is supported.
-
-
-Installation
-============
-
-You can install the latest stable package running this command::
-
-    $ pip install django-axes
-
-
-Development
-===========
-
-You can contribute to this project forking it from github and sending pull requests.
-
-Running tests
--------------
-
-Clone the repository and install the django version you want. Then run::
-
-    $ ./runtests.py
-
-
-Configuration
-=============
-
-Just add `axes` to your ``INSTALLED_APPS``::
-
-    INSTALLED_APPS = (
-        'django.contrib.admin',
-        'django.contrib.auth',
-        'django.contrib.contenttypes',
-        'django.contrib.sessions',
-        'django.contrib.sites',
-        ...
-        'axes',
-        ...
-    )
-
-Remember to run ``python manage.py migrate`` to sync the database.
-
-
-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``
-* ``AXES_NEVER_LOCKOUT_WHITELIST``: If ``True``, users can always login from
-  whitelisted IP addresses.
-  Default: ``False``
-* ``AXES_BEHIND_REVERSE_PROXY``: If ``True``, it will look for the IP address from the header defined at ``AXES_REVERSE_PROXY_HEADER``. Please make sure if you enable this setting to configure your proxy to set the correct value for the header, otherwise you could be attacked by setting this header directly in every request.
-  Default: ``False``
-* ``AXES_REVERSE_PROXY_HEADER``: If ``AXES_BEHIND_REVERSE_PROXY`` is ``True``, it will look for the IP address from this header.
-  Default: ``HTTP_X_FORWARDED_FOR``
-
-
-Usage
-=====
-
-Using ``django-axes`` is extremely simple. 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 a 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 my.custom.app import login
-    from axes.decorators import watch_login
-    ...
-    urlpatterns = patterns('',
-        (r'^login/$', watch_login(login)),
-    ...
-
-
-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"
-
-
-Using a captcha
-===============
-
-Using https://github.com/mbi/django-simple-captcha you do the following:
-
-1. Change axes lockout url in ``settings.py``::
-
-    AXES_LOCKOUT_URL = '/locked'
-
-2. Add the url in ``urls.py``::
-
-    url(r'^locked/$', locked_out, name='locked_out'),
-
-3. Create a captcha form::
-
-    class AxesCaptchaForm(forms.Form):
-        captcha = CaptchaField()
-
-4. Create a captcha view for the above url that resets on captcha success and redirects::
-
-    def locked_out(request):
-        if request.POST:
-            form = AxesCaptchaForm(request.POST)
-            if form.is_valid():
-                ip = get_ip_address_from_request(request)
-                reset(ip=ip)
-                return HttpResponseRedirect(reverse_lazy('signin'))
-        else:
-            form = AxesCaptchaForm()
-
-        return render_to_response('locked_out.html', dict(form=form), context_instance=RequestContext(request))
-
-5. Add a captcha template::
-
-    <form action="" method="post">
-        {% csrf_token %}
-
-        {{ form.captcha.errors }}
-        {{ form.captcha }}
-
-        <div class="form-actions">
-            <input type="submit" value="Submit" />
-        </div>
-    </form>
-
-Done.

+ 0 - 11
desktop/core/ext-py/django-axes-2.2.0/axes/apps.py

@@ -1,11 +0,0 @@
-from django import apps
-
-
-class AppConfig(apps.AppConfig):
-    name = 'axes'
-
-    def ready(self):
-        from django.contrib.auth import views as auth_views
-        from axes.decorators import watch_login
-
-        auth_views.login = watch_login(auth_views.login)

+ 0 - 486
desktop/core/ext-py/django-axes-2.2.0/axes/decorators.py

@@ -1,486 +0,0 @@
-import json
-import logging
-
-from datetime import timedelta
-
-from django.conf import settings
-from django.contrib.auth import get_user_model
-from django.contrib.auth import logout
-from django.http import HttpResponse
-from django.http import HttpResponseRedirect
-from django.shortcuts import render
-from django.utils import six
-from django.utils import timezone as datetime
-
-from axes.models import AccessAttempt
-from axes.models import AccessLog
-from axes.signals import user_locked_out
-from axes.utils import iso8601
-import axes
-
-
-# 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)
-
-# 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)
-
-LOCKOUT_URL = getattr(settings, 'AXES_LOCKOUT_URL', None)
-
-VERBOSE = getattr(settings, 'AXES_VERBOSE', True)
-
-# whitelist and blacklist
-# TODO: convert the strings to IPv4 on startup to avoid type conversion during processing
-NEVER_LOCKOUT_WHITELIST = \
-    getattr(settings, 'AXES_NEVER_LOCKOUT_WHITELIST', False)
-
-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)
-
-
-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')
-    log.debug('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:
-        # Hue's LB can be hosted on Private IP address.
-        if is_valid_ip(x_forwarded_for):
-            ip_address = x_forwarded_for.strip()
-    elif x_forwarded_for and ',' in x_forwarded_for:
-        # Hue Server may have more than one reverse proxy in front of it.
-        x_forwarded_for_first_ip = x_forwarded_for.split(',')[0]
-        if is_valid_ip(x_forwarded_for_first_ip):
-            ip_address = x_forwarded_for_first_ip.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):
-    ip = get_ip_address_from_request(request)
-    return ip
-
-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
-    via excessively large payloads.
-    """
-    return '\n'.join([
-        '%s=%s' % (k, v) for k, v in six.iteritems(items)
-        if k != PASSWORD_FORM_FIELD
-    ][:int(max_length / 2)])[: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(get_user_model(), 'USERNAME_FIELD', 'username')
-        kwargs = {
-            field: request.POST.get(USERNAME_FORM_FIELD)
-        }
-        user = get_user_model().objects.get(**kwargs)
-
-        if hasattr(user, 'nolockout'):
-            # need to invert since we need to return
-            # false for users that can't be blocked
-            return not user.nolockout
-
-    except get_user_model().DoesNotExist:
-        # not a valid user
-        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:
-        params = {'ip_address': ip, 'trusted': False}
-        if USE_USER_AGENT:
-            params['user_agent'] = ua
-        if LOCK_OUT_BY_COMBINATION_USER_AND_IP:
-            params['username'] = username
-
-        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.
-    """
-
-    # Don't decorate multiple times
-    if func.__name__ == 'decorated_login':
-        return func
-
-    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
-            )
-
-            user_agent = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
-            http_accept = request.META.get('HTTP_ACCEPT', '<unknown>')[:1025]
-            path_info = request.META.get('PATH_INFO', '<unknown>')[:255]
-            AccessLog.objects.create(
-                user_agent=user_agent,
-                ip_address=get_ip(request),
-                username=request.POST.get(USERNAME_FORM_FIELD, None),
-                http_accept=http_accept,
-                path_info=path_info,
-                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):
-    context = {
-        'failure_limit': FAILURE_LIMIT,
-        'username': request.POST.get(USERNAME_FORM_FIELD, '')
-    }
-
-    if request.is_ajax():
-        if COOLOFF_TIME:
-            context.update({'cooloff_time': iso8601(COOLOFF_TIME)})
-
-        return HttpResponse(
-            json.dumps(context),
-            content_type='application/json',
-            status=403,
-        )
-
-    elif LOCKOUT_TEMPLATE:
-        if COOLOFF_TIME:
-            context.update({'cooloff_time': iso8601(COOLOFF_TIME)})
-
-        return render(request, LOCKOUT_TEMPLATE, context, status=403)
-
-    elif LOCKOUT_URL:
-        return HttpResponseRedirect(LOCKOUT_URL)
-
-    else:
-        msg = 'Account locked: too many login attempts. {0}'
-        if COOLOFF_TIME:
-            msg = msg.format('Please try again later.')
-        else:
-            msg = msg.format('Contact an admin to unlock your account.')
-
-        return HttpResponse(msg, status=403)
-
-
-def is_already_locked(request):
-    ip = get_ip(request)
-
-    if NEVER_LOCKOUT_WHITELIST and ip_in_whitelist(ip):
-        return False
-
-    if ONLY_WHITELIST and not ip_in_whitelist(ip):
-        return True
-
-    if ip_in_blacklist(ip):
-        return True
-
-    if not is_user_lockable(request):
-        return False
-
-    for attempt in get_user_attempts(request):
-        if attempt.failures_since_start >= FAILURE_LIMIT and LOCK_OUT_AT_FAILURE:
-            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),
-                )
-                attempt.post_data = '%s\n---------\n%s' % (
-                    attempt.post_data,
-                    query2str(request.POST)
-                )
-                attempt.http_accept = \
-                    request.META.get('HTTP_ACCEPT', '<unknown>')[:1025]
-                attempt.path_info = \
-                    request.META.get('PATH_INFO', '<unknown>')[:255]
-                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)
-
-    if NEVER_LOCKOUT_WHITELIST and ip_in_whitelist(ip_address):
-        return True
-
-    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)
-
-    AccessAttempt.objects.create(
-        user_agent=ua,
-        ip_address=ip,
-        username=username,
-        get_data=query2str(request.GET),
-        post_data=query2str(request.POST),
-        http_accept=request.META.get('HTTP_ACCEPT', '<unknown>'),
-        path_info=request.META.get('PATH_INFO', '<unknown>'),
-        failures_since_start=failures,
-    )
-
-    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),
-        post_data=query2str(request.POST),
-        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-2.2.0/axes/migrations/__init__.py


+ 0 - 27
desktop/core/ext-py/django-axes-2.2.0/axes/signals.py

@@ -1,27 +0,0 @@
-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 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
-
-    access_logs = AccessLog.objects.filter(
-        username=user.get_username(),
-        logout_time__isnull=True,
-    ).order_by('-attempt_time')
-
-    if access_logs.exists():
-        access_log = access_logs.first()
-        access_log.logout_time = now()
-        access_log.save()

+ 0 - 16
desktop/core/ext-py/django-axes-2.2.0/axes/test_urls.py

@@ -1,16 +0,0 @@
-from django.conf.urls import url
-from django.contrib import admin
-
-try:
-    # django < 1.10
-    from django.conf.urls import patterns
-    from django.conf.urls import include
-
-    urlpatterns = patterns(
-        '',
-        url(r'^admin/', include(admin.site.urls)),
-    )
-except ImportError:
-    urlpatterns = [
-        url(r'^admin/', admin.site.urls),
-    ]

+ 0 - 299
desktop/core/ext-py/django-axes-2.2.0/axes/tests.py

@@ -1,299 +0,0 @@
-import random
-import string
-import time
-import json
-import datetime
-
-from django.test import TestCase
-from django.test.utils import override_settings
-from django.contrib.auth.models import User
-from django.urls import NoReverseMatch
-from django.urls import reverse
-from django.utils import six
-
-from axes.decorators import COOLOFF_TIME
-from axes.decorators import FAILURE_LIMIT
-from axes.decorators import is_valid_ip
-from axes.models import AccessAttempt, AccessLog
-from axes.signals import user_locked_out
-from axes.utils import reset, iso8601
-
-
-class AccessAttemptTest(TestCase):
-    """Test case using custom settings for testing
-    """
-    VALID_USERNAME = 'valid-username'
-    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_username=False, is_valid_password=False,
-               is_json=False, **kwargs):
-        """Login a user. A valid credential is used when is_valid_username 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_username:
-            # Use a valid username
-            username = self.VALID_USERNAME
-        else:
-            # Generate a wrong random username
-            chars = string.ascii_uppercase + string.digits
-            username = ''.join(random.choice(chars) for x in range(10))
-
-        if is_valid_password:
-            password = self.VALID_PASSWORD
-        else:
-            password = 'invalid-password'
-
-        headers = {
-            'user_agent': 'test-browser'
-        }
-        post_data = {
-            'username': username,
-            'password': password,
-            'this_is_the_login_form': 1,
-        }
-        post_data.update(kwargs)
-
-        if is_json:
-            headers.update({
-                'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
-                'content_type': 'application/json',
-            })
-            post_data = json.dumps(post_data)
-        response = self.client.post(admin_login, post_data, **headers)
-
-        return response
-
-    def setUp(self):
-        """Create a valid user for login
-        """
-        self.user = User.objects.create_superuser(
-            username=self.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
-        """
-        # test until one try before the 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.
-        # But we should get one now
-        response = self._login()
-        self.assertContains(response, self.LOCKED_MESSAGE, status_code=403)
-
-    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, status_code=403)
-
-    def test_valid_login(self):
-        """Tests a valid login for a real username
-        """
-        response = self._login(is_valid_username=True, is_valid_password=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_username=True, is_valid_password=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_username=True,
-            is_valid_password=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, status_code=403)
-
-    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)
-
-    @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
-    def test_lockout_by_combination_user_and_ip(self):
-        """Tests the login lock with a valid username and invalid password
-        when AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP is True
-        """
-        # test until one try before the limit
-        for i in range(1, FAILURE_LIMIT):
-            response = self._login(
-                is_valid_username=True,
-                is_valid_password=False,
-            )
-            # 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(is_valid_username=True, is_valid_password=False)
-        self.assertContains(response, self.LOCKED_MESSAGE, status_code=403)
-
-    def test_log_data_truncated(self):
-        """Tests that query2str properly truncates data to the max_length (default 1024)
-        """
-        # An impossibly large post dict
-        extra_data = {string.ascii_letters * x: x for x in range(0, 1000)}
-        self._login(**extra_data)
-        self.assertEquals(
-            len(AccessAttempt.objects.latest('id').post_data), 1024
-        )
-
-    def test_json_response(self):
-        """Tests response content type and status code for the ajax request
-        """
-        self.test_failure_limit_once()
-        response = self._login(is_json=True)
-        self.assertEquals(response.status_code, 403)
-        self.assertEquals(response.get('Content-Type'), 'application/json')
-
-
-class IPClassifierTest(TestCase):
-    def test_classify_private_ips(self):
-        """Tests whether is_valid_public_ip correctly classifies IPs as being
-        bot public and valid
-        """
-        EXPECTED = {
-            'foobar': False,  # invalid - not octects
-            '192.168.0': False,  # invalid - only 3 octets
-            '192.168.0.0': False,  # private
-            '192.168.165.1': False,  # private
-            '192.249.19.1': True,  # public but 192 prefix
-            '10.0.201.13': False,  # private
-            '172.15.12.1': True,  # public but 172 prefix
-            '172.16.12.1': False,  # private
-            '172.31.12.1': False,  # private
-            '172.32.0.1': True,  # public but 127 prefix
-            '200.150.23.5': True,  # normal public
-        }
-        for ip_address, is_valid_public in six.iteritems(EXPECTED):
-            self.assertEqual(is_valid_ip(ip_address), is_valid_public)
-
-
-class UtilsTest(TestCase):
-    def test_iso8601(self):
-        """Tests iso8601 correctly translates datetime.timdelta to ISO 8601
-        formatted duration."""
-        EXPECTED = {
-            datetime.timedelta(days=1, hours=25, minutes=42, seconds=8):
-                'P2DT1H42M8S',
-            datetime.timedelta(days=7, seconds=342):
-                'P7DT5M42S',
-            datetime.timedelta(days=0, hours=2, minutes=42):
-                'PT2H42M',
-            datetime.timedelta(hours=20, seconds=42):
-                'PT20H42S',
-            datetime.timedelta(seconds=300):
-                'PT5M',
-            datetime.timedelta(seconds=9005):
-                'PT2H30M5S',
-            datetime.timedelta(minutes=9005):
-                'P6DT6H5M',
-            datetime.timedelta(days=15):
-                'P15D'
-        }
-        for timedelta, iso_duration in six.iteritems(EXPECTED):
-            self.assertEqual(iso8601(timedelta), iso_duration)

+ 0 - 41
desktop/core/ext-py/django-axes-2.2.0/axes/utils.py

@@ -1,41 +0,0 @@
-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
-
-
-def iso8601(timestamp):
-    """Returns datetime.timedelta translated to ISO 8601 formatted duration.
-    """
-    seconds = timestamp.total_seconds()
-    minutes, seconds = divmod(seconds, 60)
-    hours, minutes = divmod(minutes, 60)
-    days, hours = divmod(hours, 24)
-
-    date = '{:.0f}D'.format(days) if days else ''
-
-    time_values = hours, minutes, seconds
-    time_designators = 'H', 'M', 'S'
-
-    time = ''.join([
-        ('{:.0f}'.format(value) + designator)
-        for value, designator in zip(time_values, time_designators)
-        if value]
-    )
-    return u'P' + date + (u'T' + time if time else '')

+ 0 - 15
desktop/core/ext-py/django-axes-2.2.0/runtests.py

@@ -1,15 +0,0 @@
-#!/usr/bin/env python
-import os
-import sys
-
-import django
-from django.conf import settings
-from django.test.utils import get_runner
-
-if __name__ == "__main__":
-    os.environ['DJANGO_SETTINGS_MODULE'] = 'axes.test_settings'
-    django.setup()
-    TestRunner = get_runner(settings)
-    test_runner = TestRunner()
-    failures = test_runner.run_tests(["axes"])
-    sys.exit(bool(failures))

+ 42 - 0
desktop/core/ext-py/django-axes-4.5.4/.travis.yml

@@ -0,0 +1,42 @@
+dist: xenial
+language: python
+cache: pip
+python:
+  - 2.7
+  - 3.4
+  - 3.5
+  - 3.6
+  - 3.7
+env:
+  - DJANGO=1.11
+  - DJANGO=2.0
+  - DJANGO=2.1
+  - DJANGO=master
+matrix:
+  exclude:
+    - python: 2.7
+      env: DJANGO=2.0
+    - python: 2.7
+      env: DJANGO=2.1
+    - python: 2.7
+      env: DJANGO=master
+    - python: 3.4
+      env: DJANGO=2.1
+    - python: 3.4
+      env: DJANGO=master
+install: pip install tox-travis codecov
+script: tox
+after_success:
+  - codecov
+deploy:
+  provider: pypi
+  user: jazzband
+  password:
+    secure: TCH5tGIggL2wsWce2svMwpEpPiwVOYqq1R3uSBTexszleP0OafNq/wZk2KZEReR5w1Aq68qp5F5Eeh2ZjJTq4f9M4LtTvqQzrmyNP55DYk/uB1rBJm9b4gBgMtAknxdI2g7unkhQEDo4suuPCVofM7rrDughySNpmvlUQYDttHQ=
+  server: https://jazzband.co/projects/django-axes/upload
+  distributions: sdist bdist_wheel
+  skip_existing: true
+  on:
+    tags: true
+    repo: jazzband/django-axes
+    python: 3.6

+ 315 - 0
desktop/core/ext-py/django-axes-2.2.0/CHANGES.txt → desktop/core/ext-py/django-axes-4.5.4/CHANGES.txt

@@ -1,6 +1,321 @@
 Changes
 =======
 
+4.5.4 (2019-01-15)
+------------------
+
+- Improve README and documentation
+  [aleksihakli]
+
+4.5.3 (2019-01-14)
+------------------
+
+- Remove the unused ``AccessAttempt.trusted`` flag from models
+  [aleksihakli]
+
+- Improve README and Travis CI setups
+  [aleksihakli]
+
+
+4.5.2 (2019-01-12)
+------------------
+
+- Added Turkish translations
+  [obayhan]
+
+
+4.5.1 (2019-01-11)
+------------------
+
+- Removed duplicated check that was causing issues when using APIs.
+  [camilonova]
+
+- Added Russian translations
+  [lubicz-sielski]
+
+
+4.5.0 (2018-12-25)
+------------------
+
+- Improve support for custom authentication credentials using the
+  ``AXES_USERNAME_FORM_FIELD`` and ``AXES_USERNAME_CALLABLE`` settings.
+  [mastacheata]
+
+- Updated behaviour for fetching username from request or credentials:
+  If no ``AXES_USERNAME_CALLABLE`` is configured, the optional
+  ``credentials`` that are supplied to the axes utility methods
+  are now the default source for client username and the HTTP
+  request POST is the fallback for fetching the user information.
+  ``AXES_USERNAME_CALLABLE`` implements an alternative signature with two
+  arguments ``request, credentials`` in addition to the old ``request``
+  call argument signature in a backwards compatible fashion.
+  [aleksihakli]
+
+- Add official support for the Django 2.1 LTS version and Python 3.7.
+  [aleksihakli]
+
+- Improve the requirements, documentation, tests, and CI setup.
+  [aleksihakli]
+
+
+4.4.3 (2018-12-08)
+------------------
+
+- Fix MANIFEST.in missing German translations
+  [aleksihakli]
+
+- Add `AXES_RESET_ON_SUCCESS` configuration flag
+  [arjenzijlstra]
+
+
+4.4.2 (2018-10-30)
+------------------
+
+- fix missing migration and add check to prevent it happening again.
+  [markddavidoff]
+
+
+4.4.1 (2018-10-24)
+------------------
+
+- Add a German translation
+  [adonig]
+
+- Documentation wording changes
+  [markddavidoff]
+
+- Use `get_client_username` in `log_user_login_failed` instead of credentials
+  [markddavidoff]
+
+- pin prospector to 0.12.11, and pin astroid to 1.6.5
+  [hsiaoyi0504]
+
+
+4.4.0 (2018-05-26)
+------------------
+
+- Added AXES_USERNAME_CALLABLE
+  [jaadus]
+
+
+4.3.1 (2018-04-21)
+------------------
+
+- Change custom authentication backend failures from error to warning log level
+  [aleksihakli]
+
+- Set up strict code linting for CI pipeline that fails builds if linting does not pass
+  [aleksihakli]
+
+- Clean up old code base and tests based on linter errors
+  [aleksihakli]
+
+
+4.3.0 (2018-04-21)
+------------------
+
+- Refactor and clean up code layout
+  [aleksihakli]
+
+- Add prospector linting and code checks to toolchain
+  [aleksihakli]
+
+- Clean up log message formatting and refactor type checks
+  [EvaSDK]
+
+- Fix faulty user locking with user agent when AXES_ONLY_USER_FAILURES is set
+  [EvaSDK]
+
+
+4.2.1 (2018-04-18)
+------------------
+
+- Fix unicode string interpolation on Python 2.7
+  [aleksihakli]
+
+
+4.2.0 (2018-04-13)
+------------------
+
+- Add configuration flags for client IP resolving
+  [aleksihakli]
+
+- Add AxesModelBackend authentication backend
+  [markdaviddoff]
+
+
+4.1.0 (2018-02-18)
+------------------
+
+- Add AXES_CACHE setting for configuring `axes` specific caching.
+  [JWvDronkelaar]
+
+- Add checks and tests for faulty LocMemCache usage in application setup.
+  [aleksihakli]
+
+
+4.0.2 (2018-01-19)
+------------------
+
+- Improve Windows compatibility on Python < 3.4 by utilizing win_inet_pton
+  [hsiaoyi0504]
+
+- Add documentation on django-allauth integration
+  [grucha]
+
+- Add documentation on known AccessAttempt caching configuration problems
+  when using axes with the `django.core.cache.backends.locmem.LocMemCache`
+  [aleksihakli]
+
+- Refactor and improve existing AccessAttempt cache reset utility
+  [aleksihakli]
+
+
+4.0.1 (2017-12-19)
+------------------
+
+- Fixes issue when not using `AXES_USERNAME_FORM_FIELD`
+  [camilonova]
+
+
+4.0.0 (2017-12-18)
+------------------
+
+- *BREAKING CHANGES*. `AXES_BEHIND_REVERSE_PROXY` `AXES_REVERSE_PROXY_HEADER`
+  `AXES_NUM_PROXIES` were removed in order to use `django-ipware` to get
+  the user ip address
+  [camilonova]
+
+- Added support for custom username field
+  [kakulukia]
+
+- Customizing Axes doc updated
+  [pckapps]
+
+- Remove filtering by username
+  [camilonova]
+
+- Fixed logging failed attempts to authenticate using a custom authentication
+  backend.
+  [D3X]
+
+
+3.0.3 (2017-11-23)
+------------------
+
+- Test against Python 2.7.
+  [mbaechtold]
+
+- Test against Python 3.4.
+  [pope1ni]
+
+
+3.0.2 (2017-11-21)
+------------------
+
+- Added form_invalid decorator. Fixes #265
+  [camilonova]
+
+
+3.0.1 (2017-11-17)
+------------------
+
+- Fix DeprecationWarning for logger warning
+  [richardowen]
+
+- Fixes global lockout possibility
+  [joeribekker]
+
+- Changed the way output is handled in the management commands
+  [ataylor32]
+
+
+3.0.0 (2017-11-17)
+------------------
+
+- BREAKING CHANGES. Support for Django >= 1.11 and signals, see issue #215.
+  Drop support for Python < 3.6
+  [camilonova]
+
+
+2.3.3 (2017-07-20)
+------------------
+
+- Many tweaks and handles successful AJAX logins.
+  [Jack Sullivan]
+
+- Add tests for proxy number parametrization
+  [aleksihakli]
+
+- Add AXES_NUM_PROXIES setting
+  [aleksihakli]
+
+- Log failed access attempts regardless of settings
+  [jimr]
+
+- Updated configuration docs to include AXES_IP_WHITELIST
+  [Minkey27]
+
+- Add test for get_cache_key function
+  [jorlugaqui]
+
+- Delete cache key in reset command line
+  [jorlugaqui]
+
+- Add signals for setting/deleting cache keys
+  [jorlugaqui]
+
+
+2.3.2 (2016-11-24)
+------------------
+
+- Only look for lockable users on a POST
+  [schinckel]
+
+- Fix and add tests for IPv4 and IPv6 parsing
+  [aleksihakli]
+
+
+2.3.1 (2016-11-12)
+------------------
+
+- Added settings for disabling success accesslogs
+  [Minkey27]
+
+- Fixed illegal IP address string passed to inet_pton
+  [samkuehn]
+
+
+2.3.0 (2016-11-04)
+------------------
+
+- Fixed ``axes_reset`` management command to skip "ip" prefix to command
+  arguments.
+  [EvaMarques]
+
+- Added ``axes_reset_user`` management command to reset lockouts and failed
+  login records for given users.
+  [vladimirnani]
+
+- Fixed Travis-PyPI release configuration.
+  [jezdez]
+
+- Make IP position argument optional.
+  [aredalen]
+
+- Added possibility to disable access log
+  [svenhertle]
+
+- Fix for IIS used as reverse proxy adding port number
+  [Dmitri-Sintsov]
+
+- Made the signal race condition safe.
+  [Minkey27]
+
+- Added AXES_ONLY_USER_FAILURES to support only looking at the user ID.
+  [lip77us]
+
+
 2.2.0 (2016-07-20)
 ------------------
 

+ 0 - 0
desktop/core/ext-py/django-axes-2.2.0/LICENSE → desktop/core/ext-py/django-axes-4.5.4/LICENSE


+ 1 - 1
desktop/core/ext-py/django-axes-2.2.0/MANIFEST.in → desktop/core/ext-py/django-axes-4.5.4/MANIFEST.in

@@ -1,4 +1,4 @@
 include LICENSE README.rst CHANGES.txt
 recursive-include axes *.py
+recursive-include axes/locale *.mo *.po
 include .travis.yml
-include Makefile

+ 345 - 167
desktop/core/ext-py/django-axes-2.2.0/PKG-INFO → desktop/core/ext-py/django-axes-4.5.4/PKG-INFO

@@ -1,17 +1,40 @@
-Metadata-Version: 1.1
+Metadata-Version: 1.2
 Name: django-axes
-Version: 2.2.0
+Version: 4.5.4
 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
+Home-page: https://github.com/jazzband/django-axes
+Author: Josh VanderLinden, Philip Neustrom, Michael Blume, Camilo Nova
+Author-email: codekoala@gmail.com
+Maintainer: Alex Clark
+Maintainer-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
+        .. image:: https://jazzband.co/static/img/badge.svg
+           :target: https://jazzband.co/
+           :alt: Jazzband
+        
+        .. image:: https://img.shields.io/pypi/v/django-axes.svg
+           :target: https://pypi.org/project/django-axes/
+           :alt: PyPI release
+        
+        .. image:: https://img.shields.io/readthedocs/django-axes.svg
+           :target: https://django-axes.readthedocs.io/
+           :alt: Documentation
+        
+        .. image:: https://secure.travis-ci.org/jazzband/django-axes.svg?branch=master
+           :target: http://travis-ci.org/jazzband/django-axes
+           :alt: Build Status
+        
+        .. image:: https://codecov.io/gh/jazzband/django-axes/branch/master/graph/badge.svg
+           :target: https://codecov.io/gh/jazzband/django-axes
+           :alt: Coverage
+        
+        .. image:: https://img.shields.io/github/stars/jazzband/django-axes.svg?label=Stars&style=socialcA
+           :target: https://github.com/jazzband/django-axes
+           :alt: GitHub
+        
         
         ``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
@@ -24,203 +47,350 @@ Description: Django Axes
           definition) your website. Hilarious, right? That's what I thought too!
         
         
-        Requirements
-        ============
+        Documentation
+        -------------
         
-        ``django-axes`` requires a supported Django version. The application is
-        intended to work around the Django admin and the regular
-        ``django.contrib.auth`` login-powered pages.
-        Look at https://www.djangoproject.com/download/ to check if your version
-        is supported.
+        For more information see the documentation at:
         
+        https://django-axes.readthedocs.io/
         
-        Installation
-        ============
         
-        You can install the latest stable package running this command::
+        Issues
+        ------
         
-            $ pip install django-axes
+        If you have questions or have trouble using the app please file a bug report at:
         
+        https://github.com/jazzband/django-axes/issues
         
-        Development
-        ===========
         
-        You can contribute to this project forking it from github and sending pull requests.
+        Contributing
+        ------------
         
-        Running tests
-        -------------
+        Separate proposed changes and PRs into small, distinct patches by type
+        so that they can be merged faster into upstream and released quicker:
         
-        Clone the repository and install the django version you want. Then run::
-        
-            $ ./runtests.py
-        
-        
-        Configuration
-        =============
-        
-        Just add `axes` to your ``INSTALLED_APPS``::
-        
-            INSTALLED_APPS = (
-                'django.contrib.admin',
-                'django.contrib.auth',
-                'django.contrib.contenttypes',
-                'django.contrib.sessions',
-                'django.contrib.sites',
-                ...
-                'axes',
-                ...
-            )
-        
-        Remember to run ``python manage.py migrate`` to sync the database.
-        
-        
-        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``
-        * ``AXES_NEVER_LOCKOUT_WHITELIST``: If ``True``, users can always login from
-          whitelisted IP addresses.
-          Default: ``False``
-        * ``AXES_BEHIND_REVERSE_PROXY``: If ``True``, it will look for the IP address from the header defined at ``AXES_REVERSE_PROXY_HEADER``. Please make sure if you enable this setting to configure your proxy to set the correct value for the header, otherwise you could be attacked by setting this header directly in every request.
-          Default: ``False``
-        * ``AXES_REVERSE_PROXY_HEADER``: If ``AXES_BEHIND_REVERSE_PROXY`` is ``True``, it will look for the IP address from this header.
-          Default: ``HTTP_X_FORWARDED_FOR``
-        
-        
-        Usage
-        =====
-        
-        Using ``django-axes`` is extremely simple. 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 a username
+        * Feature
+        * Bugfix
+        * Code style
+        * Documentation
         
+        Changes
+        =======
         
-        Issues
-        ======
+        4.5.4 (2019-01-15)
+        ------------------
+        
+        - Improve README and documentation
+          [aleksihakli]
+        
+        4.5.3 (2019-01-14)
+        ------------------
         
-        Not being locked out after failed attempts
-        ------------------------------------------
+        - Remove the unused ``AccessAttempt.trusted`` flag from models
+          [aleksihakli]
         
-        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.
+        - Improve README and Travis CI setups
+          [aleksihakli]
         
-        For example, in your urls.py::
         
-            ...
-            from my.custom.app import login
-            from axes.decorators import watch_login
-            ...
-            urlpatterns = patterns('',
-                (r'^login/$', watch_login(login)),
-            ...
+        4.5.2 (2019-01-12)
+        ------------------
         
+        - Added Turkish translations
+          [obayhan]
         
-        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:
+        4.5.1 (2019-01-11)
+        ------------------
         
-            AXES_USERNAME_FORM_FIELD = "email"
+        - Removed duplicated check that was causing issues when using APIs.
+          [camilonova]
         
+        - Added Russian translations
+          [lubicz-sielski]
         
-        Using a captcha
-        ===============
         
-        Using https://github.com/mbi/django-simple-captcha you do the following:
+        4.5.0 (2018-12-25)
+        ------------------
         
-        1. Change axes lockout url in ``settings.py``::
+        - Improve support for custom authentication credentials using the
+          ``AXES_USERNAME_FORM_FIELD`` and ``AXES_USERNAME_CALLABLE`` settings.
+          [mastacheata]
         
-            AXES_LOCKOUT_URL = '/locked'
+        - Updated behaviour for fetching username from request or credentials:
+          If no ``AXES_USERNAME_CALLABLE`` is configured, the optional
+          ``credentials`` that are supplied to the axes utility methods
+          are now the default source for client username and the HTTP
+          request POST is the fallback for fetching the user information.
+          ``AXES_USERNAME_CALLABLE`` implements an alternative signature with two
+          arguments ``request, credentials`` in addition to the old ``request``
+          call argument signature in a backwards compatible fashion.
+          [aleksihakli]
         
-        2. Add the url in ``urls.py``::
+        - Add official support for the Django 2.1 LTS version and Python 3.7.
+          [aleksihakli]
         
-            url(r'^locked/$', locked_out, name='locked_out'),
+        - Improve the requirements, documentation, tests, and CI setup.
+          [aleksihakli]
         
-        3. Create a captcha form::
         
-            class AxesCaptchaForm(forms.Form):
-                captcha = CaptchaField()
+        4.4.3 (2018-12-08)
+        ------------------
         
-        4. Create a captcha view for the above url that resets on captcha success and redirects::
+        - Fix MANIFEST.in missing German translations
+          [aleksihakli]
         
-            def locked_out(request):
-                if request.POST:
-                    form = AxesCaptchaForm(request.POST)
-                    if form.is_valid():
-                        ip = get_ip_address_from_request(request)
-                        reset(ip=ip)
-                        return HttpResponseRedirect(reverse_lazy('signin'))
-                else:
-                    form = AxesCaptchaForm()
+        - Add `AXES_RESET_ON_SUCCESS` configuration flag
+          [arjenzijlstra]
         
-                return render_to_response('locked_out.html', dict(form=form), context_instance=RequestContext(request))
         
-        5. Add a captcha template::
+        4.4.2 (2018-10-30)
+        ------------------
         
-            <form action="" method="post">
-                {% csrf_token %}
+        - fix missing migration and add check to prevent it happening again.
+          [markddavidoff]
         
-                {{ form.captcha.errors }}
-                {{ form.captcha }}
         
-                <div class="form-actions">
-                    <input type="submit" value="Submit" />
-                </div>
-            </form>
+        4.4.1 (2018-10-24)
+        ------------------
         
-        Done.
+        - Add a German translation
+          [adonig]
+        
+        - Documentation wording changes
+          [markddavidoff]
+        
+        - Use `get_client_username` in `log_user_login_failed` instead of credentials
+          [markddavidoff]
+        
+        - pin prospector to 0.12.11, and pin astroid to 1.6.5
+          [hsiaoyi0504]
+        
+        
+        4.4.0 (2018-05-26)
+        ------------------
+        
+        - Added AXES_USERNAME_CALLABLE
+          [jaadus]
+        
+        
+        4.3.1 (2018-04-21)
+        ------------------
+        
+        - Change custom authentication backend failures from error to warning log level
+          [aleksihakli]
+        
+        - Set up strict code linting for CI pipeline that fails builds if linting does not pass
+          [aleksihakli]
+        
+        - Clean up old code base and tests based on linter errors
+          [aleksihakli]
+        
+        
+        4.3.0 (2018-04-21)
+        ------------------
+        
+        - Refactor and clean up code layout
+          [aleksihakli]
+        
+        - Add prospector linting and code checks to toolchain
+          [aleksihakli]
+        
+        - Clean up log message formatting and refactor type checks
+          [EvaSDK]
+        
+        - Fix faulty user locking with user agent when AXES_ONLY_USER_FAILURES is set
+          [EvaSDK]
+        
+        
+        4.2.1 (2018-04-18)
+        ------------------
+        
+        - Fix unicode string interpolation on Python 2.7
+          [aleksihakli]
+        
+        
+        4.2.0 (2018-04-13)
+        ------------------
+        
+        - Add configuration flags for client IP resolving
+          [aleksihakli]
+        
+        - Add AxesModelBackend authentication backend
+          [markdaviddoff]
+        
+        
+        4.1.0 (2018-02-18)
+        ------------------
+        
+        - Add AXES_CACHE setting for configuring `axes` specific caching.
+          [JWvDronkelaar]
+        
+        - Add checks and tests for faulty LocMemCache usage in application setup.
+          [aleksihakli]
+        
+        
+        4.0.2 (2018-01-19)
+        ------------------
+        
+        - Improve Windows compatibility on Python < 3.4 by utilizing win_inet_pton
+          [hsiaoyi0504]
+        
+        - Add documentation on django-allauth integration
+          [grucha]
+        
+        - Add documentation on known AccessAttempt caching configuration problems
+          when using axes with the `django.core.cache.backends.locmem.LocMemCache`
+          [aleksihakli]
+        
+        - Refactor and improve existing AccessAttempt cache reset utility
+          [aleksihakli]
+        
+        
+        4.0.1 (2017-12-19)
+        ------------------
+        
+        - Fixes issue when not using `AXES_USERNAME_FORM_FIELD`
+          [camilonova]
+        
+        
+        4.0.0 (2017-12-18)
+        ------------------
+        
+        - *BREAKING CHANGES*. `AXES_BEHIND_REVERSE_PROXY` `AXES_REVERSE_PROXY_HEADER`
+          `AXES_NUM_PROXIES` were removed in order to use `django-ipware` to get
+          the user ip address
+          [camilonova]
+        
+        - Added support for custom username field
+          [kakulukia]
+        
+        - Customizing Axes doc updated
+          [pckapps]
+        
+        - Remove filtering by username
+          [camilonova]
+        
+        - Fixed logging failed attempts to authenticate using a custom authentication
+          backend.
+          [D3X]
+        
+        
+        3.0.3 (2017-11-23)
+        ------------------
+        
+        - Test against Python 2.7.
+          [mbaechtold]
+        
+        - Test against Python 3.4.
+          [pope1ni]
+        
+        
+        3.0.2 (2017-11-21)
+        ------------------
+        
+        - Added form_invalid decorator. Fixes #265
+          [camilonova]
+        
+        
+        3.0.1 (2017-11-17)
+        ------------------
+        
+        - Fix DeprecationWarning for logger warning
+          [richardowen]
+        
+        - Fixes global lockout possibility
+          [joeribekker]
+        
+        - Changed the way output is handled in the management commands
+          [ataylor32]
+        
+        
+        3.0.0 (2017-11-17)
+        ------------------
+        
+        - BREAKING CHANGES. Support for Django >= 1.11 and signals, see issue #215.
+          Drop support for Python < 3.6
+          [camilonova]
+        
+        
+        2.3.3 (2017-07-20)
+        ------------------
+        
+        - Many tweaks and handles successful AJAX logins.
+          [Jack Sullivan]
+        
+        - Add tests for proxy number parametrization
+          [aleksihakli]
+        
+        - Add AXES_NUM_PROXIES setting
+          [aleksihakli]
+        
+        - Log failed access attempts regardless of settings
+          [jimr]
+        
+        - Updated configuration docs to include AXES_IP_WHITELIST
+          [Minkey27]
+        
+        - Add test for get_cache_key function
+          [jorlugaqui]
+        
+        - Delete cache key in reset command line
+          [jorlugaqui]
+        
+        - Add signals for setting/deleting cache keys
+          [jorlugaqui]
+        
+        
+        2.3.2 (2016-11-24)
+        ------------------
+        
+        - Only look for lockable users on a POST
+          [schinckel]
+        
+        - Fix and add tests for IPv4 and IPv6 parsing
+          [aleksihakli]
+        
+        
+        2.3.1 (2016-11-12)
+        ------------------
+        
+        - Added settings for disabling success accesslogs
+          [Minkey27]
+        
+        - Fixed illegal IP address string passed to inet_pton
+          [samkuehn]
+        
+        
+        2.3.0 (2016-11-04)
+        ------------------
+        
+        - Fixed ``axes_reset`` management command to skip "ip" prefix to command
+          arguments.
+          [EvaMarques]
+        
+        - Added ``axes_reset_user`` management command to reset lockouts and failed
+          login records for given users.
+          [vladimirnani]
+        
+        - Fixed Travis-PyPI release configuration.
+          [jezdez]
+        
+        - Make IP position argument optional.
+          [aredalen]
+        
+        - Added possibility to disable access log
+          [svenhertle]
+        
+        - Fix for IIS used as reverse proxy adding port number
+          [Dmitri-Sintsov]
+        
+        - Made the signal race condition safe.
+          [Minkey27]
+        
+        - Added AXES_ONLY_USER_FAILURES to support only looking at the user ID.
+          [lip77us]
         
-        Changes
-        =======
         
         2.2.0 (2016-07-20)
         ------------------
@@ -556,13 +726,21 @@ Platform: UNKNOWN
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Environment :: Web Environment
 Classifier: Framework :: Django
+Classifier: Framework :: Django :: 1.11
+Classifier: Framework :: Django :: 2.0
+Classifier: Framework :: Django :: 2.1
 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
 Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
 Classifier: Topic :: Internet :: Log Analysis
 Classifier: Topic :: Security
 Classifier: Topic :: System :: Logging

+ 65 - 0
desktop/core/ext-py/django-axes-4.5.4/README.rst

@@ -0,0 +1,65 @@
+Django Axes
+===========
+
+.. image:: https://jazzband.co/static/img/badge.svg
+   :target: https://jazzband.co/
+   :alt: Jazzband
+
+.. image:: https://img.shields.io/pypi/v/django-axes.svg
+   :target: https://pypi.org/project/django-axes/
+   :alt: PyPI release
+
+.. image:: https://img.shields.io/readthedocs/django-axes.svg
+   :target: https://django-axes.readthedocs.io/
+   :alt: Documentation
+
+.. image:: https://secure.travis-ci.org/jazzband/django-axes.svg?branch=master
+   :target: http://travis-ci.org/jazzband/django-axes
+   :alt: Build Status
+
+.. image:: https://codecov.io/gh/jazzband/django-axes/branch/master/graph/badge.svg
+   :target: https://codecov.io/gh/jazzband/django-axes
+   :alt: Coverage
+
+.. image:: https://img.shields.io/github/stars/jazzband/django-axes.svg?label=Stars&style=socialcA
+   :target: https://github.com/jazzband/django-axes
+   :alt: GitHub
+
+
+``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!
+
+
+Documentation
+-------------
+
+For more information see the documentation at:
+
+https://django-axes.readthedocs.io/
+
+
+Issues
+------
+
+If you have questions or have trouble using the app please file a bug report at:
+
+https://github.com/jazzband/django-axes/issues
+
+
+Contributing
+------------
+
+Separate proposed changes and PRs into small, distinct patches by type
+so that they can be merged faster into upstream and released quicker:
+
+* Feature
+* Bugfix
+* Code style
+* Documentation

+ 3 - 1
desktop/core/ext-py/django-axes-2.2.0/axes/__init__.py → desktop/core/ext-py/django-axes-4.5.4/axes/__init__.py

@@ -1,4 +1,6 @@
-__version__ = '2.2.0'
+from __future__ import unicode_literals
+
+__version__ = '4.5.4'
 
 default_app_config = 'axes.apps.AppConfig'
 

+ 37 - 5
desktop/core/ext-py/django-axes-2.2.0/axes/admin.py → desktop/core/ext-py/django-axes-4.5.4/axes/admin.py

@@ -1,4 +1,7 @@
+from __future__ import unicode_literals
+
 from django.contrib import admin
+from django.utils.translation import gettext_lazy as _
 
 from axes.models import AccessLog
 from axes.models import AccessAttempt
@@ -16,7 +19,6 @@ class AccessAttemptAdmin(admin.ModelAdmin):
 
     list_filter = [
         'attempt_time',
-        'username',
         'path_info',
     ]
 
@@ -33,14 +35,30 @@ class AccessAttemptAdmin(admin.ModelAdmin):
         (None, {
             'fields': ('path_info', 'failures_since_start')
         }),
-        ('Form Data', {
+        (_('Form Data'), {
             'fields': ('get_data', 'post_data')
         }),
-        ('Meta Data', {
+        (_('Meta Data'), {
             'fields': ('user_agent', 'ip_address', 'http_accept')
         })
     )
 
+    readonly_fields = [
+        'user_agent',
+        'ip_address',
+        'username',
+        'http_accept',
+        'path_info',
+        'attempt_time',
+        'get_data',
+        'post_data',
+        'failures_since_start'
+    ]
+
+    def has_add_permission(self, request):
+        return False
+
+
 admin.site.register(AccessAttempt, AccessAttemptAdmin)
 
 
@@ -57,7 +75,6 @@ class AccessLogAdmin(admin.ModelAdmin):
     list_filter = [
         'attempt_time',
         'logout_time',
-        'username',
         'path_info',
     ]
 
@@ -74,9 +91,24 @@ class AccessLogAdmin(admin.ModelAdmin):
         (None, {
             'fields': ('path_info',)
         }),
-        ('Meta Data', {
+        (_('Meta Data'), {
             'fields': ('user_agent', 'ip_address', 'http_accept')
         })
     )
 
+    readonly_fields = [
+        'user_agent',
+        'ip_address',
+        'username',
+        'trusted',
+        'http_accept',
+        'path_info',
+        'attempt_time',
+        'logout_time'
+    ]
+
+    def has_add_permission(self, request):
+        return False
+
+
 admin.site.register(AccessLog, AccessLogAdmin)

+ 29 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/apps.py

@@ -0,0 +1,29 @@
+from __future__ import unicode_literals
+
+from django import apps
+
+
+class AppConfig(apps.AppConfig):
+    name = 'axes'
+
+    def ready(self):
+        from django.conf import settings
+        from django.core.exceptions import ImproperlyConfigured
+
+        if settings.CACHES[getattr(settings, 'AXES_CACHE', 'default')]['BACKEND'] == \
+                'django.core.cache.backends.locmem.LocMemCache':
+            raise ImproperlyConfigured(
+                'django-axes does not work properly with LocMemCache as the default cache backend'
+                ' please add e.g. a DummyCache backend for axes and configure it with AXES_CACHE'
+            )
+
+        from django.contrib.auth.views import LoginView
+        from django.utils.decorators import method_decorator
+
+        from axes import signals  # pylint: disable=unused-import,unused-variable
+
+        from axes.decorators import axes_dispatch
+        from axes.decorators import axes_form_invalid
+
+        LoginView.dispatch = method_decorator(axes_dispatch)(LoginView.dispatch)
+        LoginView.form_invalid = method_decorator(axes_form_invalid)(LoginView.form_invalid)

+ 213 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/attempts.py

@@ -0,0 +1,213 @@
+from __future__ import unicode_literals
+
+from datetime import timedelta
+from hashlib import md5
+
+from django.contrib.auth import get_user_model
+from django.utils import timezone
+
+from axes.conf import settings
+from axes.models import AccessAttempt
+from axes.utils import get_axes_cache, get_client_ip, get_client_username
+
+
+def _query_user_attempts(request, credentials=None):
+    """Returns access attempt record if it exists.
+    Otherwise return None.
+    """
+    ip = get_client_ip(request)
+    username = get_client_username(request, credentials)
+
+    if settings.AXES_ONLY_USER_FAILURES:
+        attempts = AccessAttempt.objects.filter(username=username)
+    elif settings.AXES_USE_USER_AGENT:
+        ua = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
+        attempts = AccessAttempt.objects.filter(
+            user_agent=ua, ip_address=ip, username=username
+        )
+    else:
+        attempts = AccessAttempt.objects.filter(
+            ip_address=ip, username=username
+        )
+
+    if not attempts:
+        params = {}
+
+        if settings.AXES_ONLY_USER_FAILURES:
+            params['username'] = username
+        elif settings.AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP:
+            params['username'] = username
+            params['ip_address'] = ip
+        else:
+            params['ip_address'] = ip
+
+        if settings.AXES_USE_USER_AGENT and not settings.AXES_ONLY_USER_FAILURES:
+            params['user_agent'] = ua
+
+        attempts = AccessAttempt.objects.filter(**params)
+
+    return attempts
+
+
+def get_cache_key(request_or_obj, credentials=None):
+    """
+    Build cache key name from request or AccessAttempt object.
+    :param  request_or_obj: Request or AccessAttempt object
+    :return cache-key: String, key to be used in cache system
+    """
+    if isinstance(request_or_obj, AccessAttempt):
+        ip = request_or_obj.ip_address
+        un = request_or_obj.username
+        ua = request_or_obj.user_agent
+    else:
+        ip = get_client_ip(request_or_obj)
+        un = get_client_username(request_or_obj, credentials)
+        ua = request_or_obj.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
+
+    ip = ip.encode('utf-8') if ip else ''.encode('utf-8')
+    un = un.encode('utf-8') if un else ''.encode('utf-8')
+    ua = ua.encode('utf-8') if ua else ''.encode('utf-8')
+
+    if settings.AXES_ONLY_USER_FAILURES:
+        attributes = un
+    elif settings.AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP:
+        attributes = ip + un
+    else:
+        attributes = ip
+
+    if settings.AXES_USE_USER_AGENT and not settings.AXES_ONLY_USER_FAILURES:
+        attributes += ua
+
+    cache_hash_key = 'axes-{}'.format(md5(attributes).hexdigest())
+
+    return cache_hash_key
+
+
+def get_cache_timeout():
+    """Returns timeout according to COOLOFF_TIME."""
+    cache_timeout = None
+    cool_off = settings.AXES_COOLOFF_TIME
+    if cool_off:
+        if isinstance(cool_off, (int, float)):
+            cache_timeout = timedelta(hours=cool_off).total_seconds()
+        else:
+            cache_timeout = cool_off.total_seconds()
+
+    return cache_timeout
+
+
+def get_user_attempts(request, credentials=None):
+    force_reload = False
+    attempts = _query_user_attempts(request, credentials)
+    cache_hash_key = get_cache_key(request, credentials)
+    cache_timeout = get_cache_timeout()
+
+    cool_off = settings.AXES_COOLOFF_TIME
+    if cool_off:
+        if isinstance(cool_off, (int, float)):
+            cool_off = timedelta(hours=cool_off)
+
+        for attempt in attempts:
+            if attempt.attempt_time + cool_off < timezone.now():
+                attempt.delete()
+                force_reload = True
+                failures_cached = get_axes_cache().get(cache_hash_key)
+                if failures_cached is not None:
+                    get_axes_cache().set(
+                        cache_hash_key, failures_cached - 1, cache_timeout
+                    )
+
+    # If objects were deleted, we need to update the queryset to reflect this,
+    # so force a reload.
+    if force_reload:
+        attempts = _query_user_attempts(request, credentials)
+
+    return attempts
+
+
+def reset_user_attempts(request, credentials=None):
+    attempts = _query_user_attempts(request, credentials)
+    count, _ = attempts.delete()
+
+    return count
+
+
+def ip_in_whitelist(ip):
+    if not settings.AXES_IP_WHITELIST:
+        return False
+
+    return ip in settings.AXES_IP_WHITELIST
+
+
+def ip_in_blacklist(ip):
+    if not settings.AXES_IP_BLACKLIST:
+        return False
+
+    return ip in settings.AXES_IP_BLACKLIST
+
+
+def is_user_lockable(request, credentials=None):
+    """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
+    """
+    if request.method != 'POST':
+        return True
+
+    try:
+        field = getattr(get_user_model(), 'USERNAME_FIELD', 'username')
+        kwargs = {
+            field: get_client_username(request, credentials)
+        }
+        user = get_user_model().objects.get(**kwargs)
+
+        if hasattr(user, 'nolockout'):
+            # need to invert since we need to return
+            # false for users that can't be blocked
+            return not user.nolockout
+
+    except get_user_model().DoesNotExist:
+        # not a valid user
+        return True
+
+    # Default behavior for a user to be lockable
+    return True
+
+
+def is_already_locked(request, credentials=None):
+    ip = get_client_ip(request)
+
+    if (
+        settings.AXES_ONLY_USER_FAILURES or
+        settings.AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP
+    ) and request.method == 'GET':
+        return False
+
+    if settings.AXES_NEVER_LOCKOUT_WHITELIST and ip_in_whitelist(ip):
+        return False
+
+    if settings.AXES_ONLY_WHITELIST and not ip_in_whitelist(ip):
+        return True
+
+    if ip_in_blacklist(ip):
+        return True
+
+    if not is_user_lockable(request, credentials):
+        return False
+
+    cache_hash_key = get_cache_key(request, credentials)
+    failures_cached = get_axes_cache().get(cache_hash_key)
+    if failures_cached is not None:
+        return (
+            failures_cached >= settings.AXES_FAILURE_LIMIT and
+            settings.AXES_LOCK_OUT_AT_FAILURE
+        )
+
+    for attempt in get_user_attempts(request, credentials):
+        if (
+            attempt.failures_since_start >= settings.AXES_FAILURE_LIMIT and
+            settings.AXES_LOCK_OUT_AT_FAILURE
+        ):
+            return True
+
+    return False

+ 49 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/backends.py

@@ -0,0 +1,49 @@
+from __future__ import unicode_literals
+
+from django.contrib.auth.backends import ModelBackend
+from django.core.exceptions import PermissionDenied
+
+from axes.attempts import is_already_locked
+from axes.utils import get_credentials, get_lockout_message
+
+
+class AxesModelBackend(ModelBackend):
+
+    class RequestParameterRequired(Exception):
+        msg = 'AxesModelBackend requires calls to authenticate to pass `request` as an argument.'
+
+        def __init__(self):
+            super(AxesModelBackend.RequestParameterRequired, self).__init__(
+                AxesModelBackend.RequestParameterRequired.msg)
+
+    def authenticate(self, request, username=None, password=None, **kwargs):
+        """Checks user lock out status and raises PermissionDenied if user is not allowed to log in.
+
+        Inserts errors directly to `return_context` that is supplied as a keyword argument.
+
+        Use this on top of your AUTHENTICATION_BACKENDS list to prevent locked out users
+        from being authenticated in the standard Django authentication flow.
+
+        Note that this method does not log your user in and delegates login to other backends.
+
+        :param request: see ModelBackend.authenticate
+        :param kwargs: see ModelBackend.authenticate
+        :keyword response_context: context dict that will be updated with error information
+        :raises PermissionDenied: if user is already locked out
+        :return: None
+        """
+
+        if request is None:
+            raise AxesModelBackend.RequestParameterRequired()
+
+        credentials = get_credentials(username=username, password=password, **kwargs)
+
+        if is_already_locked(request, credentials):
+            # locked out, don't try to authenticate, just update return_context and return
+            # Its a bit weird to pass a context and expect a response value but its nice to get a "why" back.
+            error_msg = get_lockout_message()
+            response_context = kwargs.get('response_context', {})
+            response_context['error'] = error_msg
+            raise PermissionDenied(error_msg)
+
+        # No-op

+ 84 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/conf.py

@@ -0,0 +1,84 @@
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.utils.translation import gettext_lazy as _
+
+from appconf import AppConf
+
+
+class MyAppConf(AppConf):
+    # see if the user has overridden the failure limit
+    FAILURE_LIMIT = 3
+
+    # see if the user has set axes to lock out logins after failure limit
+    LOCK_OUT_AT_FAILURE = True
+
+    USE_USER_AGENT = False
+
+    # use a specific username field to retrieve from login POST data
+    USERNAME_FORM_FIELD = 'username'
+
+    # use a specific password field to retrieve from login POST data
+    PASSWORD_FORM_FIELD = 'password'
+
+    # use a provided callable to transform the POSTed username into the one used in credentials
+    USERNAME_CALLABLE = None
+
+    # only check user name and not location or user_agent
+    ONLY_USER_FAILURES = False
+
+    # reset the number of failed attempts after one successful attempt
+    RESET_ON_SUCCESS = False
+
+    # lock out user from particular IP based on combination USER+IP
+    LOCK_OUT_BY_COMBINATION_USER_AND_IP = False
+
+    DISABLE_ACCESS_LOG = False
+
+    DISABLE_SUCCESS_ACCESS_LOG = False
+
+    LOGGER = 'axes.watch_login'
+
+    LOCKOUT_TEMPLATE = None
+
+    LOCKOUT_URL = None
+
+    COOLOFF_TIME = None
+
+    VERBOSE = True
+
+    # whitelist and blacklist
+    # TODO: convert the strings to IPv4 on startup to avoid type conversion during processing
+    NEVER_LOCKOUT_WHITELIST = False
+
+    ONLY_WHITELIST = False
+
+    IP_WHITELIST = None
+
+    IP_BLACKLIST = None
+
+    # message to show when locked out and have cooloff enabled
+    COOLOFF_MESSAGE = _('Account locked: too many login attempts. Please try again later')
+
+    # message to show when locked out and have cooloff disabled
+    PERMALOCK_MESSAGE = _('Account locked: too many login attempts. Contact an admin to unlock your account.')
+
+    # if your deployment is using reverse proxies, set this value to 'left-most' or 'right-most' per your configuration
+    PROXY_ORDER = 'left-most'
+
+    # if your deployment is using reverse proxies, set this value to the number of proxies in front of Django
+    PROXY_COUNT = None
+
+    # if your deployment is using reverse proxies, set to your trusted proxy IP addresses prefixes if needed
+    PROXY_TRUSTED_IPS = None
+
+    # set to the names of request.META attributes that should be checked for the IP address of the client
+    # if your deployment is using reverse proxies, ensure that the header attributes are securely set by the proxy
+    # ensure that the client can not spoof the headers by setting them and sending them through the proxy
+    META_PRECEDENCE_ORDER = getattr(
+        settings, 'AXES_META_PRECEDENCE_ORDER', getattr(
+            settings, 'IPWARE_META_PRECEDENCE_ORDER', (
+                'REMOTE_ADDR',
+            )
+        )
+    )

+ 80 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/decorators.py

@@ -0,0 +1,80 @@
+from __future__ import unicode_literals
+
+from datetime import timedelta
+from functools import wraps
+import json
+import logging
+
+from django.http import HttpResponse
+from django.http import HttpResponseRedirect
+from django.shortcuts import render
+
+from axes import get_version
+from axes.conf import settings
+from axes.attempts import is_already_locked
+from axes.utils import iso8601, get_client_username, get_lockout_message
+
+log = logging.getLogger(settings.AXES_LOGGER)
+if settings.AXES_VERBOSE:
+    log.info('AXES: BEGIN LOG')
+    log.info('AXES: Using django-axes %s', get_version())
+    if settings.AXES_ONLY_USER_FAILURES:
+        log.info('AXES: blocking by username only.')
+    elif settings.AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP:
+        log.info('AXES: blocking by combination of username and IP.')
+    else:
+        log.info('AXES: blocking by IP only.')
+
+
+def axes_dispatch(func):
+    def inner(request, *args, **kwargs):
+        if is_already_locked(request):
+            return lockout_response(request)
+
+        return func(request, *args, **kwargs)
+
+    return inner
+
+
+def axes_form_invalid(func):
+    @wraps(func)
+    def inner(self, *args, **kwargs):
+        if is_already_locked(self.request):
+            return lockout_response(self.request)
+
+        return func(self, *args, **kwargs)
+
+    return inner
+
+
+def lockout_response(request):
+    context = {
+        'failure_limit': settings.AXES_FAILURE_LIMIT,
+        'username': get_client_username(request) or ''
+    }
+
+    cool_off = settings.AXES_COOLOFF_TIME
+    if cool_off:
+        if isinstance(cool_off, (int, float)):
+            cool_off = timedelta(hours=cool_off)
+
+        context.update({
+            'cooloff_time': iso8601(cool_off)
+        })
+
+    if request.is_ajax():
+        return HttpResponse(
+            json.dumps(context),
+            content_type='application/json',
+            status=403,
+        )
+
+    if settings.AXES_LOCKOUT_TEMPLATE:
+        return render(
+            request, settings.AXES_LOCKOUT_TEMPLATE, context, status=403
+        )
+
+    if settings.AXES_LOCKOUT_URL:
+        return HttpResponseRedirect(settings.AXES_LOCKOUT_URL)
+
+    return HttpResponse(get_lockout_message(), status=403)

BIN
desktop/core/ext-py/django-axes-4.5.4/axes/locale/de/LC_MESSAGES/django.mo


+ 97 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/locale/de/LC_MESSAGES/django.po

@@ -0,0 +1,97 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-07-17 15:56+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: axes/admin.py:38
+msgid "Form Data"
+msgstr "Form-Daten"
+
+#: axes/admin.py:41 axes/admin.py:95
+msgid "Meta Data"
+msgstr "Meta-Daten"
+
+#: axes/conf.py:58
+msgid "Account locked: too many login attempts. Please try again later"
+msgstr ""
+"Zugang gesperrt: zu viele fehlgeschlagene Anmeldeversuche. Bitte versuchen "
+"Sie es später erneut."
+
+#: axes/conf.py:61
+msgid ""
+"Account locked: too many login attempts. Contact an admin to unlock your "
+"account."
+msgstr ""
+"Zugang gesperrt: zu viele fehlgeschlagene Anmeldeversuche. Kontaktieren Sie "
+"einen Administrator, um Ihren Zugang zu entsperren."
+
+#: axes/models.py:9
+msgid "User Agent"
+msgstr "Browserkennung"
+
+#: axes/models.py:15
+msgid "IP Address"
+msgstr "IP-Adresse"
+
+#: axes/models.py:21
+msgid "Username"
+msgstr "Benutzername"
+
+#: axes/models.py:35
+msgid "HTTP Accept"
+msgstr ""
+
+#: axes/models.py:40
+msgid "Path"
+msgstr "Pfad"
+
+#: axes/models.py:45
+msgid "Attempt Time"
+msgstr "Zugriffszeitpunkt"
+
+#: axes/models.py:57
+msgid "GET Data"
+msgstr "GET-Daten"
+
+#: axes/models.py:61
+msgid "POST Data"
+msgstr "POST-Daten"
+
+#: axes/models.py:65
+msgid "Failed Logins"
+msgstr "Fehlgeschlagene Anmeldeversuche"
+
+#: axes/models.py:76
+msgid "access attempt"
+msgstr "Zugriffsversuch"
+
+#: axes/models.py:77
+msgid "access attempts"
+msgstr "Zugriffsversuche"
+
+#: axes/models.py:81
+msgid "Logout Time"
+msgstr "Abmeldezeitpunkt"
+
+#: axes/models.py:90
+msgid "access log"
+msgstr "Zugriffslog"
+
+#: axes/models.py:91
+msgid "access logs"
+msgstr "Zugriffslogs"

BIN
desktop/core/ext-py/django-axes-4.5.4/axes/locale/ru/LC_MESSAGES/django.mo


+ 97 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/locale/ru/LC_MESSAGES/django.po

@@ -0,0 +1,97 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-01-11 12:20+0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: axes/admin.py:38
+msgid "Form Data"
+msgstr "Данные формы"
+
+#: axes/admin.py:41 axes/admin.py:95
+msgid "Meta Data"
+msgstr "Метаданные"
+
+#: axes/conf.py:58
+msgid "Account locked: too many login attempts. Please try again later"
+msgstr ""
+"Учетная запись заблокирована: слишком много попыток входа. "
+"Повторите попытку позже."
+
+#: axes/conf.py:61
+msgid ""
+"Account locked: too many login attempts. Contact an admin to unlock your "
+"account."
+msgstr ""
+"Учетная запись заблокирована: слишком много попыток входа. "
+"Обратитесь к администратору для разблокирования учетной записи."
+
+#: axes/models.py:9
+msgid "User Agent"
+msgstr "Браузер пользователя"
+
+#: axes/models.py:15
+msgid "IP Address"
+msgstr "Адрес IP"
+
+#: axes/models.py:21
+msgid "Username"
+msgstr "Пользователь"
+
+#: axes/models.py:35
+msgid "HTTP Accept"
+msgstr "Запрос HTTP"
+
+#: axes/models.py:40
+msgid "Path"
+msgstr "Путь"
+
+#: axes/models.py:45
+msgid "Attempt Time"
+msgstr "Время входа"
+
+#: axes/models.py:57
+msgid "GET Data"
+msgstr "Данные GET-запроса"
+
+#: axes/models.py:61
+msgid "POST Data"
+msgstr "Данные POST-запроса"
+
+#: axes/models.py:65
+msgid "Failed Logins"
+msgstr "Ошибочные попытки"
+
+#: axes/models.py:76
+msgid "access attempt"
+msgstr "Запись о попытке доступа"
+
+#: axes/models.py:77
+msgid "access attempts"
+msgstr "Попытки доступа"
+
+#: axes/models.py:81
+msgid "Logout Time"
+msgstr "Время выхода"
+
+#: axes/models.py:90
+msgid "access log"
+msgstr "Запись о доступе"
+
+#: axes/models.py:91
+msgid "access logs"
+msgstr "Логи доступа"

BIN
desktop/core/ext-py/django-axes-4.5.4/axes/locale/tr/LC_MESSAGES/django.mo


+ 96 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/locale/tr/LC_MESSAGES/django.po

@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-07-17 15:56+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: axes/admin.py:38
+msgid "Form Data"
+msgstr "Form-Verisi"
+
+#: axes/admin.py:41 axes/admin.py:95
+msgid "Meta Data"
+msgstr "Meta-Verisi"
+
+#: axes/conf.py:58
+msgid "Account locked: too many login attempts. Please try again later"
+msgstr ""
+"Hesap kilitlendi: cok fazla erişim denemesi. Lütfen daha sonra tekrar deneyiniz"
+
+#: axes/conf.py:61
+msgid ""
+"Account locked: too many login attempts. Contact an admin to unlock your "
+"account."
+msgstr ""
+"Hesap kilitlendi: cok fazla erişim denemesi. Hesabını açtırmak için yöneticiyle iletişime"
+"geçin"
+
+#: axes/models.py:9
+msgid "User Agent"
+msgstr ""
+
+#: axes/models.py:15
+msgid "IP Address"
+msgstr "IP-Adresi"
+
+#: axes/models.py:21
+msgid "Username"
+msgstr "Kullanıcı Adı"
+
+#: axes/models.py:35
+msgid "HTTP Accept"
+msgstr ""
+
+#: axes/models.py:40
+msgid "Path"
+msgstr "Yol"
+
+#: axes/models.py:45
+msgid "Attempt Time"
+msgstr "Girişim Zamanı"
+
+#: axes/models.py:57
+msgid "GET Data"
+msgstr "GET-Verisi"
+
+#: axes/models.py:61
+msgid "POST Data"
+msgstr "POST-Verisi"
+
+#: axes/models.py:65
+msgid "Failed Logins"
+msgstr "Geçersiz Girişler"
+
+#: axes/models.py:76
+msgid "access attempt"
+msgstr "erişim denemesi"
+
+#: axes/models.py:77
+msgid "access attempts"
+msgstr "erişim denemeleri"
+
+#: axes/models.py:81
+msgid "Logout Time"
+msgstr "Çıkış Zamanı"
+
+#: axes/models.py:90
+msgid "access log"
+msgstr "erişim kaydı"
+
+#: axes/models.py:91
+msgid "access logs"
+msgstr "erişim kayıtları"

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

@@ -0,0 +1 @@
+from __future__ import unicode_literals

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

@@ -0,0 +1 @@
+from __future__ import unicode_literals

+ 4 - 2
desktop/core/ext-py/django-axes-2.2.0/axes/management/commands/axes_list_attempts.py → desktop/core/ext-py/django-axes-4.5.4/axes/management/commands/axes_list_attempts.py

@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
 from django.core.management.base import BaseCommand
 
 from axes.models import AccessAttempt
@@ -7,9 +9,9 @@ class Command(BaseCommand):
     args = ''
     help = ('List registered login attempts')
 
-    def handle(self, *args, **kwargs):
+    def handle(self, *args, **kwargs):  # pylint: disable=unused-argument
         for obj in AccessAttempt.objects.all():
-            print('{ip}\t{username}\t{failures}'.format(
+            self.stdout.write('{ip}\t{username}\t{failures}'.format(
                 ip=obj.ip_address,
                 username=obj.username,
                 failures=obj.failures,

+ 10 - 7
desktop/core/ext-py/django-axes-2.2.0/axes/management/commands/axes_reset.py → desktop/core/ext-py/django-axes-4.5.4/axes/management/commands/axes_reset.py

@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
 from django.core.management.base import BaseCommand
 
 from axes.utils import reset
@@ -8,17 +10,18 @@ class Command(BaseCommand):
             "IP, resets only for that IP")
 
     def add_arguments(self, parser):
-        parser.add_argument('ip', nargs='+')
+        parser.add_argument('ip', nargs='*')
 
     def handle(self, *args, **kwargs):
         count = 0
-        if kwargs:
-            for ip in kwargs['ip']:
+        if kwargs and kwargs.get('ip'):
+            for ip in kwargs['ip'][1:]:
                 count += reset(ip=ip)
         else:
             count = reset()
 
-        if count:
-            print('{0} attempts removed.'.format(count))
-        else:
-            print('No attempts found.')
+        if kwargs['verbosity']:
+            if count:
+                self.stdout.write('{0} attempts removed.'.format(count))
+            else:
+                self.stdout.write('No attempts found.')

+ 22 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/management/commands/axes_reset_user.py

@@ -0,0 +1,22 @@
+from __future__ import unicode_literals
+
+from django.core.management.base import BaseCommand
+
+from axes.utils import reset
+
+
+class Command(BaseCommand):
+    help = ("Resets any lockouts or failed login records. If called with an "
+            "User name, resets only for that User name")
+
+    def add_arguments(self, parser):
+        parser.add_argument('username')
+
+    def handle(self, *args, **kwargs):
+        count = 0
+        count += reset(username=kwargs['username'])
+        if kwargs['verbosity']:
+            if count:
+                self.stdout.write('{0} attempts removed.'.format(count))
+            else:
+                self.stdout.write('No attempts found.')

+ 0 - 0
desktop/core/ext-py/django-axes-2.2.0/axes/migrations/0001_initial.py → desktop/core/ext-py/django-axes-4.5.4/axes/migrations/0001_initial.py


+ 0 - 0
desktop/core/ext-py/django-axes-2.2.0/axes/migrations/0002_auto_20151217_2044.py → desktop/core/ext-py/django-axes-4.5.4/axes/migrations/0002_auto_20151217_2044.py


+ 0 - 0
desktop/core/ext-py/django-axes-2.2.0/axes/migrations/0003_auto_20160322_0929.py → desktop/core/ext-py/django-axes-4.5.4/axes/migrations/0003_auto_20160322_0929.py


+ 58 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/migrations/0004_auto_20181024_1538.py

@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.16 on 2018-10-24 22:38
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('axes', '0003_auto_20160322_0929'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='accessattempt',
+            options={'verbose_name': 'access attempt', 'verbose_name_plural': 'access attempts'},
+        ),
+        migrations.AlterModelOptions(
+            name='accesslog',
+            options={'verbose_name': 'access log', 'verbose_name_plural': 'access logs'},
+        ),
+        migrations.AlterField(
+            model_name='accessattempt',
+            name='attempt_time',
+            field=models.DateTimeField(auto_now_add=True, verbose_name='Attempt Time'),
+        ),
+        migrations.AlterField(
+            model_name='accessattempt',
+            name='user_agent',
+            field=models.CharField(db_index=True, max_length=255, verbose_name='User Agent'),
+        ),
+        migrations.AlterField(
+            model_name='accessattempt',
+            name='username',
+            field=models.CharField(db_index=True, max_length=255, null=True, verbose_name='Username'),
+        ),
+        migrations.AlterField(
+            model_name='accesslog',
+            name='attempt_time',
+            field=models.DateTimeField(auto_now_add=True, verbose_name='Attempt Time'),
+        ),
+        migrations.AlterField(
+            model_name='accesslog',
+            name='logout_time',
+            field=models.DateTimeField(blank=True, null=True, verbose_name='Logout Time'),
+        ),
+        migrations.AlterField(
+            model_name='accesslog',
+            name='user_agent',
+            field=models.CharField(db_index=True, max_length=255, verbose_name='User Agent'),
+        ),
+        migrations.AlterField(
+            model_name='accesslog',
+            name='username',
+            field=models.CharField(db_index=True, max_length=255, null=True, verbose_name='Username'),
+        ),
+    ]

+ 17 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/migrations/0005_remove_accessattempt_trusted.py

@@ -0,0 +1,17 @@
+# Generated by Django 2.1.4 on 2018-12-23 09:03
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('axes', '0004_auto_20181024_1538'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='accessattempt',
+            name='trusted',
+        ),
+    ]

+ 0 - 0
desktop/core/ext-py/django-axes-2.2.0/axes/management/__init__.py → desktop/core/ext-py/django-axes-4.5.4/axes/migrations/__init__.py


+ 33 - 21
desktop/core/ext-py/django-axes-2.2.0/axes/models.py → desktop/core/ext-py/django-axes-4.5.4/axes/models.py

@@ -1,44 +1,42 @@
+from __future__ import unicode_literals
+
 from django.db import models
-from django.utils import six
+from django.utils.translation import gettext_lazy as _
 
 
 class CommonAccess(models.Model):
     user_agent = models.CharField(
+        _('User Agent'),
         max_length=255,
         db_index=True,
     )
 
     ip_address = models.GenericIPAddressField(
-        verbose_name='IP Address',
+        _('IP Address'),
         null=True,
         db_index=True,
     )
 
     username = models.CharField(
+        _('Username'),
         max_length=255,
         null=True,
         db_index=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,
-        db_index=True,
-    )
-
     http_accept = models.CharField(
-        verbose_name='HTTP Accept',
+        _('HTTP Accept'),
         max_length=1025,
     )
 
     path_info = models.CharField(
-        verbose_name='Path',
+        _('Path'),
         max_length=255,
     )
 
     attempt_time = models.DateTimeField(
-        auto_now=True,
+        _('Attempt Time'),
+        auto_now_add=True,
     )
 
     class Meta:
@@ -49,32 +47,46 @@ class CommonAccess(models.Model):
 
 class AccessAttempt(CommonAccess):
     get_data = models.TextField(
-        verbose_name='GET Data',
+        _('GET Data'),
     )
 
     post_data = models.TextField(
-        verbose_name='POST Data',
+        _('POST Data'),
     )
 
     failures_since_start = models.PositiveIntegerField(
-        verbose_name='Failed Logins',
+        _('Failed Logins'),
     )
 
     @property
     def failures(self):
         return self.failures_since_start
 
-    def __unicode__(self):
-        return six.u('Attempted Access: %s') % self.attempt_time
+    def __str__(self):
+        return 'Attempted Access: %s' % self.attempt_time
+
+    class Meta:
+        verbose_name = _('access attempt')
+        verbose_name_plural = _('access attempts')
 
 
 class AccessLog(CommonAccess):
+    # 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,
+        db_index=True,
+    )
+
     logout_time = models.DateTimeField(
+        _('Logout Time'),
         null=True,
         blank=True,
     )
 
-    def __unicode__(self):
-        return six.u('Access Log for %s @ %s') % (
-            self.username, self.attempt_time
-        )
+    def __str__(self):
+        return 'Access Log for %s @ %s' % (self.username, self.attempt_time)
+
+    class Meta:
+        verbose_name = _('access log')
+        verbose_name_plural = _('access logs')

+ 184 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/signals.py

@@ -0,0 +1,184 @@
+from __future__ import unicode_literals
+
+import logging
+
+from django.contrib.auth.signals import user_logged_in
+from django.contrib.auth.signals import user_logged_out
+from django.contrib.auth.signals import user_login_failed
+from django.db.models.signals import post_save, post_delete
+from django.dispatch import receiver
+from django.dispatch import Signal
+from django.utils import timezone
+
+from axes.conf import settings
+from axes.attempts import get_cache_key
+from axes.attempts import get_cache_timeout
+from axes.attempts import get_user_attempts
+from axes.attempts import is_user_lockable
+from axes.attempts import ip_in_whitelist
+from axes.attempts import reset_user_attempts
+from axes.models import AccessLog, AccessAttempt
+from axes.utils import get_client_str
+from axes.utils import query2str
+from axes.utils import get_axes_cache, get_client_ip, get_client_username, get_credentials
+
+
+log = logging.getLogger(settings.AXES_LOGGER)
+
+
+user_locked_out = Signal(providing_args=['request', 'username', 'ip_address'])
+
+
+@receiver(user_login_failed)
+def log_user_login_failed(sender, credentials, request, **kwargs):  # pylint: disable=unused-argument
+    """ Create an AccessAttempt record if the login wasn't successful
+    """
+    if request is None:
+        log.warning('Attempt to authenticate with a custom backend failed.')
+        return
+
+    ip_address = get_client_ip(request)
+    username = get_client_username(request, credentials)
+    user_agent = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
+    path_info = request.META.get('PATH_INFO', '<unknown>')[:255]
+    http_accept = request.META.get('HTTP_ACCEPT', '<unknown>')[:1025]
+
+    if settings.AXES_NEVER_LOCKOUT_WHITELIST and ip_in_whitelist(ip_address):
+        return
+
+    failures = 0
+    attempts = get_user_attempts(request, credentials)
+    cache_hash_key = get_cache_key(request, credentials)
+    cache_timeout = get_cache_timeout()
+
+    failures_cached = get_axes_cache().get(cache_hash_key)
+    if failures_cached is not None:
+        failures = failures_cached
+    else:
+        for attempt in attempts:
+            failures = max(failures, attempt.failures_since_start)
+
+    # add a failed attempt for this user
+    failures += 1
+    get_axes_cache().set(cache_hash_key, failures, cache_timeout)
+
+    # has already attempted, update the info
+    if attempts:
+        for attempt in attempts:
+            attempt.get_data = '%s\n---------\n%s' % (
+                attempt.get_data,
+                query2str(request.GET),
+            )
+            attempt.post_data = '%s\n---------\n%s' % (
+                attempt.post_data,
+                query2str(request.POST)
+            )
+            attempt.http_accept = http_accept
+            attempt.path_info = path_info
+            attempt.failures_since_start = failures
+            attempt.attempt_time = timezone.now()
+            attempt.save()
+
+            log.info(
+                'AXES: Repeated login failure by %s. Count = %d of %d',
+                get_client_str(username, ip_address, user_agent, path_info),
+                failures,
+                settings.AXES_FAILURE_LIMIT
+            )
+    else:
+        # Record failed attempt. Whether or not the IP address or user agent is
+        # used in counting failures is handled elsewhere, so we just record
+        # everything here.
+        AccessAttempt.objects.create(
+            user_agent=user_agent,
+            ip_address=ip_address,
+            username=username,
+            get_data=query2str(request.GET),
+            post_data=query2str(request.POST),
+            http_accept=http_accept,
+            path_info=path_info,
+            failures_since_start=failures,
+        )
+
+        log.info(
+            'AXES: New login failure by %s. Creating access record.',
+            get_client_str(username, ip_address, user_agent, path_info)
+        )
+
+    # 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 >= settings.AXES_FAILURE_LIMIT and
+        settings.AXES_LOCK_OUT_AT_FAILURE and
+        is_user_lockable(request, credentials)
+    ):
+        log.warning(
+            'AXES: locked out %s after repeated login attempts.',
+            get_client_str(username, ip_address, user_agent, path_info)
+        )
+
+        # send signal when someone is locked out.
+        user_locked_out.send(
+            'axes', request=request, username=username, ip_address=ip_address
+        )
+
+
+@receiver(user_logged_in)
+def log_user_logged_in(sender, request, user, **kwargs):  # pylint: disable=unused-argument
+    """ When a user logs in, update the access log
+    """
+    username = user.get_username()
+    credentials = get_credentials(username)
+    ip_address = get_client_ip(request)
+    user_agent = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
+    path_info = request.META.get('PATH_INFO', '<unknown>')[:255]
+    http_accept = request.META.get('HTTP_ACCEPT', '<unknown>')[:1025]
+    log.info(
+        'AXES: Successful login by %s.',
+        get_client_str(username, ip_address, user_agent, path_info)
+    )
+
+    if not settings.AXES_DISABLE_SUCCESS_ACCESS_LOG:
+        AccessLog.objects.create(
+            user_agent=user_agent,
+            ip_address=ip_address,
+            username=username,
+            http_accept=http_accept,
+            path_info=path_info,
+            trusted=True,
+        )
+
+    if settings.AXES_RESET_ON_SUCCESS:
+        count = reset_user_attempts(request, credentials)
+        log.info(
+            'AXES: Deleted %d failed login attempts by %s.',
+            count,
+            get_client_str(username, ip_address, user_agent, path_info)
+        )
+
+
+@receiver(user_logged_out)
+def log_user_logged_out(sender, request, user, **kwargs):  # pylint: disable=unused-argument
+    """ When a user logs out, update the access log
+    """
+    log.info('AXES: Successful logout by %s.', user)
+
+    if user and not settings.AXES_DISABLE_ACCESS_LOG:
+        AccessLog.objects.filter(
+            username=user.get_username(),
+            logout_time__isnull=True,
+        ).update(logout_time=timezone.now())
+
+
+@receiver(post_save, sender=AccessAttempt)
+def update_cache_after_save(instance, **kwargs):  # pylint: disable=unused-argument
+    cache_hash_key = get_cache_key(instance)
+    if not get_axes_cache().get(cache_hash_key):
+        cache_timeout = get_cache_timeout()
+        get_axes_cache().set(cache_hash_key, instance.failures_since_start, cache_timeout)
+
+
+@receiver(post_delete, sender=AccessAttempt)
+def delete_cache_after_delete(instance, **kwargs):  # pylint: disable=unused-argument
+    cache_hash_key = get_cache_key(instance)
+    get_axes_cache().delete(cache_hash_key)

+ 33 - 6
desktop/core/ext-py/django-axes-2.2.0/axes/test_settings.py → desktop/core/ext-py/django-axes-4.5.4/axes/test_settings.py

@@ -1,4 +1,4 @@
-from datetime import timedelta
+from __future__ import unicode_literals
 
 DATABASES = {
     'default': {
@@ -7,12 +7,24 @@ DATABASES = {
     }
 }
 
+CACHES = {
+    'default': {
+        'BACKEND': 'django.core.cache.backends.dummy.DummyCache'
+    }
+}
+
 SITE_ID = 1
 
-MIDDLEWARE_CLASSES = (
+MIDDLEWARE = (
     'django.middleware.common.CommonMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+)
+
+AUTHENTICATION_BACKENDS = (
+    'axes.backends.AxesModelBackend',
+    'django.contrib.auth.backends.ModelBackend',
 )
 
 ROOT_URLCONF = 'axes.test_urls'
@@ -24,8 +36,7 @@ INSTALLED_APPS = (
     'django.contrib.sites',
     'django.contrib.messages',
     'django.contrib.admin',
-
-    'axes.apps.AppConfig',
+    'axes',
 )
 
 TEMPLATES = [
@@ -44,6 +55,23 @@ TEMPLATES = [
     },
 ]
 
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'handlers': {
+        'console': {
+            'class': 'logging.StreamHandler',
+        },
+    },
+    'loggers': {
+        'axes': {
+            'handlers': ['console'],
+            'level': 'INFO',
+            'propagate': False,
+        },
+    },
+}
+
 SECRET_KEY = 'too-secret-for-test'
 
 USE_I18N = False
@@ -54,5 +82,4 @@ USE_TZ = False
 
 LOGIN_REDIRECT_URL = '/admin/'
 
-AXES_LOGIN_FAILURE_LIMIT = 10
-AXES_COOLOFF_TIME = timedelta(seconds=2)
+AXES_FAILURE_LIMIT = 10

+ 11 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/test_settings_cache.py

@@ -0,0 +1,11 @@
+from __future__ import unicode_literals
+
+from .test_settings import *  # pylint: disable=unused-wildcard-import
+
+AXES_CACHE = 'axes'
+
+CACHES = {
+    'axes': {
+        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
+    }
+}

+ 8 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/test_urls.py

@@ -0,0 +1,8 @@
+from __future__ import unicode_literals
+
+from django.conf.urls import url
+from django.contrib import admin
+
+urlpatterns = [
+    url(r'^admin/', admin.site.urls),
+]

+ 1 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/tests/__init__.py

@@ -0,0 +1 @@
+from __future__ import unicode_literals

+ 6 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/tests/compatibility.py

@@ -0,0 +1,6 @@
+from __future__ import unicode_literals
+
+try:
+    from unittest.mock import patch  # pylint: disable=unused-import
+except ImportError:
+    from mock import patch  # pylint: disable=unused-import

+ 476 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/tests/test_access_attempt.py

@@ -0,0 +1,476 @@
+from __future__ import unicode_literals
+
+import datetime
+import hashlib
+import json
+import random
+import string
+import time
+
+from django.http import HttpRequest
+from django.test import TestCase, override_settings
+from django.urls import reverse
+from django.contrib.auth import authenticate
+from django.contrib.auth.models import User
+from django.test.client import RequestFactory
+
+from axes.conf import settings
+from axes.attempts import get_cache_key
+from axes.models import AccessAttempt, AccessLog
+from axes.signals import user_locked_out
+from axes.tests.compatibility import patch
+from axes.utils import reset
+
+
+@override_settings(AXES_COOLOFF_TIME=datetime.timedelta(seconds=2))
+class AccessAttemptTest(TestCase):
+    """Test case using custom settings for testing
+    """
+    VALID_USERNAME = 'valid-username'
+    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_username=False, is_valid_password=False,
+               is_json=False, **kwargs):
+        """Login a user. A valid credential is used when is_valid_username is True,
+        otherwise it will use a random string to make a failed login.
+        """
+        if is_valid_username:
+            # Use a valid username
+            username = self.VALID_USERNAME
+        else:
+            # Generate a wrong random username
+            chars = string.ascii_uppercase + string.digits
+            username = ''.join(random.choice(chars) for x in range(10))
+
+        if is_valid_password:
+            password = self.VALID_PASSWORD
+        else:
+            password = 'invalid-password'
+
+        headers = {
+            'user_agent': 'test-browser'
+        }
+        post_data = {
+            'username': username,
+            'password': password,
+            'this_is_the_login_form': 1,
+        }
+        post_data.update(kwargs)
+
+        if is_json:
+            headers.update({
+                'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
+                'content_type': 'application/json',
+            })
+            post_data = json.dumps(post_data)
+
+        response = self.client.post(
+            reverse('admin:login'), post_data, **headers
+        )
+
+        return response
+
+    def setUp(self):
+        """Create a valid user for login
+        """
+        self.user = User.objects.create_superuser(
+            username=self.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
+        """
+        # test until one try before the limit
+        for _ in range(1, settings.AXES_FAILURE_LIMIT):
+            response = self._login()
+            # Check if we are in the same login page
+            self.assertContains(response, self.LOGIN_FORM_KEY, html=True)
+
+        # 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, status_code=403)
+
+    def test_failure_limit_many(self):
+        """Tests the login lock trying to login a lot of times more
+        than failure limit
+        """
+        for _ in range(1, settings.AXES_FAILURE_LIMIT):
+            response = self._login()
+            # Check if we are in the same login page
+            self.assertContains(response, self.LOGIN_FORM_KEY, html=True)
+
+        # So, we shouldn't have gotten a lock-out yet.
+        # We should get a locked message each time we try again
+        for _ in range(random.randrange(1, settings.AXES_FAILURE_LIMIT)):
+            response = self._login()
+            self.assertContains(response, self.LOCKED_MESSAGE, status_code=403)
+
+    def test_valid_login(self):
+        """Tests a valid login for a real username
+        """
+        response = self._login(is_valid_username=True, is_valid_password=True)
+        self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=302, html=True)
+
+    def test_valid_logout(self):
+        """Tests a valid logout and make sure the logout_time is updated
+        """
+        response = self._login(is_valid_username=True, is_valid_password=True)
+        self.assertEqual(AccessLog.objects.latest('id').logout_time, None)
+
+        response = self.client.get(reverse('admin:logout'))
+        self.assertNotEqual(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(settings.AXES_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_username=True,
+            is_valid_password=True,
+            user_agent=long_user_agent,
+        )
+        self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=302, html=True)
+
+    def test_long_user_agent_not_valid(self):
+        """Tests if can handle a long user agent with failure
+        """
+        long_user_agent = 'ie6' * 1024
+        for _ in range(settings.AXES_FAILURE_LIMIT + 1):
+            response = self._login(user_agent=long_user_agent)
+
+        self.assertContains(response, self.LOCKED_MESSAGE, status_code=403)
+
+    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()
+
+    @patch('axes.utils.get_client_ip', return_value='127.0.0.1')
+    def test_get_cache_key(self, _):
+        """ Test the cache key format"""
+        # Getting cache key from request
+        ip_address = '127.0.0.1'
+        cache_hash_key = 'axes-{}'.format(
+            hashlib.md5(ip_address.encode()).hexdigest()
+        )
+
+        request_factory = RequestFactory()
+        request = request_factory.post('/admin/login/',
+                                       data={
+                                           'username': self.VALID_USERNAME,
+                                           'password': 'test'
+                                       })
+
+        self.assertEqual(cache_hash_key, get_cache_key(request))
+
+        # Getting cache key from AccessAttempt Object
+        attempt = AccessAttempt(
+            user_agent='<unknown>',
+            ip_address=ip_address,
+            username=self.VALID_USERNAME,
+            get_data='',
+            post_data='',
+            http_accept=request.META.get('HTTP_ACCEPT', '<unknown>'),
+            path_info=request.META.get('PATH_INFO', '<unknown>'),
+            failures_since_start=0,
+        )
+        self.assertEqual(cache_hash_key, get_cache_key(attempt))
+
+
+    @patch('axes.utils.get_client_ip', return_value='127.0.0.1')
+    def test_get_cache_key_credentials(self, _):
+        """ Test the cache key format"""
+        # Getting cache key from request
+        ip_address = '127.0.0.1'
+        cache_hash_key = 'axes-{}'.format(
+            hashlib.md5(ip_address.encode()).hexdigest()
+        )
+
+        request_factory = RequestFactory()
+        request = request_factory.post('/admin/login/',
+                                       data={
+                                           'username': self.VALID_USERNAME,
+                                           'password': 'test'
+                                       })
+
+        # Difference between the upper test: new call signature with credentials
+        credentials = {'username': self.VALID_USERNAME}
+
+        self.assertEqual(cache_hash_key, get_cache_key(request, credentials))
+
+        # Getting cache key from AccessAttempt Object
+        attempt = AccessAttempt(
+            user_agent='<unknown>',
+            ip_address=ip_address,
+            username=self.VALID_USERNAME,
+            get_data='',
+            post_data='',
+            http_accept=request.META.get('HTTP_ACCEPT', '<unknown>'),
+            path_info=request.META.get('PATH_INFO', '<unknown>'),
+            failures_since_start=0,
+        )
+        self.assertEqual(cache_hash_key, get_cache_key(attempt))
+
+    def test_send_lockout_signal(self):
+        """Test if the lockout signal is emitted
+        """
+        # this "hack" is needed so we don't have to use global variables or python3 features
+        class Scope(object): pass
+        scope = Scope()
+        scope.signal_received = 0
+
+        def signal_handler(request, username, ip_address, *args, **kwargs):  # pylint: disable=unused-argument
+            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.assertEqual(scope.signal_received, 1)
+
+        reset()
+
+        # Make another lockout
+        self.test_failure_limit_once()
+        self.assertEqual(scope.signal_received, 2)
+
+    @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
+    def test_lockout_by_combination_user_and_ip(self):
+        """Tests the login lock with a valid username and invalid password
+        when AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP is True
+        """
+        # test until one try before the limit
+        for _ in range(1, settings.AXES_FAILURE_LIMIT):
+            response = self._login(
+                is_valid_username=True,
+                is_valid_password=False,
+            )
+            # Check if we are in the same login page
+            self.assertContains(response, self.LOGIN_FORM_KEY, html=True)
+
+        # So, we shouldn't have gotten a lock-out yet.
+        # But we should get one now
+        response = self._login(is_valid_username=True, is_valid_password=False)
+        self.assertContains(response, self.LOCKED_MESSAGE, status_code=403)
+
+    @override_settings(AXES_ONLY_USER_FAILURES=True)
+    def test_lockout_by_user_only(self):
+        """Tests the login lock with a valid username and invalid password
+        when AXES_ONLY_USER_FAILURES is True
+        """
+        # test until one try before the limit
+        for _ in range(1, settings.AXES_FAILURE_LIMIT):
+            response = self._login(
+                is_valid_username=True,
+                is_valid_password=False,
+            )
+            # Check if we are in the same login page
+            self.assertContains(response, self.LOGIN_FORM_KEY, html=True)
+
+        # So, we shouldn't have gotten a lock-out yet.
+        # But we should get one now
+        response = self._login(is_valid_username=True, is_valid_password=False)
+        self.assertContains(response, self.LOCKED_MESSAGE, status_code=403)
+
+        # reset the username only and make sure we can log in now even though
+        # our IP has failed each time
+        reset(username=AccessAttemptTest.VALID_USERNAME)
+        response = self._login(
+            is_valid_username=True,
+            is_valid_password=True,
+        )
+        # Check if we are still in the login page
+        self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=302, html=True)
+
+        # now create failure_limit + 1 failed logins and then we should still
+        # be able to login with valid_username
+        for _ in range(settings.AXES_FAILURE_LIMIT):
+            response = self._login(
+                is_valid_username=False,
+                is_valid_password=False,
+            )
+        # Check if we can still log in with valid user
+        response = self._login(is_valid_username=True, is_valid_password=True)
+        self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=302, html=True)
+
+    def test_log_data_truncated(self):
+        """Tests that query2str properly truncates data to the
+        max_length (default 1024)
+        """
+        # An impossibly large post dict
+        extra_data = {string.ascii_letters * x: x for x in range(0, 1000)}
+        self._login(**extra_data)
+        self.assertEqual(
+            len(AccessAttempt.objects.latest('id').post_data), 1024
+        )
+
+    def test_json_response(self):
+        """Tests response content type and status code for the ajax request
+        """
+        self.test_failure_limit_once()
+        response = self._login(is_json=True)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.get('Content-Type'), 'application/json')
+
+    @override_settings(AXES_DISABLE_SUCCESS_ACCESS_LOG=True)
+    def test_valid_logout_without_success_log(self):
+        AccessLog.objects.all().delete()
+
+        response = self._login(is_valid_username=True, is_valid_password=True)
+        response = self.client.get(reverse('admin:logout'))
+
+        self.assertEqual(AccessLog.objects.all().count(), 0)
+        self.assertContains(response, 'Logged out', html=True)
+
+    @override_settings(AXES_DISABLE_SUCCESS_ACCESS_LOG=True)
+    def test_valid_login_without_success_log(self):
+        """
+        A valid login doesn't generate an AccessLog when
+        `DISABLE_SUCCESS_ACCESS_LOG=True`.
+        """
+        AccessLog.objects.all().delete()
+
+        response = self._login(is_valid_username=True, is_valid_password=True)
+
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(AccessLog.objects.all().count(), 0)
+
+    @override_settings(AXES_DISABLE_ACCESS_LOG=True)
+    def test_valid_logout_without_log(self):
+        AccessLog.objects.all().delete()
+
+        response = self._login(is_valid_username=True, is_valid_password=True)
+        response = self.client.get(reverse('admin:logout'))
+
+        self.assertEqual(AccessLog.objects.first().logout_time, None)
+        self.assertContains(response, 'Logged out', html=True)
+
+    @override_settings(AXES_DISABLE_ACCESS_LOG=True)
+    def test_non_valid_login_without_log(self):
+        """
+        A non-valid login does generate an AccessLog when
+        `DISABLE_ACCESS_LOG=True`.
+        """
+        AccessLog.objects.all().delete()
+
+        response = self._login(is_valid_username=True, is_valid_password=False)
+        self.assertEqual(response.status_code, 200)
+
+        self.assertEqual(AccessLog.objects.all().count(), 0)
+
+    @override_settings(AXES_DISABLE_ACCESS_LOG=True)
+    def test_check_is_not_made_on_GET(self):
+        AccessLog.objects.all().delete()
+
+        response = self.client.get(reverse('admin:login'))
+        self.assertEqual(response.status_code, 200)
+
+        response = self._login(is_valid_username=True, is_valid_password=True)
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.get(reverse('admin:index'))
+        self.assertEqual(response.status_code, 200)
+
+    def test_custom_authentication_backend(self):
+        '''
+        ``log_user_login_failed`` should shortcircuit if an attempt to authenticate
+        with a custom authentication backend fails.
+        '''
+
+        request = HttpRequest()
+        request.user = self.user
+        authenticate(request=request, foo='bar')
+        self.assertEqual(AccessLog.objects.all().count(), 0)
+
+    def _assert_resets_on_success(self):
+        """Sets up for testing the AXES_RESET_ON_SUCCESS setting.
+        """
+        # test until one try before the limit
+        for _ in range(settings.AXES_FAILURE_LIMIT - 1):
+            response = self._login()
+            # Check if we are in the same login page
+            self.assertContains(response, self.LOGIN_FORM_KEY, html=True)
+
+        # Perform a valid login
+        response = self._login(is_valid_username=True, is_valid_password=True)
+        self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=302, html=True)
+
+        return self._login()
+
+    # by default, AXES_RESET_ON_SUCCESS = False
+    def test_reset_on_success_default(self):
+        """Tests that the failure attempts does not reset after one successful
+        attempt by default.
+        """
+        response = self._assert_resets_on_success()
+
+        # So, we shouldn't have found a lock-out yet.
+        # But we should find one now
+        self.assertContains(response, self.LOCKED_MESSAGE, status_code=403)
+
+    @override_settings(AXES_RESET_ON_SUCCESS=True)
+    def test_reset_on_success(self):
+        """Tests that the failure attempts resets after one successful
+        attempt when using the corresponding setting.
+        """
+        response = self._assert_resets_on_success()
+
+        # So, we shouldn't have found a lock-out yet.
+        # And we shouldn't find one now
+        self.assertContains(response, self.LOGIN_FORM_KEY, html=True)
+        for _ in range(settings.AXES_FAILURE_LIMIT - 2):
+            response = self._login()
+            # Check if we are on the same login page.
+            self.assertContains(response, self.LOGIN_FORM_KEY, html=True)
+
+        # But we should find one now
+        response = self._login()
+        self.assertContains(response, self.LOCKED_MESSAGE, status_code=403)

+ 435 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/tests/test_access_attempt_config.py

@@ -0,0 +1,435 @@
+from __future__ import unicode_literals
+
+import json
+
+from django.test import TestCase, override_settings
+from django.urls import reverse
+from django.contrib.auth.models import User
+
+from axes.conf import settings
+
+
+class AccessAttemptConfigTest(TestCase):
+    """ This set of tests checks for lockouts under different configurations
+    and circumstances to prevent false positives and false negatives.
+    Always block attempted logins for the same user from the same IP.
+    Always allow attempted logins for a different user from a different IP.
+    """
+    IP_1 = '10.1.1.1'
+    IP_2 = '10.2.2.2'
+    USER_1 = 'valid-user-1'
+    USER_2 = 'valid-user-2'
+    VALID_PASSWORD = 'valid-password'
+    WRONG_PASSWORD = 'wrong-password'
+    LOCKED_MESSAGE = 'Account locked: too many login attempts.'
+    LOGIN_FORM_KEY = '<input type="submit" value="Log in" />'
+    ALLOWED = 302
+    BLOCKED = 403
+
+    def _login(self, username, password, ip_addr='127.0.0.1',
+               is_json=False, **kwargs):
+        """Login a user and get the response.
+        IP address can be configured to test IP blocking functionality.
+        """
+        headers = {
+            'user_agent': 'test-browser'
+        }
+        post_data = {
+            'username': username,
+            'password': password,
+            'this_is_the_login_form': 1,
+        }
+        post_data.update(kwargs)
+
+        if is_json:
+            headers.update({
+                'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
+                'content_type': 'application/json',
+            })
+            post_data = json.dumps(post_data)
+
+        response = self.client.post(
+            reverse('admin:login'), post_data, REMOTE_ADDR=ip_addr, **headers
+        )
+        return response
+
+    def _lockout_user_from_ip(self, username, ip_addr):
+        for _ in range(settings.AXES_FAILURE_LIMIT):
+            response = self._login(
+                username=username,
+                password=self.WRONG_PASSWORD,
+                ip_addr=ip_addr,
+            )
+        return response
+
+    def _lockout_user1_from_ip1(self):
+        return self._lockout_user_from_ip(
+            username=self.USER_1,
+            ip_addr=self.IP_1,
+        )
+
+    def setUp(self):
+        """Create two valid users for authentication.
+        """
+        self.user = User.objects.create_superuser(
+            username=self.USER_1,
+            email='test_1@example.com',
+            password=self.VALID_PASSWORD,
+        )
+        self.user = User.objects.create_superuser(
+            username=self.USER_2,
+            email='test_2@example.com',
+            password=self.VALID_PASSWORD,
+        )
+
+    # Test for true and false positives when blocking by IP *OR* user (default)
+    # Cache disabled. Default settings.
+    def test_lockout_by_ip_blocks_when_same_user_same_ip_without_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 1 is still blocked from IP 1.
+        response = self._login(
+            self.USER_1,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_1
+        )
+        self.assertEqual(response.status_code, self.BLOCKED)
+
+    def test_lockout_by_ip_allows_when_same_user_diff_ip_without_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 1 can still login from IP 2.
+        response = self._login(
+            self.USER_1,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_2
+        )
+        self.assertEqual(response.status_code, self.ALLOWED)
+
+    def test_lockout_by_ip_blocks_when_diff_user_same_ip_without_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 2 is also locked out from IP 1.
+        response = self._login(
+            self.USER_2,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_1
+        )
+        self.assertEqual(response.status_code, self.BLOCKED)
+
+    def test_lockout_by_ip_allows_when_diff_user_diff_ip_without_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 2 can still login from IP 2.
+        response = self._login(
+            self.USER_2,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_2
+        )
+        self.assertEqual(response.status_code, self.ALLOWED)
+
+    # Test for true and false positives when blocking by user only.
+    # Cache disabled. When AXES_ONLY_USER_FAILURES = True
+    @override_settings(AXES_ONLY_USER_FAILURES=True)
+    def test_lockout_by_user_blocks_when_same_user_same_ip_without_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 1 is still blocked from IP 1.
+        response = self._login(
+            self.USER_1,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_1
+        )
+        self.assertEqual(response.status_code, self.BLOCKED)
+
+    @override_settings(AXES_ONLY_USER_FAILURES=True)
+    def test_lockout_by_user_blocks_when_same_user_diff_ip_without_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 1 is also locked out from IP 2.
+        response = self._login(
+            self.USER_1,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_2
+        )
+        self.assertEqual(response.status_code, self.BLOCKED)
+
+    @override_settings(AXES_ONLY_USER_FAILURES=True)
+    def test_lockout_by_user_allows_when_diff_user_same_ip_without_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 2 can still login from IP 1.
+        response = self._login(
+            self.USER_2,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_1
+        )
+        self.assertEqual(response.status_code, self.ALLOWED)
+
+    @override_settings(AXES_ONLY_USER_FAILURES=True)
+    def test_lockout_by_user_allows_when_diff_user_diff_ip_without_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 2 can still login from IP 2.
+        response = self._login(
+            self.USER_2,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_2
+        )
+        self.assertEqual(response.status_code, self.ALLOWED)
+
+    @override_settings(AXES_ONLY_USER_FAILURES=True)
+    def test_lockout_by_user_with_empty_username_allows_other_users_without_cache(self):
+        # User with empty username is locked out from IP 1.
+        self._lockout_user_from_ip(username='', ip_addr=self.IP_1)
+
+        # Still possible to access the login page
+        response = self.client.get(reverse('admin:login'), REMOTE_ADDR=self.IP_1)
+        self.assertContains(response, self.LOGIN_FORM_KEY, status_code=200, html=True)
+
+    # Test for true and false positives when blocking by user and IP together.
+    # Cache disabled. When LOCK_OUT_BY_COMBINATION_USER_AND_IP = True
+    @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
+    def test_lockout_by_user_and_ip_blocks_when_same_user_same_ip_without_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 1 is still blocked from IP 1.
+        response = self._login(
+            self.USER_1,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_1
+        )
+        self.assertEqual(response.status_code, self.BLOCKED)
+
+    @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
+    def test_lockout_by_user_and_ip_allows_when_same_user_diff_ip_without_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 1 can still login from IP 2.
+        response = self._login(
+            self.USER_1,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_2
+        )
+        self.assertEqual(response.status_code, self.ALLOWED)
+
+    @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
+    def test_lockout_by_user_and_ip_allows_when_diff_user_same_ip_without_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 2 can still login from IP 1.
+        response = self._login(
+            self.USER_2,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_1
+        )
+        self.assertEqual(response.status_code, self.ALLOWED)
+
+    @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
+    def test_lockout_by_user_and_ip_allows_when_diff_user_diff_ip_without_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 2 can still login from IP 2.
+        response = self._login(
+            self.USER_2,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_2
+        )
+        self.assertEqual(response.status_code, self.ALLOWED)
+
+    @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
+    def test_lockout_by_user_and_ip_with_empty_username_allows_other_users_without_cache(self):
+        # User with empty username is locked out from IP 1.
+        self._lockout_user_from_ip(username='', ip_addr=self.IP_1)
+
+        # Still possible to access the login page
+        response = self.client.get(reverse('admin:login'), REMOTE_ADDR=self.IP_1)
+        self.assertContains(response, self.LOGIN_FORM_KEY, status_code=200, html=True)
+
+    # Test for true and false positives when blocking by IP *OR* user (default)
+    # With cache enabled. Default criteria.
+    def test_lockout_by_ip_blocks_when_same_user_same_ip_using_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 1 is still blocked from IP 1.
+        response = self._login(
+            self.USER_1,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_1
+        )
+        self.assertEqual(response.status_code, self.BLOCKED)
+
+    def test_lockout_by_ip_allows_when_same_user_diff_ip_using_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 1 can still login from IP 2.
+        response = self._login(
+            self.USER_1,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_2
+        )
+        self.assertEqual(response.status_code, self.ALLOWED)
+
+    def test_lockout_by_ip_blocks_when_diff_user_same_ip_using_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 2 is also locked out from IP 1.
+        response = self._login(
+            self.USER_2,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_1
+        )
+        self.assertEqual(response.status_code, self.BLOCKED)
+
+    def test_lockout_by_ip_allows_when_diff_user_diff_ip_using_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 2 can still login from IP 2.
+        response = self._login(
+            self.USER_2,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_2
+        )
+        self.assertEqual(response.status_code, self.ALLOWED)
+
+    @override_settings(AXES_ONLY_USER_FAILURES=True)
+    def test_lockout_by_user_with_empty_username_allows_other_users_using_cache(self):
+        # User with empty username is locked out from IP 1.
+        self._lockout_user_from_ip(username='', ip_addr=self.IP_1)
+
+        # Still possible to access the login page
+        response = self.client.get(reverse('admin:login'), REMOTE_ADDR=self.IP_1)
+        self.assertContains(response, self.LOGIN_FORM_KEY, status_code=200, html=True)
+
+    # Test for true and false positives when blocking by user only.
+    # With cache enabled. When AXES_ONLY_USER_FAILURES = True
+    @override_settings(AXES_ONLY_USER_FAILURES=True)
+    def test_lockout_by_user_blocks_when_same_user_same_ip_using_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 1 is still blocked from IP 1.
+        response = self._login(
+            self.USER_1,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_1
+        )
+        self.assertEqual(response.status_code, self.BLOCKED)
+
+    @override_settings(AXES_ONLY_USER_FAILURES=True)
+    def test_lockout_by_user_blocks_when_same_user_diff_ip_using_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 1 is also locked out from IP 2.
+        response = self._login(
+            self.USER_1,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_2
+        )
+        self.assertEqual(response.status_code, self.BLOCKED)
+
+    @override_settings(AXES_ONLY_USER_FAILURES=True)
+    def test_lockout_by_user_allows_when_diff_user_same_ip_using_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 2 can still login from IP 1.
+        response = self._login(
+            self.USER_2,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_1
+        )
+        self.assertEqual(response.status_code, self.ALLOWED)
+
+    @override_settings(AXES_ONLY_USER_FAILURES=True)
+    def test_lockout_by_user_allows_when_diff_user_diff_ip_using_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 2 can still login from IP 2.
+        response = self._login(
+            self.USER_2,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_2
+        )
+        self.assertEqual(response.status_code, self.ALLOWED)
+
+    # Test for true and false positives when blocking by user and IP together.
+    # With cache enabled. When LOCK_OUT_BY_COMBINATION_USER_AND_IP = True
+    @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
+    def test_lockout_by_user_and_ip_blocks_when_same_user_same_ip_using_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 1 is still blocked from IP 1.
+        response = self._login(
+            self.USER_1,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_1
+        )
+        self.assertEqual(response.status_code, self.BLOCKED)
+
+    @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
+    def test_lockout_by_user_and_ip_allows_when_same_user_diff_ip_using_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 1 can still login from IP 2.
+        response = self._login(
+            self.USER_1,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_2
+        )
+        self.assertEqual(response.status_code, self.ALLOWED)
+
+    @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
+    def test_lockout_by_user_and_ip_allows_when_diff_user_same_ip_using_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 2 can still login from IP 1.
+        response = self._login(
+            self.USER_2,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_1
+        )
+        self.assertEqual(response.status_code, self.ALLOWED)
+
+    @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
+    def test_lockout_by_user_and_ip_allows_when_diff_user_diff_ip_using_cache(self):
+        # User 1 is locked out from IP 1.
+        self._lockout_user1_from_ip1()
+
+        # User 2 can still login from IP 2.
+        response = self._login(
+            self.USER_2,
+            self.VALID_PASSWORD,
+            ip_addr=self.IP_2
+        )
+        self.assertEqual(response.status_code, self.ALLOWED)
+
+    @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
+    def test_lockout_by_user_and_ip_with_empty_username_allows_other_users_using_cache(self):
+        # User with empty username is locked out from IP 1.
+        self._lockout_user_from_ip(username='', ip_addr=self.IP_1)
+
+        # Still possible to access the login page
+        response = self.client.get(reverse('admin:login'), REMOTE_ADDR=self.IP_1)
+        self.assertContains(response, self.LOGIN_FORM_KEY, status_code=200, html=True)

+ 27 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/tests/test_models.py

@@ -0,0 +1,27 @@
+from django.test import TestCase
+
+
+class MigrationsCheck(TestCase):
+    def setUp(self):
+        from django.utils import translation
+        self.saved_locale = translation.get_language()
+        translation.deactivate_all()
+
+    def tearDown(self):
+        if self.saved_locale is not None:
+            from django.utils import translation
+            translation.activate(self.saved_locale)
+
+    def test_missing_migrations(self):
+        from django.db import connection
+        from django.apps.registry import apps
+        from django.db.migrations.executor import MigrationExecutor
+        executor = MigrationExecutor(connection)
+        from django.db.migrations.autodetector import MigrationAutodetector
+        from django.db.migrations.state import ProjectState
+        autodetector = MigrationAutodetector(
+            executor.loader.project_state(),
+            ProjectState.from_apps(apps),
+        )
+        changes = autodetector.changes(graph=executor.loader.graph)
+        self.assertEqual({}, changes)

+ 239 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/tests/test_utils.py

@@ -0,0 +1,239 @@
+from __future__ import unicode_literals
+
+import datetime
+
+from django.http import HttpRequest
+from django.test import TestCase, override_settings
+from django.utils import six
+
+from axes.utils import iso8601, is_ipv6, get_client_str, get_client_username
+
+
+class UtilsTest(TestCase):
+    def test_iso8601(self):
+        """Tests iso8601 correctly translates datetime.timdelta to ISO 8601
+        formatted duration."""
+        EXPECTED = {
+            datetime.timedelta(days=1, hours=25, minutes=42, seconds=8):
+                'P2DT1H42M8S',
+            datetime.timedelta(days=7, seconds=342):
+                'P7DT5M42S',
+            datetime.timedelta(days=0, hours=2, minutes=42):
+                'PT2H42M',
+            datetime.timedelta(hours=20, seconds=42):
+                'PT20H42S',
+            datetime.timedelta(seconds=300):
+                'PT5M',
+            datetime.timedelta(seconds=9005):
+                'PT2H30M5S',
+            datetime.timedelta(minutes=9005):
+                'P6DT6H5M',
+            datetime.timedelta(days=15):
+                'P15D'
+        }
+        for timedelta, iso_duration in six.iteritems(EXPECTED):
+            self.assertEqual(iso8601(timedelta), iso_duration)
+
+    def test_is_ipv6(self):
+        self.assertTrue(is_ipv6('ff80::220:16ff:fec9:1'))
+        self.assertFalse(is_ipv6('67.255.125.204'))
+        self.assertFalse(is_ipv6('foo'))
+
+    @override_settings(AXES_VERBOSE=True)
+    def test_verbose_ip_only_client_details(self):
+        username = 'test@example.com'
+        ip = '127.0.0.1'
+        user_agent = 'Googlebot/2.1 (+http://www.googlebot.com/bot.html)'
+        path_info = '/admin/'
+
+        details = "{{user: '{0}', ip: '{1}', user-agent: '{2}', path: '{3}'}}"
+        expected = details.format(username, ip, user_agent, path_info)
+        actual = get_client_str(username, ip, user_agent, path_info)
+
+        self.assertEqual(expected, actual)
+
+    @override_settings(AXES_VERBOSE=False)
+    def test_non_verbose_ip_only_client_details(self):
+        username = 'test@example.com'
+        ip = '127.0.0.1'
+        user_agent = 'Googlebot/2.1 (+http://www.googlebot.com/bot.html)'
+        path_info = '/admin/'
+
+        expected = ip
+        actual = get_client_str(username, ip, user_agent, path_info)
+
+        self.assertEqual(expected, actual)
+
+    @override_settings(AXES_ONLY_USER_FAILURES=True)
+    @override_settings(AXES_VERBOSE=True)
+    def test_verbose_user_only_client_details(self):
+        username = 'test@example.com'
+        ip = '127.0.0.1'
+        user_agent = 'Googlebot/2.1 (+http://www.googlebot.com/bot.html)'
+        path_info = '/admin/'
+
+        details = "{{user: '{0}', ip: '{1}', user-agent: '{2}', path: '{3}'}}"
+        expected = details.format(username, ip, user_agent, path_info)
+        actual = get_client_str(username, ip, user_agent, path_info)
+
+        self.assertEqual(expected, actual)
+
+    @override_settings(AXES_ONLY_USER_FAILURES=True)
+    @override_settings(AXES_VERBOSE=False)
+    def test_non_verbose_user_only_client_details(self):
+        username = 'test@example.com'
+        ip = '127.0.0.1'
+        user_agent = 'Googlebot/2.1 (+http://www.googlebot.com/bot.html)'
+        path_info = '/admin/'
+
+        expected = username
+        actual = get_client_str(username, ip, user_agent, path_info)
+
+        self.assertEqual(expected, actual)
+
+    @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
+    @override_settings(AXES_VERBOSE=True)
+    def test_verbose_user_ip_combo_client_details(self):
+        username = 'test@example.com'
+        ip = '127.0.0.1'
+        user_agent = 'Googlebot/2.1 (+http://www.googlebot.com/bot.html)'
+        path_info = '/admin/'
+
+        details = "{{user: '{0}', ip: '{1}', user-agent: '{2}', path: '{3}'}}"
+        expected = details.format(username, ip, user_agent, path_info)
+        actual = get_client_str(username, ip, user_agent, path_info)
+
+        self.assertEqual(expected, actual)
+
+    @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
+    @override_settings(AXES_VERBOSE=False)
+    def test_non_verbose_user_ip_combo_client_details(self):
+        username = 'test@example.com'
+        ip = '127.0.0.1'
+        user_agent = 'Googlebot/2.1 (+http://www.googlebot.com/bot.html)'
+        path_info = '/admin/'
+
+        expected = '{0} from {1}'.format(username, ip)
+        actual = get_client_str(username, ip, user_agent, path_info)
+
+        self.assertEqual(expected, actual)
+
+    @override_settings(AXES_USE_USER_AGENT=True)
+    @override_settings(AXES_VERBOSE=True)
+    def test_verbose_user_agent_client_details(self):
+        username = 'test@example.com'
+        ip = '127.0.0.1'
+        user_agent = 'Googlebot/2.1 (+http://www.googlebot.com/bot.html)'
+        path_info = '/admin/'
+
+        details = "{{user: '{0}', ip: '{1}', user-agent: '{2}', path: '{3}'}}"
+        expected = details.format(username, ip, user_agent, path_info)
+        actual = get_client_str(username, ip, user_agent, path_info)
+
+        self.assertEqual(expected, actual)
+
+    @override_settings(AXES_USE_USER_AGENT=True)
+    @override_settings(AXES_VERBOSE=False)
+    def test_non_verbose_user_agent_client_details(self):
+        username = 'test@example.com'
+        ip = '127.0.0.1'
+        user_agent = 'Googlebot/2.1 (+http://www.googlebot.com/bot.html)'
+        path_info = '/admin/'
+
+        expected = ip + '(user-agent={0})'.format(user_agent)
+        actual = get_client_str(username, ip, user_agent, path_info)
+
+        self.assertEqual(expected, actual)
+
+    @override_settings(AXES_USERNAME_FORM_FIELD='username')
+    def test_default_get_client_username(self):
+        expected = 'test-username'
+
+        request = HttpRequest()
+        request.POST['username'] = expected
+
+        actual = get_client_username(request)
+
+        self.assertEqual(expected, actual)
+
+    @override_settings(AXES_USERNAME_FORM_FIELD='username')
+    def test_default_get_client_username_credentials(self):
+        expected = 'test-username'
+        expected_in_credentials = 'test-credentials-username'
+
+        request = HttpRequest()
+        request.POST['username'] = expected
+        credentials = {
+            'username': expected_in_credentials
+        }
+
+        actual = get_client_username(request, credentials)
+
+        self.assertEqual(expected_in_credentials, actual)
+
+    def sample_customize_username(request):
+        return 'prefixed-' + request.POST.get('username')
+
+    @override_settings(AXES_USERNAME_FORM_FIELD='username')
+    @override_settings(AXES_USERNAME_CALLABLE=sample_customize_username)
+    def test_custom_get_client_username(self):
+        provided = 'test-username'
+        expected = 'prefixed-' + provided
+
+        request = HttpRequest()
+        request.POST['username'] = provided
+
+        actual = get_client_username(request)
+
+        self.assertEqual(expected, actual)
+
+    @override_settings(AXES_USERNAME_FORM_FIELD='username')
+    @override_settings(AXES_USERNAME_CALLABLE=sample_customize_username)
+    def test_custom_get_client_username_from_request(self):
+        provided = 'test-username'
+        expected = 'prefixed-' + provided
+        provided_in_credentials = 'test-credentials-username'
+        expected_in_credentials = 'prefixed-' + provided_in_credentials
+
+        request = HttpRequest()
+        request.POST['username'] = provided
+        credentials = {'username': provided_in_credentials}
+
+        actual = get_client_username(request, credentials)
+
+        self.assertEqual(expected, actual)
+
+    def sample_customize_username_credentials(request, credentials):
+        return 'prefixed-' + credentials.get('username')
+
+    @override_settings(AXES_USERNAME_FORM_FIELD='username')
+    @override_settings(AXES_USERNAME_CALLABLE=sample_customize_username_credentials)
+    def test_custom_get_client_username_from_credentials(self):
+        provided = 'test-username'
+        expected = 'prefixed-' + provided
+        provided_in_credentials = 'test-credentials-username'
+        expected_in_credentials = 'prefixed-' + provided_in_credentials
+
+        request = HttpRequest()
+        request.POST['username'] = provided
+        credentials = {'username': provided_in_credentials}
+
+        actual = get_client_username(request, credentials)
+
+        self.assertEqual(expected_in_credentials, actual)
+
+    def sample_get_client_username_too_few_arguments():
+        pass
+
+    @override_settings(AXES_USERNAME_CALLABLE=sample_get_client_username_too_few_arguments)
+    def test_get_client_username_too_few_arguments_invalid_callable(self):
+        with self.assertRaises(TypeError):
+            actual = get_client_username(HttpRequest(), {})
+
+    def sample_get_client_username_too_many_arguments(one, two, three):
+        pass
+
+    @override_settings(AXES_USERNAME_CALLABLE=sample_get_client_username_too_many_arguments)
+    def test_get_client_username_too_many_arguments_invalid_callable(self):
+        with self.assertRaises(TypeError):
+            actual = get_client_username(HttpRequest(), {})

+ 167 - 0
desktop/core/ext-py/django-axes-4.5.4/axes/utils.py

@@ -0,0 +1,167 @@
+from __future__ import unicode_literals
+
+try:
+    import win_inet_pton  # pylint: disable=unused-import
+except ImportError:
+    pass
+
+from inspect import getargspec
+from logging import getLogger
+from socket import error, inet_pton, AF_INET6
+
+from django.core.cache import caches
+from django.utils import six
+
+import ipware.ip2
+
+from axes.conf import settings
+from axes.models import AccessAttempt
+
+logger = getLogger(__name__)
+
+def get_axes_cache():
+    return caches[getattr(settings, 'AXES_CACHE', 'default')]
+
+
+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
+    via excessively large payloads.
+    """
+    return '\n'.join([
+        '%s=%s' % (k, v) for k, v in six.iteritems(items)
+        if k != settings.AXES_PASSWORD_FORM_FIELD
+    ][:int(max_length / 2)])[:max_length]
+
+
+def get_client_str(username, ip_address, user_agent=None, path_info=None):
+    if settings.AXES_VERBOSE:
+        if isinstance(path_info, tuple):
+            path_info = path_info[0]
+        details = "{{user: '{0}', ip: '{1}', user-agent: '{2}', path: '{3}'}}"
+        return details.format(username, ip_address, user_agent, path_info)
+
+    if settings.AXES_ONLY_USER_FAILURES:
+        client = username
+    elif settings.AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP:
+        client = '{0} from {1}'.format(username, ip_address)
+    else:
+        client = ip_address
+
+    if settings.AXES_USE_USER_AGENT:
+        client += '(user-agent={0})'.format(user_agent)
+
+    return client
+
+
+def get_client_ip(request):
+    client_ip_attribute = 'axes_client_ip'
+
+    if not hasattr(request, client_ip_attribute):
+        client_ip, _ = ipware.ip2.get_client_ip(
+            request,
+            proxy_order=settings.AXES_PROXY_ORDER,
+            proxy_count=settings.AXES_PROXY_COUNT,
+            proxy_trusted_ips=settings.AXES_PROXY_TRUSTED_IPS,
+            request_header_order=settings.AXES_META_PRECEDENCE_ORDER,
+        )
+        setattr(request, client_ip_attribute, client_ip)
+    return getattr(request, client_ip_attribute)
+
+
+def get_client_username(request, credentials=None):
+    """Resolve client username from the given request or credentials if supplied
+
+    The order of preference for fetching the username is as follows:
+
+    1. If configured, use `AXES_USERNAME_CALLABLE`, and supply either `request` or `request, credentials` as arguments
+       depending on the function argument count (multiple signatures are supported for backwards compatibility)
+    2. If given, use `credentials` and fetch username from `AXES_USERNAME_FORM_FIELD` (defaults to `username`)
+    3. Use request.POST and fetch username from `AXES_USERNAME_FORM_FIELD` (defaults to `username`)
+
+    :param request: incoming Django `HttpRequest` or similar object from authentication backend or other source
+    :param credentials: incoming credentials `dict` or similar object from authentication backend or other source
+    """
+
+    if settings.AXES_USERNAME_CALLABLE:
+        num_args = len(
+            getargspec(settings.AXES_USERNAME_CALLABLE).args  # pylint: disable=deprecated-method
+        )
+
+        if num_args == 2:
+            logger.debug('Using AXES_USERNAME_CALLABLE for username with two arguments: request, credentials')
+            return settings.AXES_USERNAME_CALLABLE(request, credentials)
+
+        if num_args == 1:
+            logger.debug('Using AXES_USERNAME_CALLABLE for username with one argument: request')
+            return settings.AXES_USERNAME_CALLABLE(request)
+
+        logger.error('Using AXES_USERNAME_CALLABLE for username failed: wrong number of arguments %s', num_args)
+        raise TypeError('Wrong number of arguments in function call to AXES_USERNAME_CALLABLE', num_args)
+
+    if credentials:
+        logger.debug('Using `credentials` to get username with key AXES_USERNAME_FORM_FIELD')
+        return credentials.get(settings.AXES_USERNAME_FORM_FIELD, None)
+
+    logger.debug('Using `request.POST` to get username with key AXES_USERNAME_FORM_FIELD')
+    return request.POST.get(settings.AXES_USERNAME_FORM_FIELD, None)
+
+
+def get_credentials(username=None, **kwargs):
+    credentials = {settings.AXES_USERNAME_FORM_FIELD: username}
+    credentials.update(kwargs)
+    return credentials
+
+
+def is_ipv6(ip):
+    try:
+        inet_pton(AF_INET6, ip)
+    except (OSError, error):
+        return False
+    return True
+
+
+def reset(ip=None, username=None):
+    """Reset records that match ip or username, and
+    return the count of removed attempts.
+    """
+
+    attempts = AccessAttempt.objects.all()
+    if ip:
+        attempts = attempts.filter(ip_address=ip)
+    if username:
+        attempts = attempts.filter(username=username)
+
+    count, _ = attempts.delete()
+
+    return count
+
+
+def iso8601(timestamp):
+    """Returns datetime.timedelta translated to ISO 8601 formatted duration.
+    """
+    seconds = timestamp.total_seconds()
+    minutes, seconds = divmod(seconds, 60)
+    hours, minutes = divmod(minutes, 60)
+    days, hours = divmod(hours, 24)
+
+    date = '{:.0f}D'.format(days) if days else ''
+
+    time_values = hours, minutes, seconds
+    time_designators = 'H', 'M', 'S'
+
+    time = ''.join([
+        ('{:.0f}'.format(value) + designator)
+        for value, designator in zip(time_values, time_designators)
+        if value]
+    )
+    return 'P' + date + ('T' + time if time else '')
+
+
+def get_lockout_message():
+    if settings.AXES_COOLOFF_TIME:
+        return settings.AXES_COOLOFF_MESSAGE
+    return settings.AXES_PERMALOCK_MESSAGE

+ 0 - 1
desktop/core/ext-py/django-axes-2.2.0/setup.cfg → desktop/core/ext-py/django-axes-4.5.4/setup.cfg

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

+ 22 - 4
desktop/core/ext-py/django-axes-2.2.0/setup.py → desktop/core/ext-py/django-axes-4.5.4/setup.py

@@ -1,5 +1,8 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
 import codecs
 from setuptools import setup, find_packages
 
@@ -8,31 +11,46 @@ from axes import get_version
 setup(
     name='django-axes',
     version=get_version(),
-    description="Keep track of failed login attempts in Django-powered sites.",
+    description='Keep track of failed login attempts in Django-powered sites.',
     long_description=(
-        codecs.open("README.rst", encoding='utf-8').read() + '\n' +
-        codecs.open("CHANGES.txt", encoding='utf-8').read()),
+        codecs.open('README.rst', encoding='utf-8').read() + '\n' +
+        codecs.open('CHANGES.txt', encoding='utf-8').read()),
     keywords='authentication django pci security'.split(),
     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',
+    url='https://github.com/jazzband/django-axes',
     license='MIT',
     package_dir={'axes': 'axes'},
+    install_requires=[
+        'pytz',
+        'django',
+        'django-appconf',
+        'django-ipware>=2.0.2',
+        'win_inet_pton ; python_version < "3.4" and sys_platform == "win32"',
+    ],
     include_package_data=True,
     packages=find_packages(),
     classifiers=[
         'Development Status :: 5 - Production/Stable',
         'Environment :: Web Environment',
         'Framework :: Django',
+        'Framework :: Django :: 1.11',
+        'Framework :: Django :: 2.0',
+        'Framework :: Django :: 2.1',
         'Intended Audience :: Developers',
         'Intended Audience :: System Administrators',
         'License :: OSI Approved :: MIT License',
         'Operating System :: OS Independent',
         'Programming Language :: Python',
+        'Programming Language :: Python :: 2',
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
         'Topic :: Internet :: Log Analysis',
         'Topic :: Security',
         'Topic :: System :: Logging',

+ 22 - 0
desktop/core/ext-py/django-ipware-2.1.0/PKG-INFO

@@ -0,0 +1,22 @@
+Metadata-Version: 1.1
+Name: django-ipware
+Version: 2.1.0
+Summary: A Django utility application that returns client's real IP address
+Home-page: https://github.com/un33k/django-ipware
+Author: Val Neekman
+Author-email: info@neekware.com
+License: MIT
+Description-Content-Type: UNKNOWN
+Description: UNKNOWN
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+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.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Topic :: Utilities

+ 224 - 0
desktop/core/ext-py/django-ipware-2.1.0/README.md

@@ -0,0 +1,224 @@
+Django IPware
+====================
+
+**A Django application to retrieve client's IP address**
+
+[![status-image]][status-link]
+[![version-image]][version-link]
+[![coverage-image]][coverage-link]
+
+Overview
+====================
+
+**Best attempt** to get client's IP address while keeping it **DRY**.
+
+Notice
+====================
+
+There is not a good `out-of-the-box` solution against fake IP addresses, aka `IP Address Spoofing`.
+You are encouraged to read the ([Advanced users](README.md#advanced-users)) section of this page and
+use `trusted_proxies_ips` and/or `proxy_count` features to match your needs, especially `if` you are
+planning to include `ipware` in any authentication, security or `anti-fraud` related architecture.
+
+
+How to install
+====================
+
+    1. easy_install django-ipware
+    2. pip install django-ipware
+    3. git clone http://github.com/un33k/django-ipware
+        a. cd django-ipware
+        b. run python setup.py install
+    4. wget https://github.com/un33k/django-ipware/zipball/master
+        a. unzip the downloaded file
+        b. cd into django-ipware-* directory
+        c. run python setup.py install
+
+
+How to use
+====================
+
+   ```python
+    # In a view or a middleware where the `request` object is available
+
+    from ipware import get_client_ip
+    client_ip, is_routable = get_client_ip(request)
+    if client_ip is None:
+       # Unable to get the client's IP address
+    else:
+        # We got the client's IP address
+        if is_routable:
+            # The client's IP address is publicly routable on the Internet
+        else:
+            # The client's IP address is private
+
+    # Order of precedence is (Public, Private, Loopback, None)
+   ```
+
+
+Advanced users:
+====================
+
+- ### Precedence Order
+The default meta precedence order is top to bottom.  However, you may customize the order
+by providing your own `IPWARE_META_PRECEDENCE_ORDER` by adding it to your project's settings.py
+
+   ```python
+    # The default meta precedence order
+    IPWARE_META_PRECEDENCE_ORDER = (
+        'HTTP_X_FORWARDED_FOR', 'X_FORWARDED_FOR',  # <client>, <proxy1>, <proxy2>
+        'HTTP_CLIENT_IP',
+        'HTTP_X_REAL_IP',
+        'HTTP_X_FORWARDED',
+        'HTTP_X_CLUSTER_CLIENT_IP',
+        'HTTP_FORWARDED_FOR',
+        'HTTP_FORWARDED',
+        'HTTP_VIA',
+        'REMOTE_ADDR',
+    )
+   ```
+**Alternativley**, you can provide your custom `request header meta precedence order` when calling `get_client_ip()`.
+```python
+get_client_ip(request, request_header_order=['X_FORWARDED_FOR'])
+get_client_ip(request, request_header_order=['X_FORWARDED_FOR', 'HTTP_X_FORWARDED_FOR'])
+```
+
+- ### Private Prefixes
+
+You may customize the prefixes to indicate an IP addresses private. This is done by adding your
+own `IPWARE_PRIVATE_IP_PREFIX` to your project's settings.py.  IP addresses matching the following
+prefixes are considered `private` & are **not** publicly routable.
+
+   ```python
+   # The default private IP prefixes
+   IPWARE_PRIVATE_IP_PREFIX = getattr(settings,
+      'IPWARE_PRIVATE_IP_PREFIX', (
+        '0.',  # messages to software
+        '10.',  # class A private block
+        '100.64.',  '100.65.',  '100.66.',  '100.67.',  '100.68.',  '100.69.',
+        '100.70.',  '100.71.',  '100.72.',  '100.73.',  '100.74.',  '100.75.',
+        '100.76.',  '100.77.',  '100.78.',  '100.79.',  '100.80.',  '100.81.',
+        '100.82.',  '100.83.',  '100.84.',  '100.85.',  '100.86.',  '100.87.',
+        '100.88.',  '100.89.',  '100.90.',  '100.91.',  '100.92.',  '100.93.',
+        '100.94.',  '100.95.',  '100.96.',  '100.97.',  '100.98.',  '100.99.',
+        '100.100.', '100.101.', '100.102.', '100.103.', '100.104.', '100.105.',
+        '100.106.', '100.107.', '100.108.', '100.109.', '100.110.', '100.111.',
+        '100.112.', '100.113.', '100.114.', '100.115.', '100.116.', '100.117.',
+        '100.118.', '100.119.', '100.120.', '100.121.', '100.122.', '100.123.',
+        '100.124.', '100.125.', '100.126.', '100.127.',  # carrier-grade NAT
+        '169.254.',  # link-local block
+        '172.16.', '172.17.', '172.18.', '172.19.',
+        '172.20.', '172.21.', '172.22.', '172.23.',
+        '172.24.', '172.25.', '172.26.', '172.27.',
+        '172.28.', '172.29.', '172.30.', '172.31.',  # class B private blocks
+        '192.0.0.',  # reserved for IANA special purpose address registry
+        '192.0.2.',  # reserved for documentation and example code
+        '192.168.',  # class C private block
+        '198.18.', '198.19.',  # reserved for inter-network communications between two separate subnets
+        '198.51.100.',  # reserved for documentation and example code
+        '203.0.113.',  # reserved for documentation and example code
+        '224.', '225.', '226.', '227.', '228.', '229.', '230.', '231.', '232.',
+        '233.', '234.', '235.', '236.', '237.', '238.', '239.',  # multicast
+        '240.', '241.', '242.', '243.', '244.', '245.', '246.', '247.', '248.',
+        '249.', '250.', '251.', '252.', '253.', '254.', '255.',  # reserved
+      ) + (
+        '::',  # Unspecified address
+        '::ffff:', '2001:10:', '2001:20:'  # messages to software
+        '2001::',  # TEREDO
+        '2001:2::',  # benchmarking
+        '2001:db8:',  # reserved for documentation and example code
+        'fc00:',  # IPv6 private block
+        'fe80:',  # link-local unicast
+        'ff00:',  # IPv6 multicast
+      )
+  )
+  ```
+
+- ### Trusted Proxies
+
+If your Django server is behind one or more known proxy server(s), you can filter out unwanted requests
+by providing the `trusted` proxy list when calling `get_client_ip(request, proxy_trusted_ips=['177.139.233.133'])`.
+In the following example, your load balancer (LB) can be seen as a `trusted` proxy.
+
+   ```
+    `Real` Client  <public> <---> <public> LB (Server) <private> <--------> <private> Django Server
+                                                                      ^
+                                                                      |
+    `Fake` Client  <private> <---> <private> LB (Server) <private> ---^
+   ```
+
+
+   ```python
+   # In the above scenario, use your load balancer's IP address as a way to filter out unwanted requests.
+   client_ip, is_routable = get_client_ip(request, proxy_trusted_ips=['177.139.233.133'])
+
+   # If you have multiple proxies, simply add them to the list
+   client_ip, is_routable = get_client_ip(request, proxy_trusted_ips=['177.139.233.133', '177.139.233.134'])
+
+   # For proxy servers with fixed sub-domain and dynamic IP, use the following pattern.
+   client_ip, is_routable = get_client_ip(request, proxy_trusted_ips=['177.139.', '177.140'])
+   client_ip, is_routable = get_client_ip(request, proxy_trusted_ips=['177.139.233.', '177.139.240'])
+   ```
+
+- ### Proxy Count
+
+If your Django server is behind a `known` number of proxy server(s), you can filter out unwanted requests
+by providing the `number` of proxies when calling `get_client_ip(request, proxy_count=1)`.
+In the following example, your load balancer (LB) can be seen as the `only` proxy.
+
+   ```
+    `Real` Client  <public> <---> <public> LB (Server) <private> <--------> <private> Django Server
+                                                                      ^
+                                                                      |
+                                          `Fake` Client  <private> ---^
+   ```
+
+   ```python
+   # In the above scenario, the total number of proxies can be used as a way to filter out unwanted requests.
+   client_ip, is_routable = get_client_ip(request, proxy_count=1)
+
+   # The above may be very useful in cases where your proxy server's IP address is assigned dynamically.
+   # However, If you have the proxy IP address, you can use it in combination to the proxy count.
+   client_ip, is_routable = get_client_ip(request, proxy_count=1, proxy_trusted_ips=['177.139.233.133'])
+   ```
+
+- ### Originating Request
+
+If your proxy server is configured such that the right most IP address is that of the originating client, you
+can indicate `right-most` as your `proxy_order` when calling `get_client_ip(request, proxy_order="right-most")`.
+Please note that the [de-facto](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) standard
+for the originating client IP address is  the `left-most` as per `<client>, <proxy1>, <proxy2>`.
+
+Running the tests
+====================
+
+To run the tests against the current environment:
+
+    python manage.py test
+
+
+License
+====================
+
+Released under a ([MIT](LICENSE)) license.
+
+
+Version
+====================
+X.Y.Z Version
+
+    `MAJOR` version -- when you make incompatible API changes,
+    `MINOR` version -- when you add functionality in a backwards-compatible manner, and
+    `PATCH` version -- when you make backwards-compatible bug fixes.
+
+[status-image]: https://secure.travis-ci.org/un33k/django-ipware.png?branch=master
+[status-link]: http://travis-ci.org/un33k/django-ipware?branch=master
+
+[version-image]: https://img.shields.io/pypi/v/django-ipware.svg
+[version-link]: https://pypi.python.org/pypi/django-ipware
+
+[coverage-image]: https://coveralls.io/repos/un33k/django-ipware/badge.svg
+[coverage-link]: https://coveralls.io/r/un33k/django-ipware
+
+[download-image]: https://img.shields.io/pypi/dm/django-ipware.svg
+[download-link]: https://pypi.python.org/pypi/django-ipware

+ 8 - 0
desktop/core/ext-py/django-ipware-2.1.0/ipware/__init__.py

@@ -0,0 +1,8 @@
+from .ip2 import get_client_ip
+
+
+default_app_config = 'ipware.apps.IPwareConfig'
+
+__author__ = 'Val Neekman @ Neekware Inc. [@vneekman]'
+__description__ = "A Django application to retrieve user's IP address"
+__version__ = '2.1.0'

+ 10 - 0
desktop/core/ext-py/django-ipware-2.1.0/ipware/apps.py

@@ -0,0 +1,10 @@
+from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
+
+
+class IPwareConfig(AppConfig):
+    """
+    Configuration entry point for the ipware app
+    """
+    label = name = 'ipware'
+    verbose_name = _("ipware app")

+ 78 - 0
desktop/core/ext-py/django-ipware-2.1.0/ipware/defaults.py

@@ -0,0 +1,78 @@
+from django.conf import settings
+
+
+# Search for the real IP address in the following order
+# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
+# X-Forwarded-For: <client>, <proxy1>, <proxy2>
+# Configurable via settings.py
+IPWARE_META_PRECEDENCE_ORDER = getattr(settings,
+    'IPWARE_META_PRECEDENCE_ORDER', (
+        'HTTP_X_FORWARDED_FOR', 'X_FORWARDED_FOR',
+        'HTTP_CLIENT_IP',
+        'HTTP_X_REAL_IP',
+        'HTTP_X_FORWARDED',
+        'HTTP_X_CLUSTER_CLIENT_IP',
+        'HTTP_FORWARDED_FOR',
+        'HTTP_FORWARDED',
+        'HTTP_VIA',
+        'REMOTE_ADDR',
+    )
+)
+
+# Private IP addresses
+# http://en.wikipedia.org/wiki/List_of_assigned_/8_IPv4_address_blocks
+# https://en.wikipedia.org/wiki/Reserved_IP_addresses
+# https://www.ietf.org/rfc/rfc1112.txt (IPv4 multicast)
+# http://www.ietf.org/rfc/rfc3330.txt (IPv4)
+# http://www.ietf.org/rfc/rfc5156.txt (IPv6)
+# https://www.ietf.org/rfc/rfc6890.txt
+# Regex would be ideal here, but this is keeping it simple
+# Configurable via settings.py
+IPWARE_PRIVATE_IP_PREFIX = getattr(settings,
+    'IPWARE_PRIVATE_IP_PREFIX', (
+        '0.',  # messages to software
+        '10.',  # class A private block
+        '100.64.',  '100.65.',  '100.66.',  '100.67.',  '100.68.',  '100.69.',
+        '100.70.',  '100.71.',  '100.72.',  '100.73.',  '100.74.',  '100.75.',
+        '100.76.',  '100.77.',  '100.78.',  '100.79.',  '100.80.',  '100.81.',
+        '100.82.',  '100.83.',  '100.84.',  '100.85.',  '100.86.',  '100.87.',
+        '100.88.',  '100.89.',  '100.90.',  '100.91.',  '100.92.',  '100.93.',
+        '100.94.',  '100.95.',  '100.96.',  '100.97.',  '100.98.',  '100.99.',
+        '100.100.', '100.101.', '100.102.', '100.103.', '100.104.', '100.105.',
+        '100.106.', '100.107.', '100.108.', '100.109.', '100.110.', '100.111.',
+        '100.112.', '100.113.', '100.114.', '100.115.', '100.116.', '100.117.',
+        '100.118.', '100.119.', '100.120.', '100.121.', '100.122.', '100.123.',
+        '100.124.', '100.125.', '100.126.', '100.127.',  # carrier-grade NAT
+        '169.254.',  # link-local block
+        '172.16.', '172.17.', '172.18.', '172.19.',
+        '172.20.', '172.21.', '172.22.', '172.23.',
+        '172.24.', '172.25.', '172.26.', '172.27.',
+        '172.28.', '172.29.', '172.30.', '172.31.',  # class B private blocks
+        '192.0.0.',  # reserved for IANA special purpose address registry
+        '192.0.2.',  # reserved for documentation and example code
+        '192.168.',  # class C private block
+        '198.18.', '198.19.',  # reserved for inter-network communications between two separate subnets
+        '198.51.100.',  # reserved for documentation and example code
+        '203.0.113.',  # reserved for documentation and example code
+        '224.', '225.', '226.', '227.', '228.', '229.', '230.', '231.', '232.',
+        '233.', '234.', '235.', '236.', '237.', '238.', '239.',  # multicast
+        '240.', '241.', '242.', '243.', '244.', '245.', '246.', '247.', '248.',
+        '249.', '250.', '251.', '252.', '253.', '254.', '255.',  # reserved
+    ) + (
+        '::',  # Unspecified address
+        '::ffff:', '2001:10:', '2001:20:'  # messages to software
+        '2001::',  # TEREDO
+        '2001:2::',  # benchmarking
+        '2001:db8:',  # reserved for documentation and example code
+        'fc00:',  # IPv6 private block
+        'fe80:',  # link-local unicast
+        'ff00:',  # IPv6 multicast
+    )
+)
+
+IPWARE_LOOPBACK_PREFIX = (
+    '127.',  # IPv4 loopback device (Host)
+    '::1',  # IPv6 loopback device (Host)
+)
+
+IPWARE_NON_PUBLIC_IP_PREFIX = IPWARE_PRIVATE_IP_PREFIX + IPWARE_LOOPBACK_PREFIX

+ 60 - 0
desktop/core/ext-py/django-ipware-2.1.0/ipware/ip.py

@@ -0,0 +1,60 @@
+from django.conf import settings
+
+from .utils import is_valid_ip
+from . import defaults as defs
+
+NON_PUBLIC_IP_PREFIX = tuple([ip.lower() for ip in defs.IPWARE_NON_PUBLIC_IP_PREFIX])
+TRUSTED_PROXY_LIST = tuple([ip.lower() for ip in getattr(settings, 'IPWARE_TRUSTED_PROXY_LIST', [])])
+
+
+def get_ip(request, real_ip_only=False, right_most_proxy=False):
+    """
+    Returns client's best-matched ip-address, or None
+    @deprecated - Do not edit
+    """
+    best_matched_ip = None
+    for key in defs.IPWARE_META_PRECEDENCE_ORDER:
+        value = request.META.get(key, request.META.get(key.replace('_', '-'), '')).strip()
+        if value is not None and value != '':
+            ips = [ip.strip().lower() for ip in value.split(',')]
+            if right_most_proxy and len(ips) > 1:
+                ips = reversed(ips)
+            for ip_str in ips:
+                if ip_str and is_valid_ip(ip_str):
+                    if not ip_str.startswith(NON_PUBLIC_IP_PREFIX):
+                        return ip_str
+                    if not real_ip_only:
+                        loopback = defs.IPWARE_LOOPBACK_PREFIX
+                        if best_matched_ip is None:
+                            best_matched_ip = ip_str
+                        elif best_matched_ip.startswith(loopback) and not ip_str.startswith(loopback):
+                            best_matched_ip = ip_str
+    return best_matched_ip
+
+
+def get_real_ip(request, right_most_proxy=False):
+    """
+    Returns client's best-matched `real` `externally-routable` ip-address, or None
+    @deprecated - Do not edit
+    """
+    return get_ip(request, real_ip_only=True, right_most_proxy=right_most_proxy)
+
+
+def get_trusted_ip(request, right_most_proxy=False, trusted_proxies=TRUSTED_PROXY_LIST):
+    """
+    Returns client's ip-address from `trusted` proxy server(s) or None
+    @deprecated - Do not edit
+    """
+    if trusted_proxies:
+        meta_keys = ['HTTP_X_FORWARDED_FOR', 'X_FORWARDED_FOR']
+        for key in meta_keys:
+            value = request.META.get(key, request.META.get(key.replace('_', '-'), '')).strip()
+            if value:
+                ips = [ip.strip().lower() for ip in value.split(',')]
+                if len(ips) > 1:
+                    if right_most_proxy:
+                        ips.reverse()
+                    for proxy in trusted_proxies:
+                        if proxy in ips[-1]:
+                            return ips[0]
+    return None

+ 60 - 0
desktop/core/ext-py/django-ipware-2.1.0/ipware/ip2.py

@@ -0,0 +1,60 @@
+from . import utils as util
+from . import defaults as defs
+
+
+def get_client_ip(
+    request,
+    proxy_order='left-most',
+    proxy_count=None,
+    proxy_trusted_ips=None,
+    request_header_order=None,
+):
+    client_ip = None
+    routable = False
+
+    if proxy_count is None:
+        proxy_count = -1
+
+    if proxy_trusted_ips is None:
+        proxy_trusted_ips = []
+
+    if request_header_order is None:
+        request_header_order = defs.IPWARE_META_PRECEDENCE_ORDER
+
+    for key in request_header_order:
+        value = util.get_request_meta(request, key)
+        if value:
+            ips, ip_count = util.get_ips_from_string(value)
+
+            if ip_count < 1:
+                # we are expecting at least one IP address to process
+                continue
+
+            if proxy_count == 0 and ip_count > 1:
+                # we are not expecting requests via any proxies
+                continue
+
+            if proxy_count > 0 and proxy_count != ip_count - 1:
+                # we are expecting requests via `proxy_count` number of proxies
+                continue
+
+            if proxy_trusted_ips and ip_count < 2:
+                # we are expecting requests via at least one trusted proxy
+                continue
+
+            if proxy_order == 'right-most' and ip_count > 1:
+                # we are expecting requests via proxies to be custom as per `<proxy2>, <proxy1>, <client>`
+                ips.reverse()
+
+            if proxy_trusted_ips:
+                for proxy in proxy_trusted_ips:
+                    if proxy in ips[-1]:
+                        client_ip, routable = util.get_ip_info(ips[0])
+                        if client_ip and routable:
+                            return client_ip, routable
+            else:
+                client_ip, routable = util.get_ip_info(util.get_best_ip(client_ip, ips[0]))
+                if client_ip and routable:
+                    return client_ip, routable
+
+    return client_ip, routable

+ 0 - 0
desktop/core/ext-py/django-axes-2.2.0/axes/management/commands/__init__.py → desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/__init__.py


+ 84 - 0
desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/tests_common.py

@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+
+from django.conf import settings
+from django.http import HttpRequest
+from django.test import TestCase
+from .. import utils as util
+
+
+class IPv4TestCase(TestCase):
+    """IP address Test"""
+
+
+    def test_is_valid_ip(self):
+        ip = '177.139.233.139'
+        self.assertTrue(util.is_valid_ip(ip))
+
+        ip = '3ffe:1900:4545:3:200:f8ff:fe21:67cf'
+        self.assertTrue(util.is_valid_ip(ip))
+
+    def test_is_invalid_ip(self):
+        ip = '177.139.233.139x'
+        self.assertFalse(util.is_valid_ip(ip))
+
+        ip = '3ffe:1900:4545:3:200:f8ff:fe21:67cz'
+        self.assertFalse(util.is_valid_ip(ip))
+
+    def test_is_private_ip(self):
+        ip = '127.0.0.1'
+        self.assertTrue(util.is_private_ip(ip))
+
+        ip = '::1/128'
+        self.assertTrue(util.is_private_ip(ip))
+
+    def test_is_public_ip(self):
+        ip = '177.139.233.139'
+        self.assertTrue(util.is_public_ip(ip))
+
+        ip = '74dc::02ba'
+        self.assertTrue(util.is_public_ip(ip))
+
+    def test_is_loopback_ip(self):
+        ip = '127.0.0.1'
+        self.assertTrue(util.is_loopback_ip(ip))
+
+        ip = '177.139.233.139'
+        self.assertFalse(util.is_loopback_ip(ip))
+
+        ip = '10.0.0.1'
+        self.assertFalse(util.is_loopback_ip(ip))
+
+        ip = '::1/128'
+        self.assertTrue(util.is_loopback_ip(ip))
+
+        ip = '74dc::02ba'
+        self.assertFalse(util.is_loopback_ip(ip))
+
+        ip = '2001:db8:'
+        self.assertFalse(util.is_loopback_ip(ip))
+
+    def test_http_request_meta_headers(self):
+        request = HttpRequest()
+        ip_str = '192.168.255.182, 10.0.0.0, 127.0.0.1, 198.84.193.157, 177.139.233.139,'
+        request.META = { 'HTTP_X_FORWARDED_FOR': ip_str }
+        value = util.get_request_meta(request, 'HTTP_X_FORWARDED_FOR')
+        self.assertEqual(value, ip_str)
+
+    def test_ips_from_strings(self):
+        request = HttpRequest()
+        ip_str = '192.168.255.182, 198.84.193.157, 177.139.233.139 ,'
+        result = util.get_ips_from_string(ip_str)
+        self.assertEqual(result, (['192.168.255.182', '198.84.193.157', '177.139.233.139'], 3))
+
+    def test_get_ip_info(self):
+        ip = '127.0.0.1'
+        result = util.get_ip_info(ip)
+        self.assertTrue(result, (ip, False))
+
+        ip = '10.0.01'
+        result = util.get_ip_info(ip)
+        self.assertTrue(result, (ip, False))
+
+        ip = '74dc::02ba'
+        result = util.get_ip_info(ip)
+        self.assertTrue(result, (ip, True))

+ 253 - 0
desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/tests_ipv4.py

@@ -0,0 +1,253 @@
+# -*- coding: utf-8 -*-
+
+from django.conf import settings
+from django.http import HttpRequest
+from django.test import TestCase
+from ipware import get_client_ip
+
+
+class IPv4TestCase(TestCase):
+    """IP address Test"""
+
+    def test_meta_none(self):
+        request = HttpRequest()
+        request.META = {}
+        ip, routable = get_client_ip(request)
+        self.assertIsNone(ip)
+        self.assertFalse(routable)
+
+    def test_meta_single(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("177.139.233.139", True))
+
+    def test_meta_multi(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("177.139.233.139", True))
+
+    def test_meta_multi_precedence_order(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '177.139.233.138, 198.84.193.157, 198.84.193.158',
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("177.139.233.139", True))
+
+    def test_meta_proxy_order_left_most(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158',
+        }
+        result = get_client_ip(request, proxy_order='left-most')
+        self.assertEqual(result, ("177.139.233.139", True))
+
+    def test_meta_proxy_order_right_most(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158',
+        }
+        result = get_client_ip(request, proxy_order='right-most')
+        self.assertEqual(result, ("198.84.193.158", True))
+
+    def test_meta_multi_precedence_private_first(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '10.0.0.0, 10.0.0.1, 10.0.0.2',
+            'X_FORWARDED_FOR': '177.139.233.138, 198.84.193.157, 198.84.193.158',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("177.139.233.138", True))
+
+    def test_meta_multi_precedence_invalid_first(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': 'unknown, 10.0.0.1, 10.0.0.2',
+            'X_FORWARDED_FOR': '177.139.233.138, 198.84.193.157, 198.84.193.158',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("177.139.233.138", True))
+
+    def test_meta_error_only(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': 'unknown, 177.139.233.139, 198.84.193.157, 198.84.193.158',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, (None, False))
+
+    def test_meta_error_first(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': 'unknown, 177.139.233.139, 198.84.193.157, 198.84.193.158',
+            'X_FORWARDED_FOR': '177.139.233.138, 198.84.193.157, 198.84.193.158',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("177.139.233.138", True))
+
+    def test_meta_singleton(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("177.139.233.139", True))
+
+    def test_meta_singleton_proxy_count(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139',
+        }
+        result = get_client_ip(request, proxy_count=1)
+        self.assertEqual(result, (None, False))
+
+    def test_meta_singleton_proxy_count_private(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '10.0.0.0',
+            'HTTP_X_REAL_IP': '177.139.233.139',
+        }
+        result = get_client_ip(request, proxy_count=1)
+        self.assertEqual(result, (None, False))
+
+    def test_meta_singleton_private_fallback(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '10.0.0.0',
+            'HTTP_X_REAL_IP': '177.139.233.139',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("177.139.233.139", True))
+
+    def test_meta_proxy_trusted_ips(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158',
+        }
+        result = get_client_ip(request, proxy_trusted_ips=['198.84.193.158'])
+        self.assertEqual(result, ("177.139.233.139", True))
+
+    def test_meta_proxy_trusted_ips_proxy_count(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158',
+        }
+        result = get_client_ip(request, proxy_count=2, proxy_trusted_ips=['198.84.193.158'])
+        self.assertEqual(result, ("177.139.233.139", True))
+
+    def test_meta_proxy_trusted_ips_proxy_count_less_error(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.158',
+        }
+        result = get_client_ip(request, proxy_count=2, proxy_trusted_ips=['198.84.193.158'])
+        self.assertEqual(result, (None, False))
+
+    def test_meta_proxy_trusted_ips_proxy_count_more_error(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158',
+        }
+        result = get_client_ip(request, proxy_count=1, proxy_trusted_ips=['198.84.193.158'])
+        self.assertEqual(result, (None, False))
+
+    def test_meta_proxy_trusted_ips_proxy_count_more_error_fallback(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158',
+            'HTTP_X_REAL_IP': '177.139.233.139',
+        }
+        result = get_client_ip(request, proxy_count=1, proxy_trusted_ips=['198.84.193.158'])
+        self.assertEqual(result, (None, False))
+
+    def test_best_matched_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_REAL_IP': '192.168.1.1',
+            'REMOTE_ADDR': '177.31.233.133',
+        }
+        ip = get_client_ip(request)
+        self.assertEqual(ip, ("177.31.233.133", True))
+
+    def test_best_matched_ip_public(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_REAL_IP': '177.31.233.122',
+            'REMOTE_ADDR': '177.31.233.133',
+        }
+        ip = get_client_ip(request)
+        self.assertEqual(ip, ("177.31.233.122", True))
+
+    def test_best_matched_ip_private(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_REAL_IP': '192.168.1.1',
+            'REMOTE_ADDR': '127.0.0.1',
+        }
+        ip = get_client_ip(request)
+        self.assertEqual(ip, ("192.168.1.1", False))
+
+    def test_best_matched_ip_private_precedence(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_REAL_IP': '127.0.0.1',
+            'REMOTE_ADDR': '192.168.1.1',
+        }
+        ip = get_client_ip(request)
+        self.assertEqual(ip, ("192.168.1.1", False))
+
+    def test_100_low_range_public(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_REAL_IP': '100.63.0.9',
+        }
+        ip = get_client_ip(request)
+        self.assertEqual(ip, ("100.63.0.9", True))
+
+    def test_100_block_private(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_REAL_IP': '100.76.0.9',
+        }
+        ip = get_client_ip(request)
+        self.assertEqual(ip, ("100.76.0.9", False))
+
+    def test_100_high_range_public(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_REAL_IP': '100.128.0.9',
+        }
+        ip = get_client_ip(request)
+        self.assertEqual(ip, ("100.128.0.9", True))
+
+    def test_request_header_order_specific(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_REAL_IP': '192.168.1.1',
+            'REMOTE_ADDR': '177.139.233.139',
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158',
+        }
+        ip = get_client_ip(request, request_header_order=['HTTP_X_FORWARDED_FOR'])
+        self.assertEqual(ip, ("177.139.233.139", True))
+
+
+    def test_request_header_order_multiple(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158',
+            'X_FORWARDED_FOR': '177.139.233.138, 198.84.193.157, 198.84.193.158',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_client_ip(request, request_header_order=['X_FORWARDED_FOR', 'HTTP_X_FORWARDED_FOR'])
+        self.assertEqual(ip, ("177.139.233.138", True))

+ 173 - 0
desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/tests_ipv6.py

@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+
+from django.conf import settings
+from django.http import HttpRequest
+from django.test import TestCase
+from ipware import get_client_ip
+
+
+class IPv4TestCase(TestCase):
+    """IP address Test"""
+
+    def test_meta_none(self):
+        request = HttpRequest()
+        request.META = {}
+        ip, routable = get_client_ip(request)
+        self.assertIsNone(ip)
+        self.assertFalse(routable)
+
+    def test_meta_single(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True))
+
+    def test_meta_multi(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb',
+            'REMOTE_ADDR': '74dc::02bc',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True))
+
+    def test_meta_multi_precedence_order(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '74dc::02be, 74dc::02bf',
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb',
+            'REMOTE_ADDR': '74dc::02bc',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True))
+
+    def test_meta_proxy_order_left_most(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb',
+        }
+        result = get_client_ip(request, proxy_order='left-most')
+        self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True))
+
+    def test_meta_proxy_order_right_most(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb',
+        }
+        result = get_client_ip(request, proxy_order='right-most')
+        self.assertEqual(result, ("74dc::02bb", True))
+
+    def test_meta_multi_precedence_private_first(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '2001:db8:, ::1',
+            'X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb',
+            'REMOTE_ADDR': '74dc::02bc',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True))
+
+    def test_meta_multi_precedence_invalid_first(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': 'unknown, 2001:db8:, ::1',
+            'X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb',
+            'REMOTE_ADDR': '74dc::02bc',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True))
+
+    def test_meta_error_only(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': 'unknown, 3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, (None, False))
+
+    def test_meta_error_first(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': 'unknown, 3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb',
+            'X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True))
+
+    def test_meta_singleton(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True))
+
+    def test_meta_singleton_proxy_count(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+            'HTTP_X_REAL_IP': '74dc::02ba',
+        }
+        result = get_client_ip(request, proxy_count=1)
+        self.assertEqual(result, (None, False))
+
+    def test_meta_singleton_proxy_count_private(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '::1',
+            'HTTP_X_REAL_IP': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+        }
+        result = get_client_ip(request, proxy_count=1)
+        self.assertEqual(result, (None, False))
+
+    def test_meta_singleton_private_fallback(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '::1',
+            'HTTP_X_REAL_IP': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+        }
+        result = get_client_ip(request)
+        self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True))
+
+    def test_meta_proxy_trusted_ips(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb',
+        }
+        result = get_client_ip(request, proxy_trusted_ips=['74dc::02bb'])
+        self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True))
+
+    def test_meta_proxy_trusted_ips_proxy_count(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb',
+        }
+        result = get_client_ip(request, proxy_count=2, proxy_trusted_ips=['74dc::02bb'])
+        self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True))
+
+    def test_meta_proxy_trusted_ips_proxy_count_less_error(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02bb',
+        }
+        result = get_client_ip(request, proxy_count=2, proxy_trusted_ips=['74dc::02bb'])
+        self.assertEqual(result, (None, False))
+
+    def test_meta_proxy_trusted_ips_proxy_count_more_error(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb',
+        }
+        result = get_client_ip(request, proxy_count=1, proxy_trusted_ips=['74dc::02bb'])
+        self.assertEqual(result, (None, False))
+
+    def test_meta_proxy_trusted_ips_proxy_count_more_error_ignore_fallback(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb',
+            'HTTP_X_REAL_IP': '74dc::02bb',
+        }
+        result = get_client_ip(request, proxy_count=1, proxy_trusted_ips=['74dc::02bb'])
+        self.assertEqual(result, (None, False))

+ 1 - 0
desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/tests_v1/__init__.py

@@ -0,0 +1 @@
+# Legacy Tests

+ 400 - 0
desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/tests_v1/tests_ipv4.py

@@ -0,0 +1,400 @@
+# -*- coding: utf-8 -*-
+
+from django.conf import settings
+from django.http import HttpRequest
+from django.test import TestCase
+from ipware.ip import get_ip
+from ipware.ip import get_real_ip
+from ipware.ip import get_trusted_ip
+
+
+class IPv4TestCase(TestCase):
+    """IP address Test"""
+
+    def test_meta_none(self):
+        request = HttpRequest()
+        request.META = {
+        }
+        ip = get_real_ip(request)
+        self.assertIsNone(ip)
+
+    def test_http_x_forwarded_for_multiple(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '192.168.255.182, 10.0.0.0, 127.0.0.1, 198.84.193.157, 177.139.233.139',
+            'HTTP_X_REAL_IP': '177.139.233.132',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "198.84.193.157")
+
+    def test_http_x_forwarded_for_multiple_left_most_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '192.168.255.182, 198.84.193.157, 10.0.0.0, 127.0.0.1, 177.139.233.139',
+            'HTTP_X_REAL_IP': '177.139.233.132',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "198.84.193.157")
+
+    def test_http_x_forwarded_for_multiple_right_most_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '192.168.255.182, 198.84.193.157, 10.0.0.0, 127.0.0.1, 177.139.233.139',
+            'HTTP_X_REAL_IP': '177.139.233.132',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request, right_most_proxy=True)
+        self.assertEqual(ip, "177.139.233.139")
+
+    def test_http_x_forwarded_for_multiple_right_most_ip_private(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '192.168.255.182, 198.84.193.157, 10.0.0.0, 127.0.0.1, 177.139.233.139',
+            'HTTP_X_REAL_IP': '177.139.233.132',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request, right_most_proxy=True)
+        self.assertEqual(ip, "177.139.233.139")
+
+    def test_http_x_forwarded_for_multiple_bad_address(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': 'unknown, 192.168.255.182, 10.0.0.0, 127.0.0.1, 198.84.193.157, 177.139.233.139',
+            'HTTP_X_REAL_IP': '177.139.233.132',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "198.84.193.157")
+
+    def test_http_x_forwarded_for_singleton(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139',
+            'HTTP_X_REAL_IP': '177.139.233.132',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.139")
+
+    def test_http_x_forwarded_for_singleton_private_address(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '192.168.255.182',
+            'HTTP_X_REAL_IP': '177.139.233.132',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.132")
+
+    def test_bad_http_x_forwarded_for_fallback_on_x_real_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': 'unknown 177.139.233.139',
+            'HTTP_X_REAL_IP': '177.139.233.132',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.132")
+
+    def test_empty_http_x_forwarded_for_fallback_on_x_real_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '',
+            'HTTP_X_REAL_IP': '177.139.233.132',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.132")
+
+    def test_empty_http_x_forwarded_for_empty_x_real_ip_fallback_on_remote_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '',
+            'HTTP_X_REAL_IP': '',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.133")
+
+    def test_empty_http_x_forwarded_for_private_x_real_ip_fallback_on_remote_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '',
+            'HTTP_X_REAL_IP': '192.168.255.182',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.133")
+
+    def test_private_http_x_forward_for_ip_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '127.0.0.1',
+            'HTTP_X_REAL_IP': '',
+            'REMOTE_ADDR': '',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, None)
+
+    def test_private_remote_addr_for_ip_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '',
+            'REMOTE_ADDR': '127.0.0.1',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, None)
+
+    def test_missing_x_forwarded(self):
+        request = HttpRequest()
+        request.META = {
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.133")
+
+    def test_missing_x_forwarded_missing_real_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.133")
+
+    def test_best_matched_real_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_REAL_IP': '127.0.0.1',
+            'REMOTE_ADDR': '177.31.233.133',
+        }
+        ip = get_ip(request)
+        self.assertEqual(ip, "177.31.233.133")
+
+    def test_best_matched_private_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_REAL_IP': '127.0.0.1',
+            'REMOTE_ADDR': '192.31.233.133',
+        }
+        ip = get_ip(request)
+        self.assertEqual(ip, "192.31.233.133")
+
+    def test_best_matched_private_ip_2(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_REAL_IP': '192.31.233.133',
+            'REMOTE_ADDR': '127.0.0.1',
+        }
+        ip = get_ip(request)
+        self.assertEqual(ip, "192.31.233.133")
+
+    def test_x_forwarded_for_multiple(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '192.168.255.182, 10.0.0.0, 127.0.0.1, 198.84.193.157, 177.139.233.139',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "198.84.193.157")
+
+    def test_x_forwarded_for_multiple_left_most_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '192.168.255.182, 198.84.193.157, 10.0.0.0, 127.0.0.1, 177.139.233.139',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "198.84.193.157")
+
+    def test_x_forwarded_for_multiple_right_most_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '192.168.255.182, 198.84.193.157, 10.0.0.0, 127.0.0.1, 177.139.233.139',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request, right_most_proxy=True)
+        self.assertEqual(ip, "177.139.233.139")
+
+    def test_x_forwarded_for_multiple_right_most_ip_private(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '192.168.255.182, 198.84.193.157, 10.0.0.0, 127.0.0.1, 177.139.233.139',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request, right_most_proxy=True)
+        self.assertEqual(ip, "177.139.233.139")
+
+    def test_x_forwarded_for_multiple_bad_address(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': 'unknown, 192.168.255.182, 10.0.0.0, 127.0.0.1, 198.84.193.157, 177.139.233.139',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "198.84.193.157")
+
+    def test_x_forwarded_for_singleton(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '177.139.233.139',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.139")
+
+    def test_x_forwarded_for_singleton_private_address(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '192.168.255.182',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.133")
+
+    def test_bad_x_forwarded_for_fallback_on_x_real_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': 'unknown 177.139.233.139',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.133")
+
+    def test_empty_x_forwarded_for_fallback_on_x_real_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.133")
+
+    def test_empty_x_forwarded_for_empty_x_real_ip_fallback_on_remote_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.133")
+
+    def test_empty_x_forwarded_for_private_x_real_ip_fallback_on_remote_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '',
+            'REMOTE_ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.133")
+
+    def test_private_x_forward_for_ip_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '127.0.0.1',
+            'REMOTE_ADDR': '',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, None)
+
+    def test_x_forwarded_for_singleton_hyphen_as_delimiter(self):
+        request = HttpRequest()
+        request.META = {
+            'X-FORWARDED-FOR': '177.139.233.139',
+            'REMOTE-ADDR': '177.139.233.133',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "177.139.233.139")
+
+
+class IPv4TrustedProxiesTestCase(TestCase):
+    """Trusted Proxies - IP address Test"""
+
+    def test_meta_none(self):
+        request = HttpRequest()
+        request.META = {
+        }
+        ip = get_trusted_ip(request)
+        self.assertIsNone(ip)
+
+    def test_http_x_forwarded_for_conf_settings(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '198.84.193.157, 177.139.200.139, 177.139.233.100',
+        }
+        ip = get_trusted_ip(request)
+        self.assertEqual(ip, "198.84.193.157")
+
+    def test_http_x_forwarded_for_no_proxy(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '198.84.193.157, 177.139.200.139, 177.139.233.139',
+        }
+        ip = get_trusted_ip(request, trusted_proxies=[])
+        self.assertIsNone(ip)
+
+    def test_http_x_forwarded_for_single_proxy(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '198.84.193.157, 177.139.200.139, 177.139.233.139',
+        }
+        ip = get_trusted_ip(request, trusted_proxies=['177.139.233.139'])
+        self.assertEqual(ip, "198.84.193.157")
+
+    def test_http_x_forwarded_for_single_proxy_with_right_most(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '177.139.233.139, 177.139.200.139, 198.84.193.157',
+        }
+        ip = get_trusted_ip(request, right_most_proxy=True, trusted_proxies=['177.139.233.139'])
+        self.assertEqual(ip, "198.84.193.157")
+
+    def test_http_x_forwarded_for_multi_proxy(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '198.84.193.157, 177.139.200.139, 177.139.233.139',
+        }
+        ip = get_trusted_ip(request, trusted_proxies=['177.139.233.138', '177.139.233.139'])
+        self.assertEqual(ip, "198.84.193.157")
+
+    def test_http_x_forwarded_for_all_proxies_in_subnet(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '198.84.193.157, 177.139.200.139, 177.139.233.139',
+        }
+        ip = get_trusted_ip(request, trusted_proxies=['177.139.233'])
+        self.assertEqual(ip, "198.84.193.157")
+
+    def test_http_x_forwarded_for_all_proxies_in_subnet_2(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '198.84.193.157, 177.139.200.139, 177.139.233.139',
+        }
+        ip = get_trusted_ip(request, trusted_proxies=['177.139'])
+        self.assertEqual(ip, "198.84.193.157")
+
+    def test_x_forwarded_for_single_proxy(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '198.84.193.157, 177.139.200.139, 177.139.233.139',
+        }
+        ip = get_trusted_ip(request, trusted_proxies=['177.139.233.139'])
+        self.assertEqual(ip, "198.84.193.157")
+
+    def test_x_forwarded_for_single_proxy_hyphens(self):
+        request = HttpRequest()
+        request.META = {
+            'X-FORWARDED-FOR': '198.84.193.157, 177.139.200.139, 177.139.233.139',
+        }
+        ip = get_trusted_ip(request, trusted_proxies=['177.139.233.139'])
+        self.assertEqual(ip, "198.84.193.157")
+
+    def test_http_x_forwarded_for_and_x_forward_for_single_proxy(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '198.84.193.156, 177.139.200.139, 177.139.233.139',
+            'X_FORWARDED_FOR': '198.84.193.157, 177.139.200.139, 177.139.233.139',
+        }
+        ip = get_trusted_ip(request, trusted_proxies=['177.139.233.139'])
+        self.assertEqual(ip, "198.84.193.156")

+ 276 - 0
desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/tests_v1/tests_ipv6.py

@@ -0,0 +1,276 @@
+# -*- coding: utf-8 -*-
+
+from django.conf import settings
+from django.http import HttpRequest
+from django.test import TestCase
+from django.test import override_settings
+from ipware.ip import get_ip
+from ipware.ip import get_real_ip
+from ipware.ip import get_trusted_ip
+
+
+class IPv6TestCase(TestCase):
+    """IP address Test"""
+
+    def test_http_x_forwarded_for_multiple(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba',
+            'HTTP_X_REAL_IP': '74dc::02ba',
+            'REMOTE_ADDR': '74dc::02ba',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "3ffe:1900:4545:3:200:f8ff:fe21:67cf")
+
+    def test_http_x_forwarded_for_multiple_bad_address(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': 'unknown, ::1/128, 74dc::02ba',
+            'HTTP_X_REAL_IP': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+            'REMOTE_ADDR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_http_x_forwarded_for_singleton(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '74dc::02ba',
+            'HTTP_X_REAL_IP': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+            'REMOTE_ADDR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_http_x_forwarded_for_singleton_private_address(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '::1/128',
+            'HTTP_X_REAL_IP': '74dc::02ba',
+            'REMOTE_ADDR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_bad_http_x_forwarded_for_fallback_on_x_real_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': 'unknown ::1/128',
+            'HTTP_X_REAL_IP': '74dc::02ba',
+            'REMOTE_ADDR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_empty_http_x_forwarded_for_fallback_on_x_real_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '',
+            'HTTP_X_REAL_IP': '74dc::02ba',
+            'REMOTE_ADDR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_empty_http_x_forwarded_for_empty_x_real_ip_fallback_on_remote_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '',
+            'HTTP_X_REAL_IP': '',
+            'REMOTE_ADDR': '74dc::02ba',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_empty_http_x_forwarded_for_private_x_real_ip_fallback_on_remote_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '',
+            'HTTP_X_REAL_IP': '::1/128',
+            'REMOTE_ADDR': '74dc::02ba',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_private_http_x_forward_for_ip_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '::1/128',
+            'HTTP_X_REAL_IP': '',
+            'REMOTE_ADDR': '',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, None)
+
+    def test_private_real_ip_for_ip_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '',
+            'HTTP_X_REAL_IP': '::1/128',
+            'REMOTE_ADDR': '',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, None)
+
+    def test_private_remote_addr_for_ip_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '',
+            'HTTP_X_REAL_IP': '',
+            'REMOTE_ADDR': '::1/128',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, None)
+
+    def test_missing_x_forwarded(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_REAL_IP': '74dc::02ba',
+            'REMOTE_ADDR': '74dc::02ba',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_missing_x_forwarded_missing_real_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'REMOTE_ADDR': '74dc::02ba',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_missing_x_forwarded_missing_real_ip_mix_case(self):
+        request = HttpRequest()
+        request.META = {
+            'REMOTE_ADDR': '74DC::02BA',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_private_remote_address(self):
+        request = HttpRequest()
+        request.META = {
+            'REMOTE_ADDR': 'fe80::02ba',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, None)
+
+    def test_best_matched_real_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_REAL_IP': '::1',
+            'REMOTE_ADDR': 'fe80::02ba',
+        }
+        ip = get_ip(request)
+        self.assertEqual(ip, "fe80::02ba")
+
+    def test_x_forwarded_for_multiple(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba',
+            'REMOTE_ADDR': '74dc::02ba',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "3ffe:1900:4545:3:200:f8ff:fe21:67cf")
+
+    def test_x_forwarded_for_multiple_bad_address(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': 'unknown, ::1/128, 74dc::02ba',
+            'REMOTE_ADDR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_x_forwarded_for_singleton(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '74dc::02ba',
+            'REMOTE_ADDR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_x_forwarded_for_singleton_private_address(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '::1/128',
+            'HTTP_X_REAL_IP': '74dc::02ba',
+            'REMOTE_ADDR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_bad_x_forwarded_for_fallback_on_x_real_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': 'unknown ::1/128',
+            'REMOTE_ADDR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "3ffe:1900:4545:3:200:f8ff:fe21:67cf")
+
+    def test_empty_x_forwarded_for_fallback_on_x_real_ip(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '',
+            'REMOTE_ADDR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "3ffe:1900:4545:3:200:f8ff:fe21:67cf")
+
+    def test_empty_x_forwarded_for_empty_x_real_ip_fallback_on_remote_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '',
+            'REMOTE_ADDR': '74dc::02ba',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_empty_x_forwarded_for_private_x_real_ip_fallback_on_remote_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '',
+            'REMOTE_ADDR': '74dc::02ba',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+    def test_private_x_forward_for_ip_addr(self):
+        request = HttpRequest()
+        request.META = {
+            'X_FORWARDED_FOR': '::1/128',
+            'REMOTE_ADDR': '',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, None)
+
+    def test_x_forwarded_for_singleton_hyphen_as_delimiter(self):
+        request = HttpRequest()
+        request.META = {
+            'X-FORWARDED-FOR': '74dc::02ba',
+            'REMOTE-ADDR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
+        }
+        ip = get_real_ip(request)
+        self.assertEqual(ip, "74dc::02ba")
+
+
+class IPv6TrustedProxiesTestCase(TestCase):
+    """Trusted Proxies - IP address Test"""
+
+    def test_http_x_forwarded_for_no_proxy(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba',
+        }
+        ip = get_trusted_ip(request, trusted_proxies=[])
+        self.assertIsNone(ip)
+
+    def test_http_x_forwarded_for_single_proxy(self):
+        request = HttpRequest()
+        request.META = {
+            'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba',
+        }
+        ip = get_trusted_ip(request, trusted_proxies=['74dc::02ba'])
+        self.assertEqual(ip, "3ffe:1900:4545:3:200:f8ff:fe21:67cf")

+ 12 - 0
desktop/core/ext-py/django-ipware-2.1.0/ipware/tests/testsettings.py

@@ -0,0 +1,12 @@
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': ':memory:',
+    },
+}
+SECRET_KEY = "un33k"
+INSTALLED_APPS = ['ipware']
+MIDDLEWARE_CLASSES = []
+
+# Version 1 support
+IPWARE_TRUSTED_PROXY_LIST = ['177.139.233.100']

+ 115 - 0
desktop/core/ext-py/django-ipware-2.1.0/ipware/utils.py

@@ -0,0 +1,115 @@
+import socket
+
+from . import defaults as defs
+
+
+def is_valid_ipv4(ip_str):
+    """
+    Check the validity of an IPv4 address
+    """
+    try:
+        socket.inet_pton(socket.AF_INET, ip_str)
+    except AttributeError:  # pragma: no cover
+        try:  # Fall-back on legacy API or False
+            socket.inet_aton(ip_str)
+        except (AttributeError, socket.error):
+            return False
+        return ip_str.count('.') == 3
+    except socket.error:
+        return False
+    return True
+
+
+def is_valid_ipv6(ip_str):
+    """
+    Check the validity of an IPv6 address
+    """
+    try:
+        socket.inet_pton(socket.AF_INET6, ip_str)
+    except socket.error:
+        return False
+    return True
+
+
+def is_valid_ip(ip_str):
+    """
+    Check the validity of an IP address
+    """
+    return is_valid_ipv4(ip_str) or is_valid_ipv6(ip_str)
+
+
+def is_private_ip(ip_str):
+    """
+    Returns true of ip_str is private & not routable, else return false
+    """
+    return ip_str.startswith(defs.IPWARE_NON_PUBLIC_IP_PREFIX)
+
+
+def is_public_ip(ip_str):
+    """
+    Returns true of ip_str is public & routable, else return false
+    """
+    return not is_private_ip(ip_str)
+
+
+def is_loopback_ip(ip_str):
+    """
+    Returns true of ip_str is public & routable, else return false
+    """
+    return ip_str.startswith(defs.IPWARE_LOOPBACK_PREFIX)
+
+
+def get_request_meta(request, key):
+    """
+    Given a key, it returns a cleaned up version of the value from request.META, or None
+    """
+    value = request.META.get(key, request.META.get(key.replace('_', '-'), '')).strip()
+    if value == '':
+        return None
+    return value
+
+
+def get_ips_from_string(ip_str):
+    """
+    Given a string, it returns a list of one or more valid IP addresses
+    """
+    ip_list = []
+
+    for ip in ip_str.split(','):
+        clean_ip = ip.strip().lower()
+        if clean_ip:
+            ip_list.append(clean_ip)
+
+    ip_count = len(ip_list)
+    if ip_count > 0:
+        if is_valid_ip(ip_list[0]) and is_valid_ip(ip_list[-1]):
+            return ip_list, ip_count
+
+    return [], 0
+
+
+def get_ip_info(ip_str):
+    """
+    Given a string, it returns a tuple of (IP, Routable).
+    """
+    ip = None
+    is_routable_ip = False
+    if is_valid_ip(ip_str):
+        ip = ip_str
+        is_routable_ip = is_public_ip(ip)
+    return ip, is_routable_ip
+
+
+def get_best_ip(last_ip, next_ip):
+    """
+    Given two IP addresses, it returns the the best match ip.
+    Order of precedence is (Public, Private, Loopback, None)
+    Right-most IP is returned
+    """
+    if last_ip is None:
+        return next_ip
+    if is_public_ip(last_ip) and not is_public_ip(next_ip):
+        return last_ip
+    if is_private_ip(last_ip) and is_loopback_ip(next_ip):
+        return last_ip
+    return next_ip

+ 4 - 0
desktop/core/ext-py/django-ipware-2.1.0/setup.cfg

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

+ 86 - 0
desktop/core/ext-py/django-ipware-2.1.0/setup.py

@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from setuptools import setup
+import re
+import os
+import sys
+
+
+name = 'django-ipware'
+package = 'ipware'
+description = "A Django utility application that returns client's real IP address"
+url = 'https://github.com/un33k/django-ipware'
+author = 'Val Neekman'
+author_email = 'info@neekware.com'
+license = 'MIT'
+install_requires = ['']
+classifiers = [
+    'Development Status :: 5 - Production/Stable',
+    'Environment :: Web Environment',
+    'Intended Audience :: Developers',
+    'License :: OSI Approved :: MIT License',
+    'Operating System :: OS Independent',
+    'Programming Language :: Python',
+    'Programming Language :: Python :: 2.7',
+    'Programming Language :: Python :: 3.4',
+    'Programming Language :: Python :: 3.5',
+    'Programming Language :: Python :: 3.6',
+    'Topic :: Utilities'
+]
+
+
+def get_version(package):
+    """
+    Return package version as listed in `__version__` in `init.py`.
+    """
+    init_py = open(os.path.join(package, '__init__.py')).read()
+    return re.search("^__version__ = ['\"]([^'\"]+)['\"]", init_py, re.MULTILINE).group(1)
+
+
+def get_packages(package):
+    """
+    Return root package and all sub-packages.
+    """
+    return [dirpath
+            for dirpath, dirnames, filenames in os.walk(package)
+            if os.path.exists(os.path.join(dirpath, '__init__.py'))]
+
+
+def get_package_data(package):
+    """
+    Return all files under the root package, that are not in a
+    package themselves.
+    """
+    walk = [(dirpath.replace(package + os.sep, '', 1), filenames)
+            for dirpath, dirnames, filenames in os.walk(package)
+            if not os.path.exists(os.path.join(dirpath, '__init__.py'))]
+
+    filepaths = []
+    for base, filenames in walk:
+        filepaths.extend([os.path.join(base, filename)
+                          for filename in filenames])
+    return {package: filepaths}
+
+
+if sys.argv[-1] == 'publish':
+    os.system("python setup.py sdist upload")
+    args = {'version': get_version(package)}
+    print("You probably want to also tag the version now:")
+    print("  git tag -a %(version)s -m 'version %(version)s' && git push --tags" % args)
+    sys.exit()
+
+
+setup(
+    name=name,
+    version=get_version(package),
+    url=url,
+    license=license,
+    description=description,
+    author=author,
+    author_email=author_email,
+    packages=get_packages(package),
+    package_data=get_package_data(package),
+    install_requires=install_requires,
+    classifiers=classifiers
+)

+ 1 - 1
desktop/core/requirements.txt

@@ -9,7 +9,7 @@ channels-redis==2.3.2
 configobj==5.0.6
 cryptography==3.2
 django-auth-ldap==1.3.0
-Django==2.0.13 # Django 2 then 3?
+Django==2.1.15 # Django 2 then 3?
 django-axes==4.5.4
 django_babel==0.6.2
 django-celery-beat==1.4.0