Override monkey-patched LoginView from django-axes 3.0.3

- Fixes #1154
- Fixes #1138

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Signed-off-by: Joseph Nuthalapati <njoseph@thoughtworks.com>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Joseph Nuthalapati 2017-11-27 16:01:03 +05:30 committed by James Valleroy
parent 4e3216ce7c
commit fc9ce8e6dd
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
5 changed files with 131 additions and 89 deletions

View File

@ -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)

29
plinth/axes_app_config.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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

View File

@ -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

View File

@ -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<slug>[\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<slug>[\w.@+-]+)/delete/$',
views.UserDelete.as_view(),
name='delete'),
views.UserDelete.as_view(), name='delete'),
url(r'^sys/users/(?P<slug>[\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'),
]

View File

@ -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<key>\w+)/$',
public(cviews.captcha_image),
name='captcha-image',
public(cviews.captcha_image), name='captcha-image',
kwargs={'scale': 1}),
url(r'image/(?P<key>\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<key>\w+)/$', public(cviews.captcha_audio), name='captcha-audio'),
url(r'audio/(?P<key>\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'),
]