Coverage for ivatar/settings.py: 77%

71 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-04 00:07 +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 "social_django", 

141] 

142 

143MIDDLEWARE = [ 

144 "django.middleware.security.SecurityMiddleware", 

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

146 "django.middleware.common.CommonMiddleware", 

147 "django.middleware.csrf.CsrfViewMiddleware", 

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

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

150 "django.middleware.clickjacking.XFrameOptionsMiddleware", 

151] 

152 

153ROOT_URLCONF = "ivatar.urls" 

154 

155TEMPLATES = [ 

156 { 

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

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

159 "APP_DIRS": True, 

160 "OPTIONS": { 

161 "context_processors": [ 

162 "django.template.context_processors.debug", 

163 "django.template.context_processors.request", 

164 "django.contrib.auth.context_processors.auth", 

165 "django.contrib.messages.context_processors.messages", 

166 "django.template.context_processors.i18n", 

167 "social_django.context_processors.login_redirect", 

168 ], 

169 "debug": DEBUG, 

170 }, 

171 }, 

172] 

173 

174WSGI_APPLICATION = "ivatar.wsgi.application" 

175 

176 

177# Database 

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

179 

180DATABASES = { 

181 "default": { 

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

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

184 "ATOMIC_REQUESTS": True, 

185 } 

186} 

187 

188 

189# Password validation 

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

191 

192AUTH_PASSWORD_VALIDATORS = [ 

193 { 

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

195 }, 

196 { 

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

198 "OPTIONS": { 

199 "min_length": 6, 

200 }, 

201 }, 

202 { 

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

204 }, 

205 { 

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

207 }, 

208] 

209 

210# Password Hashing (more secure) 

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

212PASSWORD_HASHERS = [] 

213 

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

215try: 

216 import argon2 # noqa: F401 

217 

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

219except ImportError: 

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

221 pass 

222 

223# Always include PBKDF2 as fallback 

224PASSWORD_HASHERS.extend( 

225 [ 

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

227 # Keep PBKDF2SHA1 for existing password compatibility only 

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

229 ] 

230) 

231 

232# Security Settings 

233SECURE_BROWSER_XSS_FILTER = True 

234SECURE_CONTENT_TYPE_NOSNIFF = True 

235X_FRAME_OPTIONS = "DENY" 

236CSRF_COOKIE_SECURE = not DEBUG 

237SESSION_COOKIE_SECURE = not DEBUG 

238 

239if not DEBUG: 

240 SECURE_SSL_REDIRECT = True 

241 SECURE_HSTS_SECONDS = 31536000 # 1 year 

242 SECURE_HSTS_INCLUDE_SUBDOMAINS = True 

243 SECURE_HSTS_PRELOAD = True 

244 

245 

246# Social authentication 

247TRUST_EMAIL_FROM_SOCIAL_AUTH_BACKENDS = ["fedora"] 

248SOCIAL_AUTH_PIPELINE = ( 

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

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

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

252 # could hit a provider API. 

253 "social_core.pipeline.social_auth.social_details", 

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

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

256 "social_core.pipeline.social_auth.social_uid", 

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

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

259 # defined). 

260 "social_core.pipeline.social_auth.auth_allowed", 

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

262 "social_core.pipeline.social_auth.social_user", 

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

264 # there's any collision. 

265 "social_core.pipeline.user.get_username", 

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

267 # Disabled by default. 

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

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

270 # a similar email address. Disabled by default. 

271 "social_core.pipeline.social_auth.associate_by_email", 

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

273 # a matching ConfirmedEmail. 

274 "ivatar.ivataraccount.auth.associate_by_confirmed_email", 

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

276 "social_core.pipeline.user.create_user", 

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

278 "social_core.pipeline.social_auth.associate_user", 

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

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

281 "social_core.pipeline.social_auth.load_extra_data", 

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

283 "social_core.pipeline.user.user_details", 

284 # Create the ConfirmedEmail if appropriate. 

285 "ivatar.ivataraccount.auth.add_confirmed_email", 

286) 

287 

288 

289# Internationalization 

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

291 

292LANGUAGE_CODE = "en-us" 

293 

294TIME_ZONE = "UTC" 

295 

296USE_I18N = True 

297 

298USE_L10N = True 

299 

300USE_TZ = True 

301 

302 

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

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

305STATIC_URL = "/static/" 

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

307 

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

309 

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

311 

312# OpenTelemetry setup - must be after config import 

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

314try: 

315 from ivatar.opentelemetry_config import setup_opentelemetry 

316 

317 setup_opentelemetry() 

318 

319 # Add OpenTelemetry middleware (always enabled) 

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

321except (ImportError, NameError): 

322 # OpenTelemetry packages not installed or configuration failed 

323 pass