Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/config.py: 89%

331 statements  

« prev     ^ index     » next       coverage.py v7.6.3, created at 2024-10-16 22:16 +0000

1import datetime 

2import hashlib 

3import logging 

4import os 

5import typing as t 

6import warnings 

7from pathlib import Path 

8 

9import google.auth 

10 

11from viur.core.version import __version__ 

12 

13if t.TYPE_CHECKING: # pragma: no cover 

14 from viur.core.email import EmailTransport 

15 from viur.core.skeleton import SkeletonInstance 

16 from viur.core.module import Module 

17 from viur.core.tasks import CustomEnvironmentHandler 

18 

19# Construct an alias with a generic type to be able to write Multiple[str] 

20# TODO: Backward compatible implementation, refactor when viur-core 

21# becomes >= Python 3.12 with a type statement (PEP 695) 

22_T = t.TypeVar("_T") 

23Multiple: t.TypeAlias = list[_T] | tuple[_T] | set[_T] | frozenset[_T] # TODO: Refactor for Python 3.12 

24 

25 

26class CaptchaDefaultCredentialsType(t.TypedDict): 

27 """Expected type of global captcha credential, see :attr:`Security.captcha_default_credentials`""" 

28 sitekey: str 

29 secret: str 

30 

31 

32class ConfigType: 

33 """An abstract class for configurations. 

34 

35 It ensures nesting and backward compatibility for the viur-core config 

36 """ 

37 _mapping = {} 

38 """Mapping from old dict-key (must not be the entire key in case of nesting) to new attribute name""" 

39 

40 _strict_mode = None 

41 """Internal strict mode for this instance. 

42 

43 Use the property getter and setter to access it!""" 

44 

45 _parent = None 

46 """Parent config instance""" 

47 

48 def __init__(self, *, 

49 strict_mode: bool = None, 

50 parent: t.Union["ConfigType", None] = None): 

51 super().__init__() 

52 self._strict_mode = strict_mode 

53 self._parent = parent 

54 

55 @property 

56 def _path(self): 

57 """Get the path in dot-Notation to the current config instance.""" 

58 if self._parent is None: 

59 return "" 

60 return f"{self._parent._path}{self.__class__.__name__.lower()}." 

61 

62 @property 

63 def strict_mode(self): 

64 """Determine if the config runs in strict mode. 

65 

66 In strict mode, the dict-item-access backward compatibility is disabled, 

67 only attribute access is allowed. 

68 Alias mapping is also disabled. Only the real attribute names are allowed. 

69 

70 If self._strict_mode is None, it would inherit the value 

71 of the parent. 

72 If it's explicitly set to True or False, that value will be used. 

73 """ 

74 if self._strict_mode is not None or self._parent is None: 

75 # This config has an explicit value set or there's no parent 

76 return self._strict_mode 

77 else: 

78 # no value set: inherit from the parent 

79 return self._parent.strict_mode 

80 

81 @strict_mode.setter 

82 def strict_mode(self, value: bool | None) -> None: 

83 """Setter for the strict mode of the current instance. 

84 

85 Does not affect other instances! 

86 """ 

87 if not isinstance(value, (bool, type(None))): 

88 raise TypeError(f"Invalid {value=} for strict mode!") 

89 self._strict_mode = value 

90 

91 def _resolve_mapping(self, key: str) -> str: 

92 """Resolve the mapping old dict -> new attribute. 

93 

94 This method must not be called in strict mode! 

95 It can be overwritten to apply additional mapping. 

96 """ 

97 if key in self._mapping: 

98 old, key = key, self._mapping[key] 

99 warnings.warn( 

100 f"Conf member {self._path}{old} is now {self._path}{key}!", 

101 DeprecationWarning, 

102 stacklevel=3, 

103 ) 

104 return key 

105 

106 def items(self, 

107 full_path: bool = False, 

108 recursive: bool = True, 

109 ) -> t.Iterator[tuple[str, t.Any]]: 

110 """Get all setting of this config as key-value mapping. 

111 

112 :param full_path: Show prefix oder only the key. 

113 :param recursive: Call .items() on ConfigType members (children)? 

114 :return: 

115 """ 

116 for key in dir(self): 

117 if key.startswith("_"): 

118 # skip internals, like _parent and _strict_mode 

119 continue 

120 value = getattr(self, key) 

121 if recursive and isinstance(value, ConfigType): 

122 yield from value.items(full_path, recursive) 

123 elif key not in dir(ConfigType): 

124 if full_path: 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true

125 yield f"{self._path}{key}", value 

126 else: 

