balls/lib/balls_pds/util/acl.ex

139 lines
3.9 KiB
Elixir
Raw Normal View History

2024-12-02 11:03:06 +00:00
defmodule BallsPDS.Util.ACL do
import Ecto.Changeset
alias BallsPDS.Util.Base58
@web_did_regex ~r/^did:web:(?<domain>(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)(?:%3A(?<port>\d+))?(?:(?::|\/)+(?<path>[^:\/][^\/]*(?:[:\/][^\/]+)*))?$/i
def parse_web_did("did:web:" <> _ = did) do
case Regex.named_captures(@web_did_regex, did) do
nil ->
{:error, :invalid}
%{
"domain" => domain,
"port" => port,
"path" => colon_separated_path
} ->
{:ok,
%{
domain: domain,
port: port,
path: colon_separated_path |> String.replace(":", "/")
}}
end
end
def is_did_key_field?(field, "did:key:" <> key) do
case Base58.decode(key) do
{:ok, <<0xED, _key::binary-size(32)>>} ->
[]
{:ok, <<0xED>> <> _} ->
[{field, "ACL ED25519 did:key invalid length"}]
{:ok, _} ->
[{field, "ACL did:key not ED25519"}]
{:error, _error} ->
[{field, "Invalid base58 for did:key ACL"}]
end
end
def is_did?("did:web:" <> _ = did), do: Regex.match?(@web_did_regex, did)
def is_did?("did:key:" <> _ = did), do: is_did_key_field?(nil, did) == []
def is_valid_url?(url) when is_binary(url) do
with true <- String.match?(url, ~r/^https?:\/\//i),
%URI{scheme: scheme, host: host} when is_binary(host) and scheme in ["https", "http"] <-
URI.parse(url) do
true
else
_ -> false
end
end
def validate_acl(changeset, field) do
validate_change(changeset, field, fn
_, "did:web:" <> _ = did ->
if Regex.match?(@web_did_regex, did) do
[]
else
[{field, "ACL invalid did:web"}]
end
_, "did:key:" <> key ->
is_did_key_field?(field, key)
# I just don't care if you send me a URL with all-caps protocol.
_, "https://" <> _ = url ->
case URI.parse(url) do
%URI{scheme: "https", host: host} when is_binary(host) -> []
_ -> [{field, "Invalid URL ACL"}]
end
_, "http://" <> _ ->
[{field, "HTTP URL ACL"}]
_, _ ->
[{field, "Unrecognized ACL type"}]
end)
end
def is_valid_acl?(acl) when is_binary(acl), do: is_did?(acl) || is_valid_url?(acl)
def is_valid_acl?(_), do: false
def render_acl_document(path, read_acls) when is_binary(path) and is_list(read_acls) do
payload = %{
"@context" => %{
"acl" => "http://www.w3.org/ns/auth/acl#",
"foaf" => "http://xmlns.com/foaf/0.1/"
}
}
2024-12-02 12:38:14 +00:00
graph =
Enum.with_index(read_acls)
|> Enum.map(fn {acl, i} ->
make_read_authorization(path, acl, "#read_acl_#{i}")
end)
2024-12-02 11:03:06 +00:00
owner_read = %{
"@id" => "#owner-read",
"@type" => "acl:Authorization",
"acl:mode" => %{"@id" => "acl:Read"},
"acl:agent" => %{"@id" => Application.get_env(:balls_pds, :owner_ap_id)},
"acl:accessTo" => %{"@id" => make_object_url(path)}
}
owner_write = %{
"@id" => "#owner-write",
"@type" => "acl:Authorization",
"acl:mode" => %{"@id" => "acl:Write"},
"acl:agent" => %{"@id" => Application.get_env(:balls_pds, :owner_ap_id)},
"acl:accessTo" => %{"@id" => make_object_url(path)}
}
graph = [owner_read | [owner_write | graph]]
Map.put(payload, "@graph", graph)
end
def make_object_url(path = "/" <> _) do
did = Application.get_env(:balls_pds, :owner_ap_id)
service = Application.get_env(:balls_pds, :did_service)
path = URI.encode(path, &URI.char_unreserved?/1)
"#{did}?service=#{service}&relativeRef=#{path}"
end
defp make_read_authorization(path, acl, id)
when is_binary(path) and is_binary(acl) and is_binary(id),
do: %{
"@id" => id,
"@type" => "acl:Authorization",
"acl:mode" => %{"@id" => "acl:Read"},
"acl:agent" => %{"@id" => acl},
"acl:accessTo" => %{"@id" => make_object_url(path)}
}
end