Skip to content

Commit 4892714

Browse files
authored
Support passing through all modules (#737)
Fixes #691
1 parent 1d89d75 commit 4892714

File tree

4 files changed

+51
-2
lines changed

4 files changed

+51
-2
lines changed

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,21 @@ my_worker = Worker(
10291029
In both of these cases, now the `pydantic` module will be passed through from outside of the sandbox instead of
10301030
being reloaded for every workflow run.
10311031

1032+
If users are sure that no imports they use in workflow files will ever need to be sandboxed (meaning all calls within
1033+
are deterministic and never mutate shared, global state), the `passthrough_all_modules` option can be set on the
1034+
restrictions or the `with_passthrough_all_modules` helper can by used, for example:
1035+
1036+
```python
1037+
my_worker = Worker(
1038+
...,
1039+
workflow_runner=SandboxedWorkflowRunner(
1040+
restrictions=SandboxRestrictions.default.with_passthrough_all_modules()
1041+
)
1042+
)
1043+
```
1044+
1045+
Note, some calls from the module may still be checked for invalid calls at runtime for certain builtins.
1046+
10321047
###### Invalid Module Members
10331048

10341049
`SandboxRestrictions.invalid_module_members` contains a root matcher that applies to all module members. This already

temporalio/worker/workflow_sandbox/_importer.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,11 @@ def _assert_valid_module(self, name: str) -> None:
252252
raise RestrictedWorkflowAccessError(name)
253253

254254
def _maybe_passthrough_module(self, name: str) -> Optional[types.ModuleType]:
255-
# If imports not passed through and name not in passthrough modules,
256-
# check parents
255+
# If imports not passed through and all modules are not passed through
256+
# and name not in passthrough modules, check parents
257257
if (
258258
not temporalio.workflow.unsafe.is_imports_passed_through()
259+
and not self.restrictions.passthrough_all_modules
259260
and name not in self.restrictions.passthrough_modules
260261
):
261262
end_dot = -1

temporalio/worker/workflow_sandbox/_restrictions.py

+17
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,17 @@ class methods (including __init__, etc). The check compares the against the
9999
fully qualified path to the item.
100100
"""
101101

102+
passthrough_all_modules: bool = False
103+
"""
104+
Pass through all modules, do not sandbox any modules. This is the equivalent
105+
of setting :py:attr:`passthrough_modules` to a list of all modules imported
106+
by the workflow. This is unsafe. This means modules are never reloaded per
107+
workflow run which means workflow authors have to be careful that they don't
108+
import modules that do non-deterministic things. Note, just because a module
109+
is passed through from outside the sandbox doesn't mean runtime restrictions
110+
on invalid calls are not still applied.
111+
"""
112+
102113
passthrough_modules_minimum: ClassVar[Set[str]]
103114
"""Set of modules that must be passed through at the minimum."""
104115

@@ -133,6 +144,12 @@ def with_passthrough_modules(self, *modules: str) -> SandboxRestrictions:
133144
self, passthrough_modules=self.passthrough_modules | set(modules)
134145
)
135146

147+
def with_passthrough_all_modules(self) -> SandboxRestrictions:
148+
"""Create a new restriction set with :py:attr:`passthrough_all_modules`
149+
as true.
150+
"""
151+
return dataclasses.replace(self, passthrough_all_modules=True)
152+
136153

137154
# We intentionally use specific fields instead of generic "matcher" callbacks
138155
# for optimization reasons.

tests/worker/workflow_sandbox/test_importer.py

+16
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,22 @@ def test_workflow_sandbox_importer_passthough_context_manager():
6464
assert id(outside) == id(inside)
6565

6666

67+
def test_workflow_sandbox_importer_passthrough_all_modules():
68+
import tests.worker.workflow_sandbox.testmodules.stateful_module as outside
69+
70+
# Confirm regular restrictions does re-import
71+
with Importer(restrictions, RestrictionContext()).applied():
72+
import tests.worker.workflow_sandbox.testmodules.stateful_module as inside1
73+
assert id(outside) != id(inside1)
74+
75+
# But that one with all modules passed through does not
76+
with Importer(
77+
restrictions.with_passthrough_all_modules(), RestrictionContext()
78+
).applied():
79+
import tests.worker.workflow_sandbox.testmodules.stateful_module as inside2
80+
assert id(outside) == id(inside2)
81+
82+
6783
def test_workflow_sandbox_importer_invalid_module_members():
6884
importer = Importer(restrictions, RestrictionContext())
6985
# Can access the function, no problem

0 commit comments

Comments
 (0)