Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
247 lines
8.9 KiB
Python
247 lines
8.9 KiB
Python
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}(?<!-)$", re.IGNORECASE)
|
|
if not hostname.isascii():
|
|
suggestion = ".".join(
|
|
"xn--" + part.encode("punycode").decode() if not part.isascii() else part
|
|
for part in hostname.split(".")
|
|
)
|
|
errors.append(
|
|
checks.Error(
|
|
f"EXTERNAL_HOST ({hostname}) contains non-ASCII characters",
|
|
hint=f"Switch to punycode: {suggestion}",
|
|
obj="settings.EXTERNAL_HOST",
|
|
id="zulip.E002",
|
|
)
|
|
)
|
|
elif not all(domain_part.match(x) for x in hostname.split(".")):
|
|
errors.append(
|
|
checks.Error(
|
|
f"EXTERNAL_HOST ({hostname}) does not validate as a hostname",
|
|
obj="settings.EXTERNAL_HOST",
|
|
id="zulip.E002",
|
|
)
|
|
)
|
|
return errors
|
|
|
|
|
|
def check_auth_settings(
|
|
app_configs: Sequence[AppConfig] | None,
|
|
databases: Sequence[str] | None,
|
|
**kwargs: Any,
|
|
) -> 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",
|
|
)
|
|
]
|