diff --git a/.gitignore b/.gitignore index 9cd722f..2a52338 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ falko_gravatar.jpg dump_all*.sql dist/ .env.local +tmp/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1d21963..7aa611a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,9 +4,25 @@ image: - "/bin/sh" - "-c" +# Cache pip deps to speed up builds +cache: + paths: + - .pipcache +variables: + PIP_CACHE_DIR: .pipcache + test_and_coverage: stage: build coverage: "/^TOTAL.*\\s+(\\d+\\%)$/" + services: + - postgres:latest + variables: + POSTGRES_DB: django_db + POSTGRES_USER: django_user + POSTGRES_PASSWORD: django_password + POSTGRES_HOST: postgres + DATABASE_URL: "postgres://django_user:django_password@postgres/django_db" + PYTHONUNBUFFERED: 1 before_script: - virtualenv -p python3 /tmp/.virtualenv - source /tmp/.virtualenv/bin/activate @@ -24,8 +40,9 @@ test_and_coverage: - echo "DEBUG = True" >> config_local.py - echo "from config import CACHES" >> config_local.py - echo "CACHES['default'] = CACHES['filesystem']" >> config_local.py + - python manage.py sqldsn - python manage.py collectstatic --noinput - - coverage run --source . manage.py test -v3 + - coverage run --source . manage.py test -v3 --noinput - coverage report --fail-under=70 - coverage html artifacts: diff --git a/config.py b/config.py index 0884d41..cb4e18c 100644 --- a/config.py +++ b/config.py @@ -153,6 +153,19 @@ if "POSTGRESQL_DATABASE" in os.environ: "HOST": "postgresql", } +# CI/CD config has different naming +if "POSTGRES_DB" in os.environ: + DATABASES["default"] = { # pragma: no cover + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ["POSTGRES_DB"], + "USER": os.environ["POSTGRES_USER"], + "PASSWORD": os.environ["POSTGRES_PASSWORD"], + "HOST": os.environ["POSTGRES_HOST"], + "TEST": { + "NAME": os.environ["POSTGRES_DB"], + }, + } + SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer" USE_X_FORWARDED_HOST = True diff --git a/ivatar/ivataraccount/test_views.py b/ivatar/ivataraccount/test_views.py index 3dfceca..8cfcebb 100644 --- a/ivatar/ivataraccount/test_views.py +++ b/ivatar/ivataraccount/test_views.py @@ -395,8 +395,8 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods self.assertEqual(response["Content-Type"], "image/jpg", "Content type wrong!?") self.assertEqual( - response.content, - self.user.photo_set.first().data, + bytes(response.content), + bytes(self.user.photo_set.first().data), "raw_image should return the same content as if we\ read it directly from the DB", ) diff --git a/ivatar/settings.py b/ivatar/settings.py index a391353..f5ca083 100644 --- a/ivatar/settings.py +++ b/ivatar/settings.py @@ -42,6 +42,7 @@ MIDDLEWARE = [ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.middleware.locale.LocaleMiddleware", ] ROOT_URLCONF = "ivatar.urls" @@ -49,7 +50,7 @@ ROOT_URLCONF = "ivatar.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], + "DIRS": [os.path.join(BASE_DIR, "templates")], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -57,7 +58,9 @@ TEMPLATES = [ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", + "django.template.context_processors.i18n", ], + "debug": DEBUG, }, }, ] @@ -72,6 +75,7 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + "ATOMIC_REQUESTS": True, } } @@ -85,6 +89,9 @@ AUTH_PASSWORD_VALIDATORS = [ }, { "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", # noqa + "OPTIONS": { + "min_length": 6, + }, }, { "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", # noqa @@ -94,6 +101,26 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] +# Password Hashing (more secure) +PASSWORD_HASHERS = [ + # This isn't working in older Python environments + # "django.contrib.auth.hashers.Argon2PasswordHasher", + "django.contrib.auth.hashers.PBKDF2PasswordHasher", + "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", +] + +# Security Settings +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True +X_FRAME_OPTIONS = "DENY" +CSRF_COOKIE_SECURE = not DEBUG +SESSION_COOKIE_SECURE = not DEBUG + +if not DEBUG: + SECURE_SSL_REDIRECT = True + SECURE_HSTS_SECONDS = 31536000 # 1 year + SECURE_HSTS_INCLUDE_SUBDOMAINS = True + SECURE_HSTS_PRELOAD = True # Internationalization # https://docs.djangoproject.com/en/2.0/topics/i18n/ @@ -116,4 +143,4 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static") DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -from config import * # pylint: disable=wildcard-import,wrong-import-position,unused-wildcard-import +from config import * # pylint: disable=wildcard-import,wrong-import-position,unused-wildcard-import # noqa diff --git a/requirements.txt b/requirements.txt index 78b5ce3..0255718 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ autopep8 bcrypt defusedxml -Django +Django>=5.1 django-anymail[mailgun] django-auth-ldap django-bootstrap4 diff --git a/templates/navbar.html b/templates/navbar.html index b95673f..08c6c9c 100644 --- a/templates/navbar.html +++ b/templates/navbar.html @@ -1,6 +1,6 @@ {% load i18n %} {% block topbar_base %} -