127 yield key, value 

128 

129 def get(self, key: str, default: t.Any = None) -> t.Any: 

130 """Return an item from the config, if it doesn't exist `default` is returned. 

131 

132 :param key: The key for the attribute lookup. 

133 :param default: The fallback value. 

134 :return: The attribute value or the fallback value. 

135 """ 

136 if self.strict_mode: 

137 raise SyntaxError( 

138 "In strict mode, the config must not be accessed " 

139 "with .get(). Only attribute access is allowed." 

140 ) 

141 try: 

142 return getattr(self, key) 

143 except (KeyError, AttributeError): 

144 return default 

145 

146 def __getitem__(self, key: str) -> t.Any: 

147 """Support the old dict-like syntax (getter). 

148 

149 Not allowed in strict mode. 

150 """ 

151 new_path = f"{self._path}{self._resolve_mapping(key)}" 

152 warnings.warn(f"conf uses now attributes! " 

153 f"Use conf.{new_path} to access your option", 

154 DeprecationWarning, 

155 stacklevel=2) 

156 

157 if self.strict_mode: 

158 raise SyntaxError( 

159 f"In strict mode, the config must not be accessed " 

160 f"with dict notation. " 

161 f"Only attribute access (conf.{new_path}) is allowed." 

162 ) 

163 

164 return getattr(self, key) 

165 

166 def __getattr__(self, key: str) -> t.Any: 

167 """Resolve dot-notation and name mapping in not strict mode. 

168 

169 This method is mostly executed by __getitem__, by the 

170 old dict-like access or by attr(conf, "key"). 

171 In strict mode it does nothing except raising an AttributeError. 

172 """ 

173 if self.strict_mode: 

174 raise AttributeError( 

175 f"AttributeError: '{self.__class__.__name__}' object has no" 

176 f" attribute '{key}' (strict mode is enabled)" 

177 ) 

178 

179 key = self._resolve_mapping(key) 

180 

181 # Got an old dict-key and resolve the segment to the first dot (.) as attribute. 

182 if "." in key: 

183 first, remaining = key.split(".", 1) 

184 return getattr(getattr(self, first), remaining) 

185 

186 return super().__getattribute__(key) 

187 

188 def __setitem__(self, key: str, value: t.Any) -> None: 

189 """Support the old dict-like syntax (setter). 

190 

191 Not allowed in strict mode. 

192 """ 

193 new_path = f"{self._path}{self._resolve_mapping(key)}" 

194 if self.strict_mode: 

195 raise SyntaxError( 

196 f"In strict mode, the config must not be accessed " 

197 f"with dict notation. " 

198 f"Only attribute access (conf.{new_path}) is allowed." 

199 ) 

200 

201 # TODO: re-enable?! 

202 # Avoid to set conf values to something which is already the default 

203 # if key in self and self[key] == value: 

204 # msg = f"Setting conf[\"{key}\"] to {value!r} has no effect, as this value has already been set" 

205 # warnings.warn(msg, stacklevel=3) 

206 # logging.warning(msg, stacklevel=3) 

207 # return 

208 

209 key = self._resolve_mapping(key) 

210 

211 # Got an old dict-key and resolve the segment to the first dot (.) as attribute. 

212 if "." in key: 

213 first, remaining = key.split(".", 1) 

214 if not hasattr(self, first): 

215 # TODO: Compatibility, remove it in a future major release! 

216 # This segment doesn't exist. Create it 

217 logging.warning(f"Creating new type for {first}") 

218 setattr(self, first, type(first.capitalize(), (ConfigType,), {})()) 

219 getattr(self, first)[remaining] = value 

220 return 

221 

222 return setattr(self, key, value) 

223 

224 def __setattr__(self, key: str, value: t.Any) -> None: 

225 """Set attributes after applying the old -> new mapping 

226 

227 In strict mode it does nothing except a super call 

228 for the default object behavior. 

229 """ 

230 if self.strict_mode: 

231 return super().__setattr__(key, value) 

232 

233 if not self.strict_mode: 233 ↛ 237line 233 didn't jump to line 237 because the condition on line 233 was always true

234 key = self._resolve_mapping(key) 

235 

236 # Got an old dict-key and resolve the segment to the first dot (.) as attribute. 

237 if "." in key: 237 ↛ 239line 237 didn't jump to line 239 because the condition on line 237 was never true

238 # TODO: Shall we allow this in strict mode as well? 

239 first, remaining = key.split(".", 1) 

240 return setattr(getattr(self, first), remaining, value) 

241 

242 return super().__setattr__(key, value) 

243 

