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

mozilla / fx-private-relay / 44ade867-362d-4592-a02f-6c43abe38ed0

06 May 2024 05:14PM CUT coverage: 84.618% (-0.01%) from 84.628%
44ade867-362d-4592-a02f-6c43abe38ed0

push

circleci

web-flow
Merge pull request #4676 from mozilla/dependabot/pip/glean-parser-14.1.1

Bump glean-parser from 14.0.1 to 14.1.1

3526 of 4578 branches covered (77.02%)

Branch coverage included in aggregate %.

14672 of 16928 relevant lines covered (86.67%)

10.91 hits per line

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

77.91
/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 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 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
1✔
42

43
    assert silk  # Suppress "imported but unused" warning
×
44

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

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

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

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

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

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

83
DEBUG = config("DEBUG", False, cast=bool)
1✔
84
if DEBUG:
1!
85
    INTERNAL_IPS = config("DJANGO_INTERNAL_IPS", default="", cast=Csv())
1✔
86
IN_PYTEST: bool = "pytest" in sys.modules
1✔
87
USE_SILK = DEBUG and HAS_SILK and not IN_PYTEST
1✔
88

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

107
#
108
# Setup CSP
109
#
110

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

113
# maps FxA / Mozilla account profile hosts to respective hosts for CSP
114
FXA_BASE_ORIGIN: str = config("FXA_BASE_ORIGIN", "https://accounts.firefox.com")
1✔
115
if FXA_BASE_ORIGIN == "https://accounts.firefox.com":
1!
116
    _AVATAR_IMG_SRC = [
×
117
        "firefoxusercontent.com",
118
        "https://profile.accounts.firefox.com",
119
    ]
120
    _ACCOUNT_CONNECT_SRC = [FXA_BASE_ORIGIN]
×
121
else:
122
    assert FXA_BASE_ORIGIN == "https://accounts.stage.mozaws.net"
1✔
123
    _AVATAR_IMG_SRC = [
1✔
124
        "mozillausercontent.com",
125
        "https://profile.stage.mozaws.net",
126
    ]
127
    _ACCOUNT_CONNECT_SRC = [
1✔
128
        FXA_BASE_ORIGIN,
129
        # fxaFlowTracker.ts will try this if runtimeData is slow
130
        "https://accounts.firefox.com",
131
    ]
132

133
API_DOCS_ENABLED = config("API_DOCS_ENABLED", False, cast=bool) or DEBUG
1✔
134
_CSP_SCRIPT_INLINE = API_DOCS_ENABLED or USE_SILK
1✔
135

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

140
if API_DOCS_ENABLED:
1!
141
    _API_DOCS_CSP_IMG_SRC = ["data:", "https://cdn.redoc.ly"]
1✔
142
    _API_DOCS_CSP_STYLE_SRC = ["https://fonts.googleapis.com"]
1✔
143
    _API_DOCS_CSP_FONT_SRC = ["https://fonts.gstatic.com"]
1✔
144
    _API_DOCS_CSP_WORKER_SRC = ["blob:"]
1✔
145
else:
146
    _API_DOCS_CSP_IMG_SRC = []
×
147
    _API_DOCS_CSP_STYLE_SRC = []
×
148
    _API_DOCS_CSP_FONT_SRC = []
×
149
    _API_DOCS_CSP_WORKER_SRC = []
×
150

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

172
    # Add the hash for an empty string (sha256-47DEQp...)
173
    # next,js injects an empty style element and then adds the content.
174
    # This hash avoids a spurious CSP error.
175
    empty_hash = base64.b64encode(sha256().digest()).decode()
×
176
    _CSP_STYLE_HASHES.append(f"'sha256-{empty_hash}'")
×
177

178
CSP_DEFAULT_SRC = ["'self'"]
1✔
179
CSP_CONNECT_SRC = [
1✔
180
    "'self'",
181
    "https://www.google-analytics.com/",
182
    "https://location.services.mozilla.com",
183
    "https://api.stripe.com",
184
    BASKET_ORIGIN,
185
] + _ACCOUNT_CONNECT_SRC
186
CSP_FONT_SRC = ["'self'"] + _API_DOCS_CSP_FONT_SRC + ["https://relay.firefox.com/"]
1✔
187
CSP_IMG_SRC = ["'self'"] + _AVATAR_IMG_SRC + _API_DOCS_CSP_IMG_SRC
1✔
188
CSP_SCRIPT_SRC = (
1✔
189
    ["'self'"]
190
    + (["'unsafe-inline'"] if _CSP_SCRIPT_INLINE else [])
191
    + [
192
        "https://www.google-analytics.com/",
193
        "https://js.stripe.com/",
194
    ]
195
)
196
CSP_WORKER_SRC = _API_DOCS_CSP_WORKER_SRC or None
1✔
197
CSP_OBJECT_SRC = ["'none'"]
1✔
198
CSP_FRAME_SRC = ["https://js.stripe.com", "https://hooks.stripe.com"]
1✔
199
CSP_STYLE_SRC = (
1✔
200
    ["'self'"]
201
    + (["'unsafe-inline'"] if _CSP_STYLE_INLINE else [])
202
    + _API_DOCS_CSP_STYLE_SRC
203
    + _CSP_STYLE_HASHES
204
)
205
CSP_REPORT_URI = config("CSP_REPORT_URI", "")
1✔
206

