common.py 31.8 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding: utf-8 -*-
"""
Django settings for funkwhale_api project.

For more information on this file, see
https://docs.djangoproject.com/en/dev/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/dev/ref/settings/
"""
from __future__ import absolute_import, unicode_literals

13
import datetime
14
15
import logging.config
import sys
16

17
from urllib.parse import urlsplit
18

19
import environ
20
21
from celery.schedules import crontab

Eliot Berriot's avatar
Eliot Berriot committed
22
from funkwhale_api import __version__
23

24
logger = logging.getLogger("funkwhale_api.config")
25
ROOT_DIR = environ.Path(__file__) - 3  # (/a/b/myfile.py - 3 = /)
Eliot Berriot's avatar
Eliot Berriot committed
26
APPS_DIR = ROOT_DIR.path("funkwhale_api")
27
28

env = environ.Env()
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

LOGLEVEL = env("LOGLEVEL", default="info").upper()
LOGGING_CONFIG = None
logging.config.dictConfig(
    {
        "version": 1,
        "disable_existing_loggers": False,
        "formatters": {
            "console": {"format": "%(asctime)s %(name)-12s %(levelname)-8s %(message)s"}
        },
        "handlers": {
            "console": {"class": "logging.StreamHandler", "formatter": "console"},
            # # Add Handler for Sentry for `warning` and above
            # 'sentry': {
            #     'level': 'WARNING',
            #     'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler',
            # },
        },
        "loggers": {
            "funkwhale_api": {
Eliot Berriot's avatar
Eliot Berriot committed
49
                "level": LOGLEVEL,
50
51
52
53
54
55
56
57
58
                "handlers": ["console"],
                # required to avoid double logging with root logger
                "propagate": False,
            },
            "": {"level": "WARNING", "handlers": ["console"]},
        },
    }
)

59
60
env_file = env("ENV_FILE", default=None)
if env_file:
61
    logger.info("Loading specified env file at %s", env_file)
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
    # we have an explicitely specified env file
    # so we try to load and it fail loudly if it does not exist
    env.read_env(env_file)
else:
    # we try to load from .env and config/.env
    # but do not crash if those files don't exist
    paths = [
        # /srv/funwhale/api/.env
        ROOT_DIR,
        # /srv/funwhale/config/.env
        ((ROOT_DIR - 1) + "config"),
    ]
    for path in paths:
        try:
            env_path = path.file(".env")
        except FileNotFoundError:
            logger.debug("No env file found at %s/.env", path)
            continue
        env.read_env(env_path)
        logger.info("Loaded env file at %s/.env", path)
        break
83

Eliot Berriot's avatar
Eliot Berriot committed
84
85
86
FUNKWHALE_PLUGINS_PATH = env.list(
    "FUNKWHALE_PLUGINS_PATH",
    default=["/srv/funkwhale/plugins/", str(ROOT_DIR.path("plugins"))],
87
88
)

Eliot Berriot's avatar
Eliot Berriot committed
89
90
91
for path in FUNKWHALE_PLUGINS_PATH:
    sys.path.append(path)

92
FUNKWHALE_HOSTNAME = None
Eliot Berriot's avatar
Eliot Berriot committed
93
94
FUNKWHALE_HOSTNAME_SUFFIX = env("FUNKWHALE_HOSTNAME_SUFFIX", default=None)
FUNKWHALE_HOSTNAME_PREFIX = env("FUNKWHALE_HOSTNAME_PREFIX", default=None)
95
96
if FUNKWHALE_HOSTNAME_PREFIX and FUNKWHALE_HOSTNAME_SUFFIX:
    # We're in traefik case, in development
Eliot Berriot's avatar
Eliot Berriot committed
97
98
99
100
    FUNKWHALE_HOSTNAME = "{}.{}".format(
        FUNKWHALE_HOSTNAME_PREFIX, FUNKWHALE_HOSTNAME_SUFFIX
    )
    FUNKWHALE_PROTOCOL = env("FUNKWHALE_PROTOCOL", default="https")
101
102
else:
    try:
Eliot Berriot's avatar
Eliot Berriot committed
103
104
        FUNKWHALE_HOSTNAME = env("FUNKWHALE_HOSTNAME")
        FUNKWHALE_PROTOCOL = env("FUNKWHALE_PROTOCOL", default="https")
105
    except Exception:
Eliot Berriot's avatar
Eliot Berriot committed
106
        FUNKWHALE_URL = env("FUNKWHALE_URL")
107
108
109
110
        _parsed = urlsplit(FUNKWHALE_URL)
        FUNKWHALE_HOSTNAME = _parsed.netloc
        FUNKWHALE_PROTOCOL = _parsed.scheme

111
112
FUNKWHALE_PROTOCOL = FUNKWHALE_PROTOCOL.lower()
FUNKWHALE_HOSTNAME = FUNKWHALE_HOSTNAME.lower()
Eliot Berriot's avatar
Eliot Berriot committed
113
FUNKWHALE_URL = "{}://{}".format(FUNKWHALE_PROTOCOL, FUNKWHALE_HOSTNAME)
114
115
116
117
118
119
120
FUNKWHALE_SPA_HTML_ROOT = env(
    "FUNKWHALE_SPA_HTML_ROOT", default=FUNKWHALE_URL + "/front/"
)
FUNKWHALE_SPA_HTML_CACHE_DURATION = env.int(
    "FUNKWHALE_SPA_HTML_CACHE_DURATION", default=60 * 15
)
FUNKWHALE_EMBED_URL = env(
121
    "FUNKWHALE_EMBED_URL", default=FUNKWHALE_URL + "/front/embed.html"
122
123
)
APP_NAME = "Funkwhale"
124