244 def __repr__(self) -> str: 

245 """Representation of this config""" 

246 return f"{self.__class__.__qualname__}({dict(self.items(False, False))})" 

247 

248 

249# Some values used more than once below 

250_project_id = google.auth.default()[1] 

251_app_version = os.getenv("GAE_VERSION") 

252 

253# Determine our basePath (as os.getCWD is broken on appengine) 

254_project_base_path = Path().absolute() 

255_core_base_path = Path(__file__).parent.parent.parent # fixme: this points to site-packages!!! 

256 

257 

258class Admin(ConfigType): 

259 """Administration tool configuration""" 

260 

261 name: str = "ViUR" 

262 """Administration tool configuration""" 

263 

264 logo: str = "" 

265 """URL for the Logo in the Topbar of the VI""" 

266 

267 login_background: str = "" 

268 """URL for the big Image in the background of the VI Login screen""" 

269 

270 login_logo: str = "" 

271 """URL for the Logo over the VI Login screen""" 

272 

273 color_primary: str = "#d00f1c" 

274 """primary color for viur-admin""" 

275 

276 color_secondary: str = "#333333" 

277 """secondary color for viur-admin""" 

278 

279 module_groups: dict[str, dict[t.Literal["name", "icon", "sortindex"], str | int]] = {} 

280 """Module Groups for the admin tool 

281 

282 Group modules in the sidebar in categories (groups). 

283 

284 Example: 

285 conf.admin.module_groups = { 

286 "content": { 

287 "name": "Content", 

288 "icon": "file-text-fill", 

289 "sortindex": 10, 

290 }, 

291 "shop": { 

292 "name": "Shop", 

293 "icon": "cart-fill", 

294 "sortindex": 20, 

295 }, 

296 } 

297 

298 To add a module to one of these groups (e.g. content), add `moduleGroup` to 

299 the admin_info of the module: 

300 "moduleGroup": "content", 

301 """ 

302 

303 _mapping: dict[str, str] = { 

304 "login.background": "login_background", 

305 "login.logo": "login_logo", 

306 "color.primary": "color_primary", 

307 "color.secondary": "color_secondary", 

308 } 

309 

310 

311class Security(ConfigType): 

312 """Security related settings""" 

313 

314 force_ssl: bool = True 

315 """If true, all requests must be encrypted (ignored on development server)""" 

316 

317 no_ssl_check_urls: Multiple[str] = ["/_tasks*", "/ah/*"] 

318 """List of URLs for which force_ssl is ignored. 

319 Add an asterisk to mark that entry as a prefix (exact match otherwise)""" 

320 

321 content_security_policy: t.Optional[dict[str, dict[str, list[str]]]] = { 

322 "enforce": { 

323 "style-src": ["self", "https://accounts.google.com/gsi/style"], 

324 "default-src": ["self"], 

325 "img-src": ["self", "storage.googleapis.com"], # Serving-URLs of file-Bones will point here 

326 "script-src": ["self", "https://accounts.google.com/gsi/client"], 

327 # Required for login with Google 

328 "frame-src": ["self", "www.google.com", "drive.google.com", "accounts.google.com"], 

329 "form-action": ["self"], 

330 "connect-src": ["self", "accounts.google.com"], 

331 "upgrade-insecure-requests": [], 

332 "object-src": ["none"], 

333 } 

334 } 

335 """If set, viur will emit a CSP http-header with each request. Use security.addCspRule to set this property""" 

336 

337 referrer_policy: str = "strict-origin" 

338 """Per default, we'll emit Referrer-Policy: strict-origin so no referrers leak to external services 

339 

340 See https://www.w3.org/TR/referrer-policy/ 

341 """ 

342 

343 permissions_policy: dict[str, list[str]] = { 

344 "autoplay": ["self"], 

345 "camera": [], 

346 "display-capture": [], 

347 "document-domain": [], 

348 "encrypted-media": [], 

349 "fullscreen": [], 

350 "geolocation": [], 

351 "microphone": [], 

352 "publickey-credentials-get": [], 

353 "usb": [], 

354 } 

355 """Include a default permissions-policy. 

356 To use the camera or microphone, you'll have to call 

357 :meth: securityheaders.setPermissionPolicyDirective to include at least "self" 

358 """ 

359 

360 enable_coep: bool = False 

361 """Shall we emit Cross-Origin-Embedder-Policy: require-corp?""" 

362 

363 enable_coop: t.Literal[ 

364 "unsafe-none", "same-origin-allow-popups", 

365 "same-origin", "same-origin-plus-COEP"] = "same-origin" 