207
REFERRER_POLICY = "strict-origin-when-cross-origin"
1✔
208

209
ALLOWED_HOSTS: list[str] = []
1✔
210
DJANGO_ALLOWED_HOSTS = config("DJANGO_ALLOWED_HOST", "", cast=Csv())
1✔
211
if DJANGO_ALLOWED_HOSTS:
1!
212
    ALLOWED_HOSTS += DJANGO_ALLOWED_HOSTS
×
213
DJANGO_ALLOWED_SUBNET = config("DJANGO_ALLOWED_SUBNET", None)
1✔
214
if DJANGO_ALLOWED_SUBNET:
1!
215
    ALLOWED_HOSTS += [str(ip) for ip in ipaddress.IPv4Network(DJANGO_ALLOWED_SUBNET)]
×
216

217

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

221

222
AWS_REGION: str | None = config("AWS_REGION", None)
1✔
223
AWS_ACCESS_KEY_ID = config("AWS_ACCESS_KEY_ID", None)
1✔
224
AWS_SECRET_ACCESS_KEY = config("AWS_SECRET_ACCESS_KEY", None)
1✔
225
AWS_SNS_TOPIC = set(config("AWS_SNS_TOPIC", "", cast=Csv()))
1✔
226
AWS_SNS_KEY_CACHE = config("AWS_SNS_KEY_CACHE", "default")
1✔
227
AWS_SES_CONFIGSET: str | None = config("AWS_SES_CONFIGSET", None)
1✔
228
AWS_SQS_EMAIL_QUEUE_URL = config("AWS_SQS_EMAIL_QUEUE_URL", None)
1✔
229
AWS_SQS_EMAIL_DLQ_URL = config("AWS_SQS_EMAIL_DLQ_URL", None)
1✔
230

231
# Dead-Letter Queue (DLQ) for SNS push subscription
232
AWS_SQS_QUEUE_URL = config("AWS_SQS_QUEUE_URL", None)
1✔
233

234
RELAY_FROM_ADDRESS: str | None = config("RELAY_FROM_ADDRESS", None)
1✔
235
GOOGLE_ANALYTICS_ID = config("GOOGLE_ANALYTICS_ID", None)
1✔
236
GOOGLE_APPLICATION_CREDENTIALS: str = config("GOOGLE_APPLICATION_CREDENTIALS", "")
1✔
237
GOOGLE_CLOUD_PROFILER_CREDENTIALS_B64: str = config(
1✔
238
    "GOOGLE_CLOUD_PROFILER_CREDENTIALS_B64", ""
239
)
240
INCLUDE_VPN_BANNER = config("INCLUDE_VPN_BANNER", False, cast=bool)
1✔
241
RECRUITMENT_BANNER_LINK = config("RECRUITMENT_BANNER_LINK", None)
1✔
242
RECRUITMENT_BANNER_TEXT = config("RECRUITMENT_BANNER_TEXT", None)
1✔
243
RECRUITMENT_EMAIL_BANNER_TEXT = config("RECRUITMENT_EMAIL_BANNER_TEXT", None)
1✔
244
RECRUITMENT_EMAIL_BANNER_LINK = config("RECRUITMENT_EMAIL_BANNER_LINK", None)
1✔
245

246
PHONES_ENABLED: bool = config("PHONES_ENABLED", False, cast=bool)
1✔
247
PHONES_NO_CLIENT_CALLS_IN_TEST = False  # Override in tests that do not test clients
1✔
248
TWILIO_ACCOUNT_SID: str | None = config("TWILIO_ACCOUNT_SID", None)
1✔
249
TWILIO_AUTH_TOKEN: str | None = config("TWILIO_AUTH_TOKEN", None)
1✔
250
TWILIO_MAIN_NUMBER: str | None = config("TWILIO_MAIN_NUMBER", None)
1✔
251
TWILIO_SMS_APPLICATION_SID: str | None = config("TWILIO_SMS_APPLICATION_SID", None)
1✔
252
TWILIO_MESSAGING_SERVICE_SID: list[str] = config(
1✔
253
    "TWILIO_MESSAGING_SERVICE_SID", "", cast=Csv()
254
)
255
TWILIO_TEST_ACCOUNT_SID: str | None = config("TWILIO_TEST_ACCOUNT_SID", None)
1✔
256
TWILIO_TEST_AUTH_TOKEN: str | None = config("TWILIO_TEST_AUTH_TOKEN", None)
1✔
257
TWILIO_ALLOWED_COUNTRY_CODES = {
1✔
258
    code.upper() for code in config("TWILIO_ALLOWED_COUNTRY_CODES", "US,CA", cast=Csv())
259
}
260
MAX_MINUTES_TO_VERIFY_REAL_PHONE: int = config(
1✔
261
    "MAX_MINUTES_TO_VERIFY_REAL_PHONE", 5, cast=int
262
)
263
MAX_TEXTS_PER_BILLING_CYCLE: int = config("MAX_TEXTS_PER_BILLING_CYCLE", 75, cast=int)
1✔
264
MAX_MINUTES_PER_BILLING_CYCLE: int = config(
1✔
265
    "MAX_MINUTES_PER_BILLING_CYCLE", 50, cast=int
266
)
267
DAYS_PER_BILLING_CYCLE = config("DAYS_PER_BILLING_CYCLE", 30, cast=int)
1✔
268
MAX_DAYS_IN_MONTH = 31
1✔
269
IQ_ENABLED = config("IQ_ENABLED", False, cast=bool)
1✔
270
IQ_FOR_VERIFICATION: bool = config("IQ_FOR_VERIFICATION", False, cast=bool)
1✔
271
IQ_FOR_NEW_NUMBERS = config("IQ_FOR_NEW_NUMBERS", False, cast=bool)
1✔
272
IQ_MAIN_NUMBER: str = config("IQ_MAIN_NUMBER", "")
1✔
273
IQ_OUTBOUND_API_KEY: str = config("IQ_OUTBOUND_API_KEY", "")
1✔
274
IQ_INBOUND_API_KEY = config("IQ_INBOUND_API_KEY", "")
1✔
275
IQ_MESSAGE_API_ORIGIN = config(
1✔
276
    "IQ_MESSAGE_API_ORIGIN", "https://messagebroker.inteliquent.com"
277
)
278
IQ_MESSAGE_PATH = "/msgbroker/rest/publishMessages"
1✔
279
IQ_PUBLISH_MESSAGE_URL: str = f"{IQ_MESSAGE_API_ORIGIN}{IQ_MESSAGE_PATH}"
1✔
280

