Skip to content

Commit 538ae75

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 c312809 commit 538ae75

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
@@ -359,8 +359,8 @@ def _configure_client_id(self, new_client_id):
359359
return
360360

361361
# If new client ID from Ably is a wildcard, but preconfigured clientId is set,
362-
# then keep the existing clientId
363-
if original_client_id != '*' and new_client_id == '*':
362+
# then keep the existing clientId. With none set, the wildcard is the clientId (RSA7b4)
363+
if original_client_id is not None and original_client_id != '*' and new_client_id == '*':
364364
self.__client_id_validated = True
365365
self.__client_id = original_client_id
366366
return
@@ -386,11 +386,12 @@ def can_assume_client_id(self, assumed_client_id):
386386

387387
async def _get_auth_headers(self):
388388
if self.__auth_mechanism == Auth.Method.BASIC:
389-
# RSA7e2
390-
if self.client_id:
389+
# RSA7e2: the configured clientId, not a wildcard the server reported
390+
client_id = self.auth_options.client_id
391+
if client_id:
391392
return {
392393
'Authorization': f'Basic {self.basic_credentials}',
393-
'X-Ably-ClientId': base64.b64encode(self.client_id.encode('utf-8'))
394+
'X-Ably-ClientId': base64.b64encode(client_id.encode('utf-8'))
394395
}
395396
return {
396397
'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 146 gated Test IDs that record SDK non-compliance — 146 tests, 148 cases — reduce to
55-
**62 distinct root causes**, 21 on the REST side and 41 on the realtime side. Three further
55+
**62 distinct root causes**, 21 on the REST side and 41 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 **65 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 **64 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.
@@ -1813,51 +1811,6 @@ JWT, fall with it.
18131811
renew and retry on a token error whatever the flag says, and only then decide whether the
18141812
final response is raised or returned.
18151813

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

18631816
| Spec points | Behaviour |
@@ -3011,9 +2964,9 @@ none of these shows up as a failure — which is why they are easy to lose.
30112964

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

@@ -3034,15 +2987,6 @@ offset. The same expired token renews correctly through `publish()`. A fix has t
30342987
the two meanings `raise_on_error` carries.
30352988
`test/uts/rest/integration/auth_test.py -k rsc10`
30362989

3037-
**I.2 `enterClient` cannot succeed on a basic-auth connection.** RSA7b4, RTP14, RTP15. A
3038-
connection built from a key alone is told `clientId: "*"`, and the branch of
3039-
`Auth._configure_client_id` (`ably/rest/auth.py:335-353`) that guards a configured clientId
3040-
against a server wildcard fires when there is no configured clientId, recording it as
3041-
validated and `None`. `can_assume_client_id` then refuses every clientId. One line from a
3042-
fix, and `enter_client` is unusable without one. No test gates on it — the three tests that
3043-
would hit it pass `client_id='*'` in setup instead, as the repository's own presence suite
3044-
does — so it will not show up as a failure.
3045-
30462990
**I.3 `httpRequestTimeout` is applied as seconds where the specification counts
30472991
milliseconds.** TO3l4, TO3l3, RSC15l2. `ably/http/http.py:193` hands
30482992
`(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)