Coverage for config.py: 98%

84 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-02 00:07 +0000

1""" 

2Configuration overrides for settings.py 

3""" 

4 

5import os 

6import sys 

7from django.urls import reverse_lazy 

8from django.utils.translation import gettext_lazy as _ 

9from django.contrib.messages import constants as message_constants 

10from ivatar.settings import BASE_DIR 

11 

12from ivatar.settings import MIDDLEWARE 

13from ivatar.settings import INSTALLED_APPS 

14from ivatar.settings import TEMPLATES 

15 

16ADMIN_USERS = [] 

17ALLOWED_HOSTS = ["*"] 

18 

19INSTALLED_APPS.extend( 

20 [ 

21 "django_extensions", 

22 "django_openid_auth", 

23 "bootstrap4", 

24 "anymail", 

25 "ivatar", 

26 "ivatar.ivataraccount", 

27 "ivatar.tools", 

28 ] 

29) 

30 

31MIDDLEWARE.extend( 

32 [ 

33 "ivatar.middleware.CustomLocaleMiddleware", 

34 ] 

35) 

36 

37# Add OpenTelemetry middleware only if feature flag is enabled 

38# Note: This will be checked at runtime, not at import time 

39MIDDLEWARE.insert( 

40 0, 

41 "ivatar.middleware.MultipleProxyMiddleware", 

42) 

43 

44AUTHENTICATION_BACKENDS = ( 

45 # Enable this to allow LDAP authentication. 

46 # See INSTALL for more information. 

47 # 'django_auth_ldap.backend.LDAPBackend', 

48 "django_openid_auth.auth.OpenIDBackend", 

49 "ivatar.ivataraccount.auth.FedoraOpenIdConnect", 

50 "django.contrib.auth.backends.ModelBackend", 

51) 

52 

53TEMPLATES[0]["DIRS"].extend( 

54 [ 

55 os.path.join(BASE_DIR, "templates"), 

56 ] 

57) 

58TEMPLATES[0]["OPTIONS"]["context_processors"].append( 

59 "ivatar.context_processors.basepage", 

60) 

61 

62OPENID_CREATE_USERS = True 

63OPENID_UPDATE_DETAILS_FROM_SREG = True 

64SOCIAL_AUTH_JSONFIELD_ENABLED = True 

65# Fedora authentication (OIDC). You need to set these two values to use it. 

66SOCIAL_AUTH_FEDORA_KEY = None # Also known as client_id 

67SOCIAL_AUTH_FEDORA_SECRET = None # Also known as client_secret 

68 

69SITE_NAME = os.environ.get("SITE_NAME", "libravatar") 

70IVATAR_VERSION = "1.8.0" 

71 

72SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2" 

73 

74SECURE_BASE_URL = os.environ.get( 

75 "SECURE_BASE_URL", "https://avatars.linux-kernel.at/avatar/" 

76) 

77BASE_URL = os.environ.get("BASE_URL", "http://avatars.linux-kernel.at/avatar/") 

78 

79LOGIN_REDIRECT_URL = reverse_lazy("profile") 

80MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294 

81 

82MAX_NUM_PHOTOS = 5 

83MAX_NUM_UNCONFIRMED_EMAILS = 5 

84MAX_PHOTO_SIZE = 10485760 # in bytes 

85MAX_PIXELS = 7000 

86AVATAR_MAX_SIZE = 512 

87JPEG_QUALITY = 85 

88 

89# Robohash Performance Optimization 

90# Enable optimized robohash implementation for 6-22x performance improvement 

91ROBOHASH_OPTIMIZATION_ENABLED = True 

92 

93# Robohash Configuration 

94# Maximum number of robot parts to cache in memory (each ~50-200KB) 

95ROBOHASH_CACHE_SIZE = 150 # ~10-30MB total cache size 

96 

97# Pagan Avatar Optimization 

98# Maximum number of pagan Avatar objects to cache in memory (each ~100-500KB) 

99PAGAN_CACHE_SIZE = 100 # ~10-50MB total cache size 

100 

101# I'm not 100% sure if single character domains are possible 

102# under any tld... so MIN_LENGTH_EMAIL/_URL, might be +1 

103MIN_LENGTH_URL = 11 # eg. http://a.io 

104MAX_LENGTH_URL = 255 # MySQL can't handle more than that (LP: 1018682) 

105MIN_LENGTH_EMAIL = 6 # eg. x@x.xx 

106MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294 

107 

