viur-core 3.9.0.dev3__tar.gz → 3.9.0.dev5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/PKG-INFO +2 -1
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/pyproject.toml +1 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/__init__.py +3 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/__init__.py +5 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/base.py +25 -2
- viur_core-3.9.0.dev5/src/viur/core/bones/captcha.py +177 -0
- viur_core-3.9.0.dev5/src/viur/core/bones/code.py +95 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/email.py +4 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/randomslice.py +2 -2
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/relational.py +17 -10
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/spatial.py +12 -6
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/string.py +4 -3
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/text.py +1 -1
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/config.py +4 -7
- viur_core-3.9.0.dev5/src/viur/core/contrib/__init__.py +43 -0
- viur_core-3.9.0.dev5/src/viur/core/contrib/loginkey.py +115 -0
- viur_core-3.9.0.dev5/src/viur/core/contrib/ratelimit.py +123 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/db/__init__.py +2 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/db/cache.py +1 -3
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/db/query.py +38 -25
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/db/transport.py +37 -14
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/db/types.py +7 -1
- viur_core-3.9.0.dev5/src/viur/core/modules/email.py +98 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/modules/file.py +4 -3
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/modules/script.py +8 -5
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/modules/translation.py +2 -2
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/modules/user.py +6 -6
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/prototypes/skelmodule.py +8 -10
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/vi/__init__.py +25 -2
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/request.py +55 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/securityheaders.py +14 -3
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/session.py +28 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/__init__.py +2 -1
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/instance.py +59 -7
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/meta.py +36 -1
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/skeleton.py +18 -5
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/tasks.py +2 -8
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/utils.py +20 -10
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/tasks.py +1 -1
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/utils/__init__.py +4 -1
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/version.py +1 -1
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur_core.egg-info/PKG-INFO +2 -1
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur_core.egg-info/SOURCES.txt +7 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur_core.egg-info/requires.txt +1 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/tests/test_config.py +0 -1
- viur_core-3.9.0.dev5/tests/test_db.py +81 -0
- viur_core-3.9.0.dev5/tests/test_decorators.py +271 -0
- viur_core-3.9.0.dev5/tests/test_errors.py +122 -0
- viur_core-3.9.0.dev5/tests/test_utils.py +299 -0
- viur_core-3.9.0.dev3/src/viur/core/bones/captcha.py +0 -130
- viur_core-3.9.0.dev3/tests/test_db.py +0 -29
- viur_core-3.9.0.dev3/tests/test_utils.py +0 -156
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/LICENSE +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/README.md +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/setup.cfg +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/boolean.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/color.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/credential.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/date.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/file.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/image.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/json.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/key.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/numeric.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/password.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/phone.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/raw.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/record.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/select.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/selectcountry.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/sortindex.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/spam.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/treeleaf.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/treenode.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/uid.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/uri.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/bones/user.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/cache.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/current.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/db/config.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/db/overrides.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/db/utils.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/decorators.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/email.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/errors.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/i18n.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/languages/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/languages/de.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/languages/en.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/logging.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/module.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/modules/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/modules/formmailer.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/modules/history.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/modules/moduleconf.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/modules/page.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/modules/site.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/pagination.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/prototypes/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/prototypes/instanced_module.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/prototypes/list.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/prototypes/singleton.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/prototypes/tree.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/ratelimit.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/abstract.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/html/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/html/default.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/date.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/debug.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/regex.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/session.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/strings.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/tests.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/viur.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/html/utils.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/json/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/render/json/default.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/scripts/viur_migrate.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/secret.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/securitykey.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/adapter.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/relskel.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/template/error.html +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/template/vi_user_google_login.html +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/utils/json.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/utils/parse.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur/core/utils/string.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur_core.egg-info/dependency_links.txt +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur_core.egg-info/entry_points.txt +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev5}/src/viur_core.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: viur-core
|
|
3
|
-
Version: 3.9.0.
|
|
3
|
+
Version: 3.9.0.dev5
|
|
4
4
|
Summary: The core component of ViUR, a development framework for Google App Engine
|
|
5
5
|
Author-email: Mausbrand Informationssysteme GmbH <devs@viur.dev>
|
|
6
6
|
Maintainer-email: Jan Max Meyer <jm@mausbrand.de>
|
|
@@ -48,6 +48,7 @@ Requires-Dist: google-cloud-bigquery~=3.0
|
|
|
48
48
|
Requires-Dist: google-cloud-datastore~=2.0
|
|
49
49
|
Requires-Dist: google-cloud-iam~=2.0
|
|
50
50
|
Requires-Dist: google-cloud-logging~=3.0
|
|
51
|
+
Requires-Dist: google-cloud-recaptcha-enterprise~=1.0
|
|
51
52
|
Requires-Dist: google-cloud-secret-manager~=2.0
|
|
52
53
|
Requires-Dist: google-cloud-storage~=2.0
|
|
53
54
|
Requires-Dist: google-cloud-tasks~=2.0
|
|
@@ -18,6 +18,7 @@ from types import ModuleType
|
|
|
18
18
|
from viur.core import i18n, request, utils
|
|
19
19
|
from viur.core.config import conf
|
|
20
20
|
from viur.core.decorators import access, exposed, force_post, force_ssl, internal_exposed, skey
|
|
21
|
+
from viur.core.request import before_request, after_request
|
|
21
22
|
from viur.core.i18n import translate
|
|
22
23
|
from viur.core.module import Method, Module
|
|
23
24
|
import inspect
|
|
@@ -60,6 +61,8 @@ __all__ = [
|
|
|
60
61
|
"PeriodicTask",
|
|
61
62
|
# Decorators
|
|
62
63
|
"access",
|
|
64
|
+
"after_request",
|
|
65
|
+
"before_request",
|
|
63
66
|
"exposed",
|
|
64
67
|
"force_post",
|
|
65
68
|
"force_ssl",
|
|
@@ -13,6 +13,7 @@ from .base import (
|
|
|
13
13
|
UniqueValue,
|
|
14
14
|
)
|
|
15
15
|
from .boolean import BooleanBone
|
|
16
|
+
from .code import CodeBone, JinjaBone, LogicsBone, PythonBone
|
|
16
17
|
from .captcha import CaptchaBone
|
|
17
18
|
from .color import ColorBone
|
|
18
19
|
from .credential import CredentialBone
|
|
@@ -51,6 +52,10 @@ __all = [
|
|
|
51
52
|
"BaseBone",
|
|
52
53
|
"BooleanBone",
|
|
53
54
|
"CaptchaBone",
|
|
55
|
+
"CodeBone",
|
|
56
|
+
"JinjaBone",
|
|
57
|
+
"LogicsBone",
|
|
58
|
+
"PythonBone",
|
|
54
59
|
"CloneBehavior",
|
|
55
60
|
"CloneStrategy",
|
|
56
61
|
"ColorBone",
|
|
@@ -798,18 +798,23 @@ class BaseBone(object):
|
|
|
798
798
|
if self.languages and isinstance(self.required, (list, tuple)):
|
|
799
799
|
missing = set(self.required).difference(filled_languages)
|
|
800
800
|
if missing:
|
|
801
|
-
|
|
801
|
+
result_errors = [
|
|
802
802
|
ReadFromClientError(ReadFromClientErrorSeverity.Empty, fieldPath=[lang])
|
|
803
803
|
for lang in missing
|
|
804
804
|
]
|
|
805
|
+
self.after_from_client(skel, name, result_errors)
|
|
806
|
+
return result_errors or None
|
|
805
807
|
|
|
806
808
|
if isEmpty:
|
|
807
|
-
|
|
809
|
+
result_errors = [ReadFromClientError(ReadFromClientErrorSeverity.Empty)]
|
|
810
|
+
self.after_from_client(skel, name, result_errors)
|
|
811
|
+
return result_errors or None
|
|
808
812
|
|
|
809
813
|
# Check multiple constraints on demand
|
|
810
814
|
if self.multiple and isinstance(self.multiple, MultipleConstraints):
|
|
811
815
|
errors.extend(self._validate_multiple_contraints(self.multiple, skel, name))
|
|
812
816
|
|
|
817
|
+
self.after_from_client(skel, name, errors)
|
|
813
818
|
return errors or None
|
|
814
819
|
|
|
815
820
|
def _get_single_destinct_hash(self, value) -> t.Any:
|
|
@@ -1387,6 +1392,24 @@ class BaseBone(object):
|
|
|
1387
1392
|
"""
|
|
1388
1393
|
pass
|
|
1389
1394
|
|
|
1395
|
+
def after_from_client(self, skel: "SkeletonInstance", name: str, errors: list[ReadFromClientError]) -> None:
|
|
1396
|
+
"""
|
|
1397
|
+
Called at the end of :meth:`fromClient` after ``skel[name]`` has been set and all
|
|
1398
|
+
validation (including multiple-constraints) has run.
|
|
1399
|
+
|
|
1400
|
+
Override to post-process or normalize ``skel[name]`` in-place, or to add/remove
|
|
1401
|
+
entries from ``errors``. Always called when the field was part of the submitted data
|
|
1402
|
+
(i.e. ``skel[name]`` has been written), regardless of whether errors occurred.
|
|
1403
|
+
The ``NotSet`` early-return (field absent from request) is the only case where this
|
|
1404
|
+
hook is *not* called.
|
|
1405
|
+
|
|
1406
|
+
:param skel: The skeleton instance whose bone value was just read.
|
|
1407
|
+
:param name: The attribute name of this bone within the skeleton.
|
|
1408
|
+
:param errors: Mutable list of :class:`ReadFromClientError` collected so far.
|
|
1409
|
+
Changes here affect the return value of :meth:`fromClient`.
|
|
1410
|
+
"""
|
|
1411
|
+
pass
|
|
1412
|
+
|
|
1390
1413
|
def clone_value(self, skel: "SkeletonInstance", src_skel: "SkeletonInstance", bone_name: str) -> None:
|
|
1391
1414
|
"""Clone / Set the value for this bone depending on :attr:`clone_behavior`"""
|
|
1392
1415
|
match self.clone_behavior.strategy:
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing as t
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
from viur.core import conf, current
|
|
6
|
+
from viur.core.bones.base import BaseBone, ReadFromClientError, ReadFromClientErrorSeverity
|
|
7
|
+
|
|
8
|
+
from google.cloud import recaptchaenterprise_v1
|
|
9
|
+
from google.cloud.recaptchaenterprise_v1 import Assessment
|
|
10
|
+
|
|
11
|
+
if t.TYPE_CHECKING:
|
|
12
|
+
from viur.core.skeleton import SkeletonInstance
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CaptchaBone(BaseBone):
|
|
16
|
+
r"""
|
|
17
|
+
The CaptchaBone validates reCAPTCHA Enterprise tokens to protect forms from bots.
|
|
18
|
+
|
|
19
|
+
It uses the Google reCAPTCHA Enterprise API and supports both invisible (v3-style score-based)
|
|
20
|
+
and visible (checkbox widget) challenges via the ``render_challenge`` parameter.
|
|
21
|
+
|
|
22
|
+
The token is submitted by the client as the bone's field value and verified server-side
|
|
23
|
+
against the configured site key. A configurable score threshold determines whether
|
|
24
|
+
invisible challenges pass.
|
|
25
|
+
|
|
26
|
+
.. seealso::
|
|
27
|
+
|
|
28
|
+
`Google reCAPTCHA Enterprise setup
|
|
29
|
+
<https://cloud.google.com/recaptcha/docs/set-up-non-google-cloud-environments-api-keys>`
|
|
30
|
+
for creating a site key and enabling the API.
|
|
31
|
+
|
|
32
|
+
Option :attr:`core.config.Security.captcha_default_public_key`
|
|
33
|
+
for global security settings.
|
|
34
|
+
|
|
35
|
+
Option :attr:`core.config.Security.captcha_enforce_always`
|
|
36
|
+
to enforce validation even on development servers.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
type = "captcha"
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
public_key: str = None,
|
|
45
|
+
score_threshold: float = 0.5,
|
|
46
|
+
render_challenge: bool = False,
|
|
47
|
+
recaptcha_action: str = "",
|
|
48
|
+
**kwargs: t.Any
|
|
49
|
+
):
|
|
50
|
+
"""
|
|
51
|
+
Initializes a new CaptchaBone.
|
|
52
|
+
|
|
53
|
+
:param public_key: The reCAPTCHA Enterprise site key shown to the client.
|
|
54
|
+
Can be omitted if set globally via :attr:`core.config.Security.captcha_default_public_key`.
|
|
55
|
+
:param score_threshold: Minimum score (0–1) required for invisible challenges to pass.
|
|
56
|
+
Ignored when ``render_challenge`` is ``True``.
|
|
57
|
+
:param render_challenge: If ``True``, renders a visible checkbox widget instead of
|
|
58
|
+
running an invisible background check.
|
|
59
|
+
:param recaptcha_action: The action name passed to reCAPTCHA for analytics and scoring.
|
|
60
|
+
Should match the action used on the client side.
|
|
61
|
+
"""
|
|
62
|
+
if "publicKey" in kwargs:
|
|
63
|
+
warnings.warn("publicKey parameter is deprecated, please use public_key",
|
|
64
|
+
DeprecationWarning, stacklevel=2)
|
|
65
|
+
public_key = kwargs.pop("publicKey")
|
|
66
|
+
super().__init__(**kwargs)
|
|
67
|
+
if not public_key and conf.security.captcha_default_public_key:
|
|
68
|
+
public_key = conf.security.captcha_default_public_key
|
|
69
|
+
if not public_key:
|
|
70
|
+
raise ValueError("CaptchaBone requires either a public_key or conf.security.captcha_default_public_key")
|
|
71
|
+
|
|
72
|
+
self.public_key = public_key
|
|
73
|
+
|
|
74
|
+
if not (0 < score_threshold <= 1):
|
|
75
|
+
raise ValueError("score_threshold must be between 0 and 1.")
|
|
76
|
+
self.render_challenge = render_challenge
|
|
77
|
+
self.recaptcha_action = recaptcha_action
|
|
78
|
+
self.score_threshold = score_threshold
|
|
79
|
+
self.required = True
|
|
80
|
+
|
|
81
|
+
def serialize(self, skel: "SkeletonInstance", name: str, parentIndexed: bool) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Serializing the Captcha bone is not possible so it return False
|
|
84
|
+
"""
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
def unserialize(self, skel: "SkeletonInstance", name) -> t.Literal[True]:
|
|
88
|
+
"""
|
|
89
|
+
Stores the public_key in the SkeletonInstance
|
|
90
|
+
|
|
91
|
+
:param skel: The target :class:`SkeletonInstance`.
|
|
92
|
+
:param name: The name of the CaptchaBone in the :class:`SkeletonInstance`.
|
|
93
|
+
|
|
94
|
+
:returns: boolean, that is true, as the Captcha bone is always unserialized successfully.
|
|
95
|
+
"""
|
|
96
|
+
skel.accessedValues[name] = self.public_key
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
def fromClient(self, skel: "SkeletonInstance", name: str, data: dict) -> None | list[ReadFromClientError]:
|
|
100
|
+
"""
|
|
101
|
+
Load the reCAPTCHA token from the provided data and validate it with the help of the API.
|
|
102
|
+
|
|
103
|
+
reCAPTCHA provides the token via callback usually as "g-recaptcha-response",
|
|
104
|
+
but to fit into the skeleton logic, we support both names.
|
|
105
|
+
So the token can be provided as "g-recaptcha-response" or the name of the CaptchaBone in the Skeleton.
|
|
106
|
+
While the latter one is the preferred name.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
if not conf.security.captcha_enforce_always and conf.instance.is_dev_server:
|
|
110
|
+
logging.info("Skipping captcha validation on development server")
|
|
111
|
+
return None
|
|
112
|
+
if not conf.security.captcha_enforce_always and (user := current.user.get()) and "root" in user["access"]:
|
|
113
|
+
logging.info("Skipping captcha validation for root user")
|
|
114
|
+
return None # Don't bother trusted users with this (not supported by admin/vi anyway)
|
|
115
|
+
|
|
116
|
+
client = recaptchaenterprise_v1.RecaptchaEnterpriseServiceClient()
|
|
117
|
+
|
|
118
|
+
# Set the attributes of the event to be tracked.
|
|
119
|
+
event = recaptchaenterprise_v1.Event()
|
|
120
|
+
event.site_key = self.public_key
|
|
121
|
+
if name in data:
|
|
122
|
+
event.token = data[name]
|
|
123
|
+
else:
|
|
124
|
+
return [ReadFromClientError(
|
|
125
|
+
ReadFromClientErrorSeverity.NotSet,
|
|
126
|
+
"Token not set"
|
|
127
|
+
)]
|
|
128
|
+
assessment = recaptchaenterprise_v1.Assessment()
|
|
129
|
+
assessment.event = event
|
|
130
|
+
|
|
131
|
+
project_name = f"projects/{conf.instance.project_id}"
|
|
132
|
+
|
|
133
|
+
# Create the assessment request.
|
|
134
|
+
request = recaptchaenterprise_v1.CreateAssessmentRequest()
|
|
135
|
+
request.assessment = assessment
|
|
136
|
+
request.parent = project_name
|
|
137
|
+
|
|
138
|
+
response = client.create_assessment(request)
|
|
139
|
+
|
|
140
|
+
if not response.token_properties.valid:
|
|
141
|
+
logging.info(
|
|
142
|
+
"The CreateAssessment call failed because the token was "
|
|
143
|
+
+ "invalid for the following reasons: "
|
|
144
|
+
+ str(response.token_properties.invalid_reason)
|
|
145
|
+
)
|
|
146
|
+
return [ReadFromClientError(
|
|
147
|
+
ReadFromClientErrorSeverity.Invalid,
|
|
148
|
+
"Invalid Token"
|
|
149
|
+
)]
|
|
150
|
+
|
|
151
|
+
# Check if the expected action was executed.
|
|
152
|
+
if response.token_properties.action != self.recaptcha_action:
|
|
153
|
+
logging.info(
|
|
154
|
+
"The action attribute in your reCAPTCHA tag does not match the action you are expecting to score"
|
|
155
|
+
)
|
|
156
|
+
return [ReadFromClientError(
|
|
157
|
+
ReadFromClientErrorSeverity.Invalid,
|
|
158
|
+
f"Invalid Action: {self.recaptcha_action}"
|
|
159
|
+
)]
|
|
160
|
+
else:
|
|
161
|
+
# Retrieve the risk score and reasons.
|
|
162
|
+
# For more information on interpreting the assessment, see:
|
|
163
|
+
# https://cloud.google.com/recaptcha/docs/interpret-assessment
|
|
164
|
+
if response.risk_analysis.score < self.score_threshold:
|
|
165
|
+
return [ReadFromClientError(
|
|
166
|
+
ReadFromClientErrorSeverity.Invalid,
|
|
167
|
+
f"Invalid Captcha: {response.risk_analysis.score}"
|
|
168
|
+
)]
|
|
169
|
+
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def structure(self) -> dict:
|
|
173
|
+
return super().structure() | {
|
|
174
|
+
"public_key": self.public_key,
|
|
175
|
+
"render_challenge": self.render_challenge,
|
|
176
|
+
"action": self.recaptcha_action
|
|
177
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import jinja2
|
|
3
|
+
import logics
|
|
4
|
+
from viur.core.bones.raw import RawBone
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CodeBone(RawBone):
|
|
8
|
+
"""
|
|
9
|
+
Stores source code with optional language-specific syntax validation.
|
|
10
|
+
|
|
11
|
+
The ``syntax`` parameter sets the type suffix used by the frontend for syntax
|
|
12
|
+
highlighting, e.g. ``syntax="python"`` yields ``type = "raw.code.python"``.
|
|
13
|
+
``type_suffix`` can override this to an arbitrary suffix.
|
|
14
|
+
|
|
15
|
+
Neither ``multiple`` nor ``languages`` are supported.
|
|
16
|
+
Setting ``validate=False`` disables any syntax validation in subclasses.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
type = "raw.code"
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
indexed: bool = False,
|
|
25
|
+
languages=None,
|
|
26
|
+
multiple: bool = False,
|
|
27
|
+
syntax: str | None = None,
|
|
28
|
+
type_suffix: str = "",
|
|
29
|
+
validate: bool = True,
|
|
30
|
+
**kwargs,
|
|
31
|
+
):
|
|
32
|
+
assert not multiple, "CodeBone does not support multiple values"
|
|
33
|
+
assert not languages, "CodeBone does not support language variants"
|
|
34
|
+
self.syntax = syntax
|
|
35
|
+
self.validate = validate
|
|
36
|
+
if self.syntax:
|
|
37
|
+
type_suffix = type_suffix or syntax
|
|
38
|
+
super().__init__(indexed=indexed, type_suffix=type_suffix, **kwargs)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LogicsBone(CodeBone):
|
|
42
|
+
"""
|
|
43
|
+
Validates its value as a Logics expression (https://github.com/viur-framework/logics).
|
|
44
|
+
Uses Python syntax highlighting in the frontend.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, *, syntax: str = "logics", **kwargs):
|
|
48
|
+
super().__init__(syntax=syntax, type_suffix="python", **kwargs)
|
|
49
|
+
|
|
50
|
+
def singleValueFromClient(self, value, skel, bone_name, client_data):
|
|
51
|
+
if value := str(value or "").strip():
|
|
52
|
+
value += "\n"
|
|
53
|
+
return super().singleValueFromClient(value, skel, bone_name, client_data)
|
|
54
|
+
|
|
55
|
+
def isInvalid(self, value):
|
|
56
|
+
if self.validate and value:
|
|
57
|
+
try:
|
|
58
|
+
logics.Logics(value)
|
|
59
|
+
except logics.ParseException as e:
|
|
60
|
+
return str(e).replace("&eof", "end-of-expression")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class JinjaBone(CodeBone):
|
|
64
|
+
"""
|
|
65
|
+
Validates its value as a Jinja2 template.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, *, syntax: str = "jinja2", **kwargs):
|
|
69
|
+
super().__init__(syntax=syntax, **kwargs)
|
|
70
|
+
|
|
71
|
+
def isInvalid(self, value):
|
|
72
|
+
if self.validate and value:
|
|
73
|
+
env = jinja2.Environment()
|
|
74
|
+
try:
|
|
75
|
+
env.parse(value)
|
|
76
|
+
except jinja2.TemplateSyntaxError as e:
|
|
77
|
+
return f"Syntax error in line {e.lineno}: {e.message}"
|
|
78
|
+
except jinja2.TemplateError as e:
|
|
79
|
+
return f"General error: {e}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PythonBone(CodeBone):
|
|
83
|
+
"""
|
|
84
|
+
Validates its value as Python source code.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, *, syntax: str = "python", **kwargs):
|
|
88
|
+
super().__init__(syntax=syntax, **kwargs)
|
|
89
|
+
|
|
90
|
+
def isInvalid(self, value):
|
|
91
|
+
if self.validate and value:
|
|
92
|
+
try:
|
|
93
|
+
ast.parse(value)
|
|
94
|
+
except SyntaxError as e:
|
|
95
|
+
return f"Syntax error in line {e.lineno}: {e.msg}"
|
|
@@ -45,6 +45,10 @@ class EmailBone(StringBone):
|
|
|
45
45
|
assert account and subDomain and tld
|
|
46
46
|
assert subDomain[0] != "."
|
|
47
47
|
assert len(account) <= 64
|
|
48
|
+
# RFC 5321: local part must not start/end with a dot or contain consecutive dots
|
|
49
|
+
assert not account.startswith(".")
|
|
50
|
+
assert not account.endswith(".")
|
|
51
|
+
assert ".." not in account
|
|
48
52
|
except (ValueError, AssertionError):
|
|
49
53
|
is_valid = False
|
|
50
54
|
|
|
@@ -112,13 +112,13 @@ class RandomSliceBone(BaseBone):
|
|
|
112
112
|
q = db.QueryDefinition(origKind, {}, [])
|
|
113
113
|
property, value = applyFilterHook(dbFilter, f"{name} <=", rndVal)
|
|
114
114
|
q.filters[property] = value
|
|
115
|
-
q.orders = [(name, db.SortOrder.Descending)]
|
|
115
|
+
q.orders = [db.QueryOrder(name, db.SortOrder.Descending)]
|
|
116
116
|
queries.append(q)
|
|
117
117
|
# Left Side
|
|
118
118
|
q = db.QueryDefinition(origKind, {}, [])
|
|
119
119
|
property, value = applyFilterHook(dbFilter, f"{name} >", rndVal)
|
|
120
120
|
q.filters[property] = value
|
|
121
|
-
q.orders = [(name
|
|
121
|
+
q.orders = [db.QueryOrder(name)]
|
|
122
122
|
queries.append(q)
|
|
123
123
|
dbFilter.queries = queries
|
|
124
124
|
# Map the original filter back in
|
|
@@ -965,7 +965,12 @@ class RelationalBone(BaseBone):
|
|
|
965
965
|
raise RuntimeError()
|
|
966
966
|
return f"src.{param}", value
|
|
967
967
|
|
|
968
|
-
def orderHook(
|
|
968
|
+
def orderHook(
|
|
969
|
+
self,
|
|
970
|
+
name: str,
|
|
971
|
+
query: db.Query,
|
|
972
|
+
orderings: list[db.QueryOrder | str] | tuple[db.QueryOrder | str, ...],
|
|
973
|
+
) -> list[db.QueryOrder | str] | tuple[db.QueryOrder | str]:
|
|
969
974
|
"""
|
|
970
975
|
Hook installed by buildDbFilter that rewrites orderings added to the query to match the layout of the
|
|
971
976
|
viur-relations index and performs sanity checks on the query.
|
|
@@ -977,21 +982,23 @@ class RelationalBone(BaseBone):
|
|
|
977
982
|
:param name: The name of the bone.
|
|
978
983
|
:param query: The datastore query to be modified.
|
|
979
984
|
:param orderings: A list or tuple of orderings to be checked and potentially modified.
|
|
980
|
-
:type orderings: List[Union[str, Tuple[str, db.SortOrder]]] or Tuple[Union[str, Tuple[str, db.SortOrder]]]
|
|
981
985
|
|
|
982
986
|
:return: A list of modified orderings that are compatible with the viur-relations index.
|
|
983
|
-
:rtype: List[Union[str, Tuple[str, db.SortOrder]]]
|
|
984
987
|
|
|
985
988
|
:raises RuntimeError: If the ordering is invalid, e.g., using properties not in 'refKeys' or 'parentKeys'.
|
|
986
989
|
"""
|
|
987
990
|
res = []
|
|
988
|
-
if
|
|
991
|
+
if isinstance(orderings, (str, db.QueryOrder)):
|
|
989
992
|
orderings = [orderings]
|
|
993
|
+
elif not isinstance(orderings, list):
|
|
994
|
+
orderings = list(orderings)
|
|
990
995
|
for order in orderings:
|
|
991
|
-
if isinstance(order,
|
|
992
|
-
orderKey = order
|
|
993
|
-
|
|
996
|
+
if isinstance(order, db.QueryOrder):
|
|
997
|
+
orderKey = order.name
|
|
998
|
+
elif isinstance(order, str):
|
|
994
999
|
orderKey = order
|
|
1000
|
+
else:
|
|
1001
|
+
raise TypeError(f"Invalid ordering {order!r} in orderHook")
|
|
995
1002
|
if orderKey.startswith("dest.") or orderKey.startswith("rel.") or orderKey.startswith("src."):
|
|
996
1003
|
# This is already valid for our relational index
|
|
997
1004
|
res.append(order)
|
|
@@ -1005,7 +1012,7 @@ class RelationalBone(BaseBone):
|
|
|
1005
1012
|
res.append(order)
|
|
1006
1013
|
else:
|
|
1007
1014
|
if isinstance(order, tuple):
|
|
1008
|
-
res.append((f"dest.{k}", order[1]))
|
|
1015
|
+
res.append(db.QueryOrder(f"dest.{k}", order[1]))
|
|
1009
1016
|
else:
|
|
1010
1017
|
res.append(f"dest.{k}")
|
|
1011
1018
|
else:
|
|
@@ -1019,7 +1026,7 @@ class RelationalBone(BaseBone):
|
|
|
1019
1026
|
f"Invalid ordering! {orderKey} is not in parentKeys of RelationalBone {name}!")
|
|
1020
1027
|
raise RuntimeError()
|
|
1021
1028
|
if isinstance(order, tuple):
|
|
1022
|
-
res.append((f"src.{orderKey}", order[1]))
|
|
1029
|
+
res.append(db.QueryOrder(f"src.{orderKey}", order[1]))
|
|
1023
1030
|
else:
|
|
1024
1031
|
res.append(f"src.{orderKey}")
|
|
1025
1032
|
return res
|
|
@@ -1107,7 +1114,7 @@ class RelationalBone(BaseBone):
|
|
|
1107
1114
|
|
|
1108
1115
|
ref_skel_cache, using_skel_cache = self._getSkels()
|
|
1109
1116
|
for idx, lang, value in self.iter_bone_value(skel, name):
|
|
1110
|
-
if value
|
|
1117
|
+
if not value:
|
|
1111
1118
|
continue
|
|
1112
1119
|
if value["dest"]:
|
|
1113
1120
|
get_values(ref_skel_cache, value["dest"])
|
|
@@ -281,22 +281,22 @@ class SpatialBone(BaseBone):
|
|
|
281
281
|
q1 = deepcopy(origQuery)
|
|
282
282
|
q1.filters[name + ".coordinates.lat >="] = lat
|
|
283
283
|
q1.filters[name + ".tiles.lat ="] = tileLat
|
|
284
|
-
q1.orders = [(name + ".coordinates.lat"
|
|
284
|
+
q1.orders = [db.QueryOrder(name + ".coordinates.lat")]
|
|
285
285
|
# Lat - Left Side
|
|
286
286
|
q2 = deepcopy(origQuery)
|
|
287
287
|
q2.filters[name + ".coordinates.lat <"] = lat
|
|
288
288
|
q2.filters[name + ".tiles.lat ="] = tileLat
|
|
289
|
-
q2.orders = [(name + ".coordinates.lat", db.SortOrder.Descending)]
|
|
289
|
+
q2.orders = [db.QueryOrder(name + ".coordinates.lat", db.SortOrder.Descending)]
|
|
290
290
|
# Lng - Down
|
|
291
291
|
q3 = deepcopy(origQuery)
|
|
292
292
|
q3.filters[name + ".coordinates.lng >="] = lng
|
|
293
293
|
q3.filters[name + ".tiles.lng ="] = tileLng
|
|
294
|
-
q3.orders = [(name + ".coordinates.lng"
|
|
294
|
+
q3.orders = [db.QueryOrder(name + ".coordinates.lng")]
|
|
295
295
|
# Lng - Top
|
|
296
296
|
q4 = deepcopy(origQuery)
|
|
297
297
|
q4.filters[name + ".coordinates.lng <"] = lng
|
|
298
298
|
q4.filters[name + ".tiles.lng ="] = tileLng
|
|
299
|
-
q4.orders = [(name + ".coordinates.lng", db.SortOrder.Descending)]
|
|
299
|
+
q4.orders = [db.QueryOrder(name + ".coordinates.lng", db.SortOrder.Descending)]
|
|
300
300
|
dbFilter.queries = [q1, q2, q3, q4]
|
|
301
301
|
dbFilter._customMultiQueryMerge = lambda *args, **kwargs: self.customMultiQueryMerge(name, lat, lng, *args,
|
|
302
302
|
**kwargs)
|
|
@@ -378,7 +378,8 @@ class SpatialBone(BaseBone):
|
|
|
378
378
|
|
|
379
379
|
:param skel: Dictionary with the current values from the skeleton the bone belongs to
|
|
380
380
|
:param boneName: The name of the bone that should be modified
|
|
381
|
-
:param value: The value that should be assigned.
|
|
381
|
+
:param value: The value that should be assigned. Either a tuple/list of (lat, lng) or a dict
|
|
382
|
+
with keys ``lat`` and ``lng``
|
|
382
383
|
:param append: If True, the given value will be appended to the existing bone values instead of
|
|
383
384
|
replacing them. Only supported on bones with multiple=True
|
|
384
385
|
:param language: Optional, the language of the value if the bone is language-aware
|
|
@@ -387,7 +388,12 @@ class SpatialBone(BaseBone):
|
|
|
387
388
|
"""
|
|
388
389
|
if append:
|
|
389
390
|
raise ValueError(f"append is not possible on {self.type} bones")
|
|
390
|
-
if
|
|
391
|
+
if isinstance(value, dict):
|
|
392
|
+
try:
|
|
393
|
+
value = (float(value["lat"]), float(value["lng"]))
|
|
394
|
+
except (KeyError, TypeError, ValueError):
|
|
395
|
+
raise ValueError("Value dict must contain 'lat' and 'lng' keys as floats")
|
|
396
|
+
if not isinstance(value, (tuple, list)) or len(value) != 2:
|
|
391
397
|
raise ValueError("Value must be a tuple or a list of (lat, lng)")
|
|
392
398
|
skel[boneName] = tuple(value)
|
|
393
399
|
|
|
@@ -6,7 +6,7 @@ import typing as t
|
|
|
6
6
|
import warnings
|
|
7
7
|
from numbers import Number
|
|
8
8
|
|
|
9
|
-
from viur.core import current, db, utils
|
|
9
|
+
from viur.core import conf, current, db, utils
|
|
10
10
|
from .base import ReadFromClientError, ReadFromClientErrorSeverity
|
|
11
11
|
from .raw import RawBone
|
|
12
12
|
|
|
@@ -29,7 +29,7 @@ class StringBone(RawBone):
|
|
|
29
29
|
max_length: int | None = 254,
|
|
30
30
|
min_length: int | None = None,
|
|
31
31
|
natural_sorting: bool | t.Callable = False,
|
|
32
|
-
escape_html: bool =
|
|
32
|
+
escape_html: bool | None = None,
|
|
33
33
|
**kwargs
|
|
34
34
|
):
|
|
35
35
|
"""
|
|
@@ -46,6 +46,7 @@ class StringBone(RawBone):
|
|
|
46
46
|
that creates the value for the index property.
|
|
47
47
|
:param escape_html: Replace some characters in the string with HTML-safe sequences with
|
|
48
48
|
using :meth:`utils.string.escape` for safe use in HTML.
|
|
49
|
+
Defaults to :attr:`conf.bone_string_escape_html` if not set explicitly.
|
|
49
50
|
:param kwargs: Inherited arguments from the BaseBone.
|
|
50
51
|
"""
|
|
51
52
|
# fixme: Remove in viur-core >= 4
|
|
@@ -71,7 +72,7 @@ class StringBone(RawBone):
|
|
|
71
72
|
elif not natural_sorting:
|
|
72
73
|
self.natural_sorting = None
|
|
73
74
|
# else: keep self.natural_sorting as is
|
|
74
|
-
self.escape_html = escape_html
|
|
75
|
+
self.escape_html = conf.bone_string_escape_html if escape_html is None else escape_html
|
|
75
76
|
|
|
76
77
|
def type_coerce_single_value(self, value: t.Any) -> str:
|
|
77
78
|
"""Convert a value to a string (if not already)
|
|
@@ -414,7 +414,7 @@ class TextBone(RawBone):
|
|
|
414
414
|
from viur.core.bones.file import ensureDerived
|
|
415
415
|
for blob_key in blob_keys:
|
|
416
416
|
file_obj = db.Query("file").filter("dlkey =", blob_key) \
|
|
417
|
-
.order(("creationdate"
|
|
417
|
+
.order(db.QueryOrder("creationdate")).getEntry()
|
|
418
418
|
if file_obj:
|
|
419
419
|
ensureDerived(file_obj.key, f"{skel.kindName}_{name}", derive_dict, skel["key"])
|
|
420
420
|
|
|
@@ -28,10 +28,6 @@ _T = t.TypeVar("_T")
|
|
|
28
28
|
Multiple: t.TypeAlias = list[_T] | tuple[_T] | set[_T] | frozenset[_T] # TODO: Refactor for Python 3.12
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
class CaptchaDefaultCredentialsType(t.TypedDict):
|
|
32
|
-
"""Expected type of global captcha credential, see :attr:`Security.captcha_default_credentials`"""
|
|
33
|
-
sitekey: str
|
|
34
|
-
secret: str
|
|
35
31
|
|
|
36
32
|
|
|
37
33
|
class ConfigType:
|
|
@@ -415,7 +411,7 @@ class Security(ConfigType):
|
|
|
415
411
|
x_permitted_cross_domain_policies: t.Optional[t.Literal["none", "master-only", "by-content-type", "all"]] = "none"
|
|
416
412
|
"""Unless set to logical none; ViUR will emit a X-Permitted-Cross-Domain-Policies with each request"""
|
|
417
413
|
|
|
418
|
-
|
|
414
|
+
captcha_default_public_key: t.Optional[str] = None
|
|
419
415
|
"""The default sitekey and secret to use for the :class:`CaptchaBone`.
|
|
420
416
|
If set, must be a dictionary of "sitekey" and "secret".
|
|
421
417
|
"""
|
|
@@ -508,8 +504,6 @@ class Security(ConfigType):
|
|
|
508
504
|
"xXssProtection": "x_xss_protection",
|
|
509
505
|
"xContentTypeOptions": "x_content_type_options",
|
|
510
506
|
"xPermittedCrossDomainPolicies": "x_permitted_cross_domain_policies",
|
|
511
|
-
"captcha_defaultCredentials": "captcha_default_credentials",
|
|
512
|
-
"captcha.defaultCredentials": "captcha_default_credentials",
|
|
513
507
|
}
|
|
514
508
|
|
|
515
509
|
|
|
@@ -794,6 +788,9 @@ class Conf(ConfigType):
|
|
|
794
788
|
bone_boolean_str2true: Multiple[str | int] = ("true", "yes", "1")
|
|
795
789
|
"""Allowed values that define a str to evaluate to true"""
|
|
796
790
|
|
|
791
|
+
bone_string_escape_html: bool = True
|
|
792
|
+
"""Default escape_html setting for StringBone. Set to False to disable HTML escaping globally."""
|
|
793
|
+
|
|
797
794
|
bone_html_default_allow: "HtmlBoneConfiguration" = {
|
|
798
795
|
"validTags": [
|
|
799
796
|
"a",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
viur.core.contrib — optional, reusable application-level components.
|
|
3
|
+
|
|
4
|
+
This package contains self-contained components that are commonly needed
|
|
5
|
+
but *not* required to run a ViUR application. Components are opt-in;
|
|
6
|
+
import only what you use.
|
|
7
|
+
|
|
8
|
+
Available modules
|
|
9
|
+
-----------------
|
|
10
|
+
loginkey
|
|
11
|
+
:class:`~viur.core.contrib.loginkey.IndexedCredentialBone` and
|
|
12
|
+
:class:`~viur.core.contrib.loginkey.LoginKey` — a
|
|
13
|
+
:class:`~viur.core.modules.user.UserPrimaryAuthentication` that
|
|
14
|
+
authenticates users via a secret token stored in a Datastore-indexed
|
|
15
|
+
:class:`~viur.core.bones.CredentialBone`. Suitable for "magic link"
|
|
16
|
+
style logins or machine-to-machine auth.
|
|
17
|
+
|
|
18
|
+
Usage example::
|
|
19
|
+
|
|
20
|
+
from viur.core.modules.user import User
|
|
21
|
+
from viur.core.contrib.loginkey import LoginKey
|
|
22
|
+
|
|
23
|
+
class MyUser(User):
|
|
24
|
+
authenticationProviders = [LoginKey, ...]
|
|
25
|
+
|
|
26
|
+
ratelimit
|
|
27
|
+
:class:`~viur.core.contrib.ratelimit.RequestRateLimit` — a
|
|
28
|
+
:class:`~viur.core.request.RequestValidator` that enforces per-IP /
|
|
29
|
+
per-user request-rate limits using App Engine Memcache. Suitable for
|
|
30
|
+
global rate-limiting and basic DDoS mitigation at the WSGI boundary.
|
|
31
|
+
|
|
32
|
+
Usage example::
|
|
33
|
+
|
|
34
|
+
from viur.core.request import Router
|
|
35
|
+
from viur.core.contrib.ratelimit import RequestRateLimit, TimeWindow
|
|
36
|
+
|
|
37
|
+
Router.requestValidators.append(
|
|
38
|
+
RequestRateLimit(
|
|
39
|
+
rate_for_guests=TimeWindow(limit=200, time_window=60),
|
|
40
|
+
rate_for_users=TimeWindow(limit=500, time_window=60),
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
"""
|