• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / fx-private-relay / a4799c68-1721-4f84-afa6-93ed17da16c0

03 Jul 2024 09:28PM CUT coverage: 85.416%. Remained the same
a4799c68-1721-4f84-afa6-93ed17da16c0

push

circleci

groovecoder
MPP-3838: restore safer CSP

Use a new EagerNonceCSPMiddleware to add nonce to the CSP and update the
React app to include it in dynamic scripts.

4081 of 5229 branches covered (78.05%)

Branch coverage included in aggregate %.

17 of 19 new or added lines in 3 files covered. (89.47%)

1 existing line in 1 file now uncovered.

15915 of 18181 relevant lines covered (87.54%)

10.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.75
/privaterelay/settings.py
1
"""
2
Django settings for privaterelay project.
3

4
Generated by 'django-admin startproject' using Django 2.2.2.
5

6
For more information on this file, see
7
https://docs.djangoproject.com/en/2.2/topics/settings/
8

9
For the full list of settings and their values, see
10
https://docs.djangoproject.com/en/2.2/ref/settings/
11
"""
12

13
from __future__ import annotations
1✔
14

15
import base64
1✔
16
import ipaddress
1✔
17
import os
1✔
18
import sys
1✔
19
from hashlib import sha256
1✔
20
from pathlib import Path
1✔
21
from typing import TYPE_CHECKING, Any, cast, get_args
1✔
22

23
from django.conf.global_settings import LANGUAGES as DEFAULT_LANGUAGES
1✔
24

25
import dj_database_url
1✔
26
import django_stubs_ext
1✔
27
import markus
1✔
28
import sentry_sdk
1✔
29
from csp.constants import NONCE, NONE, SELF, UNSAFE_INLINE
1✔
30
from decouple import Choices, Csv, config
1✔
31
from sentry_sdk.integrations.django import DjangoIntegration
1✔
32
from sentry_sdk.integrations.logging import ignore_logger
1✔
33

34
from .types import CONTENT_SECURITY_POLICY_T, RELAY_CHANNEL_NAME
1✔
35

36
if TYPE_CHECKING:
37
    import wsgiref.headers
38

39
try:
1✔
40
    # Silk is a live profiling and inspection tool for the Django framework
41
    # https://github.com/jazzband/django-silk
42
    import silk  # noqa: F401
1✔
43

44
    HAS_SILK = True
×
45
except ImportError:
1✔
46
    HAS_SILK = False
1✔
47

48
try:
1✔
49
    import google.cloud.sqlcommenter  # noqa: F401
1✔
50

51
    HAS_SQLCOMMENTER = True
1✔
52
except ImportError:
×
53
    HAS_SQLCOMMENTER = False
×
54

55
try:
1✔
56
    from privaterelay.glean.server_events import GLEAN_EVENT_MOZLOG_TYPE
1✔
57
except ImportError:
×
58
    # File may not be generated yet. Will be checked at initialization
59
    GLEAN_EVENT_MOZLOG_TYPE = "glean-server-event"
×
60

61
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
62
BASE_DIR: str = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1✔
63
TMP_DIR = os.path.join(BASE_DIR, "tmp")
1✔
64
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
1✔
65

66
# Quick-start development settings - unsuitable for production
67
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
68

69
# defaulting to blank to be production-broken by default
70
SECRET_KEY = config("SECRET_KEY", None)
1✔
71
SECRET_KEY_FALLBACKS = config("SECRET_KEY_FALLBACKS", "", cast=Csv())
1✔
72
SITE_ORIGIN: str | None = config("SITE_ORIGIN", None)
1✔
73

74
ORIGIN_CHANNEL_MAP: dict[str, RELAY_CHANNEL_NAME] = {
1✔
75
    "http://127.0.0.1:8000": "local",
76
    "https://dev.fxprivaterelay.nonprod.cloudops.mozgcp.net": "dev",
77
    "https://stage.fxprivaterelay.nonprod.cloudops.mozgcp.net": "stage",
78
    "https://relay.firefox.com": "prod",
79
}
80
RELAY_CHANNEL: RELAY_CHANNEL_NAME = cast(
1✔
81
    RELAY_CHANNEL_NAME,
82
    config(
83
        "RELAY_CHANNEL",
84
        default=ORIGIN_CHANNEL_MAP.get(SITE_ORIGIN or "", "local"),
85
        cast=Choices(get_args(RELAY_CHANNEL_NAME), cast=str),
86
    ),
87
)
88

89
DEBUG = config("DEBUG", False, cast=bool)
1✔
90
if DEBUG:
1!
91
    INTERNAL_IPS = config("DJANGO_INTERNAL_IPS", default="", cast=Csv())
1✔
92
IN_PYTEST: bool = "pytest" in sys.modules
1✔
93
USE_SILK = DEBUG and HAS_SILK and not IN_PYTEST
1✔
94

95
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
96
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
1✔
97
SECURE_SSL_HOST = config("DJANGO_SECURE_SSL_HOST", None)
1✔
98
SECURE_SSL_REDIRECT = config("DJANGO_SECURE_SSL_REDIRECT", False, cast=bool)
1✔
99
SECURE_REDIRECT_EXEMPT = [
1✔
100
    r"^__version__",
101
    r"^__heartbeat__",
102
    r"^__lbheartbeat__",
103
]
104
SECURE_HSTS_INCLUDE_SUBDOMAINS = config(
1✔
105
    "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", False, cast=bool
106
)
107
SECURE_HSTS_PRELOAD = config("DJANGO_SECURE_HSTS_PRELOAD", False, cast=bool)
1✔
108
SECURE_HSTS_SECONDS = config("DJANGO_SECURE_HSTS_SECONDS", None)
1✔
109
SECURE_BROWSER_XSS_FILTER = config("DJANGO_SECURE_BROWSER_XSS_FILTER", True)
1✔
110
SESSION_COOKIE_SECURE = config("DJANGO_SESSION_COOKIE_SECURE", False, cast=bool)
1✔
111
CSRF_COOKIE_SECURE = config("DJANGO_CSRF_COOKIE_SECURE", False, cast=bool)
1✔
112

113
#
114
# Setup CSP
115
#
116

117
BASKET_ORIGIN = config("BASKET_ORIGIN", "https://basket.mozilla.org")
1✔
118

