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',