mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}:")
|
||||
|
||||
@@ -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", %{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,14 +23,6 @@
|
||||
Clients
|
||||
</.sidebar_item>
|
||||
|
||||
<.sidebar_item
|
||||
current_path={@current_path}
|
||||
navigate={~p"/#{@account}/gateway_groups"}
|
||||
icon="hero-arrow-left-on-rectangle-solid"
|
||||
>
|
||||
Gateways
|
||||
</.sidebar_item>
|
||||
|
||||
<.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>
|
||||
|
||||
<.sidebar_item
|
||||
|
||||
@@ -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</.breadcrumb>
|
||||
</.breadcrumbs>
|
||||
|
||||
<.section>
|
||||
<:title>
|
||||
Gateways
|
||||
</:title>
|
||||
<:action>
|
||||
<.add_button navigate={~p"/#{@account}/gateway_groups/new"}>
|
||||
Add Instance Group
|
||||
</.add_button>
|
||||
</:action>
|
||||
<:content>
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden">
|
||||
<!--<.resource_filter />-->
|
||||
<.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 %>
|
||||
</.link>
|
||||
<%= if not Enum.empty?(group.tags), do: "(" <> Enum.join(group.tags, ", ") <> ")" %>
|
||||
|
||||
<div class="font-light flex">
|
||||
<span class="pr-1 inline-block">Resources:</span>
|
||||
<.intersperse_blocks>
|
||||
<:separator><span class="pr-1">,</span></: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 %></.link>
|
||||
</:item>
|
||||
</.intersperse_blocks>
|
||||
</div>
|
||||
</:group>
|
||||
|
||||
<: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 %>
|
||||
</.link>
|
||||
</:col>
|
||||
<:col :let={gateway} label="REMOTE IP">
|
||||
<code class="block text-xs">
|
||||
<%= gateway.last_seen_remote_ip %>
|
||||
</code>
|
||||
</:col>
|
||||
|
||||
<:col :let={gateway} label="STATUS">
|
||||
<.connection_status schema={gateway} />
|
||||
</:col>
|
||||
<:empty>
|
||||
<div class="flex justify-center text-center text-slate-500 p-4">
|
||||
<div class="w-auto">
|
||||
<div class="pb-4">
|
||||
No gateway instance groups to display
|
||||
</div>
|
||||
<.add_button navigate={~p"/#{@account}/gateway_groups/new"}>
|
||||
Add Instance Group
|
||||
</.add_button>
|
||||
</div>
|
||||
</div>
|
||||
</:empty>
|
||||
</.table_with_groups>
|
||||
<!--<.paginator page={3} total_pages={100} collection_base_path={~p"/#{@account}/gateway_groups"} />-->
|
||||
</div>
|
||||
</:content>
|
||||
</.section>
|
||||
"""
|
||||
end
|
||||
|
||||
def resource_filter(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col md:flex-row items-center justify-between space-y-3 md:space-y-0 md:space-x-4 p-4">
|
||||
<div class="w-full md:w-1/2">
|
||||
<form class="flex items-center">
|
||||
<label for="simple-search" class="sr-only">Search</label>
|
||||
<div class="relative w-full">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<.icon name="hero-magnifying-glass" class="w-5 h-5 text-gray-500 dark:text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="simple-search"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full pl-10 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
|
||||
placeholder="Search"
|
||||
required=""
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<.button_group>
|
||||
<:first>
|
||||
All
|
||||
</:first>
|
||||
<:middle>
|
||||
Online
|
||||
</:middle>
|
||||
<:last>
|
||||
Deleted
|
||||
</:last>
|
||||
</.button_group>
|
||||
</div>
|
||||
"""
|
||||
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
|
||||
@@ -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>
|
||||
<.breadcrumb path={~p"/#{@account}/gateway_groups/new"}>Add</.breadcrumb>
|
||||
</.breadcrumbs>
|
||||
|
||||
<.section>
|
||||
<:title :if={is_nil(@group)}>
|
||||
Add a new Gateway Instance Group
|
||||
</:title>
|
||||
<:title :if={not is_nil(@group)}>
|
||||
Deploy your Gateway Instance
|
||||
</:title>
|
||||
<:content>
|
||||
<div class="py-8 px-4 mx-auto max-w-2xl lg:py-16">
|
||||
<.form :if={is_nil(@group)} for={@form} phx-change={:change} phx-submit={:submit}>
|
||||
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
|
||||
<div>
|
||||
<.input
|
||||
label="Name Prefix"
|
||||
field={@form[:name_prefix]}
|
||||
placeholder="Name of this Gateway Instance Group"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<.input label="Tags" type="taglist" field={@form[:tags]} placeholder="Tag" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.submit_button>
|
||||
Save
|
||||
</.submit_button>
|
||||
</.form>
|
||||
|
||||
<div :if={not is_nil(@group)}>
|
||||
<div class="text-xl mb-2">
|
||||
Select deployment method:
|
||||
</div>
|
||||
<.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 \<br />
|
||||
--name=firezone-gateway-0 \<br />
|
||||
--restart=always \<br />
|
||||
-v /dev/net/tun:/dev/net/tun \<br />
|
||||
-e FZ_SECRET=<%= Gateways.encode_token!(hd(@group.tokens)) %> \<br />
|
||||
us-east1-docker.pkg.dev/firezone/firezone/gateway:stable
|
||||
</.code_block>
|
||||
</:tab>
|
||||
<:tab id="systemd-instructions" label="Systemd">
|
||||
<.code_block id="code-sample-systemd" class="w-full rounded-b-lg" phx-no-format>
|
||||
[Unit]<br />
|
||||
Description=zigbee2mqtt<br />
|
||||
After=network.target<br />
|
||||
<br />
|
||||
[Service]<br />
|
||||
ExecStart=/usr/bin/npm start<br />
|
||||
WorkingDirectory=/opt/zigbee2mqtt<br />
|
||||
StandardOutput=inherit<br />
|
||||
StandardError=inherit<br />
|
||||
Restart=always<br />
|
||||
User=pi
|
||||
</.code_block>
|
||||
</:tab>
|
||||
</.tabs>
|
||||
|
||||
<div class="mt-4 animate-pulse">
|
||||
Waiting for gateway connection...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</:content>
|
||||
</.section>
|
||||
"""
|
||||
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
|
||||
@@ -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>
|
||||
<.breadcrumb path={~p"/#{@account}/gateway_groups/#{@gateway.group}"}>
|
||||
<.breadcrumb path={~p"/#{@account}/sites"}>Sites</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@gateway.group}"}>
|
||||
<%= @gateway.group.name_prefix %>
|
||||
</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@gateway.group}?#gateways"}>
|
||||
Gateways
|
||||
</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/gateways/#{@gateway}"}>
|
||||
<%= @gateway.name_suffix %>
|
||||
</.breadcrumb>
|
||||
@@ -35,10 +38,10 @@ defmodule Web.Gateways.Show do
|
||||
<:content>
|
||||
<.vertical_table id="gateway">
|
||||
<.vertical_table_row>
|
||||
<:label>Instance Group Name</:label>
|
||||
<:label>Site</:label>
|
||||
<: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}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -173,7 +173,7 @@ defmodule Web.Resources.Components do
|
||||
|
||||
~H"""
|
||||
<fieldset class="flex flex-col gap-2">
|
||||
<legend class="mb-2">Gateway Instance Groups</legend>
|
||||
<legend class="mb-2">Sites</legend>
|
||||
|
||||
<.error :for={msg <- @errors} data-validation-error-for="connections">
|
||||
<%= msg %>
|
||||
@@ -205,17 +205,13 @@ defmodule Web.Resources.Components do
|
||||
|
||||
<div class="w-64 no-grow text-gray-500">
|
||||
<.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 %>
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= Enum.join(gateway_group.tags, ", ") %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -84,13 +84,13 @@ defmodule Web.Resources.Show do
|
||||
|
||||
<.section>
|
||||
<:title>
|
||||
Linked Gateway Instance Groups
|
||||
Sites
|
||||
</:title>
|
||||
<: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 %>
|
||||
|
||||
@@ -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>
|
||||
<.breadcrumb path={~p"/#{@account}/gateway_groups/#{@group}"}>
|
||||
<.breadcrumb path={~p"/#{@account}/sites"}>Sites</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@group}"}>
|
||||
<%= @group.name_prefix %>
|
||||
</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/gateway_groups/#{@group}/edit"}>Edit</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@group}/edit"}>Edit</.breadcrumb>
|
||||
</.breadcrumbs>
|
||||
|
||||
<.section>
|
||||
<:title>Edit Gateway Instance Group: <code><%= @group.name_prefix %></code></:title>
|
||||
<:title>Edit Site: <code><%= @group.name_prefix %></code></:title>
|
||||
<:content>
|
||||
<div class="py-8 px-4 mx-auto max-w-2xl lg:py-16">
|
||||
<.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
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<.input label="Tags" type="taglist" field={@form[:tags]} placeholder="Tag" />
|
||||
</div>
|
||||
</div>
|
||||
<.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} ->
|
||||
133
elixir/apps/web/lib/web/live/sites/index.ex
Normal file
133
elixir/apps/web/lib/web/live/sites/index.ex
Normal file
@@ -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</.breadcrumb>
|
||||
</.breadcrumbs>
|
||||
|
||||
<.section>
|
||||
<:title>
|
||||
Sites
|
||||
</:title>
|
||||
<:action>
|
||||
<.add_button navigate={~p"/#{@account}/sites/new"}>
|
||||
Add Site
|
||||
</.add_button>
|
||||
</:action>
|
||||
<: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 %>
|
||||
</.link>
|
||||
</:col>
|
||||
|
||||
<: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
|
||||
</:empty>
|
||||
|
||||
<:separator>
|
||||
<span class="pr-1">,</span>
|
||||
</: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 %></.link>
|
||||
</:item>
|
||||
|
||||
<:tail :let={count}>
|
||||
<span class="pl-1">
|
||||
and
|
||||
<.link
|
||||
navigate={~p"/#{@account}/sites/#{group}?#resources"}
|
||||
class="font-bold text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
<%= count %> more.
|
||||
</.link>
|
||||
</span>
|
||||
</:tail>
|
||||
</.peek>
|
||||
</:col>
|
||||
|
||||
<:col :let={group} label="gateways">
|
||||
<% peek = %{count: length(group.gateways), items: Enum.take(group.gateways, 5)} %>
|
||||
<.peek peek={peek}>
|
||||
<:empty>
|
||||
None
|
||||
</:empty>
|
||||
|
||||
<:separator>
|
||||
<span class="pr-1">,</span>
|
||||
</: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 %></.link>
|
||||
</:item>
|
||||
|
||||
<:tail :let={count}>
|
||||
<span class="pl-1">
|
||||
and
|
||||
<.link
|
||||
navigate={~p"/#{@account}/sites/#{group}?#gateways"}
|
||||
class="font-bold text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
<%= count %> more.
|
||||
</.link>
|
||||
</span>
|
||||
</:tail>
|
||||
</.peek>
|
||||
</:col>
|
||||
|
||||
<:empty>
|
||||
<div class="flex justify-center text-center text-slate-500 p-4">
|
||||
<div class="w-auto">
|
||||
<div class="pb-4">
|
||||
No sites to display
|
||||
</div>
|
||||
<.add_button navigate={~p"/#{@account}/sites/new"}>
|
||||
Add Site
|
||||
</.add_button>
|
||||
</div>
|
||||
</div>
|
||||
</:empty>
|
||||
</.table>
|
||||
</:content>
|
||||
</.section>
|
||||
"""
|
||||
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
|
||||
152
elixir/apps/web/lib/web/live/sites/new.ex
Normal file
152
elixir/apps/web/lib/web/live/sites/new.ex
Normal file
@@ -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>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/new"}>Add</.breadcrumb>
|
||||
</.breadcrumbs>
|
||||
|
||||
<.section>
|
||||
<:title :if={is_nil(@group)}>
|
||||
Add a new Site
|
||||
</:title>
|
||||
<:title :if={not is_nil(@group)}>
|
||||
Deploy your Gateway
|
||||
</:title>
|
||||
<:content>
|
||||
<div class="py-8 px-4 mx-auto max-w-2xl lg:py-16">
|
||||
<.form :if={is_nil(@group)} for={@form} phx-change={:change} phx-submit={:submit}>
|
||||
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
|
||||
<div>
|
||||
<.input
|
||||
label="Name Prefix"
|
||||
field={@form[:name_prefix]}
|
||||
placeholder="Name of this Site"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.submit_button>
|
||||
Save
|
||||
</.submit_button>
|
||||
</.form>
|
||||
|
||||
<div :if={not is_nil(@group)}>
|
||||
<div class="text-xl mb-2">
|
||||
Select deployment method:
|
||||
</div>
|
||||
<.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 \<br />
|
||||
--restart=unless-stopped \<br />
|
||||
--pull=always \<br />
|
||||
--health-cmd="ip link | grep tun-firezone" \<br />
|
||||
--name=firezone-gateway \<br />
|
||||
--cap-add=NET_ADMIN \<br />
|
||||
--sysctl net.ipv4.ip_forward=1 \<br />
|
||||
--sysctl net.ipv4.conf.all.src_valid_mark=1 \<br />
|
||||
--sysctl net.ipv6.conf.all.disable_ipv6=0 \<br />
|
||||
--sysctl net.ipv6.conf.all.forwarding=1 \<br />
|
||||
--sysctl net.ipv6.conf.default.forwarding=1 \<br />
|
||||
--device="/dev/net/tun:/dev/net/tun" \<br />
|
||||
--env FIREZONE_ID="<%= Ecto.UUID.generate() %>" \<br />
|
||||
--env FIREZONE_TOKEN="<%= Gateways.encode_token!(hd(@group.tokens)) %>" \<br />
|
||||
--env FIREZONE_ENABLE_MASQUERADE=1 \<br />
|
||||
--env FIREZONE_HOSTNAME="`hostname`" \<br />
|
||||
--env RUST_LOG="warn" \<br />
|
||||
ghcr.io/firezone/gateway:${FIREZONE_VERSION:-1}
|
||||
</.code_block>
|
||||
</:tab>
|
||||
<: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
|
||||
</.code_block>
|
||||
</:tab>
|
||||
</.tabs>
|
||||
|
||||
<div class="mt-4 animate-pulse">
|
||||
Waiting for gateway connection...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</:content>
|
||||
</.section>
|
||||
"""
|
||||
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
|
||||
114
elixir/apps/web/lib/web/live/sites/new_token.ex
Normal file
114
elixir/apps/web/lib/web/live/sites/new_token.ex
Normal file
@@ -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>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@group}"}>
|
||||
<%= @group.name_prefix %>
|
||||
</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@group}/new_token"}>Deploy</.breadcrumb>
|
||||
</.breadcrumbs>
|
||||
|
||||
<.section>
|
||||
<:title :if={is_nil(@group)}>
|
||||
Add a new Site
|
||||
</:title>
|
||||
<:title :if={not is_nil(@group)}>
|
||||
Deploy your Gateway
|
||||
</:title>
|
||||
<:content>
|
||||
<div class="py-8 px-4 mx-auto max-w-2xl lg:py-16">
|
||||
<div class="text-xl mb-2">
|
||||
Select deployment method:
|
||||
</div>
|
||||
<.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 \<br />
|
||||
--restart=unless-stopped \<br />
|
||||
--pull=always \<br />
|
||||
--health-cmd="ip link | grep tun-firezone" \<br />
|
||||
--name=firezone-gateway \<br />
|
||||
--cap-add=NET_ADMIN \<br />
|
||||
--sysctl net.ipv4.ip_forward=1 \<br />
|
||||
--sysctl net.ipv4.conf.all.src_valid_mark=1 \<br />
|
||||
--sysctl net.ipv6.conf.all.disable_ipv6=0 \<br />
|
||||
--sysctl net.ipv6.conf.all.forwarding=1 \<br />
|
||||
--sysctl net.ipv6.conf.default.forwarding=1 \<br />
|
||||
--device="/dev/net/tun:/dev/net/tun" \<br />
|
||||
--env FIREZONE_ID="<%= Ecto.UUID.generate() %>" \<br />
|
||||
--env FIREZONE_TOKEN="<%= Gateways.encode_token!(hd(@group.tokens)) %>" \<br />
|
||||
--env FIREZONE_ENABLE_MASQUERADE=1 \<br />
|
||||
--env FIREZONE_HOSTNAME="`hostname`" \<br />
|
||||
--env RUST_LOG="warn" \<br />
|
||||
ghcr.io/firezone/gateway:${FIREZONE_VERSION:-1}
|
||||
</.code_block>
|
||||
</:tab>
|
||||
<:tab id="systemd-instructions" label="Systemd">
|
||||
<.code_block id="code-sample-systemd" class="w-full rounded-b-lg" phx-no-format>
|
||||
[Unit]<br />
|
||||
Description=Firezone Gateway<br />
|
||||
After=network.target<br />
|
||||
<br />
|
||||
[Service]<br />
|
||||
Type=simple<br />
|
||||
Environment="FIREZONE_TOKEN=<%= Gateways.encode_token!(hd(@group.tokens)) %>"<br />
|
||||
Environment="FIREZONE_VERSION=1.20231001.0"<br />
|
||||
Environment="FIREZONE_HOSTNAME=`hostname`"<br />
|
||||
Environment="FIREZONE_ENABLE_MASQUERADE=1"<br />
|
||||
ExecStartPre=/bin/sh -c ' \<br />
|
||||
if [ -e /usr/local/bin/firezone-gateway ]; then \<br />
|
||||
current_version=$(/usr/local/bin/firezone-gateway --version 2>&1 | awk "{print $NF}"); \<br />
|
||||
else \<br />
|
||||
current_version=""; \<br />
|
||||
fi; \<br />
|
||||
if [ ! "$$current_version" = "${FIREZONE_VERSION}" ]; then \<br />
|
||||
arch=$(uname -m); \<br />
|
||||
case $$arch in \<br />
|
||||
aarch64) \<br />
|
||||
bin_url="https://github.com/firezone/firezone/releases/download/${FIREZONE_VERSION}/gateway-aarch64-unknown-linux-musl-${FIREZONE_VERSION}" ;; \<br />
|
||||
armv7l) \<br />
|
||||
bin_url="https://github.com/firezone/firezone/releases/download/${FIREZONE_VERSION}/gateway-armv7-unknown-linux-musleabihf-${FIREZONE_VERSION}" ;; \<br />
|
||||
x86_64) \<br />
|
||||
bin_url="https://github.com/firezone/firezone/releases/download/${FIREZONE_VERSION}/gateway-x86_64-unknown-linux-musl-${FIREZONE_VERSION}" ;; \<br />
|
||||
*) \<br />
|
||||
echo "Unsupported architecture"; \<br />
|
||||
exit 1 ;; \<br />
|
||||
esac; \<br />
|
||||
wget -O /usr/local/bin/firezone-gateway $$bin_url; \<br />
|
||||
fi \<br />
|
||||
'<br />
|
||||
ExecStartPre=/usr/bin/chmod +x /usr/local/bin/firezone-gateway<br />
|
||||
ExecStart=/usr/local/bin/firezone-gateway<br />
|
||||
Restart=always<br />
|
||||
RestartSec=3<br />
|
||||
<br />
|
||||
[Install]<br />
|
||||
WantedBy=multi-user.target<br />
|
||||
</.code_block>
|
||||
</:tab>
|
||||
</.tabs>
|
||||
|
||||
<div class="mt-4 animate-pulse">
|
||||
Waiting for gateway connection...
|
||||
</div>
|
||||
</div>
|
||||
</:content>
|
||||
</.section>
|
||||
"""
|
||||
end
|
||||
end
|
||||
101
elixir/apps/web/lib/web/live/sites/resources/edit.ex
Normal file
101
elixir/apps/web/lib/web/live/sites/resources/edit.ex
Normal file
@@ -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>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}"}>
|
||||
<%= @gateway_group.name_prefix %>
|
||||
</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}?#resources"}>Resources</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}/resources/#{@resource}"}>
|
||||
<%= @resource.name %>
|
||||
</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}/resources/#{@resource}/edit"}>
|
||||
Edit
|
||||
</.breadcrumb>
|
||||
</.breadcrumbs>
|
||||
<.section>
|
||||
<:title>
|
||||
Edit Resource
|
||||
</:title>
|
||||
<:content>
|
||||
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
|
||||
<h2 class="mb-4 text-xl font-bold text-gray-900 dark:text-white">Edit Resource details</h2>
|
||||
|
||||
<.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
|
||||
</.submit_button>
|
||||
</.form>
|
||||
</div>
|
||||
</:content>
|
||||
</.section>
|
||||
"""
|
||||
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
|
||||
104
elixir/apps/web/lib/web/live/sites/resources/new.ex
Normal file
104
elixir/apps/web/lib/web/live/sites/resources/new.ex
Normal file
@@ -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>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}"}>
|
||||
<%= @gateway_group.name_prefix %>
|
||||
</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}?#resources"}>Resources</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}/resources/new"}>
|
||||
Add Resource
|
||||
</.breadcrumb>
|
||||
</.breadcrumbs>
|
||||
<.section>
|
||||
<:title>
|
||||
Add Resource
|
||||
</:title>
|
||||
<:content>
|
||||
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
|
||||
<h2 class="mb-4 text-xl font-bold text-gray-900 dark:text-white">Resource details</h2>
|
||||
<.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
|
||||
</.submit_button>
|
||||
</.form>
|
||||
</div>
|
||||
</:content>
|
||||
</.section>
|
||||
"""
|
||||
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
|
||||
195
elixir/apps/web/lib/web/live/sites/resources/show.ex
Normal file
195
elixir/apps/web/lib/web/live/sites/resources/show.ex
Normal file
@@ -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>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}"}>
|
||||
<%= @gateway_group.name_prefix %>
|
||||
</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}?#resources"}>Resources</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@gateway_group}/resources/#{@resource.id}"}>
|
||||
<%= @resource.name %>
|
||||
</.breadcrumb>
|
||||
</.breadcrumbs>
|
||||
<.section>
|
||||
<:title>
|
||||
Resource: <code><%= @resource.name %></code>
|
||||
</:title>
|
||||
<:action>
|
||||
<.edit_button navigate={
|
||||
~p"/#{@account}/sites/#{@gateway_group}/resources/#{@resource.id}/edit"
|
||||
}>
|
||||
Edit Resource
|
||||
</.edit_button>
|
||||
</:action>
|
||||
<:content>
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden">
|
||||
<.vertical_table id="resource">
|
||||
<.vertical_table_row>
|
||||
<:label>
|
||||
Name
|
||||
</:label>
|
||||
<:value>
|
||||
<%= @resource.name %>
|
||||
</:value>
|
||||
</.vertical_table_row>
|
||||
<.vertical_table_row>
|
||||
<:label>
|
||||
Address
|
||||
</:label>
|
||||
<:value>
|
||||
<%= @resource.address %>
|
||||
</:value>
|
||||
</.vertical_table_row>
|
||||
<.vertical_table_row>
|
||||
<:label>
|
||||
Traffic Filtering Rules
|
||||
</:label>
|
||||
<:value>
|
||||
<div :if={@resource.filters == []} %>
|
||||
No traffic filtering rules
|
||||
</div>
|
||||
<div :for={filter <- @resource.filters} :if={@resource.filters != []} %>
|
||||
<code>
|
||||
<%= pretty_print_filter(filter) %>
|
||||
</code>
|
||||
</div>
|
||||
</:value>
|
||||
</.vertical_table_row>
|
||||
<.vertical_table_row>
|
||||
<:label>
|
||||
Created
|
||||
</:label>
|
||||
<:value>
|
||||
<.created_by account={@account} schema={@resource} />
|
||||
</:value>
|
||||
</.vertical_table_row>
|
||||
</.vertical_table>
|
||||
</div>
|
||||
</:content>
|
||||
</.section>
|
||||
|
||||
<.section>
|
||||
<:title>
|
||||
Authorizations
|
||||
</:title>
|
||||
<:content>
|
||||
<.table id="flows" rows={@flows} row_id={&"flows-#{&1.id}"}>
|
||||
<:col :let={flow} label="AUTHORIZED AT">
|
||||
<.relative_datetime datetime={flow.inserted_at} />
|
||||
</:col>
|
||||
<:col :let={flow} label="EXPIRES AT">
|
||||
<.relative_datetime datetime={flow.expires_at} />
|
||||
</:col>
|
||||
<: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} />
|
||||
</.link>
|
||||
</:col>
|
||||
<: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 %>
|
||||
</.link>
|
||||
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 %>
|
||||
</.link>
|
||||
(<%= flow.client_remote_ip %>)
|
||||
</:col>
|
||||
<: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 %>
|
||||
</.link>
|
||||
(<%= flow.gateway_remote_ip %>)
|
||||
</:col>
|
||||
<: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
|
||||
</.link>
|
||||
</:col>
|
||||
<:empty>
|
||||
<div class="text-center text-slate-500 p-4">No authorizations to display</div>
|
||||
</:empty>
|
||||
</.table>
|
||||
</:content>
|
||||
</.section>
|
||||
|
||||
<.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
|
||||
</.delete_button>
|
||||
</:action>
|
||||
<:content></:content>
|
||||
</.danger_zone>
|
||||
"""
|
||||
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
|
||||
@@ -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>
|
||||
<.breadcrumb path={~p"/#{@account}/gateway_groups/#{@group}"}>
|
||||
<.breadcrumb path={~p"/#{@account}/sites"}>Sites</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/sites/#{@group}"}>
|
||||
<%= @group.name_prefix %>
|
||||
</.breadcrumb>
|
||||
</.breadcrumbs>
|
||||
|
||||
<.section>
|
||||
<:title>
|
||||
Gateway Instance Group: <code><%= @group.name_prefix %></code>
|
||||
Site: <code><%= @group.name_prefix %></code>
|
||||
</:title>
|
||||
<:action>
|
||||
<.edit_button navigate={~p"/#{@account}/gateway_groups/#{@group}/edit"}>
|
||||
Edit Instance Group
|
||||
<.edit_button navigate={~p"/#{@account}/sites/#{@group}/edit"}>
|
||||
Edit Site
|
||||
</.edit_button>
|
||||
</:action>
|
||||
|
||||
<:content>
|
||||
<.vertical_table id="group">
|
||||
<.vertical_table_row>
|
||||
<:label>Instance Group Name</:label>
|
||||
<:label>Name</:label>
|
||||
<:value><%= @group.name_prefix %></:value>
|
||||
</.vertical_table_row>
|
||||
<.vertical_table_row>
|
||||
<:label>Tags</:label>
|
||||
<:value>
|
||||
<div class="flex flex-wrap">
|
||||
<.badge :for={tag <- @group.tags} class="mb-2">
|
||||
<%= tag %>
|
||||
</.badge>
|
||||
</div>
|
||||
</:value>
|
||||
</.vertical_table_row>
|
||||
<.vertical_table_row>
|
||||
<:label>Created</:label>
|
||||
<:value>
|
||||
@@ -64,7 +59,47 @@ defmodule Web.GatewayGroups.Show do
|
||||
</.section>
|
||||
|
||||
<.section>
|
||||
<:title>Gateway Instances</:title>
|
||||
<:title>
|
||||
Resources
|
||||
</:title>
|
||||
<:action>
|
||||
<.add_button navigate={~p"/#{@account}/sites/#{@group}/resources/new"}>
|
||||
Create
|
||||
</.add_button>
|
||||
</:action>
|
||||
<:content>
|
||||
<div class="relative overflow-x-auto">
|
||||
<.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 %>
|
||||
</.link>
|
||||
</:col>
|
||||
<:col :let={resource} label="ADDRESS">
|
||||
<%= resource.address %>
|
||||
</:col>
|
||||
<:empty>
|
||||
<div class="text-center text-slate-500 p-4">No resources to display</div>
|
||||
</:empty>
|
||||
</.table>
|
||||
</div>
|
||||
</:content>
|
||||
</.section>
|
||||
|
||||
<.section>
|
||||
<:title>Gateways</:title>
|
||||
<:action>
|
||||
<.add_button navigate={~p"/#{@account}/sites/#{@group}/new_token"}>
|
||||
Deploy
|
||||
</.add_button>
|
||||
</:action>
|
||||
<:content>
|
||||
<div class="relative overflow-x-auto">
|
||||
<.table id="gateways" rows={@group.gateways}>
|
||||
@@ -95,39 +130,13 @@ defmodule Web.GatewayGroups.Show do
|
||||
</:content>
|
||||
</.section>
|
||||
|
||||
<.section>
|
||||
<:title>
|
||||
Linked Resources
|
||||
</:title>
|
||||
<:content>
|
||||
<div class="relative overflow-x-auto">
|
||||
<.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 %>
|
||||
</.link>
|
||||
</:col>
|
||||
<:col :let={resource} label="ADDRESS">
|
||||
<%= resource.address %>
|
||||
</:col>
|
||||
<:empty>
|
||||
<div class="text-center text-slate-500 p-4">No resources to display</div>
|
||||
</:empty>
|
||||
</.table>
|
||||
</div>
|
||||
</:content>
|
||||
</.section>
|
||||
|
||||
<.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
|
||||
</.delete_button>
|
||||
</:action>
|
||||
<:content></: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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
240
elixir/apps/web/test/web/live/sites/resources/edit_test.exs
Normal file
240
elixir/apps/web/test/web/live/sites/resources/edit_test.exs
Normal file
@@ -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
|
||||
207
elixir/apps/web/test/web/live/sites/resources/new_test.exs
Normal file
207
elixir/apps/web/test/web/live/sites/resources/new_test.exs
Normal file
@@ -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
|
||||
194
elixir/apps/web/test/web/live/sites/resources/show_test.exs
Normal file
194
elixir/apps/web/test/web/live/sites/resources/show_test.exs
Normal file
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user