neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
Let a client talk back, on its own uplink
e8ac20e · Gabriel Morell · 2026-09-03 16:01
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