mirror of
https://github.com/jazzband/django-defender.git
synced 2026-03-16 22:10:32 +00:00
Merge pull request #25 from kencochrane/new-admin
initial commit, adding admin pages to manage blocked users
This commit is contained in:
commit
58daedcd05
16 changed files with 376 additions and 57 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/*
|
||||
|
|
|
|||
87
README.md
87
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.
|
||||
|
||||
|
|
@ -18,6 +18,10 @@ Build status
|
|||
|
||||
[](https://travis-ci.org/kencochrane/django-defender) [](https://coveralls.io/r/kencochrane/django-defender)[](https://landscape.io/github/kencochrane/django-defender/master)
|
||||
|
||||
Sites using Defender:
|
||||
=====================
|
||||
- https://hub.docker.com
|
||||
|
||||
Goals for 0.1
|
||||
=============
|
||||
|
||||
|
|
@ -129,63 +133,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 +210,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:
|
||||
----------------
|
||||
|
||||
|
|
@ -257,30 +257,31 @@ You have a couple options available to you to customize ``django-defender`` a bi
|
|||
These should be defined in your ``settings.py`` file.
|
||||
|
||||
* ``DEFENDER_LOGIN_FAILURE_LIMIT``: Int: The number of login attempts allowed before a
|
||||
record is created for the failed logins. Default: ``3``
|
||||
record is created for the failed logins. [Default: ``3``]
|
||||
* ``DEFENDER_USE_USER_AGENT``: Boolean: 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``
|
||||
the same IP are treated differently. [Default: ``False``]
|
||||
* ``DEFENDER_COOLOFF_TIME``: Int: If set, defines a period of inactivity after which
|
||||
old failed login attempts will be forgotten. An integer, will be interpreted as a
|
||||
number of seconds. If ``0``, the locks will not expire. Default: ``300``
|
||||
* ``DEFENDER_LOCKOUT_TEMPLATE``: String: 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``
|
||||
number of seconds. If ``0``, the locks will not expire. [Default: ``300``]
|
||||
* ``DEFENDER_LOCKOUT_TEMPLATE``: String: [Default: ``None``] If set, specifies a template to render when a user is locked out. Template receives the following context variables:
|
||||
- ``cooloff_time_seconds``: The cool off time in seconds
|
||||
- ``cooloff_time_minutes``: The cool off time in minutes
|
||||
- ``failure_limit``: The number of failures before you get blocked.
|
||||
* ``DEFENDER_USERNAME_FORM_FIELD``: String: the name of the form field that contains your
|
||||
users usernames. Default: ``username``
|
||||
users usernames. [Default: ``username``]
|
||||
* ``DEFENDER_REVERSE_PROXY_HEADER``: String: the name of the http header with your
|
||||
reverse proxy IP address Default: ``HTTP_X_FORWARDED_FOR``
|
||||
reverse proxy IP address [Default: ``HTTP_X_FORWARDED_FOR``]
|
||||
* ``DEFENDER_CACHE_PREFIX``: String: The cache prefix for your defender keys.
|
||||
Default: ``defender``
|
||||
[Default: ``defender``]
|
||||
* ``DEFENDER_LOCKOUT_URL``: String: The URL you want to redirect to if someone is
|
||||
locked out.
|
||||
* ``DEFENDER_REDIS_URL``: String: the redis url for defender.
|
||||
Default: ``redis://localhost:6379/0``
|
||||
[Default: ``redis://localhost:6379/0``]
|
||||
(Example with password: ``redis://:mypassword@localhost:6379/0``)
|
||||
* ``DEFENDER_USE_CELERY``: Boolean: If you want to use Celery to store the login
|
||||
attempt to the database, set to True. If False, it is saved inline.
|
||||
Default: ``False``
|
||||
[Default: ``False``]
|
||||
|
||||
Running Tests
|
||||
=============
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import AccessAttempt
|
||||
|
||||
|
||||
|
|
@ -33,12 +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')
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
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%}
|
||||
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 %}
|
||||
|
|
@ -2,6 +2,6 @@
|
|||
<body>
|
||||
<h1>Locked out</h1>
|
||||
<p>Your have attempted to login {{failure_limit}} times, with no success.
|
||||
Your account is locked for {{cooloff_time}} seconds</p>
|
||||
Your account is locked for {{cooloff_time_seconds}} seconds</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
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"),
|
||||
)
|
||||
|
|
@ -97,6 +97,36 @@ def get_username_blocked_cache_key(username):
|
|||
return "{0}:blocked:username:{1}".format(config.CACHE_PREFIX, username)
|
||||
|
||||
|
||||
def strip_keys(key_list):
|
||||
""" Given a list of keys, remove the prefix and remove just
|
||||
the data we care about.
|
||||
|
||||
for example:
|
||||
|
||||
['defender:blocked:ip:ken', 'defender:blocked:ip:joffrey']
|
||||
|
||||
would result in:
|
||||
|
||||
['ken', 'joffrey']
|
||||
|
||||
"""
|
||||
return [key.split(":")[-1] for key in key_list]
|
||||
|
||||
|
||||
def get_blocked_ips():
|
||||
""" get a list of blocked ips from redis """
|
||||
key = get_ip_blocked_cache_key("*")
|
||||
key_list = redis_server.keys(key)
|
||||
return strip_keys(key_list)
|
||||
|
||||
|
||||
def get_blocked_usernames():
|
||||
""" get a list of blocked usernames from redis """
|
||||
key = get_username_blocked_cache_key("*")
|
||||
key_list = redis_server.keys(key)
|
||||
return strip_keys(key_list)
|
||||
|
||||
|
||||
def increment_key(key):
|
||||
""" given a key increment the value """
|
||||
pipe = redis_server.pipeline()
|
||||
|
|
@ -163,16 +193,40 @@ def record_failed_attempt(ip, username):
|
|||
return True
|
||||
|
||||
|
||||
def unblock_ip(ip, pipe=None):
|
||||
""" unblock the given IP """
|
||||
do_commit = False
|
||||
if not pipe:
|
||||
pipe = redis_server.pipeline()
|
||||
do_commit = True
|
||||
if ip:
|
||||
pipe.delete(get_ip_attempt_cache_key(ip))
|
||||
pipe.delete(get_ip_blocked_cache_key(ip))
|
||||
if do_commit:
|
||||
pipe.execute()
|
||||
|
||||
|
||||
def unblock_username(username, pipe=None):
|
||||
""" unblock the given Username """
|
||||
do_commit = False
|
||||
if not pipe:
|
||||
pipe = redis_server.pipeline()
|
||||
do_commit = True
|
||||
if username:
|
||||
pipe.delete(get_username_attempt_cache_key(username))
|
||||
pipe.delete(get_username_blocked_cache_key(username))
|
||||
if do_commit:
|
||||
pipe.execute()
|
||||
|
||||
|
||||
def reset_failed_attempts(ip=None, username=None):
|
||||
""" reset the failed attempts for these ip's and usernames
|
||||
"""
|
||||
pipe = redis_server.pipeline()
|
||||
if ip:
|
||||
pipe.delete(get_ip_attempt_cache_key(ip))
|
||||
pipe.delete(get_ip_blocked_cache_key(ip))
|
||||
if username:
|
||||
pipe.delete(get_username_attempt_cache_key(username))
|
||||
pipe.delete(get_username_blocked_cache_key(username))
|
||||
|
||||
unblock_ip(ip, pipe=pipe)
|
||||
unblock_username(username, pipe=pipe)
|
||||
|
||||
pipe.execute()
|
||||
|
||||
|
||||
|
|
@ -180,7 +234,8 @@ def lockout_response(request):
|
|||
""" if we are locked out, here is the response """
|
||||
if config.LOCKOUT_TEMPLATE:
|
||||
context = {
|
||||
'cooloff_time': config.COOLOFF_TIME,
|
||||
'cooloff_time_seconds': config.COOLOFF_TIME,
|
||||
'cooloff_time_minutes': config.COOLOFF_TIME / 60,
|
||||
'failure_limit': config.FAILURE_LIMIT,
|
||||
}
|
||||
return render_to_response(config.LOCKOUT_TEMPLATE, context,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
from django.shortcuts import render_to_response
|
||||
from django.template import RequestContext
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
|
||||
from .utils import (
|
||||
get_blocked_ips, get_blocked_usernames, unblock_ip, unblock_username)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
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))
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def unblock_ip_view(request, ip):
|
||||
""" upblock the given ip """
|
||||
if request.method == 'POST':
|
||||
unblock_ip(ip)
|
||||
return HttpResponseRedirect(reverse("defender_blocks_view"))
|
||||
|
||||
|
||||
@staff_member_required
|
||||
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