125
# XXX: deprecated, see #186
Eliot Berriot's avatar
Eliot Berriot committed
126
FEDERATION_ENABLED = env.bool("FEDERATION_ENABLED", default=True)
127
FEDERATION_HOSTNAME = env("FEDERATION_HOSTNAME", default=FUNKWHALE_HOSTNAME).lower()
128
# XXX: deprecated, see #186
Eliot Berriot's avatar
Eliot Berriot committed
129
FEDERATION_COLLECTION_PAGE_SIZE = env.int("FEDERATION_COLLECTION_PAGE_SIZE", default=50)
130
# XXX: deprecated, see #186
131
FEDERATION_MUSIC_NEEDS_APPROVAL = env.bool(
Eliot Berriot's avatar
Eliot Berriot committed
132
    "FEDERATION_MUSIC_NEEDS_APPROVAL", default=True
133
)
134
# XXX: deprecated, see #186
Eliot Berriot's avatar
Eliot Berriot committed
135
FEDERATION_ACTOR_FETCH_DELAY = env.int("FEDERATION_ACTOR_FETCH_DELAY", default=60 * 12)
Eliot Berriot's avatar
Eliot Berriot committed
136
137
138
FEDERATION_SERVICE_ACTOR_USERNAME = env(
    "FEDERATION_SERVICE_ACTOR_USERNAME", default="service"
)
139
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=[]) + [FUNKWHALE_HOSTNAME]
140

141
142
143
# APP CONFIGURATION
# ------------------------------------------------------------------------------
DJANGO_APPS = (
Eliot Berriot's avatar
Eliot Berriot committed
144
    "channels",
145
    # Default Django apps:
Eliot Berriot's avatar
Eliot Berriot committed
146
147
148
149
150
151
152
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.sites",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "django.contrib.postgres",
153
154
155
    # Useful template tags:
    # 'django.contrib.humanize',
    # Admin
Eliot Berriot's avatar
Eliot Berriot committed
156
    "django.contrib.admin",
157
158
159
)
THIRD_PARTY_APPS = (
    # 'crispy_forms',  # Form layouts
Eliot Berriot's avatar
Eliot Berriot committed
160
161
162
163
    "allauth",  # registration
    "allauth.account",  # registration
    "allauth.socialaccount",  # registration
    "corsheaders",
164
    "oauth2_provider",
Eliot Berriot's avatar
Eliot Berriot committed
165
166
167
168
169
170
171
    "rest_framework",
    "rest_framework.authtoken",
    "rest_auth",
    "rest_auth.registration",
    "dynamic_preferences",
    "django_filters",
    "django_cleanup",
Eliot Berriot's avatar
Eliot Berriot committed
172
    "versatileimagefield",
173
174
)

Eliot Berriot's avatar
Eliot Berriot committed
175
176
177

# Sentry
RAVEN_ENABLED = env.bool("RAVEN_ENABLED", default=False)
Eliot Berriot's avatar
Eliot Berriot committed
178
RAVEN_DSN = env("RAVEN_DSN", default="")
Eliot Berriot's avatar
Eliot Berriot committed
179
180
181

if RAVEN_ENABLED:
    RAVEN_CONFIG = {
Eliot Berriot's avatar
Eliot Berriot committed
182
        "dsn": RAVEN_DSN,
Eliot Berriot's avatar
Eliot Berriot committed
183
184
        # If you are using git, you can also automatically configure the
        # release based on the git info.
Eliot Berriot's avatar
Eliot Berriot committed
185
        "release": __version__,
Eliot Berriot's avatar
Eliot Berriot committed
186
    }
Eliot Berriot's avatar
Eliot Berriot committed
187
    THIRD_PARTY_APPS += ("raven.contrib.django.raven_compat",)
Eliot Berriot's avatar
Eliot Berriot committed
188

189
190
# Apps specific for this project go here.
LOCAL_APPS = (
191
    "funkwhale_api.common.apps.CommonConfig",
Eliot Berriot's avatar
Eliot Berriot committed
192
    "funkwhale_api.plugins",
Eliot Berriot's avatar
Eliot Berriot committed
193
194
    "funkwhale_api.activity.apps.ActivityConfig",
    "funkwhale_api.users",  # custom users app
195
    "funkwhale_api.users.oauth",
196
    # Your stuff: custom apps go here
Eliot Berriot's avatar
Eliot Berriot committed
197
198
199
200
201
    "funkwhale_api.instance",
    "funkwhale_api.music",
    "funkwhale_api.requests",
    "funkwhale_api.favorites",
    "funkwhale_api.federation",
202
    "funkwhale_api.moderation.apps.ModerationConfig",
Eliot Berriot's avatar
Eliot Berriot committed
203
204
205
206
    "funkwhale_api.radios",
    "funkwhale_api.history",
    "funkwhale_api.playlists",
    "funkwhale_api.subsonic",
Eliot Berriot's avatar
Eliot Berriot committed
207
    "funkwhale_api.tags",
208
209
210
)

# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
Eliot Berriot's avatar
Eliot Berriot committed
211

212
213
214
215
216
217
218
219
220
221
222
223
224

