fix: bypass ACL for local callers
This commit is contained in:
@@ -71,6 +71,9 @@ defmodule Parrhesia.API.ACL do
|
|||||||
|
|
||||||
`opts[:context]` defaults to an empty `Parrhesia.API.RequestContext`, which means protected
|
`opts[:context]` defaults to an empty `Parrhesia.API.RequestContext`, which means protected
|
||||||
subjects will fail with `{:error, :auth_required}` until authenticated pubkeys are present.
|
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()}
|
@spec check(atom(), map(), keyword()) :: :ok | {:error, term()}
|
||||||
def check(capability, subject, opts \\ [])
|
def check(capability, subject, opts \\ [])
|
||||||
@@ -80,13 +83,8 @@ defmodule Parrhesia.API.ACL do
|
|||||||
context = Keyword.get(opts, :context, %RequestContext{})
|
context = Keyword.get(opts, :context, %RequestContext{})
|
||||||
|
|
||||||
with {:ok, normalized_capability} <- normalize_capability(capability),
|
with {:ok, normalized_capability} <- normalize_capability(capability),
|
||||||
{:ok, normalized_context} <- normalize_context(context),
|
{:ok, normalized_context} <- normalize_context(context) do
|
||||||
{:ok, protected_filters} <- protected_filters() do
|
maybe_authorize_subject(normalized_capability, subject, normalized_context)
|
||||||
if protected_subject?(normalized_capability, subject, protected_filters) do
|
|
||||||
authorize_subject(normalized_capability, subject, normalized_context)
|
|
||||||
else
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -134,6 +132,18 @@ defmodule Parrhesia.API.ACL do
|
|||||||
end
|
end
|
||||||
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
|
defp list_rules_for_capability(capability) do
|
||||||
Storage.acl().list_rules(%{}, principal_type: :pubkey, capability: capability)
|
Storage.acl().list_rules(%{}, principal_type: :pubkey, capability: capability)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -41,11 +41,14 @@ defmodule Parrhesia.API.ACLTest do
|
|||||||
authenticated_pubkey = String.duplicate("b", 64)
|
authenticated_pubkey = String.duplicate("b", 64)
|
||||||
|
|
||||||
assert {:error, :auth_required} =
|
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} =
|
assert {:error, :sync_read_not_allowed} =
|
||||||
ACL.check(:sync_read, filter,
|
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 =
|
assert :ok =
|
||||||
@@ -58,7 +61,10 @@ defmodule Parrhesia.API.ACLTest do
|
|||||||
|
|
||||||
assert :ok =
|
assert :ok =
|
||||||
ACL.check(:sync_read, filter,
|
ACL.check(:sync_read, filter,
|
||||||
context: %RequestContext{authenticated_pubkeys: MapSet.new([authenticated_pubkey])}
|
context: %RequestContext{
|
||||||
|
caller: :websocket,
|
||||||
|
authenticated_pubkeys: MapSet.new([authenticated_pubkey])
|
||||||
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -75,7 +81,38 @@ defmodule Parrhesia.API.ACLTest do
|
|||||||
|
|
||||||
assert {:error, :sync_read_not_allowed} =
|
assert {:error, :sync_read_not_allowed} =
|
||||||
ACL.check(:sync_read, %{"kinds" => [5000]},
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -92,6 +92,37 @@ defmodule Parrhesia.API.EventsTest do
|
|||||||
)
|
)
|
||||||
end
|
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
|
defp with_sync_relay_guard(enabled?) when is_boolean(enabled?) do
|
||||||
[{:config, previous}] = :ets.lookup(Parrhesia.Config, :config)
|
[{:config, previous}] = :ets.lookup(Parrhesia.Config, :config)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user