Coverage for ivatar/settings.py: 77%

71 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 11:51 +0000

1""" 

2Django settings for ivatar project. 

3""" 

4 

5import os 

6import logging 

7 

8log_level = logging.DEBUG # pylint: disable=invalid-name 

9logger = logging.getLogger("ivatar") # pylint: disable=invalid-name 

10logger.setLevel(log_level) 

11 

12PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__)) 

13BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 

14 

15# Logging directory - can be overridden in local config 

16LOGS_DIR = os.path.join(BASE_DIR, "logs") 

17 

18 

19def _test_logs_directory_writeability(logs_dir): 

20 """ 

21 Test if a logs directory is actually writable by attempting to create and write a test file 

22 """ 

23 try: 

24 # Ensure directory exists 

25 os.makedirs(logs_dir, exist_ok=True) 

26 

27 # Test if we can actually write to the directory 

28 test_file = os.path.join(logs_dir, ".write_test") 

29 with open(test_file, "w") as f: 

30 f.write("test") 

31 

32 # Clean up test file 

33 os.remove(test_file) 

34 return True 

35 except (OSError, PermissionError): 

36 return False 

37 

38 

39# Ensure logs directory exists and is writable - worst case, fall back to /tmp 

40if not _test_logs_directory_writeability(LOGS_DIR): 

41 LOGS_DIR = "/tmp/libravatar-logs" 

42 if not _test_logs_directory_writeability(LOGS_DIR): 

43 # If even /tmp fails, use a user-specific temp directory 

44 import tempfile 

45 

46 LOGS_DIR = os.path.join(tempfile.gettempdir(), f"libravatar-logs-{os.getuid()}") 

47 _test_logs_directory_writeability(LOGS_DIR) # This should always succeed 

48 

49 logger.warning(f"Failed to write to logs directory, falling back to {LOGS_DIR}") 

50 

51# SECURITY WARNING: keep the secret key used in production secret! 

52SECRET_KEY = "=v(+-^t#ahv^a&&e)uf36g8algj$d1@6ou^w(r0@%)#8mlc*zk" 

53 

54# SECURITY WARNING: don't run with debug turned on in production! 

55DEBUG = True 

56 

57ALLOWED_HOSTS = [] 

58 

59# Comprehensive Logging Configuration 

60LOGGING = { 

61 "version": 1, 

62 "disable_existing_loggers": False, 

63 "formatters": { 

64 "verbose": { 

65 "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", 

66 "style": "{", 

67 }, 

68 "simple": { 

69 "format": "{levelname} {asctime} {message}", 

70 "style": "{", 

71 }, 

72 "detailed": { 

73 "format": "{levelname} {asctime} {name} {module} {funcName} {lineno:d} {message}", 

74 "style": "{", 

75 }, 

76 }, 

77 "handlers": { 

78 "file": { 

79 "level": "INFO", 

80 "class": "logging.FileHandler", 

81 "filename": os.path.join(LOGS_DIR, "ivatar.log"), 

82 "formatter": "verbose", 

83 }, 

84 "file_debug": { 

85 "level": "DEBUG", 

86 "class": "logging.FileHandler", 

87 "filename": os.path.join(LOGS_DIR, "ivatar_debug.log"), 

88 "formatter": "detailed", 

89 }, 

90 "console": { 

91 "level": "DEBUG" if DEBUG else "INFO", 

92 "class": "logging.StreamHandler", 

93 "formatter": "simple", 

94 }, 

95 "security": { 

96 "level": "WARNING", 

97 "class": "logging.FileHandler", 

98 "filename": os.path.join(LOGS_DIR, "security.log"), 

99 "formatter": "detailed", 

100 }, 

101 }, 

102 "loggers": { 

103 "ivatar": { 

104 "handlers": ["file", "console"], 

105 "level": "INFO", # Restore normal logging level 

106 "propagate": True, 

107 }, 

108 "ivatar.security": { 

109 "handlers": ["security", "console"], 

110 "level": "WARNING", 

111 "propagate": False, 

112 }, 

113 "ivatar.debug": { 

114 "handlers": ["file_debug"], 

115 "level": "DEBUG", 

116 "propagate": False, 

117 }, 

118 "django.security": { 

119 "handlers": ["security"], 

120 "level": "WARNING", 

121 "propagate": False, 

122 }, 

123 }, 

124 "root": { 

125 "handlers": ["console"], 

126 "level": "INFO", 

127 }, 

128} 

