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
« 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
9import google.auth
11from viur.core.version import __version__
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
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
26class CaptchaDefaultCredentialsType(t.TypedDict):
27 """Expected type of global captcha credential, see :attr:`Security.captcha_default_credentials`"""
28 sitekey: str
29 secret: str
32class ConfigType:
33 """An abstract class for configurations.
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"""
40 _strict_mode = None
41 """Internal strict mode for this instance.
43 Use the property getter and setter to access it!"""
45 _parent = None
46 """Parent config instance"""
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
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()}."
62 @property
63 def strict_mode(self):
64 """Determine if the config runs in strict mode.
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.
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
81 @strict_mode.setter
82 def strict_mode(self, value: bool | None) -> None:
83 """Setter for the strict mode of the current instance.
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
91 def _resolve_mapping(self, key: str) -> str:
92 """Resolve the mapping old dict -> new attribute.
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
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.
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
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.
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
146 def __getitem__(self, key: str) -> t.Any:
147 """Support the old dict-like syntax (getter).
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)
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 )
164 return getattr(self, key)
166 def __getattr__(self, key: str) -> t.Any:
167 """Resolve dot-notation and name mapping in not strict mode.
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 )
179 key = self._resolve_mapping(key)
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)
186 return super().__getattribute__(key)
188 def __setitem__(self, key: str, value: t.Any) -> None:
189 """Support the old dict-like syntax (setter).
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 )
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
209 key = self._resolve_mapping(key)
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
222 return setattr(self, key, value)
224 def __setattr__(self, key: str, value: t.Any) -> None:
225 """Set attributes after applying the old -> new mapping
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)
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)
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)
242 return super().__setattr__(key, value)
244 def __repr__(self) -> str:
245 """Representation of this config"""
246 return f"{self.__class__.__qualname__}({dict(self.items(False, False))})"
249# Some values used more than once below
250_project_id = google.auth.default()[1]
251_app_version = os.getenv("GAE_VERSION")
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!!!
258class Admin(ConfigType):
259 """Administration tool configuration"""
261 name: str = "ViUR"
262 """Administration tool configuration"""
264 logo: str = ""
265 """URL for the Logo in the Topbar of the VI"""
267 login_background: str = ""
268 """URL for the big Image in the background of the VI Login screen"""
270 login_logo: str = ""
271 """URL for the Logo over the VI Login screen"""
273 color_primary: str = "#d00f1c"
274 """primary color for viur-admin"""
276 color_secondary: str = "#333333"
277 """secondary color for viur-admin"""
279 module_groups: dict[str, dict[t.Literal["name", "icon", "sortindex"], str | int]] = {}
280 """Module Groups for the admin tool
282 Group modules in the sidebar in categories (groups).
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 }
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 """
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 }
311class Security(ConfigType):
312 """Security related settings"""
314 force_ssl: bool = True
315 """If true, all requests must be encrypted (ignored on development server)"""
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)"""
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"""
337 referrer_policy: str = "strict-origin"
338 """Per default, we'll emit Referrer-Policy: strict-origin so no referrers leak to external services
340 See https://www.w3.org/TR/referrer-policy/
341 """
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 """
360 enable_coep: bool = False
361 """Shall we emit Cross-Origin-Embedder-Policy: require-corp?"""
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?
368 See https://html.spec.whatwg.org/multipage/browsers.html#cross-origin-opener-policy-value
369 """
371 enable_corp: t.Literal["same-origin", "same-site", "cross-origin"] = "same-origin"
372 """Emit a Cross-Origin-Resource-Policy Header?
374 See https://fetch.spec.whatwg.org/#cross-origin-resource-policy-header
375 """
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"""
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
386 In case of allow-from, the second parameters must be the host-url.
387 Otherwise, it can be None.
388 """
390 x_xss_protection: t.Optional[bool] = True
391 """ViUR will emit an X-XSS-Protection header if set (the default)"""
393 x_content_type_options: bool = True
394 """ViUR will emit X-Content-Type-Options: nosniff Header unless set to False"""
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"""
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 """
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 """
411 password_recovery_key_length: int = 42
412 """Length of the Password recovery key"""
414 closed_system: bool = False
415 """If `True` it activates a mode in which only authenticated users can access all routes."""
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."""
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."""
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 }
459class Debug(ConfigType):
460 """Several debug flags"""
462 trace: bool = False
463 """If enabled, trace any routing, HTTPExceptions and decorations for debugging and insight"""
465 trace_exceptions: bool = False
466 """If enabled, user-generated exceptions from the viur.core.errors module won't be caught and handled"""
468 trace_external_call_routing: bool = False
469 """If enabled, ViUR will log which (exposed) function are called from outside with what arguments"""
471 trace_internal_call_routing: bool = False
472 """If enabled, ViUR will log which (internal-exposed) function are called from templates with what arguments"""
474 skeleton_from_client: bool = False
475 """If enabled, log errors raises from skeleton.fromClient()"""
477 dev_server_cloud_logging: bool = False
478 """If disabled the local logging will not send with requestLogger to the cloud"""
480 disable_cache: bool = False
481 """If set to true, the decorator @enableCache from viur.core.cache has no effect"""
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 }
493class Email(ConfigType):
494 """Email related settings."""
496 log_retention: datetime.timedelta = datetime.timedelta(days=30)
497 """For how long we'll keep successfully send emails in the viur-emails table"""
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 """
504 mailjet_api_key: t.Optional[str] = None
505 """API Key for MailJet"""
507 mailjet_api_secret: t.Optional[str] = None
508 """API Secret for MailJet"""
510 sendinblue_api_key: t.Optional[str] = None
511 """API Key for SendInBlue (now Brevo) for the EmailTransportSendInBlue
512 """
514 sendinblue_thresholds: tuple[int] | list[int] = (1000, 500, 100)
515 """Warning thresholds for remaining email quota
517 Used by email.EmailTransportSendInBlue.check_sib_quota
518 """
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 """
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 """
530 sender_override: str | None = None
531 """If set, this sender will be used, regardless of what the templates advertise as sender"""
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."""
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 }
548class I18N(ConfigType):
549 """All i18n, multilang related settings."""
551 available_languages: Multiple[str] = ["en"]
552 """List of language-codes, which are valid for this application"""
554 default_language: str = "en"
555 """Unless overridden by the Project: Use english as default language"""
557 domain_language_mapping: dict[str, str] = {}
558 """Maps Domains to alternative default languages"""
560 language_alias_map: dict[str, str] = {}
561 """Allows mapping of certain languages to one translation (i.e. us->en)"""
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 """
570 language_module_map: dict[str, dict[str, str]] = {}
571 """Maps modules to their translation (if set)"""
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())
581 add_missing_translations: bool = False
582 """Add missing translation into datastore.
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 """
591class User(ConfigType):
592 """User, session, login related settings"""
594 access_rights: Multiple[str] = ["root", "admin"]
595 """Additional access rights available on this project"""
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"""
606 session_life_time: int = 60 * 60
607 """Default is 60 minutes lifetime for ViUR sessions"""
609 session_persistent_fields_on_login: Multiple[str] = ["language"]
610 """If set, these Fields will survive the session.reset() called on user/login"""
612 session_persistent_fields_on_logout: Multiple[str] = ["language"]
613 """If set, these Fields will survive the session.reset() called on user/logout"""
615 max_password_length: int = 512
616 """Prevent Denial of Service attacks using large inputs for pbkdf2"""
618 otp_issuer: t.Optional[str] = None
619 """The name of the issuer for the opt token"""
621 google_client_id: t.Optional[str] = None
622 """OAuth Client ID for Google Login"""
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."""
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"""
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"""
640 is_dev_server: bool = os.getenv("GAE_ENV") == "localdev"
641 """Determine whether instance is running on a local development server"""
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"""
646 project_id: str = _project_id
647 """The instance's project ID"""
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"""
653class Conf(ConfigType):
654 """Conf class wraps the conf dict and allows to handle
655 deprecated keys or other special operations.
656 """
658 bone_boolean_str2true: Multiple[str | int] = ("true", "yes", "1")
659 """Allowed values that define a str to evaluate to true"""
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"""
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."""
674 db_engine: str = "viur.datastore"
675 """Database engine module"""
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"""
680 error_logo: str = None
681 """Path to a logo (static file). Will be used for the default error template"""
683 static_embed_svg_path: str = "/static/svgs/"
684 """Path to the static SVGs folder. Will be used by the jinja-renderer-method: embedSvg"""
686 file_hmac_key: str = None
687 """Hmac-Key used to sign download urls - set automatically"""
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"""
693 file_thumbnailer_url: t.Optional[str] = None
694 # TODO: """docstring"""
696 main_app: "Module" = None
697 """Reference to our pre-build Application-Instance"""
699 main_resolver: dict[str, dict] = None
700 """Dictionary for Resolving functions for URLs"""
702 max_post_params_count: int = 250
703 """Upper limit of the amount of parameters we accept per request. Prevents Hash-Collision-Attacks"""
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 """
711 moduleconf_admin_info: dict[str, t.Any] = {
712 "icon": "gear-fill",
713 "display": "hidden",
714 }
715 """Describing the internal ModuleConfig-module"""
717 script_admin_info: dict[str, t.Any] = {
718 "icon": "file-code-fill",
719 "display": "hidden",
720 }
721 """Describing the Script module"""
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"""
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"""
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"""
732 search_valid_chars: str = "abcdefghijklmnopqrstuvwxyzäöüß0123456789"
733 """Characters valid for the internal search functionality (all other chars are ignored)"""
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"""
742 _tasks_custom_environment_handler: t.Optional["CustomEnvironmentHandler"] = None
744 @property
745 def tasks_custom_environment_handler(self) -> t.Optional["CustomEnvironmentHandler"]:
746 """
747 Preserve additional environment in deferred tasks.
749 If set, it must be an instance of CustomEnvironmentHandler
750 for serializing/restoring environment data.
751 """
752 return self._tasks_custom_environment_handler
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.")
776 valid_application_ids: list[str] = []
777 """Which application-ids we're supposed to run on"""
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)"""
782 viur2import_blobsource: t.Optional[dict[t.Literal["infoURL", "gsdir"], str]] = None
783 """Configuration to import file blobs from ViUR2"""
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)
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 }
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)
861conf = Conf(
862 strict_mode=os.getenv("VIUR_CORE_CONFIG_STRICT_MODE", "").lower() == "true",
863)