snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
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
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
This commit is contained in:
0
zproject/__init__.py
Normal file
0
zproject/__init__.py
Normal file
4540
zproject/backends.py
Normal file
4540
zproject/backends.py
Normal file
File diff suppressed because it is too large
Load Diff
1328
zproject/computed_settings.py
Normal file
1328
zproject/computed_settings.py
Normal file
File diff suppressed because it is too large
Load Diff
67
zproject/config.py
Normal file
67
zproject/config.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import configparser
|
||||
import os
|
||||
from typing import overload
|
||||
|
||||
from scripts.lib.zulip_tools import get_config as get_config_from_file
|
||||
|
||||
|
||||
class ZulipSettingsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
DEPLOY_ROOT = os.path.realpath(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
config_file = configparser.RawConfigParser()
|
||||
config_file.read("/etc/zulip/zulip.conf")
|
||||
|
||||
# Whether this instance of Zulip is running in a production environment.
|
||||
PRODUCTION = config_file.has_option("machine", "deploy_type")
|
||||
DEVELOPMENT = not PRODUCTION
|
||||
secrets_file = configparser.RawConfigParser()
|
||||
if PRODUCTION: # nocoverage
|
||||
secrets_file.read("/etc/zulip/zulip-secrets.conf")
|
||||
else:
|
||||
secrets_file.read(os.path.join(DEPLOY_ROOT, "zproject/dev-secrets.conf"))
|
||||
|
||||
|
||||
@overload
|
||||
def get_secret(
|
||||
key: str, default_value: None = None, development_only: bool = False
|
||||
) -> str | None: ...
|
||||
@overload
|
||||
def get_secret(key: str, default_value: str, development_only: bool = False) -> str: ...
|
||||
def get_secret(
|
||||
key: str, default_value: str | None = None, development_only: bool = False
|
||||
) -> str | None:
|
||||
if development_only and PRODUCTION: # nocoverage
|
||||
return default_value
|
||||
return secrets_file.get("secrets", key, fallback=default_value)
|
||||
|
||||
|
||||
def get_mandatory_secret(key: str) -> str:
|
||||
secret = get_secret(key)
|
||||
if secret is None:
|
||||
if os.environ.get("DISABLE_MANDATORY_SECRET_CHECK") == "True":
|
||||
return ""
|
||||
raise ZulipSettingsError(f'Mandatory secret "{key}" is not set')
|
||||
return secret
|
||||
|
||||
|
||||
@overload
|
||||
def get_config(section: str, key: str, default_value: None = None) -> str | None: ...
|
||||
@overload
|
||||
def get_config(section: str, key: str, default_value: str) -> str: ...
|
||||
@overload
|
||||
def get_config(section: str, key: str, default_value: bool) -> bool: ...
|
||||
def get_config(
|
||||
section: str, key: str, default_value: str | bool | None = None
|
||||
) -> str | bool | None:
|
||||
return get_config_from_file(config_file, section, key, default_value)
|
||||
|
||||
|
||||
def get_from_file_if_exists(path: str) -> str:
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
return f.read()
|
||||
else:
|
||||
return ""
|
||||
41
zproject/configured_settings.py
Normal file
41
zproject/configured_settings.py
Normal file
@@ -0,0 +1,41 @@
|
||||
########################################################################
|
||||
# DEFAULT VALUES FOR SETTINGS
|
||||
########################################################################
|
||||
|
||||
import os
|
||||
|
||||
# For any settings that are not set in the site-specific configuration file
|
||||
# (/etc/zulip/settings.py in production, or dev_settings.py or test_settings.py
|
||||
# in dev and test), we want to initialize them to sane defaults.
|
||||
from .default_settings import * # noqa: F403 isort: skip
|
||||
|
||||
# Import variables like secrets from the prod_settings file
|
||||
# Import prod_settings after determining the deployment/machine type
|
||||
from .config import PRODUCTION
|
||||
|
||||
TEST_SUITE = os.getenv("ZULIP_TEST_SUITE") == "true"
|
||||
CUSTOM_DEVELOPMENT_SETTINGS = False
|
||||
|
||||
if PRODUCTION: # nocoverage
|
||||
from .prod_settings import * # noqa: F403 isort: skip
|
||||
else:
|
||||
# For the Dev VM environment, we use the same settings as the
|
||||
# sample prod_settings.py file, with a few exceptions.
|
||||
from .prod_settings_template import * # noqa: F403 isort: skip
|
||||
from .dev_settings import * # noqa: F403 isort: skip
|
||||
|
||||
# Support for local overrides to dev_settings.py is implemented here.
|
||||
#
|
||||
# We're careful to avoid those overrides applying to automated tests.
|
||||
if not TEST_SUITE: # nocoverage
|
||||
import contextlib
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
from zproject.custom_dev_settings import * # type: ignore[import, unused-ignore] # noqa: F403
|
||||
|
||||
# Track that we've got settings changes, so you know if
|
||||
# you're testing non-base code; runtornado will log.
|
||||
CUSTOM_DEVELOPMENT_SETTINGS = True
|
||||
|
||||
# Do not add any code after these wildcard imports! Add it to
|
||||
# computed_settings instead.
|
||||
791
zproject/default_settings.py
Normal file
791
zproject/default_settings.py
Normal file
@@ -0,0 +1,791 @@
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from email.headerregistry import Address
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional
|
||||
|
||||
from django_auth_ldap.config import GroupOfUniqueNamesType, LDAPGroupType
|
||||
|
||||
from scripts.lib.zulip_tools import deport
|
||||
from zproject.settings_types import JwtAuthKey, OIDCIdPConfigDict, SAMLIdPConfigDict, SCIMConfigDict
|
||||
|
||||
from .config import DEVELOPMENT, PRODUCTION, get_config, get_secret
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_auth_ldap.config import LDAPSearch
|
||||
|
||||
if PRODUCTION: # nocoverage
|
||||
from .prod_settings import EXTERNAL_HOST, ZULIP_ADMINISTRATOR
|
||||
else:
|
||||
from .dev_settings import EXTERNAL_HOST, ZULIP_ADMINISTRATOR
|
||||
|
||||
DEBUG = DEVELOPMENT
|
||||
|
||||
EXTERNAL_HOST_WITHOUT_PORT = deport(EXTERNAL_HOST)
|
||||
|
||||
STATIC_URL: str | None = None
|
||||
|
||||
# These settings are intended for the server admin to set. We document them in
|
||||
# prod_settings_template.py, and in the initial /etc/zulip/settings.py on a new
|
||||
# install of the Zulip server.
|
||||
|
||||
# Extra HTTP "Host" values to allow (standard ones added in computed_settings.py)
|
||||
ALLOWED_HOSTS: list[str] = []
|
||||
|
||||
# Basic email settings
|
||||
NOREPLY_EMAIL_ADDRESS = Address(username="noreply", domain=EXTERNAL_HOST_WITHOUT_PORT).addr_spec
|
||||
ADD_TOKENS_TO_NOREPLY_ADDRESS = True
|
||||
TOKENIZED_NOREPLY_EMAIL_ADDRESS = Address(
|
||||
username="noreply-{token}", domain=EXTERNAL_HOST_WITHOUT_PORT
|
||||
).addr_spec
|
||||
PHYSICAL_ADDRESS = ""
|
||||
FAKE_EMAIL_DOMAIN = EXTERNAL_HOST_WITHOUT_PORT
|
||||
EMAIL_MAX_CONNECTION_LIFETIME_IN_MINUTES: int | None = 0
|
||||
|
||||
# SMTP settings
|
||||
EMAIL_HOST: str | None = None
|
||||
# Other settings, like EMAIL_HOST_USER, EMAIL_PORT, and EMAIL_USE_TLS,
|
||||
# we leave up to Django's defaults.
|
||||
|
||||
# LDAP auth
|
||||
AUTH_LDAP_SERVER_URI = ""
|
||||
AUTH_LDAP_BIND_DN = ""
|
||||
AUTH_LDAP_USER_SEARCH: Optional["LDAPSearch"] = None
|
||||
LDAP_APPEND_DOMAIN: str | None = None
|
||||
LDAP_EMAIL_ATTR: str | None = None
|
||||
AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
|
||||
AUTH_LDAP_USERNAME_ATTR: str | None = None
|
||||
|
||||
# AUTH_LDAP_USER_ATTR_MAP is uncommented in prod_settings_template.py,
|
||||
# so the value here mainly serves to help document the default.
|
||||
AUTH_LDAP_USER_ATTR_MAP: dict[str, str] = {
|
||||
"full_name": "cn",
|
||||
}
|
||||
# Automatically deactivate users not found by the AUTH_LDAP_USER_SEARCH query.
|
||||
LDAP_DEACTIVATE_NON_MATCHING_USERS: bool | None = None
|
||||
# AUTH_LDAP_CONNECTION_OPTIONS: we set ldap.OPT_REFERRALS in settings.py if unset.
|
||||
AUTH_LDAP_CONNECTION_OPTIONS: dict[int, object] = {}
|
||||
# Disable django-auth-ldap caching, to prevent problems with OU changes.
|
||||
AUTH_LDAP_CACHE_TIMEOUT = 0
|
||||
# Disable syncing user on each login; Using sync_ldap_user_data cron is recommended.
|
||||
AUTH_LDAP_ALWAYS_UPDATE_USER = False
|
||||
# Development-only settings for fake LDAP authentication; used to
|
||||
# support local development of LDAP auth without an LDAP server.
|
||||
# Detailed docs in zproject/dev_settings.py.
|
||||
FAKE_LDAP_MODE: str | None = None
|
||||
FAKE_LDAP_NUM_USERS = 8
|
||||
AUTH_LDAP_ADVANCED_REALM_ACCESS_CONTROL: dict[str, Any] | None = None
|
||||
LDAP_SYNCHRONIZED_GROUPS_BY_REALM: dict[str, list[str]] = {}
|
||||
AUTH_LDAP_GROUP_TYPE: LDAPGroupType = GroupOfUniqueNamesType()
|
||||
|
||||
# Social auth; we support providing values for some of these
|
||||
# settings in zulip-secrets.conf instead of settings.py in development.
|
||||
SOCIAL_AUTH_GITHUB_KEY = get_secret("social_auth_github_key", development_only=True)
|
||||
SOCIAL_AUTH_GITHUB_ORG_NAME: str | None = None
|
||||
SOCIAL_AUTH_GITHUB_TEAM_ID: str | None = None
|
||||
SOCIAL_AUTH_GITLAB_KEY = get_secret("social_auth_gitlab_key", development_only=True)
|
||||
SOCIAL_AUTH_SUBDOMAIN: str | None = None
|
||||
SOCIAL_AUTH_AZUREAD_OAUTH2_KEY = get_secret("social_auth_azuread_oauth2_key", development_only=True)
|
||||
SOCIAL_AUTH_GOOGLE_KEY = get_secret("social_auth_google_key", development_only=True)
|
||||
SOCIAL_AUTH_DISCORD_KEY = get_secret("social_auth_discord_key", development_only=True)
|
||||
# SAML:
|
||||
SOCIAL_AUTH_SAML_SP_ENTITY_ID: str | None = None
|
||||
SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = ""
|
||||
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = ""
|
||||
SOCIAL_AUTH_SAML_ORG_INFO: dict[str, dict[str, str]] | None = None
|
||||
SOCIAL_AUTH_SAML_TECHNICAL_CONTACT: dict[str, str] | None = None
|
||||
SOCIAL_AUTH_SAML_SUPPORT_CONTACT: dict[str, str] | None = None
|
||||
SOCIAL_AUTH_SAML_ENABLED_IDPS: dict[str, SAMLIdPConfigDict] = {}
|
||||
SOCIAL_AUTH_SAML_SECURITY_CONFIG: dict[str, Any] = {}
|
||||
|
||||
# Set this to True to enforce that any configured IdP needs to specify
|
||||
# the limit_to_subdomains setting to be considered valid:
|
||||
SAML_REQUIRE_LIMIT_TO_SUBDOMAINS = False
|
||||
OIDC_REQUIRE_LIMIT_TO_SUBDOMAINS = False
|
||||
|
||||
# Historical name for SOCIAL_AUTH_GITHUB_KEY; still allowed in production.
|
||||
GOOGLE_OAUTH2_CLIENT_ID: str | None = None
|
||||
|
||||
# Apple:
|
||||
SOCIAL_AUTH_APPLE_SERVICES_ID = get_secret("social_auth_apple_services_id", development_only=True)
|
||||
SOCIAL_AUTH_APPLE_APP_ID = get_secret("social_auth_apple_app_id", development_only=True)
|
||||
SOCIAL_AUTH_APPLE_KEY = get_secret("social_auth_apple_key", development_only=True)
|
||||
SOCIAL_AUTH_APPLE_TEAM = get_secret("social_auth_apple_team", development_only=True)
|
||||
SOCIAL_AUTH_APPLE_SCOPE = ["name", "email"]
|
||||
SOCIAL_AUTH_APPLE_EMAIL_AS_USERNAME = True
|
||||
|
||||
# Generic OpenID Connect:
|
||||
SOCIAL_AUTH_OIDC_ENABLED_IDPS: dict[str, OIDCIdPConfigDict] = {}
|
||||
SOCIAL_AUTH_OIDC_FULL_NAME_VALIDATED = False
|
||||
|
||||
SOCIAL_AUTH_SYNC_ATTRS_DICT: dict[
|
||||
str, dict[str, dict[str, str | bool | list[str | tuple[str, str]]]]
|
||||
] = {}
|
||||
|
||||
# Other auth
|
||||
SSO_APPEND_DOMAIN: str | None = None
|
||||
CUSTOM_HOME_NOT_LOGGED_IN: str | None = None
|
||||
|
||||
VIDEO_ZOOM_API_URL: str = "https://api.zoom.us"
|
||||
VIDEO_ZOOM_OAUTH_URL: str = "https://zoom.us"
|
||||
VIDEO_ZOOM_SERVER_TO_SERVER_ACCOUNT_ID = get_secret("video_zoom_account_id", development_only=True)
|
||||
VIDEO_ZOOM_CLIENT_ID = get_secret("video_zoom_client_id", development_only=True)
|
||||
VIDEO_ZOOM_CLIENT_SECRET = get_secret("video_zoom_client_secret")
|
||||
VIDEO_WEBEX_API_URL: str = "https://webexapis.com/v1/"
|
||||
VIDEO_WEBEX_CLIENT_ID = get_secret("video_webex_client_id", development_only=True)
|
||||
VIDEO_WEBEX_CLIENT_SECRET = get_secret("video_webex_client_secret")
|
||||
|
||||
# Email gateway
|
||||
EMAIL_GATEWAY_PATTERN = ""
|
||||
EMAIL_GATEWAY_LOGIN: str | None = None
|
||||
EMAIL_GATEWAY_IMAP_SERVER: str | None = None
|
||||
EMAIL_GATEWAY_IMAP_PORT: int | None = None
|
||||
EMAIL_GATEWAY_IMAP_FOLDER: str | None = None
|
||||
# Not documented for in /etc/zulip/settings.py, since it's rarely needed.
|
||||
EMAIL_GATEWAY_EXTRA_PATTERN_HACK: str | None = None
|
||||
|
||||
# Error reporting
|
||||
ERROR_REPORTING = True
|
||||
LOGGING_SHOW_MODULE = False
|
||||
LOGGING_SHOW_PID = False
|
||||
|
||||
# Sentry.io error defaults to off
|
||||
SENTRY_DSN: str | None = get_config("sentry", "project_dsn", None)
|
||||
SENTRY_TRACE_WORKER_RATE: float | dict[str, float] = 0.0
|
||||
SENTRY_TRACE_RATE: float = 0.0
|
||||
SENTRY_PROFILE_RATE: float = 0.1
|
||||
SENTRY_FRONTEND_DSN: str | None = get_config("sentry", "frontend_project_dsn", None)
|
||||
SENTRY_FRONTEND_SAMPLE_RATE: float = 1.0
|
||||
SENTRY_FRONTEND_TRACE_RATE: float = 0.1
|
||||
|
||||
# File uploads and avatars
|
||||
# TODO: Rename MAX_FILE_UPLOAD_SIZE to have unit in name.
|
||||
DEFAULT_AVATAR_URI: str | None = None
|
||||
DEFAULT_LOGO_URI: str | None = None
|
||||
S3_AVATAR_BUCKET = ""
|
||||
S3_AUTH_UPLOADS_BUCKET = ""
|
||||
S3_EXPORT_BUCKET = ""
|
||||
S3_REGION: str | None = None
|
||||
S3_ENDPOINT_URL: str | None = None
|
||||
S3_ADDRESSING_STYLE: Literal["auto", "virtual", "path"] = "auto"
|
||||
S3_SKIP_PROXY = True
|
||||
S3_UPLOADS_STORAGE_CLASS: Literal[
|
||||
"GLACIER_IR",
|
||||
"INTELLIGENT_TIERING",
|
||||
"ONEZONE_IA",
|
||||
"REDUCED_REDUNDANCY",
|
||||
"STANDARD",
|
||||
"STANDARD_IA",
|
||||
] = "STANDARD"
|
||||
S3_AVATAR_PUBLIC_URL_PREFIX: str | None = None
|
||||
S3_SKIP_CHECKSUM: bool = False
|
||||
LOCAL_UPLOADS_DIR: str | None = None
|
||||
LOCAL_AVATARS_DIR: str | None = None
|
||||
LOCAL_FILES_DIR: str | None = None
|
||||
MAX_FILE_UPLOAD_SIZE = 100
|
||||
# How many GB an organization on a cloud standard plan can upload per user,
|
||||
# on zulipchat.com.
|
||||
UPLOAD_QUOTA_PER_USER_GB_FOR_STANDARD = 5
|
||||
|
||||
# How many GB an organization on a cloud plus plan can upload per user,
|
||||
# on zulipchat.com.
|
||||
UPLOAD_QUOTA_PER_USER_GB_FOR_PLUS = 25
|
||||
|
||||
# Jitsi Meet video call integration; set to None to disable integration.
|
||||
JITSI_SERVER_URL: str | None = "https://meet.jit.si"
|
||||
|
||||
# GIPHY API key.
|
||||
GIPHY_API_KEY = get_secret("giphy_api_key")
|
||||
|
||||
# Tenor API key
|
||||
TENOR_API_KEY = get_secret("tenor_api_key")
|
||||
|
||||
# Klipy API key
|
||||
KLIPY_API_KEY = get_secret("klipy_api_key")
|
||||
|
||||
# Allow setting BigBlueButton settings in zulip-secrets.conf in
|
||||
# development; this is useful since there are no public BigBlueButton servers.
|
||||
BIG_BLUE_BUTTON_URL = get_secret("big_blue_button_url", development_only=True)
|
||||
|
||||
# Allow setting Constructor Groups URL in development.
|
||||
CONSTRUCTOR_GROUPS_URL = get_secret("constructor_groups_url", development_only=True)
|
||||
|
||||
# Allow setting Nextcloud Talk settings in zulip-secrets.conf in
|
||||
# development; this is useful since there are no public Nextcloud Talk servers.
|
||||
NEXTCLOUD_SERVER = get_secret("nextcloud_server", development_only=True)
|
||||
|
||||
# Max state storage per user
|
||||
# TODO: Add this to zproject/prod_settings_template.py once stateful bots are fully functional.
|
||||
USER_STATE_SIZE_LIMIT = 10000000
|
||||
# Max size of a single configuration entry of an embedded bot.
|
||||
BOT_CONFIG_SIZE_LIMIT = 10000
|
||||
|
||||
# External service configuration
|
||||
CAMO_URI = ""
|
||||
KATEX_SERVER = get_config("application_server", "katex_server", True)
|
||||
KATEX_SERVER_PORT = get_config("application_server", "katex_server_port", "9700")
|
||||
MEMCACHED_LOCATION = "127.0.0.1:11211"
|
||||
MEMCACHED_USERNAME = None if get_secret("memcached_password") is None else "zulip@localhost"
|
||||
RABBITMQ_HOST = "127.0.0.1"
|
||||
RABBITMQ_PORT = 5672
|
||||
RABBITMQ_VHOST = "/"
|
||||
RABBITMQ_USERNAME = "zulip"
|
||||
RABBITMQ_USE_TLS = False
|
||||
REDIS_HOST = "127.0.0.1"
|
||||
REDIS_PORT = 6379
|
||||
REMOTE_POSTGRES_HOST = ""
|
||||
REMOTE_POSTGRES_PORT = 5432
|
||||
REMOTE_POSTGRES_SSLMODE = "verify-full"
|
||||
|
||||
TORNADO_PORTS: list[int] = []
|
||||
USING_TORNADO = True
|
||||
|
||||
# ToS/Privacy templates
|
||||
POLICIES_DIRECTORY: str = "zerver/policies_absent"
|
||||
|
||||
# Security
|
||||
ENABLE_FILE_LINKS = False
|
||||
ENABLE_GRAVATAR = True
|
||||
## Overrides the above setting for individual realms, by integer ID.
|
||||
GRAVATAR_REALM_OVERRIDE: dict[int, bool] = {}
|
||||
INLINE_IMAGE_PREVIEW = True
|
||||
INLINE_URL_EMBED_PREVIEW = True
|
||||
NAME_CHANGES_DISABLED = False
|
||||
AVATAR_CHANGES_DISABLED = False
|
||||
PASSWORD_MIN_LENGTH = 8
|
||||
PASSWORD_MAX_LENGTH = 100
|
||||
PASSWORD_MIN_GUESSES = 10000
|
||||
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
|
||||
SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2 # 2 weeks
|
||||
|
||||
ZULIP_SERVICES_URL: str | None = "https://push.zulipchat.com"
|
||||
ZULIP_SERVICE_PUSH_NOTIFICATIONS = False
|
||||
|
||||
# For this setting, we need to have None as the default value, so
|
||||
# that we can distinguish between the case of the setting not being
|
||||
# set at all and being disabled (set to False).
|
||||
# That's because unless the setting is explicitly configured, we want to
|
||||
# enable it in computed_settings when ZULIP_SERVICE_PUSH_NOTIFICATIONS
|
||||
# is enabled.
|
||||
ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS: bool | None = None
|
||||
ZULIP_SERVICE_SECURITY_ALERTS = False
|
||||
|
||||
# Old setting kept around for backwards compatibility. Some old servers
|
||||
# may have it in their settings.py.
|
||||
PUSH_NOTIFICATION_BOUNCER_URL: str | None = None
|
||||
# Keep this default True, so that legacy deployments that configured PUSH_NOTIFICATION_BOUNCER_URL
|
||||
# without overriding SUBMIT_USAGE_STATISTICS get the original behavior. If a server configures
|
||||
# the modern ZULIP_SERVICE_* settings, all this will be ignored.
|
||||
SUBMIT_USAGE_STATISTICS = True
|
||||
|
||||
PROMOTE_SPONSORING_ZULIP = True
|
||||
RATE_LIMITING = True
|
||||
RATE_LIMITING_AUTHENTICATE = True
|
||||
RATE_LIMIT_TOR_TOGETHER = False
|
||||
SEND_LOGIN_EMAILS = True
|
||||
EMBEDDED_BOTS_ENABLED = False
|
||||
|
||||
USING_CAPTCHA = False
|
||||
DEFAULT_RATE_LIMITING_RULES = {
|
||||
# Limits total number of API requests per unit time by each user.
|
||||
# Rate limiting general API access protects the server against
|
||||
# clients causing unreasonable server load.
|
||||
"api_by_user": [
|
||||
# 200 requests per minute
|
||||
(60, 200),
|
||||
],
|
||||
# Limits total number of unauthenticated API requests (primarily
|
||||
# used by the public access option). Since these are
|
||||
# unauthenticated requests, each IPv4 address is a separate bucket.
|
||||
# For IPv6, one bucket is used for each /64 subnet.
|
||||
"api_by_ip": [
|
||||
# 100 requests per minute.
|
||||
(60, 100),
|
||||
],
|
||||
# Limits total requests to the Mobile Push Notifications Service
|
||||
# by each individual Zulip server that is using the service. This
|
||||
# is a Zulip Cloud setting that has no effect on self-hosted Zulip
|
||||
# servers that are not hosting their own copy of the push
|
||||
# notifications service.
|
||||
"api_by_remote_server": [
|
||||
(60, 1000),
|
||||
],
|
||||
# Limits how many authentication attempts with login+password can
|
||||
# be made to a single username. This applies to the authentication
|
||||
# backends such as LDAP or email+password where a login+password
|
||||
# gets submitted to the Zulip server. No limit is applied for
|
||||
# external authentication methods (like GitHub SSO), since with
|
||||
# those authentication backends, we only receive a username if
|
||||
# authentication is successful.
|
||||
"authenticate_by_username": [
|
||||
# 5 failed login attempts within 30 minutes
|
||||
(1800, 5),
|
||||
],
|
||||
# Limits how many requests a user can make to change their email
|
||||
# address. A low/strict limit is recommended here, since there is
|
||||
# not real use case for triggering several of these from a single
|
||||
# user account, and by definition, the emails are sent to an email
|
||||
# address that does not already have a relationship with Zulip, so
|
||||
# this feature can be abused to attack the server's spam
|
||||
# reputation. Applies in addition to sends_email_by_ip.
|
||||
"email_change_by_user": [
|
||||
# 2 emails per hour, and up to 5 per day.
|
||||
(3600, 2),
|
||||
(86400, 5),
|
||||
],
|
||||
# Limits how many requests to send password reset emails can be
|
||||
# made for a single email address. A low/strict limit is
|
||||
# desirable, since this feature could be used to spam users with
|
||||
# password reset emails, given their email address. Applies in
|
||||
# addition to sends_email_by_ip, below.
|
||||
"password_reset_form_by_email": [
|
||||
# 2 emails per hour, and up to 5 per day.
|
||||
(3600, 2),
|
||||
(86400, 5),
|
||||
],
|
||||
# This limit applies to all requests which directly trigger the
|
||||
# sending of an email, restricting the number per IP address. This
|
||||
# is a general anti-spam measure.
|
||||
"sends_email_by_ip": [
|
||||
# 5 emails per day.
|
||||
(86400, 5),
|
||||
],
|
||||
# Limits access to uploaded files, in web-public contexts, done by
|
||||
# unauthenticated users. Each file gets its own bucket, and every
|
||||
# access to the file by an unauthenticated user counts towards the
|
||||
# limit. This is important to prevent abuse of Zulip's file
|
||||
# uploads feature for file distribution.
|
||||
"spectator_attachment_access_by_file": [
|
||||
# 1000 per day per file
|
||||
(86400, 1000),
|
||||
],
|
||||
# A zilencer-only limit that applies to requests to the
|
||||
# remote billing system that trigger the sending of an email.
|
||||
"sends_email_by_remote_server": [
|
||||
# 10 emails per day
|
||||
(86400, 10),
|
||||
],
|
||||
# Limits how many demo organizations can be created per IP
|
||||
# address. This is important to prevent abuse of the demo
|
||||
# organization feature.
|
||||
"demo_realm_creation_by_ip": [
|
||||
# 10 demos per day
|
||||
(86400, 10),
|
||||
],
|
||||
"transfer_remote_server_registration_endpoint_by_ip": [
|
||||
# 10 transfer registration requests per day per IP
|
||||
(86400, 10),
|
||||
],
|
||||
}
|
||||
# Rate limiting defaults can be individually overridden by adding
|
||||
# entries in this object, which is merged with
|
||||
# DEFAULT_RATE_LIMITING_RULES.
|
||||
RATE_LIMITING_RULES: dict[str, list[tuple[int, int]]] = {}
|
||||
|
||||
# Rate limits for endpoints which have absolute limits on how much
|
||||
# they can be used in a given time period.
|
||||
# These will be extremely rare, and most likely for zilencer endpoints
|
||||
# only, so we don't need a nice overriding system for them like we do
|
||||
# for RATE_LIMITING_RULES.
|
||||
ABSOLUTE_USAGE_LIMITS_BY_ENDPOINT = {
|
||||
"verify_registration_transfer_challenge_ack_endpoint": [
|
||||
# 30 requests per day
|
||||
(86400, 30),
|
||||
],
|
||||
}
|
||||
|
||||
# Two factor authentication is not yet implementation-complete
|
||||
TWO_FACTOR_AUTHENTICATION_ENABLED = False
|
||||
|
||||
# The new user tutorial can be disabled for self-hosters who want to
|
||||
# disable the tutorial entirely on their system. Primarily useful for
|
||||
# products embedding Zulip as their chat feature.
|
||||
TUTORIAL_ENABLED = True
|
||||
|
||||
# We log emails in development environment for accessing
|
||||
# them easily through /emails page
|
||||
DEVELOPMENT_LOG_EMAILS = DEVELOPMENT
|
||||
|
||||
# The push bouncer expects to get its requests on the root subdomain,
|
||||
# but that makes it more of a hassle to test bouncer endpoints in
|
||||
# the development environment - so this setting allows us to disable
|
||||
# that check.
|
||||
DEVELOPMENT_DISABLE_PUSH_BOUNCER_DOMAIN_CHECK = False
|
||||
|
||||
|
||||
# These settings are not documented in prod_settings_template.py.
|
||||
# They should either be documented here, or documented there.
|
||||
#
|
||||
# Settings that it makes sense to document here instead of in
|
||||
# prod_settings_template.py are those that
|
||||
# * don't make sense to change in production, but rather are intended
|
||||
# for dev and test environments; or
|
||||
# * don't make sense to change on a typical production server with
|
||||
# one or a handful of realms, though they might on an installation
|
||||
# like Zulip Cloud or to work around a problem on another server.
|
||||
|
||||
# Set to True by the docker-zulip Helm chart via SETTING_RUNNING_IN_HELM, so that
|
||||
# error messages can recommend Helm-specific knobs (e.g. setting LOADBALANCER_IPS
|
||||
# via `zulip.environment.LOADBALANCER_IPS` in values) instead of bare env vars
|
||||
# the operator has no direct way to set under Helm.
|
||||
RUNNING_IN_HELM = False
|
||||
|
||||
NOTIFICATION_BOT = "notification-bot@zulip.com"
|
||||
EMAIL_GATEWAY_BOT = "emailgateway@zulip.com"
|
||||
NAGIOS_SEND_BOT = "nagios-send-bot@zulip.com"
|
||||
NAGIOS_RECEIVE_BOT = "nagios-receive-bot@zulip.com"
|
||||
WELCOME_BOT = "welcome-bot@zulip.com"
|
||||
REMINDER_BOT = "reminder-bot@zulip.com"
|
||||
|
||||
# The following bots are optional system bots not enabled by
|
||||
# default. The default ones are defined in INTERNAL_BOTS, in settings.py.
|
||||
|
||||
# These are extra bot users for our end-to-end Nagios message
|
||||
# sending tests.
|
||||
NAGIOS_STAGING_SEND_BOT = "nagios-staging-send-bot@zulip.com" if PRODUCTION else None
|
||||
NAGIOS_STAGING_RECEIVE_BOT = "nagios-staging-receive-bot@zulip.com" if PRODUCTION else None
|
||||
# SYSTEM_BOT_REALM would be a constant always set to 'zulip',
|
||||
# except that it isn't that on Zulip Cloud. We will likely do a
|
||||
# migration and eliminate this parameter in the future.
|
||||
SYSTEM_BOT_REALM = "zulipinternal"
|
||||
|
||||
# Structurally, we will probably eventually merge
|
||||
# analytics into part of the main server, rather
|
||||
# than a separate app.
|
||||
EXTRA_INSTALLED_APPS = ["analytics"]
|
||||
|
||||
# Used to construct URLs to point to the Zulip server. Since we
|
||||
# only support HTTPS in production, this is just for development.
|
||||
EXTERNAL_URI_SCHEME = "https://"
|
||||
|
||||
# Whether anyone can create a new organization on the Zulip server.
|
||||
OPEN_REALM_CREATION = False
|
||||
|
||||
# Whether it's possible to create web-public streams on this server.
|
||||
WEB_PUBLIC_STREAMS_ENABLED = False
|
||||
|
||||
# Setting for where the system bot users are. Likely has no
|
||||
# purpose now that the REALMS_HAVE_SUBDOMAINS migration is finished.
|
||||
SYSTEM_ONLY_REALMS = {"zulip"}
|
||||
|
||||
# Default deadline for demo organizations
|
||||
DEMO_ORG_DEADLINE_DAYS: int | None = None
|
||||
|
||||
# Alternate hostnames to serve particular realms on, in addition to
|
||||
# their usual subdomains. Keys are realm string_ids (aka subdomains),
|
||||
# and values are alternate hosts.
|
||||
# The values will also be added to ALLOWED_HOSTS.
|
||||
REALM_HOSTS: dict[str, str] = {}
|
||||
|
||||
# Map used to rewrite the URIs for certain realms during mobile
|
||||
# authentication. This, combined with adding the relevant hosts to
|
||||
# ALLOWED_HOSTS, can be used for environments where security policies
|
||||
# mean that a different hostname must be used for mobile access.
|
||||
REALM_MOBILE_REMAP_URIS: dict[str, str] = {}
|
||||
|
||||
# Whether the server is using the PGroonga full-text search
|
||||
# backend. Plan is to turn this on for everyone after further
|
||||
# testing.
|
||||
USING_PGROONGA = False
|
||||
|
||||
# How Django should send emails. Set for most contexts in settings.py, but
|
||||
# available for sysadmin override in unusual cases.
|
||||
EMAIL_BACKEND: str | None = None
|
||||
|
||||
# Whether to give admins a warning in the web app that email isn't set up.
|
||||
# Set in settings.py when email isn't configured.
|
||||
WARN_NO_EMAIL = False
|
||||
|
||||
# If enabled, all email-sending will happen in the worker. This means
|
||||
# that the UI cannot display configuration errors which prevented
|
||||
# email sending, so this is usually left off except in
|
||||
# well-established deployments which know the configuration is
|
||||
# correct.
|
||||
EMAIL_ALWAYS_ENQUEUED = False
|
||||
|
||||
# If True, disable rate-limiting and other filters on sending error messages
|
||||
# to admins, and enable logging on the error-reporting itself. Useful
|
||||
# mainly in development.
|
||||
DEBUG_ERROR_REPORTING = False
|
||||
|
||||
# Whether to flush memcached after data migrations. Because of
|
||||
# how we do deployments in a way that avoids reusing memcached,
|
||||
# this is disabled in production, but we need it in development.
|
||||
POST_MIGRATION_CACHE_FLUSHING = False
|
||||
|
||||
# Settings for APNS. Only needed on push.zulipchat.com or if
|
||||
# rebuilding the mobile app with a different push notifications
|
||||
# server.
|
||||
APNS_CERT_FILE: str | None = None
|
||||
APNS_TOKEN_KEY_FILE: str | None = None
|
||||
APNS_TOKEN_KEY_ID = get_secret("apns_token_key_id", development_only=True)
|
||||
APNS_TEAM_ID = get_secret("apns_team_id", development_only=True)
|
||||
APNS_SANDBOX = True
|
||||
# APNS_TOPIC is obsolete. Clients now pass the APNs topic to use.
|
||||
# ZULIP_IOS_APP_ID is obsolete. Clients now pass the iOS app ID to use for APNs.
|
||||
ANDROID_FCM_CREDENTIALS_PATH: str | None = None
|
||||
|
||||
# Limits related to the size of file uploads; last few in MB.
|
||||
DATA_UPLOAD_MAX_MEMORY_SIZE = 25 * 1024 * 1024
|
||||
MAX_AVATAR_FILE_SIZE_MIB = 5
|
||||
MAX_ICON_FILE_SIZE_MIB = 5
|
||||
MAX_LOGO_FILE_SIZE_MIB = 5
|
||||
MAX_EMOJI_FILE_SIZE_MIB = 5
|
||||
|
||||
# Limits to help prevent spam, in particular by sending invitations.
|
||||
#
|
||||
# A non-admin user who's joined an open realm this recently can't invite at all.
|
||||
INVITES_MIN_USER_AGE_DAYS = 3
|
||||
# Default for a realm's `max_invites`; which applies per day,
|
||||
# and only applies if OPEN_REALM_CREATION is true.
|
||||
INVITES_DEFAULT_REALM_DAILY_MAX = 100
|
||||
# Global rate-limit (list of pairs (days, max)) on invites from new realms.
|
||||
# Only applies if OPEN_REALM_CREATION is true.
|
||||
INVITES_NEW_REALM_LIMIT_DAYS = [(1, 100)]
|
||||
# Definition of a new realm for INVITES_NEW_REALM_LIMIT.
|
||||
INVITES_NEW_REALM_DAYS = 7
|
||||
|
||||
# Controls for which links are published in portico footers/headers/etc.
|
||||
REGISTER_LINK_DISABLED: bool | None = None
|
||||
|
||||
# What domains to treat like the root domain
|
||||
ROOT_SUBDOMAIN_ALIASES = ["www"]
|
||||
# Whether the root domain is a landing page or can host a realm.
|
||||
ROOT_DOMAIN_LANDING_PAGE = False
|
||||
|
||||
# Subdomain for serving endpoints to users from self-hosted deployments.
|
||||
SELF_HOSTING_MANAGEMENT_SUBDOMAIN: str | None = None
|
||||
|
||||
# When security-relevant links in emails expire.
|
||||
CONFIRMATION_LINK_DEFAULT_VALIDITY_DAYS = 1
|
||||
INVITATION_LINK_VALIDITY_DAYS = 10
|
||||
CAN_CREATE_REALM_LINK_VALIDITY_DAYS = 7
|
||||
|
||||
# Version number for ToS. Change this if you want to force every
|
||||
# user to click through to re-accept terms of service before using
|
||||
# Zulip again on the web.
|
||||
TERMS_OF_SERVICE_VERSION: str | None = None
|
||||
# HTML template path (e.g. "corporate/zulipchat_migration_tos.html")
|
||||
# displayed to users when increasing TERMS_OF_SERVICE_VERSION when a
|
||||
# user is to accept the terms of service for the first time, but
|
||||
# already has an account. This primarily comes up when doing a data
|
||||
# import.
|
||||
FIRST_TIME_TERMS_OF_SERVICE_TEMPLATE: str | None = None
|
||||
# Custom message (HTML allowed) to be displayed to explain why users
|
||||
# need to re-accept the terms of service when a new major version is
|
||||
# written.
|
||||
TERMS_OF_SERVICE_MESSAGE: str | None = None
|
||||
|
||||
# Configuration for JWT auth (sign in and API key fetch)
|
||||
JWT_AUTH_KEYS: dict[str, JwtAuthKey] = {}
|
||||
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#std:setting-SERVER_EMAIL
|
||||
# Django setting for what from address to use in error emails.
|
||||
SERVER_EMAIL = ZULIP_ADMINISTRATOR
|
||||
# Django setting for who receives error emails.
|
||||
ADMINS = (("Zulip Administrator", ZULIP_ADMINISTRATOR),)
|
||||
|
||||
# From address for welcome emails.
|
||||
WELCOME_EMAIL_SENDER: dict[str, str] | None = None
|
||||
|
||||
# Whether to send periodic digests of activity.
|
||||
SEND_DIGEST_EMAILS = True
|
||||
# The variable part of email sender names to be used for outgoing emails.
|
||||
INSTALLATION_NAME = EXTERNAL_HOST
|
||||
|
||||
# Used to change the Zulip logo in portico pages.
|
||||
CUSTOM_LOGO_URL: str | None = None
|
||||
|
||||
# Random salt used when deterministically generating passwords in
|
||||
# development.
|
||||
INITIAL_PASSWORD_SALT: str | None = None
|
||||
|
||||
# Settings configuring the special instrumentation of the send_event
|
||||
# code path used in generating API documentation for /events.
|
||||
LOG_API_EVENT_TYPES = False
|
||||
|
||||
# Used to control whether certain management commands are run on
|
||||
# the server.
|
||||
# TODO: Replace this with a smarter "run on only one server" system.
|
||||
STAGING = False
|
||||
|
||||
# Presence tuning parameters. These values were hardcoded in clients
|
||||
# before Zulip 7.0 (feature level 164); modern clients should get them
|
||||
# via the /register API response, making it possible to tune these to
|
||||
# adjust the trade-off between freshness and presence-induced load.
|
||||
#
|
||||
# The default for OFFLINE_THRESHOLD_SECS is chosen as
|
||||
# `PRESENCE_PING_INTERVAL_SECS * 3 + 20`, which is designed to allow 2
|
||||
# round trips, plus an extra in case an update fails.
|
||||
#
|
||||
# How long to wait before clients should treat a user as offline.
|
||||
OFFLINE_THRESHOLD_SECS = 200
|
||||
# How often a client should ping by asking for presence data of all users.
|
||||
PRESENCE_PING_INTERVAL_SECS = 60
|
||||
# Zulip sends immediate presence updates via the events system when a
|
||||
# user joins or becomes online. In larger organizations, this can
|
||||
# become prohibitively expensive, so we limit how many active users an
|
||||
# organization can have before these presence update events are
|
||||
# disabled.
|
||||
USER_LIMIT_FOR_SENDING_PRESENCE_UPDATE_EVENTS = 100
|
||||
|
||||
# Controls the how much newer a user presence update needs to be
|
||||
# than the currently saved last_active_time or last_connected_time in order for us to
|
||||
# update the database state. E.g. If set to 0, we will do
|
||||
# a database write each time a client sends a presence update.
|
||||
PRESENCE_UPDATE_MIN_FREQ_SECONDS = 55
|
||||
|
||||
# Controls the timedelta between last_connected_time and last_active_time
|
||||
# within which the user should be considered ACTIVE for the purposes of
|
||||
# legacy presence events. That is - when sending a presence update about a user to clients,
|
||||
# we will specify ACTIVE status as long as the timedelta is within this limit and IDLE otherwise.
|
||||
PRESENCE_LEGACY_EVENT_OFFSET_FOR_ACTIVITY_SECONDS = 70
|
||||
|
||||
# The web app doesn't pass params to / when initially loading, so it can't directly
|
||||
# pick its history_limit_days value. Instead, the server chooses the value and
|
||||
# passes it to the web app in page_params.
|
||||
PRESENCE_HISTORY_LIMIT_DAYS_FOR_WEB_APP = 365
|
||||
|
||||
# How many days deleted messages data should be kept before being
|
||||
# permanently deleted.
|
||||
ARCHIVED_DATA_VACUUMING_DELAY_DAYS = 30
|
||||
|
||||
# Enables billing pages and plan-based feature gates. If False, all features
|
||||
# are available to all realms.
|
||||
BILLING_ENABLED = False
|
||||
|
||||
CLOUD_FREE_TRIAL_DAYS: int | None = int(get_secret("cloud_free_trial_days", "0"))
|
||||
SELF_HOSTING_FREE_TRIAL_DAYS: int | None = int(get_secret("self_hosting_free_trial_days", "30"))
|
||||
|
||||
# Custom message (supports HTML) to be shown in the navbar of landing pages. Used mainly for
|
||||
# making announcements.
|
||||
LANDING_PAGE_NAVBAR_MESSAGE: str | None = None
|
||||
|
||||
# Automatically catch-up soft deactivated users when running the
|
||||
# `soft-deactivate-users` cron. Turn this off if the server has 10Ks of
|
||||
# users, and you would like to save some disk space. Soft-deactivated
|
||||
# returning users would still be caught-up normally.
|
||||
AUTO_CATCH_UP_SOFT_DEACTIVATED_USERS = True
|
||||
|
||||
# Enables Google Analytics on selected portico pages.
|
||||
GOOGLE_ANALYTICS_ID: str | None = None
|
||||
|
||||
# This is overridden by dev_settings.py for droplets.
|
||||
IS_DEV_DROPLET = False
|
||||
|
||||
# Used by the `check_send_receive_time` monitoring tool.
|
||||
NAGIOS_BOT_HOST = SYSTEM_BOT_REALM + "." + EXTERNAL_HOST
|
||||
|
||||
# Use half of the available CPUs for data import purposes.
|
||||
DEFAULT_DATA_EXPORT_IMPORT_PARALLELISM = (len(os.sched_getaffinity(0)) // 2) or 1
|
||||
|
||||
# Use the default tmpfile path for automated imports
|
||||
IMPORT_TMPFILE_DIRECTORY: str | None = None
|
||||
|
||||
# How long after the last upgrade to nag users that the server needs
|
||||
# to be upgraded because of likely security releases in the meantime.
|
||||
# Default is 18 months, constructed as 12 months before someone should
|
||||
# upgrade, plus 6 months for the system administrator to get around to it.
|
||||
SERVER_UPGRADE_NAG_DEADLINE_DAYS = 30 * 18
|
||||
|
||||
# How long servers have to respond to outgoing webhook requests
|
||||
OUTGOING_WEBHOOK_TIMEOUT_SECONDS = 10
|
||||
|
||||
# Maximum length of message content allowed.
|
||||
# Any message content exceeding this limit will be truncated.
|
||||
# See: `_internal_prep_message` function in zerver/actions/message_send.py.
|
||||
MAX_MESSAGE_LENGTH = 10000
|
||||
|
||||
# Maximum length of note text for a reminder.
|
||||
# NOTE: Keep it significantly smaller than MAX_MESSAGE_LENGTH
|
||||
# to avoid message being completely truncated when reminder is sent.
|
||||
MAX_REMINDER_NOTE_LENGTH = 1000
|
||||
|
||||
# The maximum number of drafts to send in the response to /register.
|
||||
# More drafts, should they exist for some crazy reason, could be
|
||||
# fetched in a separate request.
|
||||
MAX_DRAFTS_IN_REGISTER_RESPONSE = 1000
|
||||
|
||||
# How long before a client should assume that another client sending
|
||||
# typing notifications has gone away and expire the active typing
|
||||
# indicator.
|
||||
TYPING_STARTED_EXPIRY_PERIOD_MILLISECONDS = 45000
|
||||
|
||||
# How long after a user has stopped interacting with the compose UI
|
||||
# that a client should send a stop notification to the server.
|
||||
TYPING_STOPPED_WAIT_PERIOD_MILLISECONDS = 12000
|
||||
|
||||
# How often a client should send start notifications to the server to
|
||||
# indicate that the user is still interacting with the compose UI.
|
||||
TYPING_STARTED_WAIT_PERIOD_MILLISECONDS = 30000
|
||||
|
||||
# The maximum number of subscribers for a stream to have typing
|
||||
# notifications enabled. Default is set to avoid excessive Tornado
|
||||
# load in large organizations.
|
||||
MAX_STREAM_SIZE_FOR_TYPING_NOTIFICATIONS = 100
|
||||
|
||||
# The maximum user-group size value upto which members should
|
||||
# be soft-reactivated in the case of user group mention.
|
||||
MAX_GROUP_SIZE_FOR_MENTION_REACTIVATION = 11
|
||||
|
||||
# The maximum number of newly subscribed users for which the server
|
||||
# will consider sending DMs to each new subscriber.
|
||||
MAX_BULK_NEW_SUBSCRIPTION_MESSAGES = 100
|
||||
|
||||
# Limiting guest access to other users via the
|
||||
# can_access_all_users_group setting makes presence queries much more
|
||||
# expensive. This can be a significant performance problem for
|
||||
# installations with thousands of users with many guests limited in
|
||||
# this way, pending further optimization of the relevant code paths.
|
||||
CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE = False
|
||||
|
||||
# General expiry time for signed tokens we may generate
|
||||
# in some places through the codebase.
|
||||
SIGNED_ACCESS_TOKEN_VALIDITY_IN_SECONDS = 60
|
||||
|
||||
CUSTOM_AUTHENTICATION_WRAPPER_FUNCTION: Callable[..., Any] | None = None
|
||||
|
||||
# Grace period during which we don't send a resolve/unresolve
|
||||
# notification to a stream and also delete the previous counter
|
||||
# notification.
|
||||
RESOLVE_TOPIC_UNDO_GRACE_PERIOD_SECONDS = 60
|
||||
|
||||
# Maximum allowed size of uploaded file for realm import on the web.
|
||||
# 0 disables import; None means no limit.
|
||||
#
|
||||
# Note that this is a limit for the size of the uploaded export
|
||||
# itself, not any additional files that may be imported as well.
|
||||
MAX_WEB_DATA_IMPORT_SIZE_MB: int | None = 0
|
||||
|
||||
# Minimum and maximum permitted number of days before full data
|
||||
# deletion when deactivating an organization. A nonzero minimum helps
|
||||
# protect against a compromised administrator account being used to
|
||||
# delete an active organization.
|
||||
MIN_DEACTIVATED_REALM_DELETION_DAYS: int | None = 14
|
||||
MAX_DEACTIVATED_REALM_DELETION_DAYS: int | None = None
|
||||
|
||||
|
||||
TOPIC_SUMMARIZATION_MODEL: str | None = None
|
||||
TOPIC_SUMMARIZATION_API_BASE: str | None = None
|
||||
TOPIC_SUMMARIZATION_PARAMETERS: dict[str, Any] = {}
|
||||
# Price per token for input and output tokens, and maximum cost. Units
|
||||
# are arbitrarily, but typically will be USD.
|
||||
INPUT_COST_PER_GIGATOKEN: int = 0
|
||||
OUTPUT_COST_PER_GIGATOKEN: int = 0
|
||||
MAX_PER_USER_MONTHLY_AI_COST: float | None = 0.5
|
||||
|
||||
# URL of the navigation tour video displayed to new users.
|
||||
# Set it to None to disable it.
|
||||
NAVIGATION_TOUR_VIDEO_URL: str | None = (
|
||||
"https://static.zulipchat.com/static/navigation-tour-video/zulip-10.mp4"
|
||||
)
|
||||
|
||||
# Webhook signature verification.
|
||||
VERIFY_WEBHOOK_SIGNATURES = True
|
||||
|
||||
# SCIM API configuration.
|
||||
SCIM_CONFIG: dict[str, SCIMConfigDict] = {}
|
||||
|
||||
# Minimum number of subscribers in a channel for us to no longer
|
||||
# send full subscriber data to the client.
|
||||
MIN_PARTIAL_SUBSCRIBERS_CHANNEL_SIZE = 1000
|
||||
255
zproject/dev_settings.py
Normal file
255
zproject/dev_settings.py
Normal file
@@ -0,0 +1,255 @@
|
||||
import os
|
||||
import pwd
|
||||
|
||||
from scripts.lib.zulip_tools import deport
|
||||
from zproject.settings_types import SCIMConfigDict
|
||||
|
||||
ZULIP_ADMINISTRATOR = "desdemona+admin@zulip.com"
|
||||
|
||||
# Initiatize TEST_SUITE early, so other code can rely on the setting.
|
||||
TEST_SUITE = os.getenv("ZULIP_TEST_SUITE") == "true"
|
||||
|
||||
# We want LOCAL_UPLOADS_DIR to be an absolute path so that code can
|
||||
# chdir without having problems accessing it. Unfortunately, this
|
||||
# means we need a duplicate definition of DEPLOY_ROOT with the one in
|
||||
# settings.py.
|
||||
DEPLOY_ROOT = os.path.realpath(os.path.dirname(os.path.dirname(__file__)))
|
||||
LOCAL_UPLOADS_DIR = os.path.join(DEPLOY_ROOT, "var/uploads")
|
||||
|
||||
# We assume dev droplets are the only places where
|
||||
# users use zulipdev as the user.
|
||||
IS_DEV_DROPLET = pwd.getpwuid(os.getuid()).pw_name == "zulipdev"
|
||||
|
||||
FORWARD_ADDRESS_CONFIG_FILE = "var/forward_address.ini"
|
||||
# Check if test_settings.py set EXTERNAL_HOST.
|
||||
external_host_env = os.getenv("EXTERNAL_HOST")
|
||||
if external_host_env is None:
|
||||
if IS_DEV_DROPLET:
|
||||
# For our droplets, we use the hostname (eg github_username.zulipdev.org) by default.
|
||||
# Note that this code is duplicated in run-dev.
|
||||
EXTERNAL_HOST = os.uname()[1].lower() + ":9991"
|
||||
else:
|
||||
# For local development environments, we use localhost by
|
||||
# default, via the "zulipdev.com" hostname.
|
||||
EXTERNAL_HOST = "zulipdev.com:9991"
|
||||
# Serve the main dev realm at the literal name "localhost",
|
||||
# so it works out of the box even when not on the Internet.
|
||||
REALM_HOSTS = {
|
||||
"zulip": "localhost:9991",
|
||||
}
|
||||
else:
|
||||
EXTERNAL_HOST = external_host_env
|
||||
REALM_HOSTS = {
|
||||
"zulip": EXTERNAL_HOST,
|
||||
}
|
||||
|
||||
EXTERNAL_HOST_WITHOUT_PORT = deport(EXTERNAL_HOST)
|
||||
|
||||
FAKE_EMAIL_DOMAIN = "zulipdev.com"
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
# Uncomment extra backends if you want to test with them. Note that
|
||||
# for Google and GitHub auth you'll need to do some pre-setup.
|
||||
AUTHENTICATION_BACKENDS: tuple[str, ...] = (
|
||||
"zproject.backends.DevAuthBackend",
|
||||
"zproject.backends.EmailAuthBackend",
|
||||
"zproject.backends.GitHubAuthBackend",
|
||||
"zproject.backends.GoogleAuthBackend",
|
||||
"zproject.backends.SAMLAuthBackend",
|
||||
# 'zproject.backends.AzureADAuthBackend',
|
||||
"zproject.backends.GitLabAuthBackend",
|
||||
"zproject.backends.AppleAuthBackend",
|
||||
"zproject.backends.GenericOpenIdConnectBackend",
|
||||
"zproject.backends.DiscordAuthBackend",
|
||||
)
|
||||
|
||||
EXTERNAL_URI_SCHEME = "http://"
|
||||
|
||||
if os.getenv("BEHIND_HTTPS_PROXY"):
|
||||
# URLs served by the development environment will be HTTPS
|
||||
EXTERNAL_URI_SCHEME = "https://"
|
||||
# Trust requests from this host (required due to Nginx proxy)
|
||||
CSRF_TRUSTED_ORIGINS = [EXTERNAL_URI_SCHEME + EXTERNAL_HOST]
|
||||
|
||||
EMAIL_GATEWAY_PATTERN = "%s@" + EXTERNAL_HOST_WITHOUT_PORT
|
||||
NOTIFICATION_BOT = "notification-bot@zulip.com"
|
||||
EMAIL_GATEWAY_BOT = "emailgateway@zulip.com"
|
||||
PHYSICAL_ADDRESS = "Zulip Headquarters, 123 Octo Stream, South Pacific Ocean"
|
||||
STAFF_SUBDOMAIN = "zulip"
|
||||
EXTRA_INSTALLED_APPS = ["zilencer", "analytics", "corporate"]
|
||||
# Disable Camo in development
|
||||
CAMO_URI = ""
|
||||
KATEX_SERVER = False
|
||||
|
||||
TORNADO_PORTS = [9993]
|
||||
|
||||
OPEN_REALM_CREATION = True
|
||||
WEB_PUBLIC_STREAMS_ENABLED = True
|
||||
INVITES_MIN_USER_AGE_DAYS = 0
|
||||
|
||||
# Redirect to /devlogin/ by default in dev mode
|
||||
CUSTOM_HOME_NOT_LOGGED_IN = "/devlogin/"
|
||||
LOGIN_URL = "/devlogin/"
|
||||
|
||||
# For development convenience, configure the ToS/Privacy Policies
|
||||
POLICIES_DIRECTORY = "corporate/policies"
|
||||
TERMS_OF_SERVICE_VERSION = "1.0"
|
||||
TERMS_OF_SERVICE_MESSAGE: str | None = "Description of changes to the ToS!"
|
||||
|
||||
EMBEDDED_BOTS_ENABLED = True
|
||||
|
||||
SYSTEM_ONLY_REALMS: set[str] = set()
|
||||
USING_PGROONGA = True
|
||||
# Flush cache after migration.
|
||||
POST_MIGRATION_CACHE_FLUSHING = True
|
||||
|
||||
# If a sandbox APNs key or cert is provided, use it.
|
||||
# To create such a key or cert, see instructions at:
|
||||
# https://github.com/zulip/zulip-mobile/blob/main/docs/howto/push-notifications.md#ios
|
||||
_candidate_apns_token_key_file = "zproject/apns-dev-key.p8"
|
||||
_candidate_apns_cert_file = "zproject/apns-dev.pem"
|
||||
if os.path.isfile(_candidate_apns_token_key_file):
|
||||
APNS_TOKEN_KEY_FILE = _candidate_apns_token_key_file
|
||||
elif os.path.isfile(_candidate_apns_cert_file):
|
||||
APNS_CERT_FILE = _candidate_apns_cert_file
|
||||
|
||||
# Don't require anything about password strength in development
|
||||
PASSWORD_MIN_LENGTH = 0
|
||||
PASSWORD_MAX_LENGTH = 100
|
||||
PASSWORD_MIN_GUESSES = 0
|
||||
|
||||
# Two factor authentication: Use the fake backend for development.
|
||||
TWO_FACTOR_CALL_GATEWAY = "two_factor.gateways.fake.Fake"
|
||||
TWO_FACTOR_SMS_GATEWAY = "two_factor.gateways.fake.Fake"
|
||||
|
||||
# FAKE_LDAP_MODE supports using a fake LDAP database in the
|
||||
# development environment, without needing an LDAP server!
|
||||
#
|
||||
# Three modes are allowed, and each will set up Zulip and the fake LDAP
|
||||
# database in a way appropriate for the corresponding mode described
|
||||
# in https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#ldap-including-active-directory
|
||||
# (A) If users' email addresses are in LDAP and used as username.
|
||||
# (B) If LDAP only has usernames but email addresses are of the form
|
||||
# username@example.com
|
||||
# (C) If LDAP usernames are completely unrelated to email addresses.
|
||||
#
|
||||
# Fake LDAP data has e.g. ("ldapuser1", "ldapuser1@zulip.com") for username/email.
|
||||
FAKE_LDAP_MODE: str | None = None
|
||||
# FAKE_LDAP_NUM_USERS = 8
|
||||
|
||||
if FAKE_LDAP_MODE:
|
||||
import ldap
|
||||
from django_auth_ldap.config import LDAPSearch
|
||||
|
||||
# To understand these parameters, read the docs in
|
||||
# prod_settings_template.py and on ReadTheDocs.
|
||||
LDAP_APPEND_DOMAIN = None
|
||||
AUTH_LDAP_USER_SEARCH = LDAPSearch(
|
||||
"ou=users,dc=zulip,dc=com", ldap.SCOPE_ONELEVEL, "(uid=%(user)s)"
|
||||
)
|
||||
AUTH_LDAP_REVERSE_EMAIL_SEARCH = LDAPSearch(
|
||||
"ou=users,dc=zulip,dc=com", ldap.SCOPE_ONELEVEL, "(email=%(email)s)"
|
||||
)
|
||||
|
||||
if FAKE_LDAP_MODE == "a":
|
||||
AUTH_LDAP_REVERSE_EMAIL_SEARCH = LDAPSearch(
|
||||
"ou=users,dc=zulip,dc=com", ldap.SCOPE_ONELEVEL, "(uid=%(email)s)"
|
||||
)
|
||||
AUTH_LDAP_USERNAME_ATTR = "uid"
|
||||
AUTH_LDAP_USER_ATTR_MAP = {
|
||||
"full_name": "cn",
|
||||
"avatar": "thumbnailPhoto",
|
||||
# This won't do much unless one changes the fact that
|
||||
# all users have LDAP_USER_ACCOUNT_CONTROL_NORMAL in
|
||||
# zerver/lib/dev_ldap_directory.py
|
||||
"userAccountControl": "userAccountControl",
|
||||
}
|
||||
elif FAKE_LDAP_MODE == "b":
|
||||
LDAP_APPEND_DOMAIN = "zulip.com"
|
||||
AUTH_LDAP_USER_ATTR_MAP = {
|
||||
"full_name": "cn",
|
||||
"avatar": "jpegPhoto",
|
||||
"custom_profile_field__birthday": "birthDate",
|
||||
"custom_profile_field__phone_number": "phoneNumber",
|
||||
}
|
||||
elif FAKE_LDAP_MODE == "c":
|
||||
AUTH_LDAP_USERNAME_ATTR = "uid"
|
||||
LDAP_EMAIL_ATTR = "email"
|
||||
AUTH_LDAP_USER_ATTR_MAP = {
|
||||
"full_name": "cn",
|
||||
}
|
||||
AUTHENTICATION_BACKENDS += ("zproject.backends.ZulipLDAPAuthBackend",)
|
||||
|
||||
BILLING_ENABLED = True
|
||||
# Set a string value here to test the navbar message in dev.
|
||||
# <a> elements and other HTML permitted.
|
||||
LANDING_PAGE_NAVBAR_MESSAGE: str | None = None
|
||||
|
||||
# Our run-dev proxy uses X-Forwarded-Port to communicate to Django
|
||||
# that the request is actually on port 9991, not port 9992 (the Django
|
||||
# server's own port); this setting tells Django to read that HTTP
|
||||
# header. Important for SAML authentication in the development
|
||||
# environment.
|
||||
USE_X_FORWARDED_PORT = True
|
||||
|
||||
# Override the default SAML entity ID
|
||||
SOCIAL_AUTH_SAML_SP_ENTITY_ID = "http://localhost:9991"
|
||||
if IS_DEV_DROPLET:
|
||||
SOCIAL_AUTH_SAML_SP_ENTITY_ID = EXTERNAL_URI_SCHEME + "zulip." + EXTERNAL_HOST
|
||||
|
||||
SOCIAL_AUTH_SUBDOMAIN = "auth"
|
||||
|
||||
MEMCACHED_USERNAME: str | None = None
|
||||
|
||||
SCIM_CONFIG: dict[str, SCIMConfigDict] = {
|
||||
"zulip": {
|
||||
"bearer_token": "token1234",
|
||||
"scim_client_name": "test-scim-client",
|
||||
"name_formatted_included": True,
|
||||
}
|
||||
}
|
||||
|
||||
SELF_HOSTING_MANAGEMENT_SUBDOMAIN = "selfhosting"
|
||||
DEVELOPMENT_DISABLE_PUSH_BOUNCER_DOMAIN_CHECK = True
|
||||
ZULIP_SERVICES_URL = f"http://{EXTERNAL_HOST}"
|
||||
|
||||
ZULIP_SERVICE_PUSH_NOTIFICATIONS = True
|
||||
ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS = True
|
||||
|
||||
# This value needs to be lower in development than usual to allow
|
||||
# for quicker testing of the feature.
|
||||
RESOLVE_TOPIC_UNDO_GRACE_PERIOD_SECONDS = 5
|
||||
|
||||
# In a dev environment, 'zulipdev.com:9991' is used to access the landing page.
|
||||
# See: https://zulip.readthedocs.io/en/latest/subsystems/realms.html#working-with-subdomains-in-development-environment
|
||||
ROOT_DOMAIN_LANDING_PAGE = True
|
||||
|
||||
# Enable demo organizations feature in dev environment.
|
||||
DEMO_ORG_DEADLINE_DAYS = 30
|
||||
|
||||
# Enable ALTCHA, so that we test this flow; we can only do this on localhost.
|
||||
if external_host_env is None and not IS_DEV_DROPLET:
|
||||
USING_CAPTCHA = True
|
||||
|
||||
TOPIC_SUMMARIZATION_MODEL = "llama-3.3-70b-versatile"
|
||||
TOPIC_SUMMARIZATION_API_BASE = "https://api.groq.com/openai/v1"
|
||||
# Defaults based on groq's pricing for Llama 3.3 70B Versatile 128k.
|
||||
# https://groq.com/pricing/
|
||||
OUTPUT_COST_PER_GIGATOKEN = 590
|
||||
INPUT_COST_PER_GIGATOKEN = 790
|
||||
MAX_PER_USER_MONTHLY_AI_COST = 1
|
||||
MAX_WEB_DATA_IMPORT_SIZE_MB = 1024
|
||||
|
||||
# We override some rate limiting rules in development, when they prove
|
||||
# excessively limiting for development and testing purposes.
|
||||
RATE_LIMITING_RULES = {
|
||||
"sends_email_by_ip": [
|
||||
# This rule puts a limit on actions such as new organization creation,
|
||||
# which we sometimes need to test repeatedly in development.
|
||||
(86400, 1000),
|
||||
],
|
||||
"demo_realm_creation_by_ip": [
|
||||
(86400, 1000),
|
||||
],
|
||||
}
|
||||
142
zproject/dev_urls.py
Normal file
142
zproject/dev_urls.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import os
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib.staticfiles.views import serve as staticfiles_serve
|
||||
from django.http.request import HttpRequest
|
||||
from django.http.response import FileResponse, HttpResponse
|
||||
from django.urls import path
|
||||
from django.views.generic import RedirectView, TemplateView
|
||||
from django.views.static import serve
|
||||
|
||||
from zerver.views.auth import login_page
|
||||
from zerver.views.development.cache import remove_caches
|
||||
from zerver.views.development.camo import handle_camo_url
|
||||
from zerver.views.development.dev_login import (
|
||||
api_dev_fetch_api_key,
|
||||
api_dev_list_users,
|
||||
dev_direct_login,
|
||||
)
|
||||
from zerver.views.development.email_log import clear_emails, email_page, generate_all_emails
|
||||
from zerver.views.development.help import help_dev_mode_view
|
||||
from zerver.views.development.integrations import (
|
||||
check_send_webhook_fixture_message,
|
||||
dev_panel,
|
||||
get_fixtures,
|
||||
send_all_webhook_fixture_messages,
|
||||
)
|
||||
from zerver.views.development.registration import (
|
||||
confirmation_key,
|
||||
register_demo_development_realm,
|
||||
register_development_realm,
|
||||
register_development_user,
|
||||
)
|
||||
from zerver.views.development.showroom import (
|
||||
showroom_component_banners,
|
||||
showroom_component_buttons,
|
||||
showroom_component_inputs,
|
||||
)
|
||||
from zerver.views.errors import config_error
|
||||
|
||||
# These URLs are available only in the development environment
|
||||
|
||||
use_prod_static = not settings.DEBUG
|
||||
|
||||
urls = [
|
||||
# Serve useful development environment resources (docs, coverage reports, etc.)
|
||||
path(
|
||||
"coverage/<path:path>",
|
||||
serve,
|
||||
{"document_root": os.path.join(settings.DEPLOY_ROOT, "var/coverage"), "show_indexes": True},
|
||||
),
|
||||
path(
|
||||
"node-coverage/<path:path>",
|
||||
serve,
|
||||
{
|
||||
"document_root": os.path.join(settings.DEPLOY_ROOT, "var/node-coverage/lcov-report"),
|
||||
"show_indexes": True,
|
||||
},
|
||||
),
|
||||
path("docs/", RedirectView.as_view(url="/docs/index.html")),
|
||||
path(
|
||||
"docs/<path:path>",
|
||||
serve,
|
||||
{"document_root": os.path.join(settings.DEPLOY_ROOT, "docs/_build/html")},
|
||||
),
|
||||
# The special no-password login endpoint for development
|
||||
path(
|
||||
"devlogin/",
|
||||
login_page,
|
||||
{"template_name": "zerver/development/dev_login.html"},
|
||||
name="login_page",
|
||||
),
|
||||
# Page for testing email templates
|
||||
path("emails/", email_page),
|
||||
path("emails/generate/", generate_all_emails),
|
||||
path("emails/clear/", clear_emails),
|
||||
# Listing of useful URLs and various tools for development
|
||||
path("devtools/", TemplateView.as_view(template_name="zerver/development/dev_tools.html")),
|
||||
# Register new user and realm
|
||||
path("devtools/register_user/", register_development_user, name="register_dev_user"),
|
||||
path("devtools/register_realm/", register_development_realm, name="register_dev_realm"),
|
||||
path(
|
||||
"devtools/register_demo_realm/",
|
||||
register_demo_development_realm,
|
||||
name="register_demo_dev_realm",
|
||||
),
|
||||
# Have easy access for error pages
|
||||
path("errors/404/", TemplateView.as_view(template_name="404.html")),
|
||||
path("errors/5xx/", TemplateView.as_view(template_name="500.html")),
|
||||
# Add a convenient way to generate webhook messages from fixtures.
|
||||
path("devtools/integrations/", dev_panel),
|
||||
path(
|
||||
"devtools/integrations/check_send_webhook_fixture_message",
|
||||
check_send_webhook_fixture_message,
|
||||
),
|
||||
path(
|
||||
"devtools/integrations/send_all_webhook_fixture_messages", send_all_webhook_fixture_messages
|
||||
),
|
||||
path("devtools/integrations/<integration_name>/fixtures", get_fixtures),
|
||||
path("config-error/<error_name>", config_error, name="config_error"),
|
||||
# Special endpoint to remove all the server-side caches.
|
||||
path("flush_caches", remove_caches),
|
||||
# Redirect camo URLs for development
|
||||
path("external_content/<digest>/<received_url>", handle_camo_url),
|
||||
# Endpoints for Showroom components.
|
||||
path("devtools/buttons/", showroom_component_buttons),
|
||||
path("devtools/banners/", showroom_component_banners),
|
||||
path("devtools/inputs/", showroom_component_inputs),
|
||||
# Development server for the help center in not run by default, we
|
||||
# show this page with zulip.com and view source links instead.
|
||||
path("help", help_dev_mode_view),
|
||||
path("help/", help_dev_mode_view),
|
||||
path("help/<path:subpath>", help_dev_mode_view),
|
||||
]
|
||||
|
||||
v1_api_mobile_patterns = [
|
||||
# This is for the signing in through the devAuthBackEnd on mobile apps.
|
||||
path("dev_fetch_api_key", api_dev_fetch_api_key),
|
||||
# This is for fetching the emails of the admins and the users.
|
||||
path("dev_list_users", api_dev_list_users),
|
||||
]
|
||||
# Serve static assets via the Django server
|
||||
if use_prod_static:
|
||||
urls += [
|
||||
path("static/<path:path>", serve, {"document_root": settings.STATIC_ROOT}),
|
||||
]
|
||||
else: # nocoverage
|
||||
|
||||
def serve_static(request: HttpRequest, path: str) -> HttpResponse | FileResponse:
|
||||
response = staticfiles_serve(request, path)
|
||||
response["Access-Control-Allow-Origin"] = "*"
|
||||
return response
|
||||
|
||||
assert settings.STATIC_URL is not None
|
||||
urls += static(urlsplit(settings.STATIC_URL).path, view=serve_static)
|
||||
|
||||
i18n_urls = [
|
||||
path("accounts/login/local/", dev_direct_login, name="login-local"),
|
||||
path("confirmation_key/", confirmation_key),
|
||||
]
|
||||
urls += i18n_urls
|
||||
176
zproject/email_backends.py
Normal file
176
zproject/email_backends.py
Normal file
@@ -0,0 +1,176 @@
|
||||
# https://zulip.readthedocs.io/en/latest/subsystems/email.html#testing-in-a-real-email-client
|
||||
import configparser
|
||||
import logging
|
||||
import smtplib
|
||||
from collections.abc import Sequence
|
||||
from email.message import Message
|
||||
from typing import Any
|
||||
|
||||
import tenacity
|
||||
from django.conf import settings
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.core.mail.message import EmailAlternative, EmailMessage
|
||||
from django.template import loader
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from typing_extensions import override
|
||||
|
||||
MAX_CONNECTION_TRIES = 3
|
||||
|
||||
# SMTPException is a subclass of OSError, so retry on connection-level
|
||||
# errors (ConnectionError, TimeoutError) and SMTPServerDisconnected
|
||||
# (dropped connection), but not on SMTP protocol errors like
|
||||
# SMTPAuthenticationError, SMTPRecipientsRefused, or SMTPDataError.
|
||||
smtp_connection_backoff = tenacity.retry(
|
||||
wait=tenacity.wait_exponential_jitter(),
|
||||
retry=tenacity.retry_if_exception(
|
||||
lambda exc: (
|
||||
isinstance(exc, OSError)
|
||||
and (
|
||||
not isinstance(exc, smtplib.SMTPException)
|
||||
or isinstance(exc, smtplib.SMTPServerDisconnected)
|
||||
)
|
||||
)
|
||||
),
|
||||
stop=tenacity.stop_after_attempt(MAX_CONNECTION_TRIES),
|
||||
reraise=True,
|
||||
)
|
||||
|
||||
|
||||
def get_forward_address() -> str:
|
||||
config = configparser.ConfigParser()
|
||||
config.read(settings.FORWARD_ADDRESS_CONFIG_FILE)
|
||||
try:
|
||||
return config.get("DEV_EMAIL", "forward_address")
|
||||
except (configparser.NoSectionError, configparser.NoOptionError):
|
||||
return ""
|
||||
|
||||
|
||||
def set_forward_address(forward_address: str) -> None:
|
||||
config = configparser.ConfigParser()
|
||||
config.read(settings.FORWARD_ADDRESS_CONFIG_FILE)
|
||||
|
||||
if not config.has_section("DEV_EMAIL"):
|
||||
config.add_section("DEV_EMAIL")
|
||||
config.set("DEV_EMAIL", "forward_address", forward_address)
|
||||
|
||||
with open(settings.FORWARD_ADDRESS_CONFIG_FILE, "w") as cfgfile:
|
||||
config.write(cfgfile)
|
||||
|
||||
|
||||
class EmailLogBackEnd(EmailBackend):
|
||||
@staticmethod
|
||||
def log_email(email: EmailMessage) -> None:
|
||||
"""Used in development to record sent emails in a nice HTML log"""
|
||||
html_message: bytes | EmailMessage | Message | str = "Missing HTML message"
|
||||
assert isinstance(email, EmailMultiAlternatives)
|
||||
if len(email.alternatives) > 0:
|
||||
html_message = email.alternatives[0][0]
|
||||
|
||||
context = {
|
||||
"subject": email.subject,
|
||||
"envelope_from": email.from_email,
|
||||
"from_email": email.extra_headers.get("From", email.from_email),
|
||||
"reply_to": email.reply_to,
|
||||
"recipients": email.to,
|
||||
"body": email.body,
|
||||
"date": email.extra_headers.get("Date", "?"),
|
||||
"html_message": html_message,
|
||||
}
|
||||
|
||||
new_email = loader.render_to_string("zerver/email.html", context)
|
||||
|
||||
# Read in the pre-existing log, so that we can add the new entry
|
||||
# at the top.
|
||||
try:
|
||||
with open(settings.EMAIL_CONTENT_LOG_PATH) as f:
|
||||
previous_emails = f.read()
|
||||
except FileNotFoundError:
|
||||
previous_emails = ""
|
||||
|
||||
with open(settings.EMAIL_CONTENT_LOG_PATH, "w+") as f:
|
||||
f.write(new_email + previous_emails)
|
||||
|
||||
@staticmethod
|
||||
def prepare_email_messages_for_forwarding(email_messages: Sequence[EmailMessage]) -> None:
|
||||
localhost_email_images_base_url = settings.ROOT_DOMAIN_URI + "/static/images/emails"
|
||||
czo_email_images_base_url = "https://chat.zulip.org/static/images/emails"
|
||||
|
||||
for email_message in email_messages:
|
||||
assert isinstance(email_message, EmailMultiAlternatives)
|
||||
# Here, we replace the image URLs used in development with
|
||||
# chat.zulip.org URLs, so that web email providers like Gmail
|
||||
# will be able to fetch the illustrations used in the emails.
|
||||
assert isinstance(email_message.alternatives[0], EmailAlternative)
|
||||
original_content = email_message.alternatives[0].content
|
||||
original_mimetype = email_message.alternatives[0].mimetype
|
||||
assert isinstance(original_content, str)
|
||||
email_message.alternatives[0] = EmailAlternative(
|
||||
content=original_content.replace(
|
||||
localhost_email_images_base_url, czo_email_images_base_url
|
||||
),
|
||||
mimetype=original_mimetype,
|
||||
)
|
||||
|
||||
email_message.to = [get_forward_address()]
|
||||
|
||||
# This wrapper function exists to allow tests easily to mock the
|
||||
# step of trying to send the emails. Previously, we had mocked
|
||||
# Django's connection.send_messages(), which caused unexplained
|
||||
# test failures when running test-backend at very high
|
||||
# concurrency.
|
||||
def _do_send_messages(self, email_messages: Sequence[EmailMessage]) -> int:
|
||||
return super().send_messages(email_messages) # nocoverage
|
||||
|
||||
@override
|
||||
def send_messages(self, email_messages: Sequence[EmailMessage]) -> int:
|
||||
num_sent = len(email_messages)
|
||||
if get_forward_address():
|
||||
self.prepare_email_messages_for_forwarding(email_messages)
|
||||
num_sent = self._do_send_messages(email_messages)
|
||||
|
||||
if settings.DEVELOPMENT_LOG_EMAILS:
|
||||
for email in email_messages:
|
||||
self.log_email(email)
|
||||
email_log_url = settings.ROOT_DOMAIN_URI + "/emails"
|
||||
logging.info("Emails sent in development are available at %s", email_log_url)
|
||||
return num_sent
|
||||
|
||||
|
||||
class PersistentSMTPEmailBackend(EmailBackend):
|
||||
@override
|
||||
def open(self, **kwargs: Any) -> bool:
|
||||
if super().open(**kwargs):
|
||||
self.opened_at = timezone_now()
|
||||
# Always return False so that Django's send_messages does not
|
||||
# auto-close the connection after sending.
|
||||
return False
|
||||
|
||||
@smtp_connection_backoff
|
||||
def ensure_connected(self) -> None:
|
||||
"""Open the connection if needed, and validate that it is
|
||||
still alive, reconnecting if necessary."""
|
||||
self.open()
|
||||
self._validate_or_reconnect()
|
||||
|
||||
def _validate_or_reconnect(self) -> None:
|
||||
"""Validate that the existing SMTP connection is still alive,
|
||||
reconnecting if necessary. Called with backoff protection,
|
||||
not from open()."""
|
||||
status = None
|
||||
time_elapsed = (timezone_now() - self.opened_at).total_seconds() / 60
|
||||
if (
|
||||
settings.EMAIL_MAX_CONNECTION_LIFETIME_IN_MINUTES is None
|
||||
or time_elapsed <= settings.EMAIL_MAX_CONNECTION_LIFETIME_IN_MINUTES
|
||||
):
|
||||
# No-op to ensure that we don't return a connection that has been
|
||||
# closed by the mail server.
|
||||
try:
|
||||
assert self.connection is not None
|
||||
status = self.connection.noop()[0]
|
||||
except Exception:
|
||||
pass
|
||||
if status is None or status != 250:
|
||||
# Close and connect again.
|
||||
super().close()
|
||||
self.open()
|
||||
54
zproject/jinja2/__init__.py
Normal file
54
zproject/jinja2/__init__.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.template.defaultfilters import pluralize, slugify
|
||||
from django.urls import reverse
|
||||
from django.utils import translation
|
||||
from django.utils.formats import localize
|
||||
from django.utils.timesince import timesince
|
||||
from jinja2 import Environment
|
||||
from two_factor.plugins.phonenumber.templatetags.phonenumber import device_action
|
||||
|
||||
from zerver.context_processors import DEFAULT_PAGE_PARAMS
|
||||
from zerver.lib.send_email import FromAddress
|
||||
from zerver.lib.templates import display_list, render_markdown_path, webpack_entry
|
||||
|
||||
|
||||
def json_dumps(obj: object) -> str:
|
||||
return orjson.dumps(obj).decode()
|
||||
|
||||
|
||||
def environment(**options: Any) -> Environment:
|
||||
env = Environment(autoescape=options.pop("autoescape", True), **options)
|
||||
env.globals.update(
|
||||
# default_page_params is provided here for responses where
|
||||
# zulip_default_context is not run, including the 404.html and
|
||||
# 500.html error pages.
|
||||
#
|
||||
# Note that we can't use detect the user's language using
|
||||
# get_language here, since this is only run once during
|
||||
# process initialization. This is functionally OK, in that
|
||||
# these error pages are translated on the server.
|
||||
default_page_params=DEFAULT_PAGE_PARAMS,
|
||||
static=staticfiles_storage.url,
|
||||
url=reverse,
|
||||
render_markdown_path=render_markdown_path,
|
||||
webpack_entry=webpack_entry,
|
||||
support_email=FromAddress.SUPPORT,
|
||||
)
|
||||
|
||||
env.install_gettext_translations(translation, True) # type: ignore[attr-defined] # Added by jinja2.ext.i18n
|
||||
|
||||
env.filters["slugify"] = slugify
|
||||
env.filters["pluralize"] = pluralize
|
||||
env.filters["display_list"] = display_list
|
||||
env.filters["device_action"] = device_action
|
||||
env.filters["timesince"] = timesince
|
||||
env.filters["localize"] = localize
|
||||
|
||||
env.policies["json.dumps_function"] = json_dumps
|
||||
env.policies["json.dumps_kwargs"] = {}
|
||||
env.policies["ext.i18n.trimmed"] = True
|
||||
|
||||
return env
|
||||
1
zproject/prod_settings.pyi
Normal file
1
zproject/prod_settings.pyi
Normal file
@@ -0,0 +1 @@
|
||||
from .prod_settings_template import * # noqa: F403
|
||||
985
zproject/prod_settings_template.py
Normal file
985
zproject/prod_settings_template.py
Normal file
@@ -0,0 +1,985 @@
|
||||
from typing import Any
|
||||
|
||||
from .config import get_secret
|
||||
|
||||
################################################################
|
||||
## Zulip Server settings.
|
||||
##
|
||||
## This file controls settings that affect the whole Zulip server.
|
||||
## See our documentation at:
|
||||
## https://zulip.readthedocs.io/en/latest/production/settings.html
|
||||
##
|
||||
## For developer documentation on the Zulip settings system, see:
|
||||
## https://zulip.readthedocs.io/en/latest/subsystems/settings.html
|
||||
##
|
||||
## Remember to restart the server after making changes here!
|
||||
## su zulip -c /home/zulip/deployments/current/scripts/restart-server
|
||||
|
||||
|
||||
################
|
||||
## Mandatory settings.
|
||||
##
|
||||
## These settings MUST be set in production. In a development environment,
|
||||
## sensible default values will be used.
|
||||
|
||||
## The email address for the person or team who maintains the Zulip
|
||||
## installation. Note that this is a public-facing email address; it may
|
||||
## appear on 404 pages, is used as the sender's address for many automated
|
||||
## emails, and is advertised as a support address. An email address like
|
||||
## support@example.com is totally reasonable, as is admin@example.com.
|
||||
## Do not put a display name; e.g. "support@example.com", not
|
||||
## "Zulip Support <support@example.com>".
|
||||
ZULIP_ADMINISTRATOR = "zulip-admin@example.com"
|
||||
|
||||
## The user-accessible Zulip hostname for this installation, e.g.
|
||||
## zulip.example.com. This should match what users will put in their
|
||||
## web browser. If you want to allow multiple hostnames, add the rest
|
||||
## to ALLOWED_HOSTS.
|
||||
##
|
||||
## If you need to access the server on a specific port, you should set
|
||||
## EXTERNAL_HOST to e.g. zulip.example.com:1234 here.
|
||||
EXTERNAL_HOST = "zulip.example.com"
|
||||
|
||||
## Alternative hostnames. A comma-separated list of strings
|
||||
## representing the host/domain names that your users can enter in
|
||||
## their browsers to access Zulip. This is a security measure; for
|
||||
## details, see the Django documentation:
|
||||
## https://docs.djangoproject.com/en/5.0/ref/settings/#allowed-hosts
|
||||
##
|
||||
## Zulip automatically adds to this list "localhost", "127.0.0.1", and
|
||||
## patterns representing EXTERNAL_HOST and subdomains of it. If you are
|
||||
## accessing your server by other hostnames, list them here.
|
||||
##
|
||||
## Note that these should just be hostnames, without port numbers.
|
||||
# ALLOWED_HOSTS = ["zulip-alias.example.com", "192.0.2.1"]
|
||||
|
||||
## If EXTERNAL_HOST is not a valid domain name (e.g. an IP address),
|
||||
## set FAKE_EMAIL_DOMAIN below to a domain that Zulip can use when
|
||||
## generating (fake) email addresses for bots, dummy users, etc.
|
||||
# FAKE_EMAIL_DOMAIN = "fake-domain.example.com"
|
||||
|
||||
|
||||
################
|
||||
## Outgoing email (SMTP) settings.
|
||||
##
|
||||
## Zulip needs to be able to send email (that is, use SMTP) so it can
|
||||
## confirm new users' email addresses and send notifications.
|
||||
##
|
||||
## If you don't already have an SMTP provider, free ones are available.
|
||||
##
|
||||
## For more details, including a list of free SMTP providers and
|
||||
## advice for troubleshooting, see the Zulip documentation:
|
||||
## https://zulip.readthedocs.io/en/latest/production/email.html
|
||||
|
||||
## EMAIL_HOST and EMAIL_HOST_USER are generally required. If your
|
||||
## SMTP server does not require authentication, leave EMAIL_HOST_USER
|
||||
## commented out.
|
||||
# EMAIL_HOST = "smtp.example.com"
|
||||
# EMAIL_HOST_USER = ""
|
||||
|
||||
## Passwords and secrets are not stored in this file. The password
|
||||
## for user EMAIL_HOST_USER goes in `/etc/zulip/zulip-secrets.conf`.
|
||||
## In that file, set `email_password`. For example:
|
||||
# email_password = abcd1234
|
||||
|
||||
## EMAIL_USE_TLS and EMAIL_PORT are required for most SMTP providers.
|
||||
# EMAIL_USE_TLS = True
|
||||
# EMAIL_PORT = 587
|
||||
|
||||
## By default, Zulip will open a new SMTP connection for each outgoing email.
|
||||
## This has a small overhead, though not one which is relevant in most installs,
|
||||
## which send very low volumes of email. You can set to a positive number to
|
||||
## reuse SMTP connections for that many minutes; set to None to leave connections
|
||||
## open indefinitely.
|
||||
# EMAIL_MAX_CONNECTION_LIFETIME_IN_MINUTES = 0
|
||||
|
||||
## The noreply address to be used as the sender for certain generated
|
||||
## emails. Messages sent to this address could contain sensitive user
|
||||
## data and should not be delivered anywhere. The default is
|
||||
## e.g. noreply-{token}@zulip.example.com (if EXTERNAL_HOST is
|
||||
## zulip.example.com). There are potential security issues if you set
|
||||
## ADD_TOKENS_TO_NOREPLY_ADDRESS=False to remove the token; see
|
||||
## https://zulip.readthedocs.io/en/latest/production/email.html for details.
|
||||
# ADD_TOKENS_TO_NOREPLY_ADDRESS = True
|
||||
# TOKENIZED_NOREPLY_EMAIL_ADDRESS = "noreply-{token}@example.com"
|
||||
## NOREPLY_EMAIL_ADDRESS is the sender for noreply emails that don't
|
||||
## contain confirmation links (where the security problem fixed by
|
||||
## ADD_TOKENS_TO_NOREPLY_ADDRESS does not exist), as well as for
|
||||
## confirmation emails when ADD_TOKENS_TO_NOREPLY_ADDRESS=False.
|
||||
# NOREPLY_EMAIL_ADDRESS = "noreply@example.com"
|
||||
|
||||
## Emails sent by the Zulip server will use a sender name starting
|
||||
## with INSTALLATION_NAME. The default is EXTERNAL_HOST. If INSTALLATION_NAME is
|
||||
## "zulip.example.com", email senders names will include:
|
||||
## * "zulip.example.com notifications" (message notification emails).
|
||||
## * "zulip.example.com account security" (account security emails).
|
||||
# INSTALLATION_NAME = "My Zulip Server"
|
||||
|
||||
## Many countries and bulk mailers require certain types of email to display
|
||||
## a physical mailing address to comply with anti-spam legislation.
|
||||
## Non-commercial and non-public-facing installations are unlikely to need
|
||||
## this setting.
|
||||
## The address should have no newlines.
|
||||
# PHYSICAL_ADDRESS = ""
|
||||
|
||||
|
||||
################
|
||||
## Email gateway integration.
|
||||
##
|
||||
## The email gateway integration supports sending messages into Zulip
|
||||
## by sending an email.
|
||||
## For details, see the documentation:
|
||||
## https://zulip.readthedocs.io/en/latest/production/email-gateway.html
|
||||
EMAIL_GATEWAY_PATTERN = ""
|
||||
|
||||
## If you are using polling, edit the IMAP settings below:
|
||||
##
|
||||
## The IMAP login; username here and password as email_gateway_password in
|
||||
## zulip-secrets.conf.
|
||||
EMAIL_GATEWAY_LOGIN = ""
|
||||
## The IMAP server & port to connect to
|
||||
EMAIL_GATEWAY_IMAP_SERVER = ""
|
||||
EMAIL_GATEWAY_IMAP_PORT = 993
|
||||
## The IMAP folder name to check for emails. All emails sent to EMAIL_GATEWAY_PATTERN above
|
||||
## must be delivered to this folder
|
||||
EMAIL_GATEWAY_IMAP_FOLDER = "INBOX"
|
||||
|
||||
|
||||
################
|
||||
## Authentication settings.
|
||||
##
|
||||
## Enable at least one of the following authentication backends.
|
||||
## See https://zulip.readthedocs.io/en/latest/production/authentication-methods.html
|
||||
## for documentation on our authentication backends.
|
||||
##
|
||||
## The install process requires EmailAuthBackend (the default) to be
|
||||
## enabled. If you want to disable it, do so after creating the
|
||||
## initial realm and user.
|
||||
AUTHENTICATION_BACKENDS: tuple[str, ...] = (
|
||||
"zproject.backends.EmailAuthBackend", # Email and password; just requires SMTP setup
|
||||
# "zproject.backends.GoogleAuthBackend", # Google auth, setup below
|
||||
# "zproject.backends.GitHubAuthBackend", # GitHub auth, setup below
|
||||
# "zproject.backends.GitLabAuthBackend", # GitLab auth, setup below
|
||||
# "zproject.backends.AzureADAuthBackend", # Microsoft Entra ID (AzureAD) auth, setup below
|
||||
# "zproject.backends.AppleAuthBackend", # Apple auth, setup below
|
||||
# "zproject.backends.SAMLAuthBackend", # SAML, setup below
|
||||
# "zproject.backends.ZulipLDAPAuthBackend", # LDAP, setup below
|
||||
# "zproject.backends.ZulipRemoteUserBackend", # Local SSO, setup docs on readthedocs
|
||||
# "zproject.backends.GenericOpenIdConnectBackend", # Generic OIDC integration, setup below
|
||||
# "zproject.backends.DiscordAuthBackend", # Discord auth, setup below
|
||||
)
|
||||
|
||||
## LDAP integration.
|
||||
##
|
||||
## Zulip supports retrieving information about users via LDAP, and
|
||||
## optionally using LDAP as an authentication mechanism.
|
||||
|
||||
import ldap
|
||||
from django_auth_ldap.config import ( # noqa: F401
|
||||
ActiveDirectoryGroupType,
|
||||
GroupOfNamesType,
|
||||
GroupOfUniqueNamesType,
|
||||
LDAPGroupQuery,
|
||||
LDAPGroupType,
|
||||
LDAPSearch,
|
||||
LDAPSearchUnion,
|
||||
MemberDNGroupType,
|
||||
NestedActiveDirectoryGroupType,
|
||||
NestedGroupOfNamesType,
|
||||
NestedGroupOfUniqueNamesType,
|
||||
NestedMemberDNGroupType,
|
||||
NestedOrganizationalRoleGroupType,
|
||||
OrganizationalRoleGroupType,
|
||||
PosixGroupType,
|
||||
)
|
||||
|
||||
## Connecting to the LDAP server.
|
||||
##
|
||||
## For detailed instructions, see the Zulip documentation:
|
||||
## https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#ldap
|
||||
|
||||
## The LDAP server to connect to. Setting this enables Zulip
|
||||
## automatically fetching each new user's name from LDAP.
|
||||
# AUTH_LDAP_SERVER_URI = "ldaps://ldap.example.com"
|
||||
|
||||
## The DN of the user to bind as (i.e., authenticate as) in order to
|
||||
## query LDAP. If unset, Zulip does an anonymous bind.
|
||||
# AUTH_LDAP_BIND_DN = ""
|
||||
|
||||
## Passwords and secrets are not stored in this file. The password
|
||||
## corresponding to AUTH_LDAP_BIND_DN goes in `/etc/zulip/zulip-secrets.conf`.
|
||||
## In that file, set `auth_ldap_bind_password`. For example:
|
||||
# auth_ldap_bind_password = abcd1234
|
||||
|
||||
## Mapping user info from LDAP to Zulip.
|
||||
##
|
||||
## For detailed instructions, see the Zulip documentation:
|
||||
## https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#ldap
|
||||
|
||||
## The LDAP search query to find a given user.
|
||||
##
|
||||
## The arguments to `LDAPSearch` are (base DN, scope, filter). In the
|
||||
## filter, the string `%(user)s` is a Python placeholder. The Zulip
|
||||
## server will replace this with the user's Zulip username, i.e. the
|
||||
## name they type into the Zulip login form.
|
||||
##
|
||||
## For more details and alternatives, see the documentation linked above.
|
||||
AUTH_LDAP_USER_SEARCH = LDAPSearch(
|
||||
"ou=users,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(uid=%(user)s)"
|
||||
)
|
||||
|
||||
## AUTH_LDAP_USERNAME_ATTR should be the Zulip username attribute
|
||||
## (defined in AUTH_LDAP_USER_SEARCH).
|
||||
# AUTH_LDAP_USERNAME_ATTR = "uid"
|
||||
|
||||
## Configuration to look up a user's LDAP data given their email address
|
||||
## (for Zulip reverse mapping). If users log in as e.g. "sam" when
|
||||
## their email address is "sam@example.com", set LDAP_APPEND_DOMAIN to
|
||||
## "example.com". Otherwise, leave LDAP_APPEND_DOMAIN=None and set
|
||||
## AUTH_LDAP_REVERSE_EMAIL_SEARCH and AUTH_LDAP_USERNAME_ATTR below.
|
||||
# LDAP_APPEND_DOMAIN = None
|
||||
|
||||
## LDAP attribute to find a user's email address.
|
||||
##
|
||||
## Leave as None if users log in with their email addresses,
|
||||
## or if using LDAP_APPEND_DOMAIN.
|
||||
# LDAP_EMAIL_ATTR = None
|
||||
|
||||
## AUTH_LDAP_REVERSE_EMAIL_SEARCH works like AUTH_LDAP_USER_SEARCH and
|
||||
## should query an LDAP user given their email address. It is required
|
||||
## when LDAP_APPEND_DOMAIN is None.
|
||||
# AUTH_LDAP_REVERSE_EMAIL_SEARCH = LDAPSearch("ou=users,dc=example,dc=com",
|
||||
# ldap.SCOPE_SUBTREE, "(email=%(email)s)")
|
||||
|
||||
## This map defines how to populate attributes of a Zulip user from LDAP.
|
||||
##
|
||||
## The format is `zulip_name: ldap_name`; each entry maps a Zulip
|
||||
## concept (on the left) to the LDAP attribute name (on the right) your
|
||||
## LDAP database uses for the same concept.
|
||||
AUTH_LDAP_USER_ATTR_MAP = {
|
||||
## full_name is required; common values include "cn" or "displayName".
|
||||
## If names are encoded in your LDAP directory as first and last
|
||||
## name, you can instead specify first_name and last_name, and
|
||||
## Zulip will combine those to construct a full_name automatically.
|
||||
"full_name": "cn",
|
||||
# "first_name": "fn",
|
||||
# "last_name": "ln",
|
||||
#
|
||||
## A stable unique identifier for a user allows Zulip to
|
||||
## automatically handle email address changes.
|
||||
## See https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#synchronizing-email-addresses
|
||||
# "unique_account_id": "objectSid",
|
||||
##
|
||||
## Profile pictures can be pulled from the LDAP "thumbnailPhoto"/"jpegPhoto" field.
|
||||
# "avatar": "thumbnailPhoto",
|
||||
##
|
||||
## This line is for having Zulip to automatically deactivate users
|
||||
## who are disabled in LDAP/Active Directory (and reactivate users who are not).
|
||||
## See docs for usage details and precise semantics.
|
||||
# "userAccountControl": "userAccountControl",
|
||||
## Alternatively, you can map "deactivated" to a boolean attribute
|
||||
## that is "TRUE" for deactivated users and "FALSE" otherwise.
|
||||
# "deactivated": "nsAccountLock",
|
||||
## Restrict access to organizations using an LDAP attribute.
|
||||
## See https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#restricting-ldap-user-access-to-specific-organizations
|
||||
# "org_membership": "department",
|
||||
}
|
||||
|
||||
## Whether to automatically deactivate users not found in LDAP. If LDAP
|
||||
## is the only authentication method, then this setting defaults to
|
||||
## True. If other authentication methods are enabled, it defaults to
|
||||
## False.
|
||||
# LDAP_DEACTIVATE_NON_MATCHING_USERS = True
|
||||
|
||||
## See: https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#restricting-ldap-user-access-to-specific-organizations
|
||||
# AUTH_LDAP_ADVANCED_REALM_ACCESS_CONTROL = {
|
||||
# "zulip":
|
||||
# [ # OR
|
||||
# { # AND
|
||||
# "department": "main",
|
||||
# "employeeType": "staff"
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
|
||||
|
||||
## LDAP group sync configuration.
|
||||
## See: https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#synchronizing-groups
|
||||
# AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType()
|
||||
# AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
|
||||
# "ou=groups,dc=www,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(objectClass=groupOfUniqueNames)"
|
||||
# )
|
||||
# LDAP_SYNCHRONIZED_GROUPS_BY_REALM = {
|
||||
# "subdomain1" : [
|
||||
# "group1",
|
||||
# "group2",
|
||||
# ]
|
||||
# }
|
||||
|
||||
########
|
||||
## Google OAuth.
|
||||
##
|
||||
## To set up Google authentication, you'll need to do the following:
|
||||
##
|
||||
## (1) Visit https://console.developers.google.com/ , navigate to
|
||||
## "APIs & Services" > "Credentials", and create a "Project" which will
|
||||
## correspond to your Zulip instance.
|
||||
##
|
||||
## (2) Go to "Oauth consent screen" and create a consent screen,
|
||||
## authorizing your domain and enabling the .../auth/userinfo.email,
|
||||
## .../auth/userinfo.profile and openid scopes. If all of your users
|
||||
## share a Google Workspace, you can select the "Internal" user type
|
||||
## during Oauth screen creation to limit authentication via this
|
||||
## backend to users within your Google Workspace organization.
|
||||
##
|
||||
## (3) Return to "Credentials", and select "Create credentials".
|
||||
## Choose "OAuth client ID", and fill in the app name as desired
|
||||
## and "Authorized redirect URIs" with a value like
|
||||
## https://zulip.example.com/complete/google/
|
||||
## based on your value for EXTERNAL_HOST.
|
||||
##
|
||||
## (4) You should get a client ID and a client secret. Copy them.
|
||||
## Use the client ID as `SOCIAL_AUTH_GOOGLE_KEY` here, and put the
|
||||
## client secret in zulip-secrets.conf as `social_auth_google_secret`.
|
||||
# SOCIAL_AUTH_GOOGLE_KEY = "<your client ID from Google>"
|
||||
|
||||
########
|
||||
## GitLab OAuth.
|
||||
##
|
||||
## To set up GitLab authentication, you'll need to do the following:
|
||||
##
|
||||
## (1) Register an OAuth application with GitLab at
|
||||
## https://gitlab.com/oauth/applications
|
||||
## Or the equivalent URL on a self-hosted GitLab server.
|
||||
## (2) Fill in the "Redirect URI" with a value like
|
||||
## http://zulip.example.com/complete/gitlab/
|
||||
## based on your value for EXTERNAL_HOST.
|
||||
## (3) For "scopes", select only "read_user", and create the application.
|
||||
## (4) You'll end up on a page with the Application ID and Secret for
|
||||
## your new GitLab application. Use the Application ID as
|
||||
## `SOCIAL_AUTH_GITLAB_KEY` here, and put the Secret in
|
||||
## zulip-secrets.conf as `social_auth_gitlab_secret`.
|
||||
## (5) If you are self-hosting GitLab, provide the URL of the
|
||||
## GitLab server as SOCIAL_AUTH_GITLAB_API_URL here.
|
||||
# SOCIAL_AUTH_GITLAB_KEY = "<your Application ID from GitLab>"
|
||||
# SOCIAL_AUTH_GITLAB_API_URL = "https://gitlab.example.com"
|
||||
|
||||
########
|
||||
## GitHub OAuth.
|
||||
##
|
||||
## To set up GitHub authentication, you'll need to do the following:
|
||||
##
|
||||
## (1) Register an OAuth2 application with GitHub at one of:
|
||||
## https://github.com/settings/developers
|
||||
## https://github.com/organizations/ORGNAME/settings/developers
|
||||
## Fill in "Callback URL" with a value like
|
||||
## https://zulip.example.com/complete/github/ as
|
||||
## based on your values for EXTERNAL_HOST and SOCIAL_AUTH_SUBDOMAIN.
|
||||
##
|
||||
## (2) You should get a page with settings for your new application,
|
||||
## showing a client ID and a client secret. Use the client ID as
|
||||
## `SOCIAL_AUTH_GITHUB_KEY` here, and put the client secret in
|
||||
## zulip-secrets.conf as `social_auth_github_secret`.
|
||||
# SOCIAL_AUTH_GITHUB_KEY = "<your client ID from GitHub>"
|
||||
|
||||
## (3) Optionally, you can configure the GitHub integration to only
|
||||
## allow members of a particular GitHub team or organization to log
|
||||
## into your Zulip server through GitHub authentication. To enable
|
||||
## this, set one of the two parameters below:
|
||||
# SOCIAL_AUTH_GITHUB_TEAM_ID = "<your team id>"
|
||||
# SOCIAL_AUTH_GITHUB_ORG_NAME = "<your org name>"
|
||||
|
||||
## (4) If you are serving multiple Zulip organizations on different
|
||||
## subdomains, you need to set SOCIAL_AUTH_SUBDOMAIN. You can set it
|
||||
## to any subdomain on which you do not plan to host a Zulip
|
||||
## organization. The default recommendation, `auth`, is a reserved
|
||||
## subdomain; if you're using this setting, the "Callback URL" should be e.g.:
|
||||
## https://auth.zulip.example.com/complete/github/
|
||||
#
|
||||
# SOCIAL_AUTH_SUBDOMAIN = "auth"
|
||||
|
||||
########
|
||||
## Discord OAuth.
|
||||
## To set up Discord authentication, you'll need to do the following:
|
||||
## (1) Register a new application at the Discord Developer Portal:
|
||||
## https://discord.com/developers/applications
|
||||
## (2) Navigate to the OAuth menu. Set the callback URI with value like:
|
||||
## https://zulip.example.com/complete/discord/
|
||||
## based on your value for EXTERNAL_HOST
|
||||
## (3) Note the Client ID and Secret for your new Discord application.
|
||||
## Use the Client ID as `SOCIAL_AUTH_DISCORD_KEY` here, and
|
||||
## put the Secret in zulip-secrets.conf as `social_auth_discord_secret`.
|
||||
#
|
||||
# SOCIAL_AUTH_DISCORD_KEY = "<your client ID from Discord>"
|
||||
|
||||
########
|
||||
## Generic OpenID Connect (OIDC). See also documentation here:
|
||||
##
|
||||
## https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#openid-connect
|
||||
##
|
||||
|
||||
SOCIAL_AUTH_OIDC_ENABLED_IDPS: dict[str, Any] = {
|
||||
## This field (example: "idp_name") may appear in URLs during
|
||||
## authentication, but is otherwise not user-visible.
|
||||
"idp_name": {
|
||||
## The base path to the provider's OIDC API. Zulip fetches the
|
||||
## IdP's configuration from the discovery endpoint, which will be
|
||||
## "{oidc_url}/.well-known/openid-configuration".
|
||||
"oidc_url": "https://example.com/api/openid",
|
||||
## The display name, used for "Log in with <display name>" buttons.
|
||||
"display_name": "Example",
|
||||
## Optional: URL of an icon to decorate "Log in with <display name>" buttons.
|
||||
"display_icon": None,
|
||||
## The client_id and secret provided by your OIDC IdP. To keep
|
||||
## settings.py free of secrets, the get_secret call below
|
||||
## reads the secret with the specified name from zulip-secrets.conf.
|
||||
"client_id": "<your client id>",
|
||||
"secret": get_secret("social_auth_oidc_secret"),
|
||||
## If you want this IdP to only be enabled for authentication
|
||||
## to certain subdomains, uncomment and edit the setting below.
|
||||
# "limit_to_subdomains": ["subdomain1", "subdomain2"],
|
||||
##
|
||||
## Determines whether "Log in with OIDC" will automatically
|
||||
## register a new account if one does not already exist. By
|
||||
## default, Zulip asks the user whether they want to create an
|
||||
## account or try to log in again using another method.
|
||||
# "auto_signup": False,
|
||||
## List of additional claims to fetch from the id_token or response
|
||||
## from the UserInfo endpoint.
|
||||
## These attributes will be available for synchronizing user
|
||||
## profile fields in SOCIAL_AUTH_SYNC_ATTRS_DICT.
|
||||
# "extra_attrs": ["title", "mobilePhone", "zulip_role"],
|
||||
},
|
||||
## Example: Microsoft Entra ID (AzureAD) OIDC configuration.
|
||||
## This is the recommended approach for Entra ID SSO on self-hosted servers.
|
||||
## See https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#microsoft-entra-id
|
||||
##
|
||||
# "entra": {
|
||||
# "oidc_url": "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0",
|
||||
# "display_name": "Microsoft",
|
||||
# "display_icon": "/static/images/authentication_backends/microsoft-icon.png",
|
||||
# "client_id": "YOUR_APPLICATION_ID",
|
||||
# "secret": get_secret("social_auth_oidc_secret"),
|
||||
# "auto_signup": True,
|
||||
# },
|
||||
}
|
||||
|
||||
## For documentation on this setting, see the relevant part of
|
||||
## https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#openid-connect
|
||||
# SOCIAL_AUTH_OIDC_FULL_NAME_VALIDATED = True
|
||||
|
||||
########
|
||||
## SAML authentication
|
||||
##
|
||||
## For SAML authentication, you will need to configure the settings
|
||||
## below using information from your SAML identity provider, as
|
||||
## explained in:
|
||||
##
|
||||
## https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#saml
|
||||
##
|
||||
## You will need to modify these SAML settings:
|
||||
SOCIAL_AUTH_SAML_ORG_INFO = {
|
||||
"en-US": {
|
||||
"displayname": "Example, Inc. Zulip",
|
||||
"name": "zulip",
|
||||
"url": "{}{}".format("https://", EXTERNAL_HOST),
|
||||
},
|
||||
}
|
||||
SOCIAL_AUTH_SAML_ENABLED_IDPS: dict[str, Any] = {
|
||||
## The fields are explained in detail here:
|
||||
## https://python-social-auth.readthedocs.io/en/latest/backends/saml.html
|
||||
"idp_name": {
|
||||
## Configure entity_id and url according to information provided to you by your IdP:
|
||||
"entity_id": "https://idp.testshib.org/idp/shibboleth",
|
||||
"url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO",
|
||||
##
|
||||
## The part below corresponds to what's likely referred to as something like
|
||||
## "Attribute Statements" (with Okta as your IdP) or "Attribute Mapping" (with Google Workspace).
|
||||
## The names on the right side need to correspond to the names under which
|
||||
## the IdP will send the user attributes. With these defaults, it's expected
|
||||
## that the user's email will be sent with the "email" attribute name,
|
||||
## the first name and the last name with the "first_name", "last_name" attribute names.
|
||||
"attr_user_permanent_id": "email",
|
||||
"attr_first_name": "first_name",
|
||||
"attr_last_name": "last_name",
|
||||
## If your IdP doesn't split the name into the first and last name, and instead sends
|
||||
## the full name in a single attribute, you can remove the above attr_*_name lines
|
||||
## and instead configure attr_full_name below.
|
||||
# "attr_full_name": "fullname",
|
||||
"attr_username": "email",
|
||||
"attr_email": "email",
|
||||
## List of additional attributes to fetch from the SAMLResponse.
|
||||
## These attributes will be available for synchronizing user profile fields.
|
||||
## in SOCIAL_AUTH_SYNC_ATTRS_DICT.
|
||||
# "extra_attrs": ["title", "mobilePhone", "zulip_role"],
|
||||
##
|
||||
## The "x509cert" attribute is automatically read from
|
||||
## /etc/zulip/saml/idps/{idp_name}.crt; don't specify it here.
|
||||
##
|
||||
## Optionally, you can edit display_name and display_icon
|
||||
## settings below to change the name and icon that will show on
|
||||
## the login button.
|
||||
"display_name": "SAML",
|
||||
##
|
||||
## URL of a square image file containing a logo to appear at
|
||||
## the left end of the login/register buttons for this IDP.
|
||||
## This can be a relative path, on the same host, most likely
|
||||
## under /static/ (stored in ~zulip/prod-static/). The default
|
||||
## of None results in a text-only button.
|
||||
# "display_icon": "https://example.com/path/to/icon.png",
|
||||
##
|
||||
## If you want this IdP to only be enabled for authentication
|
||||
## to certain subdomains, uncomment and edit the setting below.
|
||||
# "limit_to_subdomains": ["subdomain1", "subdomain2"],
|
||||
##
|
||||
## You can also limit subdomains by setting "attr_org_membership"
|
||||
## to be a SAML attribute containing the allowed subdomains for a user.
|
||||
# "attr_org_membership": "member",
|
||||
##
|
||||
## Determines whether "Log in with SAML" will automatically
|
||||
## register a new account if one does not already exist. By
|
||||
## default, Zulip asks the user whether they want to create an
|
||||
## account or try to log in again using another method.
|
||||
# "auto_signup": False,
|
||||
## Determines whether Service Provider initiated SAML Single Logout should be enabled.
|
||||
## Note that IdP-initiated Single Logout must be configured before enabling this.
|
||||
# "sp_initiated_logout_enabled": False,
|
||||
},
|
||||
}
|
||||
|
||||
# More complete documentation of the configurable security settings
|
||||
# are available in the "security" part of https://github.com/onelogin/python3-saml#settings.
|
||||
SOCIAL_AUTH_SAML_SECURITY_CONFIG: dict[str, Any] = {
|
||||
## If you've set up the optional private and public server keys,
|
||||
## set this to True to enable signing of SAMLRequests using the
|
||||
## private key.
|
||||
"authnRequestsSigned": False,
|
||||
## If you'd like the Zulip server to request that the IdP limit user identity
|
||||
## verification to a specific set of authentication contexts, you can do this
|
||||
## by changing the requestedAuthnContext parameter to a list of specific
|
||||
## Authentication Context Classes that you want to include in the AuthnContext. E.g.:
|
||||
##
|
||||
# "requestedAuthnContext": ["urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
|
||||
# "urn:oasis:names:tc:SAML:2.0:ac:classes:X509"],
|
||||
##
|
||||
## For details on this, see the aforementioned python3-saml documentation
|
||||
## and https://docs.oasis-open.org/security/saml/v2.0/saml-authn-context-2.0-os.pdf
|
||||
"requestedAuthnContext": False,
|
||||
}
|
||||
|
||||
## These SAML settings you likely won't need to modify.
|
||||
SOCIAL_AUTH_SAML_SP_ENTITY_ID = "https://" + EXTERNAL_HOST
|
||||
SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = {
|
||||
"givenName": "Technical team",
|
||||
"emailAddress": ZULIP_ADMINISTRATOR,
|
||||
}
|
||||
SOCIAL_AUTH_SAML_SUPPORT_CONTACT = {
|
||||
"givenName": "Support team",
|
||||
"emailAddress": ZULIP_ADMINISTRATOR,
|
||||
}
|
||||
|
||||
## Note: Any additional attributes used here must be listed in the "extra_attrs"
|
||||
## field in SOCIAL_AUTH_SAML_ENABLED_IDPS (SAML) or SOCIAL_AUTH_OIDC_ENABLED_IDPS (OIDC).
|
||||
## Sync for other backends is not currently supported.
|
||||
# SOCIAL_AUTH_SYNC_ATTRS_DICT = {
|
||||
# "example_org": {
|
||||
# "saml": {
|
||||
# # No extra_attrs are required to enable syncing names, which will use
|
||||
# # the standard SAML name attributes.
|
||||
# "full_name": True,
|
||||
# # role is the other supported major attribute.
|
||||
# "role": "zulip_role",
|
||||
# # Specify custom profile fields with a custom__ prefix for the
|
||||
# # Zulip field name.
|
||||
# "custom__title": "title",
|
||||
# # Sync the membership of the listed Zulip groups with
|
||||
# # the list of group names sent in the "zulip_groups"
|
||||
# # attribute in the SAMLResponse.
|
||||
# "groups": ["group1", "group2", ("samlgroup3", "zulipgroup3"), "group4"],
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
########
|
||||
## Apple authentication ("Sign in with Apple").
|
||||
##
|
||||
## Configure the below settings by following the instructions here:
|
||||
##
|
||||
## https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#sign-in-with-apple
|
||||
#
|
||||
# SOCIAL_AUTH_APPLE_TEAM = "<your Team ID>"
|
||||
# SOCIAL_AUTH_APPLE_SERVICES_ID = "<your Services ID>"
|
||||
# SOCIAL_AUTH_APPLE_APP_ID = "<your App ID>"
|
||||
# SOCIAL_AUTH_APPLE_KEY = "<your Key ID>"
|
||||
|
||||
########
|
||||
## Microsoft Entra ID (AzureAD) OAuth (common tenant only).
|
||||
##
|
||||
## NOTE: For self-hosted servers that need to restrict authentication to a
|
||||
## specific Entra ID tenant, we recommend using the OIDC integration instead.
|
||||
## See the OpenID Connect section above.
|
||||
##
|
||||
## The backend below uses the Microsoft "common" tenant endpoint, which allows
|
||||
## any Microsoft account to authenticate rather than restricting logins to a
|
||||
## specific organization.
|
||||
##
|
||||
## To set up Microsoft Entra ID authentication, you'll need to do the following:
|
||||
##
|
||||
## (1) Sign in to the Microsoft Entra admin center at https://entra.microsoft.com,
|
||||
## browse to "Entra ID > App registrations", and click "New registration".
|
||||
##
|
||||
## (2) In the "Redirect URI (optional)" section, select Web as the platform
|
||||
## and enter https://zulip.example.com/complete/azuread-oauth2/ as the redirect URI,
|
||||
## based on your values of EXTERNAL_HOST and SOCIAL_AUTH_SUBDOMAIN.
|
||||
##
|
||||
## (3) After registering the app, go to "Certificates & secrets" and
|
||||
## generate a new client secret. Make sure to save the generated Value.
|
||||
##
|
||||
## (4) Enter the application ID for the app as SOCIAL_AUTH_AZUREAD_OAUTH2_KEY below
|
||||
## and the generated secret Value in zulip-secrets.conf as `social_auth_azuread_oauth2_secret`.
|
||||
# SOCIAL_AUTH_AZUREAD_OAUTH2_KEY = ""
|
||||
|
||||
########
|
||||
## SSO via REMOTE_USER.
|
||||
##
|
||||
## If you are using the ZulipRemoteUserBackend authentication backend,
|
||||
## and REMOTE_USER does not already include a domain, set this to your
|
||||
## domain (e.g. if REMOTE_USER is "username" and the corresponding
|
||||
## email address is "username@example.com", set SSO_APPEND_DOMAIN =
|
||||
## "example.com"), otherwise leave this as None.
|
||||
# SSO_APPEND_DOMAIN = None
|
||||
|
||||
## JWT authentication.
|
||||
##
|
||||
## JWT authentication is supported both to transparently log users
|
||||
## into Zulip or to fetch users' API keys. The JWT secret key and
|
||||
## algorithm must be configured here.
|
||||
##
|
||||
## See https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#json-web-tokens-jwt
|
||||
# JWT_AUTH_KEYS = {
|
||||
# # Subdomain for which this JWT configuration will apply.
|
||||
# "zulip": {
|
||||
# # Shared secret key used to validate jwt tokens, which should be stored
|
||||
# # in zulip-secrets.conf and is read by the get_secret call below.
|
||||
# # The key needs to be securely, randomly generated. Note that if you're
|
||||
# # using the default HS256 algorithm, per RFC 7518, the key needs
|
||||
# # to have at least 256 bits of entropy.
|
||||
# "key": get_secret("jwt_auth_key"),
|
||||
# # Algorithm with which the JWT token are signed.
|
||||
# "algorithms": ["HS256"],
|
||||
# }
|
||||
# }
|
||||
|
||||
################
|
||||
## Service configuration
|
||||
|
||||
########
|
||||
## PostgreSQL configuration.
|
||||
##
|
||||
## To access an external PostgreSQL database you should define the host name in
|
||||
## REMOTE_POSTGRES_HOST, port in REMOTE_POSTGRES_PORT, password in the secrets file in the
|
||||
## property postgres_password, and the SSL connection mode in REMOTE_POSTGRES_SSLMODE
|
||||
## Valid values for REMOTE_POSTGRES_SSLMODE are documented in the
|
||||
## "SSL Mode Descriptions" table in
|
||||
## https://www.postgresql.org/docs/9.5/static/libpq-ssl.html
|
||||
# REMOTE_POSTGRES_HOST = "dbserver.example.com"
|
||||
# REMOTE_POSTGRES_PORT = "5432"
|
||||
# REMOTE_POSTGRES_SSLMODE = "verify-full"
|
||||
|
||||
########
|
||||
## RabbitMQ configuration.
|
||||
##
|
||||
## By default, Zulip connects to RabbitMQ running locally on the
|
||||
## machine, but Zulip also supports connecting to RabbitMQ over the
|
||||
## network; to use a remote RabbitMQ instance, set RABBITMQ_HOST, and
|
||||
## optionally RABBITMQ_PORT, to the hostname and port here.
|
||||
# RABBITMQ_HOST = "127.0.0.1"
|
||||
# RABBITMQ_PORT = 5672
|
||||
## To use a different RabbitMQ "virtual host", adjust this.
|
||||
# RABBITMQ_VHOST = "/"
|
||||
## To use another RabbitMQ user than the default "zulip", set RABBITMQ_USERNAME here.
|
||||
# RABBITMQ_USERNAME = "zulip"
|
||||
## To access the RabbitMQ server over TLS, set this to True; this is
|
||||
## generally only necessary if RabbitMQ is running on a separate,
|
||||
## cloud-managed, host.
|
||||
# RABBITMQ_USE_TLS = False
|
||||
|
||||
########
|
||||
## Redis configuration.
|
||||
##
|
||||
## By default, Zulip connects to Redis running locally on the machine,
|
||||
## but Zulip also supports connecting to Redis over the network;
|
||||
## to use a remote Redis instance, set REDIS_HOST here.
|
||||
# REDIS_HOST = "127.0.0.1"
|
||||
## For a different Redis port set the REDIS_PORT here.
|
||||
# REDIS_PORT = 6379
|
||||
## If you set redis_password in zulip-secrets.conf, Zulip will use that password
|
||||
## to connect to the Redis server.
|
||||
|
||||
########
|
||||
## Memcached configuration.
|
||||
##
|
||||
## By default, Zulip connects to memcached running locally on the machine,
|
||||
## but Zulip also supports connecting to memcached over the network;
|
||||
## to use a remote Memcached instance, set MEMCACHED_LOCATION here.
|
||||
## Format HOST:PORT
|
||||
# MEMCACHED_LOCATION = 127.0.0.1:11211
|
||||
## To authenticate to memcached, set memcached_password in zulip-secrets.conf,
|
||||
## and optionally change the default username "zulip@localhost" here.
|
||||
# MEMCACHED_USERNAME = "zulip@localhost"
|
||||
|
||||
|
||||
################
|
||||
## Previews.
|
||||
|
||||
########
|
||||
## Image and URL previews.
|
||||
##
|
||||
## Controls whether or not Zulip will provide inline image preview when
|
||||
## a link to an image is referenced in a message. Note: this feature
|
||||
## can also be disabled in a realm's organization settings.
|
||||
# INLINE_IMAGE_PREVIEW = True
|
||||
|
||||
## Controls whether or not Zulip will provide inline previews of
|
||||
## websites that are referenced in links in messages. Note: this feature
|
||||
## can also be disabled in a realm's organization settings.
|
||||
# INLINE_URL_EMBED_PREVIEW = True
|
||||
|
||||
|
||||
################
|
||||
## Logging and error reporting.
|
||||
##
|
||||
## Controls whether or not error reports (tracebacks) are emailed to the
|
||||
## server administrators.
|
||||
# ERROR_REPORTING = True
|
||||
|
||||
## Controls the DSN used to report errors to Sentry.io
|
||||
# SENTRY_DSN = "https://aaa@bbb.ingest.sentry.io/1234"
|
||||
# SENTRY_FRONTEND_DSN = "https://aaa@bbb.ingest.sentry.io/1234"
|
||||
## What portion of events are sampled (https://docs.sentry.io/platforms/javascript/configuration/sampling/):
|
||||
# SENTRY_FRONTEND_SAMPLE_RATE = 1.0
|
||||
# SENTRY_FRONTEND_TRACE_RATE = 0.1
|
||||
|
||||
## If True, each log message in the server logs will identify the
|
||||
## Python module where it came from. Useful for tracking down a
|
||||
## mysterious log message, but a little verbose.
|
||||
# LOGGING_SHOW_MODULE = False
|
||||
|
||||
## If True, each log message in the server logs will identify the
|
||||
## process ID. Useful for correlating logs with information from
|
||||
## system-level monitoring tools.
|
||||
# LOGGING_SHOW_PID = False
|
||||
|
||||
#################
|
||||
## GIF picker / search engine integrations. To get an API key, see:
|
||||
## https://zulip.readthedocs.io/en/latest/production/gif-picker-integrations.html
|
||||
# GIPHY_API_KEY = "<Your API key from GIPHY>"
|
||||
# TENOR_API_KEY = "<Your API key from Tenor>"
|
||||
# KLIPY_API_KEY = "<Your API key from KLIPY>"
|
||||
|
||||
################
|
||||
## Video call integrations.
|
||||
##
|
||||
## Controls the Zoom video call integrations. See:
|
||||
## https://zulip.readthedocs.io/en/latest/production/video-calls.html
|
||||
# VIDEO_ZOOM_CLIENT_ID = "<your Zoom client ID>"
|
||||
# VIDEO_ZOOM_SERVER_TO_SERVER_ACCOUNT_ID = "<your Zoom account ID>"
|
||||
## Set these if using a Zoom host that is not https://zoom.us.
|
||||
# VIDEO_ZOOM_OAUTH_URL = "https://zoom.example.com"
|
||||
# VIDEO_ZOOM_API_URL = "https://api.zoom.example.com"
|
||||
|
||||
## Controls the Webex video call integration. See:
|
||||
## https://zulip.readthedocs.io/en/latest/production/video-calls.html
|
||||
## You must also set video_webex_client_secret in
|
||||
## /etc/zulip/zulip-secrets.conf to enable Webex as a call provider.
|
||||
# VIDEO_WEBEX_CLIENT_ID = "<your Webex client ID>"
|
||||
|
||||
## Controls the Jitsi Meet video call integration. By default, the
|
||||
## integration uses the SaaS https://meet.jit.si server. You can specify
|
||||
## your own Jitsi Meet server, or if you'd like to disable the
|
||||
## integration, set JITSI_SERVER_URL = None.
|
||||
# JITSI_SERVER_URL = "https://jitsi.example.com"
|
||||
|
||||
## Controls the BigBlueButton video call integration. You must also
|
||||
## set big_blue_button_secret in zulip-secrets.conf.
|
||||
# BIG_BLUE_BUTTON_URL = "https://bbb.example.com/bigbluebutton/"
|
||||
|
||||
## Controls the Constructor Groups video call integration. You must also
|
||||
## set constructor_groups_access_key and constructor_groups_secret_key in
|
||||
## zulip-secrets.conf.
|
||||
# CONSTRUCTOR_GROUPS_URL = "https://example.constructor.app/api/groups/xapi"
|
||||
|
||||
## Controls the Nextcloud Talk video call integration. You must also
|
||||
## set nextcloud_talk_username and nextcloud_talk_password in
|
||||
## zulip-secrets.conf.
|
||||
# NEXTCLOUD_SERVER = "https://nextcloud.example.com"
|
||||
|
||||
################
|
||||
## AI Features
|
||||
##
|
||||
## Specify the model to use for topic summarization. Zulip uses the
|
||||
## OpenAI Python SDK, which supports OpenAI's API as well as any
|
||||
## OpenAI-compatible provider.
|
||||
# TOPIC_SUMMARIZATION_MODEL = "meta-llama/Meta-Llama-3-8B-Instruct"
|
||||
## Override the API base URL when using an OpenAI-compatible provider
|
||||
## (e.g., Hugging Face, Groq, Together, or a self-hosted endpoint).
|
||||
## Leave unset to use OpenAI's default endpoint.
|
||||
# TOPIC_SUMMARIZATION_API_BASE = "https://router.huggingface.co/v1"
|
||||
## Other configuration parameters, passed through to the OpenAI SDK's
|
||||
## `chat.completions.create` call. See
|
||||
## https://platform.openai.com/docs/api-reference/chat/create
|
||||
# TOPIC_SUMMARIZATION_PARAMETERS = {}
|
||||
|
||||
## Set usage costs based on your model, and a maximum per-user monthly
|
||||
## quota. Units are USD or other currency of your choice.
|
||||
# OUTPUT_COST_PER_GIGATOKEN = 500
|
||||
# INPUT_COST_PER_GIGATOKEN = 400
|
||||
# MAX_PER_USER_MONTHLY_AI_COST = 0.5
|
||||
|
||||
################
|
||||
## Miscellaneous settings.
|
||||
|
||||
## If you host multiple organizations on the same Zulip server, you
|
||||
## can customize the hostname that one of them uses by adding it here.
|
||||
## See https://zulip.readthedocs.io/en/latest/production/multiple-organizations.html#other-hostnames
|
||||
## Note that you still need SSL certificates for this other custom hostname:
|
||||
## https://zulip.readthedocs.io/en/latest/production/multiple-organizations.html#ssl-certificates
|
||||
# REALM_HOSTS = {
|
||||
# "example": "hostname.example.com",
|
||||
# }
|
||||
|
||||
## How long outgoing webhook requests time out after
|
||||
# OUTGOING_WEBHOOK_TIMEOUT_SECONDS = 10
|
||||
|
||||
## Mobile push notifications require registering for the Zulip Mobile
|
||||
## Push Notification Service and configuring your server to use the
|
||||
## service here. For complete documentation, see:
|
||||
##
|
||||
## https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html
|
||||
##
|
||||
# ZULIP_SERVICE_PUSH_NOTIFICATIONS = True
|
||||
|
||||
## By default, a Zulip server that has registered for Zulip services
|
||||
## submits both basic metadata (required for billing and for determining
|
||||
## free plan eligibility), as well as aggregate usage statistics. You
|
||||
## can disable submitting usage statistics here.
|
||||
# ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS = False
|
||||
|
||||
## Whether to lightly advertise sponsoring Zulip in the gear menu.
|
||||
# PROMOTE_SPONSORING_ZULIP = True
|
||||
|
||||
## Controls whether session cookies expire when the browser closes
|
||||
# SESSION_EXPIRE_AT_BROWSER_CLOSE = False
|
||||
|
||||
## Session cookie expiry in seconds after the last page load
|
||||
# SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2 # 2 weeks
|
||||
|
||||
## Password strength requirements; learn about configuration at
|
||||
## https://zulip.readthedocs.io/en/latest/production/securing-your-zulip-server.html.
|
||||
# PASSWORD_MIN_LENGTH = 8
|
||||
# PASSWORD_MAX_LENGTH = 100
|
||||
# PASSWORD_MIN_GUESSES = 10000
|
||||
|
||||
## Controls whether Zulip sends "new login" email notifications.
|
||||
# SEND_LOGIN_EMAILS = True
|
||||
|
||||
## Controls whether or not Zulip will parse links starting with
|
||||
## "file:///" as a hyperlink (useful if you have e.g. an NFS share).
|
||||
# ENABLE_FILE_LINKS = False
|
||||
|
||||
## By default, files uploaded by users and profile pictures are stored
|
||||
## directly on the Zulip server. You can configure files being instead
|
||||
## stored in Amazon S3 or another scalable data store here. See docs at:
|
||||
##
|
||||
## https://zulip.readthedocs.io/en/latest/production/upload-backends.html
|
||||
##
|
||||
## If you change LOCAL_UPLOADS_DIR to a different path, you will also
|
||||
## need to manually edit Zulip's nginx configuration to use the new
|
||||
## path. For that reason, we recommend replacing /home/zulip/uploads
|
||||
## with a symlink instead of changing LOCAL_UPLOADS_DIR.
|
||||
LOCAL_UPLOADS_DIR = "/home/zulip/uploads"
|
||||
# S3_AUTH_UPLOADS_BUCKET = ""
|
||||
# S3_AVATAR_BUCKET = ""
|
||||
# S3_EXPORT_BUCKET = ""
|
||||
# S3_REGION = None
|
||||
# S3_ENDPOINT_URL = None
|
||||
# S3_AVATAR_PUBLIC_URL_PREFIX = None
|
||||
# S3_ADDRESSING_STYLE = "auto"
|
||||
# S3_SKIP_PROXY = True
|
||||
# S3_UPLOADS_STORAGE_CLASS = "STANDARD"
|
||||
# S3_SKIP_CHECKSUM = False
|
||||
|
||||
## Maximum allowed size of uploaded files, in megabytes. Set
|
||||
## MAX_FILE_UPLOAD_SIZE to 0 to disable file uploads completely
|
||||
## (including hiding upload-related options from UI).
|
||||
# MAX_FILE_UPLOAD_SIZE = 100
|
||||
|
||||
## Controls whether name changes are completely disabled for this
|
||||
## installation. This is useful when you're syncing names from an
|
||||
## integrated LDAP/Active Directory.
|
||||
# NAME_CHANGES_DISABLED = False
|
||||
|
||||
## Controls whether avatar changes are completely disabled for this
|
||||
## installation. This is useful when you're syncing avatars from an
|
||||
## integrated LDAP/Active Directory.
|
||||
# AVATAR_CHANGES_DISABLED = False
|
||||
|
||||
## Controls whether users who have not uploaded an avatar will receive an avatar
|
||||
## from gravatar.com.
|
||||
# ENABLE_GRAVATAR = True
|
||||
|
||||
## To override the default avatar image if ENABLE_GRAVATAR is False, place your
|
||||
## custom default avatar image at /home/zulip/local-static/default-avatar.png
|
||||
## and uncomment the following line.
|
||||
# DEFAULT_AVATAR_URI = "/local-static/default-avatar.png"
|
||||
|
||||
## The default CAMO_URI of "/external_content/" is served by the camo
|
||||
## setup in the default Zulip nginx configuration. Setting CAMO_URI
|
||||
## to "" will disable the Camo integration.
|
||||
CAMO_URI = "/external_content/"
|
||||
|
||||
## Controls various features explaining Zulip to new users. Disabling
|
||||
## this is only recommended for installations that are using a limited
|
||||
## subset of the Zulip UI, such as embedding it in a larger app.
|
||||
# TUTORIAL_ENABLED = True
|
||||
|
||||
## Controls whether Zulip will rate-limit user requests.
|
||||
# RATE_LIMITING = True
|
||||
|
||||
## Entries in this dictionary will override Zulip's default rate
|
||||
## limits. Rules which are not explicitly overridden here
|
||||
## will be as default. View the current rules using:
|
||||
## /home/zulip/deployments/current/scripts/get-django-setting RATE_LIMITING_RULES
|
||||
##
|
||||
## The limits are tuples of a number of seconds and a number of
|
||||
## requests allowed over that many seconds. If multiple tuples are
|
||||
## given in a rule, a request breaching any of them will trigger a
|
||||
## rate-limited response to the client. For example, to change the
|
||||
## limits for total API requests by each user to be at most 100
|
||||
## requests per minute, and at most 200 requests per hour, add:
|
||||
## "api_by_user": [(60, 100), (3600, 200)],
|
||||
# RATE_LIMITING_RULES = {
|
||||
# "api_by_ip": [
|
||||
# (60, 100),
|
||||
# ],
|
||||
# }
|
||||
|
||||
## Fetch TOR exit node list every hour, and group all TOR exit nodes
|
||||
## together into one bucket when applying rate-limiting.
|
||||
# RATE_LIMIT_TOR_TOGETHER = False
|
||||
|
||||
## Configuration for Terms of Service and Privacy Policy for the
|
||||
## server. If unset, Zulip will never prompt users to accept Terms of
|
||||
## Service. Users will be prompted to accept the terms during account
|
||||
## registration, and during login if this value has changed.
|
||||
# TERMS_OF_SERVICE_VERSION = "1.0"
|
||||
|
||||
## Directory containing Markdown files for the server's policies.
|
||||
# POLICIES_DIRECTORY = "/etc/zulip/policies/"
|
||||
|
||||
## URL of the navigation tour video to show to new users. You can use this
|
||||
## to host the official video on your network, or to provide your own
|
||||
## introductory video with details on how your organization uses Zulip.
|
||||
##
|
||||
## A value of None disables the navigation tour video experience.
|
||||
# NAVIGATION_TOUR_VIDEO_URL = "https://static.zulipchat.com/static/navigation-tour-video/zulip-10.mp4"
|
||||
121
zproject/sentry.py
Normal file
121
zproject/sentry.py
Normal file
@@ -0,0 +1,121 @@
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
import sentry_sdk
|
||||
from django.utils.translation import override as override_language
|
||||
from sentry_sdk.integrations import Integration
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from sentry_sdk.integrations.logging import ignore_logger
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
from sentry_sdk.integrations.tornado import TornadoIntegration
|
||||
from sentry_sdk.utils import capture_internal_exceptions
|
||||
|
||||
from version import ZULIP_VERSION
|
||||
from zproject.config import DEPLOY_ROOT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sentry_sdk._types import Event, Hint
|
||||
|
||||
|
||||
def add_context(event: "Event", hint: "Hint") -> Optional["Event"]:
|
||||
if "exc_info" in hint:
|
||||
_, exc_value, _ = hint["exc_info"]
|
||||
# Ignore GeneratorExit, KeyboardInterrupt, and SystemExit exceptions
|
||||
if not isinstance(exc_value, Exception):
|
||||
return None
|
||||
from django.conf import settings
|
||||
|
||||
from zerver.models.users import get_user_profile_by_id
|
||||
|
||||
with capture_internal_exceptions():
|
||||
# event.user is the user context, from Sentry, which is
|
||||
# pre-populated with some keys via its Django integration:
|
||||
# https://docs.sentry.io/platforms/python/guides/django/enriching-error-data/additional-data/identify-user/
|
||||
event.setdefault("tags", {})
|
||||
user_info = event.get("user", {})
|
||||
user_id = user_info.get("id")
|
||||
if isinstance(user_id, str):
|
||||
user_profile = get_user_profile_by_id(int(user_id))
|
||||
event["tags"]["realm"] = user_info["realm"] = user_profile.realm.string_id or "root"
|
||||
with override_language(settings.LANGUAGE_CODE):
|
||||
# str() to force the lazy-translation to apply now,
|
||||
# since it won't serialize into json for Sentry otherwise
|
||||
user_info["role"] = str(user_profile.get_role_name())
|
||||
|
||||
# These are PII, and should be scrubbed
|
||||
if "username" in user_info:
|
||||
del user_info["username"]
|
||||
if "email" in user_info:
|
||||
del user_info["email"]
|
||||
|
||||
return event
|
||||
|
||||
|
||||
def traces_sampler(sampling_context: dict[str, Any]) -> float | bool:
|
||||
from django.conf import settings
|
||||
|
||||
queue = sampling_context.get("queue")
|
||||
if queue is not None and isinstance(queue, str):
|
||||
if isinstance(settings.SENTRY_TRACE_WORKER_RATE, dict):
|
||||
return settings.SENTRY_TRACE_WORKER_RATE.get(queue, 0.0)
|
||||
else:
|
||||
return settings.SENTRY_TRACE_WORKER_RATE
|
||||
else:
|
||||
return settings.SENTRY_TRACE_RATE
|
||||
|
||||
|
||||
def setup_sentry(dsn: str | None, environment: str) -> None:
|
||||
from django.conf import settings
|
||||
|
||||
if not dsn:
|
||||
return
|
||||
|
||||
sentry_release = ZULIP_VERSION
|
||||
if os.path.exists(os.path.join(DEPLOY_ROOT, "sentry-release")):
|
||||
with open(os.path.join(DEPLOY_ROOT, "sentry-release")) as sentry_release_file:
|
||||
sentry_release = sentry_release_file.readline().strip()
|
||||
integrations: list[Integration] = [
|
||||
RedisIntegration(),
|
||||
]
|
||||
disabled_integrations = []
|
||||
if settings.RUNNING_INSIDE_TORNADO:
|
||||
integrations.append(TornadoIntegration())
|
||||
disabled_integrations.append(DjangoIntegration())
|
||||
else:
|
||||
integrations.append(DjangoIntegration())
|
||||
sentry_sdk.init(
|
||||
dsn=dsn,
|
||||
environment=environment,
|
||||
release=sentry_release,
|
||||
integrations=integrations,
|
||||
disabled_integrations=disabled_integrations,
|
||||
before_send=add_context,
|
||||
# Increase possible max wait to send exceptions during
|
||||
# shutdown, from 2 to 10; potentially-large exceptions are of
|
||||
# value to catch during shutdown.
|
||||
shutdown_timeout=10,
|
||||
# Because we strip the email/username from the Sentry data
|
||||
# above, the effect of this flag is that the requests/users
|
||||
# involved in exceptions will be identified in Sentry only by
|
||||
# their IP address, user ID, realm, and role. We consider
|
||||
# this an appropriate balance between avoiding Sentry getting
|
||||
# PII while having the identifiers needed to determine that an
|
||||
# exception only affects a small subset of users or realms.
|
||||
send_default_pii=True,
|
||||
traces_sampler=traces_sampler,
|
||||
profiles_sample_rate=settings.SENTRY_PROFILE_RATE,
|
||||
)
|
||||
|
||||
# Ignore all of the loggers from django.security that are for user
|
||||
# errors; see https://docs.djangoproject.com/en/5.0/ref/exceptions/#suspiciousoperation
|
||||
ignore_logger("django.security.SuspiciousOperation")
|
||||
ignore_logger("django.security.DisallowedHost")
|
||||
ignore_logger("django.security.DisallowedModelAdminLookup")
|
||||
ignore_logger("django.security.DisallowedModelAdminToField")
|
||||
ignore_logger("django.security.DisallowedRedirect")
|
||||
ignore_logger("django.security.InvalidSessionKey")
|
||||
ignore_logger("django.security.RequestDataTooBig")
|
||||
ignore_logger("django.security.SuspiciousFileOperation")
|
||||
ignore_logger("django.security.SuspiciousMultipartForm")
|
||||
ignore_logger("django.security.SuspiciousSession")
|
||||
ignore_logger("django.security.TooManyFieldsSent")
|
||||
45
zproject/settings.py
Normal file
45
zproject/settings.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Django settings for zulip project.
|
||||
########################################################################
|
||||
# Here's how settings for the Zulip project work:
|
||||
#
|
||||
# * configured_settings.py imports default_settings.py, which contains
|
||||
# default values for settings configurable in prod_settings.py.
|
||||
#
|
||||
# * configured_settings.py imports prod_settings.py, and any site-specific
|
||||
# configuration belongs there. The template for prod_settings.py is
|
||||
# prod_settings_template.py.
|
||||
#
|
||||
# * computed_settings.py contains non-site-specific and settings
|
||||
# configuration for the Zulip Django app.
|
||||
#
|
||||
# See https://zulip.readthedocs.io/en/latest/subsystems/settings.html for more information
|
||||
#
|
||||
########################################################################
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# Patch datetime.fromisoformat for Python < 3.11
|
||||
if sys.version_info < (3, 11): # nocoverage
|
||||
from backports.datetime_fromisoformat import MonkeyPatch
|
||||
|
||||
MonkeyPatch.patch_fromisoformat()
|
||||
|
||||
# Monkey-patch certain types that are declared as generic types
|
||||
# generic in django-stubs, but not (yet) as generic types in Django
|
||||
# itself. This is necessary to ensure type references like
|
||||
# django.db.models.Lookup[int] work correctly at runtime.
|
||||
#
|
||||
# Hide this from mypy to avoid creating stupid cycles like
|
||||
# zproject.settings → django_stubs_ext → django_stubs_ext.patch →
|
||||
# django.contrib.admin.options → django.contrib.contenttypes.models →
|
||||
# confirmation.models → django.conf → zproject.settings.
|
||||
if not TYPE_CHECKING:
|
||||
import django_stubs_ext
|
||||
|
||||
django_stubs_ext.monkeypatch()
|
||||
|
||||
from .configured_settings import * # noqa: F403 isort: skip
|
||||
from .computed_settings import * # noqa: F403 isort: skip
|
||||
|
||||
# Do not add any code after these wildcard imports! Add it to
|
||||
# computed_settings instead.
|
||||
47
zproject/settings_types.py
Normal file
47
zproject/settings_types.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class JwtAuthKey(TypedDict):
|
||||
key: str
|
||||
# See https://pyjwt.readthedocs.io/en/latest/algorithms.html for a list
|
||||
# of supported algorithms.
|
||||
algorithms: list[str]
|
||||
|
||||
|
||||
class SAMLIdPConfigDict(TypedDict, total=False):
|
||||
entity_id: str
|
||||
url: str
|
||||
slo_url: str
|
||||
sp_initiated_logout_enabled: bool
|
||||
attr_user_permanent_id: str
|
||||
attr_first_name: str
|
||||
attr_last_name: str
|
||||
attr_username: str
|
||||
attr_email: str
|
||||
attr_org_membership: str
|
||||
auto_signup: bool
|
||||
display_name: str
|
||||
display_icon: str
|
||||
limit_to_subdomains: list[str]
|
||||
extra_attrs: list[str]
|
||||
x509cert: str
|
||||
x509cert_path: str
|
||||
|
||||
|
||||
class OIDCIdPConfigDict(TypedDict, total=False):
|
||||
oidc_url: str
|
||||
display_name: str
|
||||
display_icon: str | None
|
||||
client_id: str
|
||||
secret: str | None
|
||||
auto_signup: bool
|
||||
limit_to_subdomains: list[str]
|
||||
extra_attrs: list[str]
|
||||
|
||||
|
||||
class SCIMConfigDict(TypedDict, total=False):
|
||||
bearer_token: str
|
||||
scim_client_name: str
|
||||
name_formatted_included: bool
|
||||
create_guests_without_streams: bool
|
||||
custom_profile_field_map: dict[str, str]
|
||||
18
zproject/template_loaders.py
Normal file
18
zproject/template_loaders.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from pathlib import Path
|
||||
|
||||
from django.template.loaders import app_directories
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
class TwoFactorLoader(app_directories.Loader):
|
||||
@override
|
||||
def get_dirs(self) -> list[str | Path]:
|
||||
dirs = super().get_dirs()
|
||||
# app_directories.Loader returns only a list of
|
||||
# Path objects by calling get_app_template_dirs
|
||||
two_factor_dirs: list[str | Path] = []
|
||||
for d in dirs:
|
||||
assert isinstance(d, Path)
|
||||
if d.match("two_factor/*"):
|
||||
two_factor_dirs.append(d)
|
||||
return two_factor_dirs
|
||||
321
zproject/test_extra_settings.py
Normal file
321
zproject/test_extra_settings.py
Normal file
@@ -0,0 +1,321 @@
|
||||
import os
|
||||
|
||||
import ldap
|
||||
from django_auth_ldap.config import LDAPSearch
|
||||
|
||||
from zerver.lib.db import TimeTrackingConnection, TimeTrackingCursor
|
||||
from zerver.lib.types import AnalyticsDataUploadLevel
|
||||
from zproject.settings_types import OIDCIdPConfigDict, SAMLIdPConfigDict, SCIMConfigDict
|
||||
|
||||
from .config import DEPLOY_ROOT, get_from_file_if_exists
|
||||
from .settings import (
|
||||
AUTHENTICATION_BACKENDS,
|
||||
CACHES,
|
||||
DATABASES,
|
||||
EXTERNAL_HOST,
|
||||
LOCAL_DATABASE_PASSWORD,
|
||||
LOGGING,
|
||||
)
|
||||
|
||||
FULL_STACK_ZULIP_TEST = "FULL_STACK_ZULIP_TEST" in os.environ
|
||||
PUPPETEER_TESTS = "PUPPETEER_TESTS" in os.environ
|
||||
|
||||
|
||||
FAKE_EMAIL_DOMAIN = "zulip.testserver"
|
||||
|
||||
# Clear out the REALM_HOSTS set in dev_settings.py
|
||||
REALM_HOSTS: dict[str, str] = {}
|
||||
|
||||
DATABASES["default"] = {
|
||||
"NAME": os.getenv("ZULIP_DB_NAME", "zulip_test"),
|
||||
"USER": "zulip_test",
|
||||
"PASSWORD": LOCAL_DATABASE_PASSWORD,
|
||||
"HOST": "localhost",
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"TEST_NAME": "django_zulip_tests",
|
||||
"OPTIONS": {
|
||||
"connection_factory": TimeTrackingConnection,
|
||||
"cursor_factory": TimeTrackingCursor,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if FULL_STACK_ZULIP_TEST:
|
||||
TORNADO_PORTS = [9983]
|
||||
else:
|
||||
# Backend tests don't use tornado
|
||||
USING_TORNADO = False
|
||||
CAMO_URI = "https://external-content.zulipcdn.net/external_content/"
|
||||
CAMO_KEY = "dummy"
|
||||
|
||||
if "RUNNING_OPENAPI_CURL_TEST" in os.environ:
|
||||
RUNNING_OPENAPI_CURL_TEST = True
|
||||
|
||||
if "GENERATE_STRIPE_FIXTURES" in os.environ:
|
||||
GENERATE_STRIPE_FIXTURES = True
|
||||
|
||||
if "GENERATE_LLM_FIXTURES" in os.environ:
|
||||
GENERATE_LLM_FIXTURES = True
|
||||
|
||||
if "BAN_CONSOLE_OUTPUT" in os.environ:
|
||||
BAN_CONSOLE_OUTPUT = True
|
||||
|
||||
# Decrease the get_updates timeout to 1 second.
|
||||
# This allows frontend tests to proceed quickly to the next test step.
|
||||
EVENT_QUEUE_LONGPOLL_TIMEOUT_SECONDS = 1
|
||||
|
||||
# Stores the messages in `django.core.mail.outbox` rather than sending them.
|
||||
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||
|
||||
# The test suite uses EmailAuthBackend
|
||||
AUTHENTICATION_BACKENDS += ("zproject.backends.EmailAuthBackend",)
|
||||
|
||||
# Configure Google OAuth2
|
||||
GOOGLE_OAUTH2_CLIENT_ID = "test_client_id"
|
||||
|
||||
# Makes testing LDAP backend require less mocking
|
||||
AUTH_LDAP_ALWAYS_UPDATE_USER = False
|
||||
AUTH_LDAP_USER_SEARCH = LDAPSearch(
|
||||
"ou=users,dc=zulip,dc=com", ldap.SCOPE_ONELEVEL, "(uid=%(user)s)"
|
||||
)
|
||||
AUTH_LDAP_USERNAME_ATTR = "uid"
|
||||
AUTH_LDAP_REVERSE_EMAIL_SEARCH = LDAPSearch(
|
||||
"ou=users,dc=zulip,dc=com", ldap.SCOPE_ONELEVEL, "(mail=%(email)s)"
|
||||
)
|
||||
|
||||
RATE_LIMITING = False
|
||||
RATE_LIMITING_AUTHENTICATE = False
|
||||
# Don't use RabbitMQ from the test suite -- the user_profile_ids for
|
||||
# any generated queue elements won't match those being used by the
|
||||
# real app.
|
||||
USING_RABBITMQ = False
|
||||
|
||||
CACHES["database"] = {
|
||||
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
|
||||
"LOCATION": "zulip-database-test-cache",
|
||||
"TIMEOUT": 3600,
|
||||
"CONN_MAX_AGE": 600,
|
||||
"OPTIONS": {
|
||||
"MAX_ENTRIES": 100000,
|
||||
},
|
||||
}
|
||||
|
||||
# Disable caching on sessions to make query counts consistent
|
||||
SESSION_ENGINE = "django.contrib.sessions.backends.db"
|
||||
|
||||
# Use production config from Webpack in tests
|
||||
if PUPPETEER_TESTS:
|
||||
WEBPACK_STATS_FILE = os.path.join(DEPLOY_ROOT, "webpack-stats-production.json")
|
||||
else:
|
||||
WEBPACK_STATS_FILE = os.path.join(DEPLOY_ROOT, "var", "webpack-stats-test.json")
|
||||
WEBPACK_BUNDLES = "webpack-bundles/"
|
||||
|
||||
if not PUPPETEER_TESTS:
|
||||
# Use local memory cache for backend tests.
|
||||
CACHES["default"] = {
|
||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
||||
}
|
||||
|
||||
# Here we set various loggers to be less noisy for unit tests.
|
||||
def set_loglevel(logger_name: str, level: str) -> None:
|
||||
LOGGING["loggers"].setdefault(logger_name, {})["level"] = level
|
||||
|
||||
set_loglevel("zulip.requests", "CRITICAL")
|
||||
set_loglevel("zulip.management", "CRITICAL")
|
||||
set_loglevel("zulip.auth", "WARNING")
|
||||
set_loglevel("django.request", "ERROR")
|
||||
set_loglevel("django_auth_ldap", "WARNING")
|
||||
set_loglevel("fakeldap", "ERROR")
|
||||
set_loglevel("zulip.send_email", "ERROR")
|
||||
set_loglevel("zerver.lib.push_notifications", "WARNING")
|
||||
set_loglevel("zerver.lib.digest", "ERROR")
|
||||
set_loglevel("zerver.lib.email_mirror", "ERROR")
|
||||
set_loglevel("zerver.worker", "WARNING")
|
||||
set_loglevel("stripe", "WARNING")
|
||||
|
||||
# This is set dynamically in `zerver/lib/test_runner.py`.
|
||||
# Allow setting LOCAL_UPLOADS_DIR in the environment so that the
|
||||
# frontend/API tests in test_server.py can control this.
|
||||
if "LOCAL_UPLOADS_DIR" in os.environ:
|
||||
LOCAL_UPLOADS_DIR = os.getenv("LOCAL_UPLOADS_DIR")
|
||||
assert LOCAL_UPLOADS_DIR is not None
|
||||
LOCAL_AVATARS_DIR = os.path.join(LOCAL_UPLOADS_DIR, "avatars")
|
||||
LOCAL_FILES_DIR = os.path.join(LOCAL_UPLOADS_DIR, "files")
|
||||
# Otherwise, we use the default value from dev_settings.py
|
||||
|
||||
S3_KEY = "test-key"
|
||||
S3_SECRET_KEY = "test-secret-key"
|
||||
S3_AUTH_UPLOADS_BUCKET = "test-authed-bucket"
|
||||
S3_AVATAR_BUCKET = "test-avatar-bucket"
|
||||
S3_EXPORT_BUCKET = "test-export-bucket"
|
||||
|
||||
INLINE_URL_EMBED_PREVIEW = False
|
||||
|
||||
HOME_NOT_LOGGED_IN = "/login/"
|
||||
LOGIN_URL = "/accounts/login/"
|
||||
|
||||
# If dev_settings.py found a key or cert file to use here, ignore it.
|
||||
APNS_TOKEN_KEY_FILE: str | None = None
|
||||
APNS_CERT_FILE: str | None = None
|
||||
|
||||
# By default will not send emails when login occurs.
|
||||
# Explicitly set this to True within tests that must have this on.
|
||||
SEND_LOGIN_EMAILS = False
|
||||
|
||||
GOOGLE_OAUTH2_CLIENT_ID = "id"
|
||||
GOOGLE_OAUTH2_CLIENT_SECRET = "secret"
|
||||
|
||||
SOCIAL_AUTH_GITHUB_KEY = "key"
|
||||
SOCIAL_AUTH_GITHUB_SECRET = "secret"
|
||||
SOCIAL_AUTH_GITLAB_KEY = "key"
|
||||
SOCIAL_AUTH_GITLAB_SECRET = "secret"
|
||||
SOCIAL_AUTH_GOOGLE_KEY = "key"
|
||||
SOCIAL_AUTH_GOOGLE_SECRET = "secret"
|
||||
SOCIAL_AUTH_SUBDOMAIN = "auth"
|
||||
SOCIAL_AUTH_APPLE_SERVICES_ID = "com.zulip.chat"
|
||||
SOCIAL_AUTH_APPLE_APP_ID = "com.zulip.bundle.id"
|
||||
SOCIAL_AUTH_APPLE_CLIENT = "com.zulip.chat"
|
||||
SOCIAL_AUTH_APPLE_AUDIENCE = [SOCIAL_AUTH_APPLE_APP_ID, SOCIAL_AUTH_APPLE_SERVICES_ID]
|
||||
SOCIAL_AUTH_APPLE_KEY = "KEYISKEY"
|
||||
SOCIAL_AUTH_APPLE_TEAM = "TEAMSTRING"
|
||||
SOCIAL_AUTH_APPLE_SECRET = get_from_file_if_exists("zerver/tests/fixtures/apple/private_key.pem")
|
||||
SOCIAL_AUTH_DISCORD_KEY = "key"
|
||||
SOCIAL_AUTH_DISCORD_SECRET = "secret"
|
||||
|
||||
|
||||
SOCIAL_AUTH_OIDC_ENABLED_IDPS: dict[str, OIDCIdPConfigDict] = {
|
||||
"testoidc": {
|
||||
"display_name": "Test OIDC",
|
||||
"oidc_url": "https://example.com/api/openid",
|
||||
"display_icon": None,
|
||||
"client_id": "key",
|
||||
"secret": "secret",
|
||||
}
|
||||
}
|
||||
SOCIAL_AUTH_OIDC_FULL_NAME_VALIDATED = True
|
||||
|
||||
|
||||
VIDEO_ZOOM_SERVER_TO_SERVER_ACCOUNT_ID = "account_id"
|
||||
VIDEO_ZOOM_API_URL = "https://api.zoom.us"
|
||||
VIDEO_ZOOM_OAUTH_URL = "https://zoom.example.com"
|
||||
VIDEO_ZOOM_CLIENT_ID = "client_id"
|
||||
VIDEO_ZOOM_CLIENT_SECRET = "client_secret"
|
||||
|
||||
VIDEO_WEBEX_CLIENT_ID = "client_id"
|
||||
VIDEO_WEBEX_CLIENT_SECRET = "client_secret"
|
||||
|
||||
BIG_BLUE_BUTTON_SECRET = "123"
|
||||
BIG_BLUE_BUTTON_URL = "https://bbb.example.com/bigbluebutton/"
|
||||
|
||||
CONSTRUCTOR_GROUPS_URL = "https://example.constructor.app/api/groups/xapi"
|
||||
CONSTRUCTOR_GROUPS_ACCESS_KEY = "test-access-key"
|
||||
CONSTRUCTOR_GROUPS_SECRET_KEY = "test-secret-key"
|
||||
|
||||
NEXTCLOUD_SERVER = "https://nextcloud.example.com"
|
||||
NEXTCLOUD_TALK_USERNAME = "username"
|
||||
NEXTCLOUD_TALK_PASSWORD = "password"
|
||||
|
||||
# By default two factor authentication is disabled in tests.
|
||||
# Explicitly set this to True within tests that must have this on.
|
||||
TWO_FACTOR_AUTHENTICATION_ENABLED = False
|
||||
|
||||
DEVELOPMENT_DISABLE_PUSH_BOUNCER_DOMAIN_CHECK = False
|
||||
ZULIP_SERVICES_URL = f"http://push.{EXTERNAL_HOST}"
|
||||
|
||||
# Disable all Zulip services by default. Tests can activate them by
|
||||
# overriding settings explicitly when they want to enable something,
|
||||
# often using activate_push_notification_service.
|
||||
ZULIP_SERVICE_PUSH_NOTIFICATIONS = False
|
||||
ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS = False
|
||||
ZULIP_SERVICE_SECURITY_ALERTS = False
|
||||
|
||||
# Hack: This should be computed in computed_settings, but the transmission
|
||||
# of test settings overrides is wonky. See test_settings for more details.
|
||||
ANALYTICS_DATA_UPLOAD_LEVEL = AnalyticsDataUploadLevel.NONE
|
||||
|
||||
# The most common value used by tests. Set it as the default so that it doesn't
|
||||
# have to be repeated every time.
|
||||
ZULIP_SERVICES_URL = "https://push.zulip.org.example.com"
|
||||
|
||||
# Logging the emails while running the tests adds them
|
||||
# to /emails page.
|
||||
DEVELOPMENT_LOG_EMAILS = False
|
||||
|
||||
SOCIAL_AUTH_SAML_SP_ENTITY_ID = "http://" + EXTERNAL_HOST
|
||||
SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = get_from_file_if_exists("zerver/tests/fixtures/saml/zulip.crt")
|
||||
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = get_from_file_if_exists("zerver/tests/fixtures/saml/zulip.key")
|
||||
|
||||
SOCIAL_AUTH_SAML_ORG_INFO = {
|
||||
"en-US": {
|
||||
"name": "example",
|
||||
"displayname": "Example Inc.",
|
||||
"url": "{}{}".format("http://", EXTERNAL_HOST),
|
||||
},
|
||||
}
|
||||
|
||||
SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = {
|
||||
"givenName": "Tech Gal",
|
||||
"emailAddress": "technical@example.com",
|
||||
}
|
||||
|
||||
SOCIAL_AUTH_SAML_SUPPORT_CONTACT = {
|
||||
"givenName": "Support Guy",
|
||||
"emailAddress": "support@example.com",
|
||||
}
|
||||
|
||||
SOCIAL_AUTH_SAML_ENABLED_IDPS: dict[str, SAMLIdPConfigDict] = {
|
||||
"test_idp": {
|
||||
"entity_id": "https://idp.testshib.org/idp/shibboleth",
|
||||
"url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO",
|
||||
"slo_url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/Logout",
|
||||
"sp_initiated_logout_enabled": True,
|
||||
"x509cert": get_from_file_if_exists("zerver/tests/fixtures/saml/idp.crt"),
|
||||
"attr_user_permanent_id": "email",
|
||||
"attr_first_name": "first_name",
|
||||
"attr_last_name": "last_name",
|
||||
"attr_username": "email",
|
||||
"attr_email": "email",
|
||||
"display_name": "Test IdP",
|
||||
},
|
||||
}
|
||||
|
||||
RATE_LIMITING_RULES: dict[str, list[tuple[int, int]]] = {
|
||||
"api_by_user": [],
|
||||
"api_by_ip": [],
|
||||
"api_by_remote_server": [],
|
||||
"authenticate_by_username": [],
|
||||
"sends_email_by_ip": [],
|
||||
"email_change_by_user": [],
|
||||
"password_reset_form_by_email": [],
|
||||
"sends_email_by_remote_server": [],
|
||||
"transfer_remote_server_registration_endpoint_by_ip": [],
|
||||
}
|
||||
|
||||
CLOUD_FREE_TRIAL_DAYS: int | None = None
|
||||
SELF_HOSTING_FREE_TRIAL_DAYS: int | None = None
|
||||
|
||||
SCIM_CONFIG: dict[str, SCIMConfigDict] = {
|
||||
"zulip": {
|
||||
"bearer_token": "token1234",
|
||||
"scim_client_name": "test-scim-client",
|
||||
"name_formatted_included": True,
|
||||
}
|
||||
}
|
||||
|
||||
# This override disables the grace period for undoing resolving/unresolving
|
||||
# a topic in tests.
|
||||
# This allows tests to not worry about the special behavior during the grace period.
|
||||
# Otherwise they would have to do lots of mocking of the timer to work around this.
|
||||
RESOLVE_TOPIC_UNDO_GRACE_PERIOD_SECONDS = 0
|
||||
|
||||
KATEX_SERVER = False
|
||||
|
||||
ROOT_DOMAIN_LANDING_PAGE = False
|
||||
|
||||
# Disable verifying webhook signatures in tests by default.
|
||||
# Tests that intend to verify webhook signatures should override this setting.
|
||||
VERIFY_WEBHOOK_SIGNATURES = False
|
||||
|
||||
AUTH_LDAP_USER_ATTR_MAP = {
|
||||
"full_name": "cn",
|
||||
}
|
||||
25
zproject/test_settings.py
Normal file
25
zproject/test_settings.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import os
|
||||
|
||||
# test_settings.py works differently from
|
||||
# dev_settings.py/prod_settings.py; it actually is directly referenced
|
||||
# by the test suite as DJANGO_SETTINGS_MODULE and imports settings.py
|
||||
# directly and then hacks up the values that are different for the
|
||||
# test suite. As will be explained, this is kinda messy and probably
|
||||
# we'd be better off switching it to work more like dev_settings.py,
|
||||
# but for now, this is what we have.
|
||||
#
|
||||
# An important downside of the test_settings.py approach is that if we
|
||||
# want to change any settings that settings.py then computes
|
||||
# additional settings from (e.g. EXTERNAL_HOST), we need to do a hack
|
||||
# like the below line(s) before we import from settings, for
|
||||
# transmitting the value of EXTERNAL_HOST to dev_settings.py so that
|
||||
# it can be set there, at the right place in the settings.py flow.
|
||||
# Ick.
|
||||
os.environ["EXTERNAL_HOST"] = os.getenv("TEST_EXTERNAL_HOST", "testserver")
|
||||
os.environ["ZULIP_TEST_SUITE"] = "true"
|
||||
|
||||
from .settings import * # noqa: F403 isort: skip
|
||||
from .test_extra_settings import * # noqa: F403 isort: skip
|
||||
|
||||
# Do not add any code after these wildcard imports! Add it to
|
||||
# test_extra_settings instead.
|
||||
37
zproject/tornado_urls.py
Normal file
37
zproject/tornado_urls.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
|
||||
from zerver.lib.rest import rest_path
|
||||
from zerver.tornado.views import (
|
||||
cleanup_event_queue,
|
||||
get_events,
|
||||
get_events_internal,
|
||||
notify,
|
||||
web_reload_clients,
|
||||
)
|
||||
|
||||
# Minimal URL configuration for Tornado. Tornado only serves 5
|
||||
# endpoints, but without a dedicated urlconf it resolves every request
|
||||
# against the full Django URL configuration (~800 patterns), wasting
|
||||
# significant CPU on regex matching.
|
||||
|
||||
api_and_json_patterns = [
|
||||
rest_path(
|
||||
"events",
|
||||
GET=(get_events, {"narrow_user_session_cache"}),
|
||||
DELETE=(cleanup_event_queue, {"narrow_user_session_cache"}),
|
||||
),
|
||||
]
|
||||
|
||||
# Tornado serves only API endpoints, and this configuration lacks the
|
||||
# URL names that Zulip's HTML error pages reverse.
|
||||
handler404 = "zerver.tornado.views.json_not_found"
|
||||
handler500 = "zerver.tornado.views.json_internal_server_error"
|
||||
|
||||
urlpatterns = [
|
||||
path("api/v1/", include(api_and_json_patterns)),
|
||||
path("json/", include(api_and_json_patterns)),
|
||||
path("api/internal/notify_tornado", notify),
|
||||
path("api/internal/web_reload_clients", web_reload_clients),
|
||||
path("api/v1/events/internal", get_events_internal),
|
||||
]
|
||||
1030
zproject/urls.py
Normal file
1030
zproject/urls.py
Normal file
File diff suppressed because it is too large
Load Diff
109
zproject/wsgi.py
Normal file
109
zproject/wsgi.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
WSGI config for zulip project.
|
||||
|
||||
This module contains the WSGI application used by Django's development server
|
||||
and any production WSGI deployments. It should expose a module-level variable
|
||||
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
|
||||
this application via the ``WSGI_APPLICATION`` setting.
|
||||
|
||||
Usually you will have the standard Django WSGI application here, but it also
|
||||
might make sense to replace the whole Django WSGI application with a custom one
|
||||
that later delegates to the Django one. For example, you could introduce WSGI
|
||||
middleware here, or combine a Django application with an application of another
|
||||
framework.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.append(BASE_DIR)
|
||||
from scripts.lib.setup_path import setup_path
|
||||
|
||||
setup_path()
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zproject.settings")
|
||||
|
||||
import contextlib
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
try:
|
||||
# This application object is used by any WSGI server configured to use this
|
||||
# file. This includes Django's development server, if the WSGI_APPLICATION
|
||||
# setting points here.
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
||||
# We force loading of the main parts of the application now, by
|
||||
# handing it a fake request, rather than have to pay that price
|
||||
# during the first request served by this process. Hitting the
|
||||
# /health endpoint will not only load Django and all of the views
|
||||
# (by loading the URL dispatcher) but will also force open any
|
||||
# lazy-loaded service connections.
|
||||
#
|
||||
# The return value (and thus response status) of this healthcheck
|
||||
# request is ignored, so we do return the application handler even
|
||||
# if connections are not fully available yet. This at least
|
||||
# allows application logging to handle any such errors, instead of
|
||||
# arcane errors from uwsgi not being able to load its handler
|
||||
# function.
|
||||
def ignored_start_response(
|
||||
status: str, headers: list[tuple[str, str]], exc_info: Any = None, /
|
||||
) -> Callable[[bytes], object]:
|
||||
return lambda x: None
|
||||
|
||||
application(
|
||||
{
|
||||
"REQUEST_METHOD": "GET",
|
||||
"SERVER_NAME": "127.0.0.1",
|
||||
"SERVER_PORT": "443",
|
||||
"PATH_INFO": "/health",
|
||||
"REMOTE_ADDR": "127.0.0.1",
|
||||
"wsgi.input": sys.stdin,
|
||||
"wsgi.url_scheme": "https",
|
||||
},
|
||||
ignored_start_response,
|
||||
)
|
||||
|
||||
with contextlib.suppress(ModuleNotFoundError):
|
||||
# The uwsgi module is only importable when running under
|
||||
# uwsgi; development uses this file as well, but inside a
|
||||
# pure-Python server. The surrounding contextmanager ensures
|
||||
# that we don't bother with these steps if we're in
|
||||
# development.
|
||||
import uwsgi
|
||||
|
||||
if uwsgi.worker_id() == uwsgi.numproc:
|
||||
# This is the last worker to load in the chain reload
|
||||
with open("/var/lib/zulip/django-workers.ready", "wb") as f:
|
||||
# The contents of this file are not read by restart-server
|
||||
# in any way, but leave some useful information about the
|
||||
# state of uwsgi.
|
||||
f.write(
|
||||
orjson.dumps(
|
||||
uwsgi.workers(), option=orjson.OPT_INDENT_2, default=lambda e: e.decode()
|
||||
),
|
||||
)
|
||||
|
||||
except Exception:
|
||||
# If /etc/zulip/settings.py contains invalid syntax, Django
|
||||
# initialization will fail in django.setup(). In this case, our
|
||||
# normal configuration to logs errors to /var/log/zulip/errors.log
|
||||
# won't have been initialized. Since it's really valuable for the
|
||||
# debugging process for a Zulip 500 error to always be "check
|
||||
# /var/log/zulip/errors.log", we log to that file directly here.
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
filename="/var/log/zulip/errors.log",
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.exception("get_wsgi_application() failed:")
|
||||
raise
|
||||
Reference in New Issue
Block a user