129 

130 

131# Application definition 

132 

133INSTALLED_APPS = [ 

134 "django.contrib.admin", 

135 "django.contrib.auth", 

136 "django.contrib.contenttypes", 

137 "django.contrib.sessions", 

138 "django.contrib.messages", 

139 "django.contrib.staticfiles", 

140 "django.contrib.postgres", 

141 "social_django", 

142] 

143 

144MIDDLEWARE = [ 

145 "django.middleware.security.SecurityMiddleware", 

146 "django.contrib.sessions.middleware.SessionMiddleware", 

147 "django.middleware.common.CommonMiddleware", 

148 "django.middleware.csrf.CsrfViewMiddleware", 

149 "django.contrib.auth.middleware.AuthenticationMiddleware", 

150 "django.contrib.messages.middleware.MessageMiddleware", 

151 "django.middleware.clickjacking.XFrameOptionsMiddleware", 

152] 

153 

154ROOT_URLCONF = "ivatar.urls" 

155 

156TEMPLATES = [ 

157 { 

158 "BACKEND": "django.template.backends.django.DjangoTemplates", 

159 "DIRS": [os.path.join(BASE_DIR, "templates")], 

160 "APP_DIRS": True, 

161 "OPTIONS": { 

162 "context_processors": [ 

163 "django.template.context_processors.debug", 

164 "django.template.context_processors.request", 

165 "django.contrib.auth.context_processors.auth", 

166 "django.contrib.messages.context_processors.messages", 

167 "django.template.context_processors.i18n", 

168 "social_django.context_processors.login_redirect", 

169 ], 

170 "debug": DEBUG, 

171 }, 

172 }, 

173] 

174 

175WSGI_APPLICATION = "ivatar.wsgi.application" 

176 

177 

178# Database 

179# https://docs.djangoproject.com/en/2.0/ref/settings/#databases 

180 

181DATABASES = { 

182 "default": { 

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

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

185 "ATOMIC_REQUESTS": True, 

186 } 

187} 

188 

189 

190# Password validation 

191# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 

192 

193AUTH_PASSWORD_VALIDATORS = [ 

194 { 

195 "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa 

196 }, 

197 { 

198 "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", # noqa 

199 "OPTIONS": { 

200 "min_length": 6, 

201 }, 

202 }, 

203 { 

204 "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", # noqa 

205 }, 

206 { 

207 "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", # noqa 

208 }, 

209] 

210 

211# Password Hashing (more secure) 

212# Try to use Argon2PasswordHasher with high security settings, fallback to PBKDF2 

213PASSWORD_HASHERS = [] 

214 

215# Try Argon2 first (requires Python 3.6+ and argon2-cffi package) 

216try: 

217 import argon2 # noqa: F401 

218 

219 PASSWORD_HASHERS.append("django.contrib.auth.hashers.Argon2PasswordHasher") 

220except ImportError: 

221 # Fallback for CentOS 7 / older systems without argon2-cffi 

222 pass 

223 

224# Always include PBKDF2 as fallback 

225PASSWORD_HASHERS.extend( 

226 [ 

227 "django.contrib.auth.hashers.PBKDF2PasswordHasher", 

228 # Keep PBKDF2SHA1 for existing password compatibility only 

229 "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", 

230 ] 

231) 

232 

233# Security Settings 

234SECURE_BROWSER_XSS_FILTER = True 

235SECURE_CONTENT_TYPE_NOSNIFF = True 