366 """Emit a Cross-Origin-Opener-Policy Header? 

367 

368 See https://html.spec.whatwg.org/multipage/browsers.html#cross-origin-opener-policy-value 

369 """ 

370 

371 enable_corp: t.Literal["same-origin", "same-site", "cross-origin"] = "same-origin" 

372 """Emit a Cross-Origin-Resource-Policy Header? 

373 

374 See https://fetch.spec.whatwg.org/#cross-origin-resource-policy-header 

375 """ 

376 

377 strict_transport_security: t.Optional[str] = "max-age=22118400" 

378 """If set, ViUR will emit a HSTS HTTP-header with each request. 

379 Use security.enableStrictTransportSecurity to set this property""" 

380 

381 x_frame_options: t.Optional[ 

382 tuple[t.Literal["deny", "sameorigin", "allow-from"], t.Optional[str]] 

383 ] = ("sameorigin", None) 

384 """If set, ViUR will emit an X-Frame-Options header 

385 

386 In case of allow-from, the second parameters must be the host-url. 

387 Otherwise, it can be None. 

388 """ 

389 

390 x_xss_protection: t.Optional[bool] = True 

391 """ViUR will emit an X-XSS-Protection header if set (the default)""" 

392 

393 x_content_type_options: bool = True 

394 """ViUR will emit X-Content-Type-Options: nosniff Header unless set to False""" 

395 

396 x_permitted_cross_domain_policies: t.Optional[t.Literal["none", "master-only", "by-content-type", "all"]] = "none" 

397 """Unless set to logical none; ViUR will emit a X-Permitted-Cross-Domain-Policies with each request""" 

398 

399 captcha_default_credentials: t.Optional[CaptchaDefaultCredentialsType] = None 

400 """The default sitekey and secret to use for the :class:`CaptchaBone`. 

401 If set, must be a dictionary of "sitekey" and "secret". 

402 """ 

403 

404 captcha_enforce_always: bool = False 

405 """By default a captcha of the :class:`CaptchaBone` must not be solved on a local development server 

406 or by a root user. But for development it can be helpful to test the implementation 

407 on a local development server. Setting this flag to True, disables this behavior and 

408 enforces always a valid captcha. 

409 """ 

410 

411 password_recovery_key_length: int = 42 

412 """Length of the Password recovery key""" 

413 

414 closed_system: bool = False 

415 """If `True` it activates a mode in which only authenticated users can access all routes.""" 

416 

417 admin_allowed_paths: t.Iterable[str] = [ 

418 "vi", 

419 "vi/skey", 

420 "vi/settings", 

421 "vi/user/auth_*", 

422 "vi/user/f2_*", 

423 "vi/user/getAuthMethods", # FIXME: deprecated, use `login` for this 

424 "vi/user/login", 

425 ] 

426 """Specifies admin tool paths which are being accessible without authenticated user.""" 

427 

428 closed_system_allowed_paths: t.Iterable[str] = admin_allowed_paths + [ 

429 "", # index site 

430 "json/skey", 

431 "json/user/auth_*", 

432 "json/user/f2_*", 

433 "json/user/getAuthMethods", # FIXME: deprecated, use `login` for this 

434 "json/user/login", 

435 "user/auth_*", 

436 "user/f2_*", 

437 "user/getAuthMethods", # FIXME: deprecated, use `login` for this 

438 "user/login", 

439 ] 

440 """Paths that are accessible without authentication in a closed system, see `closed_system` for details.""" 

441 

442 _mapping = { 

443 "contentSecurityPolicy": "content_security_policy", 

444 "referrerPolicy": "referrer_policy", 

445 "permissionsPolicy": "permissions_policy", 

446 "enableCOEP": "enable_coep", 

447 "enableCOOP": "enable_coop", 

448 "enableCORP": "enable_corp", 

449 "strictTransportSecurity": "strict_transport_security", 

450 "xFrameOptions": "x_frame_options", 

451 "xXssProtection": "x_xss_protection", 

452 "xContentTypeOptions": "x_content_type_options", 

453 "xPermittedCrossDomainPolicies": "x_permitted_cross_domain_policies", 

454 "captcha_defaultCredentials": "captcha_default_credentials", 

455 "captcha.defaultCredentials": "captcha_default_credentials", 

456 } 

457 

458 

459class Debug(ConfigType): 

460 """Several debug flags""" 

461 

462 trace: bool = False 

463 """If enabled, trace any routing, HTTPExceptions and decorations for debugging and insight""" 

464 

465 trace_exceptions: bool = False 