119
# maps FxA / Mozilla account profile hosts to respective hosts for CSP
120
FXA_BASE_ORIGIN: str = config("FXA_BASE_ORIGIN", "https://accounts.firefox.com")
1✔
121
if FXA_BASE_ORIGIN == "https://accounts.firefox.com":
1!
122
    _AVATAR_IMG_SRC = [
×
123
        "firefoxusercontent.com",
124
        "https://profile.accounts.firefox.com",
125
    ]
126
    _ACCOUNT_CONNECT_SRC = [FXA_BASE_ORIGIN]
×
127
else:
128
    if not FXA_BASE_ORIGIN == "https://accounts.stage.mozaws.net":
1!
129
        raise ValueError(
×
130
            "FXA_BASE_ORIGIN must be either https://accounts.firefox.com or https://accounts.stage.mozaws.net"
131
        )
132
    _AVATAR_IMG_SRC = [
1✔
133
        "mozillausercontent.com",
134
        "https://profile.stage.mozaws.net",
135
    ]
136
    _ACCOUNT_CONNECT_SRC = [
1✔
137
        FXA_BASE_ORIGIN,
138
        # fxaFlowTracker.ts will try this if runtimeData is slow
139
        "https://accounts.firefox.com",
140
    ]
141

142
API_DOCS_ENABLED = config("API_DOCS_ENABLED", False, cast=bool) or DEBUG
1✔
143
_CSP_SCRIPT_INLINE = API_DOCS_ENABLED or USE_SILK
1✔
144
_CSP_SCRIPT_INLINE = False
1✔
145

146
# When running locally, styles might get refreshed while the server is running, so their
147
# hashes would get oudated. Hence, we just allow all of them.
148
_CSP_STYLE_INLINE = API_DOCS_ENABLED or RELAY_CHANNEL == "local"
1✔
149

150
if API_DOCS_ENABLED:
1!
151
    _API_DOCS_CSP_IMG_SRC = ["data:", "https://cdn.redoc.ly"]
1✔
152
    _API_DOCS_CSP_STYLE_SRC = ["https://fonts.googleapis.com"]
1✔
153
    _API_DOCS_CSP_FONT_SRC = ["https://fonts.gstatic.com"]
1✔
154
    _API_DOCS_CSP_WORKER_SRC = ["blob:"]
1✔
155
else:
156
    _API_DOCS_CSP_IMG_SRC = []
×
157
    _API_DOCS_CSP_STYLE_SRC = []
×
158
    _API_DOCS_CSP_FONT_SRC = []
×
159
    _API_DOCS_CSP_WORKER_SRC = []
×
160

161
# Next.js dynamically inserts the relevant styles when switching pages,
162
# by injecting them as inline styles. We need to explicitly allow those styles
163
# in our Content Security Policy.
164
_CSP_STYLE_HASHES: list[str] = []
1✔
165
if _CSP_STYLE_INLINE:
1!
166
    # 'unsafe-inline' is not compatible with hash sources
167
    _CSP_STYLE_HASHES = []
1✔
168
else:
169
    # When running in production, we want to disallow inline styles that are
170
    # not set by us, so we use an explicit allowlist with the hashes of the
171
    # styles generated by Next.js.
172
    _next_css_path = Path(STATIC_ROOT) / "_next" / "static" / "css"
×
173
    for path in _next_css_path.glob("*.css"):
×
174
        # Use sha256 hashes, to keep in sync with Chrome.
175
        # When CSP rules fail in Chrome, it provides the sha256 hash that would
176
        # have matched, useful for debugging.
177
        content = open(path, "rb").read()
×
178
        the_hash = base64.b64encode(sha256(content).digest()).decode()
×
179
        _CSP_STYLE_HASHES.append(f"'sha256-{the_hash}'")
×
180
    _CSP_STYLE_HASHES.sort()
×
181

182
    # Add the hash for an empty string (sha256-47DEQp...)
183
    # next,js injects an empty style element and then adds the content.
184
    # This hash avoids a spurious CSP error.
185
    empty_hash = base64.b64encode(sha256().digest()).decode()
×
186
    _CSP_STYLE_HASHES.append(f"'sha256-{empty_hash}'")
×
187

188
CONTENT_SECURITY_POLICY: CONTENT_SECURITY_POLICY_T = {
1✔
189
    "DIRECTIVES": {
190
        "default-src": [SELF],
191
        "connect-src": [
192
            SELF,
193
            "https://*.google-analytics.com",
194
            "https://*.analytics.google.com",
195
            "https://*.googletagmanager.com",
196
            "https://location.services.mozilla.com",
197
            "https://api.stripe.com",
198
            BASKET_ORIGIN,
199
        ],
200
        "font-src": [SELF, "https://relay.firefox.com/"],
201
        "frame-src": ["https://js.stripe.com", "https://hooks.stripe.com"],
202
        "img-src": [
203
            SELF,
204
            "https://*.google-analytics.com",
205
            "https://*.googletagmanager.com",
206
        ],
207
        "object-src": [NONE],
208
        "script-src": [
209
            SELF,
210
            NONCE,
211
            "https://www.google-analytics.com/",
212
            "https://*.googletagmanager.com",
213
            "https://js.stripe.com/",
214
        ],
215
        "style-src": [SELF],
216
        "worker-src": [SELF, "blob:"],  # TODO: remove blob: temporary fix for GA4
217
    }
218
}
219
CONTENT_SECURITY_POLICY["DIRECTIVES"]["connect-src"].extend(_ACCOUNT_CONNECT_SRC)
1✔
220
CONTENT_SECURITY_POLICY["DIRECTIVES"]["font-src"].extend(_API_DOCS_CSP_FONT_SRC)
1✔
221
CONTENT_SECURITY_POLICY["DIRECTIVES"]["img-src"].extend(_AVATAR_IMG_SRC)
1✔
222
CONTENT_SECURITY_POLICY["DIRECTIVES"]["img-src"].extend(_API_DOCS_CSP_IMG_SRC)
1✔
223
CONTENT_SECURITY_POLICY["DIRECTIVES"]["style-src"].extend(_API_DOCS_CSP_STYLE_SRC)
1✔
224
CONTENT_SECURITY_POLICY["DIRECTIVES"]["style-src"].extend(_CSP_STYLE_HASHES)
1✔
225
if _CSP_SCRIPT_INLINE:
1!
UNCOV
226
    CONTENT_SECURITY_POLICY["DIRECTIVES"]["script-src"].append(UNSAFE_INLINE)