PLUGINS = [p for p in env.list("FUNKWHALE_PLUGINS", default=[]) if p]
if PLUGINS:
    logger.info("Running with the following plugins enabled: %s", ", ".join(PLUGINS))
else:
    logger.info("Running with no plugins")

INSTALLED_APPS = (
    DJANGO_APPS
    + THIRD_PARTY_APPS
    + LOCAL_APPS
    + tuple(["{}.apps.Plugin".format(p) for p in PLUGINS])
)
225
226
227

# MIDDLEWARE CONFIGURATION
# ------------------------------------------------------------------------------
Eliot Berriot's avatar
Eliot Berriot committed
228
MIDDLEWARE = (
229
    "django.middleware.security.SecurityMiddleware",
230
231
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "corsheaders.middleware.CorsMiddleware",
232
    "funkwhale_api.common.middleware.SPAFallbackMiddleware",
Eliot Berriot's avatar
Eliot Berriot committed
233
234
235
236
237
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
238
    "funkwhale_api.users.middleware.RecordActivityMiddleware",
239
    "funkwhale_api.common.middleware.ThrottleStatusMiddleware",
240
241
242
243
244
245
246
247
248
249
)

# DEBUG
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = env.bool("DJANGO_DEBUG", False)

# FIXTURE CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS
Eliot Berriot's avatar
Eliot Berriot committed
250
FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),)
251
252
253

# EMAIL CONFIGURATION
# ------------------------------------------------------------------------------
254
255
256
257

# EMAIL
# ------------------------------------------------------------------------------
DEFAULT_FROM_EMAIL = env(
Eliot Berriot's avatar
Eliot Berriot committed
258
259
    "DEFAULT_FROM_EMAIL", default="Funkwhale <noreply@{}>".format(FUNKWHALE_HOSTNAME)
)
260

Eliot Berriot's avatar
Eliot Berriot committed
261
262
EMAIL_SUBJECT_PREFIX = env("EMAIL_SUBJECT_PREFIX", default="[Funkwhale] ")
SERVER_EMAIL = env("SERVER_EMAIL", default=DEFAULT_FROM_EMAIL)
263
264


Eliot Berriot's avatar
Eliot Berriot committed
265
EMAIL_CONFIG = env.email_url("EMAIL_CONFIG", default="consolemail://")
266
267

vars().update(EMAIL_CONFIG)
268
269
270
271
272
273

# DATABASE CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {
    # Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
Eliot Berriot's avatar
Eliot Berriot committed
274
    "default": env.db("DATABASE_URL")
275
}
Eliot Berriot's avatar
Eliot Berriot committed
276
DATABASES["default"]["ATOMIC_REQUESTS"] = True
Eliot Berriot's avatar
Eliot Berriot committed
277
DATABASES["default"]["CONN_MAX_AGE"] = env("DB_CONN_MAX_AGE", default=60 * 5)
278
279
280
281
282

MIGRATION_MODULES = {
    # see https://github.com/jazzband/django-oauth-toolkit/issues/634
    # swappable models are badly designed in oauth2_provider
    # ignore migrations and provide our own models.
283
284
    "oauth2_provider": None,
    "sites": "funkwhale_api.contrib.sites.migrations",
285
286
}

287
288
289
290
291
292
293
294
295
296
297
298
299
#
# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.sqlite3',
#         'NAME': 'db.sqlite3',
#     }
# }
# GENERAL CONFIGURATION
# ------------------------------------------------------------------------------
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
Eliot Berriot's avatar
Eliot Berriot committed
300
TIME_ZONE = "UTC"
301
302

# See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code
Eliot Berriot's avatar
Eliot Berriot committed
303
LANGUAGE_CODE = "en-us"
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322

# See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1

# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n
USE_I18N = True

# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n
USE_L10N = True

# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
USE_TZ = True

# TEMPLATE CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#templates
TEMPLATES = [
    {
        # See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND
Eliot Berriot's avatar
Eliot Berriot committed
323
        "BACKEND": "django.template.backends.django.DjangoTemplates",
324
        # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs
Eliot Berriot's avatar
Eliot Berriot committed
325
326
        "DIRS": [str(APPS_DIR.path("templates"))],
        "OPTIONS": {
327
            # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug
Eliot Berriot's avatar
Eliot Berriot committed
328
            "debug": DEBUG,
329
330
            # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders
            # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types
Eliot Berriot's avatar
Eliot Berriot committed
331
332
333
            "loaders": [
                "django.template.loaders.filesystem.Loader",
                "django.template.loaders.app_directories.Loader",
334
335
            ],
            # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
Eliot Berriot's avatar
Eliot Berriot committed
336
337
338
339
340
341
342
343
344
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.template.context_processors.i18n",
                "django.template.context_processors.media",
                "django.template.context_processors.static",
                "django.template.context_processors.tz",
                "django.contrib.messages.context_processors.messages",
345
346
347
                # Your stuff: custom template context processors go here
            ],
        },
Eliot Berriot's avatar
Eliot Berriot committed
348
    }
349
350
351
]

# See: http://django-crispy-forms.readthedocs.org/en/latest/install.html#template-packs
Eliot Berriot's avatar
Eliot Berriot committed
352
CRISPY_TEMPLATE_PACK = "bootstrap3"
353
354
355
356

# STATIC FILE CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root
Eliot Berriot's avatar
Eliot Berriot committed
357
STATIC_ROOT = env("STATIC_ROOT", default=str(ROOT_DIR("staticfiles")))
358
359

# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
Eliot Berriot's avatar
Eliot Berriot committed
360
361
STATIC_URL = env("STATIC_URL", default="/staticfiles/")
DEFAULT_FILE_STORAGE = "funkwhale_api.common.storage.ASCIIFileSystemStorage"
362

363
PROXY_MEDIA = env.bool("PROXY_MEDIA", default=True)
364
AWS_DEFAULT_ACL = None
365
366
367
368
369
AWS_QUERYSTRING_AUTH = env.bool("AWS_QUERYSTRING_AUTH", default=not PROXY_MEDIA)
AWS_S3_MAX_MEMORY_SIZE = env.int(
    "AWS_S3_MAX_MEMORY_SIZE", default=1000 * 1000 * 1000 * 20
)
AWS_QUERYSTRING_EXPIRE = env.int("AWS_QUERYSTRING_EXPIRE", default=3600)
370
371
372
373
374
375
376
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", default=None)

if AWS_ACCESS_KEY_ID:
    AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID
    AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY")
    AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
    AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL", default=None)
377
378
    AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", default=None)
    AWS_S3_SIGNATURE_VERSION = "s3v4"
379
    AWS_LOCATION = env("AWS_LOCATION", default="")
380
    DEFAULT_FILE_STORAGE = "funkwhale_api.common.storage.ASCIIS3Boto3Storage"
381

382
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
Eliot Berriot's avatar
Eliot Berriot committed
383
STATICFILES_DIRS = (str(APPS_DIR.path("static")),)
384
385
386

# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
STATICFILES_FINDERS = (
Eliot Berriot's avatar
Eliot Berriot committed
387
388
    "django.contrib.staticfiles.finders.FileSystemFinder",
    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
389
390
391
392
393
)

# MEDIA CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root
Eliot Berriot's avatar
Eliot Berriot committed
394
MEDIA_ROOT = env("MEDIA_ROOT", default=str(APPS_DIR("media")))
395
396

# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url
Eliot Berriot's avatar
Eliot Berriot committed
397
MEDIA_URL = env("MEDIA_URL", default="/media/")
398
FILE_UPLOAD_PERMISSIONS = 0o644
399
400
# URL Configuration
# ------------------------------------------------------------------------------
Eliot Berriot's avatar
Eliot Berriot committed
401
ROOT_URLCONF = "config.urls"
402
SPA_URLCONF = "config.spa_urls"
Eliot Berriot's avatar
Eliot Berriot committed
403
ASGI_APPLICATION = "config.routing.application"
404

405
# This ensures that Django will be able to detect a secure connection
Eliot Berriot's avatar
Eliot Berriot committed
406
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
407
408
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
409

410
411
412
# AUTHENTICATION CONFIGURATION
# ------------------------------------------------------------------------------
AUTHENTICATION_BACKENDS = (
413
    "funkwhale_api.users.auth_backends.ModelBackend",
Eliot Berriot's avatar
Eliot Berriot committed
414
    "allauth.account.auth_backends.AuthenticationBackend",
415
)
416
SESSION_COOKIE_HTTPONLY = False
417
# Some really nice defaults
Eliot Berriot's avatar
Eliot Berriot committed
418
ACCOUNT_AUTHENTICATION_METHOD = "username_email"
419
ACCOUNT_EMAIL_REQUIRED = True
Eliot Berriot's avatar
Eliot Berriot committed
420
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
421
ACCOUNT_USERNAME_VALIDATORS = "funkwhale_api.users.serializers.username_validators"
422
423
424

# Custom user app defaults
# Select the correct user model
Eliot Berriot's avatar
Eliot Berriot committed
425
426
427
AUTH_USER_MODEL = "users.User"
LOGIN_REDIRECT_URL = "users:redirect"
LOGIN_URL = "account_login"
428

429
430
431
432
433
434
435
436
437
438
# OAuth configuration
from funkwhale_api.users.oauth import scopes  # noqa

OAUTH2_PROVIDER = {
    "SCOPES": {s.id: s.label for s in scopes.SCOPES_BY_ID.values()},
    "ALLOWED_REDIRECT_URI_SCHEMES": ["http", "https", "urn"],
    # we keep expired tokens for 15 days, for tracability
    "REFRESH_TOKEN_EXPIRE_SECONDS": 3600 * 24 * 15,
    "AUTHORIZATION_CODE_EXPIRE_SECONDS": 5 * 60,
    "ACCESS_TOKEN_EXPIRE_SECONDS": 60 * 60 * 10,
Eliot Berriot's avatar
Eliot Berriot committed
439
    "OAUTH2_SERVER_CLASS": "funkwhale_api.users.oauth.server.OAuth2Server",
440
441
442
443
444
445
}
OAUTH2_PROVIDER_APPLICATION_MODEL = "users.Application"
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "users.AccessToken"
OAUTH2_PROVIDER_GRANT_MODEL = "users.Grant"
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "users.RefreshToken"