466 """If enabled, user-generated exceptions from the viur.core.errors module won't be caught and handled""" 

467 

468 trace_external_call_routing: bool = False 

469 """If enabled, ViUR will log which (exposed) function are called from outside with what arguments""" 

470 

471 trace_internal_call_routing: bool = False 

472 """If enabled, ViUR will log which (internal-exposed) function are called from templates with what arguments""" 

473 

474 skeleton_from_client: bool = False 

475 """If enabled, log errors raises from skeleton.fromClient()""" 

476 

477 dev_server_cloud_logging: bool = False 

478 """If disabled the local logging will not send with requestLogger to the cloud""" 

479 

480 disable_cache: bool = False 

481 """If set to true, the decorator @enableCache from viur.core.cache has no effect""" 

482 

483 _mapping = { 

484 "skeleton.fromClient": "skeleton_from_client", 

485 "traceExceptions": "trace_exceptions", 

486 "traceExternalCallRouting": "trace_external_call_routing", 

487 "traceInternalCallRouting": "trace_internal_call_routing", 

488 "skeleton_fromClient": "skeleton_from_client", 

489 "disableCache": "disable_cache", 

490 } 

491 

492 

493class Email(ConfigType): 

494 """Email related settings.""" 

495 

496 log_retention: datetime.timedelta = datetime.timedelta(days=30) 

497 """For how long we'll keep successfully send emails in the viur-emails table""" 

498 

499 transport_class: t.Type["EmailTransport"] = None 

500 """Class that actually delivers the email using the service provider 

501 of choice. See email.py for more details 

502 """ 

503 

504 mailjet_api_key: t.Optional[str] = None 

505 """API Key for MailJet""" 

506 

507 mailjet_api_secret: t.Optional[str] = None 

508 """API Secret for MailJet""" 

509 

510 sendinblue_api_key: t.Optional[str] = None 

511 """API Key for SendInBlue (now Brevo) for the EmailTransportSendInBlue 

512 """ 

513 

514 sendinblue_thresholds: tuple[int] | list[int] = (1000, 500, 100) 

515 """Warning thresholds for remaining email quota 

516 

517 Used by email.EmailTransportSendInBlue.check_sib_quota 

518 """ 

519 

520 send_from_local_development_server: bool = False 

521 """If set, we'll enable sending emails from the local development server. 

522 Otherwise, they'll just be logged. 

523 """ 

524 

525 recipient_override: str | list[str] | t.Callable[[], str | list[str]] | t.Literal[False] = None 

526 """If set, all outgoing emails will be sent to this address 

527 (overriding the 'dests'-parameter in email.sendEmail) 

528 """ 

529 

530 sender_override: str | None = None 

531 """If set, this sender will be used, regardless of what the templates advertise as sender""" 

532 

533 admin_recipients: str | list[str] | t.Callable[[], str | list[str]] = None 

534 """Sets recipients for mails send with email.sendEMailToAdmins. If not set, all root users will be used.""" 

535 

536 _mapping = { 

537 "logRetention": "log_retention", 

538 "transportClass": "transport_class", 

539 "sendFromLocalDevelopmentServer": "send_from_local_development_server", 

540 "recipientOverride": "recipient_override", 

541 "senderOverride": "sender_override", 

542 "admin_recipients": "admin_recipients", 

543 "sendInBlue.apiKey": "sendinblue_api_key", 

544 "sendInBlue.thresholds": "sendinblue_thresholds", 

545 } 

546 

547 

548class I18N(ConfigType): 

549 """All i18n, multilang related settings.""" 

550 

551 available_languages: Multiple[str] = ["en"] 

552 """List of language-codes, which are valid for this application""" 

553 

554 default_language: str = "en" 

555 """Unless overridden by the Project: Use english as default language""" 

556 

557 domain_language_mapping: dict[str, str] = {} 

558 """Maps Domains to alternative default languages""" 

559 

560 language_alias_map: dict[str, str] = {} 

561 """Allows mapping of certain languages to one translation (i.e. us->en)""" 

562 

563 language_method: t.Literal["session", "url", "domain"] = "session" 

564 """Defines how translations are applied: 

565 - session: Per Session 

566 - url: inject language prefix in url 

567 - domain: one domain per language 

568 """ 

569 

570 language_module_map: dict[str, dict[str, str]] = {} 

571 """Maps modules to their translation (if set)""" 

572 

573 @property 

574 def available_dialects(self) -> list[str]: 

575 """Main languages and language aliases""" 

576 # Use a dict to keep the order and remove duplicates 

577 res = dict.fromkeys(self.available_languages) 