×
227
if _CSP_STYLE_INLINE:
1!
228
    CONTENT_SECURITY_POLICY["DIRECTIVES"]["style-src"].append(UNSAFE_INLINE)
1✔
229
if _API_DOCS_CSP_WORKER_SRC:
1!
230
    CONTENT_SECURITY_POLICY["DIRECTIVES"]["worker-src"].extend(_API_DOCS_CSP_WORKER_SRC)
1✔
231
if _CSP_REPORT_URI := config("CSP_REPORT_URI", ""):
1!
232
    CONTENT_SECURITY_POLICY["DIRECTIVES"]["report-uri"] = _CSP_REPORT_URI
×
233

234
REFERRER_POLICY = "strict-origin-when-cross-origin"
1✔
235

236
ALLOWED_HOSTS: list[str] = []
1✔
237
DJANGO_ALLOWED_HOSTS = config("DJANGO_ALLOWED_HOST", "", cast=Csv())
1✔
238
if DJANGO_ALLOWED_HOSTS:
1!
239
    ALLOWED_HOSTS += DJANGO_ALLOWED_HOSTS
×
240
DJANGO_ALLOWED_SUBNET = config("DJANGO_ALLOWED_SUBNET", None)
1✔
241
if DJANGO_ALLOWED_SUBNET:
1!
242
    ALLOWED_HOSTS += [str(ip) for ip in ipaddress.IPv4Network(DJANGO_ALLOWED_SUBNET)]
×
243

244

245
# Get our backing resource configs to check if we should install the app
246
ADMIN_ENABLED = config("ADMIN_ENABLED", False, cast=bool)
1✔
247

248

249
AWS_REGION: str | None = config("AWS_REGION", None)
1✔
250
AWS_ACCESS_KEY_ID = config("AWS_ACCESS_KEY_ID", None)
1✔
251
AWS_SECRET_ACCESS_KEY = config("AWS_SECRET_ACCESS_KEY", None)
1✔
252
AWS_SNS_TOPIC = set(config("AWS_SNS_TOPIC", "", cast=Csv()))
1✔
253
AWS_SNS_KEY_CACHE = config("AWS_SNS_KEY_CACHE", "default")
1✔
254
AWS_SES_CONFIGSET: str | None = config("AWS_SES_CONFIGSET", None)
1✔
255
AWS_SQS_EMAIL_QUEUE_URL = config("AWS_SQS_EMAIL_QUEUE_URL", None)
1✔
256
AWS_SQS_EMAIL_DLQ_URL = config("AWS_SQS_EMAIL_DLQ_URL", None)
1✔
257

258
# Dead-Letter Queue (DLQ) for SNS push subscription
259
AWS_SQS_QUEUE_URL = config("AWS_SQS_QUEUE_URL", None)
1✔
260

261
RELAY_FROM_ADDRESS: str | None = config("RELAY_FROM_ADDRESS", None)
1✔
262
GOOGLE_ANALYTICS_ID = config("GOOGLE_ANALYTICS_ID", None)
1✔
263
GA4_MEASUREMENT_ID = config("GA4_MEASUREMENT_ID", None)
1✔
264
GOOGLE_APPLICATION_CREDENTIALS: str = config("GOOGLE_APPLICATION_CREDENTIALS", "")
1✔
265
GOOGLE_CLOUD_PROFILER_CREDENTIALS_B64: str = config(
1✔
266
    "GOOGLE_CLOUD_PROFILER_CREDENTIALS_B64", ""
267
)
268
INCLUDE_VPN_BANNER = config("INCLUDE_VPN_BANNER", False, cast=bool)
1✔
269
RECRUITMENT_BANNER_LINK = config("RECRUITMENT_BANNER_LINK", None)
1✔
270
RECRUITMENT_BANNER_TEXT = config("RECRUITMENT_BANNER_TEXT", None)
1✔
271
RECRUITMENT_EMAIL_BANNER_TEXT = config("RECRUITMENT_EMAIL_BANNER_TEXT", None)
1✔
272
RECRUITMENT_EMAIL_BANNER_LINK = config("RECRUITMENT_EMAIL_BANNER_LINK", None)
1✔
273

274
PHONES_ENABLED: bool = config("PHONES_ENABLED", False, cast=bool)
1✔
275
PHONES_NO_CLIENT_CALLS_IN_TEST = False  # Override in tests that do not test clients
1✔
276
TWILIO_ACCOUNT_SID: str | None = config("TWILIO_ACCOUNT_SID", None)
1✔
277
TWILIO_AUTH_TOKEN: str | None = config("TWILIO_AUTH_TOKEN", None)
1✔
278
TWILIO_MAIN_NUMBER: str | None = config("TWILIO_MAIN_NUMBER", None)
1✔
279
TWILIO_SMS_APPLICATION_SID: str | None = config("TWILIO_SMS_APPLICATION_SID", None)
1✔
280
TWILIO_MESSAGING_SERVICE_SID: list[str] = config(
1✔
281
    "TWILIO_MESSAGING_SERVICE_SID", "", cast=Csv()
282
)
283
TWILIO_TEST_ACCOUNT_SID: str | None = config("TWILIO_TEST_ACCOUNT_SID", None)
1✔
284
TWILIO_TEST_AUTH_TOKEN: str | None = config("TWILIO_TEST_AUTH_TOKEN", None)
1✔
285
TWILIO_ALLOWED_COUNTRY_CODES = {
1✔
286
    code.upper() for code in config("TWILIO_ALLOWED_COUNTRY_CODES", "US,CA", cast=Csv())
287
}
288
MAX_MINUTES_TO_VERIFY_REAL_PHONE: int = config(
1✔
289
    "MAX_MINUTES_TO_VERIFY_REAL_PHONE", 5, cast=int
290
)
291
MAX_TEXTS_PER_BILLING_CYCLE: int = config("MAX_TEXTS_PER_BILLING_CYCLE", 75, cast=int)
1✔
292
MAX_MINUTES_PER_BILLING_CYCLE: int = config(
1✔
293
    "MAX_MINUTES_PER_BILLING_CYCLE", 50, cast=int
294
)
295
DAYS_PER_BILLING_CYCLE = config("DAYS_PER_BILLING_CYCLE", 30, cast=int)
1✔
296
MAX_DAYS_IN_MONTH = 31
1✔
297
IQ_ENABLED = config("IQ_ENABLED", False, cast=bool)
1✔
298
IQ_FOR_VERIFICATION: bool = config("IQ_FOR_VERIFICATION", False, cast=bool)
1✔
299
IQ_FOR_NEW_NUMBERS = config("IQ_FOR_NEW_NUMBERS", False, cast=bool)
1✔
300
IQ_MAIN_NUMBER: str = config("IQ_MAIN_NUMBER", "")
1✔
301
IQ_OUTBOUND_API_KEY: str = config("IQ_OUTBOUND_API_KEY", "")
1✔
302
IQ_INBOUND_API_KEY = config("IQ_INBOUND_API_KEY", "")
1✔
303
IQ_MESSAGE_API_ORIGIN = config(
1✔
304
    "IQ_MESSAGE_API_ORIGIN", "https://messagebroker.inteliquent.com"
305
)
306
IQ_MESSAGE_PATH = "/msgbroker/rest/publishMessages"
1✔
307
IQ_PUBLISH_MESSAGE_URL: str = f"{IQ_MESSAGE_API_ORIGIN}{IQ_MESSAGE_PATH}"
1✔
308

