Introduce Sites (#2516)

Closes #2513
This commit is contained in:
Andrew Dryga
2023-10-27 13:10:36 -06:00
committed by GitHub
parent 559b3bd591
commit 98383e8622
43 changed files with 1714 additions and 586 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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}:")

View File

@@ -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", %{

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 />
&nbsp; --name=firezone-gateway-0 \<br />
&nbsp; --restart=always \<br />
&nbsp; -v /dev/net/tun:/dev/net/tun \<br />
&nbsp; -e FZ_SECRET=<%= Gateways.encode_token!(hd(@group.tokens)) %> \<br />
&nbsp; 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

View File

@@ -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}

View File

@@ -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
/>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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 %>

View File

@@ -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} ->

View 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

View 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 />
&nbsp; --restart=unless-stopped \<br />
&nbsp; --pull=always \<br />
&nbsp; --health-cmd="ip link | grep tun-firezone" \<br />
&nbsp; --name=firezone-gateway \<br />
&nbsp; --cap-add=NET_ADMIN \<br />
&nbsp; --sysctl net.ipv4.ip_forward=1 \<br />
&nbsp; --sysctl net.ipv4.conf.all.src_valid_mark=1 \<br />
&nbsp; --sysctl net.ipv6.conf.all.disable_ipv6=0 \<br />
&nbsp; --sysctl net.ipv6.conf.all.forwarding=1 \<br />
&nbsp; --sysctl net.ipv6.conf.default.forwarding=1 \<br />
&nbsp; --device="/dev/net/tun:/dev/net/tun" \<br />
&nbsp; --env FIREZONE_ID="<%= Ecto.UUID.generate() %>" \<br />
&nbsp; --env FIREZONE_TOKEN="<%= Gateways.encode_token!(hd(@group.tokens)) %>" \<br />
&nbsp; --env FIREZONE_ENABLE_MASQUERADE=1 \<br />
&nbsp; --env FIREZONE_HOSTNAME="`hostname`" \<br />
&nbsp; --env RUST_LOG="warn" \<br />
&nbsp; 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

View 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 />
&nbsp; --restart=unless-stopped \<br />
&nbsp; --pull=always \<br />
&nbsp; --health-cmd="ip link | grep tun-firezone" \<br />
&nbsp; --name=firezone-gateway \<br />
&nbsp; --cap-add=NET_ADMIN \<br />
&nbsp; --sysctl net.ipv4.ip_forward=1 \<br />
&nbsp; --sysctl net.ipv4.conf.all.src_valid_mark=1 \<br />
&nbsp; --sysctl net.ipv6.conf.all.disable_ipv6=0 \<br />
&nbsp; --sysctl net.ipv6.conf.all.forwarding=1 \<br />
&nbsp; --sysctl net.ipv6.conf.default.forwarding=1 \<br />
&nbsp; --device="/dev/net/tun:/dev/net/tun" \<br />
&nbsp; --env FIREZONE_ID="<%= Ecto.UUID.generate() %>" \<br />
&nbsp; --env FIREZONE_TOKEN="<%= Gateways.encode_token!(hd(@group.tokens)) %>" \<br />
&nbsp; --env FIREZONE_ENABLE_MASQUERADE=1 \<br />
&nbsp; --env FIREZONE_HOSTNAME="`hostname`" \<br />
&nbsp; --env RUST_LOG="warn" \<br />
&nbsp; 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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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("&quot;")
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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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
})
}
]
}