Let a client talk back, on its own uplink

e8ac20e · Gabriel Morell · 2026-09-03 16:01

7 files +58 -29
Message
{commit_body(@commit)}

Files changed

modified apps/room_hermes/lib/room_hermes_web/controllers/resource_controller.ex
+6 −1
@@ -22,11 +22,16 @@ defmodule RoomHermesWeb.ResourceController do
22 22 end
23 23
24 24 {"/", "exchange"} ->
25 − IO.puts("exch")
26 25 case name do
26 + # Write is allowed at the exchange so a client can talk back --
27 + # a phone reporting where it is, for a foci that travels with it.
28 + # What it may write is not decided here: the topic check that
29 + # follows confines a client to its own uplink prefix, and this
30 + # would be far too broad on its own.
27 31 "amq.topic" ->
28 32 case permission do
29 33 "read" -> send_resp(conn, :ok, "allow")
34 + "write" -> send_resp(conn, :ok, "allow")
30 35 _otherwise -> send_resp(conn, :ok, "deny")
31 36 end
32 37
modified apps/room_hermes/lib/room_hermes_web/controllers/topic_controller.ex
+52 −28
@@ -6,39 +6,63 @@ defmodule RoomHermesWeb.TopicController do
6 6
7 7 action_fallback(RoomHermesWeb.FallbackController)
8 8
9 + @doc """
10 + Which routing keys a rabbit user may read and write.
11 +
12 + Reading is the whole of its own topic: the board, the image beside it, and
13 + anything else a Pythiae publishes there.
14 +
15 + Writing is confined to an uplink prefix, `<topic>.up.`. A client is not
16 + allowed to write the topic a Pythiae publishes on, because everything else
17 + subscribed to that Ankyra -- a panel on a wall, a phone in a pocket -- would
18 + believe what it said. The uplink is a different direction on the same
19 + connection, not the same channel back to front.
20 + """
9 21 def create(conn, %{
10 22 "username" => username,
11 − "vhost" => vhost,
12 − "resource" => resource,
13 − "name" => name,
23 + "vhost" => "/",
24 + "resource" => "topic",
25 + "name" => "amq.topic",
14 26 "permission" => permission,
15 27 "routing_key" => routing_key
16 − } = x) do
17 − user = Accounts.find_rabbit_user(username)
18 − IO.inspect(x)
19 − IO.inspect(user)
20 − case vhost do
21 − "/" ->
22 − case resource do
23 − "topic" ->
24 − case name do
25 − "amq.topic" ->
26 − if String.starts_with?(routing_key, user.topic) do
27 − send_resp(conn, :ok, "allow")
28 − else
29 − send_resp(conn, :ok, "deny")
30 − end
31 −
32 − _otherwise ->
33 − send_resp(conn, :ok, "deny")
34 − end
35 −
36 − _otherwise ->
37 − send_resp(conn, :ok, "deny")
38 − end
39 −
40 − _otherwise ->
28 + }) do
29 + case Accounts.find_rabbit_user(username) do
30 + nil ->
41 31 send_resp(conn, :ok, "deny")
32 +
33 + user ->
34 + if permitted?(user.topic, permission, routing_key) do
35 + send_resp(conn, :ok, "allow")
36 + else
37 + send_resp(conn, :ok, "deny")
38 + end
42 39 end
43 40 end
41 +
42 + def create(conn, _params), do: send_resp(conn, :ok, "deny")
43 +
44 + @doc """
45 + Whether a routing key is permitted, given the user's own topic.
46 +
47 + Public because it is the whole of the rule and worth exercising directly.
48 + There is no test here doing so: this app has no runnable test suite, its
49 + migrations expecting tables the sanctum repo owns, and giving it one is its
50 + own piece of work. Until then:
51 +
52 + mix run --no-start -e 'IO.inspect RoomHermesWeb.TopicController.permitted?("T", "write", "T.up.loc")'
53 + """
54 + def permitted?(topic, "read", routing_key), do: String.starts_with?(routing_key, topic)
55 +
56 + def permitted?(topic, "write", routing_key),
57 + do: String.starts_with?(routing_key, uplink_prefix(topic))
58 +
59 + def permitted?(_topic, _permission, _routing_key), do: false
60 +
61 + @doc """
62 + Where a client publishes to, for a given Ankyra topic.
63 +
64 + Named here rather than in each caller so the two ends of the rule cannot
65 + drift: this is both what the broker allows and what a client should send to.
66 + """
67 + def uplink_prefix(topic), do: topic <> ".up."
44 68 end

Parents: c3437b5