309
DJANGO_STATSD_ENABLED = config("DJANGO_STATSD_ENABLED", False, cast=bool)
1✔
310
STATSD_DEBUG = config("STATSD_DEBUG", False, cast=bool)
1✔
311
STATSD_ENABLED: bool = DJANGO_STATSD_ENABLED or STATSD_DEBUG
1✔
312
STATSD_HOST = config("DJANGO_STATSD_HOST", "127.0.0.1")
1✔
313
STATSD_PORT = config("DJANGO_STATSD_PORT", "8125")
1✔
314
STATSD_PREFIX = config("DJANGO_STATSD_PREFIX", "fx.private.relay")
1✔
315

316
SERVE_ADDON = config("SERVE_ADDON", None)
1✔
317

318
# Application definition
319
INSTALLED_APPS = [
1✔
320
    "whitenoise.runserver_nostatic",
321
    "django.contrib.staticfiles",
322
    "django.contrib.auth",
323
    "django.contrib.contenttypes",
324
    "django.contrib.sessions",
325
    "django.contrib.messages",
326
    "django.contrib.sites",
327
    "django_filters",
328
    "django_ftl.apps.DjangoFtlConfig",
329
    "dockerflow.django",
330
    "allauth",
331
    "allauth.account",
332
    "allauth.socialaccount",
333
    "allauth.socialaccount.providers.fxa",
334
    "rest_framework",
335
    "rest_framework.authtoken",
336
    "corsheaders",
337
    "csp",
338
    "waffle",
339
    "privaterelay.apps.PrivateRelayConfig",
340
    "api.apps.ApiConfig",
341
]
342

343
if API_DOCS_ENABLED:
1!
344
    INSTALLED_APPS += [
1✔
345
        "drf_spectacular",
346
        "drf_spectacular_sidecar",
347
    ]
348

349
if DEBUG:
1!
350
    INSTALLED_APPS += [
1✔
351
        "debug_toolbar",
352
    ]
353

354
if USE_SILK:
1!
355
    INSTALLED_APPS.append("silk")
×
356

357
if ADMIN_ENABLED:
1!
358
    INSTALLED_APPS += [
×
359
        "django.contrib.admin",
360
    ]
361

362
if AWS_SES_CONFIGSET and AWS_SNS_TOPIC:
1!
363
    INSTALLED_APPS += [
1✔
364
        "emails.apps.EmailsConfig",
365
    ]
366

367
if PHONES_ENABLED:
1!
368
    INSTALLED_APPS += [
1✔
369
        "phones.apps.PhonesConfig",
370
    ]
371

372

373
MIDDLEWARE = ["privaterelay.middleware.ResponseMetrics"]
1✔
374

375
if USE_SILK:
1!
376
    MIDDLEWARE.append("silk.middleware.SilkyMiddleware")
×
377
if DEBUG:
1!
378
    MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
1✔
379

380
MIDDLEWARE += [
1✔
381
    "django.middleware.security.SecurityMiddleware",
382
    "privaterelay.middleware.EagerNonceCSPMiddleware",
383
    "privaterelay.middleware.RedirectRootIfLoggedIn",
384
    "privaterelay.middleware.RelayStaticFilesMiddleware",
385
    "django.contrib.sessions.middleware.SessionMiddleware",
386
    "corsheaders.middleware.CorsMiddleware",
387
    "django.middleware.common.CommonMiddleware",
388
    "django.middleware.csrf.CsrfViewMiddleware",
389
    "django.contrib.auth.middleware.AuthenticationMiddleware",
390
    "django.contrib.messages.middleware.MessageMiddleware",
391
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
392
    "django.middleware.locale.LocaleMiddleware",
393
    "allauth.account.middleware.AccountMiddleware",
394
    "django_ftl.middleware.activate_from_request_language_code",
395
    "django_referrer_policy.middleware.ReferrerPolicyMiddleware",
396
    "dockerflow.django.middleware.DockerflowMiddleware",
397
    "waffle.middleware.WaffleMiddleware",
398
    "privaterelay.middleware.AddDetectedCountryToRequestAndResponseHeaders",
399
    "privaterelay.middleware.StoreFirstVisit",
400
]
401

402
if HAS_SQLCOMMENTER:
1!
403
    MIDDLEWARE.append("google.cloud.sqlcommenter.django.middleware.SqlCommenter")
1✔
404

405
ROOT_URLCONF = "privaterelay.urls"
1✔
406

407
TEMPLATES = [
1✔
408
    {
409
        "BACKEND": "django.template.backends.django.DjangoTemplates",
410
        "DIRS": [
411
            os.path.join(BASE_DIR, "privaterelay", "templates"),
412
        ],
413
        "APP_DIRS": True,
414
        "OPTIONS": {
415
            "context_processors": [
416
                "django.template.context_processors.debug",
417
                "django.template.context_processors.request",
418
                "django.contrib.auth.context_processors.auth",
419
                "django.contrib.messages.context_processors.messages",
420
            ],
421
        },
422
    },
423
]
424

