diff --git a/lib/matrix_server/event.ex b/lib/matrix_server/event.ex index 032c8bc..739322a 100644 --- a/lib/matrix_server/event.ex +++ b/lib/matrix_server/event.ex @@ -229,7 +229,6 @@ defmodule MatrixServer.Event do when sender != state_key, do: false - # TODO: Rule 5.3.1 # All other rules will be checked during state resolution. defp do_prevalidate(_, _, _), do: true diff --git a/lib/matrix_server/state_resolution.ex b/lib/matrix_server/state_resolution.ex index 4488e06..0528497 100644 --- a/lib/matrix_server/state_resolution.ex +++ b/lib/matrix_server/state_resolution.ex @@ -35,9 +35,309 @@ defmodule MatrixServer.StateResolution do end end - def do_resolve([], _), do: %{} + def is_authorized_by_auth_events(%Event{auth_events: auth_event_ids} = event) do + # We assume the auth events are validated beforehand. + state_set = + Event + |> where([e], e.event_id in ^auth_event_ids) + |> Repo.all() + |> Enum.reduce(%{}, &update_state_set/2) - def do_resolve(state_sets, room_events) do + is_authorized(event, state_set) + end + + def resolve_forward_extremities(%Event{room_id: room_id}) do + room_events = + Event + |> where([e], e.room_id == ^room_id) + |> select([e], {e.event_id, e}) + |> Repo.all() + |> Enum.into(%{}) + + Event + |> where([e], e.room_id == ^room_id) + |> join(:inner, [e], r in Room, on: e.room_id == r.id) + |> where([e, r], e.event_id == fragment("ANY(?)", r.forward_extremities)) + |> Repo.all() + |> Enum.map(&resolve/1) + |> do_resolve(room_events) + end + + def is_authorized(%Event{type: "m.room.create", prev_events: prev_events}, %{}), + do: prev_events == [] + + # Check rule: 5.2.1 + def is_authorized(%Event{type: "m.room.member", state_key: state_key}, %{ + {"m.room.create", ""} => %Event{content: %{"creator" => creator}} + }), + do: state_key == creator + + def is_authorized( + %Event{type: "m.room.member", sender: sender, content: %{"membership" => "join"}}, + state_set + ) do + join_rule = get_join_rule(state_set) + membership = get_membership(sender, state_set) + + # Check rules: 5.2.3, 5.2.4, 5.2.5 + cond do + membership == "ban" -> false + join_rule == "invite" -> membership in ["invite", "join"] + join_rule == "public" -> true + true -> false + end + end + + # TODO: rule 5.3.1 + def is_authorized( + %Event{ + type: "m.room.member", + content: %{"membership" => "invite", "third_party_invite" => _} + }, + _ + ), + do: false + + def is_authorized( + %Event{ + type: "m.room.member", + sender: sender, + content: %{"membership" => "invite"}, + state_key: state_key + }, + state_set + ) do + sender_membership = get_membership(sender, state_set) + target_membership = get_membership(state_key, state_set) + power_levels = get_power_levels(state_set) + + # Check rules: 5.3.2, 5.3.3, 5.3.4 + cond do + sender_membership != "join" -> false + target_membership in ["join", "ban"] -> false + has_power_level(sender, power_levels, :invite) -> true + true -> false + end + end + + def is_authorized( + %Event{ + type: "m.room.member", + sender: sender, + content: %{"membership" => "leave"}, + state_key: sender + }, + state_set + ) do + # Check rule: 5.4.1 + get_membership(sender, state_set) in ["invite", "join"] + end + + def is_authorized( + %Event{ + type: "m.room.member", + sender: sender, + content: %{"membership" => "leave"}, + state_key: state_key + }, + state_set + ) do + sender_membership = get_membership(sender, state_set) + target_membership = get_membership(state_key, state_set) + power_levels = get_power_levels(state_set) + sender_pl = get_user_power_level(sender, power_levels) + target_pl = get_user_power_level(state_key, power_levels) + + # Check rules: 5.4.2, 5.4.3, 5.4.4 + cond do + sender_membership != "join" -> false + target_membership == "ban" and not has_power_level(sender, power_levels, :ban) -> false + has_power_level(sender, power_levels, :kick) and target_pl < sender_pl -> true + true -> false + end + end + + def is_authorized( + %Event{ + type: "m.room.member", + sender: sender, + content: %{"membership" => "ban"}, + state_key: state_key + }, + state_set + ) do + sender_membership = get_membership(sender, state_set) + power_levels = get_power_levels(state_set) + sender_pl = get_user_power_level(sender, power_levels) + target_pl = get_user_power_level(state_key, power_levels) + + # Check rules: 5.5.1, 5.5.2 + cond do + sender_membership != "join" -> false + has_power_level(sender, power_levels, :ban) and target_pl < sender_pl -> true + true -> false + end + end + + # Check rule: 5.6 + def is_authorized(%Event{type: "m.room.member"}, _), do: false + + def is_authorized(%Event{sender: sender} = event, state_set) do + # Check rule: 6 + get_membership(sender, state_set) == "join" and _is_authorized(event, state_set) + end + + defp _is_authorized(%Event{type: "m.room.third_party_invite", sender: sender}, state_set) do + # Check rule: 7.1 + has_power_level(sender, state_set, :invite) + end + + defp _is_authorized(%Event{state_key: state_key, sender: sender} = event, state_set) do + power_levels = get_power_levels(state_set) + + # Check rules: 8, 9 + cond do + not has_power_level(sender, power_levels, {:event, event}) -> false + String.starts_with?(state_key, "@") and state_key != sender -> false + true -> __is_authorized(event, state_set) + end + end + + defp __is_authorized( + %Event{type: "m.room.power_levels", sender: sender, content: content}, + state_set + ) do + current_pls = get_power_levels(state_set) + new_pls = content + sender_pl = get_user_power_level(sender, new_pls) + + # Check rules: 10.2, 10.3, 10.4, 10.5 + cond do + not is_map_key(state_set, {"m.room.power_levels", ""}) -> true + not authorize_power_levels(sender, sender_pl, current_pls, new_pls) -> false + true -> true + end + end + + # TODO: Rule 11 + + defp __is_authorized(_, _), do: true + + defp authorize_power_levels( + user, + user_pl, + %{"events" => current_events, "users" => current_users} = current_pls, + %{"events" => new_events, "users" => new_users} = new_pls + ) do + keys = ["users_default", "events_default", "state_default", "ban", "redact", "kick", "invite"] + + valid_power_level_key_changes(Map.take(current_pls, keys), Map.take(new_pls, keys), user_pl) and + valid_power_level_key_changes(current_events, new_events, user_pl) and + valid_power_level_key_changes(current_users, new_users, user_pl) and + valid_power_level_users_changes(current_users, new_users, user, user_pl) + end + + defp has_power_level(user, power_levels, action) do + user_pl = get_user_power_level(user, power_levels) + action_pl = get_action_power_level(action, power_levels) + + user_pl >= action_pl + end + + defp get_user_power_level(user, %{"users" => users}) when is_map_key(users, user), + do: users[user] + + defp get_user_power_level(_, %{"users_default" => pl}), do: pl + defp get_user_power_level(_, _), do: 0 + + defp get_action_power_level(:invite, %{"invite" => pl}), do: pl + defp get_action_power_level(:invite, _), do: 50 + defp get_action_power_level(:ban, %{"ban" => pl}), do: pl + defp get_action_power_level(:ban, _), do: 50 + defp get_action_power_level(:redact, %{"redact" => pl}), do: pl + defp get_action_power_level(:redact, _), do: 50 + + defp get_action_power_level({:event, %Event{type: type}}, %{"events" => events}) + when is_map_key(events, type), + do: events[type] + + defp get_action_power_level({:event, event}, power_levels) do + if Event.is_state_event(event) do + case power_levels do + %{"state_default" => pl} -> pl + %{} -> 50 + _ -> 0 + end + else + case power_levels do + %{"events_default" => pl} -> pl + _ -> 0 + end + end + end + + defp get_power_levels(state_set) do + case state_set[{"m.room.power_levels", ""}] do + %Event{content: content} -> content + nil -> nil + end + end + + defp get_join_rule(state_set) do + case state_set[{"m.room.join_rules", ""}] do + %Event{content: %{"join_rule" => join_rule}} -> join_rule + nil -> nil + end + end + + defp get_membership(user, state_set) do + case state_set[{"m.room.member", user}] do + %Event{content: %{"membership" => membership}} -> membership + nil -> nil + end + end + + defp valid_power_level_key_changes(l1, l2, user_pl) do + set1 = MapSet.new(l1) + set2 = MapSet.new(l2) + + MapSet.difference( + MapSet.union(set1, set2), + MapSet.intersection(set1, set2) + ) + |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) + |> Enum.all?(fn {_k, values} -> + Enum.all?(values, &(&1 <= user_pl)) + end) + end + + defp valid_power_level_users_changes(current_users, new_users, user, user_pl) do + set1 = MapSet.new(current_users) + set2 = MapSet.new(new_users) + + MapSet.difference( + MapSet.union(set1, set2), + MapSet.intersection(set1, set2) + ) + |> Enum.all?(fn + {_k, values} when length(values) != 2 -> true + {k, _} when k == user -> true + {_k, [old_value, _]} -> old_value != user_pl + end) + end + + def testing do + %Event{content: content} = event = Event.power_levels("room1", "charlie") + event = %Event{event | content: %{content | "ban" => 0}} + + event + |> Map.put(:prev_events, ["b", "fork"]) + |> Map.put(:auth_events, ["create", "join_charlie", "b"]) + end + + defp do_resolve([], _), do: %{} + + defp do_resolve(state_sets, room_events) do {unconflicted_state_map, conflicted_state_set} = calculate_conflict(state_sets, room_events) if MapSet.size(conflicted_state_set) == 0 do @@ -47,7 +347,7 @@ defmodule MatrixServer.StateResolution do end end - def do_resolve(state_sets, room_events, unconflicted_state_map, conflicted_state_set) do + defp do_resolve(state_sets, room_events, unconflicted_state_map, conflicted_state_set) do full_conflicted_set = MapSet.union(conflicted_state_set, auth_difference(state_sets, room_events)) @@ -81,7 +381,7 @@ defmodule MatrixServer.StateResolution do |> Map.merge(unconflicted_state_map) end - def calculate_conflict(state_sets, room_events) do + defp calculate_conflict(state_sets, room_events) do {unconflicted, conflicted} = state_sets |> Enum.flat_map(&Map.keys/1) @@ -116,7 +416,7 @@ defmodule MatrixServer.StateResolution do {unconflicted_state_map, conflicted_state_set} end - def auth_difference(state_sets, room_events) do + defp auth_difference(state_sets, room_events) do full_auth_chains = Enum.map(state_sets, fn state_set -> state_set @@ -130,13 +430,13 @@ defmodule MatrixServer.StateResolution do MapSet.difference(auth_chain_union, auth_chain_intersection) end - def full_auth_chain(events, room_events) do + defp full_auth_chain(events, room_events) do events |> Enum.map(&auth_chain(&1, room_events)) |> Enum.reduce(MapSet.new(), &MapSet.union/2) end - def auth_chain(%Event{auth_events: auth_events}, room_events) do + defp auth_chain(%Event{auth_events: auth_events}, room_events) do auth_events |> Enum.map(&room_events[&1]) |> Enum.reduce(MapSet.new(), fn %Event{event_id: auth_event_id} = auth_event, acc -> @@ -147,7 +447,7 @@ defmodule MatrixServer.StateResolution do end) end - def rev_top_pow_order(room_events) do + defp rev_top_pow_order(room_events) do fn %Event{origin_server_ts: timestamp1, event_id: event_id1} = event1, %Event{origin_server_ts: timestamp2, event_id: event_id2} = event2 -> power1 = get_power_level(event1, room_events) @@ -165,19 +465,20 @@ defmodule MatrixServer.StateResolution do end end - def get_power_level(%Event{sender: sender, auth_events: auth_event_ids}, room_events) do + defp get_power_level(%Event{sender: sender, auth_events: auth_event_ids}, room_events) do pl_event_id = Enum.find(auth_event_ids, fn id -> room_events[id].type == "m.room.power_levels" end) + # TODO: refactor case room_events[pl_event_id] do %Event{content: %{"users" => pl_users}} -> Map.get(pl_users, sender, 0) nil -> 0 end end - def mainline_order(event, room_events) do + defp mainline_order(event, room_events) do mainline_map = event |> mainline(room_events) @@ -220,13 +521,13 @@ defmodule MatrixServer.StateResolution do end end - def mainline(event, room_events) do + defp mainline(event, room_events) do event |> mainline([], room_events) |> Enum.reverse() end - def mainline(%Event{auth_events: auth_event_ids} = event, acc, room_events) do + defp mainline(%Event{auth_events: auth_event_ids} = event, acc, room_events) do pl_event_id = Enum.find(auth_event_ids, fn id -> room_events[id].type == "m.room.power_levels" @@ -238,20 +539,20 @@ defmodule MatrixServer.StateResolution do end end - def iterative_auth_checks(events, state_set, room_events) do + defp iterative_auth_checks(events, state_set, room_events) do Enum.reduce(events, state_set, fn event, acc -> if is_authorized2(event, acc, room_events), do: update_state_set(event, acc), else: acc end) end - def update_state_set( - %Event{type: event_type, state_key: state_key} = event, - state_set - ) do + defp update_state_set( + %Event{type: event_type, state_key: state_key} = event, + state_set + ) do Map.put(state_set, {event_type, state_key}, event) end - def is_authorized2(%Event{auth_events: auth_event_ids} = event, state_set, room_events) do + defp is_authorized2(%Event{auth_events: auth_event_ids} = event, state_set, room_events) do state_set = auth_event_ids |> Enum.map(&room_events[&1]) @@ -259,94 +560,4 @@ defmodule MatrixServer.StateResolution do is_authorized(event, state_set) end - - # TODO: join and power levels events - def is_authorized(%Event{type: "m.room.create", prev_events: prev_events}, _), - do: prev_events == [] - - def is_authorized( - %Event{type: "m.room.member", content: %{"membership" => "join"}, state_key: user}, - state_set - ) do - allowed_to_join(user, state_set) - end - - def is_authorized(%Event{sender: sender} = event, state_set) do - in_room(sender, state_set) and - has_power_level( - sender, - get_power_levels(state_set), - get_event_power_level(event) - ) - end - - def in_room(user, state_set) when is_map_key(state_set, {"m.room.member", user}) do - case state_set[{"m.room.member", user}].content["membership"] do - "join" -> true - _ -> false - end - end - - def in_room(_, _), do: false - - def get_power_levels(state_set) - when is_map_key(state_set, {"m.room.power_levels", ""}) do - state_set[{"m.room.power_levels", ""}].content - end - - def get_power_levels(_), do: nil - - def has_power_level(user, %{"users" => users}, level) do - Map.get(users, user, 0) >= level - end - - def has_power_level(_, _, _) do - true - end - - defp get_event_power_level(%Event{state_key: ""}), do: 0 - defp get_event_power_level(_), do: 50 - - # No join rules specified, allow joining for room creator only. - def allowed_to_join(user, state_set) - when not is_map_key(state_set, {"m.room.join_rules", ""}) do - state_set[{"m.room.create", ""}].sender == user - end - - def is_authorized_by_auth_events(%Event{auth_events: auth_event_ids} = event) do - # We assume the auth events are validated beforehand. - state_set = - Event - |> where([e], e.event_id in ^auth_event_ids) - |> Repo.all() - |> Enum.reduce(%{}, &update_state_set/2) - - is_authorized(event, state_set) - end - - def resolve_forward_extremities(%Event{room_id: room_id}) do - room_events = - Event - |> where([e], e.room_id == ^room_id) - |> select([e], {e.event_id, e}) - |> Repo.all() - |> Enum.into(%{}) - - Event - |> where([e], e.room_id == ^room_id) - |> join(:inner, [e], r in Room, on: e.room_id == r.id) - |> where([e, r], e.event_id == fragment("ANY(?)", r.forward_extremities)) - |> Repo.all() - |> Enum.map(&resolve/1) - |> do_resolve(room_events) - end - - def testing do - %Event{content: content} = event = Event.power_levels("room1", "charlie") - event = %Event{event | content: %{content | "ban" => 0}} - - event - |> Map.put(:prev_events, ["b", "fork"]) - |> Map.put(:auth_events, ["create", "join_charlie", "b"]) - end end