Refactor ingress to listener-based configuration

This commit is contained in:
2026-03-16 23:47:17 +01:00
parent 5f4f086d28
commit 1f608ee2bd
18 changed files with 1231 additions and 210 deletions

View File

@@ -11,7 +11,6 @@ defmodule Parrhesia.ApplicationTest do
assert is_pid(Process.whereis(Parrhesia.Sync.Supervisor))
assert is_pid(Process.whereis(Parrhesia.Policy.Supervisor))
assert is_pid(Process.whereis(Parrhesia.Web.Endpoint))
assert is_pid(Process.whereis(Parrhesia.Web.MetricsEndpoint))
assert is_pid(Process.whereis(Parrhesia.Tasks.Supervisor))
assert Enum.any?(Supervisor.which_children(Parrhesia.Web.Endpoint), fn {_id, pid, _type,

View File

@@ -123,6 +123,69 @@ defmodule Parrhesia.Web.ConnectionTest do
Enum.find(decoded, fn frame -> List.first(frame) == "OK" end)
end
test "listener can require NIP-42 for reads and writes" do
listener =
listener(%{
auth: %{nip42_required: true, nip98_required_for_admin: true}
})
state = connection_state(listener: listener)
req_payload = JSON.encode!(["REQ", "sub-auth", %{"kinds" => [1]}])
assert {:push, frames, ^state} = Connection.handle_in({req_payload, [opcode: :text]}, state)
assert Enum.map(frames, fn {:text, frame} -> JSON.decode!(frame) end) == [
["AUTH", state.auth_challenge],
["CLOSED", "sub-auth", "auth-required: authentication required"]
]
event = valid_event(%{"content" => "auth required"})
assert {:push, event_frames, ^state} =
Connection.handle_in({JSON.encode!(["EVENT", event]), [opcode: :text]}, state)
decoded = Enum.map(event_frames, fn {:text, frame} -> JSON.decode!(frame) end)
assert ["AUTH", state.auth_challenge] in decoded
assert ["OK", event["id"], false, "auth-required: authentication required"] in decoded
end
test "listener baseline ACL can deny read and write shapes before sync ACLs" do
listener =
listener(%{
baseline_acl: %{
read: [%{action: :deny, match: %{"kinds" => [5000]}}],
write: [%{action: :deny, match: %{"kinds" => [5000]}}]
}
})
state = connection_state(listener: listener)
req_payload = JSON.encode!(["REQ", "sub-baseline", %{"kinds" => [5000]}])
assert {:push, req_frames, ^state} =
Connection.handle_in({req_payload, [opcode: :text]}, state)
assert Enum.map(req_frames, fn {:text, frame} -> JSON.decode!(frame) end) == [
["AUTH", state.auth_challenge],
["CLOSED", "sub-baseline", "restricted: listener baseline denies requested filters"]
]
event =
valid_event(%{"kind" => 5000, "content" => "baseline blocked"}) |> recalculate_event_id()
assert {:push, {:text, response}, ^state} =
Connection.handle_in({JSON.encode!(["EVENT", event]), [opcode: :text]}, state)
assert JSON.decode!(response) == [
"OK",
event["id"],
false,
"restricted: listener baseline denies event"
]
end
test "protected sync REQ requires matching ACL grant" do
previous_acl = Application.get_env(:parrhesia, :acl, [])
@@ -766,6 +829,27 @@ defmodule Parrhesia.Web.ConnectionTest do
state
end
defp listener(overrides) do
base = %{
id: :test,
enabled: true,
bind: %{ip: {127, 0, 0, 1}, port: 4413},
transport: %{scheme: :http, tls: %{mode: :disabled}},
proxy: %{trusted_cidrs: [], honor_x_forwarded_for: true},
network: %{allow_all: true},
features: %{
nostr: %{enabled: true},
admin: %{enabled: true},
metrics: %{enabled: false, access: %{allow_all: true}, auth_token: nil}
},
auth: %{nip42_required: false, nip98_required_for_admin: true},
baseline_acl: %{read: [], write: []},
bandit_options: []
}
Map.merge(base, overrides)
end
defp live_event(id, kind) do
%{
"id" => id,

View File

@@ -7,6 +7,7 @@ defmodule Parrhesia.Web.RouterTest do
alias Ecto.Adapters.SQL.Sandbox
alias Parrhesia.Protocol.EventValidator
alias Parrhesia.Repo
alias Parrhesia.Web.Listener
alias Parrhesia.Web.Router
setup do
@@ -44,7 +45,13 @@ defmodule Parrhesia.Web.RouterTest do
end
test "GET /metrics returns prometheus payload for private-network clients" do
conn = conn(:get, "/metrics") |> Router.call([])
conn =
conn(:get, "/metrics")
|> route_conn(
listener(%{
features: %{metrics: %{enabled: true, access: %{private_networks_only: true}}}
})
)
assert conn.status == 200
assert get_resp_header(conn, "content-type") == ["text/plain; charset=utf-8"]
@@ -53,6 +60,14 @@ defmodule Parrhesia.Web.RouterTest do
test "GET /metrics denies public-network clients by default" do
conn = conn(:get, "/metrics")
conn = %{conn | remote_ip: {8, 8, 8, 8}}
test_listener =
listener(%{features: %{metrics: %{enabled: true, access: %{private_networks_only: true}}}})
conn = Listener.put_conn(conn, listener: test_listener)
refute Listener.metrics_allowed?(Listener.from_conn(conn), conn)
conn = Router.call(conn, [])
assert conn.status == 403
@@ -60,47 +75,34 @@ defmodule Parrhesia.Web.RouterTest do
end
test "GET /metrics can be disabled on the main endpoint" do
previous_metrics = Application.get_env(:parrhesia, :metrics, [])
Application.put_env(
:parrhesia,
:metrics,
Keyword.put(previous_metrics, :enabled_on_main_endpoint, false)
)
on_exit(fn ->
Application.put_env(:parrhesia, :metrics, previous_metrics)
end)
conn = conn(:get, "/metrics") |> Router.call([])
conn =
conn(:get, "/metrics")
|> route_conn(listener(%{features: %{metrics: %{enabled: false}}}))
assert conn.status == 404
assert conn.resp_body == "not found"
end
test "GET /metrics accepts bearer auth when configured" do
previous_metrics = Application.get_env(:parrhesia, :metrics, [])
test_listener =
listener(%{
features: %{
metrics: %{
enabled: true,
access: %{private_networks_only: false},
auth_token: "secret-token"
}
}
})
Application.put_env(
:parrhesia,
:metrics,
previous_metrics
|> Keyword.put(:private_networks_only, false)
|> Keyword.put(:auth_token, "secret-token")
)
on_exit(fn ->
Application.put_env(:parrhesia, :metrics, previous_metrics)
end)
denied_conn = conn(:get, "/metrics") |> Router.call([])
denied_conn = conn(:get, "/metrics") |> route_conn(test_listener)
assert denied_conn.status == 403
allowed_conn =
conn(:get, "/metrics")
|> put_req_header("authorization", "Bearer secret-token")
|> Router.call([])
|> route_conn(test_listener)
assert allowed_conn.status == 200
end
@@ -247,6 +249,15 @@ defmodule Parrhesia.Web.RouterTest do
assert byte_size(pubkey) == 64
end
test "POST /management returns not found when admin feature is disabled on the listener" do
conn =
conn(:post, "/management", JSON.encode!(%{"method" => "ping", "params" => %{}}))
|> put_req_header("content-type", "application/json")
|> route_conn(listener(%{features: %{admin: %{enabled: false}}}))
assert conn.status == 404
end
defp nip98_event(method, url) do
now = System.system_time(:second)
@@ -261,4 +272,41 @@ defmodule Parrhesia.Web.RouterTest do
Map.put(base, "id", EventValidator.compute_id(base))
end
defp listener(overrides) do
deep_merge(
%{
id: :test,
enabled: true,
bind: %{ip: {127, 0, 0, 1}, port: 4413},
transport: %{scheme: :http, tls: %{mode: :disabled}},
proxy: %{trusted_cidrs: [], honor_x_forwarded_for: true},
network: %{allow_all: true},
features: %{
nostr: %{enabled: true},
admin: %{enabled: true},
metrics: %{enabled: true, access: %{private_networks_only: true}, auth_token: nil}
},
auth: %{nip42_required: false, nip98_required_for_admin: true},
baseline_acl: %{read: [], write: []}
},
overrides
)
end
defp deep_merge(left, right) when is_map(left) and is_map(right) do
Map.merge(left, right, fn _key, left_value, right_value ->
if is_map(left_value) and is_map(right_value) do
deep_merge(left_value, right_value)
else
right_value
end
end)
end
defp route_conn(conn, listener) do
conn
|> Listener.put_conn(listener: listener)
|> Router.call([])
end
end