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

mozilla / fx-private-relay / 35084fc5-73ae-405a-93ba-7c0998b1a2a7

15 Oct 2025 02:15PM UTC coverage: 88.889% (+0.03%) from 88.862%
35084fc5-73ae-405a-93ba-7c0998b1a2a7

Pull #5964

circleci

jwhitlock
docs: Add ticket references to TODOs
Pull Request #5964: Remove heroku references and code paths (MPP-4425)

2920 of 3925 branches covered (74.39%)

Branch coverage included in aggregate %.

7 of 9 new or added lines in 5 files covered. (77.78%)

1 existing line in 1 file now uncovered.

18072 of 19691 relevant lines covered (91.78%)

11.41 hits per line

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

78.67
/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, 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 sentry_sdk
1✔
28
from csp.constants import NONCE, NONE, SELF, UNSAFE_INLINE
1✔
29
from decouple import Choices, Csv, config
1✔
30
from sentry_sdk.integrations.django import DjangoIntegration
1✔
31
from sentry_sdk.integrations.logging import ignore_logger
1✔
32

33
from .types import CONTENT_SECURITY_POLICY_T, RELAY_CHANNEL_NAME
1✔
34

35
if TYPE_CHECKING:
36
    import wsgiref.headers
37

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

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

47
try:
1✔
48
    from privaterelay.glean.server_events import GLEAN_EVENT_MOZLOG_TYPE
1✔
49
except ImportError:
×
50
    # File may not be generated yet. Will be checked at initialization
51
    GLEAN_EVENT_MOZLOG_TYPE = "glean-server-event"
×
52

53
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
54
BASE_DIR: str = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1✔
55
TMP_DIR = os.path.join(BASE_DIR, "tmp")
1✔
56
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
1✔
57

58
# Quick-start development settings - unsuitable for production
59
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
60

61
# defaulting to blank to be production-broken by default
62
SECRET_KEY = config("SECRET_KEY", None)
1✔
63
SECRET_KEY_FALLBACKS = config("SECRET_KEY_FALLBACKS", "", cast=Csv())
1✔
64
SITE_ORIGIN: str | None = config("SITE_ORIGIN", None)
1✔
65

66
ORIGIN_CHANNEL_MAP: dict[str, RELAY_CHANNEL_NAME] = {
1✔
67
    "http://127.0.0.1:8000": "local",
68
    "https://relay.firefox.com": "prod",
69
    # GCPv1
70
    "https://stage.fxprivaterelay.nonprod.cloudops.mozgcp.net": "stage",
71
    # GCPv2
72
    "https://relay-dev.allizom.org": "dev",
73
    "https://relay.allizom.org": "stage",
74
}
75
RELAY_CHANNEL: RELAY_CHANNEL_NAME = cast(
1✔
76
    RELAY_CHANNEL_NAME,
77
    config(
78
        "RELAY_CHANNEL",
79
        default=ORIGIN_CHANNEL_MAP.get(SITE_ORIGIN or "", "local"),
80
        cast=Choices(get_args(RELAY_CHANNEL_NAME), cast=str),
81
    ),
82
)
83

84
DEBUG = config("DEBUG", False, cast=bool)
1✔
85
if DEBUG:
1!
86
    INTERNAL_IPS = config("INTERNAL_IPS", default="", cast=Csv()) or config(
1✔
87
        "DJANGO_INTERNAL_IPS", default="", cast=Csv()
88
    )
89
IN_PYTEST: bool = "pytest" in sys.modules
1✔
90
USE_SILK = DEBUG and HAS_SILK and not IN_PYTEST
1✔
91
DEFAULT_EXCEPTION_REPORTER_FILTER = (
1✔
92
    "privaterelay.debug.RelaySaferExceptionReporterFilter"
93
)
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("SECURE_SSL_HOST", None) or config(
1✔
98
    "DJANGO_SECURE_SSL_HOST", None
99
)
100
SECURE_SSL_REDIRECT = config("SECURE_SSL_REDIRECT", False, cast=bool) or config(
1✔
101
    "DJANGO_SECURE_SSL_REDIRECT", False, cast=bool
102
)
103
SECURE_REDIRECT_EXEMPT = [
1✔
104
    r"^__version__",
105
    r"^__heartbeat__",
106
    r"^__lbheartbeat__",
107
]
108
SECURE_HSTS_INCLUDE_SUBDOMAINS = config(
1✔
109
    "SECURE_HSTS_INCLUDE_SUBDOMAINS", False, cast=bool
110
) or config("DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", False, cast=bool)
111
SECURE_HSTS_PRELOAD = config("SECURE_HSTS_PRELOAD", False, cast=bool) or config(
1✔
112
    "DJANGO_SECURE_HSTS_PRELOAD", False, cast=bool
113
)
114
SECURE_HSTS_SECONDS = config("SECURE_HSTS_SECONDS", None) or config(
1✔
115
    "DJANGO_SECURE_HSTS_SECONDS", None
116
)
117
# Default to "false" in first envvar check so that we fall back to the GCP v1 value
118
SECURE_BROWSER_XSS_FILTER = config("SECURE_BROWSER_XSS_FILTER", False) or config(
1✔
119
    "DJANGO_SECURE_BROWSER_XSS_FILTER", True
120
)
121
SESSION_COOKIE_SECURE = config("DJANGO_SESSION_COOKIE_SECURE", False, cast=bool)
1✔
122
CSRF_COOKIE_SECURE = config("DJANGO_CSRF_COOKIE_SECURE", False, cast=bool)
1✔
123

124
#
125
# Setup CSP
126
#
127

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

130
# maps FxA / Mozilla account profile hosts to respective hosts for CSP
131
FXA_BASE_ORIGIN: str = config("FXA_BASE_ORIGIN", "https://accounts.firefox.com")
1✔
132
if FXA_BASE_ORIGIN == "https://accounts.firefox.com":
1!
133
    _AVATAR_IMG_SRC = [
×
134
        "firefoxusercontent.com",
135
        "https://profile.accounts.firefox.com",
136
    ]
