FreedomBox/plinth/web_framework.py
Sunil Mohan Adapa fd345aca80
web_framework: Generate and retain a secret key
- Secret is important for various functions of Django. There is no impact on
existing installations due to the change. Improves the security of existing
functions in minor ways and will be useful in future usage of Django.

- Create the file in /var/lib/plinth/ with 0o600 permissions.

- Make git ignore the file in code folder.

- Don't copy the file during './setup.py install' operation.

Impact to users after upgrade:

- All existing sessions will get logged out. This is because SECRET_KEY is used
to generate user session hash that is used to logout users when their password
changes.

Tests performed:

- Run development version of service. File should get created in
data/var/lib/plinth/django-secret.key. Permissions should be 0o600.

- Run again, the file should not be overwritten. Printing
django.conf.settings.SECRET_KEY should match the one in the file.

- Run `setup.py install`. This should not install django-secret.key in
/var/lib/plinth.

- Run `sudo -u plinth plinth`. This should create the secret key file in
/var/lib/plinth/django-secret.key. Permissions on the file should be 0o600.
Ownership should be plinth:plinth.

- Remove the file in both cases, a fresh new file should get created with new key.

- Truncate the file to less than 128 chars, the existing file should get
overwritten with new key.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2020-02-24 18:04:20 -05:00

127 lines
3.9 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Setup Django web framework.
"""
import logging
import os
import pathlib
import random
import stat
import django.conf
import django.core.management
import django.core.wsgi
from django.conf import global_settings
from django.contrib.messages import constants as message_constants
from . import cfg, log, module_loader, settings
logger = logging.getLogger(__name__)
def init():
"""Setup Django configuration in the absence of .settings file"""
if cfg.secure_proxy_ssl_header:
settings.SECURE_PROXY_SSL_HEADER = (cfg.secure_proxy_ssl_header,
'https')
if cfg.use_x_forwarded_for:
settings.IPWARE_META_PRECEDENCE_ORDER = ('HTTP_X_FORWARDED_FOR', )
settings.DATABASES['default']['NAME'] = cfg.store_file
settings.DEBUG = cfg.develop
settings.FORCE_SCRIPT_NAME = cfg.server_dir
settings.INSTALLED_APPS += module_loader.get_modules_to_load()
settings.LANGUAGES = get_languages()
settings.LOGGING = log.get_configuration()
settings.MESSAGE_TAGS = {message_constants.ERROR: 'danger'}
settings.SECRET_KEY = _get_secret_key()
settings.SESSION_FILE_PATH = os.path.join(cfg.data_dir, 'sessions')
settings.STATIC_URL = '/'.join([cfg.server_dir,
'static/']).replace('//', '/')
settings.USE_X_FORWARDED_HOST = cfg.use_x_forwarded_host
kwargs = {}
for setting in dir(settings):
if setting.isupper():
kwargs[setting] = getattr(settings, setting)
django.conf.settings.configure(**kwargs)
django.setup(set_prefix=True)
logger.debug('Configured Django with applications - %s',
settings.INSTALLED_APPS)
logger.debug('Creating or adding new tables to data file')
verbosity = 1 if cfg.develop else 0
django.core.management.call_command('migrate', '--fake-initial',
interactive=False, verbosity=verbosity)
os.chmod(cfg.store_file, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
def _get_secret_key():
"""Retrieve or create a new Django secret key."""
secret_key_file = pathlib.Path(cfg.data_dir) / 'django-secret.key'
if secret_key_file.exists():
secret_key = secret_key_file.read_text()
if len(secret_key) >= 128:
return secret_key
secret_key = _generate_secret_key()
# File should be created with permission 0o700
old_umask = os.umask(0o077)
try:
secret_key_file.write_text(secret_key)
finally:
os.umask(old_umask)
return secret_key
def _generate_secret_key():
"""Generate a new random secret key for use with Django."""
# We could have used django.core.management.utils.get_random_secret_key
# but it is not documented and should be considered private.
length = 128
chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)'
rand = random.SystemRandom()
return ''.join(rand.choice(chars) for _ in range(length))
def get_languages():
"""Return list of languages to show in the interface.
Add additional languages that FreedomBox support but Django doesn't.
"""
def gettext_noop(string):
"""Django's actual translation methods need Django to be setup."""
return string
return sorted(
list(global_settings.LANGUAGES) + [
('gu', gettext_noop('Gujarati')),
])
def get_wsgi_application():
"""Return Django wsgi application."""
return django.core.wsgi.get_wsgi_application()
def get_static_url():
"""Return Django static URL."""
return django.conf.settings.STATIC_URL
def get_ip_address_from_request(request):
"""Return the IP address of the original client."""
if cfg.use_x_forwarded_for:
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
ip_address = x_forwarded_for.split(',')[0]
else:
ip_address = request.META.get('REMOTE_ADDR')
return ip_address