You've already forked tribes-plugin-trust
157106aa66
CI / Test (push) Failing after 16s
Rename the Trust plugin identity to tribe-one-trust and update config namespace/docs accordingly.
333 lines
12 KiB
Elixir
333 lines
12 KiB
Elixir
defmodule TribeOne.TribesPlugin.Trust do
|
|
@moduledoc false
|
|
|
|
alias TribeOne.TribesPlugin.Trust.Domain
|
|
|
|
@federation_capabilities [
|
|
"org.tribes.federation.hello@1",
|
|
"org.tribes.federation.relationship.status@1",
|
|
"org.tribes.trust.attest@1"
|
|
]
|
|
|
|
def federation_capabilities, do: @federation_capabilities
|
|
|
|
def federation_hidden? do
|
|
case Tribes.ConfigStore.get_for_plugin("tribe-one-trust", "federation.hidden", default: false) do
|
|
{:ok, value} -> value in [true, "true", "1", "on"]
|
|
{:error, _reason} -> false
|
|
end
|
|
end
|
|
|
|
def set_federation_hidden?(value, opts \\ []) when is_boolean(value) do
|
|
Tribes.ConfigStore.put_for_plugin("tribe-one-trust", "federation.hidden", value, opts)
|
|
end
|
|
|
|
def receive_hello(attrs, opts \\ []) when is_map(attrs) do
|
|
with {:ok, tribe_attrs} <- normalize_remote_tribe_attrs(attrs),
|
|
{:ok, relationship_attrs} <- normalize_inbound_relationship_attrs(attrs),
|
|
{:ok, tribe} <- Domain.upsert_remote_tribe(tribe_attrs, default_trust_opts(opts)),
|
|
{:ok, relationship} <-
|
|
upsert_inbound_relationship(tribe.pubkey, relationship_attrs, opts) do
|
|
{:ok, %{tribe: tribe, relationship: relationship}}
|
|
end
|
|
end
|
|
|
|
def observe_remote_tribe(attrs, opts \\ []) when is_map(attrs) do
|
|
with {:ok, tribe_attrs} <- normalize_remote_tribe_attrs(attrs),
|
|
{:ok, relationship_attrs} <- normalize_observed_relationship_attrs(attrs),
|
|
{:ok, tribe} <- Domain.upsert_remote_tribe(tribe_attrs, default_trust_opts(opts)),
|
|
{:ok, relationship} <-
|
|
ensure_observed_relationship(tribe.pubkey, relationship_attrs, opts) do
|
|
{:ok, %{tribe: tribe, relationship: relationship}}
|
|
end
|
|
end
|
|
|
|
def list_relationship_rows(opts \\ []) do
|
|
with {:ok, tribes} <- Domain.list_remote_tribes(default_trust_opts(opts)),
|
|
{:ok, relationships} <- Domain.list_tribe_relationships(default_trust_opts(opts)) do
|
|
tribes_by_pubkey = Map.new(tribes, &{&1.pubkey, &1})
|
|
|
|
rows =
|
|
relationships
|
|
|> Enum.map(fn relationship ->
|
|
%{
|
|
tribe: Map.get(tribes_by_pubkey, relationship.remote_tribe_pubkey),
|
|
relationship: relationship
|
|
}
|
|
end)
|
|
|> Enum.sort_by(fn %{relationship: relationship} ->
|
|
{status_rank(relationship.status), relationship.updated_at || relationship.inserted_at}
|
|
end)
|
|
|
|
{:ok, rows}
|
|
end
|
|
end
|
|
|
|
def access_trust_facts(%{type: "tribe", id: pubkey}), do: access_trust_facts(pubkey)
|
|
def access_trust_facts(%{"type" => "tribe", "id" => pubkey}), do: access_trust_facts(pubkey)
|
|
|
|
def access_trust_facts(pubkey) when is_binary(pubkey) do
|
|
with {:ok, pubkey} <- normalize_pubkey(pubkey),
|
|
{:ok, relationship} <-
|
|
Domain.get_tribe_relationship(
|
|
pubkey,
|
|
Keyword.put(default_trust_opts([]), :not_found_error?, false)
|
|
) do
|
|
{:ok,
|
|
%{
|
|
status: relationship_status(relationship),
|
|
trust_score: relationship_trust_score(relationship),
|
|
granted_capabilities: relationship_capabilities(relationship)
|
|
}}
|
|
end
|
|
end
|
|
|
|
def access_trust_facts(_subject), do: {:error, :unsupported_subject}
|
|
|
|
def update_relationship(remote_pubkey, attrs, opts \\ []) when is_binary(remote_pubkey) do
|
|
with {:ok, pubkey} <- normalize_pubkey(remote_pubkey),
|
|
{:ok, existing} <-
|
|
Domain.get_tribe_relationship(
|
|
pubkey,
|
|
Keyword.put(default_trust_opts(opts), :not_found_error?, false)
|
|
),
|
|
{:ok, attrs} <- normalize_relationship_update(attrs, existing) do
|
|
Domain.upsert_tribe_relationship(attrs, default_trust_opts(opts))
|
|
end
|
|
end
|
|
|
|
defp upsert_inbound_relationship(remote_pubkey, attrs, opts) do
|
|
existing =
|
|
case Domain.get_tribe_relationship(
|
|
remote_pubkey,
|
|
Keyword.put(default_trust_opts(opts), :not_found_error?, false)
|
|
) do
|
|
{:ok, relationship} -> relationship
|
|
{:error, _reason} -> nil
|
|
end
|
|
|
|
status =
|
|
case existing do
|
|
%{status: status} when status in [:active, :blocked, :revoked] -> status
|
|
_other -> :pending_inbound
|
|
end
|
|
|
|
attrs
|
|
|> Map.merge(%{remote_tribe_pubkey: remote_pubkey, status: status})
|
|
|> Domain.upsert_tribe_relationship(default_trust_opts(opts))
|
|
end
|
|
|
|
defp ensure_observed_relationship(remote_pubkey, attrs, opts) do
|
|
case Domain.get_tribe_relationship(
|
|
remote_pubkey,
|
|
Keyword.put(default_trust_opts(opts), :not_found_error?, false)
|
|
) do
|
|
{:ok, nil} ->
|
|
Domain.upsert_tribe_relationship(
|
|
Map.merge(
|
|
%{
|
|
remote_tribe_pubkey: remote_pubkey,
|
|
status: :observed,
|
|
requested_capabilities: [],
|
|
granted_capabilities: [],
|
|
trust_score: 0
|
|
},
|
|
attrs
|
|
),
|
|
default_trust_opts(opts)
|
|
)
|
|
|
|
{:ok, relationship} ->
|
|
{:ok, relationship}
|
|
|
|
{:error, _reason} = error ->
|
|
error
|
|
end
|
|
end
|
|
|
|
defp normalize_remote_tribe_attrs(attrs) do
|
|
with {:ok, pubkey} <-
|
|
normalize_pubkey(attrs["from_tribe"] || attrs["pubkey"] || attrs[:pubkey]) do
|
|
{:ok,
|
|
%{
|
|
pubkey: pubkey,
|
|
name: normalize_optional_string(attrs["name"] || attrs[:name]) || pubkey,
|
|
description: normalize_optional_string(attrs["description"] || attrs[:description]),
|
|
last_seen_at: DateTime.utc_now()
|
|
}}
|
|
end
|
|
end
|
|
|
|
defp normalize_inbound_relationship_attrs(attrs) do
|
|
with {:ok, requested_capabilities} <-
|
|
normalize_string_list(
|
|
attrs["requested_capabilities"] || attrs[:requested_capabilities] || []
|
|
),
|
|
{:ok, remote_relay_urls} <-
|
|
normalize_string_list(attrs["relay_urls"] || attrs[:relay_urls] || []),
|
|
{:ok, remote_capabilities} <-
|
|
normalize_string_list(attrs["capabilities"] || attrs[:capabilities] || []) do
|
|
{:ok,
|
|
%{
|
|
remote_api_url: normalize_optional_string(attrs["api_url"] || attrs[:api_url]),
|
|
remote_profile_url:
|
|
normalize_optional_string(attrs["profile_url"] || attrs[:profile_url]),
|
|
remote_relay_urls: remote_relay_urls,
|
|
remote_capabilities: remote_capabilities,
|
|
requested_capabilities: requested_capabilities,
|
|
inbound_message: normalize_optional_string(attrs["message"] || attrs[:message]),
|
|
last_inbound_at: DateTime.utc_now()
|
|
}}
|
|
end
|
|
end
|
|
|
|
defp normalize_observed_relationship_attrs(attrs) do
|
|
with {:ok, remote_relay_urls} <-
|
|
normalize_string_list(attrs["relay_urls"] || attrs[:relay_urls] || []),
|
|
{:ok, remote_capabilities} <-
|
|
normalize_string_list(attrs["capabilities"] || attrs[:capabilities] || []) do
|
|
{:ok,
|
|
%{
|
|
remote_api_url: normalize_optional_string(attrs["api_url"] || attrs[:api_url]),
|
|
remote_profile_url:
|
|
normalize_optional_string(attrs["profile_url"] || attrs[:profile_url]),
|
|
remote_relay_urls: remote_relay_urls,
|
|
remote_capabilities: remote_capabilities
|
|
}}
|
|
end
|
|
end
|
|
|
|
defp normalize_relationship_update(_attrs, nil), do: {:error, :relationship_not_found}
|
|
|
|
defp normalize_relationship_update(attrs, existing) do
|
|
with {:ok, status} <- normalize_status(attrs["status"] || attrs[:status] || existing.status),
|
|
{:ok, trust_score} <-
|
|
normalize_trust_score(
|
|
attrs["trust_score"] || attrs[:trust_score] || existing.trust_score
|
|
),
|
|
{:ok, granted_capabilities} <-
|
|
normalize_capability_input(
|
|
attrs["granted_capabilities"] || attrs[:granted_capabilities] ||
|
|
existing.granted_capabilities || []
|
|
) do
|
|
{:ok,
|
|
%{
|
|
remote_tribe_pubkey: existing.remote_tribe_pubkey,
|
|
status: status,
|
|
remote_api_url: existing.remote_api_url,
|
|
remote_profile_url: existing.remote_profile_url,
|
|
remote_relay_urls: existing.remote_relay_urls || [],
|
|
remote_capabilities: existing.remote_capabilities || [],
|
|
requested_capabilities: existing.requested_capabilities || [],
|
|
granted_capabilities: granted_capabilities,
|
|
trust_score: trust_score,
|
|
trust_note:
|
|
normalize_optional_string(
|
|
attrs["trust_note"] || attrs[:trust_note] || existing.trust_note
|
|
),
|
|
inbound_message: existing.inbound_message,
|
|
last_inbound_at: existing.last_inbound_at,
|
|
last_outbound_at: existing.last_outbound_at
|
|
}}
|
|
end
|
|
end
|
|
|
|
defp normalize_capability_input(value) when is_binary(value) do
|
|
value
|
|
|> String.split([",", "\n"], trim: true)
|
|
|> Enum.map(&String.trim/1)
|
|
|> normalize_string_list()
|
|
end
|
|
|
|
defp normalize_capability_input(value), do: normalize_string_list(value)
|
|
|
|
defp normalize_pubkey(value) when is_binary(value) do
|
|
pubkey = value |> String.trim() |> String.downcase()
|
|
|
|
if String.match?(pubkey, ~r/\A[0-9a-f]{64}\z/) do
|
|
{:ok, pubkey}
|
|
else
|
|
{:error, :invalid_pubkey}
|
|
end
|
|
end
|
|
|
|
defp normalize_pubkey(_value), do: {:error, :invalid_pubkey}
|
|
|
|
defp normalize_status(:observed), do: {:ok, :observed}
|
|
defp normalize_status(:pending_inbound), do: {:ok, :pending_inbound}
|
|
defp normalize_status(:pending_outbound), do: {:ok, :pending_outbound}
|
|
defp normalize_status(:active), do: {:ok, :active}
|
|
defp normalize_status(:blocked), do: {:ok, :blocked}
|
|
defp normalize_status(:revoked), do: {:ok, :revoked}
|
|
defp normalize_status("observed"), do: {:ok, :observed}
|
|
defp normalize_status("pending_inbound"), do: {:ok, :pending_inbound}
|
|
defp normalize_status("pending_outbound"), do: {:ok, :pending_outbound}
|
|
defp normalize_status("active"), do: {:ok, :active}
|
|
defp normalize_status("blocked"), do: {:ok, :blocked}
|
|
defp normalize_status("revoked"), do: {:ok, :revoked}
|
|
defp normalize_status(_status), do: {:error, :invalid_status}
|
|
|
|
defp normalize_trust_score(value) when is_integer(value) and value >= -100 and value <= 100,
|
|
do: {:ok, value}
|
|
|
|
defp normalize_trust_score(value) when is_binary(value) do
|
|
case Integer.parse(String.trim(value)) do
|
|
{score, ""} -> normalize_trust_score(score)
|
|
_other -> {:error, :invalid_trust_score}
|
|
end
|
|
end
|
|
|
|
defp normalize_trust_score(_value), do: {:error, :invalid_trust_score}
|
|
|
|
defp normalize_string_list(values) when is_list(values) do
|
|
values
|
|
|> Enum.reduce_while({:ok, []}, fn
|
|
value, {:ok, acc} when is_binary(value) ->
|
|
trimmed = String.trim(value)
|
|
next = if trimmed == "", do: acc, else: [trimmed | acc]
|
|
{:cont, {:ok, next}}
|
|
|
|
_value, _acc ->
|
|
{:halt, {:error, :invalid_string_list}}
|
|
end)
|
|
|> case do
|
|
{:ok, values} -> {:ok, Enum.reverse(values)}
|
|
{:error, _reason} = error -> error
|
|
end
|
|
end
|
|
|
|
defp normalize_string_list(_values), do: {:error, :invalid_string_list}
|
|
|
|
defp normalize_optional_string(nil), do: nil
|
|
|
|
defp normalize_optional_string(value) when is_binary(value) do
|
|
case String.trim(value) do
|
|
"" -> nil
|
|
trimmed -> trimmed
|
|
end
|
|
end
|
|
|
|
defp normalize_optional_string(_value), do: nil
|
|
|
|
defp status_rank(:pending_inbound), do: 0
|
|
defp status_rank(:pending_outbound), do: 1
|
|
defp status_rank(:active), do: 2
|
|
defp status_rank(:observed), do: 3
|
|
defp status_rank(:blocked), do: 4
|
|
defp status_rank(:revoked), do: 5
|
|
defp status_rank(_status), do: 6
|
|
|
|
defp relationship_status(nil), do: "unknown"
|
|
defp relationship_status(%{status: status}), do: to_string(status)
|
|
|
|
defp relationship_trust_score(nil), do: nil
|
|
defp relationship_trust_score(%{trust_score: trust_score}), do: trust_score
|
|
|
|
defp relationship_capabilities(nil), do: []
|
|
defp relationship_capabilities(%{granted_capabilities: capabilities}), do: capabilities || []
|
|
|
|
defp default_trust_opts(opts) do
|
|
Keyword.put_new(opts, :context, %{private: %{system?: true, system_purpose: :trust_plugin}})
|
|
end
|
|
end
|