137
    _ACCOUNT_CONNECT_SRC = [FXA_BASE_ORIGIN]
×
138
else:
139
    if not FXA_BASE_ORIGIN == "https://accounts.stage.mozaws.net":
1!
140
        raise ValueError(
×
141
            "FXA_BASE_ORIGIN must be either https://accounts.firefox.com or https://accounts.stage.mozaws.net"
142
        )
143
    _AVATAR_IMG_SRC = [
1✔
144
        "mozillausercontent.com",
145
        "https://profile.stage.mozaws.net",
146
    ]
147
    _ACCOUNT_CONNECT_SRC = [
1✔
148
        FXA_BASE_ORIGIN,
149
        # fxaFlowTracker.ts will try this if runtimeData is slow
150
        "https://accounts.firefox.com",
151
    ]
152

153
API_DOCS_ENABLED = config("API_DOCS_ENABLED", False, cast=bool) or DEBUG
1✔
154
_CSP_SCRIPT_INLINE = USE_SILK
1✔
155

156
# When running locally, styles might get refreshed while the server is running, so their
157
# hashes would get outdated. Hence, we just allow all of them.
158
_CSP_STYLE_INLINE = API_DOCS_ENABLED or RELAY_CHANNEL == "local"
1✔
159

160
if API_DOCS_ENABLED:
1!
161
    _API_DOCS_CSP_IMG_SRC = ["data:", "https://cdn.redoc.ly"]
1✔
162
    _API_DOCS_CSP_STYLE_SRC = ["https://fonts.googleapis.com"]
1✔
163
    _API_DOCS_CSP_FONT_SRC = ["https://fonts.gstatic.com"]
1✔
164
    _API_DOCS_CSP_WORKER_SRC = ["blob:"]
1✔
165
else:
166
    _API_DOCS_CSP_IMG_SRC = []
×
167
    _API_DOCS_CSP_STYLE_SRC = []
×
168
    _API_DOCS_CSP_FONT_SRC = []
×
169
    _API_DOCS_CSP_WORKER_SRC = []
×
170

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

192
    # Add the hash for an empty string (sha256-47DEQp...)
193
    # next,js injects an empty style element and then adds the content.
194
    # This hash avoids a spurious CSP error.
195
    empty_hash = base64.b64encode(sha256().digest()).decode()
×
196
    _CSP_STYLE_HASHES.append(f"'sha256-{empty_hash}'")
×
197

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

244
REFERRER_POLICY = "strict-origin-when-cross-origin"
1✔
245

246
ALLOWED_HOSTS: list[str] = []
1✔
247
DJANGO_ALLOWED_HOSTS = config("ALLOWED_HOSTS", "", cast=Csv()) or config(
1✔
248
    "DJANGO_ALLOWED_HOST", "", cast=Csv()
249
)
250

251
if DJANGO_ALLOWED_HOSTS:
1!
252
    ALLOWED_HOSTS += DJANGO_ALLOWED_HOSTS
×
253
ALLOWED_SUBNET = config("ALLOWED_SUBNET", None) or config("DJANGO_ALLOWED_SUBNET", None)
1✔
254
if ALLOWED_SUBNET:
1!
255
    ALLOWED_HOSTS += [str(ip) for ip in ipaddress.IPv4Network(ALLOWED_SUBNET)]
×
256

257

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

261

262
AWS_REGION: str | None = config("AWS_REGION", None)
1✔
263
AWS_ACCESS_KEY_ID = config("AWS_ACCESS_KEY_ID", None)
1✔
264
AWS_SECRET_ACCESS_KEY = config("AWS_SECRET_ACCESS_KEY", None)
1✔
265
AWS_SNS_TOPIC = set(config("AWS_SNS_TOPIC", "", cast=Csv()))
1✔
266
AWS_SNS_KEY_CACHE = config("AWS_SNS_KEY_CACHE", "default")
1✔
267
AWS_SES_CONFIGSET: str | None = config("AWS_SES_CONFIGSET", None)
1✔
268
AWS_SQS_EMAIL_QUEUE_URL = config("AWS_SQS_EMAIL_QUEUE_URL", None)
1✔
269
AWS_SQS_EMAIL_DLQ_URL = config("AWS_SQS_EMAIL_DLQ_URL", None)
1✔
270

271
RELAY_FROM_ADDRESS: str = config("RELAY_FROM_ADDRESS", "")
1✔
272
GOOGLE_ANALYTICS_ID = config("GOOGLE_ANALYTICS_ID", None)
1✔
273
GA4_MEASUREMENT_ID = config("GA4_MEASUREMENT_ID", None)
1✔
274
GOOGLE_APPLICATION_CREDENTIALS: str = config("GOOGLE_APPLICATION_CREDENTIALS", "")
1✔
275
GOOGLE_CLOUD_PROFILER_CREDENTIALS_B64: str = config(
1✔
276
    "GOOGLE_CLOUD_PROFILER_CREDENTIALS_B64", ""
277
)
278
RECRUITMENT_BANNER_LINK = config("RECRUITMENT_BANNER_LINK", None)
1✔
279
RECRUITMENT_BANNER_TEXT = config("RECRUITMENT_BANNER_TEXT", None)
1✔
280
RECRUITMENT_EMAIL_BANNER_TEXT = config("RECRUITMENT_EMAIL_BANNER_TEXT", None)
1✔
281
RECRUITMENT_EMAIL_BANNER_LINK = config("RECRUITMENT_EMAIL_BANNER_LINK", None)
1✔
282

