Skip to content

Commit 395f10d

Browse files
committed
tackle rejoin
1 parent 661d6a8 commit 395f10d

3 files changed

Lines changed: 110 additions & 34 deletions

File tree

lib/realtime_web/channels/realtime_channel.ex

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -94,38 +94,8 @@ defmodule RealtimeWeb.RealtimeChannel do
9494
maybe_replay_messages(params["config"], sub_topic, db_conn, tenant_id, socket.assigns.private?) do
9595
tenant_topic = Tenants.tenant_topic(tenant_id, sub_topic, !socket.assigns.private?)
9696

97-
# presence.read gate carried in the fastlane metadata so the dispatcher can withhold
98-
# presence_diff from members denied presence.read:
99-
# * public channel (no policies) -> true (no presence authorization, always receive diffs)
100-
# * private + presence enabled at join -> the authorized presence.read value (true/false)
101-
# * private + presence not enabled -> nil (read not evaluated yet). The dispatcher routes
102-
# these diffs to the channel process (handle_info) instead of fastlaning, where presence.read
103-
# is consulted at delivery time (it is authorized on-demand when presence is auto-enabled via
104-
# a track message - see PresenceHandler).
105-
presence_read? =
106-
case socket.assigns.policies do
107-
nil -> true
108-
%Policies{presence: %{read: read}} -> read
109-
end
110-
111-
broadcast_read? =
112-
case socket.assigns.policies do
113-
nil -> true
114-
%Policies{broadcast: %{read: read}} -> read
115-
end
116-
11797
# fastlane subscription
118-
metadata =
119-
MessageDispatcher.fastlane_metadata(
120-
transport_pid,
121-
serializer,
122-
topic,
123-
log_level,
124-
tenant_id,
125-
replayed_message_ids,
126-
presence_read?,
127-
broadcast_read?
128-
)
98+
metadata = fastlane_metadata(socket, replayed_message_ids)
12999

130100
RealtimeWeb.Endpoint.subscribe(tenant_topic, metadata: metadata)
131101
RealtimeWeb.Endpoint.subscribe("realtime:operations:" <> tenant_id, metadata: metadata)
@@ -160,7 +130,9 @@ defmodule RealtimeWeb.RealtimeChannel do
160130
self_broadcast: Join.self_broadcast?(join),
161131
tenant_topic: tenant_topic,
162132
channel_name: sub_topic,
163-
presence_enabled?: presence_enabled?
133+
presence_enabled?: presence_enabled?,
134+
fastlane_metadata: metadata,
135+
replayed_message_ids: replayed_message_ids
164136
}
165137

166138
assigns =
@@ -583,6 +555,8 @@ defmodule RealtimeWeb.RealtimeChannel do
583555
{:ok, db_conn} <- Connect.lookup_or_start_connection(tenant_id),
584556
{:ok, socket} <- maybe_assign_policies(channel_name, db_conn, socket),
585557
:ok <- check_read_permissions_revoked(previous_policies, socket.assigns.policies) do
558+
socket = maybe_resubscribe_fastlane(socket)
559+
586560
Helpers.cancel_timer(pg_sub_ref)
587561
pg_change_params = Enum.map(pg_change_params, &Map.put(&1, :claims, claims))
588562

@@ -1024,6 +998,46 @@ defmodule RealtimeWeb.RealtimeChannel do
1024998

1025999
defp maybe_assign_policies(_, _, socket), do: {:ok, assign(socket, policies: nil)}
10261000

1001+
# presence.read gate carried in the fastlane metadata so the dispatcher can withhold
1002+
# presence_diff from members denied presence.read:
1003+
# * public channel (no policies) -> true (no presence authorization, always receive diffs)
1004+
# * private + presence enabled at join -> the authorized presence.read value (true/false)
1005+
# * private + presence not enabled -> nil (read not evaluated yet). The dispatcher routes
1006+
# these diffs to the channel process (handle_info) instead of fastlaning, where presence.read
1007+
# is consulted at delivery time (it is authorized on-demand when presence is auto-enabled via
1008+
# a track message - see PresenceHandler).
1009+
defp fastlane_metadata(socket, replayed_message_ids) do
1010+
%{assigns: %{tenant: tenant_id, log_level: log_level, policies: policies}} = socket
1011+
1012+
MessageDispatcher.fastlane_metadata(
1013+
socket.transport_pid,
1014+
socket.serializer,
1015+
socket.topic,
1016+
log_level,
1017+
tenant_id,
1018+
replayed_message_ids,
1019+
if(policies, do: policies.presence.read, else: true),
1020+
if(policies, do: policies.broadcast.read, else: true)
1021+
)
1022+
end
1023+
1024+
defp maybe_resubscribe_fastlane(socket) do
1025+
%{assigns: %{fastlane_metadata: current, tenant: tenant_id, tenant_topic: tenant_topic}} = socket
1026+
1027+
case fastlane_metadata(socket, socket.assigns.replayed_message_ids) do
1028+
^current ->
1029+
socket
1030+
1031+
updated ->
1032+
for pubsub_topic <- [tenant_topic, "realtime:operations:" <> tenant_id] do
1033+
RealtimeWeb.Endpoint.unsubscribe(pubsub_topic)
1034+
RealtimeWeb.Endpoint.subscribe(pubsub_topic, metadata: updated)
1035+
end
1036+
1037+
assign(socket, :fastlane_metadata, updated)
1038+
end
1039+
end
1040+
10271041
defp can_replay?(%{"broadcast" => %{"replay" => _}}, topic, %{
10281042
assigns: %{policies: %Policies{broadcast: %BroadcastPolicies{read: false}}}
10291043
}),