446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# LDAP AUTHENTICATION CONFIGURATION
# ------------------------------------------------------------------------------
AUTH_LDAP_ENABLED = env.bool("LDAP_ENABLED", default=False)
if AUTH_LDAP_ENABLED:

    # Import the LDAP modules here; this way, we don't need the dependency unless someone
    # actually enables the LDAP support
    import ldap
    from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion, GroupOfNamesType

    # Add LDAP to the authentication backends
    AUTHENTICATION_BACKENDS += ("django_auth_ldap.backend.LDAPBackend",)

    # Basic configuration
    AUTH_LDAP_SERVER_URI = env("LDAP_SERVER_URI")
    AUTH_LDAP_BIND_DN = env("LDAP_BIND_DN", default="")
    AUTH_LDAP_BIND_PASSWORD = env("LDAP_BIND_PASSWORD", default="")
    AUTH_LDAP_SEARCH_FILTER = env("LDAP_SEARCH_FILTER", default="(uid={0})").format(
        "%(user)s"
    )
    AUTH_LDAP_START_TLS = env.bool("LDAP_START_TLS", default=False)

    DEFAULT_USER_ATTR_MAP = [
        "first_name:givenName",
        "last_name:sn",
        "username:cn",
        "email:mail",
    ]
    LDAP_USER_ATTR_MAP = env.list("LDAP_USER_ATTR_MAP", default=DEFAULT_USER_ATTR_MAP)
    AUTH_LDAP_USER_ATTR_MAP = {}
    for m in LDAP_USER_ATTR_MAP:
        funkwhale_field, ldap_field = m.split(":")
        AUTH_LDAP_USER_ATTR_MAP[funkwhale_field.strip()] = ldap_field.strip()

    # Determine root DN supporting multiple root DNs
    AUTH_LDAP_ROOT_DN = env("LDAP_ROOT_DN")
    AUTH_LDAP_ROOT_DN_LIST = []
    for ROOT_DN in AUTH_LDAP_ROOT_DN.split():
        AUTH_LDAP_ROOT_DN_LIST.append(
            LDAPSearch(ROOT_DN, ldap.SCOPE_SUBTREE, AUTH_LDAP_SEARCH_FILTER)
        )
    # Search for the user in all the root DNs
    AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(*AUTH_LDAP_ROOT_DN_LIST)

    # Search for group types
    LDAP_GROUP_DN = env("LDAP_GROUP_DN", default="")
    if LDAP_GROUP_DN:
        AUTH_LDAP_GROUP_DN = LDAP_GROUP_DN
        # Get filter
        AUTH_LDAP_GROUP_FILTER = env("LDAP_GROUP_FILER", default="")
        # Search for the group in the specified DN
        AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
            AUTH_LDAP_GROUP_DN, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_FILTER
        )
        AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()

        # Configure basic group support
        LDAP_REQUIRE_GROUP = env("LDAP_REQUIRE_GROUP", default="")
        if LDAP_REQUIRE_GROUP:
            AUTH_LDAP_REQUIRE_GROUP = LDAP_REQUIRE_GROUP
        LDAP_DENY_GROUP = env("LDAP_DENY_GROUP", default="")
        if LDAP_DENY_GROUP:
            AUTH_LDAP_DENY_GROUP = LDAP_DENY_GROUP


511
# SLUGLIFIER
Eliot Berriot's avatar
Eliot Berriot committed
512
AUTOSLUG_SLUGIFY_FUNCTION = "slugify.slugify"
513

514
CACHE_DEFAULT = "redis://127.0.0.1:6379/0"
515
516
517
518
519
520
521
CACHES = {
    "default": env.cache_url("CACHE_URL", default=CACHE_DEFAULT),
    "local": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
        "LOCATION": "local-cache",
    },
}
522

523
CACHES["default"]["BACKEND"] = "django_redis.cache.RedisCache"
Eliot Berriot's avatar
Eliot Berriot committed
524

Eliot Berriot's avatar
Eliot Berriot committed
525
526
527
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
528
        "CONFIG": {"hosts": [CACHES["default"]["LOCATION"]]},
Eliot Berriot's avatar
Eliot Berriot committed
529
    }
Eliot Berriot's avatar
Eliot Berriot committed
530
531
}

532
533
534
CACHES["default"]["OPTIONS"] = {
    "CLIENT_CLASS": "django_redis.client.DefaultClient",
    "IGNORE_EXCEPTIONS": True,  # mimics memcache behavior.
Eliot Berriot's avatar
Eliot Berriot committed
535
    # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
536
537
538
}


539
# CELERY
Eliot Berriot's avatar
Eliot Berriot committed
540
INSTALLED_APPS += ("funkwhale_api.taskapp.celery.CeleryConfig",)
541
CELERY_BROKER_URL = env(
Eliot Berriot's avatar
Eliot Berriot committed
542
543
    "CELERY_BROKER_URL", default=env("CACHE_URL", default=CACHE_DEFAULT)
)
544
# END CELERY
545
# Location of root django.contrib.admin URL, use {% url 'admin:index' %}
Eliot Berriot's avatar
Eliot Berriot committed
546

547
# Your common stuff: Below this line define 3rd party library settings
548
549
CELERY_TASK_DEFAULT_RATE_LIMIT = 1
CELERY_TASK_TIME_LIMIT = 300
550
CELERY_BEAT_SCHEDULE = {
Eliot Berriot's avatar
Eliot Berriot committed
551
    "federation.clean_music_cache": {
552
        "task": "federation.clean_music_cache",
553
        "schedule": crontab(minute="0", hour="*/2"),
Eliot Berriot's avatar
Eliot Berriot committed
554
        "options": {"expires": 60 * 2},
555
556
557
    },
    "music.clean_transcoding_cache": {
        "task": "music.clean_transcoding_cache",
558
        "schedule": crontab(minute="0", hour="*"),
559
560
        "options": {"expires": 60 * 2},
    },
561
562
563
564
565
    "oauth.clear_expired_tokens": {
        "task": "oauth.clear_expired_tokens",
        "schedule": crontab(minute="0", hour="0"),
        "options": {"expires": 60 * 60 * 24},
    },
566
567
568
569
570
    "federation.refresh_nodeinfo_known_nodes": {
        "task": "federation.refresh_nodeinfo_known_nodes",
        "schedule": crontab(minute="0", hour="*"),
        "options": {"expires": 60 * 60},
    },
571
572
}

573
574
NODEINFO_REFRESH_DELAY = env.int("NODEINFO_REFRESH_DELAY", default=3600 * 24)

575
576
577
578
579
580
581

def get_user_secret_key(user):
    from django.conf import settings

    return settings.SECRET_KEY + str(user.secret_key)


582
JWT_AUTH = {
Eliot Berriot's avatar
Eliot Berriot committed
583
584
585
586
    "JWT_ALLOW_REFRESH": True,
    "JWT_EXPIRATION_DELTA": datetime.timedelta(days=7),
    "JWT_REFRESH_EXPIRATION_DELTA": datetime.timedelta(days=30),
    "JWT_AUTH_HEADER_PREFIX": "JWT",
587
    "JWT_GET_USER_SECRET_KEY": get_user_secret_key,
588
}
589
OLD_PASSWORD_FIELD_ENABLED = True
590
591
592
593
594
595
596
597
598
599
600
AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
        "OPTIONS": {"min_length": env.int("PASSWORD_MIN_LENGTH", default=8)},
    },
    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
Eliot Berriot's avatar
Eliot Berriot committed
601
ACCOUNT_ADAPTER = "funkwhale_api.users.adapters.FunkwhaleAccountAdapter"
602
603
604
605
606
607
CORS_ORIGIN_ALLOW_ALL = True
# CORS_ORIGIN_WHITELIST = (
#     'localhost',
#     'funkwhale.localhost',
# )
CORS_ALLOW_CREDENTIALS = True
608

609
REST_FRAMEWORK = {
Eliot Berriot's avatar
Eliot Berriot committed
610
611
612
613
614
615
616
    "DEFAULT_PAGINATION_CLASS": "funkwhale_api.common.pagination.FunkwhalePagination",
    "PAGE_SIZE": 25,
    "DEFAULT_PARSER_CLASSES": (
        "rest_framework.parsers.JSONParser",
        "rest_framework.parsers.FormParser",
        "rest_framework.parsers.MultiPartParser",
        "funkwhale_api.federation.parsers.ActivityParser",
617
    ),
Eliot Berriot's avatar
Eliot Berriot committed
618
    "DEFAULT_AUTHENTICATION_CLASSES": (
619
        "oauth2_provider.contrib.rest_framework.OAuth2Authentication",
Eliot Berriot's avatar
Eliot Berriot committed
620
621
        "funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS",
        "funkwhale_api.common.authentication.BearerTokenHeaderAuth",
622
        "funkwhale_api.common.authentication.JSONWebTokenAuthentication",
Eliot Berriot's avatar
Eliot Berriot committed
623
        "rest_framework.authentication.BasicAuthentication",
624
        "rest_framework.authentication.SessionAuthentication",
625
    ),
626
627
628
    "DEFAULT_PERMISSION_CLASSES": (
        "funkwhale_api.users.oauth.permissions.ScopePermission",
    ),
Eliot Berriot's avatar
Eliot Berriot committed
629
630
631
    "DEFAULT_FILTER_BACKENDS": (
        "rest_framework.filters.OrderingFilter",
        "django_filters.rest_framework.DjangoFilterBackend",
632
    ),
Eliot Berriot's avatar
Eliot Berriot committed
633
    "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
634
    "NUM_PROXIES": env.int("NUM_PROXIES", default=1),
635
}
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
THROTTLING_ENABLED = env.bool("THROTTLING_ENABLED", default=True)
if THROTTLING_ENABLED:
    REST_FRAMEWORK["DEFAULT_THROTTLE_CLASSES"] = env.list(
        "THROTTLE_CLASSES",
        default=["funkwhale_api.common.throttling.FunkwhaleThrottle"],
    )

THROTTLING_SCOPES = {
    "*": {"anonymous": "anonymous-wildcard", "authenticated": "authenticated-wildcard"},
    "create": {
        "authenticated": "authenticated-create",
        "anonymous": "anonymous-create",
    },
    "list": {"authenticated": "authenticated-list", "anonymous": "anonymous-list"},
    "retrieve": {
        "authenticated": "authenticated-retrieve",
        "anonymous": "anonymous-retrieve",
    },
    "destroy": {
        "authenticated": "authenticated-destroy",
        "anonymous": "anonymous-destroy",
    },
    "update": {
        "authenticated": "authenticated-update",
        "anonymous": "anonymous-update",
    },
    "partial_update": {
        "authenticated": "authenticated-update",
        "anonymous": "anonymous-update",
    },
}

THROTTLING_USER_RATES = env.dict("THROTTLING_RATES", default={})