283
PHONES_ENABLED: bool = config("PHONES_ENABLED", False, cast=bool)
1✔
284
PHONES_NO_CLIENT_CALLS_IN_TEST = False  # Override in tests that do not test clients
1✔
285
TWILIO_ACCOUNT_SID: str | None = config("TWILIO_ACCOUNT_SID", None)
1✔
286
TWILIO_AUTH_TOKEN: str | None = config("TWILIO_AUTH_TOKEN", None)
1✔
287
TWILIO_MAIN_NUMBER: str | None = config("TWILIO_MAIN_NUMBER", None)
1✔
288
TWILIO_SMS_APPLICATION_SID: str | None = config("TWILIO_SMS_APPLICATION_SID", None)
1✔
289
TWILIO_MESSAGING_SERVICE_SID: list[str] = config(
1✔
290
    "TWILIO_MESSAGING_SERVICE_SID", "", cast=Csv()
291
)
292
TWILIO_TEST_ACCOUNT_SID: str | None = config("TWILIO_TEST_ACCOUNT_SID", None)
1✔
293
TWILIO_TEST_AUTH_TOKEN: str | None = config("TWILIO_TEST_AUTH_TOKEN", None)
1✔
294
TWILIO_ALLOWED_COUNTRY_CODES = {
1✔
295
    code.upper()
296
    for code in config("TWILIO_ALLOWED_COUNTRY_CODES", "US,CA,PR", cast=Csv())
297
}
298
TWILIO_NEEDS_10DLC_CAMPAIGN = {
1✔
299
    code.upper() for code in config("TWILIO_NEEDS_10DLC_CAMPAIGN", "US,PR", cast=Csv())
300
}
301
MAX_MINUTES_TO_VERIFY_REAL_PHONE: int = config(
1✔
302
    "MAX_MINUTES_TO_VERIFY_REAL_PHONE", 5, cast=int
303
)
304
MAX_TEXTS_PER_BILLING_CYCLE: int = config("MAX_TEXTS_PER_BILLING_CYCLE", 75, cast=int)
1✔
305
MAX_MINUTES_PER_BILLING_CYCLE: int = config(
1✔
306
    "MAX_MINUTES_PER_BILLING_CYCLE", 50, cast=int
307
)
308
DAYS_PER_BILLING_CYCLE = config("DAYS_PER_BILLING_CYCLE", 30, cast=int)
1✔
309
MAX_DAYS_IN_MONTH = 31
1✔
310

311
STATSD_DEBUG = config("STATSD_DEBUG", False, cast=bool)
1✔
312
STATSD_ENABLED: bool = (
1✔
313
    config("STATSD_ENABLED", False, cast=bool)
314
    or config("DJANGO_STATSD_ENABLED", False, cast=bool)
315
    or STATSD_DEBUG
316
)
317
STATSD_HOST = config("STATSD_HOST", "") or config("DJANGO_STATSD_HOST", "127.0.0.1")
1✔
318

319
STATSD_PORT = config("STATSD_PORT", "") or config("DJANGO_STATSD_PORT", "8125")
1✔
320
STATSD_PREFIX = config("STATSD_PREFIX", "") or config(
1✔
321
    "DJANGO_STATSD_PREFIX", "firefox_relay"
322
)
323

324
SERVE_ADDON = config("SERVE_ADDON", None)
1✔
325

326
# Application definition
327
INSTALLED_APPS = [
1✔
328
    "whitenoise.runserver_nostatic",
329
    "django.contrib.staticfiles",
330
    "django.contrib.auth",
331
    "django.contrib.contenttypes",
332
    "django.contrib.sessions",
333
    "django.contrib.messages",
334
    "django.contrib.sites",
335
    "django_filters",
336
    "django_ftl.apps.DjangoFtlConfig",
337
    "dockerflow.django",
338
    "allauth",
339
    "allauth.account",
340
    "allauth.socialaccount",
341
    "allauth.socialaccount.providers.fxa",
342
    "rest_framework",
343
    "rest_framework.authtoken",
344
    "corsheaders",
345
    "csp",
346
    "waffle",
347
    "privaterelay.apps.PrivateRelayConfig",
348
    "api.apps.ApiConfig",
349
]
350

351
if API_DOCS_ENABLED:
1!
352
    INSTALLED_APPS += [
1✔
353
        "drf_spectacular",
354
        "drf_spectacular_sidecar",
355
    ]
356

357
if DEBUG:
1!
358
    INSTALLED_APPS += [
1✔
359
        "debug_toolbar",
360
    ]
361

362
if USE_SILK:
1!
363
    INSTALLED_APPS.append("silk")
×
364

365
if ADMIN_ENABLED:
1!
366
    INSTALLED_APPS += [
×
367
        "django.contrib.admin",
368
    ]
369

370
if AWS_SES_CONFIGSET and AWS_SNS_TOPIC:
1!
371
    INSTALLED_APPS += [
1✔
372
        "emails.apps.EmailsConfig",
373
    ]
374

375
if PHONES_ENABLED:
1!
376
    INSTALLED_APPS += [
1✔
377
        "phones.apps.PhonesConfig",
378
    ]
379

380

381
MIDDLEWARE = ["privaterelay.middleware.ResponseMetrics"]
1✔
382

383
if USE_SILK:
1!
384
    MIDDLEWARE.append("silk.middleware.SilkyMiddleware")
×
385
if DEBUG:
1!
386
    MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
1✔
387

388
MIDDLEWARE += [
1✔
389
    "django.middleware.security.SecurityMiddleware",
390
    "privaterelay.middleware.EagerNonceCSPMiddleware",
391
    "privaterelay.middleware.RedirectRootIfLoggedIn",
392
    "privaterelay.middleware.RelayStaticFilesMiddleware",
393
    "django.contrib.sessions.middleware.SessionMiddleware",
394
    "corsheaders.middleware.CorsMiddleware",
395
    "django.middleware.common.CommonMiddleware",
396
    "django.middleware.csrf.CsrfViewMiddleware",
397
    "django.contrib.auth.middleware.AuthenticationMiddleware",
398
    "django.contrib.messages.middleware.MessageMiddleware",
399
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
400
    "django.middleware.locale.LocaleMiddleware",
401
    "allauth.account.middleware.AccountMiddleware",
402
    "django_ftl.middleware.activate_from_request_language_code",
403
    "django_referrer_policy.middleware.ReferrerPolicyMiddleware",
404
    "dockerflow.django.middleware.DockerflowMiddleware",
405
    "waffle.middleware.WaffleMiddleware",
406
    "privaterelay.middleware.AddDetectedCountryToRequestAndResponseHeaders",
407
    "privaterelay.middleware.StoreFirstVisit",
408
    "privaterelay.middleware.GleanApiAccessMiddleware",
409
]
410

