diff --git a/INSTALL b/INSTALL index 96b8c5831..811813321 100644 --- a/INSTALL +++ b/INSTALL @@ -27,6 +27,7 @@ python3-configobj \ python3-coverage \ python3-django \ + python3-django-captcha \ python3-django-stronghold \ python3-gi \ python3-psutil \ diff --git a/plinth/__main__.py b/plinth/__main__.py index 684444729..c97ba4d37 100644 --- a/plinth/__main__.py +++ b/plinth/__main__.py @@ -209,6 +209,7 @@ def configure_django(): ] applications = ['bootstrapform', + 'captcha', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.messages', @@ -242,6 +243,7 @@ def configure_django(): ], CACHES={'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}}, + CAPTCHA_FONT_PATH=['/usr/share/fonts/truetype/ttf-bitstream-vera/Vera.ttf'], DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': cfg.store_file}}, @@ -271,6 +273,9 @@ def configure_django(): SESSION_ENGINE='django.contrib.sessions.backends.file', SESSION_FILE_PATH=sessions_directory, STATIC_URL='/'.join([cfg.server_dir, 'static/']).replace('//', '/'), + # STRONGHOLD_PUBLIC_URLS=(r'^captcha/', ), + STRONGHOLD_PUBLIC_NAMED_URLS=('captcha-image', 'captcha-image-2x', + 'captcha-audio', 'captcha-refresh', ), TEMPLATES=templates, USE_L10N=True, USE_X_FORWARDED_HOST=cfg.use_x_forwarded_host) diff --git a/plinth/module_loader.py b/plinth/module_loader.py index ea1982ad4..503d31b0f 100644 --- a/plinth/module_loader.py +++ b/plinth/module_loader.py @@ -27,7 +27,6 @@ import os import re from plinth import cfg -from plinth import urls from plinth import setup from plinth.signals import pre_module_loading, post_module_loading @@ -114,6 +113,7 @@ def _insert_modules(module_name, module, remaining_modules, ordered_modules): def _include_module_urls(module_import_path, module_name): """Include the module's URLs in global project URLs list""" + from plinth import urls url_module = module_import_path + '.urls' try: urls.urlpatterns += [ diff --git a/plinth/modules/sso/forms.py b/plinth/modules/sso/forms.py new file mode 100644 index 000000000..6d344125d --- /dev/null +++ b/plinth/modules/sso/forms.py @@ -0,0 +1,26 @@ +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Forms for the Single Sign On module of Plinth +""" + +from django.contrib.auth.forms import AuthenticationForm as DjangoAuthenticationForm +from captcha.fields import CaptchaField + + +class AuthenticationForm(DjangoAuthenticationForm): + captcha = CaptchaField() diff --git a/plinth/modules/sso/urls.py b/plinth/modules/sso/urls.py index 8cd6227a4..a4b4638d6 100644 --- a/plinth/modules/sso/urls.py +++ b/plinth/modules/sso/urls.py @@ -20,6 +20,7 @@ URLs for the Single Sign On module. from django.conf.urls import url + from .views import SSOLoginView, refresh from stronghold.decorators import public diff --git a/plinth/modules/sso/views.py b/plinth/modules/sso/views.py index a3de34ef8..57ecba6fb 100644 --- a/plinth/modules/sso/views.py +++ b/plinth/modules/sso/views.py @@ -21,6 +21,8 @@ Views for the Single Sign On module of Plinth import os import urllib +from .forms import AuthenticationForm + from plinth import actions from django.http import HttpResponseRedirect @@ -51,15 +53,21 @@ class SSOLoginView(LoginView): """View to login to Plinth and set a auth_pubtkt cookie which will be used to provide Single Sign On for some other applications """ - redirect_authenticated_user = True template_name = 'login.html' def dispatch(self, request, *args, **kwargs): response = super(SSOLoginView, self).dispatch(request, *args, **kwargs) - return set_ticket_cookie( - request.user, - response) if request.user.is_authenticated else response + if request.POST: + if request.user.is_authenticated: + return set_ticket_cookie(request.user, response) + else: # Redirect user to captcha page + return HttpResponseRedirect('captcha') + return response + + +class CaptchaLoginView(SSOLoginView): + form_class = AuthenticationForm class SSOLogoutView(LogoutView): diff --git a/plinth/modules/users/urls.py b/plinth/modules/users/urls.py index e306e2128..497f5850f 100644 --- a/plinth/modules/users/urls.py +++ b/plinth/modules/users/urls.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # - """ URLs for the Users module """ @@ -24,24 +23,31 @@ from django.urls import reverse_lazy from stronghold.decorators import public from plinth.utils import non_admin_view -from plinth.modules.sso.views import SSOLoginView, SSOLogoutView +from plinth.modules.sso.views import SSOLoginView, SSOLogoutView, CaptchaLoginView from . import views - urlpatterns = [ url(r'^sys/users/$', views.UserList.as_view(), name='index'), url(r'^sys/users/create/$', views.UserCreate.as_view(), name='create'), url(r'^sys/users/(?P[\w.@+-]+)/edit/$', - non_admin_view(views.UserUpdate.as_view()), name='edit'), - url(r'^sys/users/(?P[\w.@+-]+)/delete/$', views.UserDelete.as_view(), + non_admin_view(views.UserUpdate.as_view()), + name='edit'), + url(r'^sys/users/(?P[\w.@+-]+)/delete/$', + views.UserDelete.as_view(), name='delete'), url(r'^sys/users/(?P[\w.@+-]+)/change_password/$', non_admin_view(views.UserChangePassword.as_view()), name='change_password'), # Add Django's login/logout urls url(r'^accounts/login/$', public(SSOLoginView.as_view()), name='login'), - url(r'^accounts/logout/$', non_admin_view(SSOLogoutView.as_view()), - {'next_page': reverse_lazy('index')}, name='logout'), - url(r'^users/firstboot/$', public(views.FirstBootView.as_view()), + url(r'^accounts/logout/$', + non_admin_view(SSOLogoutView.as_view()), + {'next_page': reverse_lazy('index')}, + name='logout'), + url(r'^accounts/login/captcha/$', + public(CaptchaLoginView.as_view()), + name='captcha-login'), + url(r'^users/firstboot/$', + public(views.FirstBootView.as_view()), name='firstboot'), ] diff --git a/plinth/urls.py b/plinth/urls.py index 18748694a..7566f7e19 100644 --- a/plinth/urls.py +++ b/plinth/urls.py @@ -14,20 +14,33 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # - """ Django URLconf file containing all urls """ - -from django.conf.urls import url +from captcha import views as cviews +from django.conf.urls import url, include from django.views.generic import TemplateView +from stronghold.decorators import public + from . import views - urlpatterns = [ url(r'^$', views.index, name='index'), - url(r'^apps/$', TemplateView.as_view(template_name='apps.html'), + url(r'^apps/$', + TemplateView.as_view(template_name='apps.html'), name='apps'), url(r'^sys/$', views.system_index, name='system'), + + # captcha urls are public + url(r'image/(?P\w+)/$', + public(cviews.captcha_image), + name='captcha-image', + kwargs={'scale': 1}), + url(r'image/(?P\w+)@2/$', + public(cviews.captcha_image), + name='captcha-image-2x', + kwargs={'scale': 2}), + url(r'audio/(?P\w+)/$', public(cviews.captcha_audio), name='captcha-audio'), + url(r'refresh/$', public(cviews.captcha_refresh), name='captcha-refresh'), ] diff --git a/requirements.txt b/requirements.txt index 7afdf650a..21d38314f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ configobj coverage >= 3.7 django >= 1.11.0 django-bootstrap-form +django-simple-captcha django-stronghold psutil python-apt diff --git a/setup.py b/setup.py index 829ca3bb5..32a83d572 100755 --- a/setup.py +++ b/setup.py @@ -212,6 +212,7 @@ setuptools.setup( 'configobj', 'django >= 1.11.0', 'django-bootstrap-form', + 'django-simple-captcha', 'django-stronghold', 'psutil', 'python-apt',