281
DJANGO_STATSD_ENABLED = config("DJANGO_STATSD_ENABLED", False, cast=bool)
1✔
282
STATSD_DEBUG = config("STATSD_DEBUG", False, cast=bool)
1✔
283
STATSD_ENABLED: bool = DJANGO_STATSD_ENABLED or STATSD_DEBUG
1✔
284
STATSD_HOST = config("DJANGO_STATSD_HOST", "127.0.0.1")
1✔
285
STATSD_PORT = config("DJANGO_STATSD_PORT", "8125")
1✔
286
STATSD_PREFIX = config("DJANGO_STATSD_PREFIX", "fx.private.relay")
1✔
287

288
SERVE_ADDON = config("SERVE_ADDON", None)
1✔
289

290
# Application definition
291
INSTALLED_APPS = [
1✔
292
    "whitenoise.runserver_nostatic",
293
    "django.contrib.staticfiles",
294
    "django.contrib.auth",
295
    "django.contrib.contenttypes",
296
    "django.contrib.sessions",
297
    "django.contrib.messages",
298
    "django.contrib.sites",
299
    "django_filters",
300
    "django_ftl.apps.DjangoFtlConfig",
301
    "dockerflow.django",
302
    "allauth",
303
    "allauth.account",
304
    "allauth.socialaccount",
305
    "allauth.socialaccount.providers.fxa",
306
    "rest_framework",
307
    "rest_framework.authtoken",
308
    "corsheaders",
309
    "waffle",
310
    "privaterelay.apps.PrivateRelayConfig",
311
    "api.apps.ApiConfig",
312
]
313

314
if API_DOCS_ENABLED:
1!
315
    INSTALLED_APPS += [
1✔
316
        "drf_spectacular",
317
        "drf_spectacular_sidecar",
318
    ]
319

320
if DEBUG:
1!
321
    INSTALLED_APPS += [
1✔
322
        "debug_toolbar",
323
    ]
324

325
if USE_SILK:
1!
326
    INSTALLED_APPS.append("silk")
×
327

328
if ADMIN_ENABLED:
1!
329
    INSTALLED_APPS += [
×
330
        "django.contrib.admin",
331
    ]
332

333
if AWS_SES_CONFIGSET and AWS_SNS_TOPIC:
1!
334
    INSTALLED_APPS += [
1✔
335
        "emails.apps.EmailsConfig",
336
    ]
337

338
if PHONES_ENABLED:
1!
339
    INSTALLED_APPS += [
1✔
340
        "phones.apps.PhonesConfig",
341
    ]
342

343

344
# statsd middleware has to be first to catch errors in everything else
345
def _get_initial_middleware() -> list[str]:
1✔
346
    if STATSD_ENABLED:
1!
347
        return [
×
348
            "privaterelay.middleware.ResponseMetrics",
349
        ]
350
    return []
1✔
351

352

353
MIDDLEWARE = _get_initial_middleware()
1✔
354

355
if USE_SILK:
1!
356
    MIDDLEWARE.append("silk.middleware.SilkyMiddleware")
×
357
if DEBUG:
1!
358
    MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
1✔
359

360
MIDDLEWARE += [
1✔
361
    "django.middleware.security.SecurityMiddleware",
362
    "csp.middleware.CSPMiddleware",
363
    "privaterelay.middleware.RedirectRootIfLoggedIn",
364
    "privaterelay.middleware.RelayStaticFilesMiddleware",
365
    "django.contrib.sessions.middleware.SessionMiddleware",
366
    "corsheaders.middleware.CorsMiddleware",
367
    "django.middleware.common.CommonMiddleware",
368
    "django.middleware.csrf.CsrfViewMiddleware",
369
    "django.contrib.auth.middleware.AuthenticationMiddleware",
370
    "django.contrib.messages.middleware.MessageMiddleware",
371
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
372
    "django.middleware.locale.LocaleMiddleware",
373
    "allauth.account.middleware.AccountMiddleware",
374
    "django_ftl.middleware.activate_from_request_language_code",
375
    "django_referrer_policy.middleware.ReferrerPolicyMiddleware",
376
    "dockerflow.django.middleware.DockerflowMiddleware",
377
    "waffle.middleware.WaffleMiddleware",
378
    "privaterelay.middleware.AddDetectedCountryToRequestAndResponseHeaders",
379
    "privaterelay.middleware.StoreFirstVisit",
380
    "google.cloud.sqlcommenter.django.middleware.SqlCommenter",
381
]
382

