settings.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. """
  2. Django settings for glitchtip project.
  3. Generated by 'django-admin startproject' using Django 3.0rc1.
  4. For more information on this file, see
  5. https://docs.djangoproject.com/en/dev/topics/settings/
  6. For the full list of settings and their values, see
  7. https://docs.djangoproject.com/en/dev/ref/settings/
  8. """
  9. import logging
  10. import os
  11. import sys
  12. import warnings
  13. from datetime import timedelta
  14. import environ
  15. import sentry_sdk
  16. from celery.schedules import crontab
  17. from corsheaders.defaults import default_headers
  18. from django.core.exceptions import ImproperlyConfigured
  19. from sentry_sdk.integrations.django import DjangoIntegration
  20. from whitenoise.storage import CompressedManifestStaticFilesStorage
  21. env = environ.Env(
  22. ALLOWED_HOSTS=(list, ["*"]),
  23. DEFAULT_FILE_STORAGE=(str, None),
  24. AWS_ACCESS_KEY_ID=(str, None),
  25. AWS_SECRET_ACCESS_KEY=(str, None),
  26. AWS_STORAGE_BUCKET_NAME=(str, None),
  27. AWS_S3_ENDPOINT_URL=(str, None),
  28. AWS_LOCATION=(str, ""),
  29. AZURE_ACCOUNT_NAME=(str, None),
  30. AZURE_ACCOUNT_KEY=(str, None),
  31. AZURE_CONTAINER=(str, None),
  32. AZURE_URL_EXPIRATION_SECS=(int, None),
  33. GS_BUCKET_NAME=(str, None),
  34. GS_PROJECT_ID=(str, None),
  35. DEBUG=(bool, False),
  36. DEBUG_TOOLBAR=(bool, False),
  37. STATIC_URL=(str, "/"),
  38. STATICFILES_STORAGE=(
  39. str,
  40. "glitchtip.settings.NoSourceMapsStorage",
  41. ),
  42. ENABLE_OBSERVABILITY_API=(bool, False),
  43. )
  44. path = environ.Path()
  45. # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
  46. BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  47. # Quick-start development settings - unsuitable for production
  48. # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/
  49. # SECURITY WARNING: keep the secret key used in production secret!
  50. SECRET_KEY = env.str("SECRET_KEY", "change_me")
  51. # SECURITY WARNING: don't run with debug turned on in production!
  52. DEBUG = env("DEBUG")
  53. # Enable only for running end to end testing. Debug must be True to use.
  54. ENABLE_TEST_API = env.bool("ENABLE_TEST_API", False)
  55. if DEBUG is False:
  56. ENABLE_TEST_API = False
  57. ALLOWED_HOSTS = env("ALLOWED_HOSTS")
  58. # Necessary for kubernetes health checks
  59. POD_IP = env.str("POD_IP", default=None)
  60. if POD_IP:
  61. ALLOWED_HOSTS.append(POD_IP)
  62. ENVIRONMENT = env.str("ENVIRONMENT", None)
  63. GLITCHTIP_VERSION = env.str("GLITCHTIP_VERSION", "0.0.0-unknown")
  64. # Used in email and DSN generation. Set to full domain such as https://glitchtip.example.com
  65. default_url = env.str(
  66. "APP_URL", env.str("GLITCHTIP_DOMAIN", "http://localhost:8000")
  67. ) # DigitalOcean App Platform uses APP_URL
  68. GLITCHTIP_URL = env.url("GLITCHTIP_URL", default_url)
  69. if GLITCHTIP_URL.scheme not in ["http", "https"]:
  70. raise ImproperlyConfigured("GLITCHTIP_DOMAIN must start with http or https")
  71. # Events and associated data older than this will be deleted from the database
  72. GLITCHTIP_MAX_EVENT_LIFE_DAYS = env.int("GLITCHTIP_MAX_EVENT_LIFE_DAYS", default=90)
  73. GLITCHTIP_MAX_TRANSACTION_EVENT_LIFE_DAYS = env.int(
  74. "GLITCHTIP_MAX_TRANSACTION_EVENT_LIFE_DAYS", default=GLITCHTIP_MAX_EVENT_LIFE_DAYS
  75. )
  76. # Defaults to twice as long as event life
  77. GLITCHTIP_MAX_FILE_LIFE_DAYS = env.int(
  78. "GLITCHTIP_MAX_EVENT_LIFE_DAYS", default=GLITCHTIP_MAX_EVENT_LIFE_DAYS * 2
  79. )
  80. # For development purposes only, prints out inbound event store json
  81. EVENT_STORE_DEBUG = env.bool("EVENT_STORE_DEBUG", False)
  82. # Static files (CSS, JavaScript, Images)
  83. # https://docs.djangoproject.com/en/dev/howto/static-files/
  84. STATIC_URL = "/static/"
  85. # GlitchTip can track GlitchTip's own errors.
  86. # If enabling this, use a different server to avoid infinite loops.
  87. def before_send(event, hint):
  88. """Don't log django.DisallowedHost errors in Sentry."""
  89. if "log_record" in hint:
  90. if hint["log_record"].name == "django.security.DisallowedHost":
  91. return None
  92. return event
  93. SENTRY_DSN = env.str("SENTRY_DSN", None)
  94. # Optionally allow a different DSN for the frontend
  95. SENTRY_FRONTEND_DSN = env.str("SENTRY_FRONTEND_DSN", SENTRY_DSN)
  96. # Set traces_sample_rate to 1.0 to capture 100%. Recommended to keep this value low.
  97. SENTRY_TRACES_SAMPLE_RATE = env.float("SENTRY_TRACES_SAMPLE_RATE", 0.1)
  98. # Ignore whitenoise served static routes
  99. def traces_sampler(sampling_context):
  100. if (
  101. sampling_context.get("wsgi_environ", {})
  102. .get("PATH_INFO", "")
  103. .startswith(STATIC_URL)
  104. ):
  105. return 0.0
  106. return SENTRY_TRACES_SAMPLE_RATE
  107. if SENTRY_DSN:
  108. release = "glitchtip@" + GLITCHTIP_VERSION if GLITCHTIP_VERSION else None
  109. sentry_sdk.init(
  110. dsn=SENTRY_DSN,
  111. integrations=[DjangoIntegration()],
  112. before_send=before_send,
  113. release=release,
  114. environment=ENVIRONMENT,
  115. auto_session_tracking=False,
  116. traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
  117. traces_sampler=traces_sampler,
  118. )
  119. def show_toolbar(request):
  120. return env("DEBUG_TOOLBAR")
  121. DEBUG_TOOLBAR = env("DEBUG_TOOLBAR")
  122. DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar}
  123. DEBUG_TOOLBAR_PANELS = [
  124. "debug_toolbar.panels.versions.VersionsPanel",
  125. "debug_toolbar.panels.timer.TimerPanel",
  126. "debug_toolbar.panels.settings.SettingsPanel",
  127. "debug_toolbar.panels.headers.HeadersPanel",
  128. "debug_toolbar.panels.request.RequestPanel",
  129. "debug_toolbar.panels.sql.SQLPanel",
  130. ]
  131. # Application definition
  132. INSTALLED_APPS = [
  133. "django_rest_mfa.mfa_admin",
  134. "django.contrib.admin",
  135. "django.contrib.auth",
  136. "django.contrib.contenttypes",
  137. "django.contrib.sessions",
  138. "django.contrib.messages",
  139. "django.contrib.staticfiles",
  140. "django.contrib.sites",
  141. "django.contrib.postgres",
  142. "django_prometheus",
  143. "allauth",
  144. "allauth.account",
  145. "allauth.socialaccount",
  146. "allauth.socialaccount.providers.digitalocean",
  147. "allauth.socialaccount.providers.gitea",
  148. "allauth.socialaccount.providers.github",
  149. "allauth.socialaccount.providers.gitlab",
  150. "allauth.socialaccount.providers.google",
  151. "allauth.socialaccount.providers.microsoft",
  152. "allauth.socialaccount.providers.nextcloud",
  153. "allauth.socialaccount.providers.keycloak",
  154. "anymail",
  155. "corsheaders",
  156. "django_celery_results",
  157. "django_filters",
  158. "django_extensions",
  159. "django_rest_mfa",
  160. ]
  161. if DEBUG_TOOLBAR:
  162. INSTALLED_APPS.append("debug_toolbar")
  163. INSTALLED_APPS += [
  164. "rest_framework",
  165. "drf_yasg",
  166. "dj_rest_auth",
  167. "dj_rest_auth.registration",
  168. "storages",
  169. "glitchtip",
  170. "alerts",
  171. "api_tokens",
  172. "environments",
  173. "files",
  174. "organizations_ext",
  175. "events",
  176. "issues",
  177. "users",
  178. "user_reports",
  179. "glitchtip.uptime",
  180. "performance",
  181. "projects",
  182. "teams",
  183. "releases",
  184. "difs",
  185. ]
  186. # Ensure no one uses runsslserver in production
  187. if SECRET_KEY == "change_me" and DEBUG is True:
  188. INSTALLED_APPS += ["sslserver"]
  189. ENABLE_OBSERVABILITY_API = env("ENABLE_OBSERVABILITY_API")
  190. # Workaround https://github.com/korfuri/django-prometheus/issues/34
  191. PROMETHEUS_EXPORT_MIGRATIONS = False
  192. MIDDLEWARE = [
  193. "django.middleware.security.SecurityMiddleware",
  194. "django.contrib.sessions.middleware.SessionMiddleware",
  195. "corsheaders.middleware.CorsMiddleware",
  196. "csp.middleware.CSPMiddleware",
  197. "django.middleware.clickjacking.XFrameOptionsMiddleware",
  198. "whitenoise.middleware.WhiteNoiseMiddleware",
  199. ]
  200. if DEBUG_TOOLBAR:
  201. MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
  202. MIDDLEWARE += [
  203. "django.middleware.common.CommonMiddleware",
  204. "django.middleware.csrf.CsrfViewMiddleware",
  205. "django.contrib.auth.middleware.AuthenticationMiddleware",
  206. "django.contrib.messages.middleware.MessageMiddleware",
  207. "django.middleware.clickjacking.XFrameOptionsMiddleware",
  208. "sentry.middleware.proxy.DecompressBodyMiddleware",
  209. "django.middleware.locale.LocaleMiddleware",
  210. ]
  211. if ENABLE_OBSERVABILITY_API:
  212. MIDDLEWARE.insert(0, "django_prometheus.middleware.PrometheusBeforeMiddleware")
  213. MIDDLEWARE.append("django_prometheus.middleware.PrometheusAfterMiddleware")
  214. ROOT_URLCONF = "glitchtip.urls"
  215. TEMPLATES = [
  216. {
  217. "BACKEND": "django.template.backends.django.DjangoTemplates",
  218. "DIRS": [path("dist"), path("templates")],
  219. "APP_DIRS": True,
  220. "OPTIONS": {
  221. "context_processors": [
  222. "django.template.context_processors.debug",
  223. "django.template.context_processors.request",
  224. "django.contrib.auth.context_processors.auth",
  225. "django.contrib.messages.context_processors.messages",
  226. ],
  227. },
  228. },
  229. ]
  230. WSGI_APPLICATION = "glitchtip.wsgi.application"
  231. CORS_ORIGIN_ALLOW_ALL = env.bool("CORS_ORIGIN_ALLOW_ALL", True)
  232. CORS_ORIGIN_WHITELIST = env.tuple("CORS_ORIGIN_WHITELIST", str, default=())
  233. CORS_ALLOW_HEADERS = list(default_headers) + [
  234. "x-sentry-auth",
  235. ]
  236. BILLING_ENABLED = False
  237. if env.str("STRIPE_TEST_PUBLIC_KEY", None) or env.str("STRIPE_LIVE_PUBLIC_KEY", None):
  238. BILLING_ENABLED = True
  239. # Set to chatwoot website token to enable live help widget. Assumes app.chatwoot.com.
  240. CHATWOOT_WEBSITE_TOKEN = env.str("CHATWOOT_WEBSITE_TOKEN", None)
  241. CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", str, [])
  242. SECURE_BROWSER_XSS_FILTER = True
  243. CSP_DEFAULT_SRC = env.list("CSP_DEFAULT_SRC", str, ["'self'"])
  244. CSP_STYLE_SRC = env.list(
  245. "CSP_STYLE_SRC", str, ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"]
  246. )
  247. CSP_STYLE_SRC_ELEM = env.list(
  248. "CSP_STYLE_SRC_ELEM",
  249. str,
  250. ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
  251. )
  252. CSP_FONT_SRC = env.list(
  253. "CSP_FONT_SRC", str, ["'self'", "https://fonts.gstatic.com", "data:"]
  254. )
  255. # Redoc requires blob
  256. CSP_WORKER_SRC = env.list("CSP_WORKER_SRC", str, ["'self'", "blob:"])
  257. # Enable Chatwoot only when configured
  258. default_connect_src = ["'self'", "https://*.glitchtip.com"]
  259. if CHATWOOT_WEBSITE_TOKEN:
  260. default_connect_src.append("https://app.chatwoot.com")
  261. CSP_CONNECT_SRC = env.list("CSP_CONNECT_SRC", str, default_connect_src)
  262. # Enable stripe by default only when configured
  263. stripe_domain = "https://js.stripe.com"
  264. default_script_src = ["'self'", "https://*.glitchtip.com"]
  265. default_frame_src = ["'self'"]
  266. if BILLING_ENABLED:
  267. default_script_src.append(stripe_domain)
  268. default_frame_src.append(stripe_domain)
  269. CSP_SCRIPT_SRC = env.list("CSP_SCRIPT_SRC", str, default_script_src)
  270. CSP_IMG_SRC = env.list("CSP_IMG_SRC", str, ["'self'"])
  271. CSP_FRAME_SRC = env.list("CSP_FRAME_SRC", str, default_frame_src)
  272. # Consider tracking CSP reports with GlitchTip itself
  273. CSP_REPORT_URI = env.tuple("CSP_REPORT_URI", str, None)
  274. CSP_REPORT_ONLY = env.bool("CSP_REPORT_ONLY", False)
  275. SECURE_HSTS_SECONDS = env.int("SECURE_HSTS_SECONDS", 0)
  276. SECURE_HSTS_PRELOAD = env.bool("SECURE_HSTS_PRELOAD", False)
  277. SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool("SECURE_HSTS_INCLUDE_SUBDOMAINS", False)
  278. SESSION_COOKIE_SECURE = env.bool("SESSION_COOKIE_SECURE", False)
  279. SESSION_COOKIE_SAMESITE = env.str("SESSION_COOKIE_SAMESITE", "Lax")
  280. DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL", "webmaster@localhost")
  281. ANYMAIL = {
  282. "MAILGUN_API_KEY": env.str("MAILGUN_API_KEY", None),
  283. "MAILGUN_SENDER_DOMAIN": env.str("MAILGUN_SENDER_DOMAIN", None),
  284. "MAILGUN_API_URL": env.str("MAILGUN_API_URL", "https://api.mailgun.net/v3"),
  285. "SENDGRID_API_KEY": env.str("SENDGRID_API_KEY", None),
  286. }
  287. ACCOUNT_EMAIL_SUBJECT_PREFIX = ""
  288. # Database
  289. # https://docs.djangoproject.com/en/dev/ref/settings/#databases
  290. DATABASES = {
  291. "default": env.db(default="postgres://postgres:postgres@postgres:5432/postgres")
  292. }
  293. # Support setting DATABASES in parts in order to get values from the postgresql helm chart
  294. DATABASE_HOST = env.str("DATABASE_HOST", None)
  295. DATABASE_PASSWORD = env.str("DATABASE_PASSWORD", None)
  296. if DATABASE_HOST and DATABASE_PASSWORD:
  297. DATABASES["default"] = {
  298. "ENGINE": "django.db.backends.postgresql",
  299. "NAME": env.str("DATABASE_NAME", "postgres"),
  300. "USER": env.str("DATABASE_USER", "postgres"),
  301. "PASSWORD": DATABASE_PASSWORD,
  302. "HOST": DATABASE_HOST,
  303. "PORT": env.str("DATABASE_PORT", "5432"),
  304. }
  305. DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
  306. # We need to support both url and broken out host to support helm redis chart
  307. REDIS_HOST = env.str("REDIS_HOST", None)
  308. if REDIS_HOST:
  309. REDIS_PORT = env.str("REDIS_PORT", "6379")
  310. REDIS_DATABASE = env.str("REDIS_DATABASE", "0")
  311. REDIS_PASSWORD = env.str("REDIS_PASSWORD", None)
  312. if REDIS_PASSWORD:
  313. REDIS_URL = (
  314. f"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DATABASE}"
  315. )
  316. else:
  317. REDIS_URL = f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DATABASE}"
  318. else:
  319. REDIS_URL = env.str("REDIS_URL", "redis://redis:6379/0")
  320. CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", REDIS_URL)
  321. CELERY_BROKER_TRANSPORT_OPTIONS = {
  322. "fanout_prefix": True,
  323. "fanout_patterns": True,
  324. }
  325. if CELERY_BROKER_URL.startswith("sentinel"):
  326. CELERY_BROKER_TRANSPORT_OPTIONS["master_name"] = env.str(
  327. "CELERY_BROKER_MASTER_NAME", "mymaster"
  328. )
  329. if socket_timeout := env.int("CELERY_BROKER_SOCKET_TIMEOUT", None):
  330. CELERY_BROKER_TRANSPORT_OPTIONS["socket_timeout"] = socket_timeout
  331. if broker_sentinel_password := env.str("CELERY_BROKER_SENTINEL_KWARGS_PASSWORD", None):
  332. CELERY_BROKER_TRANSPORT_OPTIONS["sentinel_kwargs"] = {
  333. "password": broker_sentinel_password
  334. }
  335. CELERY_RESULT_BACKEND = "django-db"
  336. CELERY_CACHE_BACKEND = "django-cache"
  337. CELERY_BEAT_SCHEDULE = {
  338. "send-alert-notifications": {
  339. "task": "alerts.tasks.process_event_alerts",
  340. "schedule": 60,
  341. },
  342. "cleanup-old-events": {
  343. "task": "issues.tasks.cleanup_old_events",
  344. "schedule": crontab(hour=6, minute=1),
  345. },
  346. "cleanup-old-transaction-events": {
  347. "task": "performance.tasks.cleanup_old_transaction_events",
  348. "schedule": crontab(hour=6, minute=10),
  349. },
  350. "cleanup-old-monitor-checks": {
  351. "task": "glitchtip.uptime.tasks.cleanup_old_monitor_checks",
  352. "schedule": crontab(hour=6, minute=20),
  353. },
  354. "cleanup-old-files": {
  355. "task": "files.tasks.cleanup_old_files",
  356. "schedule": crontab(hour=6, minute=30),
  357. },
  358. "uptime-dispatch-checks": {
  359. "task": "glitchtip.uptime.tasks.dispatch_checks",
  360. "schedule": timedelta(seconds=30),
  361. },
  362. }
  363. if os.environ.get("CACHE_URL"):
  364. CACHES = {
  365. "default": env.cache(),
  366. }
  367. else: # Default to REDIS when unset
  368. CACHES = {
  369. "default": {
  370. "BACKEND": "django.core.cache.backends.redis.RedisCache",
  371. "LOCATION": REDIS_URL,
  372. }
  373. }
  374. if cache_sentinel_url := env.str("CACHE_SENTINEL_URL", None):
  375. try:
  376. cache_sentinel_host, cache_sentinel_port = cache_sentinel_url.split(":")
  377. SENTINELS = [(cache_sentinel_host, int(cache_sentinel_port))]
  378. except ValueError as err:
  379. raise ImproperlyConfigured(
  380. "Invalid cache redis sentinel url, format is host:port"
  381. ) from err
  382. DJANGO_REDIS_CONNECTION_FACTORY = "django_redis.pool.SentinelConnectionFactory"
  383. CACHES["default"]["OPTIONS"]["SENTINELS"] = SENTINELS
  384. if cache_sentinel_password := env.str("CACHE_SENTINEL_PASSWORD", None):
  385. CACHES["default"]["OPTIONS"]["SENTINEL_KWARGS"] = {
  386. "password": cache_sentinel_password
  387. }
  388. if os.environ.get("SESSION_ENGINE"):
  389. SESSION_ENGINE = env.str("SESSION_ENGINE")
  390. if os.environ.get("SESSION_CACHE_ALIAS"):
  391. SESSION_CACHE_ALIAS = env.str("SESSION_CACHE_ALIAS")
  392. if os.environ.get("SESSION_COOKIE_AGE"):
  393. SESSION_COOKIE_AGE = env.int("SESSION_COOKIE_AGE")
  394. # Password validation
  395. # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
  396. AUTH_PASSWORD_VALIDATORS = [
  397. {
  398. "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
  399. },
  400. {
  401. "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
  402. },
  403. {
  404. "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
  405. },
  406. {
  407. "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
  408. },
  409. ]
  410. # Internationalization
  411. # https://docs.djangoproject.com/en/dev/topics/i18n/
  412. LANGUAGE_CODE = "en-us"
  413. TIME_ZONE = "UTC"
  414. USE_I18N = True
  415. USE_L10N = True
  416. USE_TZ = True
  417. SITE_ID = 1
  418. if env("DEFAULT_FILE_STORAGE"):
  419. DEFAULT_FILE_STORAGE = env("DEFAULT_FILE_STORAGE")
  420. AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID")
  421. AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY")
  422. AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
  423. AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL")
  424. AWS_LOCATION = env("AWS_LOCATION")
  425. AZURE_ACCOUNT_NAME = env("AZURE_ACCOUNT_NAME")
  426. AZURE_ACCOUNT_KEY = env("AZURE_ACCOUNT_KEY")
  427. AZURE_CONTAINER = env("AZURE_CONTAINER")
  428. AZURE_URL_EXPIRATION_SECS = env("AZURE_URL_EXPIRATION_SECS")
  429. GS_BUCKET_NAME = env("GS_BUCKET_NAME")
  430. GS_PROJECT_ID = env("GS_PROJECT_ID")
  431. if AWS_S3_ENDPOINT_URL:
  432. MEDIA_URL = env.str(
  433. "MEDIA_URL", "https://%s/%s/" % (AWS_S3_ENDPOINT_URL, AWS_LOCATION)
  434. )
  435. DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
  436. else:
  437. MEDIA_URL = "media/"
  438. MEDIA_ROOT = env.str("MEDIA_ROOT", "")
  439. STATICFILES_DIRS = [
  440. "assets",
  441. "dist",
  442. ]
  443. STATIC_ROOT = path("static/")
  444. STATICFILES_STORAGE = env("STATICFILES_STORAGE")
  445. EMAIL_BACKEND = env.str(
  446. "EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend"
  447. )
  448. if os.getenv("EMAIL_HOST_USER"):
  449. EMAIL_HOST_USER = env.str("EMAIL_HOST_USER")
  450. if os.getenv("EMAIL_HOST_PASSWORD"):
  451. EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD")
  452. if os.getenv("EMAIL_HOST"):
  453. EMAIL_HOST = env.str("EMAIL_HOST")
  454. if os.getenv("EMAIL_PORT"):
  455. EMAIL_PORT = env.str("EMAIL_PORT")
  456. if os.getenv("EMAIL_USE_TLS"):
  457. EMAIL_USE_TLS = env.str("EMAIL_USE_TLS")
  458. if os.getenv("EMAIL_USE_SSL"):
  459. EMAIL_USE_SSL = env.str("EMAIL_USE_SSL")
  460. if os.getenv("EMAIL_TIMEOUT"):
  461. EMAIL_TIMEOUT = env.str("EMAIL_TIMEOUT")
  462. if os.getenv("EMAIL_FILE_PATH"):
  463. EMAIL_FILE_PATH = env.str("EMAIL_FILE_PATH")
  464. if os.getenv(
  465. "EMAIL_URL"
  466. ): # Careful, this will override most EMAIL_*** settings. Set them all individually, or use EMAIL_URL to set them all at once, but don't do both.
  467. EMAIL_CONFIG = env.email_url("EMAIL_URL")
  468. vars().update(EMAIL_CONFIG)
  469. AUTH_USER_MODEL = "users.User"
  470. ACCOUNT_AUTHENTICATION_METHOD = "email"
  471. ACCOUNT_EMAIL_REQUIRED = True
  472. ACCOUNT_USERNAME_REQUIRED = False
  473. ACCOUNT_USER_MODEL_USERNAME_FIELD = None
  474. ACCOUNT_ADAPTER = "glitchtip.social.MFAAccountAdapter"
  475. INVITATION_BACKEND = "organizations_ext.invitation_backend.InvitationBackend"
  476. SOCIALACCOUNT_PROVIDERS = {}
  477. GITLAB_URL = env.url("SOCIALACCOUNT_PROVIDERS_gitlab_GITLAB_URL", None)
  478. if GITLAB_URL:
  479. SOCIALACCOUNT_PROVIDERS["gitlab"] = {"GITLAB_URL": GITLAB_URL.geturl()}
  480. GITEA_URL = env.url("SOCIALACCOUNT_PROVIDERS_gitea_GITEA_URL", None)
  481. if GITEA_URL:
  482. SOCIALACCOUNT_PROVIDERS["gitea"] = {"GITEA_URL": GITEA_URL.geturl()}
  483. NEXTCLOUD_URL = env.url("SOCIALACCOUNT_PROVIDERS_nextcloud_SERVER", None)
  484. if NEXTCLOUD_URL:
  485. SOCIALACCOUNT_PROVIDERS["nextcloud"] = {"SERVER": NEXTCLOUD_URL.geturl()}
  486. KEYCLOAK_URL = env.url("SOCIALACCOUNT_PROVIDERS_keycloak_KEYCLOAK_URL", None)
  487. if KEYCLOAK_URL:
  488. alt_url_env = env.url("SOCIALACCOUNT_PROVIDERS_keycloak_KEYCLOAK_URL_ALT", None)
  489. if alt_url_env:
  490. alt_url = alt_url_env.geturl()
  491. else:
  492. alt_url = None
  493. SOCIALACCOUNT_PROVIDERS["keycloak"] = {
  494. "KEYCLOAK_URL": KEYCLOAK_URL.geturl(),
  495. "KEYCLOAK_REALM": env.str(
  496. "SOCIALACCOUNT_PROVIDERS_keycloak_KEYCLOAK_REALM", None
  497. ),
  498. "KEYCLOAK_URL_ALT": alt_url,
  499. }
  500. OLD_PASSWORD_FIELD_ENABLED = True
  501. LOGOUT_ON_PASSWORD_CHANGE = False
  502. REST_AUTH_SERIALIZERS = {
  503. "USER_DETAILS_SERIALIZER": "users.serializers.UserSerializer",
  504. "TOKEN_SERIALIZER": "users.serializers.NoopTokenSerializer",
  505. "PASSWORD_RESET_SERIALIZER": "users.serializers.PasswordSetResetSerializer",
  506. }
  507. REST_AUTH_REGISTER_SERIALIZERS = {
  508. "REGISTER_SERIALIZER": "users.serializers.RegisterSerializer",
  509. }
  510. REST_AUTH_TOKEN_MODEL = None
  511. REST_AUTH_TOKEN_CREATOR = "users.utils.noop_token_creator"
  512. # By default (False) only the first user, superuser, or organization owners may register
  513. # and create an organization. Other users must be invited. Intended for private instances
  514. ENABLE_OPEN_USER_REGISTRATION = env.bool("ENABLE_OPEN_USER_REGISTRATION", False)
  515. AUTHENTICATION_BACKENDS = (
  516. # Needed to login by username in Django admin, regardless of `allauth`
  517. "django.contrib.auth.backends.ModelBackend",
  518. # `allauth` specific authentication methods, such as login by e-mail
  519. "allauth.account.auth_backends.AuthenticationBackend",
  520. )
  521. DEFAULT_RENDERER_CLASSES = ("rest_framework.renderers.JSONRenderer",)
  522. if DEBUG:
  523. DEFAULT_RENDERER_CLASSES = DEFAULT_RENDERER_CLASSES + (
  524. "rest_framework.renderers.BrowsableAPIRenderer",
  525. )
  526. REST_FRAMEWORK = {
  527. "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
  528. "DEFAULT_PAGINATION_CLASS": "glitchtip.pagination.LinkHeaderPagination",
  529. "PAGE_SIZE": 50,
  530. "ORDERING_PARAM": "sort",
  531. "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",),
  532. "DEFAULT_RENDERER_CLASSES": DEFAULT_RENDERER_CLASSES,
  533. "DEFAULT_AUTHENTICATION_CLASSES": [
  534. "rest_framework.authentication.SessionAuthentication",
  535. "glitchtip.authentication.BearerTokenAuthentication",
  536. ],
  537. "DEFAULT_THROTTLE_RATES": {"anon": "100/minute"},
  538. }
  539. DRF_YASG_EXCLUDE_VIEWS = [
  540. "users.views.SocialAccountDisconnectView",
  541. ]
  542. SWAGGER_SETTINGS = {
  543. "DEFAULT_AUTO_SCHEMA_CLASS": "glitchtip.yasg.SquadSwaggerAutoSchema",
  544. }
  545. LOGGING_HANDLER_CLASS = env.str("DJANGO_LOGGING_HANDLER_CLASS", "logging.StreamHandler")
  546. LOGGING = {
  547. "version": 1,
  548. "disable_existing_loggers": False,
  549. "handlers": {
  550. "null": {
  551. "class": "logging.NullHandler",
  552. },
  553. "console": {
  554. "class": LOGGING_HANDLER_CLASS,
  555. },
  556. },
  557. "loggers": {
  558. "django.security.DisallowedHost": {
  559. "handlers": ["null"],
  560. "propagate": False,
  561. },
  562. },
  563. "root": {"handlers": ["console"]},
  564. }
  565. if LOGGING_HANDLER_CLASS is not logging.StreamHandler:
  566. from celery.signals import after_setup_logger, after_setup_task_logger
  567. @after_setup_logger.connect
  568. @after_setup_task_logger.connect
  569. def setup_celery_logging(logger, **kwargs):
  570. from django.utils.module_loading import import_string
  571. handler = import_string(LOGGING_HANDLER_CLASS)
  572. for h in logger.handlers:
  573. logger.removeHandler(h)
  574. logger.addHandler(handler())
  575. def organization_request_callback(request):
  576. """Gets an organization instance from the id passed through ``request``"""
  577. user = request.user
  578. if user:
  579. return user.organizations_ext_organization.filter(
  580. owner__organization_user__user=user
  581. ).first()
  582. # Set to track activity with Plausible
  583. PLAUSIBLE_URL = env.str("PLAUSIBLE_URL", default=None)
  584. PLAUSIBLE_DOMAIN = env.str("PLAUSIBLE_DOMAIN", default=None)
  585. # Is running unit test
  586. TESTING = len(sys.argv) > 1 and sys.argv[1] == "test"
  587. # See https://liberapay.com/GlitchTip/donate - suggested self-host donation is $5/month/user.
  588. # Support plans available. Email info@burkesoftware.com for more info.
  589. I_PAID_FOR_GLITCHTIP = env.bool("I_PAID_FOR_GLITCHTIP", False)
  590. # Max events per month for free tier
  591. BILLING_FREE_TIER_EVENTS = env.int("BILLING_FREE_TIER_EVENTS", 1000)
  592. DJSTRIPE_SUBSCRIBER_MODEL = "organizations_ext.Organization"
  593. DJSTRIPE_SUBSCRIBER_MODEL_REQUEST_CALLBACK = organization_request_callback
  594. DJSTRIPE_USE_NATIVE_JSONFIELD = True
  595. DJSTRIPE_FOREIGN_KEY_TO_FIELD = "djstripe_id"
  596. STRIPE_AUTOMATIC_TAX = env.bool("STRIPE_AUTOMATIC_TAX", False)
  597. STRIPE_LIVE_MODE = env.bool("STRIPE_LIVE_MODE", False)
  598. if BILLING_ENABLED:
  599. I_PAID_FOR_GLITCHTIP = True
  600. INSTALLED_APPS.append("djstripe")
  601. INSTALLED_APPS.append("djstripe_ext")
  602. STRIPE_TEST_PUBLIC_KEY = env.str("STRIPE_TEST_PUBLIC_KEY", None)
  603. STRIPE_TEST_SECRET_KEY = env.str("STRIPE_TEST_SECRET_KEY", None)
  604. STRIPE_LIVE_PUBLIC_KEY = env.str("STRIPE_LIVE_PUBLIC_KEY", None)
  605. STRIPE_LIVE_SECRET_KEY = env.str("STRIPE_LIVE_SECRET_KEY", None)
  606. DJSTRIPE_WEBHOOK_SECRET = env.str("DJSTRIPE_WEBHOOK_SECRET", None)
  607. CELERY_BEAT_SCHEDULE["set-organization-throttle"] = {
  608. "task": "organizations_ext.tasks.set_organization_throttle",
  609. "schedule": crontab(hour=7, minute=1),
  610. }
  611. CELERY_BEAT_SCHEDULE["warn-organization-throttle"] = {
  612. "task": "djstripe_ext.tasks.warn_organization_throttle",
  613. "schedule": crontab(minute=30),
  614. }
  615. elif TESTING:
  616. # Must run tests with djstripe enabled
  617. BILLING_ENABLED = True
  618. INSTALLED_APPS.append("djstripe")
  619. INSTALLED_APPS.append("djstripe_ext")
  620. STRIPE_TEST_PUBLIC_KEY = "fake"
  621. STRIPE_TEST_SECRET_KEY = "sk_test_fake" # nosec
  622. DJSTRIPE_WEBHOOK_SECRET = "whsec_fake" # nosec
  623. logging.disable(logging.WARNING)
  624. CELERY_TASK_ALWAYS_EAGER = env.bool("CELERY_TASK_ALWAYS_EAGER", False)
  625. if TESTING:
  626. CELERY_TASK_ALWAYS_EAGER = True
  627. STATICFILES_STORAGE = None
  628. # https://github.com/evansd/whitenoise/issues/215
  629. warnings.filterwarnings(
  630. "ignore", message="No directory at", module="whitenoise.base"
  631. )
  632. if CELERY_TASK_ALWAYS_EAGER:
  633. CACHES = {
  634. "default": {
  635. "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
  636. }
  637. }
  638. MFA_SERVER_NAME = "GlitchTip"
  639. FIDO_SERVER_ID = GLITCHTIP_URL.hostname
  640. # Workaround for error encountered at build time (source: https://github.com/axnsan12/drf-yasg/issues/761#issuecomment-1014530805)
  641. class NoSourceMapsStorage(CompressedManifestStaticFilesStorage):
  642. patterns = (
  643. (
  644. "*.css",
  645. (
  646. "(?P<matched>url\\(['\"]{0,1}\\s*(?P<url>.*?)[\"']{0,1}\\))",
  647. (
  648. "(?P<matched>@import\\s*[\"']\\s*(?P<url>.*?)[\"'])",
  649. '@import url("%(url)s")',
  650. ),
  651. ),
  652. ),
  653. )