From b5f93958ffa6ea9d6f870ee433bd88fdc0104b07 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Mon, 28 Dec 2020 12:35:36 +0100 Subject: [PATCH] WIP: setup the project --- Makefile | 1 + README.md | 4 +- conf/django_ynh.service | 2 +- ...ettings.py => django_ynh_demo_settings.py} | 70 +++++------- conf/{urls.py => django_ynh_demo_urls.py} | 2 +- conf/manage.py | 2 +- conf/wsgi.py | 2 +- django_ynh/base_settings.py | 100 ++++++++++++++++++ django_ynh/create_superuser.py | 25 ++++- django_ynh/secret_key.py | 23 ++++ django_ynh/sso_auth/auth_backend.py | 15 +-- django_ynh/sso_auth/auth_middleware.py | 27 +++-- django_ynh/sso_auth/signals.py | 12 +++ django_ynh/sso_auth/user_profile.py | 13 +-- django_ynh/test_tools/YnhTestCase.py | 7 ++ django_ynh/test_tools/__init__.py | 0 django_ynh/views.py | 11 +- django_ynh_tests/test_project/settings.py | 46 +------- django_ynh_tests/test_project/signals.py | 26 +++++ local_test.py | 74 +++++++------ pyproject.toml | 1 + scripts/change_url | 4 +- scripts/install | 4 +- scripts/upgrade | 4 +- 24 files changed, 311 insertions(+), 164 deletions(-) rename conf/{ynh_settings.py => django_ynh_demo_settings.py} (66%) rename conf/{urls.py => django_ynh_demo_urls.py} (89%) create mode 100644 django_ynh/base_settings.py create mode 100644 django_ynh/secret_key.py create mode 100644 django_ynh/sso_auth/signals.py create mode 100644 django_ynh/test_tools/YnhTestCase.py create mode 100644 django_ynh/test_tools/__init__.py create mode 100644 django_ynh_tests/test_project/signals.py diff --git a/Makefile b/Makefile index b9d9e15..86358fe 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,7 @@ publish: ## Release new version to PyPi poetry run publish local-test: check-poetry ## Run local_test.py to run the project locally + poetry install poetry run ./local_test.py local-diff-settings: ## Run "manage.py diffsettings" with local test diff --git a/README.md b/README.md index 77c65b7..06600bd 100644 --- a/README.md +++ b/README.md @@ -86,13 +86,13 @@ drwxr-xr-x 3 root root 3 Dec 8 08:36 .. -rw-r--r-- 1 django_ynh django_ynh 171 Dec 8 08:39 secret.txt drwxr-xr-x 6 django_ynh django_ynh 6 Dec 8 08:37 venv -rw-r--r-- 1 django_ynh django_ynh 115 Dec 8 08:39 wsgi.py --rw-r--r-- 1 django_ynh django_ynh 4737 Dec 8 08:39 django_ynh.settings.py +-rw-r--r-- 1 django_ynh django_ynh 4737 Dec 8 08:39 django_ynh_demo_settings.py root@yunohost:~# cd /opt/yunohost/django_ynh/ root@yunohost:/opt/yunohost/django_ynh# source venv/bin/activate (venv) root@yunohost:/opt/yunohost/django_ynh# ./manage.py check django_ynh v0.8.2 (Django v2.2.17) -DJANGO_SETTINGS_MODULE='django_ynh.settings' +DJANGO_SETTINGS_MODULE='django_ynh_demo_settings' PROJECT_PATH:/opt/yunohost/django_ynh/venv/lib/python3.7/site-packages BASE_PATH:/opt/yunohost/django_ynh System check identified no issues (0 silenced). diff --git a/conf/django_ynh.service b/conf/django_ynh.service index ec602b0..7b11dd0 100644 --- a/conf/django_ynh.service +++ b/conf/django_ynh.service @@ -1,5 +1,5 @@ [Unit] -Description=django_ynh application server +Description=django_ynh DEMO application server After=redis.service postgresql.service [Service] diff --git a/conf/ynh_settings.py b/conf/django_ynh_demo_settings.py similarity index 66% rename from conf/ynh_settings.py rename to conf/django_ynh_demo_settings.py index 3aa281e..2ef986d 100644 --- a/conf/ynh_settings.py +++ b/conf/django_ynh_demo_settings.py @@ -1,18 +1,24 @@ -################################################################################ -################################################################################ +""" + ************************************************************************** + Please do not modify this file, it will be reset at the next update. + You can edit the file __FINAL_HOME_PATH__/local_settings.py and add/modify + the settings you need. -# Please do not modify this file, it will be reset at the next update. -# You can edit the file __FINAL_HOME_PATH__/local_settings.py and add/modify the settings you need. -# The parameters you add in local_settings.py will overwrite these, -# but you can use the options and documentation in this file to find out what can be done. - -################################################################################ -################################################################################ + The parameters you add in local_settings.py will overwrite these, + but you can use the options and documentation in this file to find out + what can be done. + ************************************************************************** + Django Settings here depends on YunoHost app settings. +""" from pathlib import Path as __Path +from django_ynh.base_settings import * # noqa +from django_ynh.secret_key import get_or_create_secret as __get_or_create_secret + + +DEBUG = True # This is only the DEMO app ;) But should never be on in production! -DEBUG = False # ----------------------------------------------------------------------------- @@ -28,35 +34,15 @@ assert LOG_FILE.is_file(), f'File not exists: {LOG_FILE}' PATH_URL = '__PATH_URL__' # $YNH_APP_ARG_PATH PATH_URL = PATH_URL.strip('/') -# ----------------------------------------------------------------------------- - -ROOT_URLCONF = 'django_ynh.urls' # /opt/yunohost/django_ynh/urls.py - -# ----------------------------------------------------------------------------- - -# Keep ModelBackend around for per-user permissions and superuser -AUTHENTICATION_BACKENDS = ( - 'axes.backends.AxesBackend', # AxesBackend should be the first backend! - - # Authenticate via SSO and nginx 'HTTP_REMOTE_USER' header: - 'django_ynh.sso_auth.auth_backend.SSOwatUserBackend', - - # Fallback to normal Django model backend: - 'django.contrib.auth.backends.ModelBackend', -) -LOGIN_REDIRECT_URL = None -LOGIN_URL = '/yunohost/sso/' -LOGOUT_REDIRECT_URL = '/yunohost/sso/' -# /yunohost/sso/?action=logout - - # ----------------------------------------------------------------------------- -ADMINS = ( - ('__ADMIN__', '__ADMINMAIL__'), -) +ROOT_URLCONF = 'django_ynh_demo_urls' + +SECRET_KEY = __get_or_create_secret(FINAL_HOME_PATH / 'secret.txt') # /opt/yunohost/$app/secret.txt + +ADMINS = (('__ADMIN__', '__ADMINMAIL__'),) MANAGERS = ADMINS @@ -92,6 +78,7 @@ DEFAULT_FROM_EMAIL = '__ADMINMAIL__' # List of URLs your site is supposed to serve ALLOWED_HOSTS = ['__DOMAIN__'] + # _____________________________________________________________________________ # Configuration for caching CACHES = { @@ -108,6 +95,7 @@ CACHES = { }, } + # _____________________________________________________________________________ # Static files (CSS, JavaScript, Images) @@ -122,18 +110,10 @@ else: STATIC_ROOT = str(FINAL_WWW_PATH / 'static') MEDIA_ROOT = str(FINAL_WWW_PATH / 'media') -# _____________________________________________________________________________ -# django-ckeditor - -CKEDITOR_BASEPATH = STATIC_URL + 'ckeditor/ckeditor/' - -# _____________________________________________________________________________ -# Django-dbbackup - -DBBACKUP_STORAGE_OPTIONS['location'] = str(FINAL_HOME_PATH / 'backups') # ----------------------------------------------------------------------------- + LOGGING = { 'version': 1, 'disable_existing_loggers': True, @@ -162,7 +142,7 @@ LOGGING = { 'django': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False}, 'axes': {'handlers': ['log_file', 'mail_admins'], 'level': 'WARNING', 'propagate': False}, 'django_tools': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False}, - 'inventory': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False}, + 'django_ynh': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False}, }, } diff --git a/conf/urls.py b/conf/django_ynh_demo_urls.py similarity index 89% rename from conf/urls.py rename to conf/django_ynh_demo_urls.py index 8518a40..e7ff176 100644 --- a/conf/urls.py +++ b/conf/django_ynh_demo_urls.py @@ -3,7 +3,7 @@ from django.conf.urls import static from django.contrib import admin from django.urls import path -from django_ynh.views.debug import request_media_debug_view +from django_ynh.views import request_media_debug_view # settings.PATH_URL is the $YNH_APP_ARG_PATH diff --git a/conf/manage.py b/conf/manage.py index 3f0ce21..de8943f 100755 --- a/conf/manage.py +++ b/conf/manage.py @@ -5,7 +5,7 @@ import sys def main(): - os.environ['DJANGO_SETTINGS_MODULE'] = 'django_ynh.settings' + os.environ['DJANGO_SETTINGS_MODULE'] = 'django_ynh_demo_settings' from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) diff --git a/conf/wsgi.py b/conf/wsgi.py index 3dd8dff..70d2e49 100644 --- a/conf/wsgi.py +++ b/conf/wsgi.py @@ -4,7 +4,7 @@ import os -os.environ['DJANGO_SETTINGS_MODULE'] = 'django_ynh.settings' +os.environ['DJANGO_SETTINGS_MODULE'] = 'django_ynh_demo_settings' from django.core.wsgi import get_wsgi_application diff --git a/django_ynh/base_settings.py b/django_ynh/base_settings.py new file mode 100644 index 0000000..71e8fa7 --- /dev/null +++ b/django_ynh/base_settings.py @@ -0,0 +1,100 @@ +""" + Base settings for a Django project installed in Yunohost. + All values should not depent on YunoHost app settings. +""" + + +# ----------------------------------------------------------------------------- +# settings that should be set in project settings: + +ROOT_URLCONF = None +SECRET_KEY = None + +# ----------------------------------------------------------------------------- + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'axes', # https://github.com/jazzband/django-axes +] + +# ----------------------------------------------------------------------------- + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + # + # login a user via HTTP_REMOTE_USER header from SSOwat: + 'django_ynh.sso_auth.auth_middleware.SSOwatRemoteUserMiddleware', + # + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + # + # AxesMiddleware should be the last middleware: + 'axes.middleware.AxesMiddleware', +] + +# ----------------------------------------------------------------------------- + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +# ----------------------------------------------------------------------------- + +# Keep ModelBackend around for per-user permissions and superuser +AUTHENTICATION_BACKENDS = ( + 'axes.backends.AxesBackend', # AxesBackend should be the first backend! + # + # Authenticate via SSO and nginx 'HTTP_REMOTE_USER' header: + 'django_ynh.sso_auth.auth_backend.SSOwatUserBackend', + # + # Fallback to normal Django model backend: + 'django.contrib.auth.backends.ModelBackend', +) + +LOGIN_REDIRECT_URL = None +LOGIN_URL = '/yunohost/sso/' +LOGOUT_REDIRECT_URL = '/yunohost/sso/' +# /yunohost/sso/?action=logout + +# _____________________________________________________________________________ +# Setting below, should be overwritten! + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{asctime} {levelname} {name} {module}.{funcName} {message}', + 'style': '{', + }, + }, + 'handlers': {'console': {'class': 'logging.StreamHandler', 'formatter': 'verbose'}}, + 'loggers': { + 'django': {'handlers': ['console'], 'level': 'INFO', 'propagate': False}, + 'django.auth': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False}, + 'django.security': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False}, + 'django.request': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False}, + 'django_ynh': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False}, + }, +} diff --git a/django_ynh/create_superuser.py b/django_ynh/create_superuser.py index 51fa7ac..95d2f40 100644 --- a/django_ynh/create_superuser.py +++ b/django_ynh/create_superuser.py @@ -1,18 +1,34 @@ #!/usr/bin/env python3 + +""" + Can be called e.g.: + + poetry run create_superuser --ds="foo.settings" --username="bar" \ + --email="foo@bar.tld" --password="no-password" + + or, e.g.: + + python3 -m django_ynh.create_superuser --ds="foo.settings" --username="bar" \ + --email="foo@bar.tld" \ + --password="no-password" +""" + import argparse import os import sys def main(): - os.environ['DJANGO_SETTINGS_MODULE'] = 'django_ynh.settings' - parser = argparse.ArgumentParser(description='Create or update Django super user.') + parser.add_argument('--ds', help='The "DJANGO_SETTINGS_MODULE" string') parser.add_argument('--username') parser.add_argument('--email') parser.add_argument('--password') args = parser.parse_args() + + os.environ['DJANGO_SETTINGS_MODULE'] = args.ds + username = args.username email = args.email or '' password = args.password @@ -26,7 +42,8 @@ def main(): User = get_user_model() user = User.objects.filter(username=username).first() if user: - print('Update existing user and set his password.', file=sys.stderr) + print(f'Update existing user "{user}" and set his password.', file=sys.stderr) + print(repr(password)) user.is_active = True user.is_staff = True user.is_superuser = True @@ -34,7 +51,7 @@ def main(): user.email = email user.save() else: - print('Create new super user', file=sys.stderr) + print(f'Create new super user "{username}"', file=sys.stderr) User.objects.create_superuser(username=username, email=email, password=password) diff --git a/django_ynh/secret_key.py b/django_ynh/secret_key.py new file mode 100644 index 0000000..eac93d2 --- /dev/null +++ b/django_ynh/secret_key.py @@ -0,0 +1,23 @@ +""" + Helper to create a random string for settings.SECRET_KEY + + SECURITY WARNING: keep the secret key used in production secret! +""" +import logging +from pathlib import Path +from secrets import token_urlsafe + + +logger = logging.getLogger(__name__) + + +def get_or_create_secret(secret_file): + assert isinstance(secret_file, Path) + assert secret_file.parent.is_dir, f'Directory does not exists: {secret_file.parent}' + + if not secret_file.is_file(): + logger.info('Generate %s', secret_file) + secret_file.open('w').write(token_urlsafe(128)) + + with secret_file.open('r') as f: + return f.read() diff --git a/django_ynh/sso_auth/auth_backend.py b/django_ynh/sso_auth/auth_backend.py index 74568ee..bc36120 100644 --- a/django_ynh/sso_auth/auth_backend.py +++ b/django_ynh/sso_auth/auth_backend.py @@ -27,6 +27,7 @@ import logging from django.contrib.auth.backends import RemoteUserBackend +from django_ynh.sso_auth.signals import setup_user from django_ynh.sso_auth.user_profile import update_user_profile @@ -46,21 +47,13 @@ class SSOwatUserBackend(RemoteUserBackend): def configure_user(self, request, user): """ - Configure a user after creation and return the updated user. - Setup a normal, non-superuser + Configure a new user after creation and return the updated user. """ logger.warning('Configure user %s', user) - user.set_unusable_password() # Always login via SSO - user.is_staff = True - user.is_superuser = False - user.save() + user = update_user_profile(request) - # TODO: Add user in "normal" user group: - # django_ynh_user_group = get_or_create_normal_user_group()[0] - # user.groups.set([django_ynh_user_group]) - - update_user_profile(request) + setup_user.send(sender=self.__class__, user=user) return user diff --git a/django_ynh/sso_auth/auth_middleware.py b/django_ynh/sso_auth/auth_middleware.py index d9bf3c1..e842d19 100644 --- a/django_ynh/sso_auth/auth_middleware.py +++ b/django_ynh/sso_auth/auth_middleware.py @@ -2,15 +2,19 @@ import base64 import logging from axes.exceptions import AxesBackendPermissionDenied +from django.conf import settings from django.contrib.auth.middleware import RemoteUserMiddleware +from django_ynh.sso_auth.signals import setup_user +from django_ynh.sso_auth.user_profile import update_user_profile + logger = logging.getLogger(__name__) class SSOwatRemoteUserMiddleware(RemoteUserMiddleware): """ - Middleware to login a user HTTP_REMOTE_USER header. + Middleware to login a user via HTTP_REMOTE_USER header. Use Django Axes if something is wrong. Update exising user informations. """ @@ -34,13 +38,17 @@ class SSOwatRemoteUserMiddleware(RemoteUserMiddleware): except KeyError: logger.error('SSOwAuthUser cookie missing!') - # emits a signal indicating user login failed, which is processed by - # axes.signals.log_user_login_failed which logs and flags the failed request. - raise AxesBackendPermissionDenied('Cookie missing') - - logger.info('SSOwat username from cookies: %r', username) - if username != request.user.username: - raise AxesBackendPermissionDenied('Wrong username') + if settings.DEBUG: + # e.g.: local test can't set a Cookie easily + logger.warning('Ignore error, because settings.DEBUG is on!') + else: + # emits a signal indicating user login failed, which is processed by + # axes.signals.log_user_login_failed which logs and flags the failed request. + raise AxesBackendPermissionDenied('Cookie missing') + else: + logger.info('SSOwat username from cookies: %r', username) + if username != request.user.username: + raise AxesBackendPermissionDenied('Wrong username') # Compare with HTTP_AUTH_USER try: @@ -72,4 +80,5 @@ class SSOwatRemoteUserMiddleware(RemoteUserMiddleware): if not was_authenticated: # First request, after login -> update user informations logger.info('Remote used was logged in') - update_user_profile(request) + user = update_user_profile(request) + setup_user.send(sender=self.__class__, user=user) diff --git a/django_ynh/sso_auth/signals.py b/django_ynh/sso_auth/signals.py new file mode 100644 index 0000000..81f5a83 --- /dev/null +++ b/django_ynh/sso_auth/signals.py @@ -0,0 +1,12 @@ +""" + "setup_user" called via: + + * SSOwatUserBackend after a new user was created + * SSOwatRemoteUserMiddleware on login request +""" + + +import django.dispatch + + +setup_user = django.dispatch.Signal(providing_args=['user']) diff --git a/django_ynh/sso_auth/user_profile.py b/django_ynh/sso_auth/user_profile.py index d4695be..906d07d 100644 --- a/django_ynh/sso_auth/user_profile.py +++ b/django_ynh/sso_auth/user_profile.py @@ -1,11 +1,6 @@ -import base64 import logging -from axes.exceptions import AxesBackendPermissionDenied -from django.contrib.auth.backends import RemoteUserBackend as OriginRemoteUserBackend -from django.contrib.auth.middleware import RemoteUserMiddleware as OriginRemoteUserMiddleware from django.core.exceptions import ValidationError -from inventory.permissions import get_or_create_normal_user_group logger = logging.getLogger(__name__) @@ -16,13 +11,17 @@ def update_user_profile(request): Update existing user information: * Email * First / Last name + + Called via: + * SSOwatUserBackend after a new user was created + * SSOwatRemoteUserMiddleware on login request """ user = request.user assert user.is_authenticated update_fields = [] - if not user.password: + if not user.has_usable_password(): # Empty password is not valid, so we can't save the model, because of full_clean() call logger.info('Set unusable password for user: %s', user) user.set_unusable_password() @@ -59,3 +58,5 @@ def update_user_profile(request): logger.exception('Can not update user: %s', user) else: user.save(update_fields=update_fields) + + return user diff --git a/django_ynh/test_tools/YnhTestCase.py b/django_ynh/test_tools/YnhTestCase.py new file mode 100644 index 0000000..e5417fb --- /dev/null +++ b/django_ynh/test_tools/YnhTestCase.py @@ -0,0 +1,7 @@ +from django.test.testcases import TestCase + + +class YnhTestCase(TestCase): + """ + + """ diff --git a/django_ynh/test_tools/__init__.py b/django_ynh/test_tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_ynh/views.py b/django_ynh/views.py index 47805ce..f05934e 100644 --- a/django_ynh/views.py +++ b/django_ynh/views.py @@ -1,12 +1,21 @@ +import logging import pprint -from django.http import HttpResponse +from django.contrib.auth import get_user_model +from django.http.response import HttpResponse from django.shortcuts import redirect +logger = logging.getLogger(__name__) + + def request_media_debug_view(request): """ debug request.META """ + if not request.user.is_authenticated: + logger.info('Deny debug view: User not logged in!') + UserModel = get_user_model() + logger.info('Existing users are: %s', ', '.join(f'"{user}"' for user in UserModel.objects.all())) return redirect('admin:index') meta = pprint.pformat(request.META) diff --git a/django_ynh_tests/test_project/settings.py b/django_ynh_tests/test_project/settings.py index 69cc85c..e17205a 100644 --- a/django_ynh_tests/test_project/settings.py +++ b/django_ynh_tests/test_project/settings.py @@ -22,34 +22,11 @@ INSTALLED_APPS = [ 'django_ynh', # <<<< ] -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django_ynh.sso_auth.auth_middleware.SSOwatRemoteUserMiddleware', # <<<< - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] + ROOT_URLCONF = 'django_ynh_tests.test_project.urls' -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] + WSGI_APPLICATION = 'django_ynh_tests.test_project.wsgi.application' @@ -84,21 +61,4 @@ INTERNAL_IPS = [ '127.0.0.1', ] -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': '{asctime} {levelname} {name} {module}.{funcName} {message}', - 'style': '{', - }, - }, - 'handlers': {'console': {'class': 'logging.StreamHandler', 'formatter': 'verbose'}}, - 'loggers': { - 'django': {'handlers': ['console'], 'level': 'INFO', 'propagate': False}, - 'django.auth': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False}, - 'django.security': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False}, - 'django.request': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False}, - 'django_ynh': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False}, - }, -} + diff --git a/django_ynh_tests/test_project/signals.py b/django_ynh_tests/test_project/signals.py new file mode 100644 index 0000000..0a2b4ca --- /dev/null +++ b/django_ynh_tests/test_project/signals.py @@ -0,0 +1,26 @@ +import logging + +from django.dispatch import receiver + +from django_ynh.sso_auth.signals import setup_user + + +logger = logging.getLogger(__name__) + + +@receiver(setup_user) +def setup_user_handler(sender, **kwargs): + """ + Make user to a "staff" user, so he can use the Django admin. + + This Signal is called via: + * SSOwatUserBackend after a new user was created + * SSOwatRemoteUserMiddleware on login request + """ + user = kwargs['user'] + logger.info('Receive "setup_user" signal for user: "%s"', user) + + if not user.is_staff: + user.is_staff = True + user.save(update_fields=['is_staff']) + logger.info('Make user %s to a staff user', user) diff --git a/local_test.py b/local_test.py index aeccdbd..49ce2dc 100755 --- a/local_test.py +++ b/local_test.py @@ -2,11 +2,13 @@ """ Start django_ynh in YunoHost setup locally. - Note: - You can only run this script, if you are in a activated django_ynh venv! + + Run via: + make local-test + see README for details ;) """ - +import base64 import os import shlex import subprocess @@ -14,15 +16,12 @@ import sys from pathlib import Path -os.environ['DJANGO_SETTINGS_MODULE'] = 'django_ynh.settings' +os.environ['DJANGO_SETTINGS_MODULE'] = 'django_ynh_demo_settings' try: - import inventory_project # noqa + import django_ynh # noqa except ImportError as err: - raise ImportError( - 'Couldn\'t import django_ynh. Did you ' - 'forget to activate a virtual environment?' - ) from err + raise ImportError('Couldn\'t import django_ynh. Did you ' 'forget to activate a virtual environment?') from err BASE_PATH = Path(__file__).parent.absolute() @@ -34,25 +33,19 @@ FINAL_WWW_PATH = TEST_PATH / 'var_www' LOG_FILE = TEST_PATH / 'var_log_django_ynh.log' MANAGE_PY_FILE = CONF_PATH / 'manage.py' -CREATE_SUPERUSER_FILE = CONF_PATH / 'create_superuser.py' -SETTINGS_FILE = CONF_PATH / 'django_ynh.settings.py' -URLS_FILE = CONF_PATH / 'ynh_urls.py' +SETTINGS_FILE = CONF_PATH / 'django_ynh_demo_settings.py' +URLS_FILE = CONF_PATH / 'django_ynh_demo_urls.py' REPLACES = { '__FINAL_HOME_PATH__': str(FINAL_HOME_PATH), '__FINAL_WWW_PATH__': str(FINAL_WWW_PATH), '__LOG_FILE__': str(TEST_PATH / 'var_log_django_ynh.log'), - '__PATH_URL__': 'app_path', '__DOMAIN__': '127.0.0.1', - 'django.db.backends.postgresql': 'django.db.backends.sqlite3', "'NAME': '__APP__',": f"'NAME': '{TEST_PATH / 'test_db.sqlite'}',", - 'django_redis.cache.RedisCache': 'django.core.cache.backends.dummy.DummyCache', - 'DEBUG = False': 'DEBUG = True', - # Just use the default logging setup from django_ynh project: 'LOGGING = {': 'HACKED_DEACTIVATED_LOGGING = {', } @@ -72,12 +65,7 @@ def verbose_check_call(command, verbose=True, **kwargs): env['PYTHONUNBUFFERED'] = '1' popenargs = shlex.split(command) - subprocess.check_call( - popenargs, - universal_newlines=True, - env=env, - **kwargs - ) + subprocess.check_call(popenargs, universal_newlines=True, env=env, **kwargs) def call_manage_py(args): @@ -102,13 +90,21 @@ def copy_patch(src_file, replaces=None): f.write(content) +def assert_is_dir(dir_path): + assert dir_path.is_dir, f'Directory does not exists: {dir_path}' + + +def assert_is_file(file_path): + assert file_path.is_file, f'File not found: {file_path}' + + def main(): print('-' * 100) - assert BASE_PATH.is_dir() - assert CONF_PATH.is_dir() - assert SETTINGS_FILE.is_file() - assert URLS_FILE.is_file() + assert_is_dir(BASE_PATH) + assert_is_dir(CONF_PATH) + assert_is_file(SETTINGS_FILE) + assert_is_file(URLS_FILE) for p in (TEST_PATH, FINAL_HOME_PATH, FINAL_WWW_PATH): if p.is_dir(): @@ -122,10 +118,7 @@ def main(): # conf/manage.py -> local_test/manage.py copy_patch(src_file=MANAGE_PY_FILE) - # conf/create_superuser.py -> local_test/opt_yunohost/create_superuser.py - copy_patch(src_file=CREATE_SUPERUSER_FILE) - - # conf/django_ynh.settings.py -> local_test/django_ynh.settings.py + # conf/django_ynh_demo_settings.py -> local_test/django_ynh_demo_settings.py copy_patch(src_file=SETTINGS_FILE, replaces=REPLACES) # conf/ynh_urls.py -> local_test/ynh_urls.py @@ -133,7 +126,8 @@ def main(): with Path(FINAL_HOME_PATH / 'local_settings.py').open('w') as f: f.write('# Only for local test run\n') - f.write('SERVE_FILES=True # used in src/inventory_project/urls.py\n') + f.write('SERVE_FILES = True # used in src/inventory_project/urls.py\n') + f.write('AUTH_PASSWORD_VALIDATORS = [] # accept all passwords\n') # call "local_test/manage.py" via subprocess: call_manage_py('check --deploy') @@ -141,10 +135,24 @@ def main(): call_manage_py('collectstatic --no-input') verbose_check_call( - command=f'{sys.executable} create_superuser.py --username="test" --password="test"', + command=( + f'{sys.executable} -m django_ynh.create_superuser' + ' --ds="django_ynh_demo_settings" --username="test" --password="test"' + ), cwd=FINAL_HOME_PATH, ) + # All environment variables are passed to Django's "runnserver" ;) + # "Simulate" SSOwat authentication, by set "http headers" + # Still missing is the 'SSOwAuthUser' cookie, + # but this is ignored, if settings.DEBUG=True ;) + os.environ['HTTP_AUTH_USER'] = 'test' + os.environ['HTTP_REMOTE_USER'] = 'test' + + creds = str(base64.b64encode(b'test:test'), encoding='utf-8') + basic_auth = f'basic {creds}' + os.environ['HTTP_AUTHORIZATION'] = basic_auth + try: call_manage_py('runserver --nostatic') except KeyboardInterrupt: diff --git a/pyproject.toml b/pyproject.toml index 1e64fb4..e5ff84a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] +create_superuser = "django_ynh.create_superuser:main" publish = "django_ynh_tests.test_project.publish:publish" [tool.isort] diff --git a/scripts/change_url b/scripts/change_url index 5ec1a59..01fe801 100644 --- a/scripts/change_url +++ b/scripts/change_url @@ -116,10 +116,10 @@ fi ynh_script_progression --message="Modify django_ynh's config file..." # save old settings file -settings="$final_path/django_ynh.settings.py" +settings="$final_path/settings.py" ynh_backup_if_checksum_is_different --file="$settings" -cp "../conf/django_ynh.settings.py" "$settings" +cp "../conf/settings.py" "$settings" ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings" ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings" diff --git a/scripts/install b/scripts/install index 8e099d0..194260e 100755 --- a/scripts/install +++ b/scripts/install @@ -137,8 +137,8 @@ chmod +x "$final_path/manage.py" cp ../conf/wsgi.py "$final_path/wsgi.py" -settings="$final_path/django_ynh.settings.py" -cp "../conf/django_ynh.settings.py" "$settings" +settings="$final_path/settings.py" +cp "../conf/settings.py" "$settings" ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings" ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings" diff --git a/scripts/upgrade b/scripts/upgrade index 00fa320..ceb35c2 100755 --- a/scripts/upgrade +++ b/scripts/upgrade @@ -127,10 +127,10 @@ ynh_backup_if_checksum_is_different --file="$final_path/wsgi.py" cp ../conf/wsgi.py "$final_path/wsgi.py" # save old settings file -settings="$final_path/django_ynh.settings.py" +settings="$final_path/settings.py" ynh_backup_if_checksum_is_different --file="$settings" -cp "../conf/django_ynh.settings.py" "$settings" +cp "../conf/settings.py" "$settings" ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings" ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings"