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

This commit is contained in:
upstream-mirror
2026-09-15 15:05:30 +05:00
commit 64898b2c79
8999 changed files with 1757358 additions and 0 deletions

0
zproject/__init__.py Normal file
View File

4540
zproject/backends.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

67
zproject/config.py Normal file
View 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 ""

View 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.

View 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
View 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
View 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
View 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()

View 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

View File

@@ -0,0 +1 @@
from .prod_settings_template import * # noqa: F403

View 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
View 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
View 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.

View 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]

View 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

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

109
zproject/wsgi.py Normal file
View 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