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"""
-
-
- <.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"""
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
+ })
}
]
}