""" Django settings for labs project. Generated by 'django-admin startproject' using Django 5.1.6. For more information on this file, see https://docs.djangoproject.com/en/5.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.1/ref/settings/ """ import sys import logging import ipaddress from pathlib import Path import sentry_sdk from environs import Env from celery.schedules import crontab from labs.utils.env import load_env_from_aws_secrets_manager # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent env = Env() env.read_env(recurse=False) if env.bool("LOAD_ENV_FROM_AWS_SECRETS_MANAGER", default=False): load_env_from_aws_secrets_manager( secret_name=env.str( "AWS_SECRETS_MANAGER_SECRET_NAME", default="prod/labs.tidb.io/backend" ), region_name=env.str("AWS_REGION", default="us-west-2"), override=True, suppress_aws_auth_error=False, ) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = env.str( "SECRET_KEY", default="django-insecure-ka531-sr2(-usojbsitbui9ze=ncm2k)595#b(1zo#cm2e&zk%", ) SECRET_KEY_FALLBACKS = env.list("SECRET_KEY_FALLBACKS", default=[]) # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env.bool("DEBUG", default=True) ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[]) # Application definition INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", "social_django", "inertia", "django_vite", "labs", "labs.admin", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "labs.middlewares.locale.LocaleMiddleware", "labs.middlewares.social.SocialAuthExceptionMiddleware", "labs.admin.inertia.InertiaSharedPropsMiddleware", "inertia.middleware.InertiaMiddleware", ] ROOT_URLCONF = "labs.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], }, }, ] WSGI_APPLICATION = "labs.wsgi.application" # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases DATABASES = { "default": { "ENGINE": "django_tidb", "NAME": env.str("DB_NAME", "lab"), "USER": env.str("DB_USER", "root"), "PASSWORD": env.str("DB_PASSWORD", ""), "HOST": env.str("DB_HOST", "127.0.0.1"), "PORT": env.int("DB_PORT", default=4000), "CONN_HEALTH_CHECKS": True, "OPTIONS": { "charset": "utf8mb4", }, }, } if env.bool("DB_ENABLE_SSL", default=True): DATABASES["default"]["OPTIONS"]["ssl_mode"] = "VERIFY_IDENTITY" DATABASES["default"]["OPTIONS"]["ssl"] = { "ca": env.str("CA_CERT_PATH", "/etc/ssl/certs/ca-certificates.crt"), } # Database field encryption # It is used to encrypt specific fields in the database, so that some sensitive data will not be stored in the database in plain text. # DB_FILED_ENCRYPTION_SALT should be different than the SECRET_KEY, it requires a random string of 32 bytes # DO NOT CHANGE DB_FILED_ENCRYPTION_SALT AFTER DEPLOYMENT, or you will not be able to decrypt the data in the database. DATABASE_FIELD_ENCRYPTION_SALT = env.str( "DATABASE_FIELD_ENCRYPTION_SALT", default="change-me" ) DATABASE_FIELD_ENCRYPTION_SECRET_KEY = SECRET_KEY DATABASE_FIELD_ENCRYPTION_FALLBACK_SECRET_KEYS = SECRET_KEY_FALLBACKS # Caches CACHES = { "default": { "BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": env.str("REDIS_URL", default="redis://localhost:6379/1"), } } if env.bool("USE_PROXY_X_FORWARDED", default=False): SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") USE_X_FORWARDED_HOST = True # Email configuration EMAIL_BACKEND = env.str("EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend") EMAIL_HOST = env.str("EMAIL_HOST", default="email-smtp.us-west-2.amazonaws.com") EMAIL_PORT = env.int("EMAIL_PORT", default=587) EMAIL_HOST_USER = env.str("EMAIL_HOST_USER", default="") EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD", default="") EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", default=True) DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL", default="TiDB Labs ") # Celery configuration CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", default="redis://localhost:6379/0") CELERY_RESULT_BACKEND = env.str( "CELERY_RESULT_BACKEND", default="redis://localhost:6379/0" ) CELERY_TASK_ACKS_LATE = True CELERY_TASK_REJECT_ON_WORKER_LOST = True CELERY_BROKER_TRANSPORT_OPTIONS = { "socket_timeout": 5, "socket_connect_timeout": 5, "socket_keepalive": True, "health_check_interval": 20, "retry_on_timeout": True, } CELERY_REDBEAT_REDIS_URL = env.str( "CELERY_REDBEAT_REDIS_URL", default="redis://localhost:6379/0" ) CELERY_BEAT_SCHEDULE = { "recycle-timeout-lab-instances-every-mins": { "task": "labs.tasks.lab_v2.recycle_timeout_lab_instances", "schedule": crontab(minute="*"), "args": (), }, "recycle-stale-lab-instances-every-day": { "task": "labs.tasks.lab_v2.recycle_stale_lab_instances", "schedule": crontab(hour=0, minute=0), "args": (), }, "aws-cloudformation-housekeeping-every-10-mins": { "task": "labs.tasks.cloud_provider.aws.aws_cloudformation_housekeeping", "schedule": crontab(minute="*/10"), "args": (), }, "alicloud-ros-housekeeping-every-10-mins": { "task": "labs.tasks.cloud_provider.alicloud.alicloud_ros_housekeeping", "schedule": crontab(minute="*/10"), "args": (), }, "ssh-servers-health-check-every-minute-v1": { "task": "labs.tasks.cronjob.ssh_server.ssh_servers_health_check", "schedule": crontab(), "args": (), }, "sync-last-3-days-ga-events-every-day-v1": { "task": "labs.tasks.cronjob.google_analytics.sync_last_3_days_ga_events", "schedule": crontab(hour=1, minute=10), "args": (), }, "sync-last-3-days-ga-events-intraday-every-3-hours-v1": { "task": "labs.tasks.cronjob.google_analytics.sync_last_3_days_ga_events_intraday", "schedule": crontab(hour="*/3", minute=0), "args": (), }, "ensure-tidbcloud-oauth-grants-revoked-after-lab-every-hour-v1": { "task": "labs.tasks.cronjob.tidbcloud_oauth.ensure_tidbcloud_oauth_grants_revoked_after_lab", "schedule": crontab(hour="*", minute="20"), "args": (), }, "backup-database-every-day": { "task": "labs.tasks.cronjob.db_backup.backup_database", "schedule": crontab(hour="2", minute=10), "args": (), }, "auto-submit-expired-exam-attempts-every-minute": { "task": "labs.tasks.exam.auto_submit_expired_exam_attempts", "schedule": crontab(minute="*"), "args": (), }, } # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] AUTH_USER_MODEL = "labs.User" AUTHENTICATION_BACKENDS = [ "social_core.backends.google.GoogleOAuth2", "labs.social.tidbcloud.TiDBCloudOAuth2", ] SOCIAL_AUTH_PIPELINE = ( "social_core.pipeline.social_auth.social_details", "social_core.pipeline.social_auth.social_uid", "social_core.pipeline.social_auth.social_user", "social_core.pipeline.social_auth.associate_by_email", "social_core.pipeline.user.create_user", "social_core.pipeline.social_auth.associate_user", "social_core.pipeline.social_auth.load_extra_data", "social_core.pipeline.user.user_details", ) SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = env.str("GOOGLE_OAUTH2_KEY", default="change-me") SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = env.str("GOOGLE_OAUTH2_SECRET", default="change-me") SOCIAL_AUTH_TIDBCLOUD_KEY = env.str("TIDBCLOUD_OAUTH2_KEY", default="change-me") SOCIAL_AUTH_TIDBCLOUD_SECRET = env.str("TIDBCLOUD_OAUTH2_SECRET", default="change-me") SESSION_COOKIE_AGE = 60 * 60 * 24 * 90 # 90 days LOGIN_URL = "/login" LOGIN_REDIRECT_URL = "/" CORS_URLS_REGEX = r"^/api/.*$" CORS_ALLOWED_ORIGINS = env.list("CORS_ALLOWED_ORIGINS", default=[]) CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[]) # Internationalization # https://docs.djangoproject.com/en/5.1/topics/i18n/ LANGUAGE_CODE = "en" LANGUAGE_COOKIE_NAME = "x-language" LANGUAGES = [ ("en", "English"), ("ja", "Japanese"), ("zh", "Simplified Chinese"), ("ko", "Korean"), ] TIME_ZONE = "UTC" USE_I18N = True USE_TZ = True # Inertia configuration ADMIN_APP_DIR = BASE_DIR / "labs" / "admin" INERTIA_LAYOUT = "labs_admin/base.html" DJANGO_VITE = { "default": { "manifest_path": ADMIN_APP_DIR / "frontend" / "dist" / ".vite" / "manifest.json", "dev_mode": DEBUG, "dev_server_protocol": "http", "dev_server_host": "localhost", "dev_server_port": 5173, # dev: avoid /static/ prefix (../ cancels /static/ in urljoin); prod uses manifest "static_url_prefix": "/admin/static/" if DEBUG else "", } } # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ STATIC_URL = "/admin/static/" STATIC_ROOT = env.str("STATIC_ROOT", default="staticfiles") STATICFILES_DIRS = [ ADMIN_APP_DIR / "frontend" / "dist", ] # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" TEST_RUNNER = "labs.tests.runner.UnManagedModelTestRunner" CERTIFICATE_STORAGE_PROVIDER = env.str("CERTIFICATE_STORAGE_PROVIDER", default="aliyun") TIDB_CLOUD_PRIVATE_KEY = env.str("TIDB_CLOUD_PRIVATE_KEY", default="") TIDB_CLOUD_PUBLIC_KEY = env.str("TIDB_CLOUD_PUBLIC_KEY", default="") TLMS_API_KEY = env.str("TLMS_API_KEY", default="") ALIYUN_CDN_URL = env.str("ALIYUN_CDN_URL", default="https://lab-static.pingcap.com") ALIYUN_OSS_BUCKET = env.str("ALIYUN_OSS_BUCKET", default="") ALIYUN_OSS_REGION = env.str("ALIYUN_OSS_REGION", default="us-west-1") ALIYUN_OSS_ACCESS_KEY_ID = env.str("ALIYUN_OSS_ACCESS_KEY_ID", default="") ALIYUN_OSS_ACCESS_KEY_SECRET = env.str("ALIYUN_OSS_ACCESS_KEY_SECRET", default="") ALICLOUD_ROS_ACCESS_KEY_ID = env.str("ALICLOUD_ROS_ACCESS_KEY_ID", default="") ALICLOUD_ROS_ACCESS_KEY_SECRET = env.str("ALICLOUD_ROS_ACCESS_KEY_SECRET", default="") # Web SSH JWT Keys for Signing WebSocket URL (Ed25519 Algorithm) WEB_SSH_JWT_PUBLIC_KEY = env.str("WEB_SSH_JWT_PUBLIC_KEY", default="") WEB_SSH_JWT_PRIVATE_KEY = env.str("WEB_SSH_JWT_PRIVATE_KEY", default="") WEB_SSH_JWT_SIGN_KEY = env.str("WEB_SSH_JWT_SIGN_KEY", default="") WEB_SSH_URL_PREFIX = env.str("WEB_SSH_URL_PREFIX", default="wss://ssh.labs.tidb.io") # Billing STRIPE_API_KEY = env.str("STRIPE_API_KEY", default="") STRIPE_WEBHOOK_SECRET = env.str("STRIPE_WEBHOOK_SECRET", default="") # Can be publicly accessible in web client-side code to securely collect payment information, STRIPE_PUBLIC_KEY = env.str("STRIPE_PUBLIC_KEY", default="") GA_GOOGLE_CREDENTIALS_JSON = env.str("GA_GOOGLE_CREDENTIALS_JSON", default="") SENTRY_DSN = env.str("SENTRY_DSN", default="") if SENTRY_DSN: sentry_sdk.init( dsn=SENTRY_DSN, # Add data like request headers and IP for users; # see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info send_default_pii=True, # Set traces_sample_rate to 1.0 to capture 100% # of transactions for tracing. traces_sample_rate=0.1, # Set profiles_sample_rate to 1.0 to profile 100% # of sampled transactions. # We recommend adjusting this value in production. profiles_sample_rate=0.1, ) REST_FRAMEWORK = { "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), "DEFAULT_PERMISSION_CLASSES": ("labs.api.permissions.NoPermission",), "DEFAULT_AUTHENTICATION_CLASSES": ( "labs.api.authentication.SessionAuthentication", "labs.api.authentication.APIKeyAuthentication", ), "TEST_REQUEST_DEFAULT_FORMAT": "json", "DEFAULT_THROTTLE_CLASSES": ( "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ), "DEFAULT_THROTTLE_RATES": {"anon": "100/m", "user": "150/m", "admin": "200/m"}, } LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "verbose": { "format": "[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s", }, }, "handlers": { "console_stdout": { "class": "logging.StreamHandler", "stream": sys.stdout, "formatter": "verbose", "level": "DEBUG", "filters": ["below_error"], }, "console_stderr": { "class": "logging.StreamHandler", "stream": sys.stderr, "formatter": "verbose", "level": "ERROR", }, }, "filters": { "below_error": { "()": "django.utils.log.CallbackFilter", "callback": lambda record: record.levelno < logging.ERROR, }, }, "loggers": { "": { "handlers": ["console_stdout", "console_stderr"], "level": "DEBUG" if DEBUG else "INFO", "propagate": False, }, "django": {"handlers": [], "propagate": True}, # Django logs 4xx errors as warnings; suppress these here. "django.request": {"handlers": [], "level": "ERROR", "propagate": True}, "django.server": {"handlers": [], "propagate": True}, }, } PROMETHEUS_TRUSTED_IPS = env.list("PROMETHEUS_TRUSTED_IPS", default=[]) # The number of proxy servers between the client and the server. # Used to get the real client IP address. PROXY_COUNT = env.int("PROXY_COUNT", default=0) COMPANY_VPN_CIDRS = [ ipaddress.ip_network(cidr, strict=False) for cidr in env.list("COMPANY_VPN_CIDRS", default=[]) ] # Admin OTP Sudo Configuration # These settings control the TOTP-based sudo authentication for admin access. # Duration in seconds for how long a sudo session remains valid after OTP verification. ADMIN_SUDO_SESSION_DURATION = env.int( "ADMIN_SUDO_SESSION_DURATION", default=3 * 60 * 60 ) # 3 hours # Maximum failed OTP attempts before rate limiting kicks in. ADMIN_OTP_MAX_ATTEMPTS = env.int("ADMIN_OTP_MAX_ATTEMPTS", default=5) # Duration in seconds for the rate limit lockout after max attempts exceeded. ADMIN_OTP_LOCKOUT_DURATION = env.int( "ADMIN_OTP_LOCKOUT_DURATION", default=15 * 60 ) # 15 minutes # Issuer name shown in authenticator apps. ADMIN_OTP_ISSUER_NAME = env.str("ADMIN_OTP_ISSUER_NAME", default="TiDB Labs Admin") # TOTP time step in seconds (RFC 6238 default is 30 seconds). # Used for TOTP generation/verification and replay attack prevention. ADMIN_OTP_TOTP_PERIOD = env.int("ADMIN_OTP_TOTP_PERIOD", default=30)