411
ROOT_URLCONF = "privaterelay.urls"
1✔
412

413
TEMPLATES = [
1✔
414
    {
415
        "BACKEND": "django.template.backends.django.DjangoTemplates",
416
        "DIRS": [
417
            os.path.join(BASE_DIR, "privaterelay", "templates"),
418
        ],
419
        "APP_DIRS": True,
420
        "OPTIONS": {
421
            "context_processors": [
422
                "django.template.context_processors.debug",
423
                "django.template.context_processors.request",
424
                "django.contrib.auth.context_processors.auth",
425
                "django.contrib.messages.context_processors.messages",
426
            ],
427
        },
428
    },
429
]
430

431
RELAY_FIREFOX_DOMAIN: str = config("RELAY_FIREFOX_DOMAIN", "relay.firefox.com")
1✔
432
MOZMAIL_DOMAIN: str = config("MOZMAIL_DOMAIN", "mozmail.com")
1✔
433
MAX_NUM_FREE_ALIASES: int = config("MAX_NUM_FREE_ALIASES", 5, cast=int)
1✔
434
PERIODICAL_PREMIUM_PROD_ID: str = config("PERIODICAL_PREMIUM_PROD_ID", "")
1✔
435
PREMIUM_PLAN_ID_US_MONTHLY: str = config(
1✔
436
    "PREMIUM_PLAN_ID_US_MONTHLY", "price_1LXUcnJNcmPzuWtRpbNOajYS"
437
)
438
PREMIUM_PLAN_ID_US_YEARLY: str = config(
1✔
439
    "PREMIUM_PLAN_ID_US_YEARLY", "price_1LXUdlJNcmPzuWtRKTYg7mpZ"
440
)
441
PHONE_PROD_ID = config("PHONE_PROD_ID", "")
1✔
442
PHONE_PLAN_ID_US_MONTHLY: str = config(
1✔
443
    "PHONE_PLAN_ID_US_MONTHLY", "price_1Li0w8JNcmPzuWtR2rGU80P3"
444
)
445
PHONE_PLAN_ID_US_YEARLY: str = config(
1✔
446
    "PHONE_PLAN_ID_US_YEARLY", "price_1Li15WJNcmPzuWtRIh0F4VwP"
447
)
448
BUNDLE_PROD_ID = config("BUNDLE_PROD_ID", "")
1✔
449
BUNDLE_PLAN_ID_US: str = config("BUNDLE_PLAN_ID_US", "price_1LwoSDJNcmPzuWtR6wPJZeoh")
1✔
450
MEGABUNDLE_PROD_ID = config("MEGABUNDLE_PROD_ID", "prod_SFb8iVuZIOPREe")
1✔
451
MEGABUNDLE_PLAN_ID_US: str = config(
1✔
452
    "MEGABUNDLE_PLAN_ID_US", "price_1RMAopKb9q6OnNsLSGe1vLtt"
453
)
454

455
SUBSCRIPTIONS_WITH_UNLIMITED: list[str] = config(
1✔
456
    "SUBSCRIPTIONS_WITH_UNLIMITED", default="", cast=Csv()
457
)
458
SUBSCRIPTIONS_WITH_PHONE: list[str] = config(
1✔
459
    "SUBSCRIPTIONS_WITH_PHONE", default="", cast=Csv()
460
)
461
SUBSCRIPTIONS_WITH_VPN: list[str] = config(
1✔
462
    "SUBSCRIPTIONS_WITH_VPN", default="", cast=Csv()
463
)
464

465
SUBSCRIPTIONS_THAT_MEGABUNDLE_PROVIDES: list[str] = config(
1✔
466
    "SUBSCRIPTIONS_THAT_MEGABUNDLE_PROVIDES", default="", cast=Csv()
467
)
468

469
MAX_ONBOARDING_AVAILABLE = config("MAX_ONBOARDING_AVAILABLE", 0, cast=int)
1✔
470
MAX_ONBOARDING_FREE_AVAILABLE = config("MAX_ONBOARDING_FREE_AVAILABLE", 3, cast=int)
1✔
471

472
MAX_ADDRESS_CREATION_PER_DAY: int = config(
1✔
473
    "MAX_ADDRESS_CREATION_PER_DAY", 100, cast=int
474
)
475
MAX_REPLIES_PER_DAY: int = config("MAX_REPLIES_PER_DAY", 100, cast=int)
1✔
476
MAX_FORWARDED_PER_DAY: int = config("MAX_FORWARDED_PER_DAY", 1000, cast=int)
1✔
477
MAX_FORWARDED_EMAIL_SIZE_PER_DAY: int = config(
1✔
478
    "MAX_FORWARDED_EMAIL_SIZE_PER_DAY", 1_000_000_000, cast=int
479
)
480
PREMIUM_FEATURE_PAUSED_DAYS: int = config(
1✔
481
    "ACCOUNT_PREMIUM_FEATURE_PAUSED_DAYS", 1, cast=int
482
)
483

484
SOFT_BOUNCE_ALLOWED_DAYS: int = config("SOFT_BOUNCE_ALLOWED_DAYS", 1, cast=int)
1✔
485
HARD_BOUNCE_ALLOWED_DAYS: int = config("HARD_BOUNCE_ALLOWED_DAYS", 30, cast=int)
1✔
486

487
WSGI_APPLICATION = "privaterelay.wsgi.application"
1✔
488

489
# Database
490
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
491