425
RELAY_FIREFOX_DOMAIN: str = config("RELAY_FIREFOX_DOMAIN", "relay.firefox.com")
1✔
426
MOZMAIL_DOMAIN: str = config("MOZMAIL_DOMAIN", "mozmail.com")
1✔
427
MAX_NUM_FREE_ALIASES: int = config("MAX_NUM_FREE_ALIASES", 5, cast=int)
1✔
428
PERIODICAL_PREMIUM_PROD_ID: str = config("PERIODICAL_PREMIUM_PROD_ID", "")
1✔
429
PREMIUM_PLAN_ID_US_MONTHLY: str = config(
1✔
430
    "PREMIUM_PLAN_ID_US_MONTHLY", "price_1LXUcnJNcmPzuWtRpbNOajYS"
431
)
432
PREMIUM_PLAN_ID_US_YEARLY: str = config(
1✔
433
    "PREMIUM_PLAN_ID_US_YEARLY", "price_1LXUdlJNcmPzuWtRKTYg7mpZ"
434
)
435
PHONE_PROD_ID = config("PHONE_PROD_ID", "")
1✔
436
PHONE_PLAN_ID_US_MONTHLY: str = config(
1✔
437
    "PHONE_PLAN_ID_US_MONTHLY", "price_1Li0w8JNcmPzuWtR2rGU80P3"
438
)
439
PHONE_PLAN_ID_US_YEARLY: str = config(
1✔
440
    "PHONE_PLAN_ID_US_YEARLY", "price_1Li15WJNcmPzuWtRIh0F4VwP"
441
)
442
BUNDLE_PROD_ID = config("BUNDLE_PROD_ID", "")
1✔
443
BUNDLE_PLAN_ID_US: str = config("BUNDLE_PLAN_ID_US", "price_1LwoSDJNcmPzuWtR6wPJZeoh")
1✔
444

445
SUBSCRIPTIONS_WITH_UNLIMITED: list[str] = config(
1✔
446
    "SUBSCRIPTIONS_WITH_UNLIMITED", default="", cast=Csv()
447
)
448
SUBSCRIPTIONS_WITH_PHONE: list[str] = config(
1✔
449
    "SUBSCRIPTIONS_WITH_PHONE", default="", cast=Csv()
450
)
451
SUBSCRIPTIONS_WITH_VPN: list[str] = config(
1✔
452
    "SUBSCRIPTIONS_WITH_VPN", default="", cast=Csv()
453
)
454

455
MAX_ONBOARDING_AVAILABLE = config("MAX_ONBOARDING_AVAILABLE", 0, cast=int)
1✔
456
MAX_ONBOARDING_FREE_AVAILABLE = config("MAX_ONBOARDING_FREE_AVAILABLE", 3, cast=int)
1✔
457

458
MAX_ADDRESS_CREATION_PER_DAY = config("MAX_ADDRESS_CREATION_PER_DAY", 100, cast=int)
1✔
459
MAX_REPLIES_PER_DAY = config("MAX_REPLIES_PER_DAY", 100, cast=int)
1✔
460
MAX_FORWARDED_PER_DAY = config("MAX_FORWARDED_PER_DAY", 1000, cast=int)
1✔
461
MAX_FORWARDED_EMAIL_SIZE_PER_DAY = config(
1✔
462
    "MAX_FORWARDED_EMAIL_SIZE_PER_DAY", 1_000_000_000, cast=int
463
)
464
PREMIUM_FEATURE_PAUSED_DAYS: int = config(
1✔
465
    "ACCOUNT_PREMIUM_FEATURE_PAUSED_DAYS", 1, cast=int
466
)
467

468
SOFT_BOUNCE_ALLOWED_DAYS: int = config("SOFT_BOUNCE_ALLOWED_DAYS", 1, cast=int)
1✔
469
HARD_BOUNCE_ALLOWED_DAYS: int = config("HARD_BOUNCE_ALLOWED_DAYS", 30, cast=int)
1✔
470

471
WSGI_APPLICATION = "privaterelay.wsgi.application"
1✔
472

473
# Database
474
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
475

476
DATABASES = {
1✔
477
    "default": dj_database_url.config(
478
        default="sqlite:///{}".format(os.path.join(BASE_DIR, "db.sqlite3"))
479
    )
480
}
481
# Optionally set a test database name.
482
# This is useful for forcing an on-disk database for SQLite.
483
TEST_DB_NAME = config("TEST_DB_NAME", "")
1✔
484
if TEST_DB_NAME:
1!
485
    DATABASES["default"]["TEST"] = {"NAME": TEST_DB_NAME}
×
486

487
REDIS_URL = config("REDIS_URL", "")
1✔
488
if REDIS_URL:
1!
489
    CACHES = {
×
490
        "default": {
491
            "BACKEND": "django_redis.cache.RedisCache",
492
            "LOCATION": REDIS_URL,
493
            "OPTIONS": {
494
                "CLIENT_CLASS": "django_redis.client.DefaultClient",
495
            },
496
        }
497
    }
498
    SESSION_ENGINE = "django.contrib.sessions.backends.cache"
×
499
    SESSION_CACHE_ALIAS = "default"
×
500
elif RELAY_CHANNEL == "local":
1!
501
    CACHES = {
1✔
502
        "default": {
503
            "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
504
        }
505
    }
506

507
# Password validation
508
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
509
# only needed when admin UI is enabled
510
if ADMIN_ENABLED:
1!
511
    _DJANGO_PWD_VALIDATION = "django.contrib.auth.password_validation"  # noqa: E501, S105 (long line, possible password)
×
512
    AUTH_PASSWORD_VALIDATORS = [
×
513
        {"NAME": _DJANGO_PWD_VALIDATION + ".UserAttributeSimilarityValidator"},
514
        {"NAME": _DJANGO_PWD_VALIDATION + ".MinimumLengthValidator"},
515
        {"NAME": _DJANGO_PWD_VALIDATION + ".CommonPasswordValidator"},
516
        {"NAME": _DJANGO_PWD_VALIDATION + ".NumericPasswordValidator"},
517
    ]
518

519

520
# Internationalization
521
# https://docs.djangoproject.com/en/2.2/topics/i18n/
522

523
LANGUAGE_CODE = "en"
1✔
524

525
# Mozilla l10n directories use lang-locale language codes,
526
# so we need to add those to LANGUAGES so Django's LocaleMiddleware
527
# can find them.
528
LANGUAGES = DEFAULT_LANGUAGES + [
1✔
529
    ("zh-tw", "Chinese"),
530
    ("zh-cn", "Chinese"),
531
    ("es-es", "Spanish"),
532
    ("pt-pt", "Portuguese"),
533
    ("skr", "Saraiki"),
534
]
535

536
TIME_ZONE = "UTC"
1✔
537

538
USE_I18N = True
1✔
539

540

541
USE_TZ = True
1✔
542