383
ROOT_URLCONF = "privaterelay.urls"
1✔
384

385
TEMPLATES = [
1✔
386
    {
387
        "BACKEND": "django.template.backends.django.DjangoTemplates",
388
        "DIRS": [
389
            os.path.join(BASE_DIR, "privaterelay", "templates"),
390
        ],
391
        "APP_DIRS": True,
392
        "OPTIONS": {
393
            "context_processors": [
394
                "django.template.context_processors.debug",
395
                "django.template.context_processors.request",
396
                "django.contrib.auth.context_processors.auth",
397
                "django.contrib.messages.context_processors.messages",
398
            ],
399
        },
400
    },
401
]
402

403
RELAY_FIREFOX_DOMAIN: str = config("RELAY_FIREFOX_DOMAIN", "relay.firefox.com")
1✔
404
MOZMAIL_DOMAIN: str = config("MOZMAIL_DOMAIN", "mozmail.com")
1✔
405
MAX_NUM_FREE_ALIASES: int = config("MAX_NUM_FREE_ALIASES", 5, cast=int)
1✔
406
PERIODICAL_PREMIUM_PROD_ID: str = config("PERIODICAL_PREMIUM_PROD_ID", "")
1✔
407
PREMIUM_PLAN_ID_US_MONTHLY: str = config(
1✔
408
    "PREMIUM_PLAN_ID_US_MONTHLY", "price_1LXUcnJNcmPzuWtRpbNOajYS"
409
)
410
PREMIUM_PLAN_ID_US_YEARLY: str = config(
1✔
411
    "PREMIUM_PLAN_ID_US_YEARLY", "price_1LXUdlJNcmPzuWtRKTYg7mpZ"
412
)
413
PHONE_PROD_ID = config("PHONE_PROD_ID", "")
1✔
414
PHONE_PLAN_ID_US_MONTHLY: str = config(
1✔
415
    "PHONE_PLAN_ID_US_MONTHLY", "price_1Li0w8JNcmPzuWtR2rGU80P3"
416
)
417
PHONE_PLAN_ID_US_YEARLY: str = config(
1✔
418
    "PHONE_PLAN_ID_US_YEARLY", "price_1Li15WJNcmPzuWtRIh0F4VwP"
419
)
420
BUNDLE_PROD_ID = config("BUNDLE_PROD_ID", "")
1✔
421
BUNDLE_PLAN_ID_US: str = config("BUNDLE_PLAN_ID_US", "price_1LwoSDJNcmPzuWtR6wPJZeoh")
1✔
422

423
SUBSCRIPTIONS_WITH_UNLIMITED: list[str] = config(
1✔
424
    "SUBSCRIPTIONS_WITH_UNLIMITED", default="", cast=Csv()
425
)
426
SUBSCRIPTIONS_WITH_PHONE: list[str] = config(
1✔
427
    "SUBSCRIPTIONS_WITH_PHONE", default="", cast=Csv()
428
)
429
SUBSCRIPTIONS_WITH_VPN: list[str] = config(
1✔
430
    "SUBSCRIPTIONS_WITH_VPN", default="", cast=Csv()
431
)
432

433
MAX_ONBOARDING_AVAILABLE = config("MAX_ONBOARDING_AVAILABLE", 0, cast=int)
1✔
434
MAX_ONBOARDING_FREE_AVAILABLE = config("MAX_ONBOARDING_FREE_AVAILABLE", 3, cast=int)
1✔
435

436
MAX_ADDRESS_CREATION_PER_DAY = config("MAX_ADDRESS_CREATION_PER_DAY", 100, cast=int)
1✔
437
MAX_REPLIES_PER_DAY = config("MAX_REPLIES_PER_DAY", 100, cast=int)
1✔
438
MAX_FORWARDED_PER_DAY = config("MAX_FORWARDED_PER_DAY", 1000, cast=int)
1✔
439
MAX_FORWARDED_EMAIL_SIZE_PER_DAY = config(
1✔
440
    "MAX_FORWARDED_EMAIL_SIZE_PER_DAY", 1_000_000_000, cast=int
441
)
442
PREMIUM_FEATURE_PAUSED_DAYS: int = config(
1✔
443
    "ACCOUNT_PREMIUM_FEATURE_PAUSED_DAYS", 1, cast=int
444
)
445

446
SOFT_BOUNCE_ALLOWED_DAYS: int = config("SOFT_BOUNCE_ALLOWED_DAYS", 1, cast=int)
1✔
447
HARD_BOUNCE_ALLOWED_DAYS: int = config("HARD_BOUNCE_ALLOWED_DAYS", 30, cast=int)
1✔
448

449
WSGI_APPLICATION = "privaterelay.wsgi.application"
1✔
450

451
# Database
452
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
453

454
DATABASES = {
1✔
455
    "default": dj_database_url.config(
456
        default="sqlite:///{}".format(os.path.join(BASE_DIR, "db.sqlite3"))
457
    )
458
}
459
# Optionally set a test database name.
460
# This is useful for forcing an on-disk database for SQLite.
461
TEST_DB_NAME = config("TEST_DB_NAME", "")
1✔
462
if TEST_DB_NAME:
1!
463
    DATABASES["default"]["TEST"] = {"NAME": TEST_DB_NAME}
