Skip to content

Commit f893550

Browse files
owenpearsonclaude
andcommitted
pubsub: let a basic-auth realtime connection enter presence for others
A realtime client built from a key alone is told clientId "*" in its CONNECTED message. _configure_client_id opens with a branch that keeps a configured clientId against a server wildcard, and with no clientId configured that branch still fired, recording the clientId as validated and None. can_assume_client_id then refused every clientId, so presence.enter_client could never succeed on such a connection (deviations.md I.2; RSA7b4, RTP14, RTP15). The UTS integration presence tests and the repository's own presence suite worked around it by configuring client_id='*'. The branch now applies only when a clientId is configured, so with none the client takes the wildcard as its clientId, which RSA7b4 asks for. Because auth.client_id can now be "*", the Basic auth X-Ably-ClientId header is taken from the configured clientId, as RSA7e2 specifies, rather than from auth.client_id, so a wildcard the server reported is never sent back to it as a clientId. A later authorize() on such a client requests a wildcard token, which RSA10h asks for and the key already permits. This is not a breaking change. The three batch presence tests and the two realtime integration presence tests that configured client_id='*' as a workaround now build their clients as the specifications do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ed2a857 commit f893550

5 files changed

Lines changed: 21 additions & 91 deletions

File tree

‎ably/pubsub/http/auth.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -377,8 +377,8 @@ def _configure_client_id(self, new_client_id):
377377
return
378378

379379
# If new client ID from Ably is a wildcard, but preconfigured clientId is set,
380-
# then keep the existing clientId
381-
if original_client_id != '*' and new_client_id == '*':
380+
# then keep the existing clientId. With none set, the wildcard is the clientId (RSA7b4)
381+
if original_client_id is not None and original_client_id != '*' and new_client_id == '*':
382382
self.__client_id_validated = True
383383
self.__client_id = original_client_id
384384
return
@@ -404,11 +404,12 @@ def can_assume_client_id(self, assumed_client_id):
404404

