diff --git a/.gitignore b/.gitignore index 3d68234..26bc4b0 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ snappea.sqlite3-wal # node (tailwind) node_modules /package* + +# conf +bugsink_conf.py diff --git a/bugsink/scripts/create_example_conf.py b/bugsink/scripts/create_example_conf.py index b30b878..c82d967 100644 --- a/bugsink/scripts/create_example_conf.py +++ b/bugsink/scripts/create_example_conf.py @@ -17,7 +17,7 @@ def main(): secret_key = get_random_secret_key() with open(args.output_file, "w") as f: - f.write('''# auto-generated example bugsink_conf.py + f.write('''from bugsink.settings.default import * # noqa # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "''' + secret_key + '''" @@ -25,7 +25,18 @@ SECRET_KEY = "''' + secret_key + '''" # Alternatively, pass the SECRET_KEY as an environment variable. (although that has security implications too!) # i.e. those may leak in shared server setups. # -# SECRET_KEY = os.getenv("SECRET_KEY") +# SECRET_KEY = os.environ["SECRET_KEY"] # use dictionary lookup rather than .getenv to ensure the variable is set. + + +ALLOWED_HOSTS = ["bugsink.example.org"] # set this to match your host (TODO: check what happens in forwarded configs?) + + +# TODO refer to database documentation and provide instructions for overrides. + + +# The time-zone here is the default for display purposes (when no project/user configuration is used). +# https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-TIME_ZONE +TIME_ZONE = 'Europe/Amsterdam' # See TODO in the docs @@ -55,9 +66,11 @@ BUGSINK = { # "BASE_URL": "http://bugsink:9000", # no trailing slash # "SITE_TITLE": "Bugsink", # you can customize this as e.g. "My Bugsink" or "Bugsink for My Company" } - ''') + print("Configuration file created at", args.output_file) + print("Edit this file to match your setup") + # some thoughts that I haven't been able to squish into a short comment yet: @@ -81,3 +94,6 @@ BUGSINK = { # 3. regarding the sensitivity of this key, and storing it in the file system: I'd argue that if the server you're # running bugsink on is compromised (and the file can be read) you have bigger problems (since the DB is also on that # server) + +if __name__ == "__main__": + main() diff --git a/bugsink/settings/default.py b/bugsink/settings/default.py index 2d7d240..6a8f817 100644 --- a/bugsink/settings/default.py +++ b/bugsink/settings/default.py @@ -6,8 +6,6 @@ from pathlib import Path from django.utils.log import DEFAULT_LOGGING -from debug_toolbar.middleware import show_toolbar - # We have a single file for our default settings, and expect (if they use the recommended setup) the end-users to # configure their setup using a single bugsink_conf.py also. To be able to have (slightly) different settings for e.g. @@ -23,23 +21,19 @@ else: # Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent +BASE_DIR = Path(__file__).resolve().parent.parent.parent -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-$@clhhieazwnxnha-_zah&(bieq%yux7#^07&xsvhn58t)8@xw' +# To allow using this file without any bugsink_conf.py overrides, we get some variables from the environment. Because +# the expected use-case of this file is using the `from bugsink.settings.default import *` idiom, which implies that +# variables may very well be defined explicitly in a bugsink_conf.py or similar explicit settings file, we cannot +# enforce the existance of environment variables, so we always use os.getenv with a sane fallback. + +# The fallback here is such that Django will fail to start if no SECRET_KEY is (eventually) defined, which is the goal. +SECRET_KEY = os.getenv("SECRET_KEY", "") DEBUG = False -ALLOWED_HOSTS = ["*"] # SECURITY WARNING: also make production-worthy - -INTERNAL_IPS = [ - "127.0.0.1", -] - - -# Application definition - INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', @@ -48,10 +42,9 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', - 'debug_toolbar', - 'tailwind', + 'tailwind', # As currently set up, this is also needed in production (templatetags) 'theme', - 'admin_auto_filters', + 'admin_auto_filters', # TODO: decide whether 'admin.py' is useful in production too. 'snappea', 'compat', @@ -68,8 +61,6 @@ INSTALLED_APPS = [ TAILWIND_APP_NAME = 'theme' MIDDLEWARE = [ - "debug_toolbar.middleware.DebugToolbarMiddleware", - 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -81,7 +72,7 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'bugsink.middleware.PerformanceStatsMiddleware', + 'bugsink.middleware.PerformanceStatsMiddleware', # TODO decide whether this is useful in production too. ] ROOT_URLCONF = 'bugsink.urls' @@ -113,7 +104,6 @@ WSGI_APPLICATION = 'bugsink.wsgi.application' # Database # https://docs.djangoproject.com/en/4.2/ref/settings/#databases - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', @@ -121,7 +111,7 @@ DATABASES = { 'TEST': { # Specifying a NAME here makes it so that sqlite doesn't run in-memory. This is what we want, because we # want our tests to be as similar to the real thing as possible. - "NAME": BASE_DIR / os.getenv("DATABASE_NAME", 'test.sqlite3'), + "NAME": BASE_DIR / os.getenv("TEST_DATABASE_NAME", 'test.sqlite3'), }, 'OPTIONS': { # the "timeout" option here is passed to the Python sqlite3.connect() translates into the busy_timeout @@ -132,7 +122,7 @@ DATABASES = { }, "snappea": { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / os.getenv("DATABASE_NAME", 'snappea.sqlite3'), + 'NAME': BASE_DIR / os.getenv("SNAPPEA_DATABASE_NAME", 'snappea.sqlite3'), # 'TEST': { postponed, for starters we'll do something like SNAPPEA_ALWAYS_EAGER 'OPTIONS': { 'timeout': 5, @@ -148,7 +138,6 @@ LOGIN_REDIRECT_URL = "/" # Password validation # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators - AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', @@ -166,7 +155,6 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ - LANGUAGE_CODE = 'en-us' TIME_ZONE = 'Europe/Amsterdam' @@ -178,7 +166,6 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ - STATIC_URL = 'static/' STATICFILES_DIRS = [ BASE_DIR / "static", @@ -190,23 +177,13 @@ STATICFILES_DIRS = [ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -def show_toolbar_for_queryparam(request): - if "__debug__" not in request.path and not request.GET.get("debug", ""): - return False - return show_toolbar(request) - - -DEBUG_TOOLBAR_CONFIG = { - "SHOW_TOOLBAR_CALLBACK": show_toolbar_for_queryparam, -} - - LOGGING = deepcopy(DEFAULT_LOGGING) if I_AM_RUNNING != "TEST": # Django's standard logging has LOGGING['handlers']['console']['filters'] = ['require_debug_true']; our app is - # configured (by default at least) to just spit everything on stdout, even in production. stdout is picked up by - # gunicorn, and we can "take it from there". + # configured (by default at least) to just spit everything on stdout, especially in production. stdout is picked up + # by e.g. gunicorn, and we can "take it from there". We don't do this when running tests, because tests are run with + # DEBUG=False and we don't want the visual pollution. LOGGING['handlers']['console']['filters'] = [] LOGGING['loggers']['bugsink'] = { @@ -221,14 +198,14 @@ LOGGING["formatters"]["snappea"] = { } LOGGING["handlers"]["snappea"] = { - "level": "DEBUG" if DEBUG else "INFO", # TODO this won't work either. but this I can do more classically (development.py) + "level": "INFO", "class": "logging.StreamHandler" } LOGGING["handlers"]["snappea"]["formatter"] = "snappea" LOGGING['loggers']['snappea'] = { - "level": "DEBUG" if DEBUG else "INFO", + "level": "INFO", "handlers": ["snappea"], } diff --git a/bugsink/settings/development.py b/bugsink/settings/development.py index ec86df7..4edfe8f 100644 --- a/bugsink/settings/development.py +++ b/bugsink/settings/development.py @@ -1,13 +1,47 @@ from .default import * # noqa +from .default import INSTALLED_APPS, MIDDLEWARE, LOGGING import os +from debug_toolbar.middleware import show_toolbar + import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration +SECRET_KEY = 'django-insecure-$@clhhieazwnxnha-_zah&(bieq%yux7#^07&xsvhn58t)8@xw' DEBUG = True +# > The Debug Toolbar is shown only if your IP address is listed in Django’s INTERNAL_IPS setting. This means that for +# > local development, you must add "127.0.0.1" to INTERNAL_IPS. +INTERNAL_IPS = [ + "127.0.0.1", +] + +# TODO monitor https://stackoverflow.com/questions/78476951/why-not-just-set-djangos-allowed-hosts-to +# (also for create_example_conf.py) +ALLOWED_HOSTS = ["*"] + +INSTALLED_APPS += [ + "debug_toolbar", +] + +MIDDLEWARE = [ + "debug_toolbar.middleware.DebugToolbarMiddleware", +] + MIDDLEWARE + + +def show_toolbar_for_queryparam(request): + if "__debug__" not in request.path and not request.GET.get("debug", ""): + return False + return show_toolbar(request) + + +DEBUG_TOOLBAR_CONFIG = { + "SHOW_TOOLBAR_CALLBACK": show_toolbar_for_queryparam, +} + + # {PROTOCOL}://{PUBLIC_KEY}:{DEPRECATED_SECRET_KEY}@{HOST}{PATH}/{PROJECT_ID} SENTRY_DSN = os.getenv("SENTRY_DSN") @@ -47,3 +81,6 @@ BUGSINK = { "BASE_URL": "http://bugsink:9000", # no trailing slash "SITE_TITLE": "Bugsink", # you can customize this as e.g. "My Bugsink" or "Bugsink for My Company" } + +LOGGING["handlers"]["snappea"]["level"] = "DEBUG" +LOGGING["loggers"]["snappea"]["level"] = "DEBUG" diff --git a/manage.py b/manage.py index babf2a3..154b270 100755 --- a/manage.py +++ b/manage.py @@ -6,7 +6,7 @@ import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bugsink.settings') + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bugsink.settings.development') try: from django.core.management import execute_from_command_line except ImportError as exc: