diff --git a/docker-compose.yml b/docker-compose.yml index bfddddc38..80d0b816c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -152,7 +152,7 @@ services: PORTAL_URL: "ws://api:8081/" PORTAL_TOKEN: "SFMyNTY.g2gDaAJtAAAAJDNjZWYwNTY2LWFkZmQtNDhmZS1hMGYxLTU4MDY3OTYwOGY2Zm0AAABAamp0enhSRkpQWkdCYy1vQ1o5RHkyRndqd2FIWE1BVWRwenVScjJzUnJvcHg3NS16bmhfeHBfNWJUNU9uby1yYm4GAEC0b0KJAWIAAVGA.9Oirn9t8rvQpfOhW7hwGBFVzeMm9di0xYGTlwf9cFFk" RUST_LOG: firezone_gateway=trace,connlib_gateway_shared=trace,firezone_tunnel=trace,connlib_shared=trace,warn - ENABLE_MASQUERADE: 1 + FIREZONE_ENABLE_MASQUERADE: 1 build: target: debug context: rust diff --git a/elixir/apps/domain/lib/domain/gateways.ex b/elixir/apps/domain/lib/domain/gateways.ex index f455bccb8..056f62cb9 100644 --- a/elixir/apps/domain/lib/domain/gateways.ex +++ b/elixir/apps/domain/lib/domain/gateways.ex @@ -117,7 +117,7 @@ defmodule Domain.Gateways do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_gateways_permission()) do group |> Repo.preload(:account) - |> Group.Changeset.update(attrs) + |> Group.Changeset.update(attrs, subject) |> Repo.update() end end diff --git a/elixir/apps/domain/lib/domain/gateways/gateway.ex b/elixir/apps/domain/lib/domain/gateways/gateway.ex index 7e8eff071..4471d8fdf 100644 --- a/elixir/apps/domain/lib/domain/gateways/gateway.ex +++ b/elixir/apps/domain/lib/domain/gateways/gateway.ex @@ -4,6 +4,7 @@ defmodule Domain.Gateways.Gateway do schema "gateways" do field :external_id, :string + # TODO: hostname field :name_suffix, :string field :public_key, :string diff --git a/elixir/apps/domain/lib/domain/gateways/group.ex b/elixir/apps/domain/lib/domain/gateways/group.ex index 50f8f022a..d1ab90e32 100644 --- a/elixir/apps/domain/lib/domain/gateways/group.ex +++ b/elixir/apps/domain/lib/domain/gateways/group.ex @@ -2,8 +2,8 @@ defmodule Domain.Gateways.Group do use Domain, :schema schema "gateway_groups" do + # TODO: name field :name_prefix, :string - field :tags, {:array, :string}, default: [] belongs_to :account, Domain.Accounts.Account has_many :gateways, Domain.Gateways.Gateway, foreign_key: :group_id, where: [deleted_at: nil] diff --git a/elixir/apps/domain/lib/domain/gateways/group/changeset.ex b/elixir/apps/domain/lib/domain/gateways/group/changeset.ex index f6c09acf4..12b9b5199 100644 --- a/elixir/apps/domain/lib/domain/gateways/group/changeset.ex +++ b/elixir/apps/domain/lib/domain/gateways/group/changeset.ex @@ -3,7 +3,7 @@ defmodule Domain.Gateways.Group.Changeset do alias Domain.{Auth, Accounts} alias Domain.Gateways - @fields ~w[name_prefix tags]a + @fields ~w[name_prefix]a def create(%Accounts.Account{} = account, attrs, %Auth.Subject{} = subject) do %Gateways.Group{account: account} @@ -19,6 +19,15 @@ defmodule Domain.Gateways.Group.Changeset do ) end + def update(%Gateways.Group{} = group, attrs, %Auth.Subject{} = subject) do + changeset(group, attrs) + |> cast_assoc(:tokens, + with: fn _token, _attrs -> + Gateways.Token.Changeset.create(group.account, subject) + end + ) + end + def update(%Gateways.Group{} = group, attrs) do changeset(group, attrs) end @@ -30,15 +39,6 @@ defmodule Domain.Gateways.Group.Changeset do |> put_default_value(:name_prefix, &Domain.NameGenerator.generate/0) |> validate_required(@fields) |> validate_length(:name_prefix, min: 1, max: 64) - |> validate_length(:tags, min: 0, max: 128) - |> validate_no_duplicates(:tags) - |> validate_list_elements(:tags, fn key, value -> - if String.length(value) > 64 do - [{key, "should be at most 64 characters long"}] - else - [] - end - end) |> unique_constraint(:name_prefix, name: :gateway_groups_account_id_name_prefix_index) end diff --git a/elixir/apps/domain/lib/domain/resources/connection.ex b/elixir/apps/domain/lib/domain/resources/connection.ex index fbe1b5ee4..b5d264a2e 100644 --- a/elixir/apps/domain/lib/domain/resources/connection.ex +++ b/elixir/apps/domain/lib/domain/resources/connection.ex @@ -3,8 +3,8 @@ defmodule Domain.Resources.Connection do @primary_key false schema "resource_connections" do - belongs_to :resource, Domain.Resources.Resource, primary_key: true - belongs_to :gateway_group, Domain.Gateways.Group, primary_key: true + belongs_to :resource, Domain.Resources.Resource, primary_key: true, where: [deleted_at: nil] + belongs_to :gateway_group, Domain.Gateways.Group, primary_key: true, where: [deleted_at: nil] field :created_by, Ecto.Enum, values: ~w[identity]a belongs_to :created_by_identity, Domain.Auth.Identity diff --git a/elixir/apps/domain/lib/domain/resources/resource/changeset.ex b/elixir/apps/domain/lib/domain/resources/resource/changeset.ex index fbb7c4864..8dc102b60 100644 --- a/elixir/apps/domain/lib/domain/resources/resource/changeset.ex +++ b/elixir/apps/domain/lib/domain/resources/resource/changeset.ex @@ -122,8 +122,6 @@ defmodule Domain.Resources.Resource.Changeset do |> put_default_value(:name, from: :address) |> validate_length(:name, min: 1, max: 255) |> put_resource_type() - |> unique_constraint(:address, name: :resources_account_id_address_index) - |> unique_constraint(:name, name: :resources_account_id_name_index) |> cast_embed(:filters, with: &cast_filter/2) |> unique_constraint(:ipv4, name: :resources_account_id_ipv4_index) |> unique_constraint(:ipv6, name: :resources_account_id_ipv6_index) diff --git a/elixir/apps/domain/priv/repo/migrations/20231026203804_remove_gateway_group_tags.exs b/elixir/apps/domain/priv/repo/migrations/20231026203804_remove_gateway_group_tags.exs new file mode 100644 index 000000000..050acffea --- /dev/null +++ b/elixir/apps/domain/priv/repo/migrations/20231026203804_remove_gateway_group_tags.exs @@ -0,0 +1,9 @@ +defmodule Domain.Repo.Migrations.RemoveGatewayGroupTags do + use Ecto.Migration + + def change do + alter table(:gateway_groups) do + remove(:tags, {:array, :string}, null: false, default: []) + end + end +end diff --git a/elixir/apps/domain/priv/repo/migrations/20231026214826_make_resource_adddress_unique_per_gateway_group.exs b/elixir/apps/domain/priv/repo/migrations/20231026214826_make_resource_adddress_unique_per_gateway_group.exs new file mode 100644 index 000000000..9a058902a --- /dev/null +++ b/elixir/apps/domain/priv/repo/migrations/20231026214826_make_resource_adddress_unique_per_gateway_group.exs @@ -0,0 +1,19 @@ +defmodule Domain.Repo.Migrations.MakeResourceAdddressUniquePerGatewayGroup do + use Ecto.Migration + + def change do + drop( + index(:resources, [:account_id, :name], + unique: true, + where: "deleted_at IS NULL" + ) + ) + + drop( + index(:resources, [:account_id, :address], + unique: true, + where: "deleted_at IS NULL" + ) + ) + end +end diff --git a/elixir/apps/domain/priv/repo/seeds.exs b/elixir/apps/domain/priv/repo/seeds.exs index 445998125..8b2ab74d9 100644 --- a/elixir/apps/domain/priv/repo/seeds.exs +++ b/elixir/apps/domain/priv/repo/seeds.exs @@ -319,6 +319,16 @@ IO.puts("") last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 73, 111}} }) +for i <- 1..5 do + {:ok, _relay} = + Relays.upsert_relay(relay_group_token, %{ + ipv4: {189, 172, 73, 111 + i}, + ipv6: {0, 0, 0, 0, 0, 0, 0, i}, + last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", + last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 73, 111}} + }) +end + IO.puts("Created relays:") IO.puts(" Group #{relay_group.name}:") IO.puts(" IPv4: #{relay.ipv4} IPv6: #{relay.ipv6}") @@ -327,7 +337,7 @@ IO.puts("") gateway_group = account |> Gateways.Group.Changeset.create( - %{name_prefix: "mycro-aws-gws", tags: ["aws", "in-da-cloud"], tokens: [%{}]}, + %{name_prefix: "mycro-aws-gws", tokens: [%{}]}, admin_subject ) |> Repo.insert!() @@ -365,6 +375,17 @@ IO.puts("") last_seen_remote_ip: %Postgrex.INET{address: {164, 112, 78, 62}} }) +for i <- 1..10 do + {:ok, _gateway} = + Gateways.upsert_gateway(gateway_group_token, %{ + external_id: Ecto.UUID.generate(), + name_suffix: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}", + public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), + last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", + last_seen_remote_ip: %Postgrex.INET{address: {164, 112, 78, 62 + i}} + }) +end + IO.puts("Created gateways:") gateway_name = "#{gateway_group.name_prefix}-#{gateway1.name_suffix}" IO.puts(" #{gateway_name}:") diff --git a/elixir/apps/domain/test/domain/gateways_test.exs b/elixir/apps/domain/test/domain/gateways_test.exs index 743ff6b7b..14b129ea5 100644 --- a/elixir/apps/domain/test/domain/gateways_test.exs +++ b/elixir/apps/domain/test/domain/gateways_test.exs @@ -134,26 +134,16 @@ defmodule Domain.GatewaysTest do test "returns error on invalid attrs", %{account: account, subject: subject} do attrs = %{ - name_prefix: String.duplicate("A", 65), - tags: Enum.map(1..129, &Integer.to_string/1) + name_prefix: String.duplicate("A", 65) } assert {:error, changeset} = create_group(attrs, subject) assert errors_on(changeset) == %{ tokens: ["can't be blank"], - name_prefix: ["should be at most 64 character(s)"], - tags: ["should have at most 128 item(s)"] + name_prefix: ["should be at most 64 character(s)"] } - attrs = %{tags: ["A", "B", "A"]} - assert {:error, changeset} = create_group(attrs, subject) - assert "should not contain duplicates" in errors_on(changeset).tags - - attrs = %{tags: [String.duplicate("A", 65)]} - assert {:error, changeset} = create_group(attrs, subject) - assert "should be at most 64 characters long" in errors_on(changeset).tags - Fixtures.Gateways.create_group(account: account, name_prefix: "foo") attrs = %{name_prefix: "foo", tokens: [%{}]} assert {:error, changeset} = create_group(attrs, subject) @@ -163,14 +153,12 @@ defmodule Domain.GatewaysTest do test "creates a group", %{subject: subject} do attrs = %{ name_prefix: "foo", - tags: ["bar"], tokens: [%{}] } assert {:ok, group} = create_group(attrs, subject) assert group.id assert group.name_prefix == "foo" - assert group.tags == ["bar"] assert group.created_by == :identity assert group.created_by_identity_id == subject.identity.id @@ -202,7 +190,7 @@ defmodule Domain.GatewaysTest do assert changeset = change_group(group, group_attrs) assert changeset.valid? - assert changeset.changes == %{name_prefix: group_attrs.name_prefix, tags: group_attrs.tags} + assert changeset.changes == %{name_prefix: group_attrs.name_prefix} end end @@ -222,25 +210,15 @@ defmodule Domain.GatewaysTest do group = Fixtures.Gateways.create_group(account: account) attrs = %{ - name_prefix: String.duplicate("A", 65), - tags: Enum.map(1..129, &Integer.to_string/1) + name_prefix: String.duplicate("A", 65) } assert {:error, changeset} = update_group(group, attrs, subject) assert errors_on(changeset) == %{ - name_prefix: ["should be at most 64 character(s)"], - tags: ["should have at most 128 item(s)"] + name_prefix: ["should be at most 64 character(s)"] } - attrs = %{tags: ["A", "B", "A"]} - assert {:error, changeset} = update_group(group, attrs, subject) - assert "should not contain duplicates" in errors_on(changeset).tags - - attrs = %{tags: [String.duplicate("A", 65)]} - assert {:error, changeset} = update_group(group, attrs, subject) - assert "should be at most 64 characters long" in errors_on(changeset).tags - Fixtures.Gateways.create_group(account: account, name_prefix: "foo") attrs = %{name_prefix: "foo"} assert {:error, changeset} = update_group(group, attrs, subject) @@ -251,13 +229,11 @@ defmodule Domain.GatewaysTest do group = Fixtures.Gateways.create_group(account: account) attrs = %{ - name_prefix: "foo", - tags: ["bar"] + name_prefix: "foo" } assert {:ok, group} = update_group(group, attrs, subject) assert group.name_prefix == "foo" - assert group.tags == ["bar"] end test "returns error when subject has no permission to manage groups", %{ diff --git a/elixir/apps/domain/test/domain/resources_test.exs b/elixir/apps/domain/test/domain/resources_test.exs index 57474501d..28bded900 100644 --- a/elixir/apps/domain/test/domain/resources_test.exs +++ b/elixir/apps/domain/test/domain/resources_test.exs @@ -624,21 +624,23 @@ defmodule Domain.ResourcesTest do refute Map.has_key?(errors_on(changeset), :address) end - test "returns error on duplicate name", %{account: account, subject: subject} do - gateway = Fixtures.Gateways.create_gateway(account: account) - resource = Fixtures.Resources.create_resource(account: account, subject: subject) - address = Fixtures.Resources.resource_attrs().address + # We allow names to be duplicate because Resources are split into Sites + # and there is no way to create a unique constraint for many-to-many (join table) relation + # test "returns error on duplicate name", %{account: account, subject: subject} do + # gateway = Fixtures.Gateways.create_gateway(account: account) + # resource = Fixtures.Resources.create_resource(account: account, subject: subject) + # address = Fixtures.Resources.resource_attrs().address - attrs = %{ - "name" => resource.name, - "address" => address, - "type" => "dns", - "connections" => [%{"gateway_group_id" => gateway.group_id}] - } + # attrs = %{ + # "name" => resource.name, + # "address" => address, + # "type" => "dns", + # "connections" => [%{"gateway_group_id" => gateway.group_id}] + # } - assert {:error, changeset} = create_resource(attrs, subject) - assert errors_on(changeset) == %{name: ["has already been taken"]} - end + # assert {:error, changeset} = create_resource(attrs, subject) + # assert errors_on(changeset) == %{name: ["has already been taken"]} + # end test "creates a dns resource", %{account: account, subject: subject} do gateway = Fixtures.Gateways.create_gateway(account: account) diff --git a/elixir/apps/domain/test/support/fixtures/gateways.ex b/elixir/apps/domain/test/support/fixtures/gateways.ex index 65f46e0f0..22c358c04 100644 --- a/elixir/apps/domain/test/support/fixtures/gateways.ex +++ b/elixir/apps/domain/test/support/fixtures/gateways.ex @@ -5,7 +5,6 @@ defmodule Domain.Fixtures.Gateways do def group_attrs(attrs \\ %{}) do Enum.into(attrs, %{ name_prefix: "group-#{unique_integer()}", - tags: ["aws", "aws-us-east-#{unique_integer()}"], tokens: [%{}] }) end diff --git a/elixir/apps/web/lib/web/components/layouts/app.html.heex b/elixir/apps/web/lib/web/components/layouts/app.html.heex index da54e9291..3ba9b7cdd 100644 --- a/elixir/apps/web/lib/web/components/layouts/app.html.heex +++ b/elixir/apps/web/lib/web/components/layouts/app.html.heex @@ -23,14 +23,6 @@ Clients - <.sidebar_item - current_path={@current_path} - navigate={~p"/#{@account}/gateway_groups"} - icon="hero-arrow-left-on-rectangle-solid" - > - Gateways - - <.sidebar_item current_path={@current_path} navigate={~p"/#{@account}/relay_groups"} @@ -41,10 +33,10 @@ <.sidebar_item current_path={@current_path} - navigate={~p"/#{@account}/resources"} - icon="hero-server-stack-solid" + navigate={~p"/#{@account}/sites"} + icon="hero-globe-alt" > - Resources + Sites <.sidebar_item diff --git a/elixir/apps/web/lib/web/live/gateway_groups/index.ex b/elixir/apps/web/lib/web/live/gateway_groups/index.ex deleted file mode 100644 index 4d3d5a2cb..000000000 --- a/elixir/apps/web/lib/web/live/gateway_groups/index.ex +++ /dev/null @@ -1,144 +0,0 @@ -defmodule Web.GatewayGroups.Index do - use Web, :live_view - alias Domain.Gateways - - def mount(_params, _session, socket) do - subject = socket.assigns.subject - - with {:ok, groups} <- - Gateways.list_groups(subject, preload: [:gateways, connections: [:resource]]) do - :ok = Gateways.subscribe_for_gateways_presence_in_account(socket.assigns.account) - {:ok, assign(socket, groups: groups)} - else - {:error, _reason} -> raise Web.LiveErrors.NotFoundError - end - end - - def render(assigns) do - ~H""" - <.breadcrumbs account={@account}> - <.breadcrumb path={~p"/#{@account}/gateway_groups"}>Gateway Instance Groups - - - <.section> - <:title> - Gateways - - <:action> - <.add_button navigate={~p"/#{@account}/gateway_groups/new"}> - Add Instance Group - - - <:content> -
- - <.table_with_groups - id="groups" - groups={@groups} - group_items={& &1.gateways} - group_id={&"group-#{&1.id}"} - row_id={&"gateway-#{&1.id}"} - > - <:group :let={group}> - <.link - navigate={~p"/#{@account}/gateway_groups/#{group.id}"} - class="font-bold text-blue-600 dark:text-blue-500 hover:underline" - > - <%= group.name_prefix %> - - <%= if not Enum.empty?(group.tags), do: "(" <> Enum.join(group.tags, ", ") <> ")" %> - -
- Resources: - <.intersperse_blocks> - <:separator>, - - <:item :for={connection <- group.connections}> - <.link - navigate={~p"/#{@account}/resources/#{connection.resource}"} - class="font-medium text-blue-600 dark:text-blue-500 hover:underline inline-block" - phx-no-format - ><%= connection.resource.name %> - - -
- - - <:col :let={gateway} label="INSTANCE"> - <.link - navigate={~p"/#{@account}/gateways/#{gateway.id}"} - class="font-medium text-blue-600 dark:text-blue-500 hover:underline" - > - <%= gateway.name_suffix %> - - - <:col :let={gateway} label="REMOTE IP"> - - <%= gateway.last_seen_remote_ip %> - - - - <:col :let={gateway} label="STATUS"> - <.connection_status schema={gateway} /> - - <:empty> -
-
-
- No gateway instance groups to display -
- <.add_button navigate={~p"/#{@account}/gateway_groups/new"}> - Add Instance Group - -
-
- - - -
- - - """ - end - - def resource_filter(assigns) do - ~H""" -
-
-
- -
-
- <.icon name="hero-magnifying-glass" class="w-5 h-5 text-gray-500 dark:text-gray-400" /> -
- -
-
-
- <.button_group> - <:first> - All - - <:middle> - Online - - <:last> - Deleted - - -
- """ - end - - def handle_info(%Phoenix.Socket.Broadcast{topic: "gateways:" <> _account_id}, socket) do - subject = socket.assigns.subject - {:ok, groups} = Gateways.list_groups(subject, preload: [:gateways, connections: [:resource]]) - {:noreply, assign(socket, groups: groups)} - end -end diff --git a/elixir/apps/web/lib/web/live/gateway_groups/new.ex b/elixir/apps/web/lib/web/live/gateway_groups/new.ex deleted file mode 100644 index 3f5315ee1..000000000 --- a/elixir/apps/web/lib/web/live/gateway_groups/new.ex +++ /dev/null @@ -1,131 +0,0 @@ -defmodule Web.GatewayGroups.New do - use Web, :live_view - alias Domain.Gateways - - def mount(_params, _session, socket) do - changeset = Gateways.new_group() - {:ok, assign(socket, form: to_form(changeset), group: nil)} - end - - def render(assigns) do - ~H""" - <.breadcrumbs account={@account}> - <.breadcrumb path={~p"/#{@account}/gateway_groups"}>Gateway Instance Groups - <.breadcrumb path={~p"/#{@account}/gateway_groups/new"}>Add - - - <.section> - <:title :if={is_nil(@group)}> - Add a new Gateway Instance Group - - <:title :if={not is_nil(@group)}> - Deploy your Gateway Instance - - <:content> -
- <.form :if={is_nil(@group)} for={@form} phx-change={:change} phx-submit={:submit}> -
-
- <.input - label="Name Prefix" - field={@form[:name_prefix]} - placeholder="Name of this Gateway Instance Group" - required - /> -
- -
- <.input label="Tags" type="taglist" field={@form[:tags]} placeholder="Tag" /> -
-
- - <.submit_button> - Save - - - -
-
- Select deployment method: -
- <.tabs id="deployment-instructions"> - <:tab id="docker-instructions" label="Docker"> - <.code_block id="code-sample-docker" class="w-full rounded-b-lg" phx-no-format> - docker run -d \
-   --name=firezone-gateway-0 \
-   --restart=always \
-   -v /dev/net/tun:/dev/net/tun \
-   -e FZ_SECRET=<%= Gateways.encode_token!(hd(@group.tokens)) %> \
-   us-east1-docker.pkg.dev/firezone/firezone/gateway:stable - - - <:tab id="systemd-instructions" label="Systemd"> - <.code_block id="code-sample-systemd" class="w-full rounded-b-lg" phx-no-format> - [Unit]
- Description=zigbee2mqtt
- After=network.target
-
- [Service]
- ExecStart=/usr/bin/npm start
- WorkingDirectory=/opt/zigbee2mqtt
- StandardOutput=inherit
- StandardError=inherit
- Restart=always
- User=pi - - - - -
- Waiting for gateway connection... -
-
-
- - - """ - end - - def handle_event("delete:group[tags]", %{"index" => index}, socket) do - changeset = socket.assigns.form.source - values = Ecto.Changeset.fetch_field!(changeset, :tags) || [] - values = List.delete_at(values, String.to_integer(index)) - changeset = Ecto.Changeset.put_change(changeset, :tags, values) - {:noreply, assign(socket, form: to_form(changeset))} - end - - def handle_event("add:group[tags]", _params, socket) do - changeset = socket.assigns.form.source - values = Ecto.Changeset.fetch_field!(changeset, :tags) || [] - changeset = Ecto.Changeset.put_change(changeset, :tags, values ++ [""]) - {:noreply, assign(socket, form: to_form(changeset))} - end - - def handle_event("change", %{"group" => attrs}, socket) do - changeset = - Gateways.new_group(attrs) - |> Map.put(:action, :insert) - - {:noreply, assign(socket, form: to_form(changeset))} - end - - def handle_event("submit", %{"group" => attrs}, socket) do - attrs = Map.put(attrs, "tokens", [%{}]) - - with {:ok, group} <- - Gateways.create_group(attrs, socket.assigns.subject) do - :ok = Gateways.subscribe_for_gateways_presence_in_group(group) - {:noreply, assign(socket, group: group)} - else - {:error, changeset} -> - {:noreply, assign(socket, form: to_form(changeset))} - end - end - - def handle_info(%Phoenix.Socket.Broadcast{topic: "gateway_groups:" <> _account_id}, socket) do - socket = - redirect(socket, to: ~p"/#{socket.assigns.account}/gateway_groups/#{socket.assigns.group}") - - {:noreply, socket} - end -end diff --git a/elixir/apps/web/lib/web/live/gateways/show.ex b/elixir/apps/web/lib/web/live/gateways/show.ex index da74dc994..63ac8cd9a 100644 --- a/elixir/apps/web/lib/web/live/gateways/show.ex +++ b/elixir/apps/web/lib/web/live/gateways/show.ex @@ -20,10 +20,13 @@ defmodule Web.Gateways.Show do def render(assigns) do ~H""" <.breadcrumbs account={@account}> - <.breadcrumb path={~p"/#{@account}/gateway_groups"}>Gateway Instance Groups - <.breadcrumb path={~p"/#{@account}/gateway_groups/#{@gateway.group}"}> + <.breadcrumb path={~p"/#{@account}/sites"}>Sites + <.breadcrumb path={~p"/#{@account}/sites/#{@gateway.group}"}> <%= @gateway.group.name_prefix %> + <.breadcrumb path={~p"/#{@account}/sites/#{@gateway.group}?#gateways"}> + Gateways + <.breadcrumb path={~p"/#{@account}/gateways/#{@gateway}"}> <%= @gateway.name_suffix %> @@ -35,10 +38,10 @@ defmodule Web.Gateways.Show do <:content> <.vertical_table id="gateway"> <.vertical_table_row> - <:label>Instance Group Name + <:label>Site <:value> <.link - navigate={~p"/#{@account}/gateway_groups/#{@gateway.group}"} + navigate={~p"/#{@account}/sites/#{@gateway.group}"} class="font-bold text-blue-600 dark:text-blue-500 hover:underline" > <%= @gateway.group.name_prefix %> @@ -191,7 +194,7 @@ defmodule Web.Gateways.Show do socket = redirect(socket, - to: ~p"/#{socket.assigns.account}/gateway_groups/#{socket.assigns.gateway.group}" + to: ~p"/#{socket.assigns.account}/sites/#{socket.assigns.gateway.group}" ) {:noreply, socket} diff --git a/elixir/apps/web/lib/web/live/policies/new.ex b/elixir/apps/web/lib/web/live/policies/new.ex index 7cff723df..1b5e05ded 100644 --- a/elixir/apps/web/lib/web/live/policies/new.ex +++ b/elixir/apps/web/lib/web/live/policies/new.ex @@ -3,7 +3,8 @@ defmodule Web.Policies.New do alias Domain.{Resources, Actors, Policies} def mount(_params, _session, socket) do - with {:ok, resources} <- Resources.list_resources(socket.assigns.subject), + with {:ok, resources} <- + Resources.list_resources(socket.assigns.subject, preload: [:gateway_groups]), {:ok, actor_groups} <- Actors.list_groups(socket.assigns.subject) do form = to_form(Policies.new_policy(%{}, socket.assigns.subject)) @@ -48,7 +49,16 @@ defmodule Web.Policies.New do field={@form[:resource_id]} label="Resource" type="select" - options={Enum.map(@resources, fn r -> [key: r.name, value: r.id] end)} + options={ + Enum.map(@resources, fn resource -> + group_names = resource.gateway_groups |> Enum.map(& &1.name_prefix) + + [ + key: "#{resource.name} - #{Enum.join(group_names, ",")}", + value: resource.id + ] + end) + } value={@form[:resource_id].value} required /> diff --git a/elixir/apps/web/lib/web/live/resources/components.ex b/elixir/apps/web/lib/web/live/resources/components.ex index 5e5c5942b..949bae992 100644 --- a/elixir/apps/web/lib/web/live/resources/components.ex +++ b/elixir/apps/web/lib/web/live/resources/components.ex @@ -173,7 +173,7 @@ defmodule Web.Resources.Components do ~H"""
- Gateway Instance Groups + Sites <.error :for={msg <- @errors} data-validation-error-for="connections"> <%= msg %> @@ -205,17 +205,13 @@ defmodule Web.Resources.Components do
<.link - navigate={~p"/#{@account}/gateway_groups/#{gateway_group.id}"} + navigate={~p"/#{@account}/sites/#{gateway_group}"} class="font-bold text-blue-600 dark:text-blue-500 hover:underline" target="_blank" > <%= gateway_group.name_prefix %>
- -
- <%= Enum.join(gateway_group.tags, ", ") %> -
diff --git a/elixir/apps/web/lib/web/live/resources/index.ex b/elixir/apps/web/lib/web/live/resources/index.ex index ae0d097f7..e5b6041f6 100644 --- a/elixir/apps/web/lib/web/live/resources/index.ex +++ b/elixir/apps/web/lib/web/live/resources/index.ex @@ -45,7 +45,7 @@ defmodule Web.Resources.Index do <:col :let={resource} label="GATEWAY INSTANCE GROUP"> <.link :for={gateway_group <- resource.gateway_groups} - navigate={~p"/#{@account}/gateway_groups"} + navigate={~p"/#{@account}/sites"} class="font-medium text-blue-600 dark:text-blue-500 hover:underline" > <.badge type="info"> diff --git a/elixir/apps/web/lib/web/live/resources/show.ex b/elixir/apps/web/lib/web/live/resources/show.ex index b522e2c40..a7d140cc2 100644 --- a/elixir/apps/web/lib/web/live/resources/show.ex +++ b/elixir/apps/web/lib/web/live/resources/show.ex @@ -84,13 +84,13 @@ defmodule Web.Resources.Show do <.section> <:title> - Linked Gateway Instance Groups + Sites <:content> <.table id="gateway_instance_groups" rows={@resource.gateway_groups}> <:col :let={gateway_group} label="NAME"> <.link - navigate={~p"/#{@account}/gateway_groups/#{gateway_group}"} + navigate={~p"/#{@account}/sites/#{gateway_group}"} class="font-medium text-blue-600 dark:text-blue-500 hover:underline" > <%= gateway_group.name_prefix %> diff --git a/elixir/apps/web/lib/web/live/gateway_groups/edit.ex b/elixir/apps/web/lib/web/live/sites/edit.ex similarity index 57% rename from elixir/apps/web/lib/web/live/gateway_groups/edit.ex rename to elixir/apps/web/lib/web/live/sites/edit.ex index da4bafee3..9665da01d 100644 --- a/elixir/apps/web/lib/web/live/gateway_groups/edit.ex +++ b/elixir/apps/web/lib/web/live/sites/edit.ex @@ -1,4 +1,4 @@ -defmodule Web.GatewayGroups.Edit do +defmodule Web.Sites.Edit do use Web, :live_view alias Domain.Gateways @@ -14,15 +14,15 @@ defmodule Web.GatewayGroups.Edit do def render(assigns) do ~H""" <.breadcrumbs account={@account}> - <.breadcrumb path={~p"/#{@account}/gateway_groups"}>Gateway Instance Groups - <.breadcrumb path={~p"/#{@account}/gateway_groups/#{@group}"}> + <.breadcrumb path={~p"/#{@account}/sites"}>Sites + <.breadcrumb path={~p"/#{@account}/sites/#{@group}"}> <%= @group.name_prefix %> - <.breadcrumb path={~p"/#{@account}/gateway_groups/#{@group}/edit"}>Edit + <.breadcrumb path={~p"/#{@account}/sites/#{@group}/edit"}>Edit <.section> - <:title>Edit Gateway Instance Group: <%= @group.name_prefix %> + <:title>Edit Site: <%= @group.name_prefix %> <:content>
<.form for={@form} phx-change={:change} phx-submit={:submit}> @@ -31,13 +31,10 @@ defmodule Web.GatewayGroups.Edit do <.input label="Name Prefix" field={@form[:name_prefix]} - placeholder="Name of this Gateway Instance Group" + placeholder="Name of this Site" required />
-
- <.input label="Tags" type="taglist" field={@form[:tags]} placeholder="Tag" /> -
<.submit_button> Save @@ -49,21 +46,6 @@ defmodule Web.GatewayGroups.Edit do """ end - def handle_event("delete:group[tags]", %{"index" => index}, socket) do - changeset = socket.assigns.form.source - values = Ecto.Changeset.fetch_field!(changeset, :tags) || [] - values = List.delete_at(values, String.to_integer(index)) - changeset = Ecto.Changeset.put_change(changeset, :tags, values) - {:noreply, assign(socket, form: to_form(changeset))} - end - - def handle_event("add:group[tags]", _params, socket) do - changeset = socket.assigns.form.source - values = Ecto.Changeset.fetch_field!(changeset, :tags) || [] - changeset = Ecto.Changeset.put_change(changeset, :tags, values ++ [""]) - {:noreply, assign(socket, form: to_form(changeset))} - end - def handle_event("change", %{"group" => attrs}, socket) do changeset = Gateways.change_group(socket.assigns.group, attrs) @@ -75,7 +57,7 @@ defmodule Web.GatewayGroups.Edit do def handle_event("submit", %{"group" => attrs}, socket) do with {:ok, group} <- Gateways.update_group(socket.assigns.group, attrs, socket.assigns.subject) do - socket = redirect(socket, to: ~p"/#{socket.assigns.account}/gateway_groups/#{group}") + socket = redirect(socket, to: ~p"/#{socket.assigns.account}/sites/#{group}") {:noreply, socket} else {:error, changeset} -> diff --git a/elixir/apps/web/lib/web/live/sites/index.ex b/elixir/apps/web/lib/web/live/sites/index.ex new file mode 100644 index 000000000..9848f8e10 --- /dev/null +++ b/elixir/apps/web/lib/web/live/sites/index.ex @@ -0,0 +1,133 @@ +defmodule Web.Sites.Index do + use Web, :live_view + alias Domain.Gateways + + def mount(_params, _session, socket) do + subject = socket.assigns.subject + + with {:ok, groups} <- + Gateways.list_groups(subject, preload: [:gateways, connections: [:resource]]) do + :ok = Gateways.subscribe_for_gateways_presence_in_account(socket.assigns.account) + {:ok, assign(socket, groups: groups)} + else + {:error, _reason} -> raise Web.LiveErrors.NotFoundError + end + end + + def render(assigns) do + ~H""" + <.breadcrumbs account={@account}> + <.breadcrumb path={~p"/#{@account}/sites"}>Sites + + + <.section> + <:title> + Sites + + <:action> + <.add_button navigate={~p"/#{@account}/sites/new"}> + Add Site + + + <:content> + <.table id="groups" rows={@groups} row_id={&"group-#{&1.id}"}> + <:col :let={group} label="site"> + <.link + navigate={~p"/#{@account}/sites/#{group}"} + class="font-bold text-blue-600 dark:text-blue-500 hover:underline" + > + <%= group.name_prefix %> + + + + <:col :let={group} label="resources"> + <% connections = Enum.reject(group.connections, &is_nil(&1.resource)) + peek = %{count: length(connections), items: Enum.take(connections, 5)} %> + <.peek peek={peek}> + <:empty> + None + + + <:separator> + , + + + <:item :let={connection}> + <.link + navigate={~p"/#{@account}/resources/#{connection.resource}"} + class="font-medium text-blue-600 dark:text-blue-500 hover:underline inline-block" + phx-no-format + ><%= connection.resource.name %> + + + <:tail :let={count}> + + and + <.link + navigate={~p"/#{@account}/sites/#{group}?#resources"} + class="font-bold text-blue-600 dark:text-blue-500 hover:underline" + > + <%= count %> more. + + + + + + + <:col :let={group} label="gateways"> + <% peek = %{count: length(group.gateways), items: Enum.take(group.gateways, 5)} %> + <.peek peek={peek}> + <:empty> + None + + + <:separator> + , + + + <:item :let={gateway}> + <.link + navigate={~p"/#{@account}/gateways/#{gateway}"} + class="font-medium text-blue-600 dark:text-blue-500 hover:underline inline-block" + phx-no-format + ><%= gateway.name_suffix %> + + + <:tail :let={count}> + + and + <.link + navigate={~p"/#{@account}/sites/#{group}?#gateways"} + class="font-bold text-blue-600 dark:text-blue-500 hover:underline" + > + <%= count %> more. + + + + + + + <:empty> +
+
+
+ No sites to display +
+ <.add_button navigate={~p"/#{@account}/sites/new"}> + Add Site + +
+
+ + + + + """ + end + + def handle_info(%Phoenix.Socket.Broadcast{topic: "gateways:" <> _account_id}, socket) do + subject = socket.assigns.subject + {:ok, groups} = Gateways.list_groups(subject, preload: [:gateways, connections: [:resource]]) + {:noreply, assign(socket, groups: groups)} + end +end diff --git a/elixir/apps/web/lib/web/live/sites/new.ex b/elixir/apps/web/lib/web/live/sites/new.ex new file mode 100644 index 000000000..929703a02 --- /dev/null +++ b/elixir/apps/web/lib/web/live/sites/new.ex @@ -0,0 +1,152 @@ +defmodule Web.Sites.New do + use Web, :live_view + alias Domain.Gateways + + def mount(_params, _session, socket) do + changeset = Gateways.new_group() + {:ok, assign(socket, form: to_form(changeset), group: nil)} + end + + def render(assigns) do + ~H""" + <.breadcrumbs account={@account}> + <.breadcrumb path={~p"/#{@account}/sites"}>Sites + <.breadcrumb path={~p"/#{@account}/sites/new"}>Add + + + <.section> + <:title :if={is_nil(@group)}> + Add a new Site + + <:title :if={not is_nil(@group)}> + Deploy your Gateway + + <:content> +
+ <.form :if={is_nil(@group)} for={@form} phx-change={:change} phx-submit={:submit}> +
+
+ <.input + label="Name Prefix" + field={@form[:name_prefix]} + placeholder="Name of this Site" + required + /> +
+
+ + <.submit_button> + Save + + + +
+
+ Select deployment method: +
+ <.tabs id="deployment-instructions"> + <:tab id="docker-instructions" label="Docker"> + <.code_block id="code-sample-docker" class="w-full rounded-b-lg" phx-no-format> + docker run -d \
+   --restart=unless-stopped \
+   --pull=always \
+   --health-cmd="ip link | grep tun-firezone" \
+   --name=firezone-gateway \
+   --cap-add=NET_ADMIN \
+   --sysctl net.ipv4.ip_forward=1 \
+   --sysctl net.ipv4.conf.all.src_valid_mark=1 \
+   --sysctl net.ipv6.conf.all.disable_ipv6=0 \
+   --sysctl net.ipv6.conf.all.forwarding=1 \
+   --sysctl net.ipv6.conf.default.forwarding=1 \
+   --device="/dev/net/tun:/dev/net/tun" \
+   --env FIREZONE_ID="<%= Ecto.UUID.generate() %>" \
+   --env FIREZONE_TOKEN="<%= Gateways.encode_token!(hd(@group.tokens)) %>" \
+   --env FIREZONE_ENABLE_MASQUERADE=1 \
+   --env FIREZONE_HOSTNAME="`hostname`" \
+   --env RUST_LOG="warn" \
+   ghcr.io/firezone/gateway:${FIREZONE_VERSION:-1} + + + <:tab id="systemd-instructions" label="Systemd"> + <.code_block id="code-sample-systemd" class="w-full rounded-b-lg" phx-no-format> + [Unit] + Description=Firezone Gateway + After=network.target + + [Service] + Type=simple + Environment="FIREZONE_TOKEN=<%= Gateways.encode_token!(hd(@group.tokens)) %>" + Environment="FIREZONE_VERSION=1.20231001.0" + Environment="FIREZONE_HOSTNAME=`hostname`" + Environment="FIREZONE_ENABLE_MASQUERADE=1" + ExecStartPre=/bin/sh -c ' \ + if [ -e /usr/local/bin/firezone-gateway ]; then \ + current_version=$(/usr/local/bin/firezone-gateway --version 2>&1 | awk "{print $NF}"); \ + else \ + current_version=""; \ + fi; \ + if [ ! "$$current_version" = "${FIREZONE_VERSION}" ]; then \ + arch=$(uname -m); \ + case $$arch in \ + aarch64) \ + bin_url="https://github.com/firezone/firezone/releases/download/${FIREZONE_VERSION}/gateway-aarch64-unknown-linux-musl-${FIREZONE_VERSION}" ;; \ + armv7l) \ + bin_url="https://github.com/firezone/firezone/releases/download/${FIREZONE_VERSION}/gateway-armv7-unknown-linux-musleabihf-${FIREZONE_VERSION}" ;; \ + x86_64) \ + bin_url="https://github.com/firezone/firezone/releases/download/${FIREZONE_VERSION}/gateway-x86_64-unknown-linux-musl-${FIREZONE_VERSION}" ;; \ + *) \ + echo "Unsupported architecture"; \ + exit 1 ;; \ + esac; \ + wget -O /usr/local/bin/firezone-gateway $$bin_url; \ + fi \ + ' + ExecStartPre=/usr/bin/chmod +x /usr/local/bin/firezone-gateway + ExecStart=/usr/local/bin/firezone-gateway + Restart=always + RestartSec=3 + + [Install] + WantedBy=multi-user.target + + + + +
+ Waiting for gateway connection... +
+
+
+ + + """ + end + + def handle_event("change", %{"group" => attrs}, socket) do + changeset = + Gateways.new_group(attrs) + |> Map.put(:action, :insert) + + {:noreply, assign(socket, form: to_form(changeset))} + end + + def handle_event("submit", %{"group" => attrs}, socket) do + attrs = Map.put(attrs, "tokens", [%{}]) + + with {:ok, group} <- + Gateways.create_group(attrs, socket.assigns.subject) do + :ok = Gateways.subscribe_for_gateways_presence_in_group(group) + {:noreply, assign(socket, group: group)} + else + {:error, changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end + + def handle_info(%Phoenix.Socket.Broadcast{topic: "gateway_groups:" <> _account_id}, socket) do + socket = + redirect(socket, to: ~p"/#{socket.assigns.account}/sites/#{socket.assigns.group}") + + {:noreply, socket} + end +end diff --git a/elixir/apps/web/lib/web/live/sites/new_token.ex b/elixir/apps/web/lib/web/live/sites/new_token.ex new file mode 100644 index 000000000..a0a0a89db --- /dev/null +++ b/elixir/apps/web/lib/web/live/sites/new_token.ex @@ -0,0 +1,114 @@ +defmodule Web.Sites.NewToken do + use Web, :live_view + alias Domain.Gateways + + def mount(%{"id" => id}, _session, socket) do + with {:ok, group} <- Gateways.fetch_group_by_id(id, socket.assigns.subject) do + {:ok, group} = + Gateways.update_group(%{group | tokens: []}, %{tokens: [%{}]}, socket.assigns.subject) + + {:ok, assign(socket, group: group)} + else + {:error, _reason} -> raise Web.LiveErrors.NotFoundError + end + end + + def render(assigns) do + ~H""" + <.breadcrumbs account={@account}> + <.breadcrumb path={~p"/#{@account}/sites"}>Sites + <.breadcrumb path={~p"/#{@account}/sites/#{@group}"}> + <%= @group.name_prefix %> + + <.breadcrumb path={~p"/#{@account}/sites/#{@group}/new_token"}>Deploy + + + <.section> + <:title :if={is_nil(@group)}> + Add a new Site + + <:title :if={not is_nil(@group)}> + Deploy your Gateway + + <:content> +
+
+ Select deployment method: +
+ <.tabs id="deployment-instructions"> + <:tab id="docker-instructions" label="Docker"> + <.code_block id="code-sample-docker" class="w-full rounded-b-lg" phx-no-format> + docker run -d \
+   --restart=unless-stopped \
+   --pull=always \
+   --health-cmd="ip link | grep tun-firezone" \
+   --name=firezone-gateway \
+   --cap-add=NET_ADMIN \
+   --sysctl net.ipv4.ip_forward=1 \
+   --sysctl net.ipv4.conf.all.src_valid_mark=1 \
+   --sysctl net.ipv6.conf.all.disable_ipv6=0 \
+   --sysctl net.ipv6.conf.all.forwarding=1 \
+   --sysctl net.ipv6.conf.default.forwarding=1 \
+   --device="/dev/net/tun:/dev/net/tun" \
+   --env FIREZONE_ID="<%= Ecto.UUID.generate() %>" \
+   --env FIREZONE_TOKEN="<%= Gateways.encode_token!(hd(@group.tokens)) %>" \
+   --env FIREZONE_ENABLE_MASQUERADE=1 \
+   --env FIREZONE_HOSTNAME="`hostname`" \
+   --env RUST_LOG="warn" \
+   ghcr.io/firezone/gateway:${FIREZONE_VERSION:-1} + + + <:tab id="systemd-instructions" label="Systemd"> + <.code_block id="code-sample-systemd" class="w-full rounded-b-lg" phx-no-format> + [Unit]
+ Description=Firezone Gateway
+ After=network.target
+
+ [Service]
+ Type=simple
+ Environment="FIREZONE_TOKEN=<%= Gateways.encode_token!(hd(@group.tokens)) %>"
+ Environment="FIREZONE_VERSION=1.20231001.0"
+ Environment="FIREZONE_HOSTNAME=`hostname`"
+ Environment="FIREZONE_ENABLE_MASQUERADE=1"
+ ExecStartPre=/bin/sh -c ' \
+ if [ -e /usr/local/bin/firezone-gateway ]; then \
+ current_version=$(/usr/local/bin/firezone-gateway --version 2>&1 | awk "{print $NF}"); \
+ else \
+ current_version=""; \
+ fi; \
+ if [ ! "$$current_version" = "${FIREZONE_VERSION}" ]; then \
+ arch=$(uname -m); \
+ case $$arch in \
+ aarch64) \
+ bin_url="https://github.com/firezone/firezone/releases/download/${FIREZONE_VERSION}/gateway-aarch64-unknown-linux-musl-${FIREZONE_VERSION}" ;; \
+ armv7l) \
+ bin_url="https://github.com/firezone/firezone/releases/download/${FIREZONE_VERSION}/gateway-armv7-unknown-linux-musleabihf-${FIREZONE_VERSION}" ;; \
+ x86_64) \
+ bin_url="https://github.com/firezone/firezone/releases/download/${FIREZONE_VERSION}/gateway-x86_64-unknown-linux-musl-${FIREZONE_VERSION}" ;; \
+ *) \
+ echo "Unsupported architecture"; \
+ exit 1 ;; \
+ esac; \
+ wget -O /usr/local/bin/firezone-gateway $$bin_url; \
+ fi \
+ '
+ ExecStartPre=/usr/bin/chmod +x /usr/local/bin/firezone-gateway
+ ExecStart=/usr/local/bin/firezone-gateway
+ Restart=always
+ RestartSec=3
+
+ [Install]
+ WantedBy=multi-user.target
+ + + + +
+ Waiting for gateway connection... +
+
+ + + """ + end +end diff --git a/elixir/apps/web/lib/web/live/sites/resources/edit.ex b/elixir/apps/web/lib/web/live/sites/resources/edit.ex new file mode 100644 index 000000000..4fcc37376 --- /dev/null +++ b/elixir/apps/web/lib/web/live/sites/resources/edit.ex @@ -0,0 +1,101 @@ +defmodule Web.Sites.Resources.Edit do + use Web, :live_view + import Web.Resources.Components + alias Domain.Gateways + alias Domain.Resources + + def mount(%{"gateway_group_id" => gateway_group_id, "id" => id}, _session, socket) do + with {:ok, gateway_group} <- + Gateways.fetch_group_by_id(gateway_group_id, socket.assigns.subject), + {:ok, resource} <- + Resources.fetch_resource_by_id(id, socket.assigns.subject, preload: [:connections]) do + form = + Resources.change_resource(resource, socket.assigns.subject) + |> to_form() + + {:ok, assign(socket, resource: resource, form: form), + temporary_assigns: [ + gateway_group: gateway_group + ]} + else + _other -> raise Web.LiveErrors.NotFoundError + end + end + + def render(assigns) do + ~H""" + <.breadcrumbs account={@account}> + <.breadcrumb path={~p"/#{@account}/sites"}>Sites + <.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}"}> + <%= @gateway_group.name_prefix %> + + <.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}?#resources"}>Resources + <.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}/resources/#{@resource}"}> + <%= @resource.name %> + + <.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}/resources/#{@resource}/edit"}> + Edit + + + <.section> + <:title> + Edit Resource + + <:content> +
+

Edit Resource details

+ + <.form for={@form} phx-change={:change} phx-submit={:submit} class="space-y-4 lg:space-y-6"> + <.input + field={@form[:name]} + type="text" + label="Name" + placeholder="Name this resource" + required + /> + + <.filters_form form={@form[:filters]} /> + + <.submit_button phx-disable-with="Updating Resource..."> + Save + + +
+ + + """ + end + + def handle_event("change", %{"resource" => attrs}, socket) do + attrs = + attrs + |> map_filters_form_attrs() + |> Map.delete("connections") + + changeset = + Resources.change_resource(socket.assigns.resource, attrs, socket.assigns.subject) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, form: to_form(changeset))} + end + + def handle_event("submit", %{"resource" => attrs}, socket) do + attrs = + attrs + |> map_filters_form_attrs() + |> Map.delete("connections") + + case Resources.update_resource(socket.assigns.resource, attrs, socket.assigns.subject) do + {:ok, resource} -> + {:noreply, + push_navigate(socket, + to: + ~p"/#{socket.assigns.account}/sites/#{socket.assigns.gateway_group}/resources/#{resource}" + )} + + {:error, changeset} -> + changeset = Map.put(changeset, :action, :validate) + {:noreply, assign(socket, form: to_form(changeset))} + end + end +end diff --git a/elixir/apps/web/lib/web/live/sites/resources/new.ex b/elixir/apps/web/lib/web/live/sites/resources/new.ex new file mode 100644 index 000000000..ed59ab0b0 --- /dev/null +++ b/elixir/apps/web/lib/web/live/sites/resources/new.ex @@ -0,0 +1,104 @@ +defmodule Web.Sites.Resources.New do + use Web, :live_view + import Web.Resources.Components + alias Domain.{Gateways, Resources} + + def mount(%{"gateway_group_id" => gateway_group_id}, _session, socket) do + with {:ok, gateway_group} <- + Gateways.fetch_group_by_id(gateway_group_id, socket.assigns.subject) do + changeset = Resources.new_resource(socket.assigns.account) + + {:ok, assign(socket, gateway_group: gateway_group), + temporary_assigns: [ + gateway_groups: [], + form: to_form(changeset) + ]} + else + _other -> raise Web.LiveErrors.NotFoundError + end + end + + def render(assigns) do + ~H""" + <.breadcrumbs account={@account}> + <.breadcrumb path={~p"/#{@account}/sites"}>Sites + <.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}"}> + <%= @gateway_group.name_prefix %> + + <.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}?#resources"}>Resources + <.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}/resources/new"}> + Add Resource + + + <.section> + <:title> + Add Resource + + <:content> +
+

