Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `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)

- 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.
- 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.

Expand Down
16 changes: 16 additions & 0 deletions src/durable_workflow/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1885,6 +1885,16 @@ def _request_body_bytes(request: httpx.Request) -> int | None:
return None


def _validate_static_credential(option: str, value: str | None) -> None:
if not value:
return
# Python's extra whitespace code points are Cc controls, also forbidden here.
# Validate the original value, including credentials masked by role fallback.
if (value[0].isspace() or value[-1].isspace()
or any(ord(char) <= 0x1F or 0x7F <= ord(char) <= 0x9F for char in value)):
raise ValueError(f"{option} must not have edge whitespace or control characters")


class Client:
"""Async HTTP client for Durable Workflow control-plane and worker APIs.

Expand All @@ -1897,6 +1907,9 @@ class Client:
``tls_context`` selects an explicit trust configuration. It must require
certificate and hostname verification. Treat the context as read-only after
construction; worker query clones and Nexus namespace views reuse it.

Static tokens must have no edge whitespace or control characters. Invalid
values raise ``ValueError`` naming the option, without exposing its value.
"""

def __init__(
Expand All @@ -1920,6 +1933,9 @@ def __init__(
external_storage_cache: ExternalPayloadCache | None = None,
telemetry: ExecutionTelemetry | None = None,
) -> None:
_validate_static_credential("token", token)
_validate_static_credential("control_token", control_token)
_validate_static_credential("worker_token", worker_token)
if tls_context is not None:
if not isinstance(tls_context, ssl.SSLContext):
raise TypeError("tls_context must be an ssl.SSLContext")
Expand Down
9 changes: 9 additions & 0 deletions tests/fixtures/auth_configuration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Static credential configuration

`static-credentials-v1.json` is copied byte-for-byte from
[`durable-workflow/server`](https://github.com/durable-workflow/server/blob/c9a75838f0d70fd5237f4cbcf70f9f046f1e5d42/tests/Fixtures/AuthConfiguration/static-credentials-v1.json).
SHA-256: `0d4f01b763d72850943acbd9a74a0cc5da1ca6db5bc1b6fa5ca37fda11810acb`.

The Python tests consume the common scalar cases, pre-selection cases and
Python role mappings. PHP byte-loader and adapter cases apply only to PHP.
All credential values in this fixture are synthetic.
Loading
Loading