492
DATABASE_URL = config(
1✔
493
    "DATABASE_URL", default="sqlite:///{}".format(os.path.join(BASE_DIR, "db.sqlite3"))
494
)
495
DATABASES = {"default": dj_database_url.parse(DATABASE_URL)}
1✔
496
# Optionally set a test database name.
497
# This is useful for forcing an on-disk database for SQLite.
498
TEST_DB_NAME = config("TEST_DB_NAME", "")
1✔
499
if TEST_DB_NAME:
1!
500
    DATABASES["default"]["TEST"] = {"NAME": TEST_DB_NAME}
×
501

502
REDIS_URL = config("REDIS_URL", "")
1✔
503
if REDIS_URL:
1!
UNCOV
504
    CACHES = {
×
505
        "default": {
506
            "BACKEND": "django_redis.cache.RedisCache",
507
            "LOCATION": REDIS_URL,
508
            "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
509
        }
510
    }
511
    SESSION_ENGINE = "django.contrib.sessions.backends.cache"
×
512
    SESSION_CACHE_ALIAS = "default"
×
513
elif RELAY_CHANNEL == "local":
1!
514
    CACHES = {
1✔
515
        "default": {
516
            "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
517
        }
518
    }
519

520
# Password validation
521
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
522
# only needed when admin UI is enabled
523
if ADMIN_ENABLED:
1!
524
    _DJANGO_PWD_VALIDATION = "django.contrib.auth.password_validation"  # noqa: E501, S105 (long line, possible password)
×
525
    AUTH_PASSWORD_VALIDATORS = [
×
526
        {"NAME": _DJANGO_PWD_VALIDATION + ".UserAttributeSimilarityValidator"},
527
        {"NAME": _DJANGO_PWD_VALIDATION + ".MinimumLengthValidator"},
528
        {"NAME": _DJANGO_PWD_VALIDATION + ".CommonPasswordValidator"},
529
        {"NAME": _DJANGO_PWD_VALIDATION + ".NumericPasswordValidator"},
530
    ]
531

532

533
# Internationalization
534
# https://docs.djangoproject.com/en/2.2/topics/i18n/
535

536
LANGUAGE_CODE = "en"
1✔
537

538
# Mozilla l10n directories use lang-locale language codes,
539
# so we need to add those to LANGUAGES so Django's LocaleMiddleware
540
# can find them.
541
LANGUAGES = DEFAULT_LANGUAGES + [
1✔
542
    ("zh-tw", "Chinese"),
543
    ("zh-cn", "Chinese"),
544
    ("es-es", "Spanish"),
545
    ("pt-pt", "Portuguese"),
546
    ("skr", "Saraiki"),
547
]
548

549
TIME_ZONE = "UTC"
1✔
550

551
USE_I18N = True
1✔
552

553

554
USE_TZ = True
1✔
555

556
STATICFILES_DIRS = [
1✔
557
    os.path.join(BASE_DIR, "frontend/out"),
558
]
559
# Static files (the front-end in /frontend/)
560
# https://whitenoise.evans.io/en/stable/django.html#using-whitenoise-with-webpack-browserify-latest-js-thing
561
STATIC_URL = "/"
1✔
562
if DEBUG:
1!
563
    # In production, we run collectstatic to index all static files.
564
    # However, when running locally, we want to automatically pick up
565
    # all files spewed out by `npm run watch` in /frontend/out,
566
    # and we're fine with the performance impact of that.
567
    WHITENOISE_ROOT = os.path.join(BASE_DIR, "frontend/out")
1✔
568
STORAGES = {
1✔
569
    "default": {
570
        "BACKEND": "django.core.files.storage.FileSystemStorage",
571
    },
572
    "staticfiles": {
573
        "BACKEND": "privaterelay.storage.RelayStaticFilesStorage",
574
    },
575
}
576

577
# Relay does not support user-uploaded files
578
MEDIA_ROOT = None
1✔
579
MEDIA_URL = None
1✔
580

581
WHITENOISE_INDEX_FILE = True
1✔
582

583

584
# See
585
# https://whitenoise.evans.io/en/stable/django.html#WHITENOISE_ADD_HEADERS_FUNCTION
586
# Intended to ensure that the homepage does not get cached in our CDN,
587
# so that the `RedirectRootIfLoggedIn` middleware can kick in for logged-in
588
# users.
589
def set_index_cache_control_headers(
1✔
590
    headers: wsgiref.headers.Headers, path: str, url: str
591
) -> None:
592
    if DEBUG:
1!
593
        home_path = os.path.join(BASE_DIR, "frontend/out", "index.html")
1✔
594
    else:
595
        home_path = os.path.join(STATIC_ROOT, "index.html")
×
596
    if path == home_path:
1✔
597
        headers["Cache-Control"] = "no-cache, public"
1✔
598

599

600
WHITENOISE_ADD_HEADERS_FUNCTION = set_index_cache_control_headers
1✔
601

602
SITE_ID = 1
1✔
603

604
AUTHENTICATION_BACKENDS = (
1✔
605
    "django.contrib.auth.backends.ModelBackend",
606
    "allauth.account.auth_backends.AuthenticationBackend",
607
)
608

609
SOCIALACCOUNT_PROVIDERS = {
1✔
610
    "fxa": {
611
        # Note: to request "profile" scope, must be a trusted Mozilla client
612
        "SCOPE": ["profile", "https://identity.mozilla.com/account/subscriptions"],
613
        "AUTH_PARAMS": {"access_type": "offline"},
614
        "OAUTH_ENDPOINT": config(
615
            "FXA_OAUTH_ENDPOINT", "https://oauth.accounts.firefox.com/v1"
616
        ),
617
        "PROFILE_ENDPOINT": config(
618
            "FXA_PROFILE_ENDPOINT", "https://profile.accounts.firefox.com/v1"
619
        ),
620
        "VERIFIED_EMAIL": True,  # Assume FxA primary email is verified
621
    }
622
}
623