Resource details

+ <.form for={@form} class="space-y-4 lg:space-y-6" phx-submit="submit" phx-change="change"> + <.input + field={@form[:name]} + type="text" + label="Name" + placeholder="Name this resource" + required + phx-debounce="300" + /> + + <.input + field={@form[:address]} + autocomplete="off" + type="text" + label="Address" + placeholder="Enter IP address, CIDR, or DNS name" + required + phx-debounce="300" + /> + + <.filters_form form={@form[:filters]} /> + + <.submit_button phx-disable-with="Creating Resource..."> + Save + + +
+ + + """ + end + + def handle_event("change", %{"resource" => attrs}, socket) do + attrs = + attrs + |> map_filters_form_attrs() + |> Map.put("connections", [%{gateway_group_id: socket.assigns.gateway_group.id}]) + + changeset = + Resources.new_resource(socket.assigns.account, attrs) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, form: to_form(changeset))} + end + + def handle_event("submit", %{"resource" => attrs}, socket) do + attrs = + attrs + |> map_filters_form_attrs() + |> Map.put("connections", [%{gateway_group_id: socket.assigns.gateway_group.id}]) + + case Resources.create_resource(attrs, socket.assigns.subject) do + {:ok, resource} -> + {:noreply, + push_navigate(socket, + to: + ~p"/#{socket.assigns.account}/sites/#{socket.assigns.gateway_group}/resources/#{resource}" + )} + + {:error, changeset} -> + changeset = Map.put(changeset, :action, :validate) + {:noreply, assign(socket, form: to_form(changeset))} + end + end +end diff --git a/elixir/apps/web/lib/web/live/sites/resources/show.ex b/elixir/apps/web/lib/web/live/sites/resources/show.ex new file mode 100644 index 000000000..b30133533 --- /dev/null +++ b/elixir/apps/web/lib/web/live/sites/resources/show.ex @@ -0,0 +1,195 @@ +defmodule Web.Sites.Resources.Show do + use Web, :live_view + import Web.Policies.Components + alias Domain.{Resources, Gateways, Flows} + + def mount(%{"gateway_group_id" => gateway_group_id, "id" => id}, _session, socket) do + with {:ok, gateway_group} <- + Gateways.fetch_group_by_id(gateway_group_id, socket.assigns.subject), + {:ok, resource} <- + Resources.fetch_resource_by_id(id, socket.assigns.subject, + preload: [created_by_identity: [:actor]] + ), + {:ok, flows} <- + Flows.list_flows_for(resource, socket.assigns.subject, + preload: [client: [:actor], gateway: [:group], policy: [:resource, :actor_group]] + ) do + {:ok, assign(socket, gateway_group: gateway_group, resource: resource, flows: flows)} + else + {:error, _reason} -> raise Web.LiveErrors.NotFoundError + end + end + + def render(assigns) do + ~H""" + <.breadcrumbs account={@account}> + <.breadcrumb path={~p"/#{@account}/sites"}>Sites + <.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}"}> + <%= @gateway_group.name_prefix %> + + <.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}?#resources"}>Resources + <.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}/resources/#{@resource.id}"}> + <%= @resource.name %> + + + <.section> + <:title> + Resource: <%= @resource.name %> + + <:action> + <.edit_button navigate={ + ~p"/#{@account}/sites/#{@gateway_group}/resources/#{@resource.id}/edit" + }> + Edit Resource + + + <:content> +
+ <.vertical_table id="resource"> + <.vertical_table_row> + <:label> + Name + + <:value> + <%= @resource.name %> + + + <.vertical_table_row> + <:label> + Address + + <:value> + <%= @resource.address %> + + + <.vertical_table_row> + <:label> + Traffic Filtering Rules + + <:value> +
+ No traffic filtering rules +
+
+ + <%= pretty_print_filter(filter) %> + +
+ + + <.vertical_table_row> + <:label> + Created + + <:value> + <.created_by account={@account} schema={@resource} /> + + + +
+ + + + <.section> + <:title> + Authorizations + + <:content> + <.table id="flows" rows={@flows} row_id={&"flows-#{&1.id}"}> + <:col :let={flow} label="AUTHORIZED AT"> + <.relative_datetime datetime={flow.inserted_at} /> + + <:col :let={flow} label="EXPIRES AT"> + <.relative_datetime datetime={flow.expires_at} /> + + <:col :let={flow} label="POLICY"> + <.link + navigate={~p"/#{@account}/policies/#{flow.policy_id}"} + class="font-medium text-blue-600 dark:text-blue-500 hover:underline" + > + <.policy_name policy={flow.policy} /> + + + <:col :let={flow} label="CLIENT, ACTOR (IP)"> + <.link + navigate={~p"/#{@account}/clients/#{flow.client_id}"} + class="font-medium text-blue-600 dark:text-blue-500 hover:underline" + > + <%= flow.client.name %> + + owned by + <.link + navigate={~p"/#{@account}/actors/#{flow.client.actor_id}"} + class="font-medium text-blue-600 dark:text-blue-500 hover:underline" + > + <%= flow.client.actor.name %> + + (<%= flow.client_remote_ip %>) + + <:col :let={flow} label="GATEWAY (IP)"> + <.link + navigate={~p"/#{@account}/gateways/#{flow.gateway_id}"} + class="font-medium text-blue-600 dark:text-blue-500 hover:underline" + > + <%= flow.gateway.group.name_prefix %>-<%= flow.gateway.name_suffix %> + + (<%= flow.gateway_remote_ip %>) + + <:col :let={flow} label="ACTIVITY"> + <.link + navigate={~p"/#{@account}/flows/#{flow.id}"} + class="font-medium text-blue-600 dark:text-blue-500 hover:underline" + > + Show + + + <:empty> +
No authorizations to display
+ + + + + + <.danger_zone> + <:action> + <.delete_button + data-confirm="Are you sure want to delete this resource?" + phx-click="delete" + phx-value-id={@resource.id} + > + Delete Resource + + + <:content> + + """ + end + + def handle_event("delete", %{"id" => _resource_id}, socket) do + {:ok, _} = Resources.delete_resource(socket.assigns.resource, socket.assigns.subject) + + {:noreply, + push_navigate(socket, + to: ~p"/#{socket.assigns.account}/sites/#{socket.assigns.gateway_group}?#resources" + )} + end + + defp pretty_print_filter(filter) do + case filter.protocol do + :all -> + "All Traffic Allowed" + + :icmp -> + "ICPM: Allowed" + + :tcp -> + "TCP: #{pretty_print_ports(filter.ports)}" + + :udp -> + "UDP: #{pretty_print_ports(filter.ports)}" + end + end + + defp pretty_print_ports([]), do: "any port" + defp pretty_print_ports(ports), do: Enum.join(ports, ", ") +end diff --git a/elixir/apps/web/lib/web/live/gateway_groups/show.ex b/elixir/apps/web/lib/web/live/sites/show.ex similarity index 76% rename from elixir/apps/web/lib/web/live/gateway_groups/show.ex rename to elixir/apps/web/lib/web/live/sites/show.ex index 3aac4f041..030cacad3 100644 --- a/elixir/apps/web/lib/web/live/gateway_groups/show.ex +++ b/elixir/apps/web/lib/web/live/sites/show.ex @@ -1,4 +1,4 @@ -defmodule Web.GatewayGroups.Show do +defmodule Web.Sites.Show do use Web, :live_view alias Domain.Gateways @@ -11,6 +11,11 @@ defmodule Web.GatewayGroups.Show do created_by_identity: [:actor] ] ) do + group = %{ + group + | gateways: Enum.sort_by(group.gateways, &{&1.online?, &1.name_suffix}, :desc) + } + :ok = Gateways.subscribe_for_gateways_presence_in_group(group) {:ok, assign(socket, group: group)} else @@ -21,38 +26,28 @@ defmodule Web.GatewayGroups.Show do def render(assigns) do ~H""" <.breadcrumbs account={@account}> - <.breadcrumb path={~p"/#{@account}/gateway_groups"}>Gateway Instance Groups - <.breadcrumb path={~p"/#{@account}/gateway_groups/#{@group}"}> + <.breadcrumb path={~p"/#{@account}/sites"}>Sites + <.breadcrumb path={~p"/#{@account}/sites/#{@group}"}> <%= @group.name_prefix %> <.section> <:title> - Gateway Instance Group: <%= @group.name_prefix %> + Site: <%= @group.name_prefix %> <:action> - <.edit_button navigate={~p"/#{@account}/gateway_groups/#{@group}/edit"}> - Edit Instance Group + <.edit_button navigate={~p"/#{@account}/sites/#{@group}/edit"}> + Edit Site <:content> <.vertical_table id="group"> <.vertical_table_row> - <:label>Instance Group Name + <:label>Name <:value><%= @group.name_prefix %> - <.vertical_table_row> - <:label>Tags - <:value> -
- <.badge :for={tag <- @group.tags} class="mb-2"> - <%= tag %> - -
- - <.vertical_table_row> <:label>Created <:value> @@ -64,7 +59,47 @@ defmodule Web.GatewayGroups.Show do <.section> - <:title>Gateway Instances + <:title> + Resources + + <:action> + <.add_button navigate={~p"/#{@account}/sites/#{@group}/resources/new"}> + Create + + + <:content> +
+ <.table + id="resources" + rows={Enum.reject(@group.connections, &is_nil(&1.resource))} + row_item={& &1.resource} + > + <:col :let={resource} label="NAME"> + <.link + navigate={~p"/#{@account}/sites/#{@group}/resources/#{resource.id}"} + class="font-medium text-blue-600 dark:text-blue-500 hover:underline" + > + <%= resource.name %> + + + <:col :let={resource} label="ADDRESS"> + <%= resource.address %> + + <:empty> +
No resources to display
+ + +
+ + + + <.section> + <:title>Gateways + <:action> + <.add_button navigate={~p"/#{@account}/sites/#{@group}/new_token"}> + Deploy + + <:content>
<.table id="gateways" rows={@group.gateways}> @@ -95,39 +130,13 @@ defmodule Web.GatewayGroups.Show do - <.section> - <:title> - Linked Resources - - <:content> -
- <.table id="resources" rows={@group.connections} row_item={& &1.resource}> - <:col :let={resource} label="NAME"> - <.link - navigate={~p"/#{@account}/resources/#{resource.id}"} - class="font-medium text-blue-600 dark:text-blue-500 hover:underline" - > - <%= resource.name %> - - - <:col :let={resource} label="ADDRESS"> - <%= resource.address %> - - <:empty> -
No resources to display
- - -
- - - <.danger_zone> <:action> <.delete_button phx-click="delete" data-confirm="Are you sure want to delete this gateway group and disconnect all it's gateways?" > - Delete Instance Group + Delete Site <:content> @@ -137,7 +146,7 @@ defmodule Web.GatewayGroups.Show do def handle_info(%Phoenix.Socket.Broadcast{topic: "gateway_groups:" <> _account_id}, socket) do socket = - redirect(socket, to: ~p"/#{socket.assigns.account}/gateway_groups/#{socket.assigns.group}") + redirect(socket, to: ~p"/#{socket.assigns.account}/sites/#{socket.assigns.group}") {:noreply, socket} end @@ -145,6 +154,6 @@ defmodule Web.GatewayGroups.Show do def handle_event("delete", _params, socket) do # TODO: make sure tokens are all deleted too! {:ok, _group} = Gateways.delete_group(socket.assigns.group, socket.assigns.subject) - {:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/gateway_groups")} + {:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/sites")} end end diff --git a/elixir/apps/web/lib/web/router.ex b/elixir/apps/web/lib/web/router.ex index 12c4116f5..85955ca18 100644 --- a/elixir/apps/web/lib/web/router.ex +++ b/elixir/apps/web/lib/web/router.ex @@ -144,10 +144,18 @@ defmodule Web.Router do live "/:id", Show end - scope "/gateway_groups", GatewayGroups do + scope "/sites", Sites do live "/", Index live "/new", New + live "/:id/new_token", NewToken live "/:id/edit", Edit + + scope "/:gateway_group_id/resources", Resources do + live "/new", New + live "/:id/edit", Edit + live "/:id", Show + end + live "/:id", Show end diff --git a/elixir/apps/web/test/web/live/gateways/show_test.exs b/elixir/apps/web/test/web/live/gateways/show_test.exs index 6f0f58572..06a564fa6 100644 --- a/elixir/apps/web/test/web/live/gateways/show_test.exs +++ b/elixir/apps/web/test/web/live/gateways/show_test.exs @@ -61,7 +61,7 @@ defmodule Web.Live.Gateways.ShowTest do assert item = Floki.find(html, "[aria-label='Breadcrumb']") breadcrumbs = String.trim(Floki.text(item)) - assert breadcrumbs =~ "Gateway Instance Groups" + assert breadcrumbs =~ "Sites" assert breadcrumbs =~ gateway.group.name_prefix assert breadcrumbs =~ gateway.name_suffix end @@ -83,7 +83,7 @@ defmodule Web.Live.Gateways.ShowTest do |> render() |> vertical_table_to_map() - assert table["instance group name"] =~ gateway.group.name_prefix + assert table["site"] =~ gateway.group.name_prefix assert table["instance name"] =~ gateway.name_suffix assert table["last seen"] assert table["last seen remote ip"] =~ to_string(gateway.last_seen_remote_ip) @@ -165,7 +165,7 @@ defmodule Web.Live.Gateways.ShowTest do assert lv |> element("button", "Delete Gateway") |> render_click() == - {:error, {:redirect, %{to: ~p"/#{account}/gateway_groups/#{gateway.group}"}}} + {:error, {:redirect, %{to: ~p"/#{account}/sites/#{gateway.group}"}}} assert Repo.get(Domain.Gateways.Gateway, gateway.id).deleted_at end diff --git a/elixir/apps/web/test/web/live/nav/sidebar_test.exs b/elixir/apps/web/test/web/live/nav/sidebar_test.exs index 21248810a..5633557a9 100644 --- a/elixir/apps/web/test/web/live/nav/sidebar_test.exs +++ b/elixir/apps/web/test/web/live/nav/sidebar_test.exs @@ -62,14 +62,14 @@ defmodule Web.Live.Nav.SidebarTest do assert String.trim(Floki.text(item)) == "Clients" end - test "renders proper active sidebar item class for gateways", %{ + test "renders proper active sidebar item class for sites", %{ account: account, identity: identity, conn: conn } do - {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/gateway_groups") - assert item = Floki.find(html, "a.bg-gray-100[href='/#{account.id}/gateway_groups']") - assert String.trim(Floki.text(item)) == "Gateways" + {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/sites") + assert item = Floki.find(html, "a.bg-gray-100[href='/#{account.id}/sites']") + assert String.trim(Floki.text(item)) == "Sites" end test "renders proper active sidebar item class for relays", %{ @@ -82,15 +82,15 @@ defmodule Web.Live.Nav.SidebarTest do assert String.trim(Floki.text(item)) == "Relays" end - test "renders proper active sidebar item class for resources", %{ - account: account, - identity: identity, - conn: conn - } do - {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/resources") - assert item = Floki.find(html, "a.bg-gray-100[href='/#{account.id}/resources']") - assert String.trim(Floki.text(item)) == "Resources" - end + # test "renders proper active sidebar item class for resources", %{ + # account: account, + # identity: identity, + # conn: conn + # } do + # {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/resources") + # assert item = Floki.find(html, "a.bg-gray-100[href='/#{account.id}/resources']") + # assert String.trim(Floki.text(item)) == "Resources" + # end test "renders proper active sidebar item class for policies", %{ account: account, diff --git a/elixir/apps/web/test/web/live/resources/edit_test.exs b/elixir/apps/web/test/web/live/resources/edit_test.exs index 552afb46b..22fe908ec 100644 --- a/elixir/apps/web/test/web/live/resources/edit_test.exs +++ b/elixir/apps/web/test/web/live/resources/edit_test.exs @@ -151,9 +151,7 @@ defmodule Web.Live.Resources.EditTest do resource: resource, conn: conn } do - other_resource = Fixtures.Resources.create_resource(account: account) - - attrs = %{name: other_resource.name} + attrs = %{name: String.duplicate("a", 500)} {:ok, lv, _html} = conn @@ -164,7 +162,7 @@ defmodule Web.Live.Resources.EditTest do |> form("form", resource: attrs) |> render_submit() |> form_validation_errors() == %{ - "resource[name]" => ["has already been taken"] + "resource[name]" => ["should be at most 255 character(s)"] } connection_attrs = @@ -172,7 +170,7 @@ defmodule Web.Live.Resources.EditTest do {connection.gateway_group_id, %{enabled: false}} end - attrs = %{connections: connection_attrs} + attrs = %{name: "fooobar", connections: connection_attrs} assert lv |> form("form", resource: attrs) diff --git a/elixir/apps/web/test/web/live/resources/new_test.exs b/elixir/apps/web/test/web/live/resources/new_test.exs index 687bec57d..5b1753cee 100644 --- a/elixir/apps/web/test/web/live/resources/new_test.exs +++ b/elixir/apps/web/test/web/live/resources/new_test.exs @@ -129,17 +129,13 @@ defmodule Web.Live.Resources.NewTest do identity: identity, conn: conn } do - resource = Fixtures.Resources.create_resource(account: account) - [connection | _] = resource.connections - attrs = %{ - name: resource.name, + name: String.duplicate("a", 500), address: "foobar.com", filters: %{ tcp: %{ports: "80, 443", enabled: true}, udp: %{ports: "100", enabled: true} - }, - connections: %{connection.gateway_group_id => %{enabled: true}} + } } {:ok, lv, _html} = @@ -151,7 +147,8 @@ defmodule Web.Live.Resources.NewTest do |> form("form", resource: attrs) |> render_submit() |> form_validation_errors() == %{ - "resource[name]" => ["has already been taken"] + "resource[name]" => ["should be at most 255 character(s)"], + "connections" => ["can't be blank"] } end @@ -160,17 +157,16 @@ defmodule Web.Live.Resources.NewTest do identity: identity, conn: conn } do - resource = Fixtures.Resources.create_resource(account: account) - [connection | _] = resource.connections + gateway_group = Fixtures.Gateways.create_group(account: account) attrs = %{ name: "foobar.com", - address: resource.address, + address: "", filters: %{ tcp: %{ports: "80, 443", enabled: true}, udp: %{ports: "100", enabled: true} }, - connections: %{connection.gateway_group_id => %{enabled: true}} + connections: %{gateway_group.id => %{enabled: true}} } {:ok, lv, _html} = @@ -182,7 +178,7 @@ defmodule Web.Live.Resources.NewTest do |> form("form", resource: attrs) |> render_submit() |> form_validation_errors() == %{ - "resource[address]" => ["has already been taken"] + "resource[address]" => ["can't be blank"] } end diff --git a/elixir/apps/web/test/web/live/gateway_groups/edit_test.exs b/elixir/apps/web/test/web/live/sites/edit_test.exs similarity index 82% rename from elixir/apps/web/test/web/live/gateway_groups/edit_test.exs rename to elixir/apps/web/test/web/live/sites/edit_test.exs index 98b327ef2..8f2a34f94 100644 --- a/elixir/apps/web/test/web/live/gateway_groups/edit_test.exs +++ b/elixir/apps/web/test/web/live/sites/edit_test.exs @@ -1,4 +1,4 @@ -defmodule Web.Live.GatewayGroups.EditTest do +defmodule Web.Live.Sites.EditTest do use Web.ConnCase, async: true setup do @@ -21,7 +21,7 @@ defmodule Web.Live.GatewayGroups.EditTest do group: group, conn: conn } do - assert live(conn, ~p"/#{account}/gateway_groups/#{group}/edit") == + assert live(conn, ~p"/#{account}/sites/#{group}/edit") == {:error, {:redirect, %{ @@ -41,7 +41,7 @@ defmodule Web.Live.GatewayGroups.EditTest do assert_raise Web.LiveErrors.NotFoundError, fn -> conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/#{group}/edit") + |> live(~p"/#{account}/sites/#{group}/edit") end end @@ -54,11 +54,11 @@ defmodule Web.Live.GatewayGroups.EditTest do {:ok, _lv, html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/#{group}/edit") + |> live(~p"/#{account}/sites/#{group}/edit") assert item = Floki.find(html, "[aria-label='Breadcrumb']") breadcrumbs = String.trim(Floki.text(item)) - assert breadcrumbs =~ "Gateway Instance Groups" + assert breadcrumbs =~ "Sites" assert breadcrumbs =~ group.name_prefix assert breadcrumbs =~ "Edit" end @@ -72,13 +72,12 @@ defmodule Web.Live.GatewayGroups.EditTest do {:ok, lv, _html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/#{group}/edit") + |> live(~p"/#{account}/sites/#{group}/edit") form = form(lv, "form") assert find_inputs(form) == [ - "group[name_prefix]", - "group[tags][]" + "group[name_prefix]" ] end @@ -93,7 +92,7 @@ defmodule Web.Live.GatewayGroups.EditTest do {:ok, lv, _html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/#{group}/edit") + |> live(~p"/#{account}/sites/#{group}/edit") lv |> form("form", group: attrs) @@ -121,7 +120,7 @@ defmodule Web.Live.GatewayGroups.EditTest do {:ok, lv, _html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/#{group}/edit") + |> live(~p"/#{account}/sites/#{group}/edit") assert lv |> form("form", group: attrs) @@ -137,20 +136,19 @@ defmodule Web.Live.GatewayGroups.EditTest do group: group, conn: conn } do - attrs = Fixtures.Gateways.group_attrs() |> Map.take([:name_prefix, :tags]) + attrs = Fixtures.Gateways.group_attrs() |> Map.take([:name_prefix]) {:ok, lv, _html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/#{group}/edit") + |> live(~p"/#{account}/sites/#{group}/edit") assert lv |> form("form", group: attrs) |> render_submit() == - {:error, {:redirect, %{to: ~p"/#{account}/gateway_groups/#{group}"}}} + {:error, {:redirect, %{to: ~p"/#{account}/sites/#{group}"}}} assert group = Repo.get_by(Domain.Gateways.Group, id: group.id) assert group.name_prefix == attrs.name_prefix - assert group.tags == attrs.tags end end diff --git a/elixir/apps/web/test/web/live/gateway_groups/index_test.exs b/elixir/apps/web/test/web/live/sites/index_test.exs similarity index 50% rename from elixir/apps/web/test/web/live/gateway_groups/index_test.exs rename to elixir/apps/web/test/web/live/sites/index_test.exs index 6a2a9870a..9abe4c0f6 100644 --- a/elixir/apps/web/test/web/live/gateway_groups/index_test.exs +++ b/elixir/apps/web/test/web/live/sites/index_test.exs @@ -1,4 +1,4 @@ -defmodule Web.Live.GatewayGroups.IndexTest do +defmodule Web.Live.Sites.IndexTest do use Web.ConnCase, async: true setup do @@ -12,7 +12,7 @@ defmodule Web.Live.GatewayGroups.IndexTest do end test "redirects to sign in page for unauthorized user", %{account: account, conn: conn} do - assert live(conn, ~p"/#{account}/gateway_groups") == + assert live(conn, ~p"/#{account}/sites") == {:error, {:redirect, %{ @@ -29,11 +29,11 @@ defmodule Web.Live.GatewayGroups.IndexTest do {:ok, _lv, html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups") + |> live(~p"/#{account}/sites") assert item = Floki.find(html, "[aria-label='Breadcrumb']") breadcrumbs = String.trim(Floki.text(item)) - assert breadcrumbs =~ "Gateway Instance Groups" + assert breadcrumbs =~ "Sites" end test "renders add group button", %{ @@ -44,13 +44,13 @@ defmodule Web.Live.GatewayGroups.IndexTest do {:ok, _lv, html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups") + |> live(~p"/#{account}/sites") - assert button = Floki.find(html, "a[href='/#{account.id}/gateway_groups/new']") - assert Floki.text(button) =~ "Add Instance Group" + assert button = Floki.find(html, "a[href='/#{account.id}/sites/new']") + assert Floki.text(button) =~ "Add Site" end - test "renders groups table", %{ + test "renders sites table", %{ account: account, identity: identity, conn: conn @@ -58,7 +58,7 @@ defmodule Web.Live.GatewayGroups.IndexTest do group = Fixtures.Gateways.create_group(account: account) gateway = Fixtures.Gateways.create_gateway(account: account, group: group) - resources = + resource = Fixtures.Resources.create_resource( account: account, connections: [%{gateway_group_id: group.id}] @@ -67,50 +67,18 @@ defmodule Web.Live.GatewayGroups.IndexTest do {:ok, lv, _html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups") + |> live(~p"/#{account}/sites") - [%{"instance" => group_header} | group_rows] = + [row] = lv |> element("#groups") |> render() |> table_to_map() - assert group_header =~ group.name_prefix - - for tag <- group.tags do - assert group_header =~ tag - end - - assert group_header =~ resources.name - - group_rows - |> with_table_row("instance", gateway.name_suffix, fn row -> - assert row["remote ip"] =~ to_string(gateway.last_seen_remote_ip) - assert row["status"] =~ "Offline" - end) - end - - test "renders online status", %{ - account: account, - identity: identity, - conn: conn - } do - group = Fixtures.Gateways.create_group(account: account) - gateway = Fixtures.Gateways.create_gateway(account: account, group: group) - - :ok = Domain.Gateways.connect_gateway(gateway) - - {:ok, lv, _html} = - conn - |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups") - - lv - |> element("#groups") - |> render() - |> table_to_map() - |> with_table_row("instance", gateway.name_suffix, fn row -> - assert row["status"] =~ "Online" - end) + assert row == %{ + "site" => group.name_prefix, + "gateways" => gateway.name_suffix, + "resources" => resource.name + } end end diff --git a/elixir/apps/web/test/web/live/gateway_groups/new_test.exs b/elixir/apps/web/test/web/live/sites/new_test.exs similarity index 72% rename from elixir/apps/web/test/web/live/gateway_groups/new_test.exs rename to elixir/apps/web/test/web/live/sites/new_test.exs index 13462f3a7..0371cf484 100644 --- a/elixir/apps/web/test/web/live/gateway_groups/new_test.exs +++ b/elixir/apps/web/test/web/live/sites/new_test.exs @@ -1,4 +1,4 @@ -defmodule Web.Live.GatewayGroups.NewTest do +defmodule Web.Live.Sites.NewTest do use Web.ConnCase, async: true setup do @@ -17,7 +17,7 @@ defmodule Web.Live.GatewayGroups.NewTest do account: account, conn: conn } do - assert live(conn, ~p"/#{account}/gateway_groups/new") == + assert live(conn, ~p"/#{account}/sites/new") == {:error, {:redirect, %{ @@ -34,11 +34,11 @@ defmodule Web.Live.GatewayGroups.NewTest do {:ok, _lv, html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/new") + |> live(~p"/#{account}/sites/new") assert item = Floki.find(html, "[aria-label='Breadcrumb']") breadcrumbs = String.trim(Floki.text(item)) - assert breadcrumbs =~ "Gateway Instance Groups" + assert breadcrumbs =~ "Sites" assert breadcrumbs =~ "Add" end @@ -50,7 +50,7 @@ defmodule Web.Live.GatewayGroups.NewTest do {:ok, lv, _html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/new") + |> live(~p"/#{account}/sites/new") form = form(lv, "form") @@ -69,7 +69,7 @@ defmodule Web.Live.GatewayGroups.NewTest do {:ok, lv, _html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/new") + |> live(~p"/#{account}/sites/new") lv |> form("form", group: attrs) @@ -80,24 +80,6 @@ defmodule Web.Live.GatewayGroups.NewTest do end) end - test "allows adding tags to gateways", %{account: account, identity: identity, conn: conn} do - {:ok, lv, _html} = - conn - |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/new") - - lv - |> element("[phx-feedback-for='group[tags]'] button", "Add") - |> render_click() - - form = form(lv, "form") - - assert find_inputs(form) == [ - "group[name_prefix]", - "group[tags][]" - ] - end - test "renders changeset errors on submit", %{ account: account, identity: identity, @@ -109,7 +91,7 @@ defmodule Web.Live.GatewayGroups.NewTest do {:ok, lv, _html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/new") + |> live(~p"/#{account}/sites/new") assert lv |> form("form", group: attrs) @@ -124,16 +106,12 @@ defmodule Web.Live.GatewayGroups.NewTest do identity: identity, conn: conn } do - attrs = Fixtures.Gateways.group_attrs() |> Map.take([:name_prefix, :tags]) + attrs = Fixtures.Gateways.group_attrs() |> Map.take([:name_prefix]) {:ok, lv, _html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/new") - - lv - |> element("[phx-feedback-for='group[tags]'] button", "Add") - |> render_click() + |> live(~p"/#{account}/sites/new") html = lv @@ -141,22 +119,21 @@ defmodule Web.Live.GatewayGroups.NewTest do |> render_submit() assert html =~ "Select deployment method" - assert html =~ "FZ_SECRET=" + assert html =~ "FIREZONE_TOKEN=" assert html =~ "docker run" assert html =~ "Waiting for gateway connection..." - token = Regex.run(~r/FZ_SECRET=([^ ]+)/, html) |> List.last() + assert Regex.run(~r/FIREZONE_ID=([^ ]+)/, html) |> List.last() + token = Regex.run(~r/FIREZONE_TOKEN=([^ ]+)/, html) |> List.last() |> String.trim(""") assert {:ok, _token} = Domain.Gateways.authorize_gateway(token) group = Repo.get_by(Domain.Gateways.Group, name_prefix: attrs.name_prefix) |> Repo.preload(:tokens) - assert group.tags == attrs.tags - gateway = Fixtures.Gateways.create_gateway(account: account, group: group) Domain.Gateways.connect_gateway(gateway) - assert assert_redirect(lv, ~p"/#{account}/gateway_groups/#{group}") + assert assert_redirect(lv, ~p"/#{account}/sites/#{group}") end end diff --git a/elixir/apps/web/test/web/live/sites/resources/edit_test.exs b/elixir/apps/web/test/web/live/sites/resources/edit_test.exs new file mode 100644 index 000000000..a7f9601fc --- /dev/null +++ b/elixir/apps/web/test/web/live/sites/resources/edit_test.exs @@ -0,0 +1,240 @@ +defmodule Web.Live.Sites.Resources.EditTest do + use Web.ConnCase, async: true + + setup do + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(account: account, actor: actor, identity: identity) + + group = Fixtures.Gateways.create_group(account: account, subject: subject) + + resource = Fixtures.Resources.create_resource(account: account, subject: subject) + + %{ + account: account, + group: group, + actor: actor, + identity: identity, + resource: resource + } + end + + test "redirects to sign in page for unauthorized user", %{ + account: account, + group: group, + resource: resource, + conn: conn + } do + assert live(conn, ~p"/#{account}/sites/#{group}/resources/#{resource}/edit") == + {:error, + {:redirect, + %{ + to: ~p"/#{account}", + flash: %{"error" => "You must log in to access this page."} + }}} + end + + test "renders not found error when resource is deleted", %{ + account: account, + group: group, + identity: identity, + resource: resource, + conn: conn + } do + resource = Fixtures.Resources.delete_resource(resource) + + assert_raise Web.LiveErrors.NotFoundError, fn -> + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/#{resource}/edit") + end + end + + test "renders breadcrumbs item", %{ + account: account, + group: group, + identity: identity, + resource: resource, + conn: conn + } do + {:ok, _lv, html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/#{resource}/edit") + + assert item = Floki.find(html, "[aria-label='Breadcrumb']") + breadcrumbs = String.trim(Floki.text(item)) + assert breadcrumbs =~ "Sites" + assert breadcrumbs =~ group.name_prefix + assert breadcrumbs =~ "Resources" + assert breadcrumbs =~ resource.name + assert breadcrumbs =~ "Edit" + end + + test "renders form", %{ + account: account, + group: group, + identity: identity, + resource: resource, + conn: conn + } do + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/#{resource}/edit") + + form = form(lv, "form") + + assert find_inputs(form) == [ + "resource[filters][all][enabled]", + "resource[filters][all][protocol]", + "resource[filters][icmp][enabled]", + "resource[filters][icmp][protocol]", + "resource[filters][tcp][enabled]", + "resource[filters][tcp][ports]", + "resource[filters][tcp][protocol]", + "resource[filters][udp][enabled]", + "resource[filters][udp][ports]", + "resource[filters][udp][protocol]", + "resource[name]" + ] + end + + test "renders changeset errors on input change", %{ + account: account, + group: group, + identity: identity, + resource: resource, + conn: conn + } do + attrs = %{ + name: "foobar.com", + filters: %{ + tcp: %{ports: "80, 443"}, + udp: %{ports: "100"} + } + } + + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/#{resource}/edit") + + lv + |> form("form", resource: attrs) + |> validate_change(%{resource: %{name: String.duplicate("a", 256)}}, fn form, _html -> + assert form_validation_errors(form) == %{ + "resource[name]" => ["should be at most 255 character(s)"] + } + end) + |> validate_change(%{resource: %{filters: %{tcp: %{ports: "a"}}}}, fn form, _html -> + assert form_validation_errors(form) == %{ + "resource[filters][tcp][ports]" => ["is invalid"] + } + end) + |> validate_change(%{resource: %{filters: %{tcp: %{ports: "8080-90"}}}}, fn form, _html -> + assert form_validation_errors(form) == %{ + "resource[filters][tcp][ports]" => ["is invalid"] + } + end) + end + + test "renders changeset errors on submit", %{ + account: account, + group: group, + identity: identity, + resource: resource, + conn: conn + } do + attrs = %{name: String.duplicate("a", 500)} + + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/#{resource}/edit") + + assert lv + |> form("form", resource: attrs) + |> render_submit() + |> form_validation_errors() == %{ + "resource[name]" => ["should be at most 255 character(s)"] + } + end + + test "updates a resource on valid attrs", %{ + account: account, + group: group, + identity: identity, + resource: resource, + conn: conn + } do + attrs = %{ + name: "foobar.com", + filters: %{ + icmp: %{enabled: true}, + tcp: %{ports: "8080, 4443"}, + udp: %{ports: "4000 - 5000"} + } + } + + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/#{resource}/edit") + + assert lv + |> form("form", resource: attrs) + |> render_submit() == + {:error, + {:live_redirect, + %{to: ~p"/#{account}/sites/#{group}/resources/#{resource}", kind: :push}}} + + assert saved_resource = Repo.get_by(Domain.Resources.Resource, id: resource.id) + assert saved_resource.name == attrs.name + + saved_filters = + for filter <- saved_resource.filters, into: %{} do + {filter.protocol, %{ports: Enum.join(filter.ports, ", ")}} + end + + assert Map.keys(saved_filters) == Map.keys(attrs.filters) + assert saved_filters.tcp == attrs.filters.tcp + assert saved_filters.udp == attrs.filters.udp + end + + test "disables all filters on a resource when 'Permit All' filter is selected", %{ + account: account, + group: group, + identity: identity, + resource: resource, + conn: conn + } do + attrs = %{ + filters: %{ + all: %{enabled: true} + } + } + + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/#{resource}/edit") + + assert lv + |> form("form", resource: attrs) + |> render_submit() == + {:error, + {:live_redirect, + %{to: ~p"/#{account}/sites/#{group}/resources/#{resource}", kind: :push}}} + + assert saved_resource = Repo.get_by(Domain.Resources.Resource, id: resource.id) + + saved_filters = + for filter <- saved_resource.filters, into: %{} do + {filter.protocol, %{ports: Enum.join(filter.ports, ", ")}} + end + + assert saved_filters == %{all: %{ports: ""}} + end +end diff --git a/elixir/apps/web/test/web/live/sites/resources/new_test.exs b/elixir/apps/web/test/web/live/sites/resources/new_test.exs new file mode 100644 index 000000000..964b01c4b --- /dev/null +++ b/elixir/apps/web/test/web/live/sites/resources/new_test.exs @@ -0,0 +1,207 @@ +defmodule Web.Live.Sites.Resources.NewTest do + use Web.ConnCase, async: true + + setup do + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(account: account, actor: actor, identity: identity) + + group = Fixtures.Gateways.create_group(account: account, subject: subject) + + %{ + account: account, + group: group, + actor: actor, + identity: identity + } + end + + test "redirects to sign in page for unauthorized user", %{ + account: account, + group: group, + conn: conn + } do + assert live(conn, ~p"/#{account}/sites/#{group}/resources/new") == + {:error, + {:redirect, + %{ + to: ~p"/#{account}", + flash: %{"error" => "You must log in to access this page."} + }}} + end + + test "renders breadcrumbs item", %{ + account: account, + group: group, + identity: identity, + conn: conn + } do + {:ok, _lv, html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/new") + + assert item = Floki.find(html, "[aria-label='Breadcrumb']") + breadcrumbs = String.trim(Floki.text(item)) + assert breadcrumbs =~ "Sites" + assert breadcrumbs =~ group.name_prefix + assert breadcrumbs =~ "Resources" + assert breadcrumbs =~ "Add Resource" + end + + test "renders form", %{ + account: account, + group: group, + identity: identity, + conn: conn + } do + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/new") + + form = form(lv, "form") + + assert find_inputs(form) == [ + "resource[address]", + "resource[filters][all][enabled]", + "resource[filters][all][protocol]", + "resource[filters][icmp][enabled]", + "resource[filters][icmp][protocol]", + "resource[filters][tcp][enabled]", + "resource[filters][tcp][ports]", + "resource[filters][tcp][protocol]", + "resource[filters][udp][enabled]", + "resource[filters][udp][ports]", + "resource[filters][udp][protocol]", + "resource[name]" + ] + end + + test "renders changeset errors on input change", %{ + account: account, + group: group, + identity: identity, + conn: conn + } do + attrs = %{ + name: "foobar.com", + address: "foobar.com", + filters: %{ + tcp: %{ports: "80, 443", enabled: true}, + udp: %{ports: "100", enabled: true} + } + } + + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/new") + + lv + |> form("form", resource: attrs) + |> validate_change(%{resource: %{name: String.duplicate("a", 256)}}, fn form, _html -> + assert form_validation_errors(form) == %{ + "resource[name]" => ["should be at most 255 character(s)"] + } + end) + |> validate_change(%{resource: %{filters: %{tcp: %{ports: "a"}}}}, fn form, _html -> + assert form_validation_errors(form) == %{ + "resource[filters][tcp][ports]" => ["is invalid"] + } + end) + |> validate_change(%{resource: %{filters: %{tcp: %{ports: "8080-90"}}}}, fn form, _html -> + assert form_validation_errors(form) == %{ + "resource[filters][tcp][ports]" => ["is invalid"] + } + end) + end + + test "renders changeset errors for name on submit", %{ + account: account, + group: group, + identity: identity, + conn: conn + } do + attrs = %{ + name: String.duplicate("a", 500), + address: "foobar.com", + filters: %{ + tcp: %{ports: "80, 443", enabled: true}, + udp: %{ports: "100", enabled: true} + } + } + + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/new") + + assert lv + |> form("form", resource: attrs) + |> render_submit() + |> form_validation_errors() == %{ + "resource[name]" => ["should be at most 255 character(s)"] + } + end + + test "renders changeset errors for address on submit", %{ + account: account, + group: group, + identity: identity, + conn: conn + } do + attrs = %{ + name: "foobar.com", + address: "", + filters: %{ + tcp: %{ports: "80, 443", enabled: true}, + udp: %{ports: "100", enabled: true} + } + } + + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/new") + + assert lv + |> form("form", resource: attrs) + |> render_submit() + |> form_validation_errors() == %{ + "resource[address]" => ["can't be blank"] + } + end + + test "creates a resource on valid attrs", %{ + account: account, + group: group, + identity: identity, + conn: conn + } do + attrs = %{ + name: "foobar.com", + address: "foobar.com", + filters: %{ + icmp: %{enabled: true}, + tcp: %{ports: "80, 443"}, + udp: %{ports: "4000 - 5000"} + } + } + + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/new") + + lv + |> form("form", resource: attrs) + |> render_submit() + + resource = Repo.get_by(Domain.Resources.Resource, %{name: attrs.name, address: attrs.address}) + assert %{connections: [connection]} = Repo.preload(resource, :connections) + assert connection.gateway_group_id == group.id + assert assert_redirect(lv, ~p"/#{account}/sites/#{group}/resources/#{resource}") + end +end diff --git a/elixir/apps/web/test/web/live/sites/resources/show_test.exs b/elixir/apps/web/test/web/live/sites/resources/show_test.exs new file mode 100644 index 000000000..fba1d9726 --- /dev/null +++ b/elixir/apps/web/test/web/live/sites/resources/show_test.exs @@ -0,0 +1,194 @@ +defmodule Web.Live.Sites.Resources.ShowTest do + use Web.ConnCase, async: true + + setup do + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(account: account, actor: actor, identity: identity) + + group = Fixtures.Gateways.create_group(account: account, subject: subject) + gateway = Fixtures.Gateways.create_gateway(account: account, group: group) + gateway = Repo.preload(gateway, :group) + + resource = + Fixtures.Resources.create_resource( + account: account, + subject: subject, + connections: [%{gateway_group_id: group.id}] + ) + + %{ + account: account, + actor: actor, + identity: identity, + subject: subject, + group: group, + gateway: gateway, + resource: resource + } + end + + test "redirects to sign in page for unauthorized user", %{ + account: account, + group: group, + resource: resource, + conn: conn + } do + assert live(conn, ~p"/#{account}/sites/#{group}/resources/#{resource}") == + {:error, + {:redirect, + %{ + to: ~p"/#{account}", + flash: %{"error" => "You must log in to access this page."} + }}} + end + + test "renders not found error when resource is deleted", %{ + account: account, + group: group, + resource: resource, + identity: identity, + conn: conn + } do + resource = Fixtures.Resources.delete_resource(resource) + + assert_raise Web.LiveErrors.NotFoundError, fn -> + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/#{resource}") + end + end + + test "renders breadcrumbs item", %{ + account: account, + group: group, + resource: resource, + identity: identity, + conn: conn + } do + {:ok, _lv, html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/#{resource}") + + assert item = Floki.find(html, "[aria-label='Breadcrumb']") + breadcrumbs = String.trim(Floki.text(item)) + assert breadcrumbs =~ "Sites" + assert breadcrumbs =~ group.name_prefix + assert breadcrumbs =~ "Resources" + assert breadcrumbs =~ resource.name + end + + test "allows editing resource", %{ + account: account, + group: group, + resource: resource, + identity: identity, + conn: conn + } do + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/#{resource}") + + assert lv + |> element("a", "Edit Resource") + |> render_click() == + {:error, + {:live_redirect, + %{ + to: ~p"/#{account}/sites/#{group}/resources/#{resource}/edit", + kind: :push + }}} + end + + test "renders resource details", %{ + account: account, + group: group, + actor: actor, + identity: identity, + resource: resource, + conn: conn + } do + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/#{resource}") + + table = + lv + |> element("#resource") + |> render() + |> vertical_table_to_map() + + assert table["name"] =~ resource.name + assert table["address"] =~ resource.address + assert table["created"] =~ actor.name + + for filter <- resource.filters do + assert String.downcase(table["traffic filtering rules"]) =~ Atom.to_string(filter.protocol) + end + end + + test "renders logs table", %{ + account: account, + group: group, + identity: identity, + resource: resource, + conn: conn + } do + flow = + Fixtures.Flows.create_flow( + account: account, + resource: resource + ) + + flow = + Repo.preload(flow, client: [:actor], gateway: [:group], policy: [:actor_group, :resource]) + + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/#{resource}") + + [row] = + lv + |> element("#flows") + |> render() + |> table_to_map() + + assert row["authorized at"] + assert row["expires at"] + assert row["policy"] =~ flow.policy.actor_group.name + assert row["policy"] =~ flow.policy.resource.name + + assert row["gateway (ip)"] == + "#{flow.gateway.group.name_prefix}-#{flow.gateway.name_suffix} (189.172.73.153)" + + assert row["client, actor (ip)"] =~ flow.client.name + assert row["client, actor (ip)"] =~ "owned by #{flow.client.actor.name}" + assert row["client, actor (ip)"] =~ to_string(flow.client_remote_ip) + end + + test "allows deleting resource", %{ + account: account, + group: group, + resource: resource, + identity: identity, + conn: conn + } do + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}/resources/#{resource}") + + assert lv + |> element("button", "Delete Resource") + |> render_click() == + {:error, + {:live_redirect, %{to: ~p"/#{account}/sites/#{group}?#resources", kind: :push}}} + + assert Repo.get(Domain.Resources.Resource, resource.id).deleted_at + end +end diff --git a/elixir/apps/web/test/web/live/gateway_groups/show_test.exs b/elixir/apps/web/test/web/live/sites/show_test.exs similarity index 81% rename from elixir/apps/web/test/web/live/gateway_groups/show_test.exs rename to elixir/apps/web/test/web/live/sites/show_test.exs index a9e208e87..cf3fe69ef 100644 --- a/elixir/apps/web/test/web/live/gateway_groups/show_test.exs +++ b/elixir/apps/web/test/web/live/sites/show_test.exs @@ -1,4 +1,4 @@ -defmodule Web.Live.GatewayGroups.ShowTest do +defmodule Web.Live.Sites.ShowTest do use Web.ConnCase, async: true setup do @@ -26,7 +26,7 @@ defmodule Web.Live.GatewayGroups.ShowTest do group: group, conn: conn } do - assert live(conn, ~p"/#{account}/gateway_groups/#{group}") == + assert live(conn, ~p"/#{account}/sites/#{group}") == {:error, {:redirect, %{ @@ -46,7 +46,7 @@ defmodule Web.Live.GatewayGroups.ShowTest do assert_raise Web.LiveErrors.NotFoundError, fn -> conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/#{group}") + |> live(~p"/#{account}/sites/#{group}") end end @@ -59,11 +59,11 @@ defmodule Web.Live.GatewayGroups.ShowTest do {:ok, _lv, html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/#{group}") + |> live(~p"/#{account}/sites/#{group}") assert item = Floki.find(html, "[aria-label='Breadcrumb']") breadcrumbs = String.trim(Floki.text(item)) - assert breadcrumbs =~ "Gateway Instance Groups" + assert breadcrumbs =~ "Sites" assert breadcrumbs =~ group.name_prefix end @@ -76,13 +76,12 @@ defmodule Web.Live.GatewayGroups.ShowTest do {:ok, lv, _html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/#{group}") + |> live(~p"/#{account}/sites/#{group}") assert lv - |> element("a", "Edit Instance Group") + |> element("a", "Edit Site") |> render_click() == - {:error, - {:live_redirect, %{to: ~p"/#{account}/gateway_groups/#{group}/edit", kind: :push}}} + {:error, {:live_redirect, %{to: ~p"/#{account}/sites/#{group}/edit", kind: :push}}} end test "renders group details", %{ @@ -95,7 +94,7 @@ defmodule Web.Live.GatewayGroups.ShowTest do {:ok, lv, _html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/#{group}") + |> live(~p"/#{account}/sites/#{group}") table = lv @@ -103,7 +102,7 @@ defmodule Web.Live.GatewayGroups.ShowTest do |> render() |> vertical_table_to_map() - assert table["instance group name"] =~ group.name_prefix + assert table["name"] =~ group.name_prefix assert table["created"] =~ actor.name end @@ -118,7 +117,7 @@ defmodule Web.Live.GatewayGroups.ShowTest do {:ok, lv, _html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/#{group}") + |> live(~p"/#{account}/sites/#{group}") lv |> element("#gateways") @@ -142,7 +141,7 @@ defmodule Web.Live.GatewayGroups.ShowTest do {:ok, lv, _html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/#{group}") + |> live(~p"/#{account}/sites/#{group}") lv |> element("#gateways") @@ -165,12 +164,12 @@ defmodule Web.Live.GatewayGroups.ShowTest do {:ok, lv, _html} = conn |> authorize_conn(identity) - |> live(~p"/#{account}/gateway_groups/#{group}") + |> live(~p"/#{account}/sites/#{group}") assert lv |> element("button", "Delete") |> render_click() == - {:error, {:redirect, %{to: ~p"/#{account}/gateway_groups"}}} + {:error, {:redirect, %{to: ~p"/#{account}/sites"}}} assert Repo.get(Domain.Gateways.Group, group.id).deleted_at end diff --git a/rust/docker-init.sh b/rust/docker-init.sh index 0fcde3d15..899efd48a 100755 --- a/rust/docker-init.sh +++ b/rust/docker-init.sh @@ -1,6 +1,6 @@ #!/bin/sh -if [ "${ENABLE_MASQUERADE}" = "1" ]; then +if [ "${FIREZONE_ENABLE_MASQUERADE}" = "1" ]; then IFACE="tun-firezone" # TODO: Can we get away with not installing iptables? Nearly 20 MB. iptables-nft -A FORWARD -i $IFACE -j ACCEPT diff --git a/terraform/environments/staging/main.tf b/terraform/environments/staging/main.tf index b0a08d11b..0770eda97 100644 --- a/terraform/environments/staging/main.tf +++ b/terraform/environments/staging/main.tf @@ -400,8 +400,10 @@ locals { value = "support@firez.one" }, { - name = "OUTBOUND_EMAIL_ADAPTER_OPTS" - value = "{\"api_key\":\"${var.postmark_server_api_token}\"}" + name = "OUTBOUND_EMAIL_ADAPTER_OPTS" + value = jsonencode({ + api_key = var.postmark_server_api_token + }) } ] }