×
464

465
REDIS_URL = config("REDIS_URL", "")
1✔
466
if REDIS_URL:
1!
467
    CACHES = {
×
468
        "default": {
469
            "BACKEND": "django_redis.cache.RedisCache",
470
            "LOCATION": REDIS_URL,
471
            "OPTIONS": {
472
                "CLIENT_CLASS": "django_redis.client.DefaultClient",
473
            },
474
        }
475
    }
476
    SESSION_ENGINE = "django.contrib.sessions.backends.cache"
×
477
    SESSION_CACHE_ALIAS = "default"
×
478
elif RELAY_CHANNEL == "local":
1!
479
    CACHES = {
1✔
480
        "default": {
481
            "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
482
        }
483
    }
484

485
# Password validation
486
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
487
# only needed when admin UI is enabled
488
if ADMIN_ENABLED:
1!
489
    _DJANGO_PWD_VALIDATION = "django.contrib.auth.password_validation"  # noqa: E501, S105 (long line, possible password)
×
490
    AUTH_PASSWORD_VALIDATORS = [
×
491
        {"NAME": _DJANGO_PWD_VALIDATION + ".UserAttributeSimilarityValidator"},
492
        {"NAME": _DJANGO_PWD_VALIDATION + ".MinimumLengthValidator"},
493
        {"NAME": _DJANGO_PWD_VALIDATION + ".CommonPasswordValidator"},
494
        {"NAME": _DJANGO_PWD_VALIDATION + ".NumericPasswordValidator"},
495
    ]
496

497

498
# Internationalization
499
# https://docs.djangoproject.com/en/2.2/topics/i18n/
500

501
LANGUAGE_CODE = "en"
1✔
502

503
# Mozilla l10n directories use lang-locale language codes,
504
# so we need to add those to LANGUAGES so Django's LocaleMiddleware
505
# can find them.
506
LANGUAGES = DEFAULT_LANGUAGES + [
1✔
507
    ("zh-tw", "Chinese"),
508
    ("zh-cn", "Chinese"),
509
    ("es-es", "Spanish"),
510
    ("pt-pt", "Portuguese"),
511
    ("skr", "Saraiki"),
512
]
513

514
TIME_ZONE = "UTC"
1✔
515

516
USE_I18N = True
1✔
517

518

519
USE_TZ = True
1✔
520

521
STATICFILES_DIRS = [
1✔
522
    os.path.join(BASE_DIR, "frontend/out"),
523
]
524
# Static files (the front-end in /frontend/)
525
# https://whitenoise.evans.io/en/stable/django.html#using-whitenoise-with-webpack-browserify-latest-js-thing
526
STATIC_URL = "/"
1✔
527
if DEBUG:
1!
528
    # In production, we run collectstatic to index all static files.
529
    # However, when running locally, we want to automatically pick up
530
    # all files spewed out by `npm run watch` in /frontend/out,
531
    # and we're fine with the performance impact of that.
532
    WHITENOISE_ROOT = os.path.join(BASE_DIR, "frontend/out")
1✔
533
STORAGES = {
1✔
534
    "default": {
535
        "BACKEND": "django.core.files.storage.FileSystemStorage",
536
    },
537
    "staticfiles": {
538
        "BACKEND": "privaterelay.storage.RelayStaticFilesStorage",
539
    },
540
}
541

542
# Relay does not support user-uploaded files
543
MEDIA_ROOT = None
1✔
544
MEDIA_URL = None
1✔
545

546
WHITENOISE_INDEX_FILE = True
1✔
547

548

549
# See
550
# https://whitenoise.evans.io/en/stable/django.html#WHITENOISE_ADD_HEADERS_FUNCTION
551
# Intended to ensure that the homepage does not get cached in our CDN,
552
# so that the `RedirectRootIfLoggedIn` middleware can kick in for logged-in
553
# users.
554
def set_index_cache_control_headers(
1✔
555
    headers: wsgiref.headers.Headers, path: str, url: str
556
) -> None:
557
    if DEBUG:
1!
558
        home_path = os.path.join(BASE_DIR, "frontend/out", "index.html")
1✔
559
    else:
560
        home_path = os.path.join(STATIC_ROOT, "index.html")
×
561
    if path == home_path:
1✔
562
        headers["Cache-Control"] = "no-cache, public"
1✔
563

564

565
WHITENOISE_ADD_HEADERS_FUNCTION = set_index_cache_control_headers
1✔
566

567
SITE_ID = 1
1✔
568

569
AUTHENTICATION_BACKENDS = (
1✔
570
    "django.contrib.auth.backends.ModelBackend",
571
    "allauth.account.auth_backends.AuthenticationBackend",
572
)
573

574
SOCIALACCOUNT_PROVIDERS = {
1✔
575
    "fxa": {
576
        # Note: to request "profile" scope, must be a trusted Mozilla client
577
        "SCOPE": ["profile", "https://identity.mozilla.com/account/subscriptions"],
578
        "AUTH_PARAMS": {"access_type": "offline"},
579
        "OAUTH_ENDPOINT": config(
580
            "FXA_OAUTH_ENDPOINT", "https://oauth.accounts.firefox.com/v1"
581
        ),
582
        "PROFILE_ENDPOINT": config(
583
            "FXA_PROFILE_ENDPOINT", "https://profile.accounts.firefox.com/v1"
584
        ),
585
        "VERIFIED_EMAIL": True,  # Assume FxA primary email is verified
586
    }
587
}
588

