mirror of
https://github.com/jazzband/django-defender.git
synced 2026-03-16 22:10:32 +00:00
finished working on the defender admin, cleaned some stuff up, added some notes and screenshots
This commit is contained in:
parent
db3eea99cc
commit
12698d7d54
15 changed files with 292 additions and 128 deletions
|
|
@ -1,2 +1,2 @@
|
|||
[run]
|
||||
omit = *_settings.py, defender/*migrations/*
|
||||
omit = *_settings.py, defender/*migrations/*, defender/exampleapp/*
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -52,3 +52,7 @@ docs/_build/
|
|||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# exampleapp
|
||||
defender/exampleapp/static/
|
||||
defender/exampleapp/media/
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@ uses:
|
|||
autodetect: yes
|
||||
ignore-patterns:
|
||||
- .*_settings.py$
|
||||
- defender/exampleapp/*
|
||||
|
|
|
|||
60
README.md
60
README.md
|
|
@ -1,7 +1,7 @@
|
|||
django-defender
|
||||
===============
|
||||
|
||||
A simple django reusable app that blocks people from brute forcing login
|
||||
A simple Django reusable app that blocks people from brute forcing login
|
||||
attempts. The goal is to make this as fast as possible, so that we do not
|
||||
slow down the login attempts.
|
||||
|
||||
|
|
@ -129,63 +129,40 @@ How it works
|
|||
|
||||
1. When someone tries to login, we first check to see if they are currently
|
||||
blocked. We check the username they are trying to use, as well as the IP
|
||||
address. If they are blocked, goto step 10. If not blocked go to step 2.
|
||||
address. If they are blocked, goto step 5. If not blocked go to step 2.
|
||||
|
||||
2. They are not blocked, so we check to see if the login was valid. If valid
|
||||
go to step 20. If not valid go to step 3.
|
||||
go to step 6. If not valid go to step 3.
|
||||
|
||||
3. Login attempt wasn't valid. Add their username and IP address for this
|
||||
attempt to the cache. If this brings them over the limit, add them to the
|
||||
blocked list, and then goto step 10. If not over the limit goto step 4.
|
||||
blocked list, and then goto step 5. If not over the limit goto step 4.
|
||||
|
||||
4. login was invalid, but not over the limit. Send them back to the login screen
|
||||
to try again.
|
||||
|
||||
|
||||
10. User is blocked: Send them to the blocked page, telling them they are
|
||||
5. User is blocked: Send them to the blocked page, telling them they are
|
||||
blocked, and give an estimate on when they will be unblocked.
|
||||
|
||||
20. Login is valid. Reset any failed login attempts, and forward to their
|
||||
6. Login is valid. Reset any failed login attempts, and forward to their
|
||||
destination.
|
||||
|
||||
|
||||
Cache backend:
|
||||
==============
|
||||
|
||||
- ip_attempts (count, TTL)
|
||||
- username_attempts (count, TTL)
|
||||
- ip_blocks (list)
|
||||
- username_blocks (list)
|
||||
|
||||
cache keys:
|
||||
-----------
|
||||
|
||||
Counters:
|
||||
- prefix:failed:ip:[ip] (count, TTL)
|
||||
- prefix:failed:username:[username] (count, TTL)
|
||||
|
||||
Booleans (if present it is blocked):
|
||||
- prefix:blocked:ip:[ip] (true, TTL)
|
||||
- prefix:blocked:username:[username] (true, TTL)
|
||||
|
||||
Rate limiting Example
|
||||
---------------------
|
||||
```
|
||||
# example of how to do rate limiting by IP
|
||||
# assuming it is 10 requests being the limit
|
||||
# this assumes there is a DECAY of DECAY_TIME
|
||||
# to remove invalid logins after a set number of time
|
||||
# For every incorrect login, we reset the block time.
|
||||
|
||||
FUNCTION LIMIT_API_CALL(ip)
|
||||
current = LLEN(ip)
|
||||
IF current > 10 THEN
|
||||
ERROR "too many requests per second"
|
||||
ELSE
|
||||
MULTI
|
||||
RPUSH(ip, ip)
|
||||
EXPIRE(ip, DECAY_TIME)
|
||||
EXEC
|
||||
END
|
||||
```
|
||||
|
||||
Installing Django-defender
|
||||
==========================
|
||||
|
||||
|
|
@ -229,6 +206,25 @@ Next, install the ``FailedLoginMiddleware`` middleware::
|
|||
)
|
||||
```
|
||||
|
||||
If you want to manage the blocked users via the Django admin, then add the
|
||||
following to your ``urls.py``
|
||||
|
||||
```
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
(r'^admin/', include(admin.site.urls)), # normal admin
|
||||
(r'^admin/defender/', include('defender.urls')), # defender admin
|
||||
# your own patterns follow…
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
Admin Pages:
|
||||
------------
|
||||

|
||||
|
||||

|
||||
|
||||
Database tables:
|
||||
----------------
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
from django.contrib import admin
|
||||
from django.conf.urls import patterns, url
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template import RequestContext
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from .models import AccessAttempt
|
||||
from .utils import (
|
||||
get_blocked_ips, get_blocked_usernames, unblock_ip, unblock_username)
|
||||
|
||||
|
||||
class AccessAttemptAdmin(admin.ModelAdmin):
|
||||
|
|
@ -40,53 +32,10 @@ class AccessAttemptAdmin(admin.ModelAdmin):
|
|||
(None, {
|
||||
'fields': ('path_info', 'login_valid')
|
||||
}),
|
||||
('Form Data', {
|
||||
'fields': ('get_data', 'post_data')
|
||||
}),
|
||||
('Meta Data', {
|
||||
'fields': ('user_agent', 'ip_address', 'http_accept')
|
||||
'fields': ('user_agent', 'ip_address')
|
||||
})
|
||||
)
|
||||
|
||||
def get_urls(self):
|
||||
""" get the default urls and add ours """
|
||||
urls = super(AccessAttemptAdmin, self).get_urls()
|
||||
my_urls = patterns(
|
||||
'',
|
||||
url(r'^blocks/$',
|
||||
self.admin_site.admin_view(self.block_view),
|
||||
name="defender_blocks_view"),
|
||||
url(r'^blocks/ip/(?P<ip>\w+)/unblock$',
|
||||
self.admin_site.admin_view(self.unblock_ip_view),
|
||||
name="defender_unblock_ip_view"),
|
||||
url(r'^blocks/username/(?P<username>\w+)/unblock$',
|
||||
self.admin_site.admin_view(self.unblock_username_view),
|
||||
name="defender_unblock_username_view"),
|
||||
)
|
||||
return my_urls + urls
|
||||
|
||||
def block_view(self, request):
|
||||
""" List the blocked IP and Usernames """
|
||||
blocked_ip_list = get_blocked_ips()
|
||||
blocked_username_list = get_blocked_usernames()
|
||||
|
||||
context = {'current_app': self.admin_site.name,
|
||||
'blocked_ip_list': blocked_ip_list,
|
||||
'blocked_username_list': blocked_username_list}
|
||||
return render_to_response(
|
||||
'admin/defender/blocks.html',
|
||||
context, context_instance=RequestContext(request))
|
||||
|
||||
def unblock_ip_view(self, request, ip):
|
||||
""" upblock the given ip """
|
||||
if request.method == 'POST':
|
||||
unblock_ip(ip)
|
||||
return HttpResponseRedirect(reverse("defender_blocks_view"))
|
||||
|
||||
def unblock_username_view(self, request, username):
|
||||
""" unblockt he given username """
|
||||
if request.method == 'POST':
|
||||
unblock_username(username)
|
||||
return HttpResponseRedirect(reverse("defender_blocks_view"))
|
||||
|
||||
admin.site.register(AccessAttempt, AccessAttemptAdmin)
|
||||
|
|
|
|||
0
defender/exampleapp/__init__.py
Normal file
0
defender/exampleapp/__init__.py
Normal file
BIN
defender/exampleapp/defender.sb
Normal file
BIN
defender/exampleapp/defender.sb
Normal file
Binary file not shown.
14
defender/exampleapp/readme.md
Normal file
14
defender/exampleapp/readme.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
Example App
|
||||
===========
|
||||
|
||||
admin password is ``admin:password``
|
||||
|
||||
|
||||
This is just a simple example app, used for testing and showing how things work
|
||||
```
|
||||
mkdir -p exampleapp/static exampleapp/media/static
|
||||
|
||||
PYTHONPATH=$PYTHONPATH:$PWD django-admin.py collectstatic --noinput --settings=defender.exampleapp.settings
|
||||
|
||||
PYTHONPATH=$PYTHONPATH:$PWD django-admin.py runserver --settings=defender.exampleapp.settings
|
||||
```
|
||||
81
defender/exampleapp/settings.py
Normal file
81
defender/exampleapp/settings.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import os
|
||||
PROJECT_DIR = lambda base: os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), base).replace('\\', '/'))
|
||||
|
||||
|
||||
MEDIA_ROOT = PROJECT_DIR(os.path.join('media'))
|
||||
MEDIA_URL = '/media/'
|
||||
STATIC_ROOT = PROJECT_DIR(os.path.join('static'))
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
STATICFILES_DIRS = (
|
||||
PROJECT_DIR(os.path.join('media', 'static')),
|
||||
)
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': PROJECT_DIR('defender.sb'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'defender.middleware.FailedLoginMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'defender.exampleapp.urls'
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.staticfiles',
|
||||
'defender',
|
||||
]
|
||||
|
||||
# List of finder classes that know how to find static files in
|
||||
# various locations.
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
)
|
||||
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY', 'too-secret-for-test')
|
||||
|
||||
LOGIN_REDIRECT_URL = '/admin'
|
||||
|
||||
DEFENDER_LOGIN_FAILURE_LIMIT = 1
|
||||
DEFENDER_COOLOFF_TIME = 60
|
||||
DEFENDER_REDIS_URL = "redis://localhost:6379/1"
|
||||
# don't use mock redis in unit tests, we will use real redis on travis.
|
||||
DEFENDER_MOCK_REDIS = False
|
||||
|
||||
# Celery settings:
|
||||
CELERY_ALWAYS_EAGER = True
|
||||
BROKER_BACKEND = 'memory'
|
||||
BROKER_URL = 'memory://'
|
||||
|
||||
import os
|
||||
|
||||
from celery import Celery
|
||||
|
||||
# set the default Django settings module for the 'celery' program.
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'defender.exampleapp.settings')
|
||||
|
||||
app = Celery('defender')
|
||||
|
||||
# Using a string here means the worker will not have to
|
||||
# pickle the object when using Windows.
|
||||
app.config_from_object('django.conf:settings')
|
||||
app.autodiscover_tasks(lambda: INSTALLED_APPS)
|
||||
|
||||
DEBUG = True
|
||||
17
defender/exampleapp/urls.py
Normal file
17
defender/exampleapp/urls.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from django.conf.urls import patterns, include
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.conf.urls.static import static
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
(r'^admin/', include(admin.site.urls)),
|
||||
(r'^admin/defender/', include('defender.urls')),
|
||||
)
|
||||
|
||||
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
25
defender/templates/admin/defender/app_index.html
Normal file
25
defender/templates/admin/defender/app_index.html
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{% extends "admin/index.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if not is_popup %}
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||
›
|
||||
{% for app in app_list %}
|
||||
{{ app.name }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
{% block sidebar %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ block.super }}
|
||||
<div class="app-defender module">
|
||||
<table><tr scope='row'><td colspan='3'>
|
||||
<h4><a href='{% url 'defender_blocks_view' %}'>Blocked Users</a></h4>
|
||||
</td></tr></table>
|
||||
</div>
|
||||
{% endblock content%}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
{% extends "admin/change_list.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block result_list %}
|
||||
<h1>Blocked Logins</h1>
|
||||
<p>Here is a list of IP's and usernames that are blocked</p>
|
||||
|
||||
<h2>Blocked IP's</h2>
|
||||
<table>
|
||||
<tr><th>IP</th><th>Action</th></tr>
|
||||
{% for block in blocked_ip_list %}
|
||||
<tr>
|
||||
<td>{{block}}</td>
|
||||
<td>
|
||||
<form method='POST' action="{% url 'defender_unblock_ip_view' block %}">
|
||||
{% csrf_token %}
|
||||
<input type='submit' value='unblock' />
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan='2'>No IP's</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<h2>Blocked Usernames</h2>
|
||||
<table>
|
||||
<tr><th>Username</th><th>Action</th></tr>
|
||||
{% for block in blocked_username_list %}
|
||||
<tr>
|
||||
<td>{{block}}</td>
|
||||
<td>
|
||||
<form method='POST' action="{% url 'defender_unblock_username_view' block %}">
|
||||
{% csrf_token %}
|
||||
<input type='submit' value='unblock' />
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan='2'>No Username's</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock result_list %}
|
||||
74
defender/templates/defender/admin/blocks.html
Normal file
74
defender/templates/defender/admin/blocks.html
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n admin_urls %}
|
||||
|
||||
{% block bodyclass %}dashboard{% endblock %}
|
||||
|
||||
{% block extrastyle %}
|
||||
{{ block.super }}
|
||||
<style>table {width: 100%;}</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url "admin:index" %}">Home</a> ›
|
||||
<a href="{% url "admin:app_list" "defender" %}">Defender</a> ›
|
||||
</div>
|
||||
{% endblock breadcrumbs %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content-main">
|
||||
|
||||
<h1>Blocked Logins</h1>
|
||||
<p>Here is a list of IP's and usernames that are blocked</p>
|
||||
|
||||
<div class="module">
|
||||
|
||||
<table>
|
||||
<caption>Blocked IP's</caption>
|
||||
<thead>
|
||||
<tr><th>IP</th><th>Action</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for block in blocked_ip_list %}
|
||||
<tr class="{% cycle "row2" "row1" %}">
|
||||
<td>{{block}}</td>
|
||||
<td>
|
||||
<form method='POST' action="{% url 'defender_unblock_ip_view' block %}">
|
||||
{% csrf_token %}
|
||||
<input type='submit' value='unblock' />
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan='2'>No IP's</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="module">
|
||||
<table>
|
||||
<caption>Blocked Usernames</caption>
|
||||
<thead>
|
||||
<tr><th>Usernames</th><th>Action</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for block in blocked_username_list %}
|
||||
<tr class="{% cycle "row2" "row1" %}">
|
||||
<td>{{block}}</td>
|
||||
<td>
|
||||
<form method='POST' action="{% url 'defender_unblock_username_view' block %}">
|
||||
{% csrf_token %}
|
||||
<input type='submit' value='unblock' />
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan='2'>No Username's</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
13
defender/urls.py
Normal file
13
defender/urls.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from django.conf.urls import patterns, url
|
||||
from views import block_view, unblock_ip_view, unblock_username_view
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^blocks/$', block_view,
|
||||
name="defender_blocks_view"),
|
||||
url(r'^blocks/ip/(?P<ip>[a-z0-9-._]+)/unblock$', unblock_ip_view,
|
||||
name="defender_unblock_ip_view"),
|
||||
url(r'^blocks/username/(?P<username>[a-z0-9-._@]+)/unblock$',
|
||||
unblock_username_view,
|
||||
name="defender_unblock_username_view"),
|
||||
)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
from django.shortcuts import render_to_response
|
||||
from django.template import RequestContext
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from .utils import (
|
||||
get_blocked_ips, get_blocked_usernames, unblock_ip, unblock_username)
|
||||
|
||||
|
||||
def block_view(request):
|
||||
""" List the blocked IP and Usernames """
|
||||
blocked_ip_list = get_blocked_ips()
|
||||
blocked_username_list = get_blocked_usernames()
|
||||
|
||||
context = {'blocked_ip_list': blocked_ip_list,
|
||||
'blocked_username_list': blocked_username_list}
|
||||
return render_to_response(
|
||||
'defender/admin/blocks.html',
|
||||
context, context_instance=RequestContext(request))
|
||||
|
||||
|
||||
def unblock_ip_view(request, ip):
|
||||
""" upblock the given ip """
|
||||
if request.method == 'POST':
|
||||
unblock_ip(ip)
|
||||
return HttpResponseRedirect(reverse("defender_blocks_view"))
|
||||
|
||||
|
||||
def unblock_username_view(request, username):
|
||||
""" unblockt he given username """
|
||||
if request.method == 'POST':
|
||||
unblock_username(username)
|
||||
return HttpResponseRedirect(reverse("defender_blocks_view"))
|
||||
Loading…
Reference in a new issue