543
STATICFILES_DIRS = [
1✔
544
    os.path.join(BASE_DIR, "frontend/out"),
545
]
546
# Static files (the front-end in /frontend/)
547
# https://whitenoise.evans.io/en/stable/django.html#using-whitenoise-with-webpack-browserify-latest-js-thing
548
STATIC_URL = "/"
1✔
549
if DEBUG:
1!
550
    # In production, we run collectstatic to index all static files.
551
    # However, when running locally, we want to automatically pick up
552
    # all files spewed out by `npm run watch` in /frontend/out,
553
    # and we're fine with the performance impact of that.
554
    WHITENOISE_ROOT = os.path.join(BASE_DIR, "frontend/out")
1✔
555
STORAGES = {
1✔
556
    "default": {
557
        "BACKEND": "django.core.files.storage.FileSystemStorage",
558
    },
559
    "staticfiles": {
560
        "BACKEND": "privaterelay.storage.RelayStaticFilesStorage",
561
    },
562
}
563

564
# Relay does not support user-uploaded files
565
MEDIA_ROOT = None
1✔
566
MEDIA_URL = None
1✔
567

568
WHITENOISE_INDEX_FILE = True
1✔
569

570

571
# See
572
# https://whitenoise.evans.io/en/stable/django.html#WHITENOISE_ADD_HEADERS_FUNCTION
573
# Intended to ensure that the homepage does not get cached in our CDN,
574
# so that the `RedirectRootIfLoggedIn` middleware can kick in for logged-in
575
# users.
576
def set_index_cache_control_headers(
1✔
577
    headers: wsgiref.headers.Headers, path: str, url: str
578
) -> None:
579
    if DEBUG:
1!
580
        home_path = os.path.join(BASE_DIR, "frontend/out", "index.html")
1✔
581
    else:
582
        home_path = os.path.join(STATIC_ROOT, "index.html")
×
583
    if path == home_path:
1✔
584
        headers["Cache-Control"] = "no-cache, public"
1✔
585

586

587
WHITENOISE_ADD_HEADERS_FUNCTION = set_index_cache_control_headers
1✔
588

589
SITE_ID = 1
1✔
590

591
AUTHENTICATION_BACKENDS = (
1✔
592
    "django.contrib.auth.backends.ModelBackend",
593
    "allauth.account.auth_backends.AuthenticationBackend",
594
)
595

596
SOCIALACCOUNT_PROVIDERS = {
1✔
597
    "fxa": {
598
        # Note: to request "profile" scope, must be a trusted Mozilla client
599
        "SCOPE": ["profile", "https://identity.mozilla.com/account/subscriptions"],
600
        "AUTH_PARAMS": {"access_type": "offline"},
601
        "OAUTH_ENDPOINT": config(
602
            "FXA_OAUTH_ENDPOINT", "https://oauth.accounts.firefox.com/v1"
603
        ),
604
        "PROFILE_ENDPOINT": config(
605
            "FXA_PROFILE_ENDPOINT", "https://profile.accounts.firefox.com/v1"
606
        ),
607
        "VERIFIED_EMAIL": True,  # Assume FxA primary email is verified
608
    }
609
}
610

611
SOCIALACCOUNT_EMAIL_VERIFICATION = "none"
1✔
612
SOCIALACCOUNT_AUTO_SIGNUP = True
1✔
613
SOCIALACCOUNT_LOGIN_ON_GET = True
1✔
614
SOCIALACCOUNT_STORE_TOKENS = True
1✔
615

616
ACCOUNT_ADAPTER = "privaterelay.allauth.AccountAdapter"
1✔
617
ACCOUNT_PRESERVE_USERNAME_CASING = False
1✔
618
ACCOUNT_USERNAME_REQUIRED = False
1✔
619

620
FXA_REQUESTS_TIMEOUT_SECONDS = config("FXA_REQUESTS_TIMEOUT_SECONDS", 1, cast=int)
1✔
621
FXA_SETTINGS_URL = config("FXA_SETTINGS_URL", f"{FXA_BASE_ORIGIN}/settings")
1✔
622
FXA_SUBSCRIPTIONS_URL = config(
1✔
623
    "FXA_SUBSCRIPTIONS_URL", f"{FXA_BASE_ORIGIN}/subscriptions"
624
)
625
# check https://mozilla.github.io/ecosystem-platform/api#tag/Subscriptions/operation/getOauthMozillasubscriptionsCustomerBillingandsubscriptions  # noqa: E501 (line too long)
626
FXA_ACCOUNTS_ENDPOINT = config(
1✔
627
    "FXA_ACCOUNTS_ENDPOINT",
628
    "https://api.accounts.firefox.com/v1",
629
)
630
FXA_SUPPORT_URL = config("FXA_SUPPORT_URL", f"{FXA_BASE_ORIGIN}/support/")
1✔
631

632
LOGGING = {
1✔
633
    "version": 1,
634
    "filters": {
635
        "request_id": {
636
            "()": "dockerflow.logging.RequestIdLogFilter",
637
        },
638
    },
639
    "formatters": {
640
        "json": {
641
            "()": "dockerflow.logging.JsonLogFormatter",
642
            "logger_name": "fx-private-relay",
643
        }
644
    },
645
    "handlers": {
646
        "console_out": {
647
            "level": "DEBUG",
648
            "class": "logging.StreamHandler",
649
            "stream": sys.stdout,
650
            "formatter": "json",
651
            "filters": ["request_id"],
652
        },
653
        "console_err": {
654
            "level": "DEBUG",
655
            "class": "logging.StreamHandler",
656
            "formatter": "json",
657
            "filters": ["request_id"],
658
        },
659
    },
660
    "loggers": {
661
        "root": {
662
            "handlers": ["console_err"],
663
            "level": "WARNING",
664
        },
665
        "request.summary": {
666
            "handlers": ["console_out"],
667
            "level": "DEBUG",
668
            # pytest's caplog fixture requires propagate=True
669
            # outside of pytest, use propagate=False to avoid double logs
670
            "propagate": IN_PYTEST,
671
        },
672
        "events": {
673
            "handlers": ["console_err"],
674
            "level": "WARNING",
675
            "propagate": IN_PYTEST,
676
        },
677
        "eventsinfo": {
678
            "handlers": ["console_out"],
679
            "level": "INFO",
680
            "propagate": IN_PYTEST,
681
        },
682
        "abusemetrics": {
683
            "handlers": ["console_out"],
684
            "level": "INFO",
685
            "propagate": IN_PYTEST,
686
        },
687
        "studymetrics": {
688
            "handlers": ["console_out"],
689
            "level": "INFO",
690
            "propagate": IN_PYTEST,
691
        },
692
        "markus": {
693
            "handlers": ["console_out"],
694
            "level": "DEBUG",
695
            "propagate": IN_PYTEST,
696
        },
697
        GLEAN_EVENT_MOZLOG_TYPE: {
698
            "handlers": ["console_out"],
699
            "level": "DEBUG",
700
            "propagate": IN_PYTEST,
701
        },
702
        "dockerflow": {
703
            "handlers": ["console_err"],
704
            "level": "WARNING",
705
            "propagate": IN_PYTEST,
706
        },
707
    },
708
}
709

710
DRF_RENDERERS = ["rest_framework.renderers.JSONRenderer"]
1✔
711
if DEBUG and not IN_PYTEST:
1!
712
    DRF_RENDERERS += [
×
713
        "rest_framework.renderers.BrowsableAPIRenderer",
714
    ]
715

716
FIRST_EMAIL_RATE_LIMIT = config("FIRST_EMAIL_RATE_LIMIT", "5/minute")
1✔
717
if IN_PYTEST or RELAY_CHANNEL in ["local", "dev"]:
1!
718
    FIRST_EMAIL_RATE_LIMIT = "1000/minute"
1✔
719

720
REST_FRAMEWORK = {
1✔
721
    "DEFAULT_AUTHENTICATION_CLASSES": [
722
        "api.authentication.FxaTokenAuthentication",
723
        "rest_framework.authentication.TokenAuthentication",
724
        "rest_framework.authentication.SessionAuthentication",
725
    ],
726
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
727
    "DEFAULT_RENDERER_CLASSES": DRF_RENDERERS,
728
    "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
729
    "EXCEPTION_HANDLER": "api.views.relay_exception_handler",
730
}
731
if API_DOCS_ENABLED:
1!
732
    REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema"
1✔
733

734
SPECTACULAR_SETTINGS = {
1✔
735
    "SWAGGER_UI_DIST": "SIDECAR",
736
    "SWAGGER_UI_FAVICON_HREF": "SIDECAR",
737
    "REDOC_DIST": "SIDECAR",
738
    "TITLE": "Firefox Relay API",
739
    "DESCRIPTION": (
740
        "Keep your email safe from hackers and trackers. This API is built with"
741
        " Django REST Framework and powers the Relay website UI, add-on,"
742
        " Firefox browser, and 3rd-party app integrations."
743
    ),
744
    "VERSION": "1.0",
745
    "SERVE_INCLUDE_SCHEMA": False,
746
    "PREPROCESSING_HOOKS": ["api.schema.preprocess_ignore_deprecated_paths"],
747
    "SORT_OPERATIONS": "api.schema.sort_by_tag",
748
}
749

750
if IN_PYTEST or RELAY_CHANNEL in ["local", "dev"]:
1!
751
    _DEFAULT_PHONE_RATE_LIMIT = "1000/minute"
1✔
752
else:
753
    _DEFAULT_PHONE_RATE_LIMIT = "5/minute"
×
754
PHONE_RATE_LIMIT = config("PHONE_RATE_LIMIT", _DEFAULT_PHONE_RATE_LIMIT)
1✔
755

756
# Turn on logging out on GET in development.
757
# This allows `/mock/logout/` in the front-end to clear the
758
# session cookie. Without this, after switching accounts in dev mode,
759
# then logging out again, API requests continue succeeding even without
760
# an auth token:
761
ACCOUNT_LOGOUT_ON_GET = DEBUG
1✔
762

763
# TODO: introduce an environment variable to control CORS_ALLOWED_ORIGINS
764
# https://mozilla-hub.atlassian.net/browse/MPP-3468
765
CORS_URLS_REGEX = r"^/api/"
1✔
766
CORS_ALLOWED_ORIGINS = [
1✔
767
    "https://vault.bitwarden.com",
768
    "https://vault.bitwarden.eu",
769
]
770
if RELAY_CHANNEL in ["dev", "stage"]:
1!
771
    CORS_ALLOWED_ORIGINS += [
×
772
        "https://vault.qa.bitwarden.pw",
773
        "https://vault.euqa.bitwarden.pw",
774
    ]
775
# Allow origins for each environment to help debug cors headers
776
if RELAY_CHANNEL == "local":
1!
777
    # In local dev, next runs on localhost and makes requests to /accounts/
778
    CORS_ALLOWED_ORIGINS += [
1✔
779
        "http://localhost:3000",
780
        "http://0.0.0.0:3000",
781
        "http://127.0.0.1:8000",
782
    ]
783
    CORS_URLS_REGEX = r"^/(api|accounts)/"
1✔
784
if RELAY_CHANNEL == "dev":
1!
785
    CORS_ALLOWED_ORIGINS += [
×
786
        "https://dev.fxprivaterelay.nonprod.cloudops.mozgcp.net",
787
    ]
788
if RELAY_CHANNEL == "stage":
1!
789
    CORS_ALLOWED_ORIGINS += [
×
790
        "https://stage.fxprivaterelay.nonprod.cloudops.mozgcp.net",
791
    ]
792

793
CSRF_TRUSTED_ORIGINS = []
1✔
794
if RELAY_CHANNEL == "local":
1!
795
    # In local development, the React UI can be served up from a different server
796
    # that needs to be allowed to make requests.
797
    # In production, the frontend is served by Django, is therefore on the same
798
    # origin and thus has access to the same cookies.
799
    CORS_ALLOW_CREDENTIALS = True
1✔
800
    SESSION_COOKIE_SAMESITE = None
1✔
801
    CSRF_TRUSTED_ORIGINS += [
1✔
802
        "http://localhost:3000",
803
        "http://0.0.0.0:3000",
804
    ]
805

806
SENTRY_RELEASE = config("SENTRY_RELEASE", "")
1✔
807
CIRCLE_SHA1 = config("CIRCLE_SHA1", "")
1✔
808
CIRCLE_TAG = config("CIRCLE_TAG", "")
1✔
809
CIRCLE_BRANCH = config("CIRCLE_BRANCH", "")
1✔
810

811
sentry_release: str | None = None
1✔
812
if SENTRY_RELEASE:
1!
813
    sentry_release = SENTRY_RELEASE
×
814
elif CIRCLE_TAG and CIRCLE_TAG != "unknown":
1!
815
    sentry_release = CIRCLE_TAG