589
SOCIALACCOUNT_EMAIL_VERIFICATION = "none"
1✔
590
SOCIALACCOUNT_AUTO_SIGNUP = True
1✔
591
SOCIALACCOUNT_LOGIN_ON_GET = True
1✔
592
SOCIALACCOUNT_STORE_TOKENS = True
1✔
593

594
ACCOUNT_ADAPTER = "privaterelay.allauth.AccountAdapter"
1✔
595
ACCOUNT_PRESERVE_USERNAME_CASING = False
1✔
596
ACCOUNT_USERNAME_REQUIRED = False
1✔
597

598
FXA_SETTINGS_URL = config("FXA_SETTINGS_URL", f"{FXA_BASE_ORIGIN}/settings")
1✔
599
FXA_SUBSCRIPTIONS_URL = config(
1✔
600
    "FXA_SUBSCRIPTIONS_URL", f"{FXA_BASE_ORIGIN}/subscriptions"
601
)
602
# check https://mozilla.github.io/ecosystem-platform/api#tag/Subscriptions/operation/getOauthMozillasubscriptionsCustomerBillingandsubscriptions  # noqa: E501 (line too long)
603
FXA_ACCOUNTS_ENDPOINT = config(
1✔
604
    "FXA_ACCOUNTS_ENDPOINT",
605
    "https://api.accounts.firefox.com/v1",
606
)
607
FXA_SUPPORT_URL = config("FXA_SUPPORT_URL", f"{FXA_BASE_ORIGIN}/support/")
1✔
608

609
LOGGING = {
1✔
610
    "version": 1,
611
    "filters": {
612
        "request_id": {
613
            "()": "dockerflow.logging.RequestIdLogFilter",
614
        },
615
    },
616
    "formatters": {
617
        "json": {
618
            "()": "dockerflow.logging.JsonLogFormatter",
619
            "logger_name": "fx-private-relay",
620
        }
621
    },
622
    "handlers": {
623
        "console_out": {
624
            "level": "DEBUG",
625
            "class": "logging.StreamHandler",
626
            "stream": sys.stdout,
627
            "formatter": "json",
628
            "filters": ["request_id"],
629
        },
630
        "console_err": {
631
            "level": "DEBUG",
632
            "class": "logging.StreamHandler",
633
            "formatter": "json",
634
            "filters": ["request_id"],
635
        },
636
    },
637
    "loggers": {
638
        "root": {
639
            "handlers": ["console_err"],
640
            "level": "WARNING",
641
        },
642
        "request.summary": {
643
            "handlers": ["console_out"],
644
            "level": "DEBUG",
645
            # pytest's caplog fixture requires propagate=True
646
            # outside of pytest, use propagate=False to avoid double logs
647
            "propagate": IN_PYTEST,
648
        },
649
        "events": {
650
            "handlers": ["console_err"],
651
            "level": "ERROR",
652
            "propagate": IN_PYTEST,
653
        },
654
        "eventsinfo": {
655
            "handlers": ["console_out"],
656
            "level": "INFO",
657
            "propagate": IN_PYTEST,
658
        },
659
        "abusemetrics": {
660
            "handlers": ["console_out"],
661
            "level": "INFO",
662
            "propagate": IN_PYTEST,
663
        },
664
        "studymetrics": {
665
            "handlers": ["console_out"],
666
            "level": "INFO",
667
            "propagate": IN_PYTEST,
668
        },
669
        "markus": {
670
            "handlers": ["console_out"],
671
            "level": "DEBUG",
672
            "propagate": IN_PYTEST,
673
        },
674
        GLEAN_EVENT_MOZLOG_TYPE: {
675
            "handlers": ["console_out"],
676
            "level": "DEBUG",
677
            "propagate": IN_PYTEST,
678
        },
679
        "dockerflow": {
680
            "handlers": ["console_err"],
681
            "level": "WARNING",
682
            "propagate": IN_PYTEST,
683
        },
684
    },
685
}
686

687
DRF_RENDERERS = ["rest_framework.renderers.JSONRenderer"]
1✔
688
if DEBUG and not IN_PYTEST:
1!
689
    DRF_RENDERERS += [
×
690
        "rest_framework.renderers.BrowsableAPIRenderer",
691
    ]
692

693
FIRST_EMAIL_RATE_LIMIT = config("FIRST_EMAIL_RATE_LIMIT", "5/minute")
1✔
694
if IN_PYTEST or RELAY_CHANNEL in ["local", "dev"]:
1!
695
    FIRST_EMAIL_RATE_LIMIT = "1000/minute"
1✔
696

697
REST_FRAMEWORK = {
1✔
698
    "DEFAULT_AUTHENTICATION_CLASSES": [
699
        "api.authentication.FxaTokenAuthentication",
700
        "rest_framework.authentication.TokenAuthentication",
701
        "rest_framework.authentication.SessionAuthentication",
702
    ],
703
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
704
    "DEFAULT_RENDERER_CLASSES": DRF_RENDERERS,
705
    "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
706
    "EXCEPTION_HANDLER": "api.views.relay_exception_handler",
707
}
708
if API_DOCS_ENABLED:
1!
709
    REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema"
