finished working on the defender admin, cleaned some stuff up, added some notes and screenshots

This commit is contained in:
Ken Cochrane 2015-01-28 20:19:16 -05:00
parent db3eea99cc
commit 12698d7d54
15 changed files with 292 additions and 128 deletions

View file

@ -1,2 +1,2 @@
[run]
omit = *_settings.py, defender/*migrations/*
omit = *_settings.py, defender/*migrations/*, defender/exampleapp/*

4
.gitignore vendored
View file

@ -52,3 +52,7 @@ docs/_build/
# PyBuilder
target/
# exampleapp
defender/exampleapp/static/
defender/exampleapp/media/

View file

@ -8,3 +8,4 @@ uses:
autodetect: yes
ignore-patterns:
- .*_settings.py$
- defender/exampleapp/*

View file

@ -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:
------------
![alt tag](https://cloud.githubusercontent.com/assets/261601/5950540/8895b570-a729-11e4-9dc3-6b00e46c8043.png)
![alt tag](https://cloud.githubusercontent.com/assets/261601/5950541/88a35194-a729-11e4-981b-3a55b44ef9d5.png)
Database tables:
----------------

View file

@ -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)

View file

Binary file not shown.

View 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
```

View 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

View 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)

View 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>
&rsaquo;
{% 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%}

View file

@ -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 %}

View 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> &rsaquo;
<a href="{% url "admin:app_list" "defender" %}">Defender</a> &rsaquo;
</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
View 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"),
)

View file

@ -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"))