Skip to content

Commit 5c8bdaa

Browse files
authored
Merge pull request #312 from durable-workflow/fix/sdk-python-304-static-credentials
Validate static credentials before client role selection
2 parents 33f2e2f + d736243 commit 5c8bdaa

5 files changed

Lines changed: 426 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Fixed
1010

11+
- `Client` refuses supplied `token`, `control_token` and `worker_token` values with edge whitespace or control characters before choosing a role or opening an HTTP client. The error names only the option. Values are never trimmed, and empty values retain their existing fallback behavior. (#304)
12+
1113
- Replay supports histories in which a run's own close settled the work it never awaited (server#1193, workflow#1117/#1121, Workflow 2.28.0). When a run completes, fails or continues as new, each still-open activity, such as a `select()` loser, gets an `ActivityCancelled` with `cancellation_reason: run_closed` just before the terminal event. Replay, query and update replay now skip that cancellation and leave the activity open, as the workflow saw it. It never resolves a selection member, never supplies replay time, and is never taken as a list group's first positional failure. Before this fix, a close's cancellation of a lower-index member could replace the failure the workflow handled. Replay also accepts the new `abandoned_activities` key on `WorkflowCompleted`, `WorkflowFailed` and `WorkflowContinuedAsNew`, which lists abandon-policy activities left running at close.
1214
- Activity workers treat a `run_closed` refusal as final. A completion refused with `recorded: false, reason: run_closed` is split on its `activity_status`, as in the PHP worker. When it is `completed` or `failed`, the activity was an abandon-policy activity that kept running after its run closed and its own outcome closed it: the task counts as completed and logs at info level. Otherwise the close cancelled the activity: its outcome is dropped, the task is reported as aborted (never completed), and a warning is logged. A failure or cancellation report refused with `run_closed` is not reported again and logs at info level. A claim refused with `run_closed` counts as an answered poll, with no failure backoff. A heartbeat refused with `run_closed` raises `ActivityLeaseAuthorityLost` and publishes no outcome, as before.
1315

‎src/durable_workflow/client.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1885,6 +1885,16 @@ def _request_body_bytes(request: httpx.Request) -> int | None:
18851885
return None
18861886

18871887

1888+
def _validate_static_credential(option: str, value: str | None) -> None:
1889+
if not value:
1890+
return
1891+
# Python's extra whitespace code points are Cc controls, also forbidden here.
1892+
# Validate the original value, including credentials masked by role fallback.
1893+
if (value[0].isspace() or value[-1].isspace()
1894+
or any(ord(char) <= 0x1F or 0x7F <= ord(char) <= 0x9F for char in value)):
1895+
raise ValueError(f"{option} must not have edge whitespace or control characters")
1896+
1897+
18881898
class Client:
18891899
"""Async HTTP client for Durable Workflow control-plane and worker APIs.
18901900
@@ -1897,6 +1907,9 @@ class Client:
18971907
``tls_context`` selects an explicit trust configuration. It must require
18981908
certificate and hostname verification. Treat the context as read-only after
18991909
construction; worker query clones and Nexus namespace views reuse it.
1910+
1911+
Static tokens must have no edge whitespace or control characters. Invalid
1912+
values raise ``ValueError`` naming the option, without exposing its value.
19001913
"""
19011914

19021915
def __init__(
@@ -1920,6 +1933,9 @@ def __init__(
19201933
external_storage_cache: ExternalPayloadCache | None = None,
19211934
telemetry: ExecutionTelemetry | None = None,
19221935
) -> None:
1936+
_validate_static_credential("token", token)
1937+
_validate_static_credential("control_token", control_token)
1938+
_validate_static_credential("worker_token", worker_token)
19231939
if tls_context is not None:
19241940
if not isinstance(tls_context, ssl.SSLContext):
19251941
raise TypeError("tls_context must be an ssl.SSLContext")
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Static credential configuration
2+
3+
`static-credentials-v1.json` is copied byte-for-byte from
4+
[`durable-workflow/server`](https://github.com/durable-workflow/server/blob/c9a75838f0d70fd5237f4cbcf70f9f046f1e5d42/tests/Fixtures/AuthConfiguration/static-credentials-v1.json).
5+
SHA-256: `0d4f01b763d72850943acbd9a74a0cc5da1ca6db5bc1b6fa5ca37fda11810acb`.
6+
7+
The Python tests consume the common scalar cases, pre-selection cases and
8+
Python role mappings. PHP byte-loader and adapter cases apply only to PHP.
9+
All credential values in this fixture are synthetic.

0 commit comments

Comments
 (0)