236X_FRAME_OPTIONS = "DENY" 

237CSRF_COOKIE_SECURE = not DEBUG 

238SESSION_COOKIE_SECURE = not DEBUG 

239 

240if not DEBUG: 

241 SECURE_SSL_REDIRECT = True 

242 SECURE_HSTS_SECONDS = 31536000 # 1 year 

243 SECURE_HSTS_INCLUDE_SUBDOMAINS = True 

244 SECURE_HSTS_PRELOAD = True 

245 

246 

247# Social authentication 

248TRUST_EMAIL_FROM_SOCIAL_AUTH_BACKENDS = ["fedora"] 

249SOCIAL_AUTH_PIPELINE = ( 

250 # Get the information we can about the user and return it in a simple 

251 # format to create the user instance later. In some cases the details are 

252 # already part of the auth response from the provider, but sometimes this 

253 # could hit a provider API. 

254 "social_core.pipeline.social_auth.social_details", 

255 # Get the social uid from whichever service we're authing thru. The uid is 

256 # the unique identifier of the given user in the provider. 

257 "social_core.pipeline.social_auth.social_uid", 

258 # Verifies that the current auth process is valid within the current 

259 # project, this is where emails and domains whitelists are applied (if 

260 # defined). 

261 "social_core.pipeline.social_auth.auth_allowed", 

262 # Checks if the current social-account is already associated in the site. 

263 "social_core.pipeline.social_auth.social_user", 

264 # Make up a username for this person, appends a random string at the end if 

265 # there's any collision. 

266 "social_core.pipeline.user.get_username", 

267 # Send a validation email to the user to verify its email address. 

268 # Disabled by default. 

269 # 'social_core.pipeline.mail.mail_validation', 

270 # Associates the current social details with another user account with 

271 # a similar email address. Disabled by default. 

272 "social_core.pipeline.social_auth.associate_by_email", 

273 # Associates the current social details with an existing user account with 

274 # a matching ConfirmedEmail. 

275 "ivatar.ivataraccount.auth.associate_by_confirmed_email", 

276 # Create a user account if we haven't found one yet. 

277 "social_core.pipeline.user.create_user", 

278 # Create the record that associates the social account with the user. 

279 "social_core.pipeline.social_auth.associate_user", 

280 # Populate the extra_data field in the social record with the values 

281 # specified by settings (and the default ones like access_token, etc). 

282 "social_core.pipeline.social_auth.load_extra_data", 

283 # Update the user record with any changed info from the auth service. 

284 "social_core.pipeline.user.user_details", 

285 # Create the ConfirmedEmail if appropriate. 

286 "ivatar.ivataraccount.auth.add_confirmed_email", 

287) 

288 

289 

290# Internationalization 

291# https://docs.djangoproject.com/en/2.0/topics/i18n/ 

292 

293LANGUAGE_CODE = "en-us" 

294 

295TIME_ZONE = "UTC" 

296 

297USE_I18N = True 

298 

299USE_L10N = True 

300 

301USE_TZ = True 

302 

303 

304# Static files configuration (esp. req. during dev.) 

305PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) 

306STATIC_URL = "/static/" 

307STATIC_ROOT = os.path.join(BASE_DIR, "static") 

308 

309DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 

310 

311from config import * # pylint: disable=wildcard-import,wrong-import-position,unused-wildcard-import # noqa 

312 

313# OpenTelemetry setup - must be after config import 

314# Always setup OpenTelemetry (instrumentation always enabled, export controlled by OTEL_EXPORT_ENABLED) 

315try: 

316 from ivatar.opentelemetry_config import setup_opentelemetry 

317 

318 setup_opentelemetry() 

319 

320 # Add OpenTelemetry middleware (always enabled) 

321 MIDDLEWARE.append("ivatar.opentelemetry_middleware.OpenTelemetryMiddleware") 

322except (ImportError, NameError): 

323 # OpenTelemetry packages not installed or configuration failed 

324 pass