import os import re from collections.abc import Iterable, Sequence from typing import Any from django.apps.config import AppConfig from django.conf import settings from django.core import checks from django.core.exceptions import ValidationError from django.core.validators import URLValidator def get_settings_error_message_strings(setting_name: str) -> tuple[str, str]: # Even in Docker, MANUAL_CONFIGURATION means the admin # manages /etc/zulip/settings.py themselves, so the SETTING_* # environment variables are not where to make changes. if settings.RUNNING_IN_HELM: settings_location = "your Helm values" setting_display_name = "zulip.environment.SETTING_" + setting_name elif settings.RUNNING_IN_DOCKER and os.environ.get("MANUAL_CONFIGURATION") != "True": settings_location = "your Docker environment configuration" setting_display_name = "SETTING_" + setting_name else: settings_location = "/etc/zulip/settings.py" setting_display_name = setting_name return settings_location, setting_display_name def check_required_settings( app_configs: Sequence[AppConfig] | None, databases: Sequence[str] | None, **kwargs: Any, ) -> Iterable[checks.CheckMessage]: # These are the settings that we will check that the user has filled in for # production deployments before starting the app. It consists of a series # of pairs of (setting name, default value that it must be changed from) required_settings = [ ("EXTERNAL_HOST", "zulip.example.com"), ("ZULIP_ADMINISTRATOR", "zulip-admin@example.com"), # SECRET_KEY doesn't really need to be here, in # that we set it automatically, but just in # case, it seems worth having in this list ("SECRET_KEY", ""), ("AUTHENTICATION_BACKENDS", ()), ] errors = [] for setting_name, default in required_settings: value = getattr(settings, setting_name, None) if value and value != default: continue settings_location, setting_display_name = get_settings_error_message_strings(setting_name) if value: # The setting is still the example value from the # documentation, which the admin must replace -- saying # "you must set" it would be confusing, as it is set. message = ( f"{setting_display_name} is still set to the example value {default!r}; " f"change it in {settings_location}" ) else: message = f"You must set {setting_display_name} in {settings_location}" errors.append( checks.Error( message, obj=f"settings.{setting_name}", id="zulip.E001", ) ) return errors def check_external_host_setting( app_configs: Sequence[AppConfig] | None, databases: Sequence[str] | None, **kwargs: Any, ) -> Iterable[checks.CheckMessage]: if not hasattr(settings, "EXTERNAL_HOST"): # nocoverage return [] errors = [] scheme = settings.EXTERNAL_URI_SCHEME if scheme != "https://" and not settings.DEVELOPMENT: errors.append( checks.Error( "Zulip does not support a non-HTTPS external scheme in production", obj="settings.EXTERNAL_URI_SCHEME", hint="Do not override EXTERNAL_URI_SCHEME in production", id="zulip.E004", ) ) hostname = settings.EXTERNAL_HOST if "." not in hostname and os.environ.get("ZULIP_TEST_SUITE") != "true" and settings.PRODUCTION: suggest = ".localdomain" if hostname == "localhost" else ".local" errors.append( checks.Error( f"EXTERNAL_HOST ({hostname}) does not contain a domain part", obj="settings.EXTERNAL_HOST", hint=f"Add {suggest} to the end", id="zulip.E002", ) ) if ":" in hostname: hostname = hostname.split(":")[0] if len(hostname) > 255: errors.append( checks.Error( f"EXTERNAL_HOST ({hostname}) is too long to be a valid hostname", obj="settings.EXTERNAL_HOST", id="zulip.E002", ) ) domain_part = re.compile(r"(?!-)[A-Z\d-]{1,63}(? Iterable[checks.CheckMessage]: errors = [] for idp_name, idp_dict in settings.SOCIAL_AUTH_SAML_ENABLED_IDPS.items(): if "zulip_groups" in idp_dict.get("extra_attrs", []): errors.append( checks.Error( "zulip_groups can't be listed in extra_attrs", obj=f'settings.SOCIAL_AUTH_SAML_ENABLED_IDPS["{idp_name}"]["extra_attrs"]', id="zulip.E003", ) ) for subdomain, config_dict in settings.SOCIAL_AUTH_SYNC_ATTRS_DICT.items(): for auth_name, attrs_map in config_dict.items(): for attr_key, attr_value in attrs_map.items(): if attr_value == "zulip_groups": errors.append( checks.Error( "zulip_groups can't be listed as a SAML attribute", obj=f'settings.SOCIAL_AUTH_SYNC_ATTRS_DICT["{subdomain}"]["{auth_name}"]["{attr_key}"]', id="zulip.E004", ) ) return errors def check_uploads_settings( app_configs: Sequence[AppConfig] | None, databases: Sequence[str] | None, **kwargs: Any, ) -> Iterable[checks.CheckMessage]: uploads_dir = settings.LOCAL_UPLOADS_DIR if uploads_dir is None: errors = [] if settings.S3_AUTH_UPLOADS_BUCKET == "": errors.append( checks.Error( "Neither settings.LOCAL_UPLOADS_DIR nor settings.S3_AUTH_UPLOADS_BUCKET is set", obj="settings.S3_AUTH_UPLOADS_BUCKET", id="zulip.E005", ) ) if settings.S3_AVATAR_BUCKET == "": errors.append( checks.Error( "Neither settings.LOCAL_UPLOADS_DIR nor settings.S3_AVATAR_BUCKET is set", obj="settings.S3_AVATAR_BUCKET", id="zulip.E005", ) ) return errors if not os.path.isdir(uploads_dir): return [ checks.Error( f"settings.LOCAL_UPLOADS_DIR ({uploads_dir}) does not exist", obj="settings.LOCAL_UPLOADS_DIR", id="zulip.E006", ) ] elif not os.access(uploads_dir, os.W_OK): return [ checks.Error( f"settings.LOCAL_UPLOADS_DIR ({uploads_dir}) is not writable", obj="settings.LOCAL_UPLOADS_DIR", id="zulip.E006", ) ] return [] def check_jitsi_server_url( app_configs: Sequence[AppConfig] | None, databases: Sequence[str] | None, **kwargs: Any, ) -> Iterable[checks.CheckMessage]: # As of Zulip 12.0, the web app will not load if a server-level # Jitsi Meet integration has been configured that is not a URL. # A None value is the valid way to disable the integration. By # default, the integration uses the SaaS https://meet.jit.si # server. jitsi_server_url = settings.JITSI_SERVER_URL if jitsi_server_url is None or jitsi_server_url == "https://meet.jit.si": return [] assert isinstance(jitsi_server_url, str) validate = URLValidator() try: validate(jitsi_server_url) return [] except ValidationError: settings_location, setting_display_name = get_settings_error_message_strings( "JITSI_SERVER_URL" ) message = f"{setting_display_name} in {settings_location} is not a URL" return [ checks.Error( message, obj="settings.JITSI_SERVER_URL", id="zulip.E007", ) ]