624
SOCIALACCOUNT_EMAIL_VERIFICATION = "none"
1✔
625
SOCIALACCOUNT_AUTO_SIGNUP = True
1✔
626
SOCIALACCOUNT_LOGIN_ON_GET = True
1✔
627
SOCIALACCOUNT_STORE_TOKENS = True
1✔
628

629
ACCOUNT_ADAPTER = "privaterelay.allauth.AccountAdapter"
1✔
630
ACCOUNT_PRESERVE_USERNAME_CASING = False
1✔
631

632
FXA_REQUESTS_TIMEOUT_SECONDS = config("FXA_REQUESTS_TIMEOUT_SECONDS", 1, cast=int)
1✔
633
FXA_SETTINGS_URL = config("FXA_SETTINGS_URL", f"{FXA_BASE_ORIGIN}/settings")
1✔
634
FXA_SUBSCRIPTIONS_URL = config(
1✔
635
    "FXA_SUBSCRIPTIONS_URL", f"{FXA_BASE_ORIGIN}/subscriptions"
636
)
637
# check https://mozilla.github.io/ecosystem-platform/api#tag/Subscriptions/operation/getOauthMozillasubscriptionsCustomerBillingandsubscriptions  # noqa: E501 (line too long)
638
FXA_ACCOUNTS_ENDPOINT = config(
1✔
639
    "FXA_ACCOUNTS_ENDPOINT",
640
    "https://api.accounts.firefox.com/v1",
641
)
642
FXA_SUPPORT_URL = config("FXA_SUPPORT_URL", f"{FXA_BASE_ORIGIN}/support/")
1✔
643
USE_SUBPLAT3 = config("USE_SUBPLAT3", False, cast=bool)
1✔
644
SUBPLAT3_HOST = (
1✔
645
    "https://payments.firefox.com"
646
    if FXA_BASE_ORIGIN == "https://accounts.firefox.com"
647
    else "https://payments-next.allizom.org"
648
)
649
SUBPLAT3_PREMIUM_PRODUCT_KEY = config(
1✔
650
    "SUBPLAT3_PREMIUM_PRODUCT_KEY", "relay-premium-127", cast=str
651
)
652
SUBPLAT3_PHONES_PRODUCT_KEY = config(
1✔
653
    "SUBPLAT3_PHONES_PRODUCT_KEY", "relay-premium-127-phone", cast=str
654
)
655
SUBPLAT3_BUNDLE_PRODUCT_KEY = config(
1✔
656
    "SUBPLAT3_BUNDLE_PRODUCT_KEY", "bundle-relay-vpn-dev", cast=str
657
)
658
SUBPLAT3_MEGABUNDLE_PRODUCT_KEY = config(
1✔
659
    "SUBPLAT3_MEGABUNDLE_PRODUCT_KEY", "privacyprotectionplan", cast=str
660
)
661

662
LOGGING = {
1✔
663
    "version": 1,
664
    "filters": {
665
        "request_id": {
666
            "()": "dockerflow.logging.RequestIdLogFilter",
667
        },
668
    },
669
    "formatters": {
670
        "json": {
671
            "()": "dockerflow.logging.JsonLogFormatter",
672
            "logger_name": "fx-private-relay",
673
        }
674
    },
675
    "handlers": {
676
        "console_out": {
677
            "level": "DEBUG",
678
            "class": "logging.StreamHandler",
679
            "stream": sys.stdout,
680
            "formatter": "json",
681
            "filters": ["request_id"],
682
        },
683
        "console_err": {
684
            "level": "DEBUG",
685
            "class": "logging.StreamHandler",
686
            "formatter": "json",
687
            "filters": ["request_id"],
688
        },
689
    },
690
    "loggers": {
691
        "root": {
692
            "handlers": ["console_err"],
693
            "level": "WARNING",
694
        },
695
        "request.summary": {
696
            "handlers": ["console_out"],
697
            "level": "DEBUG",
698
            # pytest's caplog fixture requires propagate=True
699
            # outside of pytest, use propagate=False to avoid double logs
700
            "propagate": IN_PYTEST,
701
        },
702
        "events": {
703
            "handlers": ["console_err"],
704
            "level": "WARNING",
705
            "propagate": IN_PYTEST,
706
        },
707
        "eventsinfo": {
708
            "handlers": ["console_out"],
709
            "level": "INFO",
710
            "propagate": IN_PYTEST,
711
        },
712
        "abusemetrics": {
713
            "handlers": ["console_out"],
714
            "level": "INFO",
715
            "propagate": IN_PYTEST,
716
        },
717
        "studymetrics": {
718
            "handlers": ["console_out"],
719
            "level": "INFO",
720
            "propagate": IN_PYTEST,
721
        },
722
        "markus": {
723
            "handlers": ["console_out"],
724
            "level": "DEBUG",
725
            "propagate": IN_PYTEST,
726
        },
727
        GLEAN_EVENT_MOZLOG_TYPE: {
728
            "handlers": ["console_out"],
729
            "level": "DEBUG",
730
            "propagate": IN_PYTEST,
731
        },
732
        "dockerflow": {
733
            "handlers": ["console_err"],
734
            "level": "WARNING",
735
            "propagate": IN_PYTEST,
736
        },
737
    },
738
}
739

740
DRF_RENDERERS = ["rest_framework.renderers.JSONRenderer"]
1✔
741
if DEBUG and not IN_PYTEST:
1!
742
    DRF_RENDERERS += [
×
743
        "rest_framework.renderers.BrowsableAPIRenderer",
744
    ]
745

746
FIRST_EMAIL_RATE_LIMIT = config("FIRST_EMAIL_RATE_LIMIT", "5/minute")
1✔
747
if IN_PYTEST or RELAY_CHANNEL in ["local", "dev"]:
1!
748
    FIRST_EMAIL_RATE_LIMIT = "1000/minute"
1✔
749