THROTTLING_RATES = {
    "anonymous-wildcard": {
        "rate": THROTTLING_USER_RATES.get("anonymous-wildcard", "1000/h"),
        "description": "Anonymous requests not covered by other limits",
    },
    "authenticated-wildcard": {
        "rate": THROTTLING_USER_RATES.get("authenticated-wildcard", "2000/h"),
        "description": "Authenticated requests not covered by other limits",
    },
    "authenticated-create": {
        "rate": THROTTLING_USER_RATES.get("authenticated-create", "1000/hour"),
        "description": "Authenticated POST requests",
    },
    "anonymous-create": {
        "rate": THROTTLING_USER_RATES.get("anonymous-create", "1000/day"),
        "description": "Anonymous POST requests",
    },
    "authenticated-list": {
        "rate": THROTTLING_USER_RATES.get("authenticated-list", "10000/hour"),
        "description": "Authenticated GET requests on resource lists",
    },
    "anonymous-list": {
        "rate": THROTTLING_USER_RATES.get("anonymous-list", "10000/day"),
        "description": "Anonymous GET requests on resource lists",
    },
    "authenticated-retrieve": {
        "rate": THROTTLING_USER_RATES.get("authenticated-retrieve", "10000/hour"),
        "description": "Authenticated GET requests on resource detail",
    },
    "anonymous-retrieve": {
        "rate": THROTTLING_USER_RATES.get("anonymous-retrieve", "10000/day"),
        "description": "Anonymous GET requests on resource detail",
    },
    "authenticated-destroy": {
        "rate": THROTTLING_USER_RATES.get("authenticated-destroy", "500/hour"),
        "description": "Authenticated DELETE requests on resource detail",
    },
    "anonymous-destroy": {
        "rate": THROTTLING_USER_RATES.get("anonymous-destroy", "1000/day"),
        "description": "Anonymous DELETE requests on resource detail",
    },
    "authenticated-update": {
        "rate": THROTTLING_USER_RATES.get("authenticated-update", "1000/hour"),
        "description": "Authenticated PATCH and PUT requests on resource detail",
    },
    "anonymous-update": {
        "rate": THROTTLING_USER_RATES.get("anonymous-update", "1000/day"),
        "description": "Anonymous PATCH and PUT requests on resource detail",
    },
    # potentially spammy / dangerous endpoints
    "authenticated-reports": {
        "rate": THROTTLING_USER_RATES.get("authenticated-reports", "100/day"),
        "description": "Authenticated report submission",
    },
    "anonymous-reports": {
        "rate": THROTTLING_USER_RATES.get("anonymous-reports", "10/day"),
        "description": "Anonymous report submission",
    },
    "authenticated-oauth-app": {
        "rate": THROTTLING_USER_RATES.get("authenticated-oauth-app", "10/hour"),
        "description": "Authenticated OAuth app creation",
    },
    "anonymous-oauth-app": {
        "rate": THROTTLING_USER_RATES.get("anonymous-oauth-app", "10/day"),
        "description": "Anonymous OAuth app creation",
    },
    "oauth-authorize": {
        "rate": THROTTLING_USER_RATES.get("oauth-authorize", "100/hour"),
        "description": "OAuth app authorization",
    },
    "oauth-token": {
        "rate": THROTTLING_USER_RATES.get("oauth-token", "100/hour"),
        "description": "OAuth token creation",
    },
    "oauth-revoke-token": {
        "rate": THROTTLING_USER_RATES.get("oauth-revoke-token", "100/hour"),
        "description": "OAuth token deletion",
    },
    "jwt-login": {
        "rate": THROTTLING_USER_RATES.get("jwt-login", "30/hour"),
        "description": "JWT token creation",
    },
    "jwt-refresh": {
        "rate": THROTTLING_USER_RATES.get("jwt-refresh", "30/hour"),
        "description": "JWT token refresh",
    },
    "signup": {
        "rate": THROTTLING_USER_RATES.get("signup", "10/day"),
        "description": "Account creation",
    },
    "verify-email": {
        "rate": THROTTLING_USER_RATES.get("verify-email", "20/h"),
        "description": "Email address confirmation",
    },
    "password-change": {
        "rate": THROTTLING_USER_RATES.get("password-change", "20/h"),
        "description": "Password change (when authenticated)",
    },
    "password-reset": {
        "rate": THROTTLING_USER_RATES.get("password-reset", "20/h"),
        "description": "Password reset request",
    },
    "password-reset-confirm": {
        "rate": THROTTLING_USER_RATES.get("password-reset-confirm", "20/h"),
        "description": "Password reset confirmation",
    },
}

778

Eliot Berriot's avatar
Eliot Berriot committed
779
BROWSABLE_API_ENABLED = env.bool("BROWSABLE_API_ENABLED", default=False)
780
if BROWSABLE_API_ENABLED:
Eliot Berriot's avatar
Eliot Berriot committed
781
782
    REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] += (
        "rest_framework.renderers.BrowsableAPIRenderer",
783
784
    )

785
REST_AUTH_SERIALIZERS = {
Eliot Berriot's avatar
Eliot Berriot committed
786
    "PASSWORD_RESET_SERIALIZER": "funkwhale_api.users.serializers.PasswordResetSerializer"  # noqa
787
788
789
}
REST_SESSION_LOGIN = False
REST_USE_JWT = True
790
791

ATOMIC_REQUESTS = False
792
793
USE_X_FORWARDED_HOST = True
USE_X_FORWARDED_PORT = True
794
795
796

# Wether we should use Apache, Nginx (or other) headers when serving audio files
# Default to Nginx
Eliot Berriot's avatar
Eliot Berriot committed
797
798
REVERSE_PROXY_TYPE = env("REVERSE_PROXY_TYPE", default="nginx")
assert REVERSE_PROXY_TYPE in ["apache2", "nginx"], "Unsupported REVERSE_PROXY_TYPE"
799

800
801
# Which path will be used to process the internal redirection
# **DO NOT** put a slash at the end
Eliot Berriot's avatar
Eliot Berriot committed
802
PROTECT_FILES_PATH = env("PROTECT_FILES_PATH", default="/_protected")
803
804
805
806


# use this setting to tweak for how long you want to cache
# musicbrainz results. (value is in seconds)
Eliot Berriot's avatar
Eliot Berriot committed
807
MUSICBRAINZ_CACHE_DURATION = env.int("MUSICBRAINZ_CACHE_DURATION", default=300)
808

809
810
811
812
813
# Use this setting to change the musicbrainz hostname, for instance to
# use a mirror. The hostname can also contain a port number (so, e.g.,
# "localhost:5000" is a valid name to set).
MUSICBRAINZ_HOSTNAME = env("MUSICBRAINZ_HOSTNAME", default="musicbrainz.org")

814
# Custom Admin URL, use {% url 'admin:index' %}
Eliot Berriot's avatar
Eliot Berriot committed
815
ADMIN_URL = env("DJANGO_ADMIN_URL", default="^api/admin/")
816
CSRF_USE_SESSIONS = True
817
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
818
819

# Playlist settings
820
# XXX: deprecated, see #186
Eliot Berriot's avatar
Eliot Berriot committed
821
PLAYLISTS_MAX_TRACKS = env.int("PLAYLISTS_MAX_TRACKS", default=250)
822
823

ACCOUNT_USERNAME_BLACKLIST = [
Eliot Berriot's avatar
Eliot Berriot committed
824
825
    "funkwhale",
    "library",
826
    "instance",
Eliot Berriot's avatar
Eliot Berriot committed
827
828
829
830
831
832
833
834
    "test",
    "status",
    "root",
    "admin",
    "owner",
    "superuser",
    "staff",
    "service",
Eliot Berriot's avatar
Eliot Berriot committed
835
    "me",
836
837
    "ghost",
    "_",
Eliot Berriot's avatar
Eliot Berriot committed
838
    "-",
839
840
    "hello",
    "contact",
Eliot Berriot's avatar
Eliot Berriot committed
841
842
843
844
845
    "inbox",
    "outbox",
    "shared-inbox",
    "shared_inbox",
    "actor",
Eliot Berriot's avatar
Eliot Berriot committed
846
847
848
] + env.list("ACCOUNT_USERNAME_BLACKLIST", default=[])

EXTERNAL_REQUESTS_VERIFY_SSL = env.bool("EXTERNAL_REQUESTS_VERIFY_SSL", default=True)
849
850
# XXX: deprecated, see #186
API_AUTHENTICATION_REQUIRED = env.bool("API_AUTHENTICATION_REQUIRED", True)
851

Eliot Berriot's avatar
Eliot Berriot committed
852
MUSIC_DIRECTORY_PATH = env("MUSIC_DIRECTORY_PATH", default=None)
853
854
855
# on Docker setup, the music directory may not match the host path,
# and we need to know it for it to serve stuff properly
MUSIC_DIRECTORY_SERVE_PATH = env(
Eliot Berriot's avatar
Eliot Berriot committed
856
857
    "MUSIC_DIRECTORY_SERVE_PATH", default=MUSIC_DIRECTORY_PATH
)
Eliot Berriot's avatar
Eliot Berriot committed
858
859
860
861

USERS_INVITATION_EXPIRATION_DAYS = env.int(
    "USERS_INVITATION_EXPIRATION_DAYS", default=14
)
862
863
864
865
866
867
868
869
870
871

VERSATILEIMAGEFIELD_RENDITION_KEY_SETS = {
    "square": [
        ("original", "url"),
        ("square_crop", "crop__400x400"),
        ("medium_square_crop", "crop__200x200"),
        ("small_square_crop", "crop__50x50"),
    ]
}
VERSATILEIMAGEFIELD_SETTINGS = {"create_images_on_demand": False}
872
873
874
875
RSA_KEY_SIZE = 2048
# for performance gain in tests, since we don't need to actually create the
# thumbnails
CREATE_IMAGE_THUMBNAILS = env.bool("CREATE_IMAGE_THUMBNAILS", default=True)
876
877
# we rotate actor keys at most every two days by default
ACTOR_KEY_ROTATION_DELAY = env.int("ACTOR_KEY_ROTATION_DELAY", default=3600 * 48)
878
879
880
SUBSONIC_DEFAULT_TRANSCODING_FORMAT = (
    env("SUBSONIC_DEFAULT_TRANSCODING_FORMAT", default="mp3") or None
)
881
882
883

# extra tags will be ignored
TAGS_MAX_BY_OBJ = env.int("TAGS_MAX_BY_OBJ", default=30)
884
885
886
FEDERATION_OBJECT_FETCH_DELAY = env.int(
    "FEDERATION_OBJECT_FETCH_DELAY", default=60 * 24 * 3
)
887
888
889
890

MODERATION_EMAIL_NOTIFICATIONS_ENABLED = env.bool(
    "MODERATION_EMAIL_NOTIFICATIONS_ENABLED", default=True
)
891
892
893
894

# Delay in days after signup before we show the "support us" messages
INSTANCE_SUPPORT_MESSAGE_DELAY = env.int("INSTANCE_SUPPORT_MESSAGE_DELAY", default=15)
FUNKWHALE_SUPPORT_MESSAGE_DELAY = env.int("FUNKWHALE_SUPPORT_MESSAGE_DELAY", default=15)