vortexpy 4.1.7__tar.gz → 4.2.0__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.
- vortexpy-4.2.0/LICENSE +22 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/PKG-INFO +1 -1
- {vortexpy-4.1.7 → vortexpy-4.2.0}/pyproject.toml +1 -1
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/DeferUtil.py +24 -7
- vortexpy-4.2.0/vortex/DeferUtil_test.py +86 -0
- vortexpy-4.2.0/vortex/MtlsAuthorizedClientRegistry.py +150 -0
- vortexpy-4.2.0/vortex/MtlsAuthorizedClientRegistry_test.py +75 -0
- vortexpy-4.2.0/vortex/MtlsServerSslOptions.py +176 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/PayloadEndpoint.py +8 -26
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/PayloadEndpointTest.py +3 -1
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/PayloadEnvelope.py +9 -9
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/PayloadIO.py +2 -9
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/PayloadResponse.py +3 -7
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/SerialiseUtil.py +0 -7
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/Tuple.py +3 -2
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +1 -4
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/TupleRestful.py +8 -25
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/TupleRestfulTest.py +7 -15
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexAccessControlContextABC.py +0 -1
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexClientTcp.py +19 -4
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexClientWebsocketFactory.py +29 -5
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexFactory.py +56 -20
- vortexpy-4.2.0/vortex/VortexFactory_test.py +114 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexPayloadProtocol.py +2 -6
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexPermission.py +0 -1
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexPermissionError.py +0 -1
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexServer.py +1 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexSession.py +2 -4
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexTcpMemoryLeakPayloadTest.py +24 -24
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexWritePushProducer.py +1 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/__init__.py +1 -1
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/data_loader/TupleDataLiveLockManager.py +5 -1
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/data_loader/TupleDataLoader.py +16 -28
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/data_loader/TupleDataLoaderDelegate.py +4 -14
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/event_bus/TupleChangeEventBus.py +8 -7
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/handler/TupleActionProcessor.py +41 -74
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/handler/TupleActionProcessorProxy.py +10 -8
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/handler/TupleActionProcessorTestHandler.py +3 -3
- vortexpy-4.2.0/vortex/handler/TupleActionProcessor_shutdown_test.py +20 -0
- vortexpy-4.2.0/vortex/handler/TupleActionPushOffline.py +180 -0
- vortexpy-4.2.0/vortex/handler/TupleActionPushOffline_test.py +133 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/handler/TupleDataObservableCache.py +5 -3
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/handler/TupleDataObservableHandler.py +20 -28
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/handler/TupleDataObservableProxyHandler.py +5 -14
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/handler/TupleDataObserverClient.py +6 -3
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/handler/TupleDataProviderABC.py +1 -3
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/rpc/RPC.py +38 -70
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/sqla_orm/OrmCrudHandler.py +5 -2
- vortexpy-4.2.0/vortex/storage/TupleActionStorageSqlite.py +156 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/storage/TupleStorageSqlite.py +3 -3
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/tuple_json_schema/tuple_json_schema_builder.py +1 -3
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/tuple_json_schema/tuple_json_schema_test.py +1 -3
- vortexpy-4.2.0/vortex/twisted_runtime_metrics.py +252 -0
- vortexpy-4.2.0/vortex/twisted_runtime_metrics_test.py +132 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortexpy.egg-info/PKG-INFO +1 -1
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortexpy.egg-info/SOURCES.txt +12 -3
- vortexpy-4.1.7/vortex/PayloadResponse_test.py +0 -42
- vortexpy-4.1.7/vortex/SerialiseUtil_test.py +0 -106
- vortexpy-4.1.7/vortex/rpc/RPC_test.py +0 -114
- {vortexpy-4.1.7 → vortexpy-4.2.0}/README.md +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/setup.cfg +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/DataWrapTuple.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/Jsonable.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/Payload.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/PayloadFilterKeys.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/PayloadPriority.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/PayloadResponseTest.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/PayloadTest.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/PerformTestActionTuple.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/TestTuple.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/TupleAction.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/TupleDataForTest.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/TuplePerfTest.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/TupleSelector.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/TupleTest.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexABC.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexAuthenticatorABC.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexClientHttp.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexClientHttpTest.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexConnectionABC.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexEndpointPermission.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexServerConnection.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexServerHttpResource.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexServerTcp.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexServerWebsocket.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexTest.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/VortexUtil.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/data_loader/__init__.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/event_bus/TupleChangeEventABC.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/event_bus/__init__.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/handler/TupleDataActionClient.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/handler/__init__.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/mypy_plugin.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/py.typed +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/restful/GzippedDataHttpClient.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/restful/RestfulHttpClient.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/restful/RestfulHttpClientTest.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/restful/RestfulResource.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/restful/RestfulResourceTest.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/restful/__init__.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/rpc/RPCTest.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/rpc/__init__.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/sqla_orm/VortexTupleJson.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/sqla_orm/__init__.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/storage/__init__.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/tuple_json_schema/__init__.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/tuple_json_schema/constants.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortexpy.egg-info/dependency_links.txt +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortexpy.egg-info/entry_points.txt +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortexpy.egg-info/requires.txt +0 -0
- {vortexpy-4.1.7 → vortexpy-4.2.0}/vortexpy.egg-info/top_level.txt +0 -0
vortexpy-4.2.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vortexpy"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.2.0"
|
|
8
8
|
license = {text = "MIT"}
|
|
9
9
|
authors = [{name = "Flying Ion Pty Ltd", email = "contact@flyingion.com"}]
|
|
10
10
|
description = "Flying Ion's observable, routable, data serialisation and transport code."
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import inspect
|
|
10
10
|
import logging
|
|
11
|
+
import time
|
|
11
12
|
from collections.abc import Callable
|
|
12
13
|
from typing import ParamSpec, TypeVar
|
|
13
14
|
|
|
@@ -209,7 +210,27 @@ def deferToThreadWrapWithLogger(
|
|
|
209
210
|
"Deferring to a thread can only be done from the main thread"
|
|
210
211
|
)
|
|
211
212
|
|
|
212
|
-
|
|
213
|
+
def timedFuncToRun(*runArgs: _P.args, **runKwargs: _P.kwargs) -> _R:
|
|
214
|
+
startMonotonic = time.monotonic()
|
|
215
|
+
try:
|
|
216
|
+
return funcToWrap(*runArgs, **runKwargs)
|
|
217
|
+
finally:
|
|
218
|
+
durationMs = (time.monotonic() - startMonotonic) * 1000.0
|
|
219
|
+
from vortex.twisted_runtime_metrics import (
|
|
220
|
+
STACK_CAPTURE_THRESHOLD_MS,
|
|
221
|
+
TwistedRuntimeMetrics,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
methodFrame = None
|
|
225
|
+
if durationMs >= STACK_CAPTURE_THRESHOLD_MS:
|
|
226
|
+
methodFrame = TwistedRuntimeMetrics._captureWorkerThreadFrame()
|
|
227
|
+
TwistedRuntimeMetrics._collector()._recordThreadedCall(
|
|
228
|
+
funcToWrap.__qualname__,
|
|
229
|
+
durationMs,
|
|
230
|
+
methodFrame,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
d: Deferred[_R] = deferToThread(timedFuncToRun, *args, **kwargs)
|
|
213
234
|
d.addCallback(checkForInlineCallbackYield)
|
|
214
235
|
d.addErrback(vortexLogFailure, loggerArg, consumeError=consumeError)
|
|
215
236
|
return d
|
|
@@ -225,13 +246,9 @@ def isMainThread():
|
|
|
225
246
|
|
|
226
247
|
def noMainThread():
|
|
227
248
|
if isMainThread():
|
|
228
|
-
raise Exception(
|
|
229
|
-
"Blocking operations shouldn't occur in the reactors thread."
|
|
230
|
-
)
|
|
249
|
+
raise Exception("Blocking operations shouldn't occur in the reactors thread.")
|
|
231
250
|
|
|
232
251
|
|
|
233
252
|
def yesMainThread():
|
|
234
253
|
if not isMainThread():
|
|
235
|
-
raise Exception(
|
|
236
|
-
"Async operations must occur in the reactors main thread."
|
|
237
|
-
)
|
|
254
|
+
raise Exception("Async operations must occur in the reactors main thread.")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
# See LICENSE for the full license text.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Generator
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from pytest_twisted import ensureDeferred # type: ignore[import-untyped]
|
|
12
|
+
|
|
13
|
+
from vortex.DeferUtil import deferToThreadWrapWithLogger
|
|
14
|
+
from vortex.twisted_runtime_metrics import TwistedRuntimeMetrics
|
|
15
|
+
from vortex.twisted_runtime_metrics_test import (
|
|
16
|
+
utilResetTwistedRuntimeMetricsSingletonForTests,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@deferToThreadWrapWithLogger(logger)
|
|
23
|
+
def _deferUtilTestBlockingWork() -> str:
|
|
24
|
+
time.sleep(0.01)
|
|
25
|
+
return "done"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@deferToThreadWrapWithLogger(logger)
|
|
29
|
+
def _deferUtilTestSlowBlockingWork() -> str:
|
|
30
|
+
time.sleep(0.06)
|
|
31
|
+
return "slow-done"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture(autouse=True)
|
|
35
|
+
def resetMetricsSingleton() -> Generator[None, None, None]:
|
|
36
|
+
utilResetTwistedRuntimeMetricsSingletonForTests()
|
|
37
|
+
yield
|
|
38
|
+
utilResetTwistedRuntimeMetricsSingletonForTests()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.twisted
|
|
42
|
+
@ensureDeferred
|
|
43
|
+
async def testDeferToThreadWrapWithLoggerRecordsThreadedCallTiming() -> None:
|
|
44
|
+
result = await _deferUtilTestBlockingWork()
|
|
45
|
+
assert result == "done", "deferred thread call must return worker result"
|
|
46
|
+
|
|
47
|
+
snapshot = TwistedRuntimeMetrics.snapshotAndResetWindow()
|
|
48
|
+
assert snapshot.threadedCallCount == 1, (
|
|
49
|
+
"defer hook must record one threaded call per invocation"
|
|
50
|
+
)
|
|
51
|
+
assert snapshot.longestThreadedMethodName == "_deferUtilTestBlockingWork", (
|
|
52
|
+
"defer hook must record wrapped function qualname"
|
|
53
|
+
)
|
|
54
|
+
assert snapshot.longestThreadedMethodMs >= 10.0, (
|
|
55
|
+
"defer hook must record at least sleep duration in ms"
|
|
56
|
+
)
|
|
57
|
+
assert snapshot.longestThreadedMethodFrame is None, (
|
|
58
|
+
"fast defer hook must not capture worker thread frame"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.twisted
|
|
63
|
+
@ensureDeferred
|
|
64
|
+
async def testDeferToThreadWrapWithLoggerCapturesSlowCallFrame() -> None:
|
|
65
|
+
result = await _deferUtilTestSlowBlockingWork()
|
|
66
|
+
assert result == "slow-done", "deferred slow thread call must return worker result"
|
|
67
|
+
|
|
68
|
+
snapshot = TwistedRuntimeMetrics.snapshotAndResetWindow()
|
|
69
|
+
assert snapshot.threadedCallCount == 1, (
|
|
70
|
+
"slow defer hook must record one threaded call"
|
|
71
|
+
)
|
|
72
|
+
assert snapshot.longestThreadedMethodName == "_deferUtilTestSlowBlockingWork", (
|
|
73
|
+
"slow defer hook must record wrapped function qualname"
|
|
74
|
+
)
|
|
75
|
+
assert snapshot.longestThreadedMethodMs >= 50.0, (
|
|
76
|
+
"slow defer hook must exceed stack capture threshold in ms"
|
|
77
|
+
)
|
|
78
|
+
assert snapshot.longestThreadedMethodFrame is not None, (
|
|
79
|
+
"slow defer hook must capture worker thread frame"
|
|
80
|
+
)
|
|
81
|
+
assert "DeferUtil.py" not in snapshot.longestThreadedMethodFrame, (
|
|
82
|
+
"captured frame must skip defer hook internals"
|
|
83
|
+
)
|
|
84
|
+
assert "twisted_runtime_metrics.py" not in snapshot.longestThreadedMethodFrame, (
|
|
85
|
+
"captured frame must skip metrics collector internals"
|
|
86
|
+
)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
"""Mutable authorized-client certificate set for mTLS server verification."""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import logging
|
|
15
|
+
import threading
|
|
16
|
+
from typing import NamedTuple
|
|
17
|
+
|
|
18
|
+
from OpenSSL import crypto
|
|
19
|
+
from txhttputil.util.PemUtil import parsePemBundleForTrustedPeers
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(name=__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthorizedClientIdentity(NamedTuple):
|
|
25
|
+
"""Metadata for one authorized mTLS client certificate."""
|
|
26
|
+
|
|
27
|
+
fingerprint: str
|
|
28
|
+
publicKeyAsn1: bytes
|
|
29
|
+
instanceId: str
|
|
30
|
+
ownerUsername: str
|
|
31
|
+
commonName: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MtlsAuthorizedClientRegistry:
|
|
35
|
+
"""Runtime-mutable allowlist of client certificate public keys.
|
|
36
|
+
|
|
37
|
+
Static PEM bundles can be loaded at startup; ``addAuthorizedClientCertificate``
|
|
38
|
+
and ``revokeAuthorizedClientCertificate`` mutate the set without restarting
|
|
39
|
+
the TLS listener when used with ``buildMtlsServerCertificateOptions``.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
self._lock = threading.RLock()
|
|
44
|
+
self._byPublicKey: dict[bytes, AuthorizedClientIdentity] = {}
|
|
45
|
+
self._byFingerprint: dict[str, AuthorizedClientIdentity] = {}
|
|
46
|
+
|
|
47
|
+
def loadTrustedPeerBundle(self, bundle_file_path: str) -> int:
|
|
48
|
+
"""Load client certificates from a PEM bundle into the registry.
|
|
49
|
+
|
|
50
|
+
:param bundle_file_path: Path to a PEM file with one or more client certs.
|
|
51
|
+
:return: Number of certificates added.
|
|
52
|
+
"""
|
|
53
|
+
trusted_peer_certificates = parsePemBundleForTrustedPeers(bundle_file_path)
|
|
54
|
+
added = 0
|
|
55
|
+
for trusted_peer in trusted_peer_certificates:
|
|
56
|
+
cert_pem = trusted_peer.as_bytes()
|
|
57
|
+
fingerprint = self.addAuthorizedClientCertificate(
|
|
58
|
+
cert_pem,
|
|
59
|
+
instanceId="",
|
|
60
|
+
ownerUsername="",
|
|
61
|
+
)
|
|
62
|
+
if fingerprint:
|
|
63
|
+
added += 1
|
|
64
|
+
return added
|
|
65
|
+
|
|
66
|
+
def addAuthorizedClientCertificate(
|
|
67
|
+
self,
|
|
68
|
+
certificate_pem: bytes | str,
|
|
69
|
+
*,
|
|
70
|
+
instanceId: str = "",
|
|
71
|
+
ownerUsername: str = "",
|
|
72
|
+
) -> str:
|
|
73
|
+
"""Authorize a client certificate for future mTLS handshakes.
|
|
74
|
+
|
|
75
|
+
:param certificate_pem: PEM-encoded X.509 client certificate.
|
|
76
|
+
:param instanceId: Codie instance id (CN when issued by hub CA).
|
|
77
|
+
:param ownerUsername: OAuth username or operator id for audit logs.
|
|
78
|
+
:return: SHA-256 fingerprint prefix (hex) for the certificate.
|
|
79
|
+
"""
|
|
80
|
+
if isinstance(certificate_pem, str):
|
|
81
|
+
certificate_pem = certificate_pem.encode("utf-8")
|
|
82
|
+
|
|
83
|
+
x509_cert = crypto.load_certificate(crypto.FILETYPE_PEM, certificate_pem)
|
|
84
|
+
public_key_asn1 = crypto.dump_publickey(
|
|
85
|
+
crypto.FILETYPE_ASN1, x509_cert.get_pubkey()
|
|
86
|
+
)
|
|
87
|
+
fingerprint = hashlib.sha256(
|
|
88
|
+
crypto.dump_certificate(crypto.FILETYPE_ASN1, x509_cert)
|
|
89
|
+
).hexdigest()[:16]
|
|
90
|
+
subject = x509_cert.get_subject()
|
|
91
|
+
common_name = (
|
|
92
|
+
subject.CN.decode("utf-8")
|
|
93
|
+
if isinstance(subject.CN, bytes)
|
|
94
|
+
else str(subject.CN or "")
|
|
95
|
+
)
|
|
96
|
+
resolved_instance_id = instanceId or common_name
|
|
97
|
+
identity = AuthorizedClientIdentity(
|
|
98
|
+
fingerprint=fingerprint,
|
|
99
|
+
publicKeyAsn1=public_key_asn1,
|
|
100
|
+
instanceId=resolved_instance_id,
|
|
101
|
+
ownerUsername=ownerUsername,
|
|
102
|
+
commonName=common_name,
|
|
103
|
+
)
|
|
104
|
+
with self._lock:
|
|
105
|
+
self._byPublicKey[public_key_asn1] = identity
|
|
106
|
+
self._byFingerprint[fingerprint] = identity
|
|
107
|
+
logger.info(
|
|
108
|
+
"Authorized mTLS client cert fingerprint=%s instanceId=%s owner=%s cn=%s",
|
|
109
|
+
fingerprint,
|
|
110
|
+
resolved_instance_id,
|
|
111
|
+
ownerUsername or "(unknown)",
|
|
112
|
+
common_name,
|
|
113
|
+
)
|
|
114
|
+
return fingerprint
|
|
115
|
+
|
|
116
|
+
def revokeAuthorizedClientCertificate(self, fingerprint: str) -> bool:
|
|
117
|
+
"""Remove a client certificate from the authorized set.
|
|
118
|
+
|
|
119
|
+
:param fingerprint: Value returned from ``addAuthorizedClientCertificate``.
|
|
120
|
+
:return: ``True`` when a certificate was removed.
|
|
121
|
+
"""
|
|
122
|
+
with self._lock:
|
|
123
|
+
identity = self._byFingerprint.pop(fingerprint, None)
|
|
124
|
+
if identity is None:
|
|
125
|
+
return False
|
|
126
|
+
self._byPublicKey.pop(identity.publicKeyAsn1, None)
|
|
127
|
+
logger.info(
|
|
128
|
+
"Revoked mTLS client cert fingerprint=%s instanceId=%s owner=%s",
|
|
129
|
+
identity.fingerprint,
|
|
130
|
+
identity.instanceId,
|
|
131
|
+
identity.ownerUsername or "(unknown)",
|
|
132
|
+
)
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
def isPublicKeyAuthorized(self, public_key_asn1: bytes) -> bool:
|
|
136
|
+
"""Return whether a TLS peer public key is currently authorized."""
|
|
137
|
+
with self._lock:
|
|
138
|
+
return public_key_asn1 in self._byPublicKey
|
|
139
|
+
|
|
140
|
+
def getIdentityForPublicKey(
|
|
141
|
+
self, public_key_asn1: bytes
|
|
142
|
+
) -> AuthorizedClientIdentity | None:
|
|
143
|
+
"""Return audit metadata for an authorized peer public key."""
|
|
144
|
+
with self._lock:
|
|
145
|
+
return self._byPublicKey.get(public_key_asn1)
|
|
146
|
+
|
|
147
|
+
def listAuthorizedIdentities(self) -> list[AuthorizedClientIdentity]:
|
|
148
|
+
"""Return a snapshot of all authorized client identities."""
|
|
149
|
+
with self._lock:
|
|
150
|
+
return list(self._byFingerprint.values())
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from cryptography import x509
|
|
15
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
16
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
17
|
+
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
|
|
18
|
+
from datetime import datetime, timedelta, timezone
|
|
19
|
+
from OpenSSL import crypto
|
|
20
|
+
|
|
21
|
+
from vortex.MtlsAuthorizedClientRegistry import MtlsAuthorizedClientRegistry
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _makeClientCertPem(common_name: str) -> bytes:
|
|
25
|
+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
26
|
+
subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)])
|
|
27
|
+
now = datetime.now(timezone.utc)
|
|
28
|
+
cert = (
|
|
29
|
+
x509.CertificateBuilder()
|
|
30
|
+
.subject_name(subject)
|
|
31
|
+
.issuer_name(subject)
|
|
32
|
+
.public_key(private_key.public_key())
|
|
33
|
+
.serial_number(x509.random_serial_number())
|
|
34
|
+
.not_valid_before(now)
|
|
35
|
+
.not_valid_after(now + timedelta(days=30))
|
|
36
|
+
.add_extension(
|
|
37
|
+
x509.ExtendedKeyUsage([ExtendedKeyUsageOID.CLIENT_AUTH]),
|
|
38
|
+
critical=False,
|
|
39
|
+
)
|
|
40
|
+
.sign(private_key, hashes.SHA256())
|
|
41
|
+
)
|
|
42
|
+
return cert.public_bytes(serialization.Encoding.PEM)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_add_and_revoke_client_certificate() -> None:
|
|
46
|
+
registry = MtlsAuthorizedClientRegistry()
|
|
47
|
+
cert_pem = _makeClientCertPem("local-mac")
|
|
48
|
+
fingerprint = registry.addAuthorizedClientCertificate(
|
|
49
|
+
cert_pem,
|
|
50
|
+
instanceId="local-mac",
|
|
51
|
+
ownerUsername="operator",
|
|
52
|
+
)
|
|
53
|
+
assert fingerprint
|
|
54
|
+
x509_cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
|
|
55
|
+
public_key_asn1 = crypto.dump_publickey(
|
|
56
|
+
crypto.FILETYPE_ASN1, x509_cert.get_pubkey()
|
|
57
|
+
)
|
|
58
|
+
assert registry.isPublicKeyAuthorized(public_key_asn1)
|
|
59
|
+
identity = registry.getIdentityForPublicKey(public_key_asn1)
|
|
60
|
+
assert identity is not None
|
|
61
|
+
assert identity.instanceId == "local-mac"
|
|
62
|
+
assert identity.ownerUsername == "operator"
|
|
63
|
+
assert registry.revokeAuthorizedClientCertificate(fingerprint)
|
|
64
|
+
assert not registry.isPublicKeyAuthorized(public_key_asn1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_load_trusted_peer_bundle(tmp_path) -> None:
|
|
68
|
+
cert_pem = _makeClientCertPem("cloud-instance")
|
|
69
|
+
bundle_path = os.path.join(str(tmp_path), "trusted-peers.pem")
|
|
70
|
+
with open(bundle_path, "wb") as handle:
|
|
71
|
+
handle.write(cert_pem)
|
|
72
|
+
registry = MtlsAuthorizedClientRegistry()
|
|
73
|
+
added = registry.loadTrustedPeerBundle(bundle_path)
|
|
74
|
+
assert added == 1
|
|
75
|
+
assert len(registry.listAuthorizedIdentities()) == 1
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
"""Build Twisted server SSL options with a dynamic mTLS client allowlist."""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from OpenSSL import SSL, crypto
|
|
17
|
+
from twisted.internet._sslverify import Certificate, KeyPair, TLSVersion
|
|
18
|
+
from twisted.internet.ssl import CertificateOptions
|
|
19
|
+
from twisted.python.randbytes import secureRandom
|
|
20
|
+
from txhttputil.util.PemUtil import PrivateKeyWithFullChain
|
|
21
|
+
|
|
22
|
+
from vortex.MtlsAuthorizedClientRegistry import MtlsAuthorizedClientRegistry
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(name=__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _MtlsServerCertificateOptions(CertificateOptions):
|
|
28
|
+
"""Server ``CertificateOptions`` with registry-backed client verification."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
authorized_client_registry: MtlsAuthorizedClientRegistry,
|
|
34
|
+
**kwargs,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._authorizedClientRegistry = authorized_client_registry
|
|
37
|
+
super().__init__(**kwargs)
|
|
38
|
+
|
|
39
|
+
def _makeContext(self):
|
|
40
|
+
ctx = self._contextFactory(self.method)
|
|
41
|
+
ctx.set_options(self._options)
|
|
42
|
+
ctx.set_mode(self._mode)
|
|
43
|
+
|
|
44
|
+
if self.certificate is not None and self.privateKey is not None:
|
|
45
|
+
ctx.use_certificate(self.certificate)
|
|
46
|
+
ctx.use_privatekey(self.privateKey)
|
|
47
|
+
for extra_cert in self.extraCertChain:
|
|
48
|
+
ctx.add_extra_chain_cert(extra_cert)
|
|
49
|
+
ctx.check_privatekey()
|
|
50
|
+
|
|
51
|
+
verify_flags = SSL.VERIFY_NONE
|
|
52
|
+
if self.verify:
|
|
53
|
+
verify_flags = SSL.VERIFY_PEER
|
|
54
|
+
if self.requireCertificate:
|
|
55
|
+
verify_flags |= SSL.VERIFY_FAIL_IF_NO_PEER_CERT
|
|
56
|
+
if self.verifyOnce:
|
|
57
|
+
verify_flags |= SSL.VERIFY_CLIENT_ONCE
|
|
58
|
+
self.trustRoot._addCACertsToContext(ctx)
|
|
59
|
+
|
|
60
|
+
registry = self._authorizedClientRegistry
|
|
61
|
+
|
|
62
|
+
def verifyCallback(
|
|
63
|
+
conn: SSL.Connection,
|
|
64
|
+
cert: crypto.X509,
|
|
65
|
+
errno: int,
|
|
66
|
+
depth: int,
|
|
67
|
+
preverify_ok: int,
|
|
68
|
+
) -> int:
|
|
69
|
+
fail = 0
|
|
70
|
+
|
|
71
|
+
if depth != 0:
|
|
72
|
+
return preverify_ok
|
|
73
|
+
|
|
74
|
+
if preverify_ok == fail:
|
|
75
|
+
return preverify_ok
|
|
76
|
+
|
|
77
|
+
session_peer_public_key_bytes = crypto.dump_publickey(
|
|
78
|
+
crypto.FILETYPE_ASN1, cert.get_pubkey()
|
|
79
|
+
)
|
|
80
|
+
if not registry.isPublicKeyAuthorized(session_peer_public_key_bytes):
|
|
81
|
+
subject = cert.get_subject()
|
|
82
|
+
logger.error(
|
|
83
|
+
"mTLS peer verify failed: certificate not authorized subject=%s",
|
|
84
|
+
subject,
|
|
85
|
+
)
|
|
86
|
+
return fail
|
|
87
|
+
|
|
88
|
+
identity = registry.getIdentityForPublicKey(
|
|
89
|
+
session_peer_public_key_bytes
|
|
90
|
+
)
|
|
91
|
+
if identity is not None:
|
|
92
|
+
logger.info(
|
|
93
|
+
"mTLS peer verified instanceId=%s owner=%s fingerprint=%s",
|
|
94
|
+
identity.instanceId or "(unknown)",
|
|
95
|
+
identity.ownerUsername or "(unknown)",
|
|
96
|
+
identity.fingerprint,
|
|
97
|
+
)
|
|
98
|
+
return preverify_ok
|
|
99
|
+
|
|
100
|
+
ctx.set_verify(verify_flags, verifyCallback)
|
|
101
|
+
if self.verifyDepth is not None:
|
|
102
|
+
ctx.set_verify_depth(self.verifyDepth)
|
|
103
|
+
|
|
104
|
+
from binascii import hexlify
|
|
105
|
+
|
|
106
|
+
session_id_context = hexlify(secureRandom(7))
|
|
107
|
+
ctx.set_session_id(session_id_context)
|
|
108
|
+
|
|
109
|
+
if self.enableSessions:
|
|
110
|
+
ctx.set_session_cache_mode(SSL.SESS_CACHE_SERVER)
|
|
111
|
+
else:
|
|
112
|
+
ctx.set_session_cache_mode(SSL.SESS_CACHE_OFF)
|
|
113
|
+
|
|
114
|
+
if self.dhParameters:
|
|
115
|
+
ctx.load_tmp_dh(self.dhParameters._dhFile.path)
|
|
116
|
+
ctx.set_cipher_list(self._cipherString.encode("ascii"))
|
|
117
|
+
|
|
118
|
+
self._ecChooser.configureECDHCurve(ctx)
|
|
119
|
+
|
|
120
|
+
if self._acceptableProtocols:
|
|
121
|
+
from twisted.internet._sslverify import _setAcceptableProtocols
|
|
122
|
+
|
|
123
|
+
_setAcceptableProtocols(ctx, self._acceptableProtocols)
|
|
124
|
+
|
|
125
|
+
return ctx
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def buildMtlsServerCertificateOptions(
|
|
129
|
+
private_key_with_full_chain: PrivateKeyWithFullChain,
|
|
130
|
+
trust_root,
|
|
131
|
+
authorized_client_registry: MtlsAuthorizedClientRegistry,
|
|
132
|
+
*,
|
|
133
|
+
acceptable_protocols: Optional[list[bytes]] = None,
|
|
134
|
+
dh_parameters=None,
|
|
135
|
+
) -> CertificateOptions:
|
|
136
|
+
"""Build server TLS options with dynamic authorized-client verification.
|
|
137
|
+
|
|
138
|
+
Existing callers that pass a static trusted-peer PEM bundle should load
|
|
139
|
+
that bundle into ``authorized_client_registry`` first; runtime ``add`` /
|
|
140
|
+
``revoke`` on the registry updates verification without rebuilding options.
|
|
141
|
+
|
|
142
|
+
:param private_key_with_full_chain: Server key + certificate chain PEM bank.
|
|
143
|
+
:param trust_root: Twisted trust root for client certificate chain validation.
|
|
144
|
+
:param authorized_client_registry: Mutable authorized client public keys.
|
|
145
|
+
:return: Twisted ``CertificateOptions`` for ``reactor.listenSSL``.
|
|
146
|
+
"""
|
|
147
|
+
if acceptable_protocols is None:
|
|
148
|
+
acceptable_protocols = [b"http/1.1"]
|
|
149
|
+
|
|
150
|
+
twisted_loaded_chain = PrivateKeyWithFullChain(
|
|
151
|
+
privateKey=KeyPair.load(
|
|
152
|
+
private_key_with_full_chain.privateKey.as_bytes(), crypto.FILETYPE_PEM
|
|
153
|
+
).original,
|
|
154
|
+
certificate=Certificate.loadPEM(
|
|
155
|
+
private_key_with_full_chain.certificate.as_bytes()
|
|
156
|
+
).original,
|
|
157
|
+
intermediateCAs=[
|
|
158
|
+
Certificate.loadPEM(intermediate_ca.as_bytes()).original
|
|
159
|
+
for intermediate_ca in private_key_with_full_chain.intermediateCAs
|
|
160
|
+
],
|
|
161
|
+
rootCA=Certificate.loadPEM(
|
|
162
|
+
private_key_with_full_chain.rootCA.as_bytes()
|
|
163
|
+
).original,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return _MtlsServerCertificateOptions(
|
|
167
|
+
authorized_client_registry=authorized_client_registry,
|
|
168
|
+
privateKey=twisted_loaded_chain.privateKey,
|
|
169
|
+
certificate=twisted_loaded_chain.certificate,
|
|
170
|
+
extraCertChain=twisted_loaded_chain.intermediateCAs,
|
|
171
|
+
trustRoot=trust_root,
|
|
172
|
+
raiseMinimumTo=TLSVersion.TLSv1_2,
|
|
173
|
+
lowerMaximumSecurityTo=TLSVersion.TLSv1_3,
|
|
174
|
+
acceptableProtocols=acceptable_protocols,
|
|
175
|
+
dhParameters=dh_parameters,
|
|
176
|
+
)
|
|
@@ -22,9 +22,7 @@ from vortex.VortexAccessControlContextABC import (
|
|
|
22
22
|
NO_AUTH_ACCESS_CONTROL_CONTEXT,
|
|
23
23
|
VortexAccessControlContextABC,
|
|
24
24
|
)
|
|
25
|
-
from vortex.VortexEndpointPermission import
|
|
26
|
-
EndpointPermissionsType as PermissionsType,
|
|
27
|
-
)
|
|
25
|
+
from vortex.VortexEndpointPermission import EndpointPermissionsType as PermissionsType
|
|
28
26
|
from vortex.VortexSession import (
|
|
29
27
|
_callableLabel,
|
|
30
28
|
checkAccessControlContextSignature,
|
|
@@ -71,7 +69,6 @@ class PayloadEndpoint:
|
|
|
71
69
|
acceptOnlyFromVortex: Optional[Union[str, tuple]] = None,
|
|
72
70
|
ignoreFromVortex: Optional[Union[str, tuple]] = None,
|
|
73
71
|
requiredPermissions: Optional[PermissionsType] = None,
|
|
74
|
-
logProcessException: bool = True,
|
|
75
72
|
) -> None:
|
|
76
73
|
"""
|
|
77
74
|
:param filt: The filter to match against payloads
|
|
@@ -83,13 +80,8 @@ class PayloadEndpoint:
|
|
|
83
80
|
the caller's session must hold. Pass an empty frozenset to
|
|
84
81
|
explicitly declare "no permissions required". Omitting the
|
|
85
82
|
parameter (``None``) emits a deprecation warning.
|
|
86
|
-
:param logProcessException: When True, handler exceptions log at ERROR.
|
|
87
|
-
When False, the same events log at DEBUG with neutral wording.
|
|
88
83
|
"""
|
|
89
|
-
self._requiredPermissions: Optional[PermissionsType] =
|
|
90
|
-
requiredPermissions
|
|
91
|
-
)
|
|
92
|
-
self._logProcessException = logProcessException
|
|
84
|
+
self._requiredPermissions: Optional[PermissionsType] = requiredPermissions
|
|
93
85
|
|
|
94
86
|
if requiredPermissions is None:
|
|
95
87
|
logger.warning(self._NO_PERMISSIONS_WARNING, filt)
|
|
@@ -97,11 +89,9 @@ class PayloadEndpoint:
|
|
|
97
89
|
callableDesc = (
|
|
98
90
|
f"PayloadEndpoint filt={filt} callback={_callableLabel(callable_)}"
|
|
99
91
|
)
|
|
100
|
-
self._callableAcceptsAccessControlContext = (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
callableDescription=callableDesc,
|
|
104
|
-
)
|
|
92
|
+
self._callableAcceptsAccessControlContext = checkAccessControlContextSignature(
|
|
93
|
+
callable_,
|
|
94
|
+
callableDescription=callableDesc,
|
|
105
95
|
)
|
|
106
96
|
checkNoHttpSessionInSignature(callable_, callableDesc)
|
|
107
97
|
|
|
@@ -178,10 +168,7 @@ class PayloadEndpoint:
|
|
|
178
168
|
return False
|
|
179
169
|
|
|
180
170
|
# Filter for the backend vortexes.
|
|
181
|
-
if
|
|
182
|
-
self._acceptOnlyFromVortex
|
|
183
|
-
and vortexName not in self._acceptOnlyFromVortex
|
|
184
|
-
):
|
|
171
|
+
if self._acceptOnlyFromVortex and vortexName not in self._acceptOnlyFromVortex:
|
|
185
172
|
return False
|
|
186
173
|
|
|
187
174
|
def removeUnhashable(filt):
|
|
@@ -225,9 +212,7 @@ class PayloadEndpoint:
|
|
|
225
212
|
if not self.check(payloadEnvelope, vortexName):
|
|
226
213
|
return
|
|
227
214
|
|
|
228
|
-
if self.PERMITTER and not self.PERMITTER.check(
|
|
229
|
-
payloadEnvelope, vortexSession
|
|
230
|
-
):
|
|
215
|
+
if self.PERMITTER and not self.PERMITTER.check(payloadEnvelope, vortexSession):
|
|
231
216
|
logger.debug("Permission failed for %s", payloadEnvelope.filt)
|
|
232
217
|
return
|
|
233
218
|
|
|
@@ -265,10 +250,7 @@ class PayloadEndpoint:
|
|
|
265
250
|
accessControlContext = NO_AUTH_ACCESS_CONTROL_CONTEXT
|
|
266
251
|
if self._callableAcceptsAccessControlContext and auth and vortexSession:
|
|
267
252
|
raw_ctx = yield maybeDeferred(
|
|
268
|
-
auth.getAccessControlContext,
|
|
269
|
-
vortexSession,
|
|
270
|
-
vortexName,
|
|
271
|
-
vortexUuid,
|
|
253
|
+
auth.getAccessControlContext, vortexSession, vortexName, vortexUuid
|
|
272
254
|
)
|
|
273
255
|
if not isinstance(raw_ctx, VortexAccessControlContextABC):
|
|
274
256
|
logger.error(
|
|
@@ -143,7 +143,9 @@ class PayloadEndpointPyTest(unittest.TestCase):
|
|
|
143
143
|
|
|
144
144
|
def subScope():
|
|
145
145
|
inst = PayloadEndpointPyTestAssignPayload(self)
|
|
146
|
-
PayloadEndpoint(
|
|
146
|
+
PayloadEndpoint(
|
|
147
|
+
filt, inst.process, requiredPermissions=frozenset()
|
|
148
|
+
)
|
|
147
149
|
|
|
148
150
|
self._processEnvelope(payload)
|
|
149
151
|
yield self._flushReactor()
|