×
816
elif (
1!
817
    CIRCLE_SHA1
818
    and CIRCLE_SHA1 != "unknown"
819
    and CIRCLE_BRANCH
820
    and CIRCLE_BRANCH != "unknown"
821
):
822
    sentry_release = f"{CIRCLE_BRANCH}:{CIRCLE_SHA1}"
1✔
823

824
SENTRY_DEBUG = config("SENTRY_DEBUG", DEBUG, cast=bool)
1✔
825

826
SENTRY_ENVIRONMENT = config("SENTRY_ENVIRONMENT", RELAY_CHANNEL)
1✔
827
# Use "local" as default rather than "prod", to catch ngrok.io URLs
828
if SENTRY_ENVIRONMENT == "prod" and SITE_ORIGIN != "https://relay.firefox.com":
1!
829
    SENTRY_ENVIRONMENT = "local"
×
830

831
sentry_sdk.init(
1✔
832
    dsn=config("SENTRY_DSN", None),
833
    integrations=[DjangoIntegration(cache_spans=not DEBUG)],
834
    debug=SENTRY_DEBUG,
835
    include_local_variables=DEBUG,
836
    release=sentry_release,
837
    environment=SENTRY_ENVIRONMENT,
838
)
839
# Duplicates events for unhandled exceptions, but without useful tracebacks
840
ignore_logger("request.summary")
1✔
841
# Security scanner attempts, no action required
842
# Can be re-enabled when hostname allow list implemented at the load balancer
843
ignore_logger("django.security.DisallowedHost")
1✔
844
# Fluent errors, mostly when a translation is unavailable for the locale.
845
# It is more effective to process these from logs using BigQuery than to track
846
# as events in Sentry.
847
ignore_logger("django_ftl.message_errors")
1✔
848
# Security scanner attempts on Heroku dev, no action required
849
if RELAY_CHANNEL == "dev":
1!
850
    ignore_logger("django.security.SuspiciousFileOperation")
×
851

852

853
_MARKUS_BACKENDS: list[dict[str, Any]] = []
1✔
854
if DJANGO_STATSD_ENABLED:
1!
855
    _MARKUS_BACKENDS.append(
×
856
        {
857
            "class": "markus.backends.datadog.DatadogMetrics",
858
            "options": {
859
                "statsd_host": STATSD_HOST,
860
                "statsd_port": STATSD_PORT,
861
                "statsd_prefix": STATSD_PREFIX,
862
            },
863
        }
864
    )
865
if STATSD_DEBUG:
1!
866
    _MARKUS_BACKENDS.append(
×
867
        {
868
            "class": "markus.backends.logging.LoggingMetrics",
869
            "options": {
870
                "logger_name": "markus",
871
                "leader": "METRICS",
872
            },
873
        }
874
    )
875
markus.configure(backends=_MARKUS_BACKENDS)
1✔
876

877
if USE_SILK:
1!
878
    SILKY_PYTHON_PROFILER = True
×
879
    SILKY_PYTHON_PROFILER_BINARY = True
×
880
    SILKY_PYTHON_PROFILER_RESULT_PATH = ".silk-profiler"
×
881

882
# Settings for manage.py process_emails_from_sqs
883
PROCESS_EMAIL_BATCH_SIZE = config(
1✔
884
    "PROCESS_EMAIL_BATCH_SIZE", 10, cast=Choices(range(1, 11), cast=int)
885
)
886
PROCESS_EMAIL_DELETE_FAILED_MESSAGES = config(
1✔
887
    "PROCESS_EMAIL_DELETE_FAILED_MESSAGES", False, cast=bool
888
)
889
PROCESS_EMAIL_HEALTHCHECK_PATH = config(
1✔
890
    "PROCESS_EMAIL_HEALTHCHECK_PATH", os.path.join(TMP_DIR, "healthcheck.json")
891
)
892
PROCESS_EMAIL_MAX_SECONDS = config("PROCESS_EMAIL_MAX_SECONDS", 0, cast=int) or None
1✔
893
PROCESS_EMAIL_VERBOSITY = config(
1✔
894
    "PROCESS_EMAIL_VERBOSITY", 1, cast=Choices(range(0, 4), cast=int)
895
)
896
PROCESS_EMAIL_VISIBILITY_SECONDS = config(
1✔
897
    "PROCESS_EMAIL_VISIBILITY_SECONDS", 120, cast=int
898
)
899
PROCESS_EMAIL_WAIT_SECONDS = config("PROCESS_EMAIL_WAIT_SECONDS", 5, cast=int)
1✔
900
PROCESS_EMAIL_HEALTHCHECK_MAX_AGE = config(
1✔
901
    "PROCESS_EMAIL_HEALTHCHECK_MAX_AGE", 120, cast=int
902
)
903
PROCESS_EMAIL_MAX_SECONDS_PER_MESSAGE = config(
1✔
904
    "PROCESS_EMAIL_MAX_SECONDS_PER_MESSAGE",
905
    PROCESS_EMAIL_MAX_SECONDS or 120.0,
906
    cast=float,
907
)
908

909
# Django 3.2 switches default to BigAutoField
910
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
1✔
911

912
# python-dockerflow settings
913
DOCKERFLOW_VERSION_CALLBACK = "privaterelay.utils.get_version_info"
1✔
914
DOCKERFLOW_CHECKS = [
1✔
915
    "dockerflow.django.checks.check_database_connected",
916
    "dockerflow.django.checks.check_migrations_applied",
917
]
918
if REDIS_URL:
1!
919
    DOCKERFLOW_CHECKS.append("dockerflow.django.checks.check_redis_connected")
×
920
DOCKERFLOW_REQUEST_ID_HEADER_NAME = config("DOCKERFLOW_REQUEST_ID_HEADER_NAME", None)
1✔
921
SILENCED_SYSTEM_CHECKS = sorted(
1✔
922
    set(config("DJANGO_SILENCED_SYSTEM_CHECKS", default="", cast=Csv()))
923
    | {
924
        # (models.W040) SQLite does not support indexes with non-key columns.
925
        # RelayAddress index idx_ra_created_by_addon uses this for PostgreSQL.
926
        "models.W040",
927
    }
928
)
929

930
# django-ftl settings
931
AUTO_RELOAD_BUNDLES = False  # Requires pyinotify
1✔
932

933
# Patching for django-types
934
django_stubs_ext.monkeypatch()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc