From 662994589e671f2d9d242b5709cab6343c641b3b Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Tue, 12 Dec 2017 03:36:45 -0200 Subject: [PATCH] Add Henforcers for MotherboardUpdateRequest --- lib/entity/henforcer/entity.ex | 59 ++++ lib/server/henforcer/component.ex | 152 +++++++++ lib/server/henforcer/server.ex | 1 + lib/server/model/motherboard.ex | 2 +- .../websocket/requests/motherboard_update.ex | 2 +- test/entity/henforcer/entity_test.exs | 64 ++++ test/server/henforcer/component_test.exs | 306 ++++++++++++++++++ .../requests/motherboard_update_test.exs | 2 +- test/support/network/setup.ex | 30 ++ 9 files changed, 615 insertions(+), 3 deletions(-) create mode 100644 lib/server/henforcer/component.ex create mode 100644 test/server/henforcer/component_test.exs diff --git a/lib/entity/henforcer/entity.ex b/lib/entity/henforcer/entity.ex index e640c5ee..1db6791c 100644 --- a/lib/entity/henforcer/entity.ex +++ b/lib/entity/henforcer/entity.ex @@ -2,8 +2,12 @@ defmodule Helix.Entity.Henforcer.Entity do import Helix.Henforcer + alias Helix.Network.Query.Network, as: NetworkQuery + alias Helix.Server.Model.Component alias Helix.Server.Model.Server + alias Helix.Server.Henforcer.Component, as: ComponentHenforcer alias Helix.Server.Henforcer.Server, as: ServerHenforcer + alias Helix.Server.Query.Component, as: ComponentQuery alias Helix.Entity.Model.Entity alias Helix.Entity.Query.Entity, as: EntityQuery @@ -63,4 +67,59 @@ defmodule Helix.Entity.Henforcer.Entity do end |> wrap_relay(%{entity: entity, server: server}) end + + def owns_component?(entity_id = %Entity.ID{}, component, owned) do + henforce entity_exists?(entity_id) do + owns_component?(relay.entity, component, owned) + end + end + + def owns_component?(entity, component_id = %Component.ID{}, owned) do + henforce(ComponentHenforcer.component_exists?(component_id)) do + owns_component?(entity, relay.component, owned) + end + end + + def owns_component?(entity = %Entity{}, component = %Component{}, nil) do + owned_components = + entity + |> EntityQuery.get_components() + |> Enum.map(&(ComponentQuery.fetch(&1.component_id))) + + owns_component?(entity, component, owned_components) + end + + def owns_component?(entity = %Entity{}, component = %Component{}, owned) do + if component in owned do + reply_ok() + else + reply_error({:component, :not_belongs}) + end + |> wrap_relay( + %{entity: entity, component: component, owned_components: owned} + ) + end + + def owns_nip?(entity_id = %Entity.ID{}, network_id, ip, owned) do + henforce entity_exists?(entity_id) do + owns_nip?(relay.entity, network_id, ip, owned) + end + end + + def owns_nip?(entity = %Entity{}, network_id, ip, nil) do + owned_nips = NetworkQuery.Connection.get_by_entity(entity.entity_id) + + owns_nip?(entity, network_id, ip, owned_nips) + end + + def owns_nip?(entity = %Entity{}, network_id, ip, owned) do + nc = Enum.find(owned, &(&1.network_id == network_id and &1.ip == ip)) + + if nc do + reply_ok(%{network_connection: nc}) + else + reply_error({:network_connection, :not_belongs}) + end + |> wrap_relay(%{entity_network_connections: owned}) + end end diff --git a/lib/server/henforcer/component.ex b/lib/server/henforcer/component.ex new file mode 100644 index 00000000..c66e54c4 --- /dev/null +++ b/lib/server/henforcer/component.ex @@ -0,0 +1,152 @@ +defmodule Helix.Server.Henforcer.Component do + + import Helix.Henforcer + + alias Helix.Entity.Henforcer.Entity, as: EntityHenforcer + alias Helix.Network.Query.Network, as: NetworkQuery + alias Helix.Server.Model.Component + alias Helix.Server.Model.Motherboard + alias Helix.Server.Query.Component, as: ComponentQuery + + @internet_id NetworkQuery.internet().network_id + + def component_exists?(component_id = %Component.ID{}) do + with component = %Component{} <- ComponentQuery.fetch(component_id) do + reply_ok(%{component: component}) + else + _ -> + reply_error({:component, :not_found}) + end + end + + def is_motherboard?(component = %Component{type: :mobo}), + do: reply_ok(%{component: component}) + def is_motherboard?(%Component{}), + do: reply_error({:component, :not_motherboard}) + def is_motherboard?(component_id = %Component.ID{}) do + henforce(component_exists?(component_id)) do + is_motherboard?(relay.component) + end + end + + def can_link?( + mobo = %Component{type: :mobo}, + component = %Component{}, + slot_id) + do + with :ok <- Motherboard.check_compatibility(mobo, component, slot_id, []) do + reply_ok() + else + {:error, reason} -> + reply_error({:motherboard, reason}) + end + end + + def has_initial_components?(components) do + if Motherboard.has_required_initial_components?(components) do + reply_ok() + else + reply_error({:motherboard, :missing_initial_components}) + end + end + + def has_public_nip?(network_connections) do + if Enum.find(network_connections, &(&1.network_id == @internet_id)) do + reply_ok() + else + reply_error({:motherboard, :missing_public_nip}) + end + end + + def can_update_mobo?(entity_id, mobo_id, components, network_connections) do + reduce_components = fn mobo -> + init = {{true, %{}}, nil} + + components + |> Enum.reduce_while(init, fn {slot_id, comp_id}, {{true, acc}, cache} -> + with \ + {true, r1} <- component_exists?(comp_id), + component = r1.component, + + {true, r2} <- + EntityHenforcer.owns_component?(entity_id, component, cache), + + {true, _} <- can_link?(mobo, component, slot_id) + do + acc_components = Map.get(acc, :components, []) + + new_acc = + acc + |> put_in([:components], acc_components ++ [{component, slot_id}]) + |> put_in([:entity], r2.entity) + |> put_in([:owned_components], r2.owned_components) + + {:cont, {{true, new_acc}, r2.owned_components}} + else + error -> + {:halt, {error, cache}} + end + end) + |> elem(0) + end + + reduce_network_connections = fn entity, components -> + init = {{true, %{}}, nil} + + network_connections + |> Enum.reduce_while(init, fn {_nic_id, nip}, {{true, acc}, cache} -> + {network_id, ip} = nip + + with \ + {true, r1} <- EntityHenforcer.owns_nip?(entity, network_id, ip, cache) + do + acc_nc = Map.get(acc, :network_connections, []) + + new_acc = + acc + |> put_in([:network_connections], acc_nc ++ [r1.network_connection]) + |> put_in( + [:entity_network_connections], r1.entity_network_connections + ) + + {:cont, {{true, new_acc}, r1.entity_network_connections}} + else + error -> + {:halt, {error, cache}} + end + end) + |> elem(0) + end + + with \ + {true, r0} <- component_exists?(mobo_id), + mobo = r0.component, + + # Make sure user is plugging components into a motherboard + {true, _} <- is_motherboard?(mobo), + + # Iterate over components/slots and make the required henforcements + {true, r1} <- reduce_components.(mobo), + components = r1.components, + entity = r1.entity, + owned = r1.owned_components, + + # Ensure mobo belongs to entity + {true, _} <- EntityHenforcer.owns_component?(entity, mobo, owned), + + # Ensure all required initial components are there + {true, _} <- has_initial_components?(components), + + # Iterate over NetworkConnections and make the required henforcements + {true, r2} <- reduce_network_connections.(entity, components), + + # The mobo must have at least one public NC assigned to it + {true, _} <- has_public_nip?(r2.network_connections) + do + reply_ok(relay([r1, r2])) + else + error -> + error + end + end +end diff --git a/lib/server/henforcer/server.ex b/lib/server/henforcer/server.ex index f2b80d4e..27c974f3 100644 --- a/lib/server/henforcer/server.ex +++ b/lib/server/henforcer/server.ex @@ -19,6 +19,7 @@ defmodule Helix.Server.Henforcer.Server do @doc """ Ensures the requested server exists on the database. """ + # TODO: REVIEW: Why does it accept `Server.t` as input? def server_exists?(server = %Server{}), do: server_exists?(server.server_id) def server_exists?(server_id = %Server.ID{}) do diff --git a/lib/server/model/motherboard.ex b/lib/server/model/motherboard.ex index 21721196..37de3666 100644 --- a/lib/server/model/motherboard.ex +++ b/lib/server/model/motherboard.ex @@ -314,7 +314,7 @@ defmodule Helix.Server.Model.Motherboard do | {:error, :wrong_slot_type} | {:error, :slot_in_use} | {:error, :bad_slot} - defp check_compatibility( + def check_compatibility( mobo = %Component{}, component = %Component{}, slot_id, diff --git a/lib/server/websocket/requests/motherboard_update.ex b/lib/server/websocket/requests/motherboard_update.ex index 403ef782..1fb0bbf0 100644 --- a/lib/server/websocket/requests/motherboard_update.ex +++ b/lib/server/websocket/requests/motherboard_update.ex @@ -78,7 +78,7 @@ request Helix.Server.Websocket.Requests.MotherboardUpdate do {:ok, ip} = IPv4.cast(nc["ip"]) {:ok, network_id} = Network.ID.cast(nc["network_id"]) - {nic_id, {ip, network_id}} + {nic_id, {network_id, ip}} end) {:ok, ncs} diff --git a/test/entity/henforcer/entity_test.exs b/test/entity/henforcer/entity_test.exs index 14a6fd00..a03c4a84 100644 --- a/test/entity/henforcer/entity_test.exs +++ b/test/entity/henforcer/entity_test.exs @@ -7,9 +7,14 @@ defmodule Helix.Entity.Henforcer.EntityTest do alias Helix.Entity.Model.Entity alias Helix.Entity.Henforcer.Entity, as: EntityHenforcer + alias Helix.Test.Network.Helper, as: NetworkHelper + alias Helix.Test.Server.Component.Setup, as: ComponentSetup + alias Helix.Test.Server.Helper, as: ServerHelper alias Helix.Test.Server.Setup, as: ServerSetup alias Helix.Test.Entity.Setup, as: EntitySetup + @internet_id NetworkHelper.internet_id() + describe "entity_exists?/1" do test "accepts when entity exists" do {entity, _} = EntitySetup.entity() @@ -50,4 +55,63 @@ defmodule Helix.Entity.Henforcer.EntityTest do assert reason == {:server, :not_belongs} end end + + describe "owns_component?/3" do + test "accepts when entity owns the component" do + {server, %{entity: entity}} = ServerSetup.server() + + assert {true, relay} = + EntityHenforcer.owns_component?( + entity.entity_id, server.motherboard_id, nil + ) + + assert relay.component.component_id == server.motherboard_id + assert relay.entity == entity + assert length(relay.owned_components) >= 4 + + assert_relay relay, [:component, :entity, :owned_components] + end + + test "rejects when entity does not own the component" do + {entity, _} = EntitySetup.entity() + {component, _} = ComponentSetup.component() + + assert {false, reason, relay} = + EntityHenforcer.owns_component?(entity, component, nil) + + assert reason == {:component, :not_belongs} + + assert_relay relay, [:component, :entity, :owned_components] + end + end + + describe "owns_nip?/4" do + test "accepts when entity owns the nip" do + {server, %{entity: entity}} = ServerSetup.server() + + %{ip: ip, network_id: network_id} = ServerHelper.get_nip(server) + + assert {true, relay} = + EntityHenforcer.owns_nip?(entity.entity_id, network_id, ip, nil) + + assert relay.entity == entity + assert relay.network_connection.network_id == network_id + assert relay.network_connection.ip == ip + assert length(relay.entity_network_connections) == 1 + + assert_relay relay, + [:entity, :network_connection, :entity_network_connections] + end + + test "rejects when entity doesn't own the nip" do + {entity, _} = EntitySetup.entity() + + assert {false, reason, _} = + EntityHenforcer.owns_nip?( + entity.entity_id, @internet_id, "1.2.3.4", nil + ) + + assert reason == {:network_connection, :not_belongs} + end + end end diff --git a/test/server/henforcer/component_test.exs b/test/server/henforcer/component_test.exs new file mode 100644 index 00000000..54e8f2c2 --- /dev/null +++ b/test/server/henforcer/component_test.exs @@ -0,0 +1,306 @@ +defmodule Helix.Server.Henforcer.ComponentTest do + + use Helix.Test.Case.Integration + + import Helix.Test.Henforcer.Macros + + alias Helix.Entity.Action.Entity, as: EntityAction + alias Helix.Network.Action.Network, as: NetworkAction + alias Helix.Server.Henforcer.Component, as: ComponentHenforcer + alias Helix.Server.Model.Component + + alias HELL.TestHelper.Random + alias Helix.Test.Network.Helper, as: NetworkHelper + alias Helix.Test.Network.Setup, as: NetworkSetup + alias Helix.Test.Server.Component.Setup, as: ComponentSetup + alias Helix.Test.Server.Setup, as: ServerSetup + + @internet_id NetworkHelper.internet_id() + + describe "can_update_mobo?/n" do + test "accepts when everything is OK" do + {server, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + components = [ + {:cpu_1, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + # Create a new NC + {:ok, new_nc} = + NetworkAction.Connection.create(@internet_id, Random.ipv4(), entity) + + nc = %{nic.component_id => {new_nc.network_id, new_nc.ip}} + + assert {true, relay} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, components, nc + ) + + # Assigned the components (`Component.t`) to the corresponding slot + Enum.each(relay.components, fn {component, slot_id} -> + cond do + slot_id == :cpu_1 -> + assert component == cpu + + slot_id == :ram_1 -> + assert component == ram + + slot_id == :nic_1 -> + assert component == nic + + slot_id == :hdd_1 -> + assert component == hdd + end + end) + + assert relay.entity == entity + assert length(relay.owned_components) >= 4 + + # All network_connections passed as a param were added to this relay + assert [new_nc] == relay.network_connections + + # And `entity_network_connections` has all NCs for that entity + assert length(relay.entity_network_connections) == 2 + + assert_relay relay, + [ + :entity, + :components, + :owned_components, + :network_connections, + :entity_network_connections + ] + end + + test "rejects when component does not exist" do + {server, %{entity: entity}} = ServerSetup.server() + + components = [{:cpu_1, Component.ID.generate()}] + + assert {false, reason, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, components, %{} + ) + + assert reason == {:component, :not_found} + end + + test "rejects when invalid component slots are used" do + {server, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + # C1 has components on the wrong slots (RAM on CPU and versa-vice) + c1 = [ + {:cpu_1, ram.component_id}, {:ram_1, cpu.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + assert {false, reason1, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, c1, %{} + ) + + assert reason1 == {:motherboard, :wrong_slot_type} + + # C2 has the components on the right slots but it uses an invalid one + c2 = [ + {:cpu_999, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + assert {false, reason2, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, c2, %{} + ) + + assert reason2 == {:motherboard, :bad_slot} + end + + test "rejects when components does not belong to entity" do + {server, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + components = [ + {:cpu_1, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + assert {false, reason, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, components, %{} + ) + + assert reason == {:component, :not_belongs} + end + + test "rejects when entity does not own the motherboard" do + {_, %{entity: entity}} = ServerSetup.server() + + {bad_mobo, _} = ComponentSetup.component(type: :mobo) + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + components = [ + {:cpu_1, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + assert {false, reason, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, bad_mobo.component_id, components, %{} + ) + + assert reason == {:component, :not_belongs} + end + + test "rejects when update does not match the initial components" do + {server, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + # Missing `hdd` + components = [ + {:cpu_1, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id} + ] + + assert {false, reason, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, components, %{} + ) + + assert reason == {:motherboard, :missing_initial_components} + end + + test "rejects when given motherboard_id is not a motherboard (!!!)" do + {_, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + components = [ + {:cpu_1, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + # Using CPU as my motherboard... because why not + assert {false, reason, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, cpu.component_id, components, %{} + ) + + assert reason == {:component, :not_motherboard} + end + + test "rejects when invalid NIP was assigned to the mobo" do + {server, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + components = [ + {:cpu_1, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + # I'm assign `Random.ipv4()` as my NC... but it doesn't belong to me! + nc = %{nic.component_id => {@internet_id, Random.ipv4()}} + + assert {false, reason, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, components, nc + ) + + assert reason == {:network_connection, :not_belongs} + end + + test "rejects when no public NIC was assigned to the mobo" do + {server, %{entity: entity}} = ServerSetup.server() + + {cpu, _} = ComponentSetup.component(type: :cpu) + {ram, _} = ComponentSetup.component(type: :ram) + {nic, _} = ComponentSetup.component(type: :nic) + {hdd, _} = ComponentSetup.component(type: :hdd) + + EntityAction.link_component(entity, cpu) + EntityAction.link_component(entity, ram) + EntityAction.link_component(entity, nic) + EntityAction.link_component(entity, hdd) + + components = [ + {:cpu_1, cpu.component_id}, {:ram_1, ram.component_id}, + {:nic_1, nic.component_id}, {:hdd_1, hdd.component_id} + ] + + {network, _} = NetworkSetup.network() + + # Create a new NC on a random network + {:ok, new_nc} = + NetworkAction.Connection.create(network, Random.ipv4(), entity) + + # Assign `new_nc` to the Mobo. Valid, but it has no public IP !!11! + nc = %{nic.component_id => {new_nc.network_id, new_nc.ip}} + + assert {false, reason, _} = + ComponentHenforcer.can_update_mobo?( + entity.entity_id, server.motherboard_id, components, nc + ) + + assert reason == {:motherboard, :missing_public_nip} + end + end +end diff --git a/test/server/websocket/requests/motherboard_update_test.exs b/test/server/websocket/requests/motherboard_update_test.exs index 3c6e40c4..7904f92d 100644 --- a/test/server/websocket/requests/motherboard_update_test.exs +++ b/test/server/websocket/requests/motherboard_update_test.exs @@ -40,7 +40,7 @@ defmodule Helix.Server.Websocket.Requests.MotherboardUpdateTest do [{nic_id, nip}] = req.params.network_connections assert nic_id == Component.ID.cast!("::5") - assert nip == {"1.2.3.4", Network.ID.cast!("::")} + assert nip == {Network.ID.cast!("::"), "1.2.3.4"} end test "handles invalid slot data" do diff --git a/test/support/network/setup.ex b/test/support/network/setup.ex index 3478c61f..63e7bdeb 100644 --- a/test/support/network/setup.ex +++ b/test/support/network/setup.ex @@ -1,7 +1,9 @@ defmodule Helix.Test.Network.Setup do + alias Ecto.Changeset alias Helix.Server.Model.Server alias Helix.Network.Model.Connection + alias Helix.Network.Model.Network alias Helix.Network.Model.Tunnel alias Helix.Network.Query.Tunnel, as: TunnelQuery alias Helix.Network.Repo, as: NetworkRepo @@ -125,4 +127,32 @@ defmodule Helix.Test.Network.Setup do {connection, %{tunnel: tunnel}} end + + @doc """ + See doc on `fake_network/1` + """ + def network(opts \\ []) do + {_, related = %{changeset: changeset}} = fake_network(opts) + {:ok, inserted} = NetworkRepo.insert(changeset) + {inserted, related} + end + + @doc """ + - network_id: specify network id. Defaults to random one + - name: Specify network name. Defaults to + """ + def fake_network(opts \\ []) do + network_id = Keyword.get(opts, :network_id, Network.ID.generate()) + name = Keyword.get(opts, :name, "LAN") + + network = + %Network{ + network_id: network_id, + name: name + } + + related = %{changeset: Changeset.change(network)} + + {network, related} + end end