blender-id/blenderid/settings.py

397 lines
13 KiB
Python

"""
Django settings for blenderid project.
Generated by 'django-admin startproject' using Django 1.9.10.
For more information on this file, see
https://docs.djangoproject.com/en/1.9/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.9/ref/settings/
"""
import datetime
import os
import pathlib
import pytz
import sys
from dotenv import load_dotenv
from django.urls import reverse_lazy
import dj_database_url
# Load variables from .env, if available
path = os.path.dirname(os.path.abspath(__file__)) + '/../.env'
if os.path.isfile(path):
load_dotenv(path)
BASE_DIR = pathlib.Path(__file__).absolute().parent.parent
# Used when generating links to ourselves outside of the context of a request.
# Preferrably HttpRequest.build_absolute_uri() is used.
PREFERRED_SCHEME = "https"
# SECURITY WARNING: keep the secret key used in production secret!
# Update this to something unique for your machine.
SECRET_KEY = os.getenv('SECRET_KEY', 'default-dev-secret')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(os.getenv('DEBUG', False))
TESTING = sys.argv[1:2] == ['test']
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'id.local').split(',')
SITE_ID = 1
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.admindocs",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.humanize",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.sites",
"django.contrib.flatpages",
"oauth2_provider",
"pipeline",
"sorl.thumbnail",
"django_admin_select2",
"loginas",
"bid_main",
"bid_api",
"bid_addon_support",
"background_task",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"bid_main.middleware.user_session_middleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"oauth2_provider.middleware.OAuth2TokenMiddleware",
]
AUTHENTICATION_BACKENDS = [
"oauth2_provider.backends.OAuth2Backend",
"django.contrib.auth.backends.ModelBackend",
]
ROOT_URLCONF = "blenderid.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
BASE_DIR / "templates",
BASE_DIR / "assets_shared" / "src" / "templates",
],
"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",
"bid_main.context_processors.settings",
],
'loaders': {
(
'pypugjs.ext.django.Loader',
(
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
),
)
},
'builtins': [
'pypugjs.ext.django.templatetags',
],
},
},
]
WSGI_APPLICATION = "blenderid.wsgi.application"
DATABASES = {
'default': dj_database_url.config(default='sqlite:///{}'.format(BASE_DIR / 'db.sqlite3')),
}
DATABASES['default']['CONN_MAX_AGE'] = 600
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
DATE_FORMAT = "d-b-Y (D)"
DATETIME_FORMAT = "d-b-Y, H:i"
SHORT_DATE_FORMAT = "Y-m-d"
SHORT_DATETIME_FORMAT = f"{SHORT_DATE_FORMAT}, H:i"
TIME_FORMAT = "H:i:s"
TIME_ZONE = "CET"
USE_TZ = True
LANGUAGE_CODE = "en-us"
USE_I18N = True
USE_L10N = False
PIPELINE = {
'JS_COMPRESSOR': 'pipeline.compressors.jsmin.JSMinCompressor',
'CSS_COMPRESSOR': 'pipeline.compressors.NoopCompressor',
'JAVASCRIPT': {
'notifications': {
'source_filenames': [
'bid_main/scripts/notifications.js',
],
'output_filename': 'js/notifications.js',
'extra_context': {'async': False, 'defer': False},
},
'web-assets': {
'source_filenames': ('tutti/10_navbar.js',),
'output_filename': 'js/web-assets.js',
'extra_context': {'async': True, 'defer': True},
},
},
'STYLESHEETS': {
'main': {
'source_filenames': ('bid_main/styles/main.sass',),
'output_filename': 'css/main.css',
'extra_context': {'media': 'screen,projection'},
},
},
'COMPILERS': ('libsasscompiler.LibSassCompiler',),
'DISABLE_WRAPPER': True,
}
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = "/static/"
STATIC_ROOT = os.getenv('STATIC_ROOT', BASE_DIR / "static")
STATICFILES_DIRS = [
str(BASE_DIR / 'assets_shared'),
str(BASE_DIR / 'assets_shared/src/scripts/'),
]
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'pipeline.finders.PipelineFinder',
]
STATICFILES_STORAGE = 'pipeline.storage.PipelineManifestStorage'
# Uploaded files
# https://docs.djangoproject.com/en/2.0/ref/settings/#std:setting-MEDIA_ROOT
MEDIA_URL = "/media/"
MEDIA_ROOT = os.getenv('MEDIA_ROOT', BASE_DIR / "media") # may not be inside STATIC_ROOT
with open(BASE_DIR / "oidc.key", "r") as file:
# Can be generated with 'openssl genrsa -out oidc.key 4096'
oidc_rsa_private_key = file.read()
# Defining one of those means you have to define them all.
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "bid_main.OAuth2AccessToken"
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "bid_main.OAuth2RefreshToken"
OAUTH2_PROVIDER_APPLICATION_MODEL = "bid_main.OAuth2Application"
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "oauth2_provider.IDToken"
OAUTH2_PROVIDER = {
"OAUTH2_VALIDATOR_CLASS": "bid_main.oauth_validator.BlenderIDOAuth2Validator",
"OIDC_ENABLED": True,
"OIDC_RSA_PRIVATE_KEY": oidc_rsa_private_key,
"SCOPES": {
"email": "Default scope",
"badge": "Read access to the user's badges",
"openid": "OpenID Connect scope",
"profile": "Profile details such as name and nickname",
},
"PKCE_REQUIRED": False,
"ALLOWED_REDIRECT_URI_SCHEMES": ["https"],
"REQUEST_APPROVAL_PROMPT": "auto",
"ACCESS_TOKEN_EXPIRE_SECONDS": 3600 * 24 * 31, # keep for a month
"REFRESH_TOKEN_EXPIRE_SECONDS": 3600 * 24 * 31, # keep for a month
"ACCESS_TOKEN_MODEL": OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL,
"REFRESH_TOKEN_MODEL": OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL,
"APPLICATION_MODEL": OAUTH2_PROVIDER_APPLICATION_MODEL,
}
# This is required for compatibility with Blender Cloud, as it performs
# a POST request to /oauth/token.
APPEND_SLASH = False
# Read https://docs.djangoproject.com/en/1.9/topics/auth/passwords/#password-upgrading
# before removing any password hasher from this list.
PASSWORD_HASHERS = [
"bid_main.hashers.BlenderIdPasswordHasher",
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
"django.contrib.auth.hashers.BCryptPasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
]
AUTH_USER_MODEL = "bid_main.User"
LOGIN_URL = "bid_main:login"
LOGIN_REDIRECT_URL = "bid_main:index"
LOGOUT_URL = reverse_lazy("bid_main:logout")
# Hosts that we allow redirecting to with a next=xxx parameter on the /login and /switch
# endpoints. This is a limited set for security reasons.
NEXT_REDIR_AFTER_LOGIN_ALLOWED_HOSTS = {
"cloud.local:5000",
"cloud.local:5001",
"cloud.blender.org",
"blender.cloud",
"blender.community",
}
CSRF_COOKIE_SECURE = True
CSRF_FAILURE_VIEW = "bid_main.views.errors.csrf_failure"
CSRF_TRUSTED_ORIGINS = ['https://*.blender.org']
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
# Privacy Policy date; anyone who agreed to the privacy policy before this date
# (or never) will be presented with an agreement prompt and has to agree before
# being able to use the website.
PPDATE = datetime.datetime(2018, 5, 18, 0, 0, 0, tzinfo=pytz.utc)
AVATAR_ALLOWED_FILE_EXTS = {".jpeg", ".jpg", ".png", ".webp"}
# Make sure this is less than the client_max_body_size nginx setting:
AVATAR_MAX_SIZE_BYTES = 2 * 1024**2
AVATAR_DEFAULT_FILENAME = "assets/images/default_user_avatar.png"
AVATAR_CONTENT_TYPE = "image/jpeg"
AVATAR_DEFAULT_SIZE_PIXELS = 160
# **N.B.**: thumbnail sizes are not intended to just be changed on the fly:
# thumbnails of existing images must exist in MEDIA_ROOT before
# the code expecting the new dimensions can be deployed!
AVATAR_THUMBNAIL_SIZES = [AVATAR_DEFAULT_SIZE_PIXELS, 128, 32]
THUMBNAIL_FORMAT = "JPEG"
THUMBNAIL_QUALITY = 83
GOOGLE_ANALYTICS_TRACKING_ID = os.getenv('GOOGLE_ANALYTICS_TRACKING_ID', '')
GOOGLE_RECAPTCHA_SITE_KEY = os.getenv('GOOGLE_RECAPTCHA_SITE_KEY', '')
GOOGLE_RECAPTCHA_SECRET_KEY = os.getenv('GOOGLE_RECAPTCHA_SECRET_KEY', '')
# Background tasks settings
MAX_ATTEMPTS = 5
BLENDER_ID_ADDON_CLIENT_ID = os.getenv('BLENDER_ID_ADDON_CLIENT_ID', 'SPECIAL-SNOWFLAKE-57')
# For testing purposes, allow HTTP as well as HTTPS. Never enable this in production!
# FIXME, add if DEBUG:
# now we have some http redirect uris in production
# they MUST go away
OAUTH2_PROVIDER['ALLOWED_REDIRECT_URI_SCHEMES'] = ['http', 'https']
LOGGING = {
"version": 1,
"disable_existing_loggers": True,
"formatters": {
"default": {"format": "%(asctime)-15s %(levelname)8s %(name)s %(message)s"},
"verbose": {
"format": "%(asctime)-15s %(levelname)8s %(name)s %(process)d %(thread)d %(message)s"
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "default", # Set to 'verbose' in production
"stream": "ext://sys.stderr",
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
'include_html': True,
},
},
"loggers": {
"bid_main": {"level": "DEBUG"},
"blenderid": {"level": "DEBUG"},
"bid_api": {"level": "DEBUG"},
"bid_addon_support": {"level": "DEBUG"},
},
"root": {
"level": "WARNING",
"handlers": [
"console",
"mail_admins",
],
},
}
if TESTING:
# Disable logging output when running tests
LOGGING = {
'version': 1,
'loggers': {
'': {
'level': 'CRITICAL',
},
},
}
# For Debug Toolbar, extend with whatever address you use to connect
# to your dev server.
if DEBUG:
CSRF_COOKIE_SECURE = False
INSTALLED_APPS += ['debug_toolbar']
INTERNAL_IPS = ["127.0.0.1"]
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
SESSION_COOKIE_SECURE = False
if SENTRY_DSN := os.getenv('SENTRY_DSN'):
import logging
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
# By default Sentry only sends ERROR level and above.
sentry_logging = LoggingIntegration(
level=logging.INFO, # Capture this level and above as breadcrumbs
event_level=logging.WARNING # Send this level and above as events
)
sentry_sdk.init(
dsn=SENTRY_DSN,
integrations=[sentry_logging, DjangoIntegration()],
send_default_pii=False,
)
# For development, dump email to the console instead of trying to actually send it.
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'Blender ID <noreply@id.blender.org>')
EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend')
if os.environ.get('EMAIL_HOST') is not None:
EMAIL_HOST = os.getenv('EMAIL_HOST')
if os.environ.get('EMAIL_PORT') is not None:
EMAIL_PORT = os.getenv('EMAIL_PORT')
if os.environ.get('EMAIL_HOST_USER') is not None:
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
if os.environ.get('EMAIL_HOST_PASSWORD') is not None:
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
if os.environ.get('ADMINS') is not None:
# Expects the following format:
# ADMINS='J Doe: jane@example.com, John Dee: john@example.com'
ADMINS = [[_.strip() for _ in adm.split(':')] for adm in os.environ.get('ADMINS').split(',')]
MAIL_SUBJECT_PREFIX = '[Blender ID]'
SERVER_EMAIL = f'django@{ALLOWED_HOSTS[0]}'