1✔
710

711
SPECTACULAR_SETTINGS = {
1✔
712
    "SWAGGER_UI_DIST": "SIDECAR",
713
    "SWAGGER_UI_FAVICON_HREF": "SIDECAR",
714
    "REDOC_DIST": "SIDECAR",
715
    "TITLE": "Firefox Relay API",
716
    "DESCRIPTION": (
717
        "Keep your email safe from hackers and trackers. This API is built with"
718
        " Django REST Framework and powers the Relay website UI, add-on,"
719
        " Firefox browser, and 3rd-party app integrations."
720
    ),
721
    "VERSION": "1.0",
722
    "SERVE_INCLUDE_SCHEMA": False,
723
    "PREPROCESSING_HOOKS": ["api.schema.preprocess_ignore_deprecated_paths"],
724
}
725

726
if IN_PYTEST or RELAY_CHANNEL in ["local", "dev"]:
1!
727
    _DEFAULT_PHONE_RATE_LIMIT = "1000/minute"
1✔
728
else:
729
    _DEFAULT_PHONE_RATE_LIMIT = "5/minute"
×
730
PHONE_RATE_LIMIT = config("PHONE_RATE_LIMIT", _DEFAULT_PHONE_RATE_LIMIT)
1✔
731

732
# Turn on logging out on GET in development.
733
# This allows `/mock/logout/` in the front-end to clear the
734
# session cookie. Without this, after switching accounts in dev mode,
735
# then logging out again, API requests continue succeeding even without
736
# an auth token:
737
ACCOUNT_LOGOUT_ON_GET = DEBUG
1✔
738

739
# TODO: introduce an environment variable to control CORS_ALLOWED_ORIGINS
740
# https://mozilla-hub.atlassian.net/browse/MPP-3468
741
CORS_URLS_REGEX = r"^/api/"
1✔
742
CORS_ALLOWED_ORIGINS = [
1✔
743
    "https://vault.bitwarden.com",
744
    "https://vault.bitwarden.eu",
745
]
746
if RELAY_CHANNEL in ["dev", "stage"]:
1!
747
    CORS_ALLOWED_ORIGINS += [
×
748
        "https://vault.qa.bitwarden.pw",
749
        "https://vault.euqa.bitwarden.pw",
750
    ]
751
# Allow origins for each environment to help debug cors headers
752
if RELAY_CHANNEL == "local":
1!
753
    # In local dev, next runs on localhost and makes requests to /accounts/
754
    CORS_ALLOWED_ORIGINS += [
1✔
755
        "http://localhost:3000",
756
        "http://0.0.0.0:3000",
757
        "http://127.0.0.1:8000",
758
    ]
759
    CORS_URLS_REGEX = r"^/(api|accounts)/"
1✔
760
if RELAY_CHANNEL == "dev":
1!
761
    CORS_ALLOWED_ORIGINS += [
×
762
        "https://dev.fxprivaterelay.nonprod.cloudops.mozgcp.net",
763
    ]
764
if RELAY_CHANNEL == "stage":
1!
765
    CORS_ALLOWED_ORIGINS += [
×
766
        "https://stage.fxprivaterelay.nonprod.cloudops.mozgcp.net",
767
    ]
768

769
CSRF_TRUSTED_ORIGINS = []
1✔
770
if RELAY_CHANNEL == "local":
1!
771
    # In local development, the React UI can be served up from a different server
772
    # that needs to be allowed to make requests.
773
    # In production, the frontend is served by Django, is therefore on the same
774
    # origin and thus has access to the same cookies.
775
    CORS_ALLOW_CREDENTIALS = True
1✔
776
    SESSION_COOKIE_SAMESITE = None
1✔
777
    CSRF_TRUSTED_ORIGINS += [
1✔
778
        "http://localhost:3000",
779
        "http://0.0.0.0:3000",
780
    ]
781

782
SENTRY_RELEASE = config("SENTRY_RELEASE", "")
1✔
783
CIRCLE_SHA1 = config("CIRCLE_SHA1", "")
1✔
784
CIRCLE_TAG = config("CIRCLE_TAG", "")
1✔
785
CIRCLE_BRANCH = config("CIRCLE_BRANCH", "")
1✔
786

787
sentry_release: str | None = None
1✔
788
if SENTRY_RELEASE:
1!
789
    sentry_release = SENTRY_RELEASE
×
790
elif CIRCLE_TAG and CIRCLE_TAG != "unknown":
1!
791
    sentry_release = CIRCLE_TAG
1✔
792
elif (
×
793
    CIRCLE_SHA1
794
    and CIRCLE_SHA1 != "unknown"
795
    and CIRCLE_BRANCH
796
    and CIRCLE_BRANCH != "unknown"
797
):
798
    sentry_release = f"{CIRCLE_BRANCH}:{CIRCLE_SHA1}"
×
799

800
SENTRY_DEBUG = config("SENTRY_DEBUG", DEBUG, cast=bool)
1✔
801

802
SENTRY_ENVIRONMENT = config("SENTRY_ENVIRONMENT", RELAY_CHANNEL)
1✔
803
# Use "local" as default rather than "prod", to catch ngrok.io URLs
804
if SENTRY_ENVIRONMENT == "prod" and SITE_ORIGIN != "https://relay.firefox.com":
1!
805
    SENTRY_ENVIRONMENT = "local"
