canvas 0.32.0__py3-none-any.whl → 0.33.1__py3-none-any.whl
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.
Potentially problematic release.
This version of canvas might be problematic. Click here for more details.
- {canvas-0.32.0.dist-info → canvas-0.33.1.dist-info}/METADATA +2 -1
- canvas-0.33.1.dist-info/RECORD +272 -0
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +4 -0
- canvas_sdk/__init__.py +3 -0
- canvas_sdk/commands/__init__.py +1 -1
- canvas_sdk/commands/base.py +3 -0
- canvas_sdk/commands/commands/__init__.py +1 -0
- canvas_sdk/commands/commands/adjust_prescription.py +3 -0
- canvas_sdk/commands/commands/allergy.py +7 -0
- canvas_sdk/commands/commands/assess.py +2 -0
- canvas_sdk/commands/commands/close_goal.py +3 -0
- canvas_sdk/commands/commands/diagnose.py +3 -0
- canvas_sdk/commands/commands/exam.py +3 -0
- canvas_sdk/commands/commands/family_history.py +3 -0
- canvas_sdk/commands/commands/follow_up.py +3 -0
- canvas_sdk/commands/commands/goal.py +3 -0
- canvas_sdk/commands/commands/history_present_illness.py +3 -0
- canvas_sdk/commands/commands/imaging_order.py +3 -0
- canvas_sdk/commands/commands/instruct.py +3 -0
- canvas_sdk/commands/commands/lab_order.py +3 -0
- canvas_sdk/commands/commands/medical_history.py +3 -0
- canvas_sdk/commands/commands/medication_statement.py +2 -0
- canvas_sdk/commands/commands/past_surgical_history.py +3 -0
- canvas_sdk/commands/commands/perform.py +3 -0
- canvas_sdk/commands/commands/plan.py +3 -0
- canvas_sdk/commands/commands/prescribe.py +8 -0
- canvas_sdk/commands/commands/questionnaire/__init__.py +3 -13
- canvas_sdk/commands/commands/questionnaire/question.py +10 -0
- canvas_sdk/commands/commands/reason_for_visit.py +3 -0
- canvas_sdk/commands/commands/refer.py +3 -0
- canvas_sdk/commands/commands/refill.py +3 -0
- canvas_sdk/commands/commands/remove_allergy.py +3 -0
- canvas_sdk/commands/commands/resolve_condition.py +3 -0
- canvas_sdk/commands/commands/review_of_systems.py +3 -0
- canvas_sdk/commands/commands/stop_medication.py +3 -0
- canvas_sdk/commands/commands/structured_assessment.py +3 -0
- canvas_sdk/commands/commands/task.py +7 -0
- canvas_sdk/commands/commands/update_diagnosis.py +3 -0
- canvas_sdk/commands/commands/update_goal.py +3 -0
- canvas_sdk/commands/commands/vitals.py +3 -0
- canvas_sdk/commands/constants.py +8 -0
- canvas_sdk/effects/__init__.py +1 -1
- canvas_sdk/effects/banner_alert/__init__.py +1 -1
- canvas_sdk/effects/banner_alert/add_banner_alert.py +3 -0
- canvas_sdk/effects/banner_alert/remove_banner_alert.py +3 -0
- canvas_sdk/effects/base.py +7 -0
- canvas_sdk/effects/billing_line_item/__init__.py +5 -1
- canvas_sdk/effects/billing_line_item/add_billing_line_item.py +3 -0
- canvas_sdk/effects/billing_line_item/remove_billing_line_item.py +3 -0
- canvas_sdk/effects/billing_line_item/update_billing_line_item.py +3 -0
- canvas_sdk/effects/launch_modal.py +3 -0
- canvas_sdk/effects/patient_chart_summary_configuration.py +3 -0
- canvas_sdk/effects/patient_portal/__init__.py +1 -0
- canvas_sdk/effects/patient_portal/application_configuration.py +3 -0
- canvas_sdk/effects/patient_portal/form_result.py +3 -0
- canvas_sdk/effects/patient_portal_menu_configuration.py +3 -0
- canvas_sdk/effects/patient_profile_configuration.py +6 -1
- canvas_sdk/effects/protocol_card/__init__.py +1 -1
- canvas_sdk/effects/protocol_card/protocol_card.py +6 -0
- canvas_sdk/effects/questionnaire_result.py +3 -0
- canvas_sdk/effects/send_invite.py +46 -0
- canvas_sdk/effects/show_button.py +3 -0
- canvas_sdk/effects/simple_api.py +9 -0
- canvas_sdk/effects/surescripts/__init__.py +2 -2
- canvas_sdk/effects/surescripts/surescripts_messages.py +7 -0
- canvas_sdk/effects/task/__init__.py +6 -1
- canvas_sdk/effects/task/task.py +8 -0
- canvas_sdk/effects/update_user.py +81 -0
- canvas_sdk/effects/widgets/__init__.py +1 -1
- canvas_sdk/effects/widgets/portal_widget.py +3 -0
- canvas_sdk/events/__init__.py +6 -1
- canvas_sdk/events/base.py +3 -0
- canvas_sdk/handlers/__init__.py +1 -1
- canvas_sdk/handlers/action_button.py +6 -0
- canvas_sdk/handlers/application.py +3 -0
- canvas_sdk/handlers/base.py +3 -0
- canvas_sdk/handlers/cron_task.py +3 -0
- canvas_sdk/handlers/simple_api/__init__.py +3 -2
- canvas_sdk/handlers/simple_api/api.py +26 -1
- canvas_sdk/handlers/simple_api/exceptions.py +10 -0
- canvas_sdk/handlers/simple_api/security.py +21 -5
- canvas_sdk/handlers/simple_api/tools.py +9 -0
- canvas_sdk/protocols/__init__.py +1 -1
- canvas_sdk/protocols/base.py +3 -0
- canvas_sdk/protocols/clinical_quality_measure.py +6 -1
- canvas_sdk/protocols/timeframe.py +3 -0
- canvas_sdk/questionnaires/__init__.py +1 -1
- canvas_sdk/questionnaires/utils.py +7 -0
- canvas_sdk/templates/__init__.py +1 -1
- canvas_sdk/templates/utils.py +3 -0
- canvas_sdk/utils/__init__.py +1 -1
- canvas_sdk/utils/http.py +69 -35
- canvas_sdk/utils/plugins.py +4 -0
- canvas_sdk/utils/stats.py +11 -0
- canvas_sdk/v1/__init__.py +1 -0
- canvas_sdk/v1/apps.py +3 -0
- canvas_sdk/v1/data/__init__.py +2 -2
- canvas_sdk/v1/data/allergy_intolerance.py +3 -0
- canvas_sdk/v1/data/appointment.py +7 -0
- canvas_sdk/v1/data/assessment.py +3 -0
- canvas_sdk/v1/data/banner_alert.py +3 -0
- canvas_sdk/v1/data/base.py +3 -0
- canvas_sdk/v1/data/billing.py +7 -0
- canvas_sdk/v1/data/care_team.py +7 -0
- canvas_sdk/v1/data/command.py +3 -0
- canvas_sdk/v1/data/common.py +18 -0
- canvas_sdk/v1/data/condition.py +7 -0
- canvas_sdk/v1/data/coverage.py +14 -0
- canvas_sdk/v1/data/detected_issue.py +3 -0
- canvas_sdk/v1/data/device.py +3 -0
- canvas_sdk/v1/data/imaging.py +7 -0
- canvas_sdk/v1/data/lab.py +16 -0
- canvas_sdk/v1/data/medication.py +3 -0
- canvas_sdk/v1/data/note.py +9 -0
- canvas_sdk/v1/data/observation.py +9 -0
- canvas_sdk/v1/data/organization.py +3 -0
- canvas_sdk/v1/data/patient.py +20 -3
- canvas_sdk/v1/data/practicelocation.py +7 -0
- canvas_sdk/v1/data/protocol_override.py +7 -0
- canvas_sdk/v1/data/questionnaire.py +16 -3
- canvas_sdk/v1/data/reason_for_visit.py +3 -0
- canvas_sdk/v1/data/staff.py +3 -0
- canvas_sdk/v1/data/task.py +12 -0
- canvas_sdk/v1/data/team.py +8 -1
- canvas_sdk/v1/data/user.py +5 -1
- canvas_sdk/v1/models.py +2 -0
- canvas_sdk/value_set/__init__.py +1 -0
- canvas_sdk/value_set/_utilities.py +16 -0
- canvas_sdk/value_set/custom.py +4 -0
- canvas_sdk/value_set/hcc2018.py +3 -0
- canvas_sdk/value_set/v2022/__init__.py +1 -0
- canvas_sdk/value_set/v2022/adverse_event.py +3 -0
- canvas_sdk/value_set/v2022/allergy.py +5 -0
- canvas_sdk/value_set/v2022/assessment.py +5 -0
- canvas_sdk/value_set/v2022/communication.py +5 -0
- canvas_sdk/value_set/v2022/condition.py +5 -0
- canvas_sdk/value_set/v2022/device.py +5 -0
- canvas_sdk/value_set/v2022/diagnostic_study.py +5 -0
- canvas_sdk/value_set/v2022/encounter.py +5 -0
- canvas_sdk/value_set/v2022/immunization.py +5 -0
- canvas_sdk/value_set/v2022/individual_characteristic.py +5 -0
- canvas_sdk/value_set/v2022/intervention.py +5 -0
- canvas_sdk/value_set/v2022/laboratory_test.py +5 -0
- canvas_sdk/value_set/v2022/medication.py +5 -0
- canvas_sdk/value_set/v2022/physical_exam.py +5 -0
- canvas_sdk/value_set/v2022/procedure.py +5 -0
- canvas_sdk/value_set/v2022/symptom.py +3 -0
- canvas_sdk/value_set/value_set.py +9 -0
- canvas_sdk/views/__init__.py +1 -0
- logger/__init__.py +2 -0
- logger/logger.py +3 -0
- plugin_runner/aws_headers.py +1 -1
- plugin_runner/load_all_plugins.py +202 -0
- plugin_runner/plugin_runner.py +26 -24
- plugin_runner/sandbox.py +497 -115
- protobufs/canvas_generated/messages/effects.proto +3 -0
- settings.py +5 -2
- canvas-0.32.0.dist-info/RECORD +0 -364
- canvas_cli/apps/auth/tests.py +0 -155
- canvas_cli/apps/plugin/tests.py +0 -85
- canvas_cli/conftest.py +0 -28
- canvas_cli/tests.py +0 -217
- canvas_cli/utils/context/tests.py +0 -131
- canvas_cli/utils/print/tests.py +0 -69
- canvas_cli/utils/urls/tests.py +0 -12
- canvas_cli/utils/validators/tests.py +0 -37
- canvas_sdk/commands/tests/protocol/__init__.py +0 -0
- canvas_sdk/commands/tests/protocol/tests.py +0 -83
- canvas_sdk/commands/tests/schema/__init__.py +0 -0
- canvas_sdk/commands/tests/schema/tests.py +0 -108
- canvas_sdk/commands/tests/test_base_command.py +0 -81
- canvas_sdk/commands/tests/test_utils.py +0 -375
- canvas_sdk/commands/tests/unit/__init__.py +0 -0
- canvas_sdk/commands/tests/unit/tests.py +0 -278
- canvas_sdk/effects/banner_alert/tests.py +0 -288
- canvas_sdk/effects/protocol_card/tests.py +0 -191
- canvas_sdk/questionnaires/tests/__init__.py +0 -0
- canvas_sdk/questionnaires/tests/test_utils.py +0 -74
- canvas_sdk/templates/tests/__init__.py +0 -0
- canvas_sdk/templates/tests/test_utils.py +0 -43
- canvas_sdk/tests/__init__.py +0 -0
- canvas_sdk/tests/handlers/__init__.py +0 -0
- canvas_sdk/tests/handlers/test_simple_api.py +0 -1167
- canvas_sdk/utils/tests.py +0 -72
- canvas_sdk/value_set/tests/test_value_sets.py +0 -72
- plugin_runner/tests/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/example_plugin/CANVAS_MANIFEST.json +0 -29
- plugin_runner/tests/fixtures/plugins/example_plugin/README.md +0 -12
- plugin_runner/tests/fixtures/plugins/example_plugin/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/example_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/example_plugin/protocols/my_protocol.py +0 -18
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/CANVAS_MANIFEST.json +0 -38
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/README.md +0 -11
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/protocols/my_protocol.py +0 -33
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/__init__.py +0 -3
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/base.py +0 -6
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/__init__.py +0 -5
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/base.py +0 -4
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/CANVAS_MANIFEST.json +0 -52
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/README.md +0 -11
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/protocols/my_protocol.py +0 -39
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/questionnaires/example_questionnaire.yml +0 -61
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/CANVAS_MANIFEST.json +0 -29
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/README.md +0 -12
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/base.py +0 -10
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/my_protocol.py +0 -18
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/CANVAS_MANIFEST.json +0 -29
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/README.md +0 -12
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/other_module/base.py +0 -10
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/protocols/my_protocol.py +0 -18
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/CANVAS_MANIFEST.json +0 -29
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/README.md +0 -12
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/other_module/base.py +0 -3
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/protocols/my_protocol.py +0 -18
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/CANVAS_MANIFEST.json +0 -29
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/README.md +0 -12
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/other_module/base.py +0 -6
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/protocols/my_protocol.py +0 -18
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/CANVAS_MANIFEST.json +0 -29
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/README.md +0 -12
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/other_module/base.py +0 -8
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/protocols/my_protocol.py +0 -18
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/CANVAS_MANIFEST.json +0 -29
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/README.md +0 -12
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/other_module/base.py +0 -3
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/protocols/my_protocol.py +0 -18
- plugin_runner/tests/fixtures/plugins/test_render_template/CANVAS_MANIFEST.json +0 -47
- plugin_runner/tests/fixtures/plugins/test_render_template/README.md +0 -11
- plugin_runner/tests/fixtures/plugins/test_render_template/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_render_template/protocols/my_protocol.py +0 -43
- plugin_runner/tests/fixtures/plugins/test_render_template/templates/template.html +0 -10
- plugin_runner/tests/fixtures/plugins/test_simple_api/CANVAS_MANIFEST.json +0 -47
- plugin_runner/tests/fixtures/plugins/test_simple_api/README.md +0 -11
- plugin_runner/tests/fixtures/plugins/test_simple_api/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_simple_api/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_simple_api/protocols/my_protocol.py +0 -43
- plugin_runner/tests/test_application.py +0 -65
- plugin_runner/tests/test_plugin_installer.py +0 -127
- plugin_runner/tests/test_plugin_runner.py +0 -388
- plugin_runner/tests/test_sandbox.py +0 -137
- {canvas-0.32.0.dist-info → canvas-0.33.1.dist-info}/WHEEL +0 -0
- {canvas-0.32.0.dist-info → canvas-0.33.1.dist-info}/entry_points.txt +0 -0
plugin_runner/sandbox.py
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import ast
|
|
2
4
|
import builtins
|
|
3
5
|
import importlib
|
|
6
|
+
import pkgutil
|
|
4
7
|
import sys
|
|
8
|
+
import types
|
|
5
9
|
from _ast import AnnAssign
|
|
10
|
+
from collections.abc import Iterable, Sequence
|
|
6
11
|
from functools import cached_property
|
|
7
12
|
from pathlib import Path
|
|
8
|
-
from typing import Any, cast
|
|
13
|
+
from typing import TYPE_CHECKING, Any, TypedDict, cast
|
|
9
14
|
|
|
15
|
+
from frozendict import frozendict
|
|
10
16
|
from RestrictedPython import (
|
|
11
17
|
CompileResult,
|
|
12
18
|
PrintCollector,
|
|
@@ -15,7 +21,6 @@ from RestrictedPython import (
|
|
|
15
21
|
safe_builtins,
|
|
16
22
|
utility_builtins,
|
|
17
23
|
)
|
|
18
|
-
from RestrictedPython.Eval import default_guarded_getitem
|
|
19
24
|
from RestrictedPython.Guards import (
|
|
20
25
|
guarded_iter_unpack_sequence,
|
|
21
26
|
guarded_unpack_sequence,
|
|
@@ -23,78 +28,259 @@ from RestrictedPython.Guards import (
|
|
|
23
28
|
from RestrictedPython.transformer import (
|
|
24
29
|
ALLOWED_FUNC_NAMES,
|
|
25
30
|
FORBIDDEN_FUNC_NAMES,
|
|
31
|
+
INSPECT_ATTRIBUTES,
|
|
26
32
|
copy_locations,
|
|
27
33
|
)
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
|
|
37
|
+
class ImportedNames(TypedDict):
|
|
38
|
+
"""
|
|
39
|
+
Type the stored imported_names dicitionary for mypy.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
names: list[str]
|
|
43
|
+
names_to_module: dict[str, str]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def find_submodules(starting_modules: Iterable[str]) -> list[str]:
|
|
47
|
+
"""
|
|
48
|
+
Given a list of modules, return a list of those modules and their submodules.
|
|
49
|
+
"""
|
|
50
|
+
submodules = set(starting_modules)
|
|
51
|
+
|
|
52
|
+
for module_path in starting_modules:
|
|
53
|
+
try:
|
|
54
|
+
module = importlib.import_module(module_path)
|
|
55
|
+
|
|
56
|
+
if not hasattr(module, "__path__"):
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
for _, name, _ in pkgutil.walk_packages(module.__path__, prefix=module.__name__ + "."):
|
|
60
|
+
submodules.add(name)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print(f"could not import {module_path}: {e}")
|
|
63
|
+
|
|
64
|
+
return sorted(submodules)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
SAFE_INTERNAL_DUNDER_READ_ATTRIBUTES = {
|
|
68
|
+
"__class__",
|
|
69
|
+
"__dict__",
|
|
70
|
+
"__eq__",
|
|
71
|
+
"__init__",
|
|
72
|
+
"__name__",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
SAFE_EXTERNAL_DUNDER_READ_ATTRIBUTES = {
|
|
77
|
+
"__dict__",
|
|
78
|
+
"__eq__",
|
|
79
|
+
"__init__",
|
|
80
|
+
"__name__",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
CANVAS_TOP_LEVEL_MODULES = (
|
|
84
|
+
"canvas_sdk.commands",
|
|
85
|
+
"canvas_sdk.effects",
|
|
86
|
+
"canvas_sdk.events",
|
|
87
|
+
"canvas_sdk.handlers",
|
|
88
|
+
"canvas_sdk.protocols",
|
|
89
|
+
"canvas_sdk.questionnaires",
|
|
90
|
+
"canvas_sdk.templates",
|
|
91
|
+
"canvas_sdk.utils",
|
|
92
|
+
"canvas_sdk.v1",
|
|
93
|
+
"canvas_sdk.value_set",
|
|
94
|
+
"canvas_sdk.views",
|
|
95
|
+
"logger",
|
|
85
96
|
)
|
|
86
97
|
|
|
98
|
+
CANVAS_SUBMODULE_NAMES = [
|
|
99
|
+
found_module
|
|
100
|
+
for found_module in find_submodules(CANVAS_TOP_LEVEL_MODULES)
|
|
101
|
+
# tests are excluded from the built and distributed module in pyproject.toml
|
|
102
|
+
if "tests" not in found_module and "test_" not in found_module
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
CANVAS_MODULES: dict[str, set[str]] = {}
|
|
106
|
+
|
|
107
|
+
for module_name in CANVAS_SUBMODULE_NAMES:
|
|
108
|
+
module = importlib.import_module(module_name)
|
|
109
|
+
|
|
110
|
+
exports = getattr(module, "__exports__", None)
|
|
111
|
+
|
|
112
|
+
if not exports:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
if module_name not in CANVAS_MODULES:
|
|
116
|
+
CANVAS_MODULES[module_name] = set()
|
|
87
117
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
118
|
+
CANVAS_MODULES[module_name].update(exports)
|
|
119
|
+
|
|
120
|
+
# In use by a current plugin...
|
|
121
|
+
CANVAS_MODULES["canvas_sdk.commands"].add("*")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
STANDARD_LIBRARY_MODULES = {
|
|
125
|
+
"__future__": {
|
|
126
|
+
"annotations",
|
|
127
|
+
},
|
|
128
|
+
"_strptime": set(), # gets imported at runtime via datetime.datetime.strptime()
|
|
129
|
+
"base64": {
|
|
130
|
+
"b64decode",
|
|
131
|
+
"b64encode",
|
|
132
|
+
},
|
|
133
|
+
"datetime": {
|
|
134
|
+
"date",
|
|
135
|
+
"datetime",
|
|
136
|
+
"timedelta",
|
|
137
|
+
"timezone",
|
|
138
|
+
"UTC",
|
|
139
|
+
},
|
|
140
|
+
"dateutil": {
|
|
141
|
+
"relativedelta",
|
|
142
|
+
},
|
|
143
|
+
"dateutil.relativedelta": {
|
|
144
|
+
"relativedelta",
|
|
145
|
+
},
|
|
146
|
+
"decimal": {
|
|
147
|
+
"Decimal",
|
|
148
|
+
},
|
|
149
|
+
"enum": {
|
|
150
|
+
"Enum",
|
|
151
|
+
"StrEnum",
|
|
152
|
+
},
|
|
153
|
+
"functools": {
|
|
154
|
+
"reduce",
|
|
155
|
+
},
|
|
156
|
+
"hashlib": {
|
|
157
|
+
"sha256",
|
|
158
|
+
},
|
|
159
|
+
"hmac": {
|
|
160
|
+
"compare_digest",
|
|
161
|
+
"new",
|
|
162
|
+
},
|
|
163
|
+
"http": {
|
|
164
|
+
"HTTPStatus",
|
|
165
|
+
},
|
|
166
|
+
"json": {
|
|
167
|
+
"dumps",
|
|
168
|
+
"loads",
|
|
169
|
+
},
|
|
170
|
+
"operator": {
|
|
171
|
+
"and_",
|
|
172
|
+
},
|
|
173
|
+
"random": {
|
|
174
|
+
"choices",
|
|
175
|
+
"uniform",
|
|
176
|
+
"randint",
|
|
177
|
+
},
|
|
178
|
+
"re": {
|
|
179
|
+
"compile",
|
|
180
|
+
"DOTALL",
|
|
181
|
+
"IGNORECASE",
|
|
182
|
+
"match",
|
|
183
|
+
"search",
|
|
184
|
+
"split",
|
|
185
|
+
"sub",
|
|
186
|
+
},
|
|
187
|
+
"string": {
|
|
188
|
+
"ascii_lowercase",
|
|
189
|
+
"digits",
|
|
190
|
+
},
|
|
191
|
+
"time": {
|
|
192
|
+
"time",
|
|
193
|
+
"sleep",
|
|
194
|
+
},
|
|
195
|
+
"typing": {
|
|
196
|
+
"Any",
|
|
197
|
+
"Dict",
|
|
198
|
+
"Final",
|
|
199
|
+
"Iterable",
|
|
200
|
+
"List",
|
|
201
|
+
"NamedTuple",
|
|
202
|
+
"NotRequired",
|
|
203
|
+
"Protocol",
|
|
204
|
+
"Sequence",
|
|
205
|
+
"Tuple",
|
|
206
|
+
"Type",
|
|
207
|
+
"TypedDict",
|
|
208
|
+
},
|
|
209
|
+
"urllib.parse": {
|
|
210
|
+
"urlencode",
|
|
211
|
+
"quote",
|
|
212
|
+
},
|
|
213
|
+
"uuid": {
|
|
214
|
+
"uuid4",
|
|
215
|
+
"UUID",
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
THIRD_PARTY_MODULES = {
|
|
221
|
+
"arrow": {
|
|
222
|
+
"get",
|
|
223
|
+
"now",
|
|
224
|
+
"utcnow",
|
|
225
|
+
},
|
|
226
|
+
"django.db.models": {
|
|
227
|
+
"BigIntegerField",
|
|
228
|
+
"Case",
|
|
229
|
+
"CharField",
|
|
230
|
+
"IntegerField",
|
|
231
|
+
"Model", # remove when hyperscribe no longer needs it
|
|
232
|
+
"Q",
|
|
233
|
+
"Value",
|
|
234
|
+
"When",
|
|
235
|
+
},
|
|
236
|
+
"django.db.models.expressions": {
|
|
237
|
+
"Case",
|
|
238
|
+
"Value",
|
|
239
|
+
"When",
|
|
240
|
+
},
|
|
241
|
+
"django.db.models.query": {
|
|
242
|
+
"QuerySet",
|
|
243
|
+
},
|
|
244
|
+
"django.utils.functional": {
|
|
245
|
+
"cached_property",
|
|
246
|
+
},
|
|
247
|
+
"jwt": {
|
|
248
|
+
"decode",
|
|
249
|
+
"encode",
|
|
250
|
+
},
|
|
251
|
+
"pydantic": {
|
|
252
|
+
"ValidationError",
|
|
253
|
+
},
|
|
254
|
+
"rapidfuzz": {
|
|
255
|
+
"fuzz",
|
|
256
|
+
"process",
|
|
257
|
+
"utils",
|
|
258
|
+
},
|
|
259
|
+
"requests": {
|
|
260
|
+
"delete",
|
|
261
|
+
"get",
|
|
262
|
+
"patch",
|
|
263
|
+
"post",
|
|
264
|
+
"put",
|
|
265
|
+
"request",
|
|
266
|
+
"RequestException",
|
|
267
|
+
"Response",
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# The modules in this list are the only ones that can be imported in a sandboxed runtime.
|
|
273
|
+
ALLOWED_MODULES = frozendict(
|
|
274
|
+
{
|
|
275
|
+
**CANVAS_MODULES,
|
|
276
|
+
**STANDARD_LIBRARY_MODULES,
|
|
277
|
+
**THIRD_PARTY_MODULES,
|
|
278
|
+
}
|
|
279
|
+
)
|
|
94
280
|
|
|
95
281
|
|
|
96
282
|
def _is_known_module(name: str) -> bool:
|
|
97
|
-
return
|
|
283
|
+
return name in ALLOWED_MODULES
|
|
98
284
|
|
|
99
285
|
|
|
100
286
|
def _unrestricted(_ob: Any, *args: Any, **kwargs: Any) -> Any:
|
|
@@ -108,7 +294,9 @@ def _apply(_ob: Any, *args: Any, **kwargs: Any) -> Any:
|
|
|
108
294
|
|
|
109
295
|
|
|
110
296
|
def _find_folder_in_path(file_path: Path, target_folder_name: str) -> Path | None:
|
|
111
|
-
"""
|
|
297
|
+
"""
|
|
298
|
+
Recursively search for a folder with the specified name in the hierarchy of the given file path.
|
|
299
|
+
"""
|
|
112
300
|
file_path = file_path.resolve()
|
|
113
301
|
|
|
114
302
|
if file_path.name == target_folder_name:
|
|
@@ -130,6 +318,50 @@ class Sandbox:
|
|
|
130
318
|
class Transformer(RestrictingNodeTransformer):
|
|
131
319
|
"""A node transformer for customizing the sandbox compiler."""
|
|
132
320
|
|
|
321
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
322
|
+
super().__init__(*args, **kwargs)
|
|
323
|
+
|
|
324
|
+
# we can't just add a self attribute here so we abuse used_names
|
|
325
|
+
# which gets returned as part of the CompileResult
|
|
326
|
+
self.used_names["__imported_names__"] = {
|
|
327
|
+
"names": [],
|
|
328
|
+
"names_to_module": {},
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
def handle_names(self, node: ast.Import | ast.ImportFrom) -> None:
|
|
332
|
+
"""
|
|
333
|
+
Store imported names.
|
|
334
|
+
"""
|
|
335
|
+
module = node.module if isinstance(node, ast.ImportFrom) else None
|
|
336
|
+
|
|
337
|
+
for name in node.names:
|
|
338
|
+
name_string = name.asname if name.asname else name.name
|
|
339
|
+
|
|
340
|
+
self.used_names["__imported_names__"]["names"].append(name_string)
|
|
341
|
+
|
|
342
|
+
if module:
|
|
343
|
+
self.used_names["__imported_names__"]["names_to_module"][name_string] = module
|
|
344
|
+
|
|
345
|
+
def visit_Import(self, node: ast.Import) -> ast.Import:
|
|
346
|
+
"""
|
|
347
|
+
Store imported names.
|
|
348
|
+
"""
|
|
349
|
+
node = super().visit_Import(node)
|
|
350
|
+
|
|
351
|
+
self.handle_names(node)
|
|
352
|
+
|
|
353
|
+
return node
|
|
354
|
+
|
|
355
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.ImportFrom:
|
|
356
|
+
"""
|
|
357
|
+
Store imported names.
|
|
358
|
+
"""
|
|
359
|
+
node = super().visit_ImportFrom(node)
|
|
360
|
+
|
|
361
|
+
self.handle_names(node)
|
|
362
|
+
|
|
363
|
+
return node
|
|
364
|
+
|
|
133
365
|
def visit_AnnAssign(self, node: AnnAssign) -> AnnAssign:
|
|
134
366
|
"""Allow type annotations."""
|
|
135
367
|
return node
|
|
@@ -145,7 +377,9 @@ class Sandbox:
|
|
|
145
377
|
for name in node.names:
|
|
146
378
|
if "*" in name.name and node.module and not _is_known_module(node.module):
|
|
147
379
|
self.error(node, '"*" imports are not allowed.')
|
|
380
|
+
|
|
148
381
|
self.check_name(node, name.name)
|
|
382
|
+
|
|
149
383
|
if name.asname:
|
|
150
384
|
self.check_name(node, name.asname)
|
|
151
385
|
|
|
@@ -189,7 +423,11 @@ class Sandbox:
|
|
|
189
423
|
def visit_Assign(self, node: ast.Assign) -> ast.AST:
|
|
190
424
|
"""Check for forbidden assignments."""
|
|
191
425
|
for target in node.targets:
|
|
192
|
-
if
|
|
426
|
+
if (
|
|
427
|
+
isinstance(target, ast.Name)
|
|
428
|
+
and target.id.startswith("__")
|
|
429
|
+
and target.id != "__all__"
|
|
430
|
+
):
|
|
193
431
|
self.error(node, f"Assignments to '{target.id}' are not allowed.")
|
|
194
432
|
elif isinstance(target, ast.Tuple | ast.List):
|
|
195
433
|
self.check_for_name_in_iterable(target)
|
|
@@ -199,7 +437,7 @@ class Sandbox:
|
|
|
199
437
|
def check_for_name_in_iterable(self, iterable_node: ast.Tuple | ast.List) -> None:
|
|
200
438
|
"""Check if any element of an iterable is a forbidden assignment."""
|
|
201
439
|
for elt in iterable_node.elts:
|
|
202
|
-
if isinstance(elt, ast.Name) and elt.id
|
|
440
|
+
if isinstance(elt, ast.Name) and elt.id.startswith("__") and elt.id != "__all__":
|
|
203
441
|
self.error(iterable_node, f"Assignments to '{elt.id}' are not allowed.")
|
|
204
442
|
elif isinstance(elt, ast.Tuple | ast.List):
|
|
205
443
|
self.check_for_name_in_iterable(elt)
|
|
@@ -240,8 +478,15 @@ class Sandbox:
|
|
|
240
478
|
|
|
241
479
|
elif isinstance(node.ctx, ast.Store | ast.Del):
|
|
242
480
|
node = self.node_contents_visit(node)
|
|
481
|
+
|
|
243
482
|
new_value = ast.Call(
|
|
244
|
-
func=ast.Name("_write_", ast.Load()),
|
|
483
|
+
func=ast.Name("_write_", ast.Load()),
|
|
484
|
+
args=[
|
|
485
|
+
node.value,
|
|
486
|
+
ast.Constant(node.value.id if isinstance(node.value, ast.Name) else None),
|
|
487
|
+
ast.Constant(node.attr),
|
|
488
|
+
],
|
|
489
|
+
keywords=[],
|
|
245
490
|
)
|
|
246
491
|
|
|
247
492
|
copy_locations(new_value, node.value)
|
|
@@ -254,26 +499,21 @@ class Sandbox:
|
|
|
254
499
|
|
|
255
500
|
def __init__(
|
|
256
501
|
self,
|
|
257
|
-
source_code:
|
|
258
|
-
namespace: str
|
|
502
|
+
source_code: Path,
|
|
503
|
+
namespace: str,
|
|
259
504
|
evaluated_modules: dict[str, bool] | None = None,
|
|
260
505
|
) -> None:
|
|
261
|
-
if source_code is None:
|
|
262
|
-
raise TypeError("source_code may not be None")
|
|
263
|
-
|
|
264
506
|
self.namespace = namespace or "protocols"
|
|
265
507
|
self.package_name = self.namespace.split(".")[0]
|
|
266
508
|
|
|
267
|
-
if
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
self.source_code = source_code
|
|
276
|
-
self.base_path = None
|
|
509
|
+
if not source_code.exists():
|
|
510
|
+
raise FileNotFoundError(f"File not found: {source_code}")
|
|
511
|
+
|
|
512
|
+
self.source_code_path = source_code.as_posix()
|
|
513
|
+
self.source_code = source_code.read_text()
|
|
514
|
+
package_path = _find_folder_in_path(source_code, self.package_name)
|
|
515
|
+
self.base_path = package_path.parent if package_path else None
|
|
516
|
+
self._evaluated_modules: dict[str, bool] = evaluated_modules or {}
|
|
277
517
|
|
|
278
518
|
@cached_property
|
|
279
519
|
def scope(self) -> dict[str, Any]:
|
|
@@ -283,41 +523,51 @@ class Sandbox:
|
|
|
283
523
|
**safe_builtins.copy(),
|
|
284
524
|
**utility_builtins.copy(),
|
|
285
525
|
"__import__": self._safe_import,
|
|
286
|
-
"classmethod": builtins.classmethod,
|
|
287
|
-
"staticmethod": builtins.staticmethod,
|
|
288
|
-
"any": builtins.any,
|
|
289
526
|
"all": builtins.all,
|
|
290
|
-
"
|
|
291
|
-
"
|
|
292
|
-
"super": builtins.super,
|
|
527
|
+
"any": builtins.any,
|
|
528
|
+
"classmethod": builtins.classmethod,
|
|
293
529
|
"dict": builtins.dict,
|
|
530
|
+
"enumerate": builtins.enumerate,
|
|
294
531
|
"filter": builtins.filter,
|
|
532
|
+
"hasattr": builtins.hasattr,
|
|
533
|
+
"iter": builtins.iter,
|
|
534
|
+
"list": builtins.list,
|
|
535
|
+
"map": builtins.map,
|
|
295
536
|
"max": builtins.max,
|
|
296
537
|
"min": builtins.min,
|
|
297
|
-
"list": builtins.list,
|
|
298
538
|
"next": builtins.next,
|
|
299
|
-
"
|
|
300
|
-
"
|
|
539
|
+
"property": builtins.property,
|
|
540
|
+
"reversed": builtins.reversed,
|
|
541
|
+
"staticmethod": builtins.staticmethod,
|
|
542
|
+
"super": builtins.super,
|
|
301
543
|
},
|
|
544
|
+
"__is_plugin__": True,
|
|
302
545
|
"__metaclass__": type,
|
|
303
546
|
"__name__": self.namespace,
|
|
304
|
-
"__is_plugin__": True,
|
|
305
|
-
"_write_": _unrestricted,
|
|
306
|
-
"_getiter_": _unrestricted,
|
|
307
|
-
"_getitem_": default_guarded_getitem,
|
|
308
|
-
"_getattr_": getattr,
|
|
309
|
-
"_print_": PrintCollector,
|
|
310
547
|
"_apply_": _apply,
|
|
548
|
+
"_getattr_": self._safe_getattr,
|
|
549
|
+
"_getitem_": self._safe_getitem,
|
|
550
|
+
"_getiter_": _unrestricted,
|
|
311
551
|
"_inplacevar_": _unrestricted,
|
|
312
552
|
"_iter_unpack_sequence_": guarded_iter_unpack_sequence,
|
|
553
|
+
"_print_": PrintCollector,
|
|
313
554
|
"_unpack_sequence_": guarded_unpack_sequence,
|
|
314
|
-
"
|
|
555
|
+
"_write_": self._safe_write,
|
|
315
556
|
}
|
|
316
557
|
|
|
317
558
|
@cached_property
|
|
318
559
|
def compile_result(self) -> CompileResult:
|
|
319
560
|
"""Compile the source code into bytecode."""
|
|
320
|
-
return compile_restricted_exec(
|
|
561
|
+
return compile_restricted_exec(
|
|
562
|
+
source=self.source_code,
|
|
563
|
+
policy=self.Transformer,
|
|
564
|
+
filename=self.source_code_path,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
def imported_names(self) -> ImportedNames:
|
|
569
|
+
"""Return the imported names collecting during parsing."""
|
|
570
|
+
return self.compile_result.used_names["__imported_names__"]
|
|
321
571
|
|
|
322
572
|
@property
|
|
323
573
|
def errors(self) -> tuple[str, ...]:
|
|
@@ -330,10 +580,7 @@ class Sandbox:
|
|
|
330
580
|
return cast(tuple[str, ...], self.compile_result.warnings)
|
|
331
581
|
|
|
332
582
|
def _is_known_module(self, name: str) -> bool:
|
|
333
|
-
return
|
|
334
|
-
_is_known_module(name)
|
|
335
|
-
or (self.package_name and name.split(".")[0] == self.package_name and self.base_path)
|
|
336
|
-
)
|
|
583
|
+
return _is_known_module(name) or self._same_module(name)
|
|
337
584
|
|
|
338
585
|
def _get_module(self, module_name: str) -> Path:
|
|
339
586
|
"""Get the module path for the given module name."""
|
|
@@ -347,10 +594,13 @@ class Sandbox:
|
|
|
347
594
|
|
|
348
595
|
def _evaluate_module(self, module_name: str) -> None:
|
|
349
596
|
"""Evaluate the given module in the sandbox.
|
|
350
|
-
|
|
597
|
+
|
|
598
|
+
If the module to import belongs to the same package as the current module,
|
|
599
|
+
evaluate it inside a sandbox.
|
|
351
600
|
"""
|
|
352
|
-
|
|
353
|
-
|
|
601
|
+
# Skip modules already evaluated
|
|
602
|
+
if not self._same_module(module_name) or module_name in self._evaluated_modules:
|
|
603
|
+
return
|
|
354
604
|
|
|
355
605
|
module = self._get_module(module_name)
|
|
356
606
|
self._evaluate_implicit_imports(module)
|
|
@@ -358,8 +608,11 @@ class Sandbox:
|
|
|
358
608
|
# Re-check after evaluating implicit imports to avoid duplicate evaluations.
|
|
359
609
|
if module_name not in self._evaluated_modules:
|
|
360
610
|
Sandbox(
|
|
361
|
-
module,
|
|
611
|
+
module,
|
|
612
|
+
namespace=module_name,
|
|
613
|
+
evaluated_modules=self._evaluated_modules,
|
|
362
614
|
).execute()
|
|
615
|
+
|
|
363
616
|
self._evaluated_modules[module_name] = True
|
|
364
617
|
|
|
365
618
|
# Reload the module if already imported to ensure the latest version is used.
|
|
@@ -372,7 +625,8 @@ class Sandbox:
|
|
|
372
625
|
parent = module.parent.parent if module.name == "__init__.py" else module.parent
|
|
373
626
|
base_path = cast(Path, self.base_path)
|
|
374
627
|
|
|
375
|
-
# Skip evaluation if the parent module is outside the base path or
|
|
628
|
+
# Skip evaluation if the parent module is outside the base path or
|
|
629
|
+
# already the source code root.
|
|
376
630
|
if not parent.is_relative_to(base_path) or parent == base_path:
|
|
377
631
|
return
|
|
378
632
|
|
|
@@ -383,8 +637,11 @@ class Sandbox:
|
|
|
383
637
|
if init_file.exists():
|
|
384
638
|
# Mark as evaluated to prevent infinite recursion.
|
|
385
639
|
self._evaluated_modules[module_name] = True
|
|
640
|
+
|
|
386
641
|
Sandbox(
|
|
387
|
-
init_file,
|
|
642
|
+
init_file,
|
|
643
|
+
namespace=module_name,
|
|
644
|
+
evaluated_modules=self._evaluated_modules,
|
|
388
645
|
).execute()
|
|
389
646
|
else:
|
|
390
647
|
# Mark as evaluated even if no init file exists to prevent redundant checks.
|
|
@@ -392,13 +649,126 @@ class Sandbox:
|
|
|
392
649
|
|
|
393
650
|
self._evaluate_implicit_imports(parent)
|
|
394
651
|
|
|
395
|
-
def
|
|
396
|
-
|
|
397
|
-
|
|
652
|
+
def _same_module(self, module: str) -> bool:
|
|
653
|
+
"""
|
|
654
|
+
Return True if `module` is within the plugin code.
|
|
655
|
+
"""
|
|
656
|
+
return bool(self.base_path) and module.split(".")[0] == self.package_name
|
|
657
|
+
|
|
658
|
+
def _safe_write(self, _ob: Any, name: str | None = None, attribute: str | None = None) -> Any:
|
|
659
|
+
"""Check if the given obj belongs to a protected resource."""
|
|
660
|
+
is_module = isinstance(_ob, types.ModuleType)
|
|
661
|
+
|
|
662
|
+
if is_module:
|
|
663
|
+
if not self._same_module(_ob.__name__):
|
|
664
|
+
raise AttributeError(f"Forbidden assignment to a module attribute: {_ob.__name__}.")
|
|
665
|
+
elif isinstance(_ob, type):
|
|
666
|
+
full_name = f"{_ob.__module__}.{_ob.__qualname__}"
|
|
667
|
+
else:
|
|
668
|
+
full_name = f"{_ob.__module__}.{_ob.__class__.__qualname__}"
|
|
669
|
+
|
|
670
|
+
if not self._same_module(_ob.__module__) and (
|
|
671
|
+
# deny if it was anything imported
|
|
672
|
+
name in self.imported_names["names"]
|
|
673
|
+
# deny if it's anything callable
|
|
674
|
+
or (attribute is not None and callable(getattr(_ob, attribute)))
|
|
675
|
+
):
|
|
676
|
+
raise AttributeError(
|
|
677
|
+
f"Forbidden assignment to a non-module attribute: {full_name} "
|
|
678
|
+
f"at {name}.{attribute}."
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
return _ob
|
|
682
|
+
|
|
683
|
+
def _safe_getitem(self, ob: Any, index: Any) -> Any:
|
|
684
|
+
"""
|
|
685
|
+
Prevent access to several classes of items.
|
|
686
|
+
"""
|
|
687
|
+
if isinstance(index, str) and index.startswith("_"):
|
|
688
|
+
raise AttributeError(f'"{index}" is an invalid item name because it starts with "_"')
|
|
689
|
+
|
|
690
|
+
return ob[index]
|
|
691
|
+
|
|
692
|
+
def _safe_getattr(self, _ob: Any, name: Any, default: Any = None) -> Any:
|
|
693
|
+
"""
|
|
694
|
+
Prevent access to several classes of attributes.
|
|
695
|
+
|
|
696
|
+
Restricted attribute types:
|
|
697
|
+
|
|
698
|
+
1. underscored attributes created outside of the defining namespace
|
|
699
|
+
2. attributes used by the `inspect` module
|
|
700
|
+
3. dunder methods except for those we deem safe
|
|
701
|
+
4. if a __exports__ module property is defined, any
|
|
702
|
+
attribute not in that property's value
|
|
703
|
+
"""
|
|
704
|
+
is_module = isinstance(_ob, types.ModuleType)
|
|
398
705
|
|
|
706
|
+
if is_module:
|
|
707
|
+
module = _ob.__name__.split(".")[0]
|
|
708
|
+
elif isinstance(_ob, type):
|
|
709
|
+
module = _ob.__module__.split(".")[0]
|
|
710
|
+
else:
|
|
711
|
+
module = _ob.__class__.__module__.split(".")[0]
|
|
712
|
+
|
|
713
|
+
if type(name) is not str:
|
|
714
|
+
raise TypeError("type(name) must be str")
|
|
715
|
+
|
|
716
|
+
if name in ("format", "format_map") and (
|
|
717
|
+
isinstance(_ob, str) or (isinstance(_ob, type) and issubclass(_ob, str))
|
|
718
|
+
):
|
|
719
|
+
raise NotImplementedError(
|
|
720
|
+
"Using the format and format_map methods of `str` is not safe"
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
if name in INSPECT_ATTRIBUTES:
|
|
724
|
+
raise AttributeError(f'"{name}" is a restricted name.')
|
|
725
|
+
|
|
726
|
+
# Code defined in the Sandbox namespace can access its own underscore variables
|
|
727
|
+
if name.startswith("_"):
|
|
728
|
+
if self._same_module(module):
|
|
729
|
+
if name.startswith("__") and name not in SAFE_INTERNAL_DUNDER_READ_ATTRIBUTES:
|
|
730
|
+
raise AttributeError(
|
|
731
|
+
f'"{name}" is an invalid attribute name because it starts with "_"'
|
|
732
|
+
)
|
|
733
|
+
else:
|
|
734
|
+
# Nothing can read dunder attributes except those on our safe list
|
|
735
|
+
if name not in SAFE_EXTERNAL_DUNDER_READ_ATTRIBUTES:
|
|
736
|
+
raise AttributeError(
|
|
737
|
+
f'"{name}" is an invalid attribute name because it starts with "__"'
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
exports = getattr(_ob, "__exports__", None)
|
|
741
|
+
|
|
742
|
+
if exports:
|
|
743
|
+
if name not in exports:
|
|
744
|
+
raise AttributeError(f'"{name}" is an invalid attribute name (not in __exports__)')
|
|
745
|
+
elif is_module and (module not in ALLOWED_MODULES or name not in ALLOWED_MODULES[module]):
|
|
746
|
+
raise AttributeError(f'"{name}" is an invalid attribute name (not in ALLOWED_MODULES)')
|
|
747
|
+
|
|
748
|
+
return getattr(_ob, name, default)
|
|
749
|
+
|
|
750
|
+
def _safe_import(
|
|
751
|
+
self,
|
|
752
|
+
name: str,
|
|
753
|
+
globals: Any = None,
|
|
754
|
+
locals: Any = None,
|
|
755
|
+
fromlist: Sequence[str] = (),
|
|
756
|
+
level: int = 0,
|
|
757
|
+
) -> Any:
|
|
758
|
+
if not self._same_module(name):
|
|
759
|
+
# Disallow importing anything not explicitly allowed by ALLOWED_MODULES
|
|
760
|
+
if name not in ALLOWED_MODULES:
|
|
761
|
+
raise ImportError(f"{name!r} is not an allowed import.")
|
|
762
|
+
|
|
763
|
+
if fromlist is not None:
|
|
764
|
+
for item in fromlist:
|
|
765
|
+
if item not in ALLOWED_MODULES.get(name, set()):
|
|
766
|
+
raise ImportError(f"{item!r} is not an allowed import from {name!r}.")
|
|
767
|
+
|
|
768
|
+
# evaluate the module in the sandbox if needed
|
|
399
769
|
self._evaluate_module(name)
|
|
400
770
|
|
|
401
|
-
return __import__(name,
|
|
771
|
+
return __import__(name, globals, locals, fromlist, level)
|
|
402
772
|
|
|
403
773
|
def execute(self) -> dict:
|
|
404
774
|
"""Execute the given code in a restricted sandbox."""
|
|
@@ -408,3 +778,15 @@ class Sandbox:
|
|
|
408
778
|
exec(self.compile_result.code, self.scope)
|
|
409
779
|
|
|
410
780
|
return self.scope
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def sandbox_from_module(base_path: Path, module_name: str) -> Sandbox:
|
|
784
|
+
"""Sandbox the code execution."""
|
|
785
|
+
module_path = base_path / str(module_name.replace(".", "/") + ".py")
|
|
786
|
+
|
|
787
|
+
if not module_path.exists():
|
|
788
|
+
raise ModuleNotFoundError(f'Could not load module "{module_name}"')
|
|
789
|
+
|
|
790
|
+
sandbox = Sandbox(module_path, namespace=module_name)
|
|
791
|
+
|
|
792
|
+
return sandbox
|