108BOOTSTRAP4 = { 

109 "include_jquery": False, 

110 "javascript_in_head": False, 

111 "css_url": { 

112 "href": "/static/css/bootstrap.min.css", 

113 "integrity": "sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB", 

114 "crossorigin": "anonymous", 

115 }, 

116 "javascript_url": { 

117 "url": "/static/js/bootstrap.min.js", 

118 "integrity": "", 

119 "crossorigin": "anonymous", 

120 }, 

121 "popper_url": { 

122 "url": "/static/js/popper.min.js", 

123 "integrity": "sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49", 

124 "crossorigin": "anonymous", 

125 }, 

126} 

127 

128if "EMAIL_BACKEND" in os.environ: 

129 EMAIL_BACKEND = os.environ["EMAIL_BACKEND"] # pragma: no cover 

130else: 

131 if "test" in sys.argv or "collectstatic" in sys.argv: 

132 EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 

133 else: 

134 try: 

135 ANYMAIL = { # pragma: no cover 

136 "MAILGUN_API_KEY": os.environ["IVATAR_MAILGUN_API_KEY"], 

137 "MAILGUN_SENDER_DOMAIN": os.environ["IVATAR_MAILGUN_SENDER_DOMAIN"], 

138 } 

139 EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" # pragma: no cover 

140 except Exception: # pragma: nocover # pylint: disable=broad-except 

141 EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" 

142 

143SERVER_EMAIL = os.environ.get("SERVER_EMAIL", "ivatar@mg.linux-kernel.at") 

144DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "ivatar@mg.linux-kernel.at") 

145 

146try: 

147 from ivatar.settings import DATABASES 

148except ImportError: # pragma: no cover 

149 DATABASES = [] # pragma: no cover 

150 

151if "default" not in DATABASES: 

152 DATABASES["default"] = { # pragma: no cover 

153 "ENGINE": "django.db.backends.sqlite3", 

154 "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 

155 } 

156 

157if "MYSQL_DATABASE" in os.environ: 

158 DATABASES["default"] = { # pragma: no cover 

159 "ENGINE": "django.db.backends.mysql", 

160 "NAME": os.environ["MYSQL_DATABASE"], 

161 "USER": os.environ["MYSQL_USER"], 

162 "PASSWORD": os.environ["MYSQL_PASSWORD"], 

163 "HOST": "mysql", 

164 } 

165 

166if "POSTGRESQL_DATABASE" in os.environ: 

167 DATABASES["default"] = { # pragma: no cover 

168 "ENGINE": "django.db.backends.postgresql", 

169 "NAME": os.environ["POSTGRESQL_DATABASE"], 

170 "USER": os.environ["POSTGRESQL_USER"], 

171 "PASSWORD": os.environ["POSTGRESQL_PASSWORD"], 

172 "HOST": "postgresql", 

173 } 

174 

175# CI/CD config has different naming 

176if "POSTGRES_DB" in os.environ: 

177 DATABASES["default"] = { # pragma: no cover 

178 "ENGINE": "django.db.backends.postgresql", 

179 "NAME": os.environ["POSTGRES_DB"], 

180 "USER": os.environ["POSTGRES_USER"], 

181 "PASSWORD": os.environ["POSTGRES_PASSWORD"], 

182 "HOST": os.environ["POSTGRES_HOST"], 

183 # Let Django use its default test database naming 

184 # "TEST": { 

185 # "NAME": os.environ["POSTGRES_DB"], 

186 # }, 

187 } 

188 

189SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer" 

190 

191USE_X_FORWARDED_HOST = True 

192ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [ 

193 "avatars.linux-kernel.at", 

194 "localhost", 

195] 

196 

197DEFAULT_AVATAR_SIZE = 80 

198 

199# Default settings for Gravatar proxy and redirect behavior 

200# These can be overridden by URL parameters 

201DEFAULT_GRAVATARPROXY = True 

202DEFAULT_GRAVATARREDIRECT = False 

203FORCEDEFAULT = False 

204 

205LANGUAGES = ( 

206 ("de", _("Deutsch")), 

207 ("en", _("English")), 

208 ("ca", _("Català")), 

209 ("cs", _("Česky")), 

210 ("es", _("Español")), 

211 ("eu", _("Basque")), 

212 ("fr", _("Français")), 

213 ("it", _("Italiano")), 

214 ("ja", _("日本語")), 

215 ("nl", _("Nederlands")), 

216 ("pt", _("Português")), 

217 ("ru", _("Русский")), 

218 ("sq", _("Shqip")), 

219 ("tr", _("Türkçe")), 

220 ("uk", _("Українська")), 

221) 