test/integration/rt_channel/presence_test.exs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ defmodule Realtime.Integration.RtChannel.PresenceTest do
134134
{service_role_socket, _} = get_connection(tenant, serializer, role: "service_role")
135135
topic = "realtime:#{topic}"
136136

137-
WebsocketClient.join(socket, topic, %{config: %{presence: %{key: "", enabled: true}, private: true}})
137+
WebsocketClient.join(socket, topic, %{config: %{presence: %{key: "authenticated", enabled: true}, private: true}})
138138
assert_receive %Message{event: "phx_reply", payload: %{"status" => "ok"}, topic: ^topic}, 500
139139
assert_receive %Message{event: "presence_state", payload: %{}, topic: ^topic}, 500
140140

@@ -145,7 +145,7 @@ defmodule Realtime.Integration.RtChannel.PresenceTest do
145145
WebsocketClient.send_event(socket, topic, "presence", %{type: "presence", event: "TRACK", payload: payload})
146146

147147
assert_receive %Message{event: "presence_diff", payload: %{"joins" => joins, "leaves" => %{}}, topic: ^topic}, 500
148-
join_payload = joins |> Map.values() |> hd() |> get_in(["metas"]) |> hd()
148+
join_payload = joins |> get_in(["authenticated", "metas"]) |> hd()
149149
assert get_in(join_payload, ["name"]) == payload.name
150150

151151
broadcast = %{"event" => "TEST", "payload" => %{"msg" => 1}, "type" => "broadcast"}
@@ -490,6 +490,55 @@ defmodule Realtime.Integration.RtChannel.PresenceTest do
490490

491491
refute_receive %Message{event: "phx_close", topic: ^realtime_topic}, 500
492492
end
493+
494+
@tag policies: [
495+
:authenticated_read_presence,
496+
:authenticated_write_presence,
497+
:authenticated_read_broadcast_based_on_claim
498+
]
499+
test "starts receiving broadcasts when broadcast read permission is granted on new access_token",
500+
%{tenant: tenant, topic: topic, serializer: serializer} do
501+
parent = self()
502+
publisher_inbox = spawn_link(fn -> forward_frames(parent, :publisher) end)
503+
{:ok, publisher_token} = token_valid(tenant, "service_role")
504+
505+
{:ok, publisher} =
506+
WebsocketClient.connect(publisher_inbox, uri(tenant, serializer), serializer, [
507+
{"x-api-key", publisher_token}
508+
])
509+
510+
# Token whose claims do not satisfy the broadcast read policy, so the join is allowed on presence alone
511+
{socket, _} = get_connection(tenant, serializer, role: "authenticated", claims: %{broadcast_read: false})
512+
realtime_topic = "realtime:#{topic}"
513+
514+
WebsocketClient.join(socket, realtime_topic, %{
515+
config: %{presence: %{key: "authenticated", enabled: true}, private: true}
516+
})
517+
518+
assert_receive %Message{event: "phx_reply", payload: %{"status" => "ok"}, topic: ^realtime_topic}, 500
519+
assert_receive %Message{event: "presence_state", topic: ^realtime_topic}, 500
520+
521+
WebsocketClient.join(publisher, realtime_topic, %{config: %{private: true}})
522+
assert_receive {:publisher, %Message{event: "phx_reply", payload: %{"status" => "ok"}}}, 500
523+
524+
broadcast = %{"event" => "TEST", "payload" => %{"msg" => 1}, "type" => "broadcast"}
525+
WebsocketClient.send_event(publisher, realtime_topic, "broadcast", broadcast)
526+
refute_receive %Message{event: "broadcast", topic: ^realtime_topic}, 500
527+
528+
# New token whose claims satisfy the broadcast read policy
529+
{:ok, new_token} =
530+
generate_token(tenant, %{
531+
exp: System.system_time(:second) + 1000,
532+
role: "authenticated",
533+
broadcast_read: true
534+
})
535+
536+
WebsocketClient.send_event(socket, realtime_topic, "access_token", %{"access_token" => new_token})
537+
refute_receive %Message{event: "phx_close", topic: ^realtime_topic}, 500
538+
539+
WebsocketClient.send_event(publisher, realtime_topic, "broadcast", broadcast)
540+
assert_receive %Message{event: "broadcast", payload: ^broadcast, topic: ^realtime_topic}, 500
541+
end
493542
end
494543

495544
describe "database connection errors" do

test/support/generators.ex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,19 @@ defmodule Generators do
272272
"""
273273
end
274274

275+
def policy_query(:authenticated_read_broadcast_based_on_claim, %{topic: name}) do
276+
"""
277+
CREATE POLICY "authenticated_read_broadcast_claim_#{name}"
278+
ON realtime.messages FOR SELECT
279+
TO authenticated
280+
USING (
281+
realtime.topic() = '#{name}'
282+
AND realtime.messages.extension = 'broadcast'
283+
AND coalesce(((current_setting('request.jwt.claims', true))::jsonb ->> 'broadcast_read')::boolean, false)
284+
);
285+
"""
286+
end
287+
275288
def policy_query(:broken_read_presence, _) do
276289
"""
277290
CREATE POLICY "authenticated_read_presence"

0 commit comments

Comments
 (0)