405405
async def _get_auth_headers(self):
406406
if self.__auth_mechanism == Auth.Method.BASIC:
407-
# RSA7e2
408-
if self.client_id:
407+
# RSA7e2: the configured clientId, not a wildcard the server reported
408+
client_id = self.auth_options.client_id
409+
if client_id:
409410
return {
410411
'Authorization': f'Basic {self.basic_credentials}',
411-
'X-Ably-ClientId': base64.b64encode(self.client_id.encode('utf-8'))
412+
'X-Ably-ClientId': base64.b64encode(client_id.encode('utf-8'))
412413
}
413414
return {
414415
'Authorization': f'Basic {self.basic_credentials}',

‎test/uts/deviations.md‎

Lines changed: 6 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,10 @@ specification: 130 under `helpers/`, for the mocks, and 18 under `objects/helper
5252
the objects builders and the synced-channel mock.
5353

5454
The 142 gated Test IDs that record SDK non-compliance — 142 tests, 144 cases — reduce to
55-
**61 distinct root causes**, 21 on the REST side and 40 on the realtime side. Three further
55+
**61 distinct root causes**, 21 on the REST side and 40 on the realtime side. Two further
5656
defects are recorded below with no test of their own, because the specification's test
57-
cannot discriminate (RTP18a), has nothing to assert against (the timezone split on
58-
synthesized LEAVE timestamps), or is worked around in the setup of every test that
59-
would otherwise trip over it (`enterClient` on an anonymous connection), so the file
60-
carries **64 SDK root causes** in all. The remaining 11 gated Test IDs are
57+
cannot discriminate (RTP18a) or has nothing to assert against (the timezone split on
58+
synthesized LEAVE timestamps), so the file carries **63 SDK root causes** in all. The remaining 11 gated Test IDs are
6159
specification faults, and reduce to 8. The objects tier records no SDK non-compliance;
6260
its specification faults are all corrected or worked around in tests that pass, and
6361
reduce to the seven candidate issues O.1–O.7, none of them filed.
@@ -1790,51 +1788,6 @@ JWT, fall with it.
17901788
renew and retry on a token error whatever the flag says, and only then decide whether the
17911789
final response is raised or returned.
17921790

1793-
#### A basic-auth connection records its clientId as validated and `None`, so `enterClient` can never succeed — no test
1794-
1795-
**Spec points:** RSA7b4, RTP14, RTP15.
1796-
1797-
A client built from a full-access key alone and asked to `enter_client("user-1", …)`
1798-
raises `AblyException: 40012 400 Unable to enter presence channel with clientId user-1 as
1799-
it does not match the current clientId None`.
1800-
1801-
The server tells such a connection `clientId: "*"` in CONNECTED, and
1802-
`Auth._configure_client_id` (`ably/rest/auth.py:335-353`) opens with a branch that exists
1803-
to stop a server wildcard overwriting a clientId the caller configured:
1804-
1805-
```python
1806-
if original_client_id != '*' and new_client_id == '*':
1807-
self.__client_id_validated = True
1808-
self.__client_id = original_client_id
1809-
return
1810-
```
1811-
1812-
With no configured clientId, `original_client_id` is `None`, `None != '*'` holds, and the
1813-
client id is recorded as **validated and `None`**. `can_assume_client_id`
1814-
(`auth.py:356-363`) then takes the validated path and answers `None == '*' or None ==
1815-
assumed`, which is `False` for every clientId, so `enter_client` cannot succeed for any of
1816-
them. The `original_client_id is None` escape in the unvalidated branch is unreachable once
1817-
CONNECTED has arrived. The branch should apply only where a clientId was configured, and
1818-
`__client_id` should become `'*'` otherwise, which is what RSA7b4 asks for.
1819-
1820-
**Tests affected:** none, which is why this is recorded here rather than gated. The three
1821-
`batch_presence.md` tests build their realtime client with `client_id='*'` instead, in
1822-
`entering_client()`, with the diagnosis in its docstring — the repository's own presence
1823-
suite carries the same workaround for the same reason, commented "Use wildcard auth for
1824-
enterClient" (`test/ably/realtime/realtimepresence_test.py:394-396`). It is setup rather
1825-
than subject: those tests are about `batchPresence`, and nothing they assert depends on how
1826-
the members got there. A fix would let the setup halves be written exactly as the
1827-
specification writes them. Both `realtime/integration` presence specifications carry the
1828-
same workaround for the same reason — `presence_lifecycle_test.py::test_rtp4_bulk_enter_observed`
1829-
and `presence/presence_sync_test.py::test_rtp2_sync_multiple_members` each build the client
1830-
that enters members on behalf of others with `client_id='*'` — which makes five tests
1831-
across two tiers that would otherwise be written as their specifications write them.
1832-
1833-
This is distinct from the wildcard-clientId contradiction recorded under UTS Spec Errors,
1834-
which is about a client that *does* configure `clientId: "*"`.
1835-
1836-
**Status:** open bug, one line from a fix.
1837-
18381791
#### Auth behaviour on the REST client
18391792

18401793
| Spec points | Behaviour |
@@ -2933,9 +2886,9 @@ none of these shows up as a failure — which is why they are easy to lose.
29332886

29342887
The five tiers above classify the realtime unit derivation; the REST unit derivation's
29352888
candidates went upstream as [#709](https://github.com/ably/ably-python/issues/709)–[#712](https://github.com/ably/ably-python/issues/712).
2936-
Five further defects came out of the two integration tiers, and none of them is filed; each
2937-
keeps the label it was first given. I.1 and I.2 are tier 2 by the ranking above — an error
2938-
where there should be none — and I.3 is tier 1 for a client that configures the option it
2889+
Four further defects came out of the two integration tiers, and none of them is filed; each
2890+
keeps the label it was first given. I.1 is tier 2 by the ranking above — an error where
2891+
there should be none — and I.3 is tier 1 for a client that configures the option it
29392892
concerns, since the call does not come back when the caller asked for it to. I.6 is tier 2
29402893
as well; I.5 is tier 3, a presence member silently leaving the set with nothing to report it.
29412894

@@ -2956,15 +2909,6 @@ offset. The same expired token renews correctly through `publish()`. A fix has t
29562909
the two meanings `raise_on_error` carries.
29572910
`test/uts/rest/integration/auth_test.py -k rsc10`
29582911

2959-
**I.2 `enterClient` cannot succeed on a basic-auth connection.** RSA7b4, RTP14, RTP15. A
2960-
connection built from a key alone is told `clientId: "*"`, and the branch of
2961-
`Auth._configure_client_id` (`ably/rest/auth.py:335-353`) that guards a configured clientId
2962-
against a server wildcard fires when there is no configured clientId, recording it as
2963-
validated and `None`. `can_assume_client_id` then refuses every clientId. One line from a
2964-
fix, and `enter_client` is unusable without one. No test gates on it — the three tests that
2965-
would hit it pass `client_id='*'` in setup instead, as the repository's own presence suite
2966-
does — so it will not show up as a failure.
2967-
29682912
**I.3 `httpRequestTimeout` is applied as seconds where the specification counts
29692913
milliseconds.** TO3l4, TO3l3, RSC15l2. `ably/http/http.py:193` hands
29702914
`(http_open_timeout, http_request_timeout)` to `httpx`, which reads seconds, so a client

‎test/uts/realtime/integration/presence/presence_sync_test.py‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@
1111
The specification is json only — it has no `## Protocol Variants` section — so these two
1212
take the JSON default `sandbox_realtime_client` applies rather than the
1313
`use_binary_protocol` fixture.
14-
15-
`enter_client` on a connection authenticated by key alone is refused 40012, a root cause
16-
recorded in [deviations.md](../../../deviations.md), so the client entering members on
17-
behalf of others is built with `client_id='*'`.
1814
"""
1915

2016
from ably.pubsub.realtime.connection import ConnectionState
@@ -61,7 +57,7 @@ async def test_rtp2_sync_multiple_members(realtime_sandbox):
6157
api_key = realtime_sandbox.key_str
6258
channel_name = 'presence-sync-multi-' + random_id()
6359

64-
client_a = sandbox_realtime_client(api_key, auto_connect=False, client_id='*')
60+
client_a = sandbox_realtime_client(api_key, auto_connect=False)
6561
client_b = sandbox_realtime_client(api_key, auto_connect=False)
6662

6763
client_a.connect()

‎test/uts/realtime/integration/presence_lifecycle_test.py‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,10 @@
1010
attach, and a presence action is named by its lowercase wire name — `'enter'`,
1111
`'present'`, `'update'`, `'leave'` — which is what `set_presence` emits.
1212
13-
Two adaptations, both of them root causes already recorded in
14-
[deviations.md](../../deviations.md):
15-
16-
- Subscribing to several actions at once (RTP6b) raises `TypeError: unhashable type:
17-
'list'`, because the list reaches pyee as a dict key. The one listener is registered
18-
once per action instead, which is what the array form means.
19-
- `enter_client` on a connection authenticated by key alone is refused 40012, so the
20-
client entering members on behalf of others is built with `client_id='*'`.
13+
One adaptation, a root cause already recorded in [deviations.md](../../deviations.md):
14+
subscribing to several actions at once (RTP6b) raises `TypeError: unhashable type:
15+
'list'`, because the list reaches pyee as a dict key. The one listener is registered once
16+
per action instead, which is what the array form means.
2117
"""
2218

2319
import asyncio
@@ -45,8 +41,7 @@ async def test_rtp4_bulk_enter_observed(realtime_sandbox, use_binary_protocol):
4541
api_key = realtime_sandbox.key_str
4642
channel_name = 'presence-bulk-' + random_id()
4743

48-
client_a = sandbox_realtime_client(
49-
api_key, use_binary_protocol=use_binary_protocol, client_id='*')
44+
client_a = sandbox_realtime_client(api_key, use_binary_protocol=use_binary_protocol)
5045
client_b = sandbox_realtime_client(api_key, use_binary_protocol=use_binary_protocol)
5146

5247
client_a.connect()

‎test/uts/rest/integration/batch_presence_test.py‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,13 @@ def member_for(entry, client_id):
4444
async def entering_client(key, use_binary_protocol):
4545
"""A CONNECTED realtime client that may enter presence for any clientId.
4646
47-
The specifications build this with the full-access key alone and call
48-
`enterClient` straight away. ably-python needs `client_id='*'` on top: a basic-auth
49-
connection is told `clientId: "*"` by the server, and `Auth._configure_client_id`
50-
answers a wildcard from the server by marking the client id validated while leaving
51-
it `None` (`ably/rest/auth.py:335`), after which `can_assume_client_id` refuses every
52-
id and `enter_client` raises 40012. The repository's own presence suite carries the
53-
same `client_id='*'` for the same reason
54-
(`test/ably/realtime/realtimepresence_test.py:396`).
47+
The specifications build this with the full-access key alone, which the server
48+
answers with the wildcard clientId `"*"` (RSA7b4).
5549
5650
The connection is awaited to CONNECTED before any enter, since a presence enter on a
5751
connection that is still CONNECTING is queued rather than sent.
5852
"""
59-
client = sandbox_realtime_client(key, client_id='*', use_binary_protocol=use_binary_protocol)
53+
client = sandbox_realtime_client(key, use_binary_protocol=use_binary_protocol)
6054
await await_connection_state(client, ConnectionState.CONNECTED)
6155
return client
6256

0 commit comments

Comments
 (0)