578 res |= self.language_alias_map 

579 return list(res.keys()) 

580 

581 add_missing_translations: bool = False 

582 """Add missing translation into datastore. 

583 

584 If a key is not found in the translation table when a translation is 

585 rendered, a database entry is created with the key and hint and 

586 default value (if set) so that the translations 

587 can be entered in the administration. 

588 """ 

589 

590 

591class User(ConfigType): 

592 """User, session, login related settings""" 

593 

594 access_rights: Multiple[str] = ["root", "admin"] 

595 """Additional access rights available on this project""" 

596 

597 roles: dict[str, str] = { 

598 "custom": "Custom", 

599 "user": "User", 

600 "viewer": "Viewer", 

601 "editor": "Editor", 

602 "admin": "Administrator", 

603 } 

604 """User roles available on this project""" 

605 

606 session_life_time: int = 60 * 60 

607 """Default is 60 minutes lifetime for ViUR sessions""" 

608 

609 session_persistent_fields_on_login: Multiple[str] = ["language"] 

610 """If set, these Fields will survive the session.reset() called on user/login""" 

611 

612 session_persistent_fields_on_logout: Multiple[str] = ["language"] 

613 """If set, these Fields will survive the session.reset() called on user/logout""" 

614 

615 max_password_length: int = 512 

616 """Prevent Denial of Service attacks using large inputs for pbkdf2""" 

617 

618 otp_issuer: t.Optional[str] = None 

619 """The name of the issuer for the opt token""" 

620 

621 google_client_id: t.Optional[str] = None 

622 """OAuth Client ID for Google Login""" 

623 

624 google_gsuite_domains: list[str] = [] 

625 """A list of domains. When a user signs in for the first time with a 

626 Google account using Google OAuth sign-in, and the user's email address 

627 belongs to one of the listed domains, a user account (UserSkel) is created. 

628 If the user's email address belongs to any other domain, 

629 no account is created.""" 

630 

631 

632class Instance(ConfigType): 

633 """All app instance related settings information""" 

634 app_version: str = _app_version 

635 """Name of this version as deployed to the appengine""" 

636 

637 core_base_path: Path = _core_base_path 

638 """The base path of the core, can be used to find file in the core folder""" 

639 

640 is_dev_server: bool = os.getenv("GAE_ENV") == "localdev" 

641 """Determine whether instance is running on a local development server""" 

642 

643 project_base_path: Path = _project_base_path 

644 """The base path of the project, can be used to find file in the project folder""" 

645 

646 project_id: str = _project_id 

647 """The instance's project ID""" 

648 

649 version_hash: str = hashlib.sha256(f"{_app_version}{project_id}".encode("UTF-8")).hexdigest()[:10] 

650 """Version hash that does not reveal the actual version name, can be used for cache-busting static resources""" 

651 

652 

653class Conf(ConfigType): 

654 """Conf class wraps the conf dict and allows to handle 

655 deprecated keys or other special operations. 

656 """ 

657 

658 bone_boolean_str2true: Multiple[str | int] = ("true", "yes", "1") 

659 """Allowed values that define a str to evaluate to true""" 

660 

661 cache_environment_key: t.Optional[t.Callable[[], str]] = None 

662 """If set, this function will be called for each cache-attempt 

663 and the result will be included in the computed cache-key""" 

664 

665 # FIXME VIUR4: REMOVE ALL COMPATIBILITY MODES! 

666 compatibility: Multiple[str] = [ 

667 "json.bone.structure.camelcasenames", # use camelCase attribute names (see #637 for details) 

668 "json.bone.structure.keytuples", # use classic structure notation: `"structure = [["key", {...}] ...]` (#649) 

669 "json.bone.structure.inlists", # dump skeleton structure with every JSON list response (#774 for details) 

670 "bone.select.structure.values.keytuple", # render old-style tuple-list in SelectBone's values structure (#1203) 

671 ] 

672 """Backward compatibility flags; Remove to enforce new style.""" 

673 

674 db_engine: str = "viur.datastore" 

675 """Database engine module""" 

676 

677 error_handler: t.Callable[[Exception], str] | None = None 

678 """If set, ViUR calls this function instead of rendering the viur.errorTemplate if an exception occurs""" 

679 

680 error_logo: str = None 

681 """Path to a logo (static file). Will be used for the default error template""" 

682 

683 static_embed_svg_path: str = "/static/svgs/" 

684 """Path to the static SVGs folder. Will be used by the jinja-renderer-method: embedSvg""" 

685 

686 file_hmac_key: str = None 

687 """Hmac-Key used to sign download urls - set automatically""" 

688 

689 # TODO: separate this type hints and use it in the File module as well 

690 file_derivations: dict[str, t.Callable[["SkeletonInstance", dict, dict], list[tuple[str, float, str, t.Any]]]] = {} 

691 """Call-Map for file pre-processors""" 

692 

693 file_thumbnailer_url: t.Optional[str] = None 

694 # TODO: """docstring""" 

695 

696 main_app: "Module" = None 

697 """Reference to our pre-build Application-Instance""" 

698 

699 main_resolver: dict[str, dict] = None 

700 """Dictionary for Resolving functions for URLs""" 

701 

702 max_post_params_count: int = 250 

703 """Upper limit of the amount of parameters we accept per request. Prevents Hash-Collision-Attacks""" 

704 

705 param_filter_function: t.Callable[[str, str], bool] = lambda _, key, value: key.startswith("_") 

706 """ 

707 Function which decides if a request parameter should be used or filtered out. 

708 Returning True means to filter out. 

709 """ 

710 

711 moduleconf_admin_info: dict[str, t.Any] = { 

712 "icon": "gear-fill", 

713 "display": "hidden", 

714 } 

715 """Describing the internal ModuleConfig-module""" 

716 

717 script_admin_info: dict[str, t.Any] = { 

718 "icon": "file-code-fill", 

719 "display": "hidden", 

720 } 

721 """Describing the Script module""" 

722 

723 render_html_download_url_expiration: t.Optional[float | int] = None 

724 """The default duration, for which downloadURLs generated by the html renderer will stay valid""" 

725 

726 render_json_download_url_expiration: t.Optional[float | int] = None 

727 """The default duration, for which downloadURLs generated by the json renderer will stay valid""" 

728 

729 request_preprocessor: t.Optional[t.Callable[[str], str]] = None 

730 """Allows the application to register a function that's called before the request gets routed""" 

731 

732 search_valid_chars: str = "abcdefghijklmnopqrstuvwxyzäöüß0123456789" 

733 """Characters valid for the internal search functionality (all other chars are ignored)""" 

734 

735 skeleton_search_path: Multiple[str] = [ 

736 "/skeletons/", # skeletons of the project 

737 "/viur/core/", # system-defined skeletons of viur-core 

738 "/viur-core/core/" # system-defined skeletons of viur-core, only used by editable installation 

739 ] 

740 """Priority, in which skeletons are loaded""" 

741 

742 _tasks_custom_environment_handler: t.Optional["CustomEnvironmentHandler"] = None 

743 

744 @property 

745 def tasks_custom_environment_handler(self) -> t.Optional["CustomEnvironmentHandler"]: 

746 """ 

747 Preserve additional environment in deferred tasks. 

748 

749 If set, it must be an instance of CustomEnvironmentHandler 

750 for serializing/restoring environment data. 

751 """ 

752 return self._tasks_custom_environment_handler 

753 

754 @tasks_custom_environment_handler.setter 

755 def tasks_custom_environment_handler(self, value: "CustomEnvironmentHandler") -> None: 

756 from .tasks import CustomEnvironmentHandler 

757 if isinstance(value, CustomEnvironmentHandler) or value is None: 

758 self._tasks_custom_environment_handler = value 

759 elif isinstance(value, tuple): 

760 if len(value) != 2: 

761 raise ValueError(f"Expected a (serialize_env_func, restore_env_func) pair") 

762 warnings.warn( 

763 f"tuple is deprecated, please provide a CustomEnvironmentHandler object!", 

764 DeprecationWarning, stacklevel=2, 

765 ) 

766 # Construct an CustomEnvironmentHandler class on the fly to be backward compatible 

767 cls = type("ProjectCustomEnvironmentHandler", (CustomEnvironmentHandler,), 

768 # serialize and restore will be bound methods. 

769 # Therefore, consume the self argument with lambda. 

770 {"serialize": lambda self: value[0](), 

771 "restore": lambda self, obj: value[1](obj)}) 

772 self._tasks_custom_environment_handler = cls() 

773 else: 

774 raise ValueError(f"Invalid type {type(value)}. Expected a CustomEnvironmentHandler object.") 

775 

776 valid_application_ids: list[str] = [] 

777 """Which application-ids we're supposed to run on""" 

778 

779 version: tuple[int, int, int] = tuple(int(part) if part.isdigit() else part for part in __version__.split(".", 3)) 

780 """Semantic version number of viur-core as a tuple of 3 (major, minor, patch-level)""" 

781 

782 viur2import_blobsource: t.Optional[dict[t.Literal["infoURL", "gsdir"], str]] = None 

783 """Configuration to import file blobs from ViUR2""" 

784 

785 def __init__(self, strict_mode: bool = False): 

786 super().__init__() 

787 self._strict_mode = strict_mode 

788 self.admin = Admin(parent=self) 

789 self.security = Security(parent=self) 

790 self.debug = Debug(parent=self) 

791 self.email = Email(parent=self) 

792 self.i18n = I18N(parent=self) 

793 self.user = User(parent=self) 

794 self.instance = Instance(parent=self) 

795 

796 _mapping = { 

797 # debug 

798 "viur.dev_server_cloud_logging": "debug.dev_server_cloud_logging", 

799 "viur.disable_cache": "debug.disable_cache", 

800 # i18n 

801 "viur.availableLanguages": "i18n.available_languages", 

802 "viur.defaultLanguage": "i18n.default_language", 

803 "viur.domainLanguageMapping": "i18n.domain_language_mapping", 

804 "viur.languageAliasMap": "i18n.language_alias_map", 

805 "viur.languageMethod": "i18n.language_method", 

806 "viur.languageModuleMap": "i18n.language_module_map", 

807 # user 

808 "viur.accessRights": "user.access_rights", 

809 "viur.maxPasswordLength": "user.max_password_length", 

810 "viur.otp.issuer": "user.otp_issuer", 

811 "viur.session.lifeTime": "user.session_life_time", 

812 "viur.session.persistentFieldsOnLogin": "user.session_persistent_fields_on_login", 

813 "viur.session.persistentFieldsOnLogout": "user.session_persistent_fields_on_logout", 

814 "viur.user.roles": "user.roles", 

815 "viur.user.google.clientID": "user.google_client_id", 

816 "viur.user.google.gsuiteDomains": "user.google_gsuite_domains", 

817 # instance 

818 "viur.instance.app_version": "instance.app_version", 

819 "viur.instance.core_base_path": "instance.core_base_path", 

820 "viur.instance.is_dev_server": "instance.is_dev_server", 

821 "viur.instance.project_base_path": "instance.project_base_path", 

822 "viur.instance.project_id": "instance.project_id", 

823 "viur.instance.version_hash": "instance.version_hash", 

824 # security 

825 "viur.forceSSL": "security.force_ssl", 

826 "viur.noSSLCheckUrls": "security.no_ssl_check_urls", 

827 # old viur-prefix 

828 "viur.cacheEnvironmentKey": "cache_environment_key", 

829 "viur.contentSecurityPolicy": "content_security_policy", 

830 "viur.bone.boolean.str2true": "bone_boolean_str2true", 

831 "viur.db.engine": "db_engine", 

832 "viur.errorHandler": "error_handler", 

833 "viur.static.embedSvg.path": "static_embed_svg_path", 

834 "viur.file.hmacKey": "file_hmac_key", 

835 "viur.file_hmacKey": "file_hmac_key", 

836 "viur.file.derivers": "file_derivations", 

837 "viur.file.thumbnailerURL": "file_thumbnailer_url", 

838 "viur.mainApp": "main_app", 

839 "viur.mainResolver": "main_resolver", 

840 "viur.maxPostParamsCount": "max_post_params_count", 

841 "viur.moduleconf.admin_info": "moduleconf_admin_info", 

842 "viur.script.admin_info": "script_admin_info", 

843 "viur.render.html.downloadUrlExpiration": "render_html_download_url_expiration", 

844 "viur.downloadUrlFor.expiration": "render_html_download_url_expiration", 

845 "viur.render.json.downloadUrlExpiration": "render_json_download_url_expiration", 

846 "viur.requestPreprocessor": "request_preprocessor", 

847 "viur.searchValidChars": "search_valid_chars", 

848 "viur.skeleton.searchPath": "skeleton_search_path", 

849 "viur.tasks.customEnvironmentHandler": "tasks_custom_environment_handler", 

850 "viur.validApplicationIDs": "valid_application_ids", 

851 "viur.viur2import.blobsource": "viur2import_blobsource", 

852 } 

853 

854 def _resolve_mapping(self, key: str) -> str: 

855 """Additional mapping for new sub confs.""" 

856 if key.startswith("viur.") and key not in self._mapping: 

857 key = key.removeprefix("viur.") 

858 return super()._resolve_mapping(key) 

859 

860 

861conf = Conf( 

862 strict_mode=os.getenv("VIUR_CORE_CONFIG_STRICT_MODE", "").lower() == "true", 

863)