diff --git a/lib/parrhesia/api/acl.ex b/lib/parrhesia/api/acl.ex index 609c6ae..02520c5 100644 --- a/lib/parrhesia/api/acl.ex +++ b/lib/parrhesia/api/acl.ex @@ -71,6 +71,9 @@ defmodule Parrhesia.API.ACL do `opts[:context]` defaults to an empty `Parrhesia.API.RequestContext`, which means protected subjects will fail with `{:error, :auth_required}` until authenticated pubkeys are present. + + Local callers bypass ACL enforcement entirely. ACL is intended to protect external sync traffic, + not trusted in-process calls. """ @spec check(atom(), map(), keyword()) :: :ok | {:error, term()} def check(capability, subject, opts \\ []) @@ -80,13 +83,8 @@ defmodule Parrhesia.API.ACL do context = Keyword.get(opts, :context, %RequestContext{}) with {:ok, normalized_capability} <- normalize_capability(capability), - {:ok, normalized_context} <- normalize_context(context), - {:ok, protected_filters} <- protected_filters() do - if protected_subject?(normalized_capability, subject, protected_filters) do - authorize_subject(normalized_capability, subject, normalized_context) - else - :ok - end + {:ok, normalized_context} <- normalize_context(context) do + maybe_authorize_subject(normalized_capability, subject, normalized_context) end end @@ -134,6 +132,18 @@ defmodule Parrhesia.API.ACL do end end + defp maybe_authorize_subject(_capability, _subject, %RequestContext{caller: :local}), do: :ok + + defp maybe_authorize_subject(capability, subject, %RequestContext{} = context) do + with {:ok, protected_filters} <- protected_filters() do + if protected_subject?(capability, subject, protected_filters) do + authorize_subject(capability, subject, context) + else + :ok + end + end + end + defp list_rules_for_capability(capability) do Storage.acl().list_rules(%{}, principal_type: :pubkey, capability: capability) end diff --git a/test/parrhesia/api/acl_test.exs b/test/parrhesia/api/acl_test.exs index 46f521d..82f2551 100644 --- a/test/parrhesia/api/acl_test.exs +++ b/test/parrhesia/api/acl_test.exs @@ -41,11 +41,14 @@ defmodule Parrhesia.API.ACLTest do authenticated_pubkey = String.duplicate("b", 64) assert {:error, :auth_required} = - ACL.check(:sync_read, filter, context: %RequestContext{}) + ACL.check(:sync_read, filter, context: %RequestContext{caller: :websocket}) assert {:error, :sync_read_not_allowed} = ACL.check(:sync_read, filter, - context: %RequestContext{authenticated_pubkeys: MapSet.new([authenticated_pubkey])} + context: %RequestContext{ + caller: :websocket, + authenticated_pubkeys: MapSet.new([authenticated_pubkey]) + } ) assert :ok = @@ -58,7 +61,10 @@ defmodule Parrhesia.API.ACLTest do assert :ok = ACL.check(:sync_read, filter, - context: %RequestContext{authenticated_pubkeys: MapSet.new([authenticated_pubkey])} + context: %RequestContext{ + caller: :websocket, + authenticated_pubkeys: MapSet.new([authenticated_pubkey]) + } ) end @@ -75,7 +81,38 @@ defmodule Parrhesia.API.ACLTest do assert {:error, :sync_read_not_allowed} = ACL.check(:sync_read, %{"kinds" => [5000]}, - context: %RequestContext{authenticated_pubkeys: MapSet.new([principal])} + context: %RequestContext{ + caller: :websocket, + authenticated_pubkeys: MapSet.new([principal]) + } + ) + end + + test "check/3 bypasses protected sync ACL for local callers" do + protected_filter = %{"kinds" => [5000], "#r" => ["tribes.accounts.user"]} + + assert :ok = + ACL.check(:sync_read, %{"ids" => [String.duplicate("d", 64)]}, + context: %RequestContext{caller: :local} + ) + + assert :ok = + ACL.check( + :sync_write, + %{ + "id" => String.duplicate("e", 64), + "kind" => 5000, + "tags" => [["r", "tribes.accounts.user"]] + }, + context: %RequestContext{caller: :local} + ) + + assert {:error, :sync_read_not_allowed} = + ACL.check(:sync_read, protected_filter, + context: %RequestContext{ + caller: :websocket, + authenticated_pubkeys: MapSet.new([String.duplicate("f", 64)]) + } ) end end diff --git a/test/parrhesia/api/events_test.exs b/test/parrhesia/api/events_test.exs index 315f333..43b453a 100644 --- a/test/parrhesia/api/events_test.exs +++ b/test/parrhesia/api/events_test.exs @@ -92,6 +92,37 @@ defmodule Parrhesia.API.EventsTest do ) end + test "local query can read protected sync events without ACL grants or kind scoping" do + previous_acl = Application.get_env(:parrhesia, :acl, []) + + Application.put_env( + :parrhesia, + :acl, + protected_filters: [%{"kinds" => [5000], "#r" => ["tribes.accounts.user"]}] + ) + + on_exit(fn -> + Application.put_env(:parrhesia, :acl, previous_acl) + end) + + protected_event = + valid_event(%{ + "kind" => 5000, + "tags" => [["r", "tribes.accounts.user"]], + "content" => "protected" + }) + + assert {:ok, %{accepted: true}} = + Events.publish(protected_event, context: %RequestContext{caller: :local}) + + assert {:ok, [stored_event]} = + Events.query([%{"ids" => [protected_event["id"]]}], + context: %RequestContext{caller: :local} + ) + + assert stored_event["id"] == protected_event["id"] + end + defp with_sync_relay_guard(enabled?) when is_boolean(enabled?) do [{:config, previous}] = :ets.lookup(Parrhesia.Config, :config)