From 12698d7d54a12b2ad250017a4c81b5a4e4d91f3d Mon Sep 17 00:00:00 2001 From: Ken Cochrane Date: Wed, 28 Jan 2015 20:19:16 -0500 Subject: [PATCH] finished working on the defender admin, cleaned some stuff up, added some notes and screenshots --- .coveragerc | 2 +- .gitignore | 4 + .landscape.yaml | 1 + README.md | 60 ++++++------- defender/admin.py | 53 +----------- defender/exampleapp/__init__.py | 0 defender/exampleapp/defender.sb | Bin 0 -> 139264 bytes defender/exampleapp/readme.md | 14 +++ defender/exampleapp/settings.py | 81 ++++++++++++++++++ defender/exampleapp/urls.py | 17 ++++ .../templates/admin/defender/app_index.html | 25 ++++++ defender/templates/admin/defender/blocks.html | 43 ---------- defender/templates/defender/admin/blocks.html | 74 ++++++++++++++++ defender/urls.py | 13 +++ defender/views.py | 33 +++++++ 15 files changed, 292 insertions(+), 128 deletions(-) create mode 100644 defender/exampleapp/__init__.py create mode 100644 defender/exampleapp/defender.sb create mode 100644 defender/exampleapp/readme.md create mode 100644 defender/exampleapp/settings.py create mode 100644 defender/exampleapp/urls.py create mode 100644 defender/templates/admin/defender/app_index.html delete mode 100644 defender/templates/admin/defender/blocks.html create mode 100644 defender/templates/defender/admin/blocks.html create mode 100644 defender/urls.py diff --git a/.coveragerc b/.coveragerc index cd0537e..52aa906 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,2 @@ [run] -omit = *_settings.py, defender/*migrations/* +omit = *_settings.py, defender/*migrations/*, defender/exampleapp/* diff --git a/.gitignore b/.gitignore index db4561e..2e3ea0c 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,7 @@ docs/_build/ # PyBuilder target/ + +# exampleapp +defender/exampleapp/static/ +defender/exampleapp/media/ diff --git a/.landscape.yaml b/.landscape.yaml index ee0d355..83151ce 100644 --- a/.landscape.yaml +++ b/.landscape.yaml @@ -8,3 +8,4 @@ uses: autodetect: yes ignore-patterns: - .*_settings.py$ + - defender/exampleapp/* diff --git a/README.md b/README.md index e2812ee..332aa43 100644 --- a/README.md +++ b/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: +------------ +![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: ---------------- diff --git a/defender/admin.py b/defender/admin.py index afdf646..491c295 100644 --- a/defender/admin.py +++ b/defender/admin.py @@ -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\w+)/unblock$', - self.admin_site.admin_view(self.unblock_ip_view), - name="defender_unblock_ip_view"), - url(r'^blocks/username/(?P\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) diff --git a/defender/exampleapp/__init__.py b/defender/exampleapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/defender/exampleapp/defender.sb b/defender/exampleapp/defender.sb new file mode 100644 index 0000000000000000000000000000000000000000..604e87b008b60f0947b17e9bb9a3cdfd2ebfe938 GIT binary patch literal 139264 zcmeI5TWlNId4Ojm-X(Et+49)dYBi?UvAMFyxp2rCxyG&{%WJJJ&Pw*$5|DC763liafLdnjk=f1VsuYF9n(wMH>`o(_4a|?L+g@pjaTA zUd}l)B!|P1wA$Ke%>J}Jo;l|~=l}ojoHKK7jtBTh|+vQnOjARkKn? zFQoL%lq-B}Zgqb4&OEoUa%28J7jwn7Q^mO3D?PC>?n*3Is~URM$QqC8dbX60#rLUG zK~`1i&yE|~W?9ec1s$2w$ZENq-fU`yp;zjLcj#%`mOHbbT$<-%-a<%eghQVgQpS~# z@}l10R&L+nR_-n>ai3aUSe{*7=N9MJuUcX|O{~0#Y>aWcS|himHLi%V9B-$wK@eu7Xmaba~hHDHnEE z^l^b}IRn?X!_6n9;#q>L^Ec;L=U3+D*X;U+MR85TmCI)Cu5|5s;7n}0>6CS4Q`>x? z=L}qHIYZww?9A=ZhF(W%+ck#gduojBXrZhXV_dVMmCL9)ay!*#saVzX?eb{ZyoE|Z z&sK0FD58SJ8rw3FxOw$tkiB@3elqDSvnINY*?!>DQO}aXF~=>We66CDsvZ9X5C6KW zyzBsg%)P+`*?63OGS_7;at@V!%VO*5gtyUFWZUkhb)DVTA9busIdpn7?N%DpMl=^t z8@j$%FE#WmZbTkUDH6YSBEa5`(-gU5Hs3BIl38>MtdZ+%w;x%d`}*XQ=tz*2BznJ# zfwR8Z37kPuPZ~~%HWJU~(R+jIE+(&n)avzYS=-dh9kO0xm0JD?k-IAkZ`_?nZLyQ% zRW4?wi$~(~(Ez(H9YNFSY8$CDklBBID9BDu((mO+-6D@9d=qG&40khZhaZ<*U28TU z)*7h!x0<;iDm~4-tThdM+r-V~pu2IYnQiW%hplz5>bf1jmz3nJ+!k3|woqyy zl?O}PV#Kf`rF7KL9R;81eJ)|+4XsdcJx6RUC>G!7c2NWJ@(UBmK0pmdM;LqXMB>75 zfSsQ_s7|ztE-SQ8N1j{`2ie(KdVjsU{Ii&$qf66lC+Ufa98g1g1dm%~hzVW|)28L< zaCu2_&Ic4Rzmou&;&{fjn_5dFYG@r<($k8>*CGM7ID0f)H#i6F9wI~L;~?oh>|b`_ zFE(mBbq93oI&=_fEBa{zCb^PFc2D20hb@sk8f!aCbPIbW6kyk;k0xq4=j3RGgw0`p zkiBq$HXS!F5=BazoCQ3Hk%;56>Tdf=@BJhamjVIywF`&(LsZ+Ikn`Y^i9ynZ-@n_b z0hUy!23qx?C)9V4Z_ajmTt6)>UhV0bwYs)SuV%OC3$R)BBS3ZFasqa;@IQ&tL3V12 z-e2p$?xb_wBL+P1c7%`1wY&Z}J9su%=VRHk&9&w7dUs|X{fqV;&7CVkoI_m?k^yRh z*9QXZ8&ikU?YL}X?F69VG&?+kj%SB&Qq12lUtvDOXv}ryT=YMqKZt%a`uXUmqgSJm zkspnGYvfBK?~ZJY$Rm;Ae;oem@H;30e1HHD00KY&2mk>f00e*l5cq#eKsxKEUZLeV zt;(T^Q5`KPT5DLGy+RYRLc3SCB(|Lhts_gWbyosg zzav3B(k=sRLzgr*#39Os+vZw|IXrbYXd8jgxlmie)=ngBt@!+WunRAV#%l0MEa0c6 ztunUrCleh5wgPey^S5Pe4VH|pf?T}d^HZ1U$yO0s0$2$-n;P^}nJ}r&wwBKL+}6|y zm>Osc+v+-Tw#rVxB^nibm{bppvJ-)IcdjDnj&(LVN-;lS{+jt?<`0gvXX$;b+Et^c3x$Y8kUa?TlIzCZ|Vz^d&lo z^JoQ~I_aY?(#|=PQ7gnwiswO2Mt$@I9l*)(<%!ciI!?RhKo~2qmCBmVh?1f00e*l5C8%|00;m9AOHjq0a*Ws zY5)X)01yBIKmZ5;0U!VbfB+Bx0zjbu3BdY)|Ccd%2n2ut5C8%|00;m9AOHk_01yBI zKmgYNp$-56AOHk_01yBIKmZ5;0U!VbfB+Eae*&=n-~VL{9s&U%00e*l5C8%|00;m9 zAOHk_01$xnf2adM00;m9AOHk_01yBIKmZ5;0U!Vb`kw%-|M!0xgNHx>2mk>f00e*l z5C8%|00;m9AOHmL`ajLAQ|K>zfB+Bx0zd!=00AHX1b_e#00KY&2mpcO5-?w)M=101 zY?vJxNruC-_08>kLCiL{G*M2!B#1oEzqFLomX*8w-PL=$OE1muZr7BR8>_{|<^1yN zno_G&9xZ4*zqY=3b5@>f&Pn(77WW>1YI&ExF7kpr%?s0_!U>rfUYU_HiHw+5WnRly zN>y@rRo~Mp^|GGG)ha#mf}D^uf*=Zb{hww&pwM6V00AHX1b_e#00KY&2mk>f00e*l z5C8)GMIbcDhRG}eUjGjf z00e+Q{}Z?p@>B7N%UUj{H=CSh7<#2{bP7;mDCOSNk2Kl}eHK0kHo(#2e@YM=}_<569=&+%!~ zaei_TWj!{w(?s^{7+d)NQv-gA9b=1)+D;vZk&_A9PhA+hP}duk648ibI#+o8Kg4Jh z^Dgrb%ukrVV7|_1%$Jxi^nVe9hd=-b00AHX1b_e#00KY&2mk>f00f>Qf%CqJ%e3dr zL1@&+PS9QR1;MC~9i!V*1%cDPiE-LBMc{wIcj+SSmf00e*l5ctI;@H5jt zyIbAMEA_N0n8gAA@{><@0#c8w%BC!)w^AudtnW0M6=SPZ-dn!6ATK}8Z>`+8D{Z`X zbNlvc!`w)eR>-W|lp=h{LHy}9*! zD~*kN^7caMVd=hz<+cj>`zz&KS*_#zmhZQIFF>3Y1jp|MNW7X(MVP-s-~ayx#e4%D z-~$AJ01yBIKmZ5;0U!VbfB+Bx0zd!={2~!}X>gft&sH2TIWd3?eo3oIn}zabK3^_M zVzIVe+!b{7!NX!>>%pT(dy-(S|Nod`e*BB%9@qi|fB+Bx0zd!=00AHX1b_e#00KY& z2ppHddD=%^yy$oxfa4U8|EUwl%@O2*01yBIKmZ5;0U!VbfB+Bx0zd!=0D8fq@3WnE6ZB0R5fbNt!vx@`d!2_hXT0UYutTK;1lXKuCY5KA;CEJnx5OP zCDF?cdGuG{Zk8H)p|+RAk%E{>@aT`Qtygok+*U)Y>aP|nTB(fRk7($7Msmxjl&@;_ zdbyO-4D{2C$vqr0wdW44l&`&g9Vx#0a`I(zrX6sLIN{XtSlFW^8-#I5K2|>*$xXp$7%sF|6mlF!QqhwIY1^dxCo=>YH zD!D+PIhUGAr4y-CikD<_!{2w#Nn%3D$cmUYH+=o*9Ir^KgsSCW|2Zcl_>_>56mw&s z@0=5rgd&QfDw^-m$hmhWS@Xj9Z1^J&JhN(6j&rkUxTxRLHy2ArQbs**Da~D3yme=J z=_*$)ZR^&EG|tU!HEI<-DWNW3LKHG+{RXHDH%zAyzX?Vq80VzLw&P; z=*ToP!=u3|>cXJm{Do2c`~PPdgJOQpe8BuW^KZ;QGylZ=E%SZm&zV1AzQgf00e*l5C8%|00;m9AaE!F{~%3= z&XOZbj%V;OI7W`A$?+6E21d#8BsreIM?XW3QF0u?N8d0x4v}L79|yza7$V0YJ`M!P z(NB&(-(Z*?#D{^w!EhLl|6%|Cp~VG}KmZ5;0U!VbfB+Bx0zd!=00AHX1fF{W`1}9E z!S7JP?}Yv;JQ@)rpBah{%?-agY>xbXlw$rY^u_R3!at3CEAqk6mq#|Df6wHP;qsFx z9b~7b=>0WA+brw&&4byx-l&wCO|-w>bvZD%IzM}7p1U*q$)$NN=8BDRS0Wtx#7g-X zhrSJ<7xf0Wa{CUqa(8Ko`_$^f^6ct5w>ZCk)e@^}6+Oo7YK`2M*0>_0F9)=QZ2|iQ zido}PUC(-D+Lp`f-I<|p7r1k~yRz`c-Ffax%$@614h7j6#3S+R0|EApDVi!(^ZH)% z?Q+S`v&hjJxyENsK3TyPF!*F*FvzMZy??jElf2o{LA6N|^@RF7==KO57qXSXA@Dkd z?TMIMoxeH1I=?bEzvfloxUAz=F|+}^-5p@ebZMGfjo>t2v7Nf%QYItjr09%|r8nxSN zCvTT`zqo+`6Ae9G8TVn8#g@>%& zqk39Ry?qz;YU}Wj-Kje}4e*eKhivs}wLJWIz;p7f)KOohrwf00e*l5C8%|00;m9AOHlOQv!JX|0Mcdj^NM2L*doP zxyYMC{^4&%e<}K1CL4Sh_-Wvl|2O>Se7}xF-~$AJK>rdjLlZ&vm1)`($aa+c1Fc%D zWyuec;C0Wel+g<*eKX~b;Ou4Cg_Rrg_qmumx}7e@-CpU9#%ucST{Cf$dciW~g10GY zDaGf~QqHYw@(5E}=?*rfH31)gcRa{mi_>P81x=moT*_`{HWi`3xAfb=FM4Wsr|Hsd zM`8&2ULE~jX%iEgtF7HsEb znRu`uHn(E~UayFoSI$`mFC1lXs|*g#{)LZha4WBmVsN`;P5P`=xh}d)CCi@8tXxP5 zVnNk923is02btwc=QhTQ#>~i>g^OaH72K^V+j7huasCJ!TImk9p^f25bR@`161_i1 zcDL9~$=MV=f}*y5dy}4r1oHELu~E-sZ(R)iByYB?ZR(DN6)&+0+GlsTNP81mCr7l- zW~D>>h32CHc3nDX^~A%~tl$yIm^YXp8;{dZy13d6-A-U%o$#dDxhGg4?d$Be{;0FG zlydGG>D~U=N`vkuT8zWLC*@vPnuUx2HA@j=_ixU zy46J69a{j<>8Q8&DCW3zC|^VCZfILhd;3oB4$WBi+MRPlXe7QbjFN3~?~OUlZ9n;J zo7>l?JPmJ0AJ=-k;#aF?@qQwYRXuwzP^;PY?tSgqLy1N4-3H&5?bOaz-L+KhOzcsa zUD?z&ALu!QWPq!bow*&_(CbKTs|ZAoBCs7Tl(k}vYgV*!8Exd{cB;)%5v^#tD=6By zTP^C@3VP(C-!*epVpL|-DY6&#v*T8o=>;8as%@b4(;QA=7<#2{Qdz!$i^5)_9jbetCVmS-PKe?8Q7G!3{e zu2@%Z(G`p9qq{E0&2wWxc2T9xYaKJJCv8Wm>#5!oea6G69-(&6!0~kL5yB=~fuvjX z;u*`f`NM4M>cjQqc=jXN*2?23+d520M+DaY`*%PGo&ff00e+Q{}RCCf13Fr#r%-@ zZvT1-o&fACG%676>2(O-ePiQcDOsO=?jq#$MzJo+PS>(yK> zx7E<9`m06s+Sqa;SF7N+nI^Z4O8F{!HE_9<)9@QvlY2O1YR?^7DPMd0I#PV~<>brc zOi7Y=zhcE`6NWG?Dx83JV5DaRDIrOU%B$wa$^LUrNT{ilsH*0|3F6$?voAUE=@(RH zCZ)`z#01(7gLcc93(WKG3)=a^XH-#<%v(|7%c*ByF3+e~L@6OF8A(l<8zcSHen_1Y z)C7-QlSOl5xbK|fm4vFw84;D-Q2#lH_SU5`ipZNAkv?-SJ(EIDNl}$klDQG?JLe=N zA*dMzx4BTCIVaEXaza6Olng4lU_Uy?^J!H?B^T&3=TbANbRv~X@sezA`1{T|NlYji zSrOCbhOZx;;}uDjP_-QFKj(x5pAs^XVr~rdopYj+P()EwMe`jRIrq*aYhDc6%BbfprMWAMx9%)2UFFK9ZQUA?#<{tzMy;YJCDi3hh(ZR9 zoMdiID`<_9ovJ-NB_qj?*PSj^w4$DTsBhK}9hr8%9-t_qg2(^Efww3|jy@Rq;_&x| z{%eSj=;7ZA{b}&$!32tc4-fzXKmZ5;fsaAJTzrvq9nL-DNvMj}$n7*g-h=b-d0bhY zky44YEC~{CE>84;H&Q~8c^=KGExkaz8GpuQ^PpBJH1tQ$sBnaIB8|T2m`_kNa=XCxsZ zt71mUn2T&5c!S1NDG8U&(i!5-b9_HTg(C_HN#s+KWG;>MfjfedNb#zyq|95Vi965f c{S1{1pH4`qqNNn`)~V;+6|^#)5oKQaAJjl2!~g&Q literal 0 HcmV?d00001 diff --git a/defender/exampleapp/readme.md b/defender/exampleapp/readme.md new file mode 100644 index 0000000..ba61ac1 --- /dev/null +++ b/defender/exampleapp/readme.md @@ -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 +``` diff --git a/defender/exampleapp/settings.py b/defender/exampleapp/settings.py new file mode 100644 index 0000000..d331af7 --- /dev/null +++ b/defender/exampleapp/settings.py @@ -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 diff --git a/defender/exampleapp/urls.py b/defender/exampleapp/urls.py new file mode 100644 index 0000000..c0d8596 --- /dev/null +++ b/defender/exampleapp/urls.py @@ -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) diff --git a/defender/templates/admin/defender/app_index.html b/defender/templates/admin/defender/app_index.html new file mode 100644 index 0000000..ccf8914 --- /dev/null +++ b/defender/templates/admin/defender/app_index.html @@ -0,0 +1,25 @@ +{% extends "admin/index.html" %} +{% load i18n %} + +{% if not is_popup %} +{% block breadcrumbs %} + +{% endblock %} +{% endif %} + +{% block sidebar %}{% endblock %} + +{% block content %} +{{ block.super }} +
+
+

Blocked Users

+
+
+{% endblock content%} diff --git a/defender/templates/admin/defender/blocks.html b/defender/templates/admin/defender/blocks.html deleted file mode 100644 index 4305e81..0000000 --- a/defender/templates/admin/defender/blocks.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends "admin/change_list.html" %} -{% load i18n %} - -{% block result_list %} -

Blocked Logins

-

Here is a list of IP's and usernames that are blocked

- -

Blocked IP's

- - - {% for block in blocked_ip_list %} - - - - - {% empty %} - - {% endfor %} -
IPAction
{{block}} -
- {% csrf_token %} - -
-
No IP's
- -

Blocked Usernames

- - - {% for block in blocked_username_list %} - - - - - {% empty %} - - {% endfor %} -
UsernameAction
{{block}} -
- {% csrf_token %} - -
-
No Username's
-{% endblock result_list %} diff --git a/defender/templates/defender/admin/blocks.html b/defender/templates/defender/admin/blocks.html new file mode 100644 index 0000000..5bda17e --- /dev/null +++ b/defender/templates/defender/admin/blocks.html @@ -0,0 +1,74 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls %} + +{% block bodyclass %}dashboard{% endblock %} + +{% block extrastyle %} +{{ block.super }} + +{% endblock %} + +{% block breadcrumbs %} + +{% endblock breadcrumbs %} + +{% block content %} +
+ +

Blocked Logins

+

Here is a list of IP's and usernames that are blocked

+ +
+ + + + + + + + {% for block in blocked_ip_list %} + + + + + {% empty %} + + {% endfor %} + +
Blocked IP's
IPAction
{{block}} +
+ {% csrf_token %} + +
+
No IP's
+
+ +
+ + + + + + + {% for block in blocked_username_list %} + + + + + {% empty %} + + {% endfor %} + +
Blocked Usernames
UsernamesAction
{{block}} +
+ {% csrf_token %} + +
+
No Username's
+ +
+
+{% endblock content %} diff --git a/defender/urls.py b/defender/urls.py new file mode 100644 index 0000000..7d4a61e --- /dev/null +++ b/defender/urls.py @@ -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[a-z0-9-._]+)/unblock$', unblock_ip_view, + name="defender_unblock_ip_view"), + url(r'^blocks/username/(?P[a-z0-9-._@]+)/unblock$', + unblock_username_view, + name="defender_unblock_username_view"), +) diff --git a/defender/views.py b/defender/views.py index e69de29..8796518 100644 --- a/defender/views.py +++ b/defender/views.py @@ -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"))