750
REST_FRAMEWORK = {
1✔
751
    "DEFAULT_AUTHENTICATION_CLASSES": [
752
        "api.authentication.FxaTokenAuthentication",
753
        "rest_framework.authentication.TokenAuthentication",
754
        "rest_framework.authentication.SessionAuthentication",
755
    ],
756
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
757
    "DEFAULT_RENDERER_CLASSES": DRF_RENDERERS,
758
    "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
759
    "EXCEPTION_HANDLER": "api.views.relay_exception_handler",
760
}
761
if API_DOCS_ENABLED:
1!
762
    REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema"
1✔
763

764
SPECTACULAR_SETTINGS = {
1✔
765
    "SWAGGER_UI_DIST": "SIDECAR",
766
    "SWAGGER_UI_FAVICON_HREF": "SIDECAR",
767
    "REDOC_DIST": "SIDECAR",
768
    "TITLE": "Firefox Relay API",
769
    "DESCRIPTION": (
770
        "Keep your email safe from hackers and trackers. This API is built with"
771
        " Django REST Framework and powers the Relay website UI, add-on,"
772
        " Firefox browser, and 3rd-party app integrations."
773
    ),
774
    "VERSION": "1.0",
775
    "SERVE_INCLUDE_SCHEMA": False,
776
    "PREPROCESSING_HOOKS": ["api.schema.preprocess_ignore_deprecated_paths"],
777
    "SORT_OPERATIONS": "api.schema.sort_by_tag",
778
}
779

780
if IN_PYTEST or RELAY_CHANNEL in ["local", "dev"]:
1!
781
    _DEFAULT_PHONE_RATE_LIMIT = "1000/minute"
1✔
782
else:
783
    _DEFAULT_PHONE_RATE_LIMIT = "5/minute"
×
784
PHONE_RATE_LIMIT = config("PHONE_RATE_LIMIT", _DEFAULT_PHONE_RATE_LIMIT)
1✔
785

786
# Turn on logging out on GET in development.
787
# This allows `/mock/logout/` in the front-end to clear the
788
# session cookie. Without this, after switching accounts in dev mode,
789
# then logging out again, API requests continue succeeding even without
790
# an auth token:
791
ACCOUNT_LOGOUT_ON_GET = DEBUG
1✔
792

793
# TODO: introduce an environment variable to control CORS_ALLOWED_ORIGINS
794
# https://mozilla-hub.atlassian.net/browse/MPP-3468
795
CORS_URLS_REGEX = r"^/api/"
1✔
796
CORS_ALLOWED_ORIGINS = [
1✔
797
    "https://vault.bitwarden.com",
798
    "https://vault.bitwarden.eu",
799
]
800
if RELAY_CHANNEL in ["dev", "stage"]:
1!
801
    CORS_ALLOWED_ORIGINS += [
×
802
        "https://vault.qa.bitwarden.pw",
803
        "https://vault.euqa.bitwarden.pw",
804
    ]
805
# Allow origins for each environment to help debug cors headers
806
if RELAY_CHANNEL == "local":
1!
807
    # In local dev, next runs on localhost and makes requests to /accounts/
808
    CORS_ALLOWED_ORIGINS += [
1✔
809
        "http://localhost:3000",
810
        "http://0.0.0.0:3000",
811
        "http://127.0.0.1:8000",
812
    ]
813
    CORS_URLS_REGEX = r"^/(api|accounts)/"
1✔
814
if RELAY_CHANNEL == "dev":
1!
815
    CORS_ALLOWED_ORIGINS += [
×
816
        "https://dev.relay.nonprod.webservices.mozgcp.net",
817
        "https://relay-dev.allizom.org",
818
    ]
819
if RELAY_CHANNEL == "stage":
1!
820
    CORS_ALLOWED_ORIGINS += [
×
821
        # GCP v1
822
        "https://stage.fxprivaterelay.nonprod.cloudops.mozgcp.net",
823
        # GCP v2
824
        "https://stage.relay.nonprod.webservices.mozgcp.net",
825
        "https://relay.allizom.org",
826
    ]
827

828
CSRF_TRUSTED_ORIGINS = []
1✔
829
if RELAY_CHANNEL == "local":
1!
830
    # In local development, the React UI can be served up from a different server
831
    # that needs to be allowed to make requests.
832
    # In production, the frontend is served by Django, is therefore on the same
833
    # origin and thus has access to the same cookies.
834
    CORS_ALLOW_CREDENTIALS = True
1✔
835
    SESSION_COOKIE_SAMESITE = None
1✔
836
    CSRF_TRUSTED_ORIGINS += [
1✔
837
        "http://localhost:3000",
838
        "http://0.0.0.0:3000",
839
    ]
840

841
SENTRY_RELEASE = config("SENTRY_RELEASE", "")
1✔
842
GIT_BRANCH = config("GIT_BRANCH", "")
1✔
843
GIT_SHA = config("GIT_SHA", "")
1✔
844
GIT_TAG = config("GIT_TAG", "")
1✔
845
CIRCLE_SHA1 = config("CIRCLE_SHA1", "")
1✔
846
CIRCLE_TAG = config("CIRCLE_TAG", "")
1✔
847
CIRCLE_BRANCH = config("CIRCLE_BRANCH", "")
1✔
848

849
sentry_release: str | None = None
1✔
850
if SENTRY_RELEASE:
1!
851
    sentry_release = SENTRY_RELEASE
×
852
elif GIT_TAG and GIT_TAG != "unknown":
1!
853
    sentry_release = GIT_TAG
×
854
elif GIT_SHA and GIT_SHA != "unknown" and GIT_BRANCH and GIT_BRANCH != "unknown":
1!
855
    sentry_release = f"{GIT_BRANCH}:{GIT_SHA}"
×
856
elif CIRCLE_TAG and CIRCLE_TAG != "unknown":
1!
857
    sentry_release = CIRCLE_TAG
