mirror of
https://github.com/jazzband/django-defender.git
synced 2026-03-16 22:10:32 +00:00
Drop support Django < 2.2 and add support for Django > 3
This commit is contained in:
parent
5e6e52fcbb
commit
cc06ab33fd
12 changed files with 42 additions and 163 deletions
|
|
@ -2,7 +2,6 @@ language: python
|
||||||
dist: xenial
|
dist: xenial
|
||||||
cache: pip
|
cache: pip
|
||||||
python:
|
python:
|
||||||
- '3.5'
|
|
||||||
- '3.6'
|
- '3.6'
|
||||||
- '3.7'
|
- '3.7'
|
||||||
- '3.8'
|
- '3.8'
|
||||||
|
|
|
||||||
16
README.rst
16
README.rst
|
|
@ -10,6 +10,10 @@ django-defender
|
||||||
:alt: Supported Python versions
|
:alt: Supported Python versions
|
||||||
:target: https://pypi.org/project/django-defender/
|
:target: https://pypi.org/project/django-defender/
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/djversions/django-defender.svg
|
||||||
|
:target: https://pypi.org/project/django-defender/
|
||||||
|
:alt: Supported Django versions
|
||||||
|
|
||||||
.. image:: https://travis-ci.org/jazzband/django-defender.svg
|
.. image:: https://travis-ci.org/jazzband/django-defender.svg
|
||||||
:target: https://travis-ci.org/jazzband/django-defender
|
:target: https://travis-ci.org/jazzband/django-defender
|
||||||
:alt: Build Status
|
:alt: Build Status
|
||||||
|
|
@ -104,8 +108,8 @@ Admin pages
|
||||||
Requirements
|
Requirements
|
||||||
------------
|
------------
|
||||||
|
|
||||||
* Python: 3.5, 3.6, 3.7, 3.8, PyPy
|
* Python: 3.6, 3.7, 3.8, PyPy
|
||||||
* Django: 1.11, 2.1, 2.2, 3.x
|
* Django: 2.2, 3.x
|
||||||
* Redis
|
* Redis
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -165,8 +169,8 @@ following to your ``urls.py``
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^admin/', include(admin.site.urls)), # normal admin
|
path('admin/', include(admin.site.urls)), # normal admin
|
||||||
url(r'^admin/defender/', include('defender.urls')), # defender admin
|
path('admin/defender/', include('defender.urls')), # defender admin
|
||||||
# your own patterns follow...
|
# your own patterns follow...
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -674,13 +678,13 @@ like:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
PYTHONPATH=$PYTHONPATH:$PWD django-admin.py test defender --settings=defender.test_settings
|
PYTHONPATH=$PYTHONPATH:$PWD django-admin test defender --settings=defender.test_settings
|
||||||
|
|
||||||
With Code coverage:
|
With Code coverage:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
PYTHONPATH=$PYTHONPATH:$PWD coverage run --source=defender $(which django-admin.py) test defender --settings=defender.test_settings
|
PYTHONPATH=$PYTHONPATH:$PWD coverage run --source=defender $(which django-admin) test defender --settings=defender.test_settings
|
||||||
|
|
||||||
|
|
||||||
Releasing
|
Releasing
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,27 @@
|
||||||
try:
|
from django.contrib.auth.views import LoginView
|
||||||
from django.utils.deprecation import MiddlewareMixin as MIDDLEWARE_BASE_CLASS
|
|
||||||
except ImportError:
|
|
||||||
MIDDLEWARE_BASE_CLASS = object
|
|
||||||
from django.contrib.auth import views as auth_views
|
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
from .decorators import watch_login
|
from .decorators import watch_login
|
||||||
|
|
||||||
|
|
||||||
class FailedLoginMiddleware(MIDDLEWARE_BASE_CLASS):
|
class FailedLoginMiddleware:
|
||||||
""" Failed login middleware """
|
""" Failed login middleware """
|
||||||
|
|
||||||
patched = False
|
patched = False
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, get_response):
|
||||||
super().__init__(*args, **kwargs)
|
self.get_response = get_response
|
||||||
|
|
||||||
# Watch the auth login.
|
# Watch the auth login.
|
||||||
# Monkey-patch only once - otherwise we would be recording
|
# Monkey-patch only once - otherwise we would be recording
|
||||||
# failed attempts multiple times!
|
# failed attempts multiple times!
|
||||||
if not FailedLoginMiddleware.patched:
|
if not FailedLoginMiddleware.patched:
|
||||||
# Django 1.11 turned the `login` function view into the
|
our_decorator = watch_login()
|
||||||
# `LoginView` class-based view
|
watch_login_method = method_decorator(our_decorator)
|
||||||
try:
|
LoginView.dispatch = watch_login_method(LoginView.dispatch)
|
||||||
from django.contrib.auth.views import LoginView
|
|
||||||
|
|
||||||
our_decorator = watch_login()
|
|
||||||
watch_login_method = method_decorator(our_decorator)
|
|
||||||
LoginView.dispatch = watch_login_method(LoginView.dispatch)
|
|
||||||
except ImportError: # Django < 1.11
|
|
||||||
auth_views.login = watch_login()(auth_views.login)
|
|
||||||
|
|
||||||
FailedLoginMiddleware.patched = True
|
FailedLoginMiddleware.patched = True
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
response = self.get_response(request)
|
||||||
|
return response
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
from south.utils import datetime_utils as datetime
|
|
||||||
from south.db import db
|
|
||||||
from south.v2 import SchemaMigration
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(SchemaMigration):
|
|
||||||
"""Initial Migration for Defender"""
|
|
||||||
|
|
||||||
def forwards(self, orm):
|
|
||||||
""" Adding model 'AccessAttempt' """
|
|
||||||
db.create_table(
|
|
||||||
"defender_accessattempt",
|
|
||||||
(
|
|
||||||
("id", self.gf("django.db.models.fields.AutoField")(primary_key=True)),
|
|
||||||
(
|
|
||||||
"user_agent",
|
|
||||||
self.gf("django.db.models.fields.CharField")(max_length=255),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"ip_address",
|
|
||||||
self.gf("django.db.models.fields.GenericIPAddressField")(
|
|
||||||
max_length=39, null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"username",
|
|
||||||
self.gf("django.db.models.fields.CharField")(
|
|
||||||
max_length=255, null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"http_accept",
|
|
||||||
self.gf("django.db.models.fields.CharField")(max_length=1025),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"path_info",
|
|
||||||
self.gf("django.db.models.fields.CharField")(max_length=255),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"attempt_time",
|
|
||||||
self.gf("django.db.models.fields.DateTimeField")(
|
|
||||||
auto_now_add=True, blank=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"login_valid",
|
|
||||||
self.gf("django.db.models.fields.BooleanField")(default=False),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
db.send_create_signal("defender", ["AccessAttempt"])
|
|
||||||
|
|
||||||
def backwards(self, orm):
|
|
||||||
# Deleting model 'AccessAttempt'
|
|
||||||
db.delete_table("defender_accessattempt")
|
|
||||||
|
|
||||||
models = {
|
|
||||||
"defender.accessattempt": {
|
|
||||||
"Meta": {"ordering": "[u'-attempt_time']", "object_name": "AccessAttempt"},
|
|
||||||
"attempt_time": (
|
|
||||||
"django.db.models.fields.DateTimeField",
|
|
||||||
[],
|
|
||||||
{"auto_now_add": "True", "blank": "True"},
|
|
||||||
),
|
|
||||||
"http_accept": (
|
|
||||||
"django.db.models.fields.CharField",
|
|
||||||
[],
|
|
||||||
{"max_length": "1025"},
|
|
||||||
),
|
|
||||||
"id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}),
|
|
||||||
"ip_address": (
|
|
||||||
"django.db.models.fields.GenericIPAddressField",
|
|
||||||
[],
|
|
||||||
{"max_length": "39", "null": "True"},
|
|
||||||
),
|
|
||||||
"login_valid": (
|
|
||||||
"django.db.models.fields.BooleanField",
|
|
||||||
[],
|
|
||||||
{"default": "False"},
|
|
||||||
),
|
|
||||||
"path_info": (
|
|
||||||
"django.db.models.fields.CharField",
|
|
||||||
[],
|
|
||||||
{"max_length": "255"},
|
|
||||||
),
|
|
||||||
"user_agent": (
|
|
||||||
"django.db.models.fields.CharField",
|
|
||||||
[],
|
|
||||||
{"max_length": "255"},
|
|
||||||
),
|
|
||||||
"username": (
|
|
||||||
"django.db.models.fields.CharField",
|
|
||||||
[],
|
|
||||||
{"max_length": "255", "null": "True"},
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
complete_apps = ["defender"]
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from django.conf.urls import url, include
|
from django.urls import path
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .urls import urlpatterns as original_urlpatterns
|
from .urls import urlpatterns as original_urlpatterns
|
||||||
|
|
||||||
urlpatterns = [url(r"^admin/", admin.site.urls),] + original_urlpatterns
|
urlpatterns = [path("admin/", admin.site.urls),] + original_urlpatterns
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,15 @@
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import time
|
import time
|
||||||
from distutils.version import StrictVersion
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django import get_version
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.contrib.sessions.backends.db import SessionStore
|
from django.contrib.sessions.backends.db import SessionStore
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from redis.client import Redis
|
from redis.client import Redis
|
||||||
|
from django.urls import reverse
|
||||||
try:
|
|
||||||
from django.urls import reverse
|
|
||||||
except ImportError:
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
from . import config
|
from . import config
|
||||||
|
|
@ -33,8 +27,6 @@ from .test import DefenderTestCase, DefenderTransactionTestCase
|
||||||
LOGIN_FORM_KEY = '<form action="/admin/login/" method="post" id="login-form">'
|
LOGIN_FORM_KEY = '<form action="/admin/login/" method="post" id="login-form">'
|
||||||
ADMIN_LOGIN_URL = reverse("admin:login")
|
ADMIN_LOGIN_URL = reverse("admin:login")
|
||||||
|
|
||||||
DJANGO_VERSION = StrictVersion(get_version())
|
|
||||||
|
|
||||||
VALID_USERNAME = VALID_PASSWORD = "valid"
|
VALID_USERNAME = VALID_PASSWORD = "valid"
|
||||||
UPPER_USERNAME = "VALID"
|
UPPER_USERNAME = "VALID"
|
||||||
|
|
||||||
|
|
@ -400,12 +392,7 @@ class AccessAttemptTest(DefenderTestCase):
|
||||||
# Check if we are in the same login page
|
# Check if we are in the same login page
|
||||||
self.assertContains(response, LOGIN_FORM_KEY)
|
self.assertContains(response, LOGIN_FORM_KEY)
|
||||||
|
|
||||||
# RFC 7231 allows relative URIs in Location header.
|
lockout_url = "/o/login/"
|
||||||
# Django from version 1.9 is support this:
|
|
||||||
# https://docs.djangoproject.com/en/1.9/releases/1.9/#http-redirects-no-longer-forced-to-absolute-uris
|
|
||||||
lockout_url = "http://testserver/o/login/"
|
|
||||||
if DJANGO_VERSION >= StrictVersion("1.9"):
|
|
||||||
lockout_url = "/o/login/"
|
|
||||||
|
|
||||||
# So, we shouldn't have gotten a lock-out yet.
|
# So, we shouldn't have gotten a lock-out yet.
|
||||||
# But we should get one now, check redirect make sure it is valid.
|
# But we should get one now, check redirect make sure it is valid.
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
from django.conf.urls import url
|
from django.urls import path, re_path
|
||||||
from .views import block_view, unblock_ip_view, unblock_username_view
|
from .views import block_view, unblock_ip_view, unblock_username_view
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^blocks/$", block_view, name="defender_blocks_view"),
|
path("blocks/", block_view, name="defender_blocks_view"),
|
||||||
url(
|
re_path(
|
||||||
r"^blocks/ip/(?P<ip_address>[A-Za-z0-9-._]+)/unblock$",
|
"blocks/ip/(?P<ip_address>[A-Za-z0-9-._]+)/unblock",
|
||||||
unblock_ip_view,
|
unblock_ip_view,
|
||||||
name="defender_unblock_ip_view",
|
name="defender_unblock_ip_view",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^blocks/username/(?P<username>[\w]+[^\/]*)/unblock$",
|
"blocks/username/<path:username>/unblock",
|
||||||
unblock_username_view,
|
unblock_username_view,
|
||||||
name="defender_unblock_username_view",
|
name="defender_unblock_username_view",
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.contrib.admin.views.decorators import staff_member_required
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
try:
|
|
||||||
from django.urls import reverse
|
|
||||||
except ImportError:
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
|
|
||||||
from .utils import get_blocked_ips, get_blocked_usernames, unblock_ip, unblock_username
|
from .utils import get_blocked_ips, get_blocked_usernames, unblock_ip, unblock_username
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ This is just a simple example app, used for testing and showing how things work
|
||||||
```
|
```
|
||||||
mkdir -p exampleapp/static exampleapp/media/static
|
mkdir -p exampleapp/static exampleapp/media/static
|
||||||
|
|
||||||
PYTHONPATH=$PYTHONPATH:$PWD django-admin.py collectstatic --noinput --settings=exampleapp.settings
|
PYTHONPATH=$PYTHONPATH:$PWD django-admin collectstatic --noinput --settings=exampleapp.settings
|
||||||
|
|
||||||
PYTHONPATH=$PYTHONPATH:$PWD django-admin.py runserver --settings=exampleapp.settings
|
PYTHONPATH=$PYTHONPATH:$PWD django-admin runserver --settings=exampleapp.settings
|
||||||
```
|
```
|
||||||
|
|
|
||||||
3
setup.py
3
setup.py
|
|
@ -31,6 +31,9 @@ setup(
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Framework :: Django",
|
"Framework :: Django",
|
||||||
|
"Framework :: Django :: 2.2",
|
||||||
|
"Framework :: Django :: 3.0",
|
||||||
|
"Framework :: Django :: 3.1",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: Apache Software License",
|
"License :: OSI Approved :: Apache Software License",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
|
|
|
||||||
13
tox.ini
13
tox.ini
|
|
@ -1,16 +1,11 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
# list of supported Django/Python versioons:
|
# list of supported Django/Python versioons:
|
||||||
# https://docs.djangoproject.com/en/2.2/faq/install/#what-python-version-can-i-use-with-django
|
py{36,37,38,py3}-django{22,30,31,master}
|
||||||
py{35,36,37,py3}-django111
|
|
||||||
py35-django(21,22)
|
|
||||||
py{36,37,py3}-django{21,22,master}
|
|
||||||
py38-django22
|
|
||||||
py38-{lint,docs}
|
py38-{lint,docs}
|
||||||
|
|
||||||
[travis]
|
[travis]
|
||||||
python =
|
python =
|
||||||
3.5: py35
|
|
||||||
3.6: py36
|
3.6: py36
|
||||||
3.7: py37
|
3.7: py37
|
||||||
3.8: py38
|
3.8: py38
|
||||||
|
|
@ -19,13 +14,13 @@ python =
|
||||||
[testenv]
|
[testenv]
|
||||||
deps =
|
deps =
|
||||||
-rrequirements.txt
|
-rrequirements.txt
|
||||||
django111: django>=1.11,<2.0
|
|
||||||
django21: django>=2.1,<2.2
|
|
||||||
django22: django>=2.2,<2.3
|
django22: django>=2.2,<2.3
|
||||||
|
django30: django>=3.0,<3.1
|
||||||
|
django31: django>=3.1,<3.2
|
||||||
djangomaster: https://github.com/django/django/archive/master.tar.gz
|
djangomaster: https://github.com/django/django/archive/master.tar.gz
|
||||||
usedevelop = True
|
usedevelop = True
|
||||||
commands =
|
commands =
|
||||||
{envbindir}/coverage run --source=defender {envbindir}/django-admin.py test defender --settings=defender.travis_settings
|
{envbindir}/coverage run --source=defender {envbindir}/django-admin test defender --settings=defender.travis_settings
|
||||||
{envbindir}/coverage report -m
|
{envbindir}/coverage report -m
|
||||||
|
|
||||||
[testenv:py38-docs]
|
[testenv:py38-docs]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue