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
|