diff --git a/plinth/__main__.py b/plinth/__main__.py
index a140c43b4..0177aa05b 100644
--- a/plinth/__main__.py
+++ b/plinth/__main__.py
@@ -17,10 +17,6 @@
#
import argparse
-import django.conf
-from django.contrib.messages import constants as message_constants
-import django.core.management
-import django.core.wsgi
import importlib
import logging
import os
@@ -28,13 +24,16 @@ import stat
import sys
import warnings
-import cherrypy
+import django.conf
+import django.core.management
+import django.core.wsgi
+from django.contrib.messages import constants as message_constants
-from plinth import cfg
-from plinth import menu
-from plinth import module_loader
-from plinth import service
-from plinth import setup
+import axes
+import cherrypy
+from plinth import cfg, menu, module_loader, service, setup
+
+axes.default_app_config = "plinth.axes_app_config.AppConfig"
logger = logging.getLogger(__name__)
@@ -47,27 +46,22 @@ def parse_arguments():
description='Plinth web interface for FreedomBox',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
# TODO: server_dir is actually a url prefix; use a better variable name
- parser.add_argument(
- '--server_dir', default=cfg.server_dir,
- help='web server path under which to serve')
- parser.add_argument(
- '--debug', action='store_true', default=cfg.debug,
- help='enable debugging and run server *insecurely*')
+ parser.add_argument('--server_dir', default=cfg.server_dir,
+ help='web server path under which to serve')
+ parser.add_argument('--debug', action='store_true', default=cfg.debug,
+ help='enable debugging and run server *insecurely*')
parser.add_argument(
'--setup', default=False, nargs='*',
help='run setup tasks on all essential modules and exit')
parser.add_argument(
'--setup-no-install', default=False, nargs='*',
help='run setup tasks without installing packages and exit')
- parser.add_argument(
- '--diagnose', action='store_true', default=False,
- help='run diagnostic tests and exit')
- parser.add_argument(
- '--list-dependencies', default=False, nargs='*',
- help='list package dependencies for essential modules')
- parser.add_argument(
- '--list-modules', default=False, nargs='*',
- help='list modules')
+ parser.add_argument('--diagnose', action='store_true', default=False,
+ help='run diagnostic tests and exit')
+ parser.add_argument('--list-dependencies', default=False, nargs='*',
+ help='list package dependencies for essential modules')
+ parser.add_argument('--list-modules', default=False, nargs='*',
+ help='list modules')
global arguments
arguments = parser.parse_args()
@@ -111,9 +105,12 @@ def setup_server():
static_dir = os.path.join(cfg.file_root, 'static')
config = {
- '/': {'tools.staticdir.root': static_dir,
- 'tools.staticdir.on': True,
- 'tools.staticdir.dir': '.'}}
+ '/': {
+ 'tools.staticdir.root': static_dir,
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': '.'
+ }
+ }
cherrypy.tree.mount(None, django.conf.settings.STATIC_URL, config)
logger.debug('Serving static directory %s on %s', static_dir,
django.conf.settings.STATIC_URL)
@@ -121,9 +118,12 @@ def setup_server():
js_dir = '/usr/share/javascript'
js_url = '/javascript'
config = {
- '/': {'tools.staticdir.root': js_dir,
- 'tools.staticdir.on': True,
- 'tools.staticdir.dir': '.'}}
+ '/': {
+ 'tools.staticdir.root': js_dir,
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': '.'
+ }
+ }
cherrypy.tree.mount(None, js_url, config)
logger.debug('Serving javascript directory %s on %s', js_dir, js_url)
@@ -131,9 +131,12 @@ def setup_server():
manual_url = '/'.join([cfg.server_dir, 'help/manual/images']) \
.replace('//', '/')
config = {
- '/': {'tools.staticdir.root': manual_dir,
- 'tools.staticdir.on': True,
- 'tools.staticdir.dir': '.'}}
+ '/': {
+ 'tools.staticdir.root': manual_dir,
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': '.'
+ }
+ }
cherrypy.tree.mount(None, manual_url, config)
logger.debug('Serving manual images %s on %s', manual_dir, manual_url)
@@ -144,9 +147,12 @@ def setup_server():
continue
config = {
- '/': {'tools.staticdir.root': static_dir,
- 'tools.staticdir.on': True,
- 'tools.staticdir.dir': '.'}}
+ '/': {
+ 'tools.staticdir.root': static_dir,
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': '.'
+ }
+ }
urlprefix = "%s%s" % (django.conf.settings.STATIC_URL, module_name)
cherrypy.tree.mount(None, urlprefix, config)
logger.debug('Serving static directory %s on %s', static_dir,
@@ -168,25 +174,25 @@ def configure_django():
'formatters': {
'default': {
'format':
- '[%(asctime)s] %(name)-14s %(levelname)-8s %(message)s',
- }
- },
+ '[%(asctime)s] %(name)-14s %(levelname)-8s %(message)s',
+ }
+ },
'handlers': {
'file': {
'class': 'logging.FileHandler',
'filename': cfg.status_log_file,
'formatter': 'default'
- },
+ },
'console': {
'class': 'logging.StreamHandler',
'formatter': 'default'
- }
- },
+ }
+ },
'root': {
'handlers': ['console', 'file'],
'level': 'DEBUG' if cfg.debug else 'INFO'
- }
}
+ }
templates = [
{
@@ -225,35 +231,45 @@ def configure_django():
if cfg.secure_proxy_ssl_header:
secure_proxy_ssl_header = (cfg.secure_proxy_ssl_header, 'https')
+ pwd = 'django.contrib.auth.password_validation'
+
django.conf.settings.configure(
ALLOWED_HOSTS=['*'],
AUTH_PASSWORD_VALIDATORS=[
{
- 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ 'NAME': '{}.UserAttributeSimilarityValidator'.format(pwd),
},
{
- 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ 'NAME': '{}.MinimumLengthValidator'.format(pwd),
'OPTIONS': {
'min_length': 8,
}
},
{
- 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ 'NAME': '{}.CommonPasswordValidator'.format(pwd),
},
{
- 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ 'NAME': '{}.NumericPasswordValidator'.format(pwd),
},
],
- AXES_LOCKOUT_URL='locked',
+ AXES_LOCKOUT_URL='locked/',
AXES_BEHIND_REVERSE_PROXY=True,
- CACHES={'default':
- {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}},
- CAPTCHA_FONT_PATH=['/usr/share/fonts/truetype/ttf-bitstream-vera/Vera.ttf'],
+ CACHES={
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.dummy.DummyCache'
+ }
+ },
+ CAPTCHA_FONT_PATH=[
+ '/usr/share/fonts/truetype/ttf-bitstream-vera/Vera.ttf'
+ ],
CAPTCHA_LENGTH=6,
CAPTCHA_FLITE_PATH='/usr/bin/flite',
- DATABASES={'default':
- {'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': cfg.store_file}},
+ DATABASES={
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': cfg.store_file
+ }
+ },
DEBUG=cfg.debug,
FORCE_SCRIPT_NAME=cfg.server_dir,
INSTALLED_APPS=applications,
@@ -274,8 +290,7 @@ def configure_django():
'plinth.middleware.AdminRequiredMiddleware',
'plinth.middleware.FirstSetupMiddleware',
'plinth.modules.first_boot.middleware.FirstBootMiddleware',
- 'plinth.middleware.SetupMiddleware',
- ),
+ 'plinth.middleware.SetupMiddleware', ),
ROOT_URLCONF='plinth.urls',
SECURE_BROWSER_XSS_FILTER=True,
SECURE_CONTENT_TYPE_NOSNIFF=True,
@@ -284,8 +299,11 @@ def configure_django():
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', ),
+ 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/axes_app_config.py b/plinth/axes_app_config.py
new file mode 100644
index 000000000..ad04d0283
--- /dev/null
+++ b/plinth/axes_app_config.py
@@ -0,0 +1,29 @@
+#
+# 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 .
+#
+"""
+Overridden AppConfig from django-axes to avoid monkey-patched LoginView
+"""
+
+from django import apps
+
+
+class AppConfig(apps.AppConfig):
+ name = 'axes'
+
+ def ready(self):
+ # Signals must be loaded for axes to get the login_failed signals
+ from axes import signals # isort:skip
diff --git a/plinth/modules/sso/views.py b/plinth/modules/sso/views.py
index 200086bfb..f90351262 100644
--- a/plinth/modules/sso/views.py
+++ b/plinth/modules/sso/views.py
@@ -18,22 +18,20 @@
Views for the Single Sign On module of Plinth
"""
+import logging
import os
import urllib
-import logging
-from .forms import AuthenticationForm
-
-from plinth import actions
-
-from django.http import HttpResponseRedirect
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import LoginView, LogoutView
+from django.http import HttpResponseRedirect
-from django.shortcuts import render_to_response
-
+from axes.decorators import axes_form_invalid
from axes.utils import reset
+from plinth import actions
+
+from .forms import AuthenticationForm
PRIVATE_KEY_FILE_NAME = 'privkey.pem'
SSO_COOKIE_NAME = 'auth_pubtkt'
@@ -70,6 +68,10 @@ class SSOLoginView(LoginView):
else:
return response
+ @axes_form_invalid
+ def form_invalid(self, *args, **kwargs):
+ return super(SSOLoginView, self).form_invalid(*args, **kwargs)
+
class CaptchaLoginView(LoginView):
redirect_authenticated_user = True
diff --git a/plinth/modules/users/urls.py b/plinth/modules/users/urls.py
index b51cc10be..b734f4c3a 100644
--- a/plinth/modules/users/urls.py
+++ b/plinth/modules/users/urls.py
@@ -21,35 +21,30 @@ URLs for the Users module
from django.conf.urls import url
from django.urls import reverse_lazy
-from stronghold.decorators import public
-from plinth.utils import non_admin_view
+from axes.decorators import axes_dispatch
from plinth.modules.sso.views import SSOLoginView, SSOLogoutView
-from . import views
+from plinth.utils import non_admin_view
+from stronghold.decorators import public
-from axes.decorators import watch_login
+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'),
+ non_admin_view(views.UserUpdate.as_view()), name='edit'),
url(r'^sys/users/(?P[\w.@+-]+)/delete/$',
- views.UserDelete.as_view(),
- name='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'),
# Authnz is handled by SSO
url(r'^accounts/login/$',
- public(watch_login(SSOLoginView.as_view())),
- name='login'),
+ public(axes_dispatch(SSOLoginView.as_view())), name='login'),
url(r'^accounts/logout/$',
non_admin_view(SSOLogoutView.as_view()),
- {'next_page': reverse_lazy('index')},
- name='logout'),
+ {'next_page': reverse_lazy('index')}, name='logout'),
url(r'^users/firstboot/$',
- public(views.FirstBootView.as_view()),
- name='firstboot'),
+ public(views.FirstBootView.as_view()), name='firstboot'),
]
diff --git a/plinth/urls.py b/plinth/urls.py
index e9b76dcde..044d08fd2 100644
--- a/plinth/urls.py
+++ b/plinth/urls.py
@@ -17,10 +17,10 @@
"""
Django URLconf file containing all urls
"""
-from captcha import views as cviews
from django.conf.urls import url
from django.views.generic import TemplateView
+from captcha import views as cviews
from plinth.modules.sso.views import CaptchaLoginView
from stronghold.decorators import public
@@ -29,20 +29,18 @@ from . import views
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'^apps/$',
- TemplateView.as_view(template_name='apps.html'),
- name='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',
+ public(cviews.captcha_image), name='captcha-image',
kwargs={'scale': 1}),
url(r'image/(?P\w+)@2/$',
- public(cviews.captcha_image),
- name='captcha-image-2x',
+ 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'audio/(?P\w+)/$',
+ public(cviews.captcha_audio), name='captcha-audio'),
url(r'refresh/$', public(cviews.captcha_refresh), name='captcha-refresh'),
url(r'locked/$', public(CaptchaLoginView.as_view()), name='locked_out'),
]