222 

223MESSAGE_TAGS = { 

224 message_constants.DEBUG: "debug", 

225 message_constants.INFO: "info", 

226 message_constants.SUCCESS: "success", 

227 message_constants.WARNING: "warning", 

228 message_constants.ERROR: "danger", 

229} 

230 

231CACHES = { 

232 "default": { 

233 "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", 

234 "LOCATION": [ 

235 "127.0.0.1:11211", 

236 ], 

237 # "OPTIONS": {"MAX_ENTRIES": 1000000}, 

238 }, 

239 "filesystem": { 

240 "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", 

241 "LOCATION": "/var/tmp/ivatar_cache", 

242 "TIMEOUT": 900, # 15 minutes 

243 "OPTIONS": {"MAX_ENTRIES": 1000000}, 

244 }, 

245} 

246 

247# This is 5 minutes caching for generated/resized images, 

248# so the sites don't hit ivatar so much - it's what's set in the HTTP header 

249CACHE_IMAGES_MAX_AGE = 5 * 60 

250 

251CACHE_RESPONSE = True 

252 

253# Trusted URLs for default redirection 

254TRUSTED_DEFAULT_URLS = [ 

255 {"schemes": ["https"], "host_equals": "ui-avatars.com", "path_prefix": "/api/"}, 

256 { 

257 "schemes": ["http", "https"], 

258 "host_equals": "gravatar.com", 

259 "path_prefix": "/avatar/", 

260 }, 

261 { 

262 "schemes": ["http", "https"], 

263 "host_suffix": ".gravatar.com", 

264 "path_prefix": "/avatar/", 

265 }, 

266 { 

267 "schemes": ["http", "https"], 

268 "host_equals": "www.gravatar.org", 

269 "path_prefix": "/avatar/", 

270 }, 

271 { 

272 "schemes": ["https"], 

273 "host_equals": "avatars.dicebear.com", 

274 "path_prefix": "/api/", 

275 }, 

276 { 

277 "schemes": ["https"], 

278 "host_equals": "api.dicebear.com", 

279 "path_prefix": "/", 

280 }, 

281 { 

282 "schemes": ["https"], 

283 "host_equals": "badges.fedoraproject.org", 

284 "path_prefix": "/static/img/", 

285 }, 

286 { 

287 "schemes": ["http"], 

288 "host_equals": "www.planet-libre.org", 

289 "path_prefix": "/themes/planetlibre/images/", 

290 }, 

291 {"schemes": ["https"], "host_equals": "www.azuracast.com", "path_prefix": "/img/"}, 

292 { 

293 "schemes": ["https"], 

294 "host_equals": "reps.mozilla.org", 

295 "path_prefix": "/static/base/img/remo/", 

296 }, 

297] 

298 

299URL_TIMEOUT = 10 

300 

301 

302def map_legacy_config(trusted_url): 

303 """ 

304 For backward compability with the legacy configuration 

305 for trusting URLs. Adapts them to fit the new config. 

306 """ 

307 if isinstance(trusted_url, str): 

308 return {"url_prefix": trusted_url} 

309 

310 return trusted_url 

311 

312 

313# Backward compability for legacy behavior 

314TRUSTED_DEFAULT_URLS = list(map(map_legacy_config, TRUSTED_DEFAULT_URLS)) 

315 

316# Bluesky settings 

317BLUESKY_IDENTIFIER = os.environ.get("BLUESKY_IDENTIFIER", None) 

318BLUESKY_APP_PASSWORD = os.environ.get("BLUESKY_APP_PASSWORD", None) 

319 

320# File upload security settings 

321FILE_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024 # 5MB 

322DATA_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024 # 5MB 

323FILE_UPLOAD_PERMISSIONS = 0o644 

324 

325# Enhanced file upload security 

326ENABLE_FILE_SECURITY_VALIDATION = True 

327ENABLE_EXIF_SANITIZATION = True 

328ENABLE_MALICIOUS_CONTENT_SCAN = True 

329 

330# Avatar optimization settings 

331PAGAN_CACHE_SIZE = 1000 # Number of pagan avatars to cache 

332 

333# Logging configuration - can be overridden in local config 

334# Example: LOGS_DIR = "/var/log/ivatar" # For production deployments 

335 

336# This MUST BE THE LAST! 

337if os.path.isfile(os.path.join(BASE_DIR, "config_local.py")): 

338 from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover