Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/config.py: 89%
340 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-07 19:28 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-07 19:28 +0000
1import datetime
2import hashlib
3import logging
4import os
5import re
6import typing as t
7import warnings
8from pathlib import Path
10import google.auth
12from viur.core.version import __version__
14if t.TYPE_CHECKING: # pragma: no cover
15 from viur.core.bones.text import HtmlBoneConfiguration
16 from viur.core.email import EmailTransport
17 from viur.core.skeleton import SkeletonInstance
18 from viur.core.module import Module
19 from viur.core.tasks import CustomEnvironmentHandler
22# Construct an alias with a generic type to be able to write Multiple[str]
23# TODO: Backward compatible implementation, refactor when viur-core
24# becomes >= Python 3.12 with a type statement (PEP 695)
25_T = t.TypeVar("_T")
26Multiple: t.TypeAlias = list[_T] | tuple[_T] | set[_T] | frozenset[_T] # TODO: Refactor for Python 3.12
29class CaptchaDefaultCredentialsType(t.TypedDict):
30 """Expected type of global captcha credential, see :attr:`Security.captcha_default_credentials`"""
31 sitekey: str
32 secret: str
35class ConfigType:
36 """An abstract class for configurations.
38 It ensures nesting and backward compatibility for the viur-core config
39 """
40 _mapping = {}
41 """Mapping from old dict-key (must not be the entire key in case of nesting) to new attribute name"""
43 _strict_mode = None
44 """Internal strict mode for this instance.
46 Use the property getter and setter to access it!"""
48 _parent = None
49 """Parent config instance"""
51 def __init__(self, *,
52 strict_mode: bool = None,
53 parent: t.Union["ConfigType", None] = None):
54 super().__init__()
55 self._strict_mode = strict_mode
56 self._parent = parent
58 @property
59 def _path(self):
60 """Get the path in dot-Notation to the current config instance."""
61 if self._parent is None:
62 return ""
63 return f"{self._parent._path}{self.__class__.__name__.lower()}."
65 @property
66 def strict_mode(self):
67 """Determine if the config runs in strict mode.
69 In strict mode, the dict-item-access backward compatibility is disabled,
70 only attribute access is allowed.
71 Alias mapping is also disabled. Only the real attribute names are allowed.
73 If self._strict_mode is None, it would inherit the value
74 of the parent.
75 If it's explicitly set to True or False, that value will be used.
76 """
77 if self._strict_mode is not None or self._parent is None:
78 # This config has an explicit value set or there's no parent
79 return self._strict_mode
80 else:
81 # no value set: inherit from the parent
82 return self._parent.strict_mode
84 @strict_mode.setter
85 def strict_mode(self, value: bool | None) -> None:
86 """Setter for the strict mode of the current instance.
88 Does not affect other instances!
89 """
90 if not isinstance(value, (bool, type(None))):
91 raise TypeError(f"Invalid {value=} for strict mode!")
92 self._strict_mode = value
94 def _resolve_mapping(self, key: str) -> str:
95 """Resolve the mapping old dict -> new attribute.
97 This method must not be called in strict mode!
98 It can be overwritten to apply additional mapping.
99 """
100 if key in self._mapping:
101 old, key = key, self._mapping[key]
102 warnings.warn(
103 f"Conf member {self._path}{old} is now {self._path}{key}!",
104 DeprecationWarning,
105 stacklevel=3,
106 )
107 return key
109 def items(self,
110 full_path: bool = False,
111 recursive: bool = True,
112 ) -> t.Iterator[tuple[str, t.Any]]:
113 """Get all setting of this config as key-value mapping.
115 :param full_path: Show prefix oder only the key.
116 :param recursive: Call .items() on ConfigType members (children)?
117 :return:
118 """
119 for key in dir(self):
120 if key.startswith("_"):
121 # skip internals, like _parent and _strict_mode
122 continue
123 value = getattr(self, key)
124 if recursive and isinstance(value, ConfigType):
125 yield from value.items(full_path, recursive)
126 elif key not in dir(ConfigType):
127 if full_path: 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true
128 yield f"{self._path}{key}", value
129 else:
130 yield key, value
132 def get(self, key: str, default: t.Any = None) -> t.Any:
133 """Return an item from the config, if it doesn't exist `default` is returned.
135 :param key: The key for the attribute lookup.
136 :param default: The fallback value.
137 :return: The attribute value or the fallback value.
138 """
139 if self.strict_mode:
140 raise SyntaxError(
141 "In strict mode, the config must not be accessed "
142 "with .get(). Only attribute access is allowed."
143 )
144 try:
145 return getattr(self, key)
146 except (KeyError, AttributeError):
147 return default
149 def __getitem__(self, key: str) -> t.Any:
150 """Support the old dict-like syntax (getter).
152 Not allowed in strict mode.
153 """
154 new_path = f"{self._path}{self._resolve_mapping(key)}"
155 warnings.warn(f"conf uses now attributes! "
156 f"Use conf.{new_path} to access your option",
157 DeprecationWarning,
158 stacklevel=2)
160 if self.strict_mode:
161 raise SyntaxError(
162 f"In strict mode, the config must not be accessed "
163 f"with dict notation. "
164 f"Only attribute access (conf.{new_path}) is allowed."
165 )
167 return getattr(self, key)
169 def __getattr__(self, key: str) -> t.Any:
170 """Resolve dot-notation and name mapping in not strict mode.
172 This method is mostly executed by __getitem__, by the
173 old dict-like access or by attr(conf, "key").
174 In strict mode it does nothing except raising an AttributeError.
175 """
176 if self.strict_mode:
177 raise AttributeError(
178 f"AttributeError: '{self.__class__.__name__}' object has no"
179 f" attribute '{key}' (strict mode is enabled)"
180 )
182 key = self._resolve_mapping(key)
184 # Got an old dict-key and resolve the segment to the first dot (.) as attribute.
185 if "." in key:
186 first, remaining = key.split(".", 1)
187 return getattr(getattr(self, first), remaining)
189 return super().__getattribute__(key)
191 def __setitem__(self, key: str, value: t.Any) -> None:
192 """Support the old dict-like syntax (setter).
194 Not allowed in strict mode.
195 """
196 new_path = f"{self._path}{self._resolve_mapping(key)}"
197 if self.strict_mode:
198 raise SyntaxError(
199 f"In strict mode, the config must not be accessed "
200 f"with dict notation. "
201 f"Only attribute access (conf.{new_path}) is allowed."
202 )
204 # TODO: re-enable?!
205 # Avoid to set conf values to something which is already the default
206 # if key in self and self[key] == value:
207 # msg = f"Setting conf[\"{key}\"] to {value!r} has no effect, as this value has already been set"
208 # warnings.warn(msg, stacklevel=3)
209 # logging.warning(msg, stacklevel=3)
210 # return
212 key = self._resolve_mapping(key)
214 # Got an old dict-key and resolve the segment to the first dot (.) as attribute.
215 if "." in key:
216 first, remaining = key.split(".", 1)
217 if not hasattr(self, first):
218 # TODO: Compatibility, remove it in a future major release!
219 # This segment doesn't exist. Create it
220 logging.warning(f"Creating new type for {first}")
221 setattr(self, first, type(first.capitalize(), (ConfigType,), {})())
222 getattr(self, first)[remaining] = value
223 return
225 return setattr(self, key, value)
227 def __setattr__(self, key: str, value: t.Any) -> None:
228 """Set attributes after applying the old -> new mapping
230 In strict mode it does nothing except a super call
231 for the default object behavior.
232 """
233 if self.strict_mode:
234 return super().__setattr__(key, value)
236 if not self.strict_mode: 236 ↛ 240line 236 didn't jump to line 240 because the condition on line 236 was always true
237 key = self._resolve_mapping(key)
239 # Got an old dict-key and resolve the segment to the first dot (.) as attribute.
240 if "." in key: 240 ↛ 242line 240 didn't jump to line 242 because the condition on line 240 was never true
241 # TODO: Shall we allow this in strict mode as well?
242 first, remaining = key.split(".", 1)
243 return setattr(getattr(self, first), remaining, value)
245 return super().__setattr__(key, value)
247 def __repr__(self) -> str:
248 """Representation of this config"""
249 return f"{self.__class__.__qualname__}({dict(self.items(False, False))})"
252# Some values used more than once below
253_project_id = google.auth.default()[1]
254_app_version = os.getenv("GAE_VERSION")
256# Determine our basePath (as os.getCWD is broken on appengine)
257_project_base_path = Path().absolute()
258_core_base_path = Path(__file__).parent.parent.parent # fixme: this points to site-packages!!!
261class Admin(ConfigType):
262 """Administration tool configuration"""
264 name: str = "ViUR"
265 """Administration tool configuration"""
267 logo: str = ""
268 """URL for the Logo in the Topbar of the VI"""
270 login_background: str = ""
271 """URL for the big Image in the background of the VI Login screen"""
273 login_logo: str = ""
274 """URL for the Logo over the VI Login screen"""
276 color_primary: str = "#d00f1c"
277 """primary color for viur-admin"""
279 color_secondary: str = "#333333"
280 """secondary color for viur-admin"""
282 module_groups: dict[str, dict[t.Literal["name", "icon", "sortindex"], str | int]] = {}
283 """Module Groups for the admin tool
285 Group modules in the sidebar in categories (groups).
287 Example:
288 conf.admin.module_groups = {
289 "content": {
290 "name": "Content",
291 "icon": "file-text-fill",
292 "sortindex": 10,
293 },
294 "shop": {
295 "name": "Shop",
296 "icon": "cart-fill",
297 "sortindex": 20,
298 },
299 }
301 To add a module to one of these groups (e.g. content), add `moduleGroup` to
302 the admin_info of the module:
303 "moduleGroup": "content",
304 """
306 _mapping: dict[str, str] = {
307 "login.background": "login_background",
308 "login.logo": "login_logo",
309 "color.primary": "color_primary",
310 "color.secondary": "color_secondary",
311 }
314class Security(ConfigType):
315 """Security related settings"""
317 force_ssl: bool = True
318 """If true, all requests must be encrypted (ignored on development server)"""
320 no_ssl_check_urls: Multiple[str] = ["/_tasks*", "/ah/*"]
321 """List of URLs for which force_ssl is ignored.
322 Add an asterisk to mark that entry as a prefix (exact match otherwise)"""
324 content_security_policy: t.Optional[dict[str, dict[str, list[str]]]] = {
325 "enforce": {
326 "style-src": ["self", "https://accounts.google.com/gsi/style"],
327 "default-src": ["self"],
328 "img-src": ["self", "storage.googleapis.com"], # Serving-URLs of file-Bones will point here
329 "script-src": ["self", "https://accounts.google.com/gsi/client"],
330 # Required for login with Google
331 "frame-src": ["self", "www.google.com", "drive.google.com", "accounts.google.com"],
332 "form-action": ["self"],
333 "connect-src": ["self", "accounts.google.com"],
334 "upgrade-insecure-requests": [],
335 "object-src": ["none"],
336 }
337 }
338 """If set, viur will emit a CSP http-header with each request. Use security.addCspRule to set this property"""
340 referrer_policy: str = "strict-origin"
341 """Per default, we'll emit Referrer-Policy: strict-origin so no referrers leak to external services
343 See https://www.w3.org/TR/referrer-policy/
344 """
346 permissions_policy: dict[str, list[str]] = {
347 "autoplay": ["self"],
348 "camera": [],
349 "display-capture": [],
350 "document-domain": [],
351 "encrypted-media": [],
352 "fullscreen": [],
353 "geolocation": [],
354 "microphone": [],
355 "publickey-credentials-get": [],
356 "usb": [],
357 }
358 """Include a default permissions-policy.
359 To use the camera or microphone, you'll have to call
360 :meth: securityheaders.setPermissionPolicyDirective to include at least "self"
361 """
363 enable_coep: bool = False
364 """Shall we emit Cross-Origin-Embedder-Policy: require-corp?"""
366 enable_coop: t.Literal[
367 "unsafe-none", "same-origin-allow-popups",
368 "same-origin", "same-origin-plus-COEP"] = "same-origin"
369 """Emit a Cross-Origin-Opener-Policy Header?
371 See https://html.spec.whatwg.org/multipage/browsers.html#cross-origin-opener-policy-value
372 """
374 enable_corp: t.Literal["same-origin", "same-site", "cross-origin"] = "same-origin"
375 """Emit a Cross-Origin-Resource-Policy Header?
377 See https://fetch.spec.whatwg.org/#cross-origin-resource-policy-header
378 """
380 strict_transport_security: t.Optional[str] = "max-age=22118400"
381 """If set, ViUR will emit a HSTS HTTP-header with each request.
382 Use security.enableStrictTransportSecurity to set this property"""
384 x_frame_options: t.Optional[
385 tuple[t.Literal["deny", "sameorigin", "allow-from"], t.Optional[str]]
386 ] = ("sameorigin", None)
387 """If set, ViUR will emit an X-Frame-Options header
389 In case of allow-from, the second parameters must be the host-url.
390 Otherwise, it can be None.
391 """
393 x_xss_protection: t.Optional[bool] = True
394 """ViUR will emit an X-XSS-Protection header if set (the default)"""
396 x_content_type_options: bool = True
397 """ViUR will emit X-Content-Type-Options: nosniff Header unless set to False"""
399 x_permitted_cross_domain_policies: t.Optional[t.Literal["none", "master-only", "by-content-type", "all"]] = "none"
400 """Unless set to logical none; ViUR will emit a X-Permitted-Cross-Domain-Policies with each request"""
402 captcha_default_credentials: t.Optional[CaptchaDefaultCredentialsType] = None
403 """The default sitekey and secret to use for the :class:`CaptchaBone`.
404 If set, must be a dictionary of "sitekey" and "secret".
405 """
407 captcha_enforce_always: bool = False
408 """By default a captcha of the :class:`CaptchaBone` must not be solved on a local development server
409 or by a root user. But for development it can be helpful to test the implementation
410 on a local development server. Setting this flag to True, disables this behavior and
411 enforces always a valid captcha.
412 """
414 password_recovery_key_length: int = 42
415 """Length of the Password recovery key"""
417 closed_system: bool = False
418 """If `True` it activates a mode in which only authenticated users can access all routes."""
420 admin_allowed_paths: t.Iterable[str] = [
421 "vi",
422 "vi/skey",
423 "vi/settings",
424 "vi/user/auth_*",
425 "vi/user/f2_*",
426 "vi/user/getAuthMethods", # FIXME: deprecated, use `login` for this
427 "vi/user/login",
428 ]
429 """Specifies admin tool paths which are being accessible without authenticated user."""
431 closed_system_allowed_paths: t.Iterable[str] = admin_allowed_paths + [
432 "", # index site
433 "json/skey",
434 "json/user/auth_*",
435 "json/user/f2_*",
436 "json/user/getAuthMethods", # FIXME: deprecated, use `login` for this
437 "json/user/login",
438 "user/auth_*",
439 "user/f2_*",
440 "user/getAuthMethods", # FIXME: deprecated, use `login` for this
441 "user/login",
442 ]
443 """Paths that are accessible without authentication in a closed system, see `closed_system` for details."""
445 # CORS Settings
447 cors_origins: t.Iterable[str | re.Pattern] | t.Literal["*"] = []
448 """Allowed origins
449 Access-Control-Allow-Origin
451 Pattern should be case-insensitive, for example:
452 >>> re.compile(r"^http://localhost:(\d{4,5})/?$", flags=re.IGNORECASE)
453 """ # noqa
455 cors_origins_use_wildcard: bool = False
456 """Use * for Access-Control-Allow-Origin -- if possible"""
458 cors_methods: t.Iterable[str] = ["get", "head", "post", "options"] # , "put", "patch", "delete"]
459 """Access-Control-Request-Method"""
461 cors_allow_headers: t.Iterable[str | re.Pattern] | t.Literal["*"] = []
462 """Access-Control-Request-Headers
464 Can also be set for specific @exposed methods with the @cors decorator.
466 Pattern should be case-insensitive, for example:
467 >>> re.compile(r"^X-ViUR-.*$", flags=re.IGNORECASE)
468 """
470 cors_allow_credentials: bool = False
471 """
472 Set Access-Control-Allow-Credentials to true
473 to support fetch requests with credentials: include
474 """
476 cors_max_age: datetime.timedelta | None = None
477 """Allow caching"""
479 _mapping = {
480 "contentSecurityPolicy": "content_security_policy",
481 "referrerPolicy": "referrer_policy",
482 "permissionsPolicy": "permissions_policy",
483 "enableCOEP": "enable_coep",
484 "enableCOOP": "enable_coop",
485 "enableCORP": "enable_corp",
486 "strictTransportSecurity": "strict_transport_security",
487 "xFrameOptions": "x_frame_options",
488 "xXssProtection": "x_xss_protection",
489 "xContentTypeOptions": "x_content_type_options",
490 "xPermittedCrossDomainPolicies": "x_permitted_cross_domain_policies",
491 "captcha_defaultCredentials": "captcha_default_credentials",
492 "captcha.defaultCredentials": "captcha_default_credentials",
493 }
496class Debug(ConfigType):
497 """Several debug flags"""
499 trace: bool = False
500 """If enabled, trace any routing, HTTPExceptions and decorations for debugging and insight"""
502 trace_exceptions: bool = False
503 """If enabled, user-generated exceptions from the viur.core.errors module won't be caught and handled"""
505 trace_external_call_routing: bool = False
506 """If enabled, ViUR will log which (exposed) function are called from outside with what arguments"""
508 trace_internal_call_routing: bool = False
509 """If enabled, ViUR will log which (internal-exposed) function are called from templates with what arguments"""
511 skeleton_from_client: bool = False
512 """If enabled, log errors raises from skeleton.fromClient()"""
514 dev_server_cloud_logging: bool = False
515 """If disabled the local logging will not send with requestLogger to the cloud"""
517 disable_cache: bool = False
518 """If set to true, the decorator @enableCache from viur.core.cache has no effect"""
520 _mapping = {
521 "skeleton.fromClient": "skeleton_from_client",
522 "traceExceptions": "trace_exceptions",
523 "traceExternalCallRouting": "trace_external_call_routing",
524 "traceInternalCallRouting": "trace_internal_call_routing",
525 "skeleton_fromClient": "skeleton_from_client",
526 "disableCache": "disable_cache",
527 }
530class Email(ConfigType):
531 """Email related settings."""
533 log_retention: datetime.timedelta = datetime.timedelta(days=30)
534 """For how long we'll keep successfully send emails in the viur-emails table"""
536 transport_class: "EmailTransport" = None
537 """EmailTransport instance that actually delivers the email using the service provider
538 of choice. See :module:`core.email` for more details
539 """
541 send_from_local_development_server: bool = False
542 """If set, we'll enable sending emails from the local development server.
543 Otherwise, they'll just be logged.
544 """
546 recipient_override: str | list[str] | t.Callable[[], str | list[str]] | t.Literal[False] = None
547 """If set, all outgoing emails will be sent to this address
548 (overriding the 'dests'-parameter in :meth:`core.email.send_email`)
549 """
551 sender_default: str = f"viur@{_project_id}.appspotmail.com"
552 """This sender is used by default for emails.
553 It can be overridden for a specific email by passing the `sender` argument
554 to :meth:`core.email.send_email` or for all emails with :attr:`sender_override`.
555 """
557 sender_override: str | None = None
558 """If set, this sender will be used, regardless of what the templates advertise as sender"""
560 admin_recipients: str | list[str] | t.Callable[[], str | list[str]] = None
561 """Sets recipients for mails send with :meth:`core.email.send_email_to_admins`.
562 If not set, all root users will be used."""
564 _mapping = {
565 "logRetention": "log_retention",
566 "transportClass": "transport_class",
567 "sendFromLocalDevelopmentServer": "send_from_local_development_server",
568 "recipientOverride": "recipient_override",
569 "senderOverride": "sender_override",
570 "sendInBlue.apiKey": "sendinblue_api_key",
571 "sendInBlue.thresholds": "sendinblue_thresholds",
572 }
575class I18N(ConfigType):
576 """All i18n, multilang related settings."""
578 available_languages: Multiple[str] = ["en"]
579 """List of language-codes, which are valid for this application"""
581 default_language: str = "en"
582 """Unless overridden by the Project: Use english as default language"""
584 domain_language_mapping: dict[str, str] = {}
585 """Maps Domains to alternative default languages"""
587 language_alias_map: dict[str, str] = {}
588 """Allows mapping of certain languages to one translation (i.e. us->en)"""
590 language_method: t.Literal["session", "url", "domain", "header"] = "session"
591 """Defines how translations are applied:
592 - session: Per Session
593 - url: inject language prefix in url
594 - domain: one domain per language
595 - header: Per Http-Header
596 """
598 language_module_map: dict[str, dict[str, str]] = {}
599 """Maps modules to their translation (if set)"""
601 @property
602 def available_dialects(self) -> list[str]:
603 """Main languages and language aliases"""
604 # Use a dict to keep the order and remove duplicates
605 res = dict.fromkeys(self.available_languages)
606 res |= self.language_alias_map
607 return list(res.keys())
609 add_missing_translations: bool = False
610 """Add missing translation into datastore.
612 If a key is not found in the translation table when a translation is
613 rendered, a database entry is created with the key and hint and
614 default value (if set) so that the translations
615 can be entered in the administration.
616 """
619class User(ConfigType):
620 """User, session, login related settings"""
622 access_rights: Multiple[str] = [
623 "root",
624 "admin",
625 "scriptor",
626 ]
627 """Additional access flags available for users on this project.
629 There are three default flags:
630 - `root` is allowed to view/add/edit/delete any module, regardless of role or other settings
631 - `admin` is allowed to use the ViUR administration tool
632 - `scriptor` is allowed to use the ViUR scripting features directly within the admin
633 This does not affect scriptor actions which are configured for modules, as they allow for
634 fine grained usage rule definitions.
635 """
637 roles: dict[str, str] = {
638 "custom": "Custom",
639 "user": "User",
640 "viewer": "Viewer",
641 "editor": "Editor",
642 "admin": "Administrator",
643 }
644 """User roles available on this project.
646 The roles can be individually defined per module, see `Module.roles`.
648 The default roles can be described as follows:
650 - `custom` for users with a custom-settings via the `User.access`-bone; includes root users.
651 - `user` for users without any additonal rights. They can log-in and view themselves, or particular modules which
652 just check for authenticated users.
653 - `viewer` for users who should only view content.
654 - `editor` for users who are allowed to edit particular content. They mostly can `view` and `edit`, but not `add`
655 or `delete`.
656 - `admin` for users with administration privileges. They can edit any data, but still aren't `root`.
658 The preset roles are for guidiance, and already fit to most projects.
659 """
661 session_life_time: int = 60 * 60
662 """Default is 60 minutes lifetime for ViUR sessions"""
664 session_persistent_fields_on_login: Multiple[str] = ["language"]
665 """If set, these Fields will survive the session.reset() called on user/login"""
667 session_persistent_fields_on_logout: Multiple[str] = ["language"]
668 """If set, these Fields will survive the session.reset() called on user/logout"""
670 max_password_length: int = 512
671 """Prevent Denial of Service attacks using large inputs for pbkdf2"""
673 otp_issuer: t.Optional[str] = None
674 """The name of the issuer for the opt token"""
676 google_client_id: t.Optional[str] = None
677 """OAuth Client ID for Google Login"""
679 google_gsuite_domains: list[str] = []
680 """A list of domains. When a user signs in for the first time with a
681 Google account using Google OAuth sign-in, and the user's email address
682 belongs to one of the listed domains, a user account (UserSkel) is created.
683 If the user's email address belongs to any other domain,
684 no account is created."""
687class Instance(ConfigType):
688 """All app instance related settings information"""
689 app_version: str = _app_version
690 """Name of this version as deployed to the appengine"""
692 core_base_path: Path = _core_base_path
693 """The base path of the core, can be used to find file in the core folder"""
695 is_dev_server: bool = os.getenv("GAE_ENV") == "localdev"
696 """Determine whether instance is running on a local development server"""
698 project_base_path: Path = _project_base_path
699 """The base path of the project, can be used to find file in the project folder"""
701 project_id: str = _project_id
702 """The instance's project ID"""
704 version_hash: str = hashlib.sha256(f"{_app_version}{project_id}".encode("UTF-8")).hexdigest()[:10]
705 """Version hash that does not reveal the actual version name, can be used for cache-busting static resources"""
708class Conf(ConfigType):
709 """Conf class wraps the conf dict and allows to handle
710 deprecated keys or other special operations.
711 """
713 bone_boolean_str2true: Multiple[str | int] = ("true", "yes", "1")
714 """Allowed values that define a str to evaluate to true"""
716 bone_html_default_allow: "HtmlBoneConfiguration" = {
717 "validTags": [
718 "a",
719 "abbr",
720 "b",
721 "blockquote",
722 "br",
723 "div",
724 "em",
725 "h1",
726 "h2",
727 "h3",
728 "h4",
729 "h5",
730 "h6",
731 "hr",
732 "i",
733 "img",
734 "li",
735 "ol",
736 "p",
737 "span",
738 "strong",
739 "sub",
740 "sup",
741 "table",
742 "tbody",
743 "td",
744 "tfoot",
745 "th",
746 "thead",
747 "tr",
748 "u",
749 "ul",
750 ],
751 "validAttrs": {
752 "a": [
753 "href",
754 "target",
755 "title",
756 ],
757 "abbr": [
758 "title",
759 ],
760 "blockquote": [
761 "cite",
762 ],
763 "img": [
764 "src",
765 "alt",
766 "title",
767 ],
768 "p": [
769 "data-indent",
770 ],
771 "span": [
772 "title",
773 ],
774 "td": [
775 "colspan",
776 "rowspan",
777 ],
779 },
780 "validStyles": [
781 "color",
782 ],
783 "validClasses": [
784 "vitxt-*",
785 "viur-txt-*"
786 ],
787 "singleTags": [
788 "br",
789 "hr",
790 "img",
791 ]
792 }
793 """
794 A dictionary containing default configurations for handling HTML content in TextBone instances.
795 """
797 cache_environment_key: t.Optional[t.Callable[[], str]] = None
798 """If set, this function will be called for each cache-attempt
799 and the result will be included in the computed cache-key"""
801 # FIXME VIUR4: REMOVE ALL COMPATIBILITY MODES!
802 compatibility: Multiple[str] = [
803 "json.bone.structure.camelcasenames", # use camelCase attribute names (see #637 for details)
804 "json.bone.structure.keytuples", # use classic structure notation: `"structure = [["key", {...}] ...]` (#649)
805 "json.bone.structure.inlists", # dump skeleton structure with every JSON list response (#774 for details)
806 "tasks.periodic.useminutes", # Interpret int/float values for @PeriodicTask as minutes
807 # instead of seconds (#1133 for details)
808 "bone.select.structure.values.keytuple", # render old-style tuple-list in SelectBone's values structure (#1203)
809 ]
810 """Backward compatibility flags; Remove to enforce new style."""
812 db_engine: str = "viur.datastore"
813 """Database engine module"""
815 error_handler: t.Callable[[Exception], str] | None = None
816 """If set, ViUR calls this function instead of rendering the viur.errorTemplate if an exception occurs"""
818 error_logo: str = None
819 """Path to a logo (static file). Will be used for the default error template"""
821 static_embed_svg_path: str = "/static/svgs/"
822 """Path to the static SVGs folder. Will be used by the jinja-renderer-method: embedSvg"""
824 file_hmac_key: str = None
825 """Hmac-Key used to sign download urls - set automatically"""
827 # TODO: separate this type hints and use it in the File module as well
828 file_derivations: dict[str, t.Callable[["SkeletonInstance", dict, dict], list[tuple[str, float, str, t.Any]]]] = {}
829 """Call-Map for file pre-processors"""
831 file_thumbnailer_url: t.Optional[str] = None
832 # TODO: """docstring"""
834 main_app: "Module" = None
835 """Reference to our pre-build Application-Instance"""
837 main_resolver: dict[str, dict] = None
838 """Dictionary for Resolving functions for URLs"""
840 max_post_params_count: int = 250
841 """Upper limit of the amount of parameters we accept per request. Prevents Hash-Collision-Attacks"""
843 param_filter_function: t.Callable[[str, str], bool] = lambda _, key, value: key.startswith("_")
844 """
845 Function which decides if a request parameter should be used or filtered out.
846 Returning True means to filter out.
847 """
849 moduleconf_admin_info: dict[str, t.Any] = {
850 "icon": "gear-fill",
851 "display": "hidden",
852 }
853 """Describing the internal ModuleConfig-module"""
855 script_admin_info: dict[str, t.Any] = {
856 "icon": "file-code-fill",
857 "display": "hidden",
858 }
859 """Describing the Script module"""
861 render_html_download_url_expiration: t.Optional[float | int] = None
862 """The default duration, for which downloadURLs generated by the html renderer will stay valid"""
864 render_json_download_url_expiration: t.Optional[float | int] = None
865 """The default duration, for which downloadURLs generated by the json renderer will stay valid"""
867 request_preprocessor: t.Optional[t.Callable[[str], str]] = None
868 """Allows the application to register a function that's called before the request gets routed"""
870 search_valid_chars: str = "abcdefghijklmnopqrstuvwxyzäöüß0123456789"
871 """Characters valid for the internal search functionality (all other chars are ignored)"""
873 skeleton_search_path: Multiple[str] = [
874 "/skeletons/", # skeletons of the project
875 "/viur/core/", # system-defined skeletons of viur-core
876 "/viur-core/core/" # system-defined skeletons of viur-core, only used by editable installation
877 ]
878 """Priority, in which skeletons are loaded"""
880 _tasks_custom_environment_handler: t.Optional["CustomEnvironmentHandler"] = None
882 @property
883 def tasks_custom_environment_handler(self) -> t.Optional["CustomEnvironmentHandler"]:
884 """
885 Preserve additional environment in deferred tasks.
887 If set, it must be an instance of CustomEnvironmentHandler
888 for serializing/restoring environment data.
889 """
890 return self._tasks_custom_environment_handler
892 @tasks_custom_environment_handler.setter
893 def tasks_custom_environment_handler(self, value: "CustomEnvironmentHandler") -> None:
894 from .tasks import CustomEnvironmentHandler
895 if isinstance(value, CustomEnvironmentHandler) or value is None:
896 self._tasks_custom_environment_handler = value
897 elif isinstance(value, tuple):
898 if len(value) != 2:
899 raise ValueError(f"Expected a (serialize_env_func, restore_env_func) pair")
900 warnings.warn(
901 f"tuple is deprecated, please provide a CustomEnvironmentHandler object!",
902 DeprecationWarning, stacklevel=2,
903 )
904 # Construct an CustomEnvironmentHandler class on the fly to be backward compatible
905 cls = type("ProjectCustomEnvironmentHandler", (CustomEnvironmentHandler,),
906 # serialize and restore will be bound methods.
907 # Therefore, consume the self argument with lambda.
908 {"serialize": lambda self: value[0](),
909 "restore": lambda self, obj: value[1](obj)})
910 self._tasks_custom_environment_handler = cls()
911 else:
912 raise ValueError(f"Invalid type {type(value)}. Expected a CustomEnvironmentHandler object.")
914 valid_application_ids: list[str] = []
915 """Which application-ids we're supposed to run on"""
917 version: tuple[int, int, int] = tuple(int(part) if part.isdigit() else part for part in __version__.split(".", 3))
918 """Semantic version number of viur-core as a tuple of 3 (major, minor, patch-level)"""
920 viur2import_blobsource: t.Optional[dict[t.Literal["infoURL", "gsdir"], str]] = None
921 """Configuration to import file blobs from ViUR2"""
923 def __init__(self, strict_mode: bool = False):
924 super().__init__()
925 self._strict_mode = strict_mode
926 self.admin = Admin(parent=self)
927 self.security = Security(parent=self)
928 self.debug = Debug(parent=self)
929 self.email = Email(parent=self)
930 self.i18n = I18N(parent=self)
931 self.user = User(parent=self)
932 self.instance = Instance(parent=self)
934 _mapping = {
935 # debug
936 "viur.dev_server_cloud_logging": "debug.dev_server_cloud_logging",
937 "viur.disable_cache": "debug.disable_cache",
938 # i18n
939 "viur.availableLanguages": "i18n.available_languages",
940 "viur.defaultLanguage": "i18n.default_language",
941 "viur.domainLanguageMapping": "i18n.domain_language_mapping",
942 "viur.languageAliasMap": "i18n.language_alias_map",
943 "viur.languageMethod": "i18n.language_method",
944 "viur.languageModuleMap": "i18n.language_module_map",
945 # user
946 "viur.accessRights": "user.access_rights",
947 "viur.maxPasswordLength": "user.max_password_length",
948 "viur.otp.issuer": "user.otp_issuer",
949 "viur.session.lifeTime": "user.session_life_time",
950 "viur.session.persistentFieldsOnLogin": "user.session_persistent_fields_on_login",
951 "viur.session.persistentFieldsOnLogout": "user.session_persistent_fields_on_logout",
952 "viur.user.roles": "user.roles",
953 "viur.user.google.clientID": "user.google_client_id",
954 "viur.user.google.gsuiteDomains": "user.google_gsuite_domains",
955 # instance
956 "viur.instance.app_version": "instance.app_version",
957 "viur.instance.core_base_path": "instance.core_base_path",
958 "viur.instance.is_dev_server": "instance.is_dev_server",
959 "viur.instance.project_base_path": "instance.project_base_path",
960 "viur.instance.project_id": "instance.project_id",
961 "viur.instance.version_hash": "instance.version_hash",
962 # security
963 "viur.forceSSL": "security.force_ssl",
964 "viur.noSSLCheckUrls": "security.no_ssl_check_urls",
965 # old viur-prefix
966 "viur.cacheEnvironmentKey": "cache_environment_key",
967 "viur.contentSecurityPolicy": "content_security_policy",
968 "viur.bone.boolean.str2true": "bone_boolean_str2true",
969 "viur.db.engine": "db_engine",
970 "viur.errorHandler": "error_handler",
971 "viur.static.embedSvg.path": "static_embed_svg_path",
972 "viur.file.hmacKey": "file_hmac_key",
973 "viur.file_hmacKey": "file_hmac_key",
974 "viur.file.derivers": "file_derivations",
975 "viur.file.thumbnailerURL": "file_thumbnailer_url",
976 "viur.mainApp": "main_app",
977 "viur.mainResolver": "main_resolver",
978 "viur.maxPostParamsCount": "max_post_params_count",
979 "viur.moduleconf.admin_info": "moduleconf_admin_info",
980 "viur.script.admin_info": "script_admin_info",
981 "viur.render.html.downloadUrlExpiration": "render_html_download_url_expiration",
982 "viur.downloadUrlFor.expiration": "render_html_download_url_expiration",
983 "viur.render.json.downloadUrlExpiration": "render_json_download_url_expiration",
984 "viur.requestPreprocessor": "request_preprocessor",
985 "viur.searchValidChars": "search_valid_chars",
986 "viur.skeleton.searchPath": "skeleton_search_path",
987 "viur.tasks.customEnvironmentHandler": "tasks_custom_environment_handler",
988 "viur.validApplicationIDs": "valid_application_ids",
989 "viur.viur2import.blobsource": "viur2import_blobsource",
990 }
992 def _resolve_mapping(self, key: str) -> str:
993 """Additional mapping for new sub confs."""
994 if key.startswith("viur.") and key not in self._mapping:
995 key = key.removeprefix("viur.")
996 return super()._resolve_mapping(key)
999conf = Conf(
1000 strict_mode=os.getenv("VIUR_CORE_CONFIG_STRICT_MODE", "").lower() == "true",
1001)