×
858
elif (
1!
859
    CIRCLE_SHA1
860
    and CIRCLE_SHA1 != "unknown"
861
    and CIRCLE_BRANCH
862
    and CIRCLE_BRANCH != "unknown"
863
):
864
    sentry_release = f"{CIRCLE_BRANCH}:{CIRCLE_SHA1}"
1✔
865

866
SENTRY_DEBUG = config("SENTRY_DEBUG", DEBUG, cast=bool)
1✔
867

868
SENTRY_ENVIRONMENT = config("SENTRY_ENVIRONMENT", RELAY_CHANNEL)
1✔
869
# Use "local" as default rather than "prod", to catch ngrok.io URLs
870
if SENTRY_ENVIRONMENT == "prod" and SITE_ORIGIN != "https://relay.firefox.com":
1!
871
    SENTRY_ENVIRONMENT = "local"
×
872

873
sentry_sdk.init(
1✔
874
    dsn=config("SENTRY_DSN", None),
875
    integrations=[DjangoIntegration(cache_spans=not DEBUG)],
876
    debug=SENTRY_DEBUG,
877
    include_local_variables=DEBUG,
878
    release=sentry_release,
879
    environment=SENTRY_ENVIRONMENT,
880
)
881
# Duplicates events for unhandled exceptions, but without useful tracebacks
882
ignore_logger("request.summary")
1✔
883
# Security scanner attempts, no action required
884
# Can be re-enabled when hostname allow list implemented at the load balancer
885
ignore_logger("django.security.DisallowedHost")
1✔
886
# Fluent errors, mostly when a translation is unavailable for the locale.
887
# It is more effective to process these from logs using BigQuery than to track
888
# as events in Sentry.
889
ignore_logger("django_ftl.message_errors")
1✔
890

891
if USE_SILK:
1!
892
    SILKY_PYTHON_PROFILER = True
×
893
    SILKY_PYTHON_PROFILER_BINARY = True
×
894
    SILKY_PYTHON_PROFILER_RESULT_PATH = ".silk-profiler"
×
895

896
# Settings for manage.py process_emails_from_sqs
897
PROCESS_EMAIL_BATCH_SIZE = config(
1✔
898
    "PROCESS_EMAIL_BATCH_SIZE", 10, cast=Choices(range(1, 11), cast=int)
899
)
900
PROCESS_EMAIL_DELETE_FAILED_MESSAGES = config(
1✔
901
    "PROCESS_EMAIL_DELETE_FAILED_MESSAGES", False, cast=bool
902
)
903
PROCESS_EMAIL_HEALTHCHECK_PATH = config(
1✔
904
    "PROCESS_EMAIL_HEALTHCHECK_PATH", os.path.join(TMP_DIR, "healthcheck.json")
905
)
906
PROCESS_EMAIL_MAX_SECONDS = config("PROCESS_EMAIL_MAX_SECONDS", 0, cast=int) or None
1✔
907
PROCESS_EMAIL_VERBOSITY = config(
1✔
908
    "PROCESS_EMAIL_VERBOSITY", 1, cast=Choices(range(0, 4), cast=int)
909
)
910
PROCESS_EMAIL_VISIBILITY_SECONDS = config(
1✔
911
    "PROCESS_EMAIL_VISIBILITY_SECONDS", 120, cast=int
912
)
913
PROCESS_EMAIL_WAIT_SECONDS = config("PROCESS_EMAIL_WAIT_SECONDS", 5, cast=int)
1✔
914
PROCESS_EMAIL_HEALTHCHECK_MAX_AGE = config(
1✔
915
    "PROCESS_EMAIL_HEALTHCHECK_MAX_AGE", 120, cast=int
916
)
917
PROCESS_EMAIL_MAX_SECONDS_PER_MESSAGE = config(
1✔
918
    "PROCESS_EMAIL_MAX_SECONDS_PER_MESSAGE",
919
    PROCESS_EMAIL_MAX_SECONDS or 120.0,
920
    cast=float,
921
)
922

923
# Django 3.2 switches default to BigAutoField
924
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
1✔
925

926
# python-dockerflow settings
927
DOCKERFLOW_VERSION_CALLBACK = "privaterelay.utils.get_version_info"
1✔
928
DOCKERFLOW_CHECKS = [
1✔
929
    "dockerflow.django.checks.check_database_connected",
930
    "dockerflow.django.checks.check_migrations_applied",
931
]
932
if REDIS_URL:
1!
933
    DOCKERFLOW_CHECKS.append("dockerflow.django.checks.check_redis_connected")
×
934
DOCKERFLOW_REQUEST_ID_HEADER_NAME = config("DOCKERFLOW_REQUEST_ID_HEADER_NAME", None)
1✔
935
SILENCED_SYSTEM_CHECKS = sorted(
1✔
936
    set(config("DJANGO_SILENCED_SYSTEM_CHECKS", default="", cast=Csv()))
937
    | {
938
        # (models.W040) SQLite does not support indexes with non-key columns.
939
        # RelayAddress index idx_ra_created_by_addon uses this for PostgreSQL.
940
        "models.W040",
941
    }
942
)
943

944
# django-ftl settings
945
AUTO_RELOAD_BUNDLES = False  # Requires pyinotify
1✔
946

947
# accounts that should not have abuse metrics
948
ALLOWED_ACCOUNTS = ["relay-team+e2e@mozilla.com"]
1✔
949

950
# settings for kinto / remote settings
951
REMOTE_SETTINGS_SERVER = config("REMOTE_SETTINGS_SERVER", "", str)
1✔
952
REMOTE_SETTINGS_AUTH = config("REMOTE_SETTINGS_AUTH", "", str)
1✔
953
REMOTE_SETTINGS_BUCKET = config("REMOTE_SETTINGS_BUCKET", "", str)
1✔
954
REMOTE_SETTINGS_COLLECTION = config("REMOTE_SETTINGS_COLLECTION", "", str)
1✔
955
ALLOWLIST_INPUT_URL = config("ALLOWLIST_INPUT_URL", "", str)
1✔
956

957
# Patching for django-types
958
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