×
806

807
sentry_sdk.init(
1✔
808
    dsn=config("SENTRY_DSN", None),
809
    integrations=[DjangoIntegration(cache_spans=not DEBUG)],
810
    debug=SENTRY_DEBUG,
811
    include_local_variables=DEBUG,
812
    release=sentry_release,
813
    environment=SENTRY_ENVIRONMENT,
814
)
815
# Duplicates events for unhandled exceptions, but without useful tracebacks
816
ignore_logger("request.summary")
1✔
817
# Security scanner attempts, no action required
818
# Can be re-enabled when hostname allow list implemented at the load balancer
819
ignore_logger("django.security.DisallowedHost")
1✔
820
# Fluent errors, mostly when a translation is unavailable for the locale.
821
# It is more effective to process these from logs using BigQuery than to track
822
# as events in Sentry.
823
ignore_logger("django_ftl.message_errors")
1✔
824
# Security scanner attempts on Heroku dev, no action required
825
if RELAY_CHANNEL == "dev":
1!
826
    ignore_logger("django.security.SuspiciousFileOperation")
×
827

828

829
_MARKUS_BACKENDS: list[dict[str, Any]] = []
1✔
830
if DJANGO_STATSD_ENABLED:
1!
831
    _MARKUS_BACKENDS.append(
×
832
        {
833
            "class": "markus.backends.datadog.DatadogMetrics",
834
            "options": {
835
                "statsd_host": STATSD_HOST,
836
                "statsd_port": STATSD_PORT,
837
                "statsd_prefix": STATSD_PREFIX,
838
            },
839
        }
840
    )
841
if STATSD_DEBUG:
1!
842
    _MARKUS_BACKENDS.append(
×
843
        {
844
            "class": "markus.backends.logging.LoggingMetrics",
845
            "options": {
846
                "logger_name": "markus",
847
                "leader": "METRICS",
848
            },
849
        }
850
    )
851
markus.configure(backends=_MARKUS_BACKENDS)
1✔
852

853
if USE_SILK:
1!
854
    SILKY_PYTHON_PROFILER = True
×
855
    SILKY_PYTHON_PROFILER_BINARY = True
×
856
    SILKY_PYTHON_PROFILER_RESULT_PATH = ".silk-profiler"
×
857

858
# Settings for manage.py process_emails_from_sqs
859
PROCESS_EMAIL_BATCH_SIZE = config(
1✔
860
    "PROCESS_EMAIL_BATCH_SIZE", 10, cast=Choices(range(1, 11), cast=int)
861
)
862
PROCESS_EMAIL_DELETE_FAILED_MESSAGES = config(
1✔
863
    "PROCESS_EMAIL_DELETE_FAILED_MESSAGES", False, cast=bool
864
)
865
PROCESS_EMAIL_HEALTHCHECK_PATH = config(
1✔
866
    "PROCESS_EMAIL_HEALTHCHECK_PATH", os.path.join(TMP_DIR, "healthcheck.json")
867
)
868
PROCESS_EMAIL_MAX_SECONDS = config("PROCESS_EMAIL_MAX_SECONDS", 0, cast=int) or None
1✔
869
PROCESS_EMAIL_VERBOSITY = config(
1✔
870
    "PROCESS_EMAIL_VERBOSITY", 1, cast=Choices(range(0, 4), cast=int)
871
)
872
PROCESS_EMAIL_VISIBILITY_SECONDS = config(
1✔
873
    "PROCESS_EMAIL_VISIBILITY_SECONDS", 120, cast=int
874
)
875
PROCESS_EMAIL_WAIT_SECONDS = config("PROCESS_EMAIL_WAIT_SECONDS", 5, cast=int)
1✔
876
PROCESS_EMAIL_HEALTHCHECK_MAX_AGE = config(
1✔
877
    "PROCESS_EMAIL_HEALTHCHECK_MAX_AGE", 120, cast=int
878
)
879

880
# Django 3.2 switches default to BigAutoField
881
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
1✔
882

883
# python-dockerflow settings
884
DOCKERFLOW_VERSION_CALLBACK = "privaterelay.utils.get_version_info"
1✔
885
DOCKERFLOW_CHECKS = [
1✔
886
    "dockerflow.django.checks.check_database_connected",
887
    "dockerflow.django.checks.check_migrations_applied",
888
]
889
if REDIS_URL:
1!
890
    DOCKERFLOW_CHECKS.append("dockerflow.django.checks.check_redis_connected")
×
891
DOCKERFLOW_REQUEST_ID_HEADER_NAME = config("DOCKERFLOW_REQUEST_ID_HEADER_NAME", None)
1✔
892
SILENCED_SYSTEM_CHECKS = sorted(
1✔
893
    set(config("DJANGO_SILENCED_SYSTEM_CHECKS", default="", cast=Csv()))
894
    | {
895
        # (models.W040) SQLite does not support indexes with non-key columns.
896
        # RelayAddress index idx_ra_created_by_addon uses this for PostgreSQL.
897
        "models.W040",
898
    }
899
)
900

901
# django-ftl settings
902
AUTO_RELOAD_BUNDLES = False  # Requires pyinotify
1✔
903

904
# Patching for django-types
905
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