mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
feat(portal): Internet Sites (#6905)
Related #6834 Co-authored-by: Jamil Bou Kheir <jamilbk@users.noreply.github.com>
This commit is contained in:
@@ -100,13 +100,19 @@ defmodule Domain.Auth.Adapters.Okta.APIClient do
|
||||
end
|
||||
end
|
||||
|
||||
if Mix.env() == :test do
|
||||
def throttle, do: :ok
|
||||
else
|
||||
def throttle, do: :timer.sleep(:timer.seconds(1))
|
||||
end
|
||||
|
||||
# TODO: Need to catch 401/403 specifically when error message is in header
|
||||
defp list(uri, headers, api_token) do
|
||||
headers = headers ++ [{"Authorization", "Bearer #{api_token}"}]
|
||||
request = Finch.build(:get, uri, headers)
|
||||
|
||||
# Crude request throttle, revisit for https://github.com/firezone/firezone/issues/6793
|
||||
:timer.sleep(:timer.seconds(1))
|
||||
throttle()
|
||||
|
||||
with {:ok, %Finch.Response{headers: headers, body: response, status: status}}
|
||||
when status in 200..299 <- Finch.request(request, @pool_name),
|
||||
|
||||
@@ -296,10 +296,13 @@ defmodule Domain.Billing.EventHandler do
|
||||
provider_identifier_confirmation: metadata["account_admin_email"] || account_email
|
||||
})
|
||||
|
||||
{:ok, _resource} = Domain.Resources.create_internet_resource(account)
|
||||
|
||||
{:ok, _gateway_group} = Domain.Gateways.create_group(account, %{name: "Default Site"})
|
||||
|
||||
{:ok, internet_gateway_group} = Domain.Gateways.create_internet_group(account)
|
||||
|
||||
{:ok, _resource} =
|
||||
Domain.Resources.create_internet_resource(account, internet_gateway_group)
|
||||
|
||||
:ok
|
||||
else
|
||||
{:error, %Ecto.Changeset{errors: [{:slug, {"has already been taken", _}} | _]}} ->
|
||||
|
||||
@@ -36,6 +36,14 @@ defmodule Domain.Gateways do
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_internet_group(%Accounts.Account{} = account) do
|
||||
Group.Query.not_deleted()
|
||||
|> Group.Query.by_managed_by(:system)
|
||||
|> Group.Query.by_account_id(account.id)
|
||||
|> Group.Query.by_name("Internet")
|
||||
|> Repo.fetch(Group.Query, [])
|
||||
end
|
||||
|
||||
def list_groups(%Auth.Subject{} = subject, opts \\ []) do
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_gateways_permission()) do
|
||||
Group.Query.not_deleted()
|
||||
@@ -46,12 +54,14 @@ defmodule Domain.Gateways do
|
||||
|
||||
def all_groups!(%Auth.Subject{} = subject) do
|
||||
Group.Query.not_deleted()
|
||||
|> Group.Query.by_managed_by(:account)
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def all_groups_for_account!(%Accounts.Account{} = account) do
|
||||
Group.Query.not_deleted()
|
||||
|> Group.Query.by_managed_by(:account)
|
||||
|> Group.Query.by_account_id(account.id)
|
||||
|> Repo.all()
|
||||
end
|
||||
@@ -78,13 +88,24 @@ defmodule Domain.Gateways do
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def create_internet_group(%Accounts.Account{} = account) do
|
||||
attrs = %{
|
||||
"name" => "Internet",
|
||||
"managed_by" => "system"
|
||||
}
|
||||
|
||||
account
|
||||
|> Group.Changeset.create(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def change_group(%Group{} = group, attrs \\ %{}) do
|
||||
group
|
||||
|> Repo.preload(:account)
|
||||
|> Group.Changeset.update(attrs)
|
||||
end
|
||||
|
||||
def update_group(%Group{} = group, attrs, %Auth.Subject{} = subject) do
|
||||
def update_group(%Group{managed_by: :account} = group, attrs, %Auth.Subject{} = subject) do
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_gateways_permission()) do
|
||||
Group.Query.not_deleted()
|
||||
|> Group.Query.by_id(group.id)
|
||||
@@ -108,7 +129,7 @@ defmodule Domain.Gateways do
|
||||
end
|
||||
end
|
||||
|
||||
def delete_group(%Group{} = group, %Auth.Subject{} = subject) do
|
||||
def delete_group(%Group{managed_by: :account} = group, %Auth.Subject{} = subject) do
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_gateways_permission()) do
|
||||
Group.Query.not_deleted()
|
||||
|> Group.Query.by_id(group.id)
|
||||
|
||||
@@ -4,6 +4,8 @@ defmodule Domain.Gateways.Group do
|
||||
schema "gateway_groups" do
|
||||
field :name, :string
|
||||
|
||||
field :managed_by, Ecto.Enum, values: ~w[account system]a
|
||||
|
||||
belongs_to :account, Domain.Accounts.Account
|
||||
has_many :gateways, Domain.Gateways.Gateway, foreign_key: :group_id, where: [deleted_at: nil]
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ defmodule Domain.Gateways.Group.Changeset do
|
||||
def create(%Accounts.Account{} = account, attrs, %Auth.Subject{} = subject) do
|
||||
%Gateways.Group{account: account}
|
||||
|> changeset(attrs)
|
||||
|> put_default_value(:managed_by, :account)
|
||||
|> put_change(:account_id, account.id)
|
||||
|> put_subject_trail(:created_by, subject)
|
||||
end
|
||||
@@ -15,6 +16,8 @@ defmodule Domain.Gateways.Group.Changeset do
|
||||
def create(%Accounts.Account{} = account, attrs) do
|
||||
%Gateways.Group{account: account}
|
||||
|> changeset(attrs)
|
||||
|> cast(attrs, ~w[managed_by]a)
|
||||
|> put_default_value(:managed_by, :account)
|
||||
|> put_change(:account_id, account.id)
|
||||
|> put_subject_trail(:created_by, :system)
|
||||
end
|
||||
|
||||
@@ -18,6 +18,14 @@ defmodule Domain.Gateways.Group.Query do
|
||||
where(queryable, [groups: groups], groups.account_id == ^account_id)
|
||||
end
|
||||
|
||||
def by_managed_by(queryable, managed_by) do
|
||||
where(queryable, [groups: groups], groups.managed_by == ^managed_by)
|
||||
end
|
||||
|
||||
def by_name(queryable, name) do
|
||||
where(queryable, [groups: groups], groups.name == ^name)
|
||||
end
|
||||
|
||||
# Pagination
|
||||
|
||||
@impl Domain.Repo.Query
|
||||
@@ -40,10 +48,19 @@ defmodule Domain.Gateways.Group.Query do
|
||||
name: :deleted?,
|
||||
type: :boolean,
|
||||
fun: &filter_deleted/1
|
||||
},
|
||||
%Domain.Repo.Filter{
|
||||
name: :managed_by,
|
||||
type: :string,
|
||||
fun: &filter_managed_by/2
|
||||
}
|
||||
]
|
||||
|
||||
def filter_deleted(queryable) do
|
||||
{queryable, dynamic([groups: groups], not is_nil(groups.deleted_at))}
|
||||
end
|
||||
|
||||
def filter_managed_by(queryable, managed_by) do
|
||||
{queryable, dynamic([groups: groups], groups.managed_by == ^managed_by)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -227,6 +227,10 @@ defmodule Domain.Repo.Filter do
|
||||
not (is_nil(from) and is_nil(to))
|
||||
end
|
||||
|
||||
defp value_type_valid?({:list, type}, {:not_in, values}) when is_list(values) do
|
||||
Enum.all?(values, &value_type_valid?(type, &1))
|
||||
end
|
||||
|
||||
defp value_type_valid?({:list, type}, values) when is_list(values) do
|
||||
Enum.all?(values, &value_type_valid?(type, &1))
|
||||
end
|
||||
|
||||
@@ -23,6 +23,16 @@ defmodule Domain.Resources do
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_internet_resource(%Auth.Subject{} = subject, opts \\ []) do
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_resources_permission()) do
|
||||
Resource.Query.all()
|
||||
|> Resource.Query.by_account_id(subject.account.id)
|
||||
|> Resource.Query.by_type(:internet)
|
||||
|> Authorizer.for_subject(Resource, subject)
|
||||
|> Repo.fetch(Resource.Query, opts)
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_resource_by_id_or_persistent_id(id, %Auth.Subject{} = subject, opts \\ []) do
|
||||
required_permissions =
|
||||
{:one_of,
|
||||
@@ -221,8 +231,18 @@ defmodule Domain.Resources do
|
||||
end
|
||||
end
|
||||
|
||||
def create_internet_resource(%Accounts.Account{} = account) do
|
||||
attrs = %{type: :internet, name: "Internet"}
|
||||
def create_internet_resource(%Accounts.Account{} = account, %Gateways.Group{} = group) do
|
||||
attrs = %{
|
||||
type: :internet,
|
||||
name: "Internet",
|
||||
connections: %{
|
||||
group.id => %{
|
||||
gateway_group_id: group.id,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changeset = Resource.Changeset.create(account, attrs)
|
||||
|
||||
with {:ok, resource} <- Repo.insert(changeset) do
|
||||
@@ -316,6 +336,11 @@ defmodule Domain.Resources do
|
||||
|> delete_connections(subject)
|
||||
end
|
||||
|
||||
def delete_connections_for(%Resource{} = resource, %Auth.Subject{} = subject) do
|
||||
Connection.Query.by_resource_id(resource.id)
|
||||
|> delete_connections(subject)
|
||||
end
|
||||
|
||||
defp delete_connections(queryable, subject) do
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_resources_permission()) do
|
||||
{count, nil} =
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule Domain.Resources.Connection do
|
||||
belongs_to :resource, Domain.Resources.Resource, primary_key: true
|
||||
belongs_to :gateway_group, Domain.Gateways.Group, primary_key: true
|
||||
|
||||
field :created_by, Ecto.Enum, values: ~w[actor identity]a
|
||||
field :created_by, Ecto.Enum, values: ~w[actor identity system]a
|
||||
belongs_to :created_by_identity, Domain.Auth.Identity
|
||||
belongs_to :created_by_actor, Domain.Actors.Actor
|
||||
|
||||
|
||||
@@ -6,11 +6,16 @@ defmodule Domain.Resources.Connection.Changeset do
|
||||
@required_fields @fields
|
||||
|
||||
def changeset(account_id, connection, attrs, %Auth.Subject{} = subject) do
|
||||
changeset(account_id, connection, attrs)
|
||||
base_changeset(account_id, connection, attrs)
|
||||
|> put_subject_trail(:created_by, subject)
|
||||
end
|
||||
|
||||
def changeset(account_id, connection, attrs) do
|
||||
base_changeset(account_id, connection, attrs)
|
||||
|> put_change(:created_by, :system)
|
||||
end
|
||||
|
||||
defp base_changeset(account_id, connection, attrs) do
|
||||
connection
|
||||
|> cast(attrs, @fields)
|
||||
|> validate_required(@required_fields)
|
||||
|
||||
@@ -26,6 +26,10 @@ defmodule Domain.Resources.Resource.Query do
|
||||
where(queryable, [resources: resources], resources.id == ^id)
|
||||
end
|
||||
|
||||
def by_type(queryable, type) do
|
||||
where(queryable, [resources: resources], resources.type == ^type)
|
||||
end
|
||||
|
||||
def by_id_or_persistent_id(queryable, id) do
|
||||
where(queryable, [resources: resources], resources.id == ^id)
|
||||
|> or_where(
|
||||
@@ -166,6 +170,11 @@ defmodule Domain.Resources.Resource.Query do
|
||||
name: :deleted?,
|
||||
type: :boolean,
|
||||
fun: &filter_deleted/1
|
||||
},
|
||||
%Domain.Repo.Filter{
|
||||
name: :type,
|
||||
type: {:list, :string},
|
||||
fun: &filter_by_type/2
|
||||
}
|
||||
]
|
||||
|
||||
@@ -186,4 +195,12 @@ defmodule Domain.Resources.Resource.Query do
|
||||
def filter_deleted(queryable) do
|
||||
{queryable, dynamic([resources: resources], not is_nil(resources.deleted_at))}
|
||||
end
|
||||
|
||||
def filter_by_type(queryable, {:not_in, types}) do
|
||||
{queryable, dynamic([resources: resources], resources.type not in ^types)}
|
||||
end
|
||||
|
||||
def filter_by_type(queryable, types) do
|
||||
{queryable, dynamic([resources: resources], resources.type in ^types)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,12 +19,5 @@ defmodule Domain.Repo.Migrations.AddInternetResources do
|
||||
name: "unique_internet_resource_per_account"
|
||||
)
|
||||
)
|
||||
|
||||
# Manual migration that needs to be run after deployment
|
||||
# (Domain.Accounts.Account.Query.not_deleted()
|
||||
# |> Domain.Repo.all()
|
||||
# |> Enum.each(fn account ->
|
||||
# Domain.Resources.create_internet_resource(account)
|
||||
# end))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
defmodule Domain.Repo.Migrations.AddGatewayGroupsManagedBy do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:gateway_groups) do
|
||||
add(:managed_by, :string, null: false, default: "account")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
defmodule Domain.Repo.Migrations.CreateUuidExtension do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
execute("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"")
|
||||
end
|
||||
|
||||
def down do
|
||||
execute("DROP EXTENSION IF EXISTS \"uuid-ossp\"")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
defmodule Domain.Repo.Migrations.CreateInternetSite do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
execute("""
|
||||
INSERT INTO gateway_groups (
|
||||
id,
|
||||
account_id,
|
||||
name,
|
||||
created_by,
|
||||
managed_by,
|
||||
inserted_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
uuid_generate_v4(),
|
||||
id,
|
||||
'Internet',
|
||||
'system',
|
||||
'system',
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM accounts
|
||||
WHERE deleted_at IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM gateway_groups
|
||||
WHERE gateway_groups.account_id = accounts.id
|
||||
AND gateway_groups.name = 'Internet'
|
||||
AND gateway_groups.created_by = 'system'
|
||||
AND gateway_groups.managed_by = 'system')
|
||||
""")
|
||||
end
|
||||
|
||||
def down do
|
||||
execute("""
|
||||
DELETE FROM gateway_groups
|
||||
WHERE name = 'Internet'
|
||||
AND created_by = 'system'
|
||||
AND managed_by = 'system'
|
||||
AND account_id IN (SELECT id FROM accounts WHERE deleted_at IS NULL);
|
||||
""")
|
||||
end
|
||||
end
|
||||
@@ -73,9 +73,16 @@ end
|
||||
|
||||
IO.puts("")
|
||||
|
||||
for account <- [account, other_account] do
|
||||
Domain.Resources.create_internet_resource(account)
|
||||
end
|
||||
{:ok, internet_gateway_group} =
|
||||
Gateways.create_internet_group(account)
|
||||
|
||||
{:ok, other_internet_gateway_group} =
|
||||
Gateways.create_internet_group(other_account)
|
||||
|
||||
Domain.Resources.create_internet_resource(account, internet_gateway_group)
|
||||
Domain.Resources.create_internet_resource(other_account, other_internet_gateway_group)
|
||||
|
||||
IO.puts("")
|
||||
|
||||
{:ok, everyone_group} =
|
||||
Domain.Actors.create_managed_group(account, %{
|
||||
|
||||
@@ -823,7 +823,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.SyncDirectoryTest do
|
||||
cancel_bypass_expectations_check(bypass)
|
||||
end
|
||||
|
||||
test "sends email on failed directory sync", %{account: account} do
|
||||
test "sends email on failed directory sync", %{account: account, provider: provider} do
|
||||
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
|
||||
_identity = Fixtures.Auth.create_identity(account: account, actor: actor)
|
||||
|
||||
@@ -882,10 +882,12 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.SyncDirectoryTest do
|
||||
end)
|
||||
end
|
||||
|
||||
for _n <- 1..10 do
|
||||
{:ok, pid} = Task.Supervisor.start_link()
|
||||
assert execute(%{task_supervisor: pid}) == :ok
|
||||
end
|
||||
provider
|
||||
|> Ecto.Changeset.change(last_syncs_failed: 9)
|
||||
|> Repo.update!()
|
||||
|
||||
{:ok, pid} = Task.Supervisor.start_link()
|
||||
assert execute(%{task_supervisor: pid}) == :ok
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
assert email.subject == "Firezone Identity Provider Sync Error"
|
||||
|
||||
@@ -562,7 +562,7 @@ defmodule Domain.Auth.Adapters.JumpCloud.Jobs.SyncDirectoryTest do
|
||||
cancel_bypass_expectations_check(bypass)
|
||||
end
|
||||
|
||||
test "sends email on failed directory sync", %{account: account} do
|
||||
test "sends email on failed directory sync", %{account: account, provider: provider} do
|
||||
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
|
||||
_identity = Fixtures.Auth.create_identity(account: account, actor: actor)
|
||||
|
||||
@@ -582,10 +582,12 @@ defmodule Domain.Auth.Adapters.JumpCloud.Jobs.SyncDirectoryTest do
|
||||
end)
|
||||
end
|
||||
|
||||
for _n <- 1..10 do
|
||||
{:ok, pid} = Task.Supervisor.start_link()
|
||||
assert execute(%{task_supervisor: pid}) == :ok
|
||||
end
|
||||
provider
|
||||
|> Ecto.Changeset.change(last_syncs_failed: 9)
|
||||
|> Repo.update!()
|
||||
|
||||
{:ok, pid} = Task.Supervisor.start_link()
|
||||
assert execute(%{task_supervisor: pid}) == :ok
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
assert email.subject == "Firezone Identity Provider Sync Error"
|
||||
|
||||
@@ -494,7 +494,7 @@ defmodule Domain.Auth.Adapters.MicrosoftEntra.Jobs.SyncDirectoryTest do
|
||||
cancel_bypass_expectations_check(bypass)
|
||||
end
|
||||
|
||||
test "sends email on failed directory sync", %{account: account} do
|
||||
test "sends email on failed directory sync", %{account: account, provider: provider} do
|
||||
bypass = Bypass.open()
|
||||
MicrosoftEntraDirectory.override_endpoint_url("http://localhost:#{bypass.port}/")
|
||||
|
||||
@@ -510,10 +510,12 @@ defmodule Domain.Auth.Adapters.MicrosoftEntra.Jobs.SyncDirectoryTest do
|
||||
end)
|
||||
end
|
||||
|
||||
for _n <- 1..10 do
|
||||
{:ok, pid} = Task.Supervisor.start_link()
|
||||
assert execute(%{task_supervisor: pid}) == :ok
|
||||
end
|
||||
provider
|
||||
|> Ecto.Changeset.change(last_syncs_failed: 9)
|
||||
|> Repo.update!()
|
||||
|
||||
{:ok, pid} = Task.Supervisor.start_link()
|
||||
assert execute(%{task_supervisor: pid}) == :ok
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
assert email.subject == "Firezone Identity Provider Sync Error"
|
||||
|
||||
@@ -790,6 +790,7 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do
|
||||
|
||||
test "sends email on failed directory sync", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
bypass: bypass
|
||||
} do
|
||||
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
|
||||
@@ -812,10 +813,13 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do
|
||||
end)
|
||||
end
|
||||
|
||||
for _n <- 1..10 do
|
||||
{:ok, pid} = Task.Supervisor.start_link()
|
||||
assert execute(%{task_supervisor: pid}) == :ok
|
||||
end
|
||||
{:ok, pid} = Task.Supervisor.start_link()
|
||||
|
||||
provider
|
||||
|> Ecto.Changeset.change(last_syncs_failed: 9)
|
||||
|> Repo.update!()
|
||||
|
||||
assert execute(%{task_supervisor: pid}) == :ok
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
assert email.subject == "Firezone Identity Provider Sync Error"
|
||||
|
||||
@@ -230,6 +230,14 @@ defmodule Domain.GatewaysTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "create_internet_group/1" do
|
||||
test "creates a group on empty attrs", %{account: account} do
|
||||
assert {:ok, group} = create_internet_group(account)
|
||||
assert group.name == "Internet"
|
||||
assert group.managed_by == :system
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_group/1" do
|
||||
test "returns changeset with given changes" do
|
||||
group = Fixtures.Gateways.create_group()
|
||||
|
||||
@@ -4,7 +4,8 @@ defmodule Domain.Fixtures.Gateways do
|
||||
|
||||
def group_attrs(attrs \\ %{}) do
|
||||
Enum.into(attrs, %{
|
||||
name: "group-#{unique_integer()}"
|
||||
name: "group-#{unique_integer()}",
|
||||
managed_by: :account
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
@@ -1475,11 +1475,12 @@ defmodule Web.CoreComponents do
|
||||
"""
|
||||
|
||||
attr :color, :string, default: "info"
|
||||
attr :title, :string, default: nil
|
||||
attr :class, :string, default: nil
|
||||
|
||||
def ping_icon(assigns) do
|
||||
~H"""
|
||||
<span class={["relative flex h-2.5 w-2.5", @class]}>
|
||||
<span class={["relative flex h-2.5 w-2.5", @class]} title={@title}>
|
||||
<span class={~w[
|
||||
animate-ping absolute inline-flex
|
||||
h-full w-full rounded-full opacity-50
|
||||
|
||||
@@ -3,6 +3,7 @@ defmodule Web.PageComponents do
|
||||
use Web, :verified_routes
|
||||
import Web.CoreComponents
|
||||
|
||||
attr :id, :string, default: nil, doc: "The id of the section"
|
||||
slot :title, required: true, doc: "The title of the section to be displayed"
|
||||
slot :action, required: false, doc: "A slot for action to the right from title"
|
||||
|
||||
@@ -14,10 +15,13 @@ defmodule Web.PageComponents do
|
||||
|
||||
def section(assigns) do
|
||||
~H"""
|
||||
<div class={[
|
||||
"mb-6 bg-white overflow-hidden shadow mx-5 rounded border px-6",
|
||||
@content != [] && "pb-6"
|
||||
]}>
|
||||
<div
|
||||
id={@id}
|
||||
class={[
|
||||
"mb-6 bg-white shadow mx-5 rounded border px-6",
|
||||
@content != [] && "pb-6"
|
||||
]}
|
||||
>
|
||||
<.header>
|
||||
<:title>
|
||||
{render_slot(@title)}
|
||||
|
||||
@@ -110,17 +110,18 @@ defmodule Web.Policies.New do
|
||||
</span>
|
||||
<% else %>
|
||||
{resource.name}
|
||||
|
||||
<span
|
||||
:if={length(resource.gateway_groups) > 0}
|
||||
class="text-neutral-500 inline-flex"
|
||||
>
|
||||
(<.resource_gateway_groups gateway_groups={resource.gateway_groups} />)
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<span :if={resource.gateway_groups == []} class="text-red-800">
|
||||
(not connected to any Site)
|
||||
</span>
|
||||
<span
|
||||
:if={length(resource.gateway_groups) > 0}
|
||||
class="text-neutral-500 inline-flex"
|
||||
>
|
||||
(<.resource_gateway_groups gateway_groups={resource.gateway_groups} />)
|
||||
</span>
|
||||
</:option>
|
||||
|
||||
<:no_options :let={name}>
|
||||
|
||||
@@ -8,7 +8,8 @@ defmodule Web.Resources.Edit do
|
||||
Resources.fetch_resource_by_id(id, socket.assigns.subject,
|
||||
preload: :gateway_groups,
|
||||
filter: [
|
||||
deleted?: false
|
||||
deleted?: false,
|
||||
type: ["cidr", "dns", "ip"]
|
||||
]
|
||||
) do
|
||||
gateway_groups = Gateways.all_groups!(socket.assigns.subject)
|
||||
|
||||
@@ -7,15 +7,26 @@ defmodule Web.Resources.Index do
|
||||
:ok = Resources.subscribe_to_events_for_account(socket.assigns.account)
|
||||
end
|
||||
|
||||
internet_site =
|
||||
case Domain.Gateways.fetch_internet_group(socket.assigns.account) do
|
||||
{:ok, internet_site} -> internet_site
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(page_title: "Resources")
|
||||
|> assign(internet_site: internet_site)
|
||||
|> assign_live_table("resources",
|
||||
query_module: Resources.Resource.Query,
|
||||
sortable_fields: [
|
||||
{:resources, :name},
|
||||
{:resources, :address}
|
||||
],
|
||||
enforce_filters: [
|
||||
# The Internet Resource is shown in another section
|
||||
{:type, {:not_in, ["internet"]}}
|
||||
],
|
||||
callback: &handle_resources_update!/2
|
||||
)
|
||||
|
||||
@@ -54,8 +65,16 @@ defmodule Web.Resources.Index do
|
||||
Resources
|
||||
</:title>
|
||||
<:help>
|
||||
Resources define the subnets, hosts, and applications for which you want to manage access. You can manage Resources per Site
|
||||
in the <.link navigate={~p"/#{@account}/sites"} class={link_style()}>Sites</.link> section.
|
||||
<p class="mb-2">
|
||||
Resources define the subnets, hosts, and applications for which you want to manage access. You can manage Resources per Site
|
||||
in the <.link navigate={~p"/#{@account}/sites"} class={link_style()}>Sites</.link> section.
|
||||
</p>
|
||||
<p :if={Domain.Accounts.internet_resource_enabled?(@account) && @internet_site}>
|
||||
The Internet Resource can now be managed in the
|
||||
<.link navigate={~p"/#{@account}/sites/#{@internet_site}"} class={link_style()}>
|
||||
Internet Site.
|
||||
</.link>
|
||||
</p>
|
||||
</:help>
|
||||
<:action>
|
||||
<.docs_action path="/deploy/resources" />
|
||||
|
||||
@@ -114,7 +114,7 @@ defmodule Web.Resources.Show do
|
||||
(replaced)
|
||||
</span>
|
||||
</:title>
|
||||
<:action :if={is_nil(@resource.deleted_at)}>
|
||||
<:action :if={@resource.type != :internet && is_nil(@resource.deleted_at)}>
|
||||
<.edit_button navigate={~p"/#{@account}/resources/#{@resource.id}/edit?#{@params}"}>
|
||||
Edit Resource
|
||||
</.edit_button>
|
||||
|
||||
@@ -423,18 +423,24 @@ defmodule Web.SignUp do
|
||||
})
|
||||
end
|
||||
)
|
||||
|> Ecto.Multi.run(
|
||||
:internet_resource,
|
||||
fn _repo, %{account: account} ->
|
||||
Domain.Resources.create_internet_resource(account)
|
||||
end
|
||||
)
|
||||
|> Ecto.Multi.run(
|
||||
:default_site,
|
||||
fn _repo, %{account: account} ->
|
||||
Domain.Gateways.create_group(account, %{name: "Default Site"})
|
||||
end
|
||||
)
|
||||
|> Ecto.Multi.run(
|
||||
:internet_site,
|
||||
fn _repo, %{account: account} ->
|
||||
Domain.Gateways.create_internet_group(account)
|
||||
end
|
||||
)
|
||||
|> Ecto.Multi.run(
|
||||
:internet_resource,
|
||||
fn _repo, %{account: account, internet_site: internet_site} ->
|
||||
Domain.Resources.create_internet_resource(account, internet_site)
|
||||
end
|
||||
)
|
||||
|> Ecto.Multi.run(
|
||||
:send_email,
|
||||
fn _repo, %{account: account, identity: identity} ->
|
||||
|
||||
@@ -65,7 +65,11 @@ defmodule Web.Sites.Gateways.Index do
|
||||
<:title>
|
||||
Site <code>{@group.name}</code> Gateways
|
||||
</:title>
|
||||
<:help>
|
||||
<:help :if={@group.managed_by == :system and @group.name == "Internet"}>
|
||||
Gateways deployed to the Internet Site will be used for full-route tunneling
|
||||
of traffic that doesn't match a more specific Resource.
|
||||
</:help>
|
||||
<:help :if={is_nil(@group.deleted_at) and @group.managed_by == :account}>
|
||||
Deploy gateways to terminate connections to your site's resources. All
|
||||
gateways deployed within a site must be able to reach all
|
||||
its resources.
|
||||
@@ -99,9 +103,16 @@ defmodule Web.Sites.Gateways.Index do
|
||||
<div class="flex flex-col items-center justify-center text-center text-neutral-500 p-4">
|
||||
<div class="pb-4">
|
||||
No gateways to display.
|
||||
<.link class={[link_style()]} navigate={~p"/#{@account}/sites/#{@group}/new_token"}>
|
||||
Deploy a gateway to connect resources.
|
||||
</.link>
|
||||
<span :if={@group.managed_by == :system and @group.name == "Internet"}>
|
||||
<.link class={[link_style()]} navigate={~p"/#{@account}/sites/#{@group}/new_token"}>
|
||||
Deploy a Gateway to the Internet Site.
|
||||
</.link>
|
||||
</span>
|
||||
<span :if={is_nil(@group.deleted_at) and @group.managed_by == :account}>
|
||||
<.link class={[link_style()]} navigate={~p"/#{@account}/sites/#{@group}/new_token"}>
|
||||
Deploy a Gateway to connect Resources.
|
||||
</.link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</:empty>
|
||||
|
||||
@@ -1,20 +1,53 @@
|
||||
defmodule Web.Sites.Index do
|
||||
use Web, :live_view
|
||||
alias Domain.Gateways
|
||||
require Logger
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
:ok = Gateways.subscribe_to_gateways_presence_in_account(socket.assigns.account)
|
||||
end
|
||||
|
||||
{:ok, managed_groups, _metadata} =
|
||||
Gateways.list_groups(socket.assigns.subject,
|
||||
preload: [
|
||||
gateways: [:online?]
|
||||
],
|
||||
filter: [
|
||||
managed_by: "system"
|
||||
]
|
||||
)
|
||||
|
||||
{internet_resource, existing_group_name} =
|
||||
with {:ok, internet_resource} <-
|
||||
Domain.Resources.fetch_internet_resource(socket.assigns.subject,
|
||||
preload: [connections: :gateway_group]
|
||||
),
|
||||
connection when not is_nil(connection) <-
|
||||
Enum.find(internet_resource.connections, fn connection ->
|
||||
connection.gateway_group.name != "Internet" && connection.managed_by != "system"
|
||||
end) do
|
||||
{internet_resource, connection.gateway_group.name}
|
||||
else
|
||||
_ -> {nil, nil}
|
||||
end
|
||||
|
||||
internet_gateway_group = Enum.find(managed_groups, fn group -> group.name == "Internet" end)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(page_title: "Sites")
|
||||
|> assign(internet_resource: internet_resource)
|
||||
|> assign(existing_internet_resource_group_name: existing_group_name)
|
||||
|> assign(internet_gateway_group: internet_gateway_group)
|
||||
|> assign_live_table("groups",
|
||||
query_module: Gateways.Group.Query,
|
||||
sortable_fields: [
|
||||
{:groups, :name}
|
||||
],
|
||||
enforce_filters: [
|
||||
{:managed_by, "account"}
|
||||
],
|
||||
callback: &handle_groups_update!/2
|
||||
)
|
||||
|
||||
@@ -170,6 +203,97 @@ defmodule Web.Sites.Index do
|
||||
</.live_table>
|
||||
</:content>
|
||||
</.section>
|
||||
|
||||
<.section :if={@internet_gateway_group} id="internet-site-banner">
|
||||
<:title>
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<span>Internet</span>
|
||||
|
||||
<% online? = Enum.any?(@internet_gateway_group.gateways, & &1.online?) %>
|
||||
|
||||
<.ping_icon
|
||||
:if={Domain.Accounts.internet_resource_enabled?(@account)}
|
||||
color={if online?, do: "success", else: "danger"}
|
||||
title={if online?, do: "Online", else: "Offline"}
|
||||
/>
|
||||
|
||||
<.link
|
||||
:if={not Domain.Accounts.internet_resource_enabled?(@account)}
|
||||
navigate={~p"/#{@account}/settings/billing"}
|
||||
class="text-sm text-primary-500"
|
||||
>
|
||||
<.badge type="primary" title="Feature available on a higher pricing plan">
|
||||
<.icon name="hero-lock-closed" class="w-3.5 h-3.5 mr-1" /> UPGRADE TO UNLOCK
|
||||
</.badge>
|
||||
</.link>
|
||||
</div>
|
||||
</:title>
|
||||
|
||||
<:action>
|
||||
<.docs_action path="/deploy/resources" fragment="the-internet-resource" />
|
||||
</:action>
|
||||
|
||||
<:action :if={Domain.Accounts.internet_resource_enabled?(@account)}>
|
||||
<.edit_button navigate={~p"/#{@account}/sites/#{@internet_gateway_group}"}>
|
||||
Manage Internet Site
|
||||
</.edit_button>
|
||||
</:action>
|
||||
|
||||
<:help>
|
||||
Use the Internet Site to manage secure, private access to the public internet for your workforce.
|
||||
</:help>
|
||||
|
||||
<:content :if={
|
||||
Domain.Accounts.internet_resource_enabled?(@account) &&
|
||||
needs_internet_resource_migration?(@internet_resource, @internet_gateway_group)
|
||||
}>
|
||||
<div class="px-1 text-neutral-500">
|
||||
<p class="mb-2">
|
||||
ACTION REQUIRED: Please migrate your existing Internet Resource to this Site before <strong>March 15, 2025</strong>.
|
||||
</p>
|
||||
<p class="mb-8 text-sm">
|
||||
<.website_link path="/blog/internet-resource-migration">
|
||||
Read more about why this is necessary.
|
||||
</.website_link>
|
||||
</p>
|
||||
<.button_with_confirmation
|
||||
id="migrate_internet_resource"
|
||||
style="warning"
|
||||
confirm_style="warning"
|
||||
icon="hero-exclamation-triangle-solid"
|
||||
on_confirm="migrate_internet_resource"
|
||||
>
|
||||
<:dialog_title>
|
||||
Confirm Internet Resource Migration from {@existing_internet_resource_group_name}
|
||||
</:dialog_title>
|
||||
<:dialog_content>
|
||||
<p class="text-center my-8">
|
||||
<.icon name="hero-exclamation-triangle-solid" class="w-16 h-16 text-primary-500" />
|
||||
</p>
|
||||
<p class="mb-2">
|
||||
Migrating the Internet Resource will permanently
|
||||
move it from the <strong>{@existing_internet_resource_group_name}</strong>
|
||||
Site to the <strong>Internet</strong>
|
||||
Site. This cannot be reversed.
|
||||
</p>
|
||||
<p class="mb-2">
|
||||
Any Clients connected to this Resource will be immediately disconnected.
|
||||
</p>
|
||||
<p>
|
||||
To minimize downtime, it is recommended to deploy new Gateways in the Internet Site before completing the migration of the Internet Resource.
|
||||
</p>
|
||||
</:dialog_content>
|
||||
<:dialog_confirm_button>
|
||||
Migrate Internet Resource
|
||||
</:dialog_confirm_button>
|
||||
<:dialog_cancel_button>
|
||||
Cancel
|
||||
</:dialog_cancel_button>
|
||||
Migrate Internet Resource
|
||||
</.button_with_confirmation>
|
||||
</div>
|
||||
</:content>
|
||||
</.section>
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -177,9 +301,93 @@ defmodule Web.Sites.Index do
|
||||
%Phoenix.Socket.Broadcast{topic: "presences:account_gateways:" <> _account_id},
|
||||
socket
|
||||
) do
|
||||
{:noreply, reload_live_table!(socket, "groups")}
|
||||
{:ok, managed_groups, _metadata} =
|
||||
Gateways.list_groups(socket.assigns.subject,
|
||||
preload: [
|
||||
gateways: [:online?]
|
||||
],
|
||||
filter: [
|
||||
managed_by: "system"
|
||||
]
|
||||
)
|
||||
|
||||
internet_resource =
|
||||
case Domain.Resources.fetch_internet_resource(socket.assigns.subject, preload: :connections) do
|
||||
{:ok, internet_resource} -> internet_resource
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
internet_gateway_group = Enum.find(managed_groups, fn group -> group.name == "Internet" end)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(internet_resource: internet_resource)
|
||||
|> assign(internet_gateway_group: internet_gateway_group)
|
||||
|> reload_live_table!("groups")
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event(event, params, socket) when event in ["paginate", "order_by", "filter"],
|
||||
do: handle_live_table_event(event, params, socket)
|
||||
|
||||
def handle_event("migrate_internet_resource", _, socket) do
|
||||
internet_resource = socket.assigns.internet_resource
|
||||
internet_gateway_group = socket.assigns.internet_gateway_group
|
||||
|
||||
case migrate_internet_resource(
|
||||
internet_resource,
|
||||
internet_gateway_group,
|
||||
socket.assigns.subject
|
||||
) do
|
||||
{:ok, internet_resource} ->
|
||||
socket =
|
||||
socket
|
||||
|> assign(internet_resource: internet_resource)
|
||||
|> put_flash(:info, "Internet Resource migrated successfully.")
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
_ ->
|
||||
{:noreply, socket |> put_flash(:error, "Failed to migrate Internet Resource.")}
|
||||
end
|
||||
end
|
||||
|
||||
defp needs_internet_resource_migration?(nil, _), do: false
|
||||
|
||||
defp needs_internet_resource_migration?(internet_resource, internet_gateway_group) do
|
||||
# can only be in the internet site now
|
||||
length(internet_resource.connections) > 1 ||
|
||||
Enum.all?(internet_resource.connections, fn connection ->
|
||||
connection.gateway_group_id != internet_gateway_group.id
|
||||
end)
|
||||
end
|
||||
|
||||
defp migrate_internet_resource(internet_resource, internet_gateway_group, subject) do
|
||||
attrs = %{
|
||||
connections: %{
|
||||
internet_gateway_group.id => %{
|
||||
gateway_group_id: internet_gateway_group.id,
|
||||
resource_id: internet_resource.id,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Domain.Repo.transaction(fn ->
|
||||
with {:ok, _count} <- Domain.Resources.delete_connections_for(internet_resource, subject),
|
||||
{:updated, resource} <-
|
||||
Domain.Resources.update_or_replace_resource(internet_resource, attrs, subject) do
|
||||
resource
|
||||
else
|
||||
{:error, changeset} ->
|
||||
Logger.error("Failed to migrate Internet Resource",
|
||||
reason: inspect(changeset),
|
||||
account: internet_resource.account_id
|
||||
)
|
||||
|
||||
Domain.Repo.rollback(changeset)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule Web.Sites.Show do
|
||||
use Web, :live_view
|
||||
alias Domain.{Gateways, Resources, Tokens}
|
||||
alias Domain.{Gateways, Resources, Policies, Flows, Tokens}
|
||||
|
||||
def mount(%{"id" => id}, _session, socket) do
|
||||
with {:ok, group} <-
|
||||
@@ -20,6 +20,27 @@ defmodule Web.Sites.Show do
|
||||
page_title: "Site #{group.name}",
|
||||
group: group
|
||||
)
|
||||
|
||||
mount_page(socket, group)
|
||||
else
|
||||
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
|
||||
end
|
||||
end
|
||||
|
||||
defp mount_page(socket, %{managed_by: :system, name: "Internet"} = group) do
|
||||
with {:ok, resource} <-
|
||||
Resources.fetch_internet_resource(socket.assigns.subject,
|
||||
preload: [
|
||||
:gateway_groups,
|
||||
:created_by_actor,
|
||||
created_by_identity: [:actor],
|
||||
replaced_by_resource: [],
|
||||
replaces_resource: []
|
||||
]
|
||||
) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(resource: resource)
|
||||
|> assign_live_table("gateways",
|
||||
query_module: Gateways.Gateway.Query,
|
||||
enforce_filters: [
|
||||
@@ -30,16 +51,23 @@ defmodule Web.Sites.Show do
|
||||
],
|
||||
callback: &handle_gateways_update!/2
|
||||
)
|
||||
|> assign_live_table("resources",
|
||||
query_module: Resources.Resource.Query,
|
||||
|> assign_live_table("flows",
|
||||
query_module: Flows.Flow.Query,
|
||||
sortable_fields: [],
|
||||
callback: &handle_flows_update!/2
|
||||
)
|
||||
|> assign_live_table("policies",
|
||||
query_module: Policies.Policy.Query,
|
||||
hide_filters: [
|
||||
:actor_group_id,
|
||||
:resource_name,
|
||||
:group_or_resource_name
|
||||
],
|
||||
enforce_filters: [
|
||||
{:gateway_group_id, group.id}
|
||||
{:resource_id, resource.id}
|
||||
],
|
||||
sortable_fields: [
|
||||
{:resources, :name},
|
||||
{:resources, :address}
|
||||
],
|
||||
callback: &handle_resources_update!/2
|
||||
sortable_fields: [],
|
||||
callback: &handle_policies_update!/2
|
||||
)
|
||||
|
||||
{:ok, socket}
|
||||
@@ -48,6 +76,34 @@ defmodule Web.Sites.Show do
|
||||
end
|
||||
end
|
||||
|
||||
defp mount_page(socket, group) do
|
||||
socket =
|
||||
socket
|
||||
|> assign_live_table("gateways",
|
||||
query_module: Gateways.Gateway.Query,
|
||||
enforce_filters: [
|
||||
{:gateway_group_id, group.id}
|
||||
],
|
||||
sortable_fields: [
|
||||
{:gateways, :last_seen_at}
|
||||
],
|
||||
callback: &handle_gateways_update!/2
|
||||
)
|
||||
|> assign_live_table("resources",
|
||||
query_module: Resources.Resource.Query,
|
||||
enforce_filters: [
|
||||
{:gateway_group_id, group.id}
|
||||
],
|
||||
sortable_fields: [
|
||||
{:resources, :name},
|
||||
{:resources, :address}
|
||||
],
|
||||
callback: &handle_resources_update!/2
|
||||
)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
def handle_params(params, uri, socket) do
|
||||
socket = handle_live_tables_params(socket, params, uri)
|
||||
{:noreply, socket}
|
||||
@@ -86,6 +142,36 @@ defmodule Web.Sites.Show do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_policies_update!(socket, list_opts) do
|
||||
list_opts = Keyword.put(list_opts, :preload, actor_group: [:provider], resource: [])
|
||||
|
||||
with {:ok, policies, metadata} <- Policies.list_policies(socket.assigns.subject, list_opts) do
|
||||
{:ok,
|
||||
assign(socket,
|
||||
policies: policies,
|
||||
policies_metadata: metadata
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_flows_update!(socket, list_opts) do
|
||||
list_opts =
|
||||
Keyword.put(list_opts, :preload,
|
||||
client: [:actor],
|
||||
gateway: [:group],
|
||||
policy: [:resource, :actor_group]
|
||||
)
|
||||
|
||||
with {:ok, flows, metadata} <-
|
||||
Flows.list_flows_for(socket.assigns.resource, socket.assigns.subject, list_opts) do
|
||||
{:ok,
|
||||
assign(socket,
|
||||
flows: flows,
|
||||
flows_metadata: metadata
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.breadcrumbs account={@account}>
|
||||
@@ -100,13 +186,18 @@ defmodule Web.Sites.Show do
|
||||
Site: <code>{@group.name}</code>
|
||||
<span :if={not is_nil(@group.deleted_at)} class="text-red-600">(deleted)</span>
|
||||
</:title>
|
||||
<:action :if={is_nil(@group.deleted_at)}>
|
||||
|
||||
<:action :if={is_nil(@group.deleted_at) and @group.managed_by == :account}>
|
||||
<.edit_button navigate={~p"/#{@account}/sites/#{@group}/edit"}>
|
||||
Edit Site
|
||||
</.edit_button>
|
||||
</:action>
|
||||
|
||||
<:content>
|
||||
<:help :if={@group.managed_by == :system and @group.name == "Internet"}>
|
||||
Use this Site to manage secure, private access to the public internet for your workforce.
|
||||
</:help>
|
||||
|
||||
<:content :if={@group.managed_by != :system and @group.name != "Internet"}>
|
||||
<.vertical_table id="group">
|
||||
<.vertical_table_row>
|
||||
<:label>Name</:label>
|
||||
@@ -158,7 +249,10 @@ defmodule Web.Sites.Show do
|
||||
Revoke All
|
||||
</.button_with_confirmation>
|
||||
</:action>
|
||||
<:help :if={is_nil(@group.deleted_at)}>
|
||||
<:help :if={@group.managed_by == :system and @group.name == "Internet"}>
|
||||
Gateways deployed to the Internet Site are used to tunnel all traffic that doesn't match any specific Resource.
|
||||
</:help>
|
||||
<:help :if={is_nil(@group.deleted_at) and @group.managed_by == :account}>
|
||||
Deploy gateways to terminate connections to your site's resources. All
|
||||
gateways deployed within a site must be able to reach all
|
||||
its resources.
|
||||
@@ -203,7 +297,15 @@ defmodule Web.Sites.Show do
|
||||
<div class="flex flex-col items-center justify-center text-center text-neutral-500 p-4">
|
||||
<div class="pb-4">
|
||||
No gateways to display.
|
||||
<span :if={is_nil(@group.deleted_at)}>
|
||||
<span :if={@group.managed_by == :system and @group.name == "Internet"}>
|
||||
<.link
|
||||
class={[link_style()]}
|
||||
navigate={~p"/#{@account}/sites/#{@group}/new_token"}
|
||||
>
|
||||
Deploy a Gateway to the Internet Site.
|
||||
</.link>
|
||||
</span>
|
||||
<span :if={is_nil(@group.deleted_at) and @group.managed_by == :account}>
|
||||
<.link
|
||||
class={[link_style()]}
|
||||
navigate={~p"/#{@account}/sites/#{@group}/new_token"}
|
||||
@@ -219,7 +321,7 @@ defmodule Web.Sites.Show do
|
||||
</:content>
|
||||
</.section>
|
||||
|
||||
<.section>
|
||||
<.section :if={@group.managed_by == :account}>
|
||||
<:title>
|
||||
Resources
|
||||
</:title>
|
||||
@@ -295,7 +397,68 @@ defmodule Web.Sites.Show do
|
||||
</:content>
|
||||
</.section>
|
||||
|
||||
<.danger_zone :if={is_nil(@group.deleted_at)}>
|
||||
<.section :if={@group.managed_by == :system and @group.name == "Internet"}>
|
||||
<:title>
|
||||
Policies
|
||||
</:title>
|
||||
<:action>
|
||||
<.add_button navigate={
|
||||
~p"/#{@account}/policies/new?resource_id=#{@resource}&site_id=#{@group}"
|
||||
}>
|
||||
Add Policy
|
||||
</.add_button>
|
||||
</:action>
|
||||
<:content>
|
||||
<.live_table
|
||||
id="policies"
|
||||
rows={@policies}
|
||||
row_id={&"policies-#{&1.id}"}
|
||||
filters={@filters_by_table_id["policies"]}
|
||||
filter={@filter_form_by_table_id["policies"]}
|
||||
ordered_by={@order_by_table_id["policies"]}
|
||||
metadata={@policies_metadata}
|
||||
>
|
||||
<:col :let={policy} label="id">
|
||||
<.link class={link_style()} navigate={~p"/#{@account}/policies/#{policy}"}>
|
||||
{policy.id}
|
||||
</.link>
|
||||
</:col>
|
||||
<:col :let={policy} label="group">
|
||||
<.group account={@account} group={policy.actor_group} />
|
||||
</:col>
|
||||
<:col :let={policy} label="status">
|
||||
<%= if is_nil(policy.deleted_at) do %>
|
||||
<%= if is_nil(policy.disabled_at) do %>
|
||||
Active
|
||||
<% else %>
|
||||
Disabled
|
||||
<% end %>
|
||||
<% else %>
|
||||
Deleted
|
||||
<% end %>
|
||||
</:col>
|
||||
<:empty>
|
||||
<div class="flex justify-center text-center text-neutral-500 p-4">
|
||||
<div class="pb-4 w-auto">
|
||||
<.icon
|
||||
name="hero-exclamation-triangle-solid"
|
||||
class="inline-block w-3.5 h-3.5 mr-1 text-red-500"
|
||||
/> No policies to display.
|
||||
<.link
|
||||
class={[link_style()]}
|
||||
navigate={~p"/#{@account}/policies/new?resource_id=#{@resource}&site_id=#{@group}"}
|
||||
>
|
||||
Add a policy
|
||||
</.link>
|
||||
to configure access to the internet.
|
||||
</div>
|
||||
</div>
|
||||
</:empty>
|
||||
</.live_table>
|
||||
</:content>
|
||||
</.section>
|
||||
|
||||
<.danger_zone :if={is_nil(@group.deleted_at) and @group.managed_by == :account}>
|
||||
<:action>
|
||||
<.button_with_confirmation
|
||||
id="delete_site"
|
||||
|
||||
@@ -79,9 +79,13 @@ defmodule Web.Live.SignUpTest do
|
||||
assert internet_resource.name == "Internet"
|
||||
assert internet_resource.type == :internet
|
||||
|
||||
gateway_group = Repo.one(Domain.Gateways.Group)
|
||||
assert gateway_group.account_id == account.id
|
||||
assert gateway_group.name == "Default Site"
|
||||
default_gateway_group = Repo.get_by(Domain.Gateways.Group, name: "Default Site")
|
||||
assert default_gateway_group.account_id == account.id
|
||||
assert default_gateway_group.managed_by == :account
|
||||
|
||||
internet_gateway_group = Repo.get_by(Domain.Gateways.Group, name: "Internet")
|
||||
assert internet_gateway_group.account_id == account.id
|
||||
assert internet_gateway_group.managed_by == :system
|
||||
end
|
||||
|
||||
test "rate limits welcome emails", %{conn: conn} do
|
||||
|
||||
@@ -112,4 +112,52 @@ defmodule Web.Live.Sites.IndexTest do
|
||||
assert row["online gateways"] =~ gateway.name
|
||||
end)
|
||||
end
|
||||
|
||||
test "renders internet site with an option to upgrade on free plans", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, group} = Domain.Gateways.create_internet_group(account)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites")
|
||||
|
||||
assert has_element?(lv, "#internet-site-banner")
|
||||
assert lv |> element("#internet-site-banner") |> render() =~ "UPGRADE TO UNLOCK"
|
||||
assert has_element?(lv, "#internet-site-banner a[href='/#{account.slug}/settings/billing']")
|
||||
|
||||
refute has_element?(lv, "#internet-site-banner a[href='/#{account.slug}/sites/#{group.id}']")
|
||||
end
|
||||
|
||||
test "renders internet site with a status and manage button on paid plans", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
account = Fixtures.Accounts.update_account(account, features: %{internet_resource: true})
|
||||
|
||||
{:ok, group} = Domain.Gateways.create_internet_group(account)
|
||||
gateway = Fixtures.Gateways.create_gateway(account: account, group: group)
|
||||
Domain.Config.put_env_override(:test_pid, self())
|
||||
:ok = Domain.Gateways.subscribe_to_gateways_presence_in_account(account)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites")
|
||||
|
||||
assert has_element?(lv, "#internet-site-banner")
|
||||
assert lv |> element("#internet-site-banner") |> render() =~ "Offline"
|
||||
refute has_element?(lv, "#internet-site-banner a[href='/#{account.slug}/settings/billing']")
|
||||
|
||||
assert has_element?(lv, "#internet-site-banner a[href='/#{account.slug}/sites/#{group.id}']")
|
||||
|
||||
:ok = Domain.Gateways.connect_gateway(gateway)
|
||||
assert_receive %Phoenix.Socket.Broadcast{topic: "presences:account_gateways:" <> _}
|
||||
assert_receive {:live_table_reloaded, "groups"}, 250
|
||||
assert lv |> element("#internet-site-banner") |> render() =~ "Online"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -71,260 +71,670 @@ defmodule Web.Live.Sites.ShowTest do
|
||||
assert breadcrumbs =~ group.name
|
||||
end
|
||||
|
||||
test "allows editing gateway groups", %{
|
||||
account: account,
|
||||
group: group,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
describe "for non-managed sites" do
|
||||
test "allows editing gateway groups", %{
|
||||
account: account,
|
||||
group: group,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
assert lv
|
||||
|> element("a", "Edit Site")
|
||||
|> render_click() ==
|
||||
{:error, {:live_redirect, %{to: ~p"/#{account}/sites/#{group}/edit", kind: :push}}}
|
||||
end
|
||||
assert lv
|
||||
|> element("a", "Edit Site")
|
||||
|> render_click() ==
|
||||
{:error, {:live_redirect, %{to: ~p"/#{account}/sites/#{group}/edit", kind: :push}}}
|
||||
end
|
||||
|
||||
test "renders group details", %{
|
||||
account: account,
|
||||
actor: actor,
|
||||
identity: identity,
|
||||
group: group,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
test "renders group details", %{
|
||||
account: account,
|
||||
actor: actor,
|
||||
identity: identity,
|
||||
group: group,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
table =
|
||||
lv
|
||||
|> element("#group")
|
||||
|> render()
|
||||
|> vertical_table_to_map()
|
||||
table =
|
||||
lv
|
||||
|> element("#group")
|
||||
|> render()
|
||||
|> vertical_table_to_map()
|
||||
|
||||
assert table["name"] =~ group.name
|
||||
assert table["created"] =~ actor.name
|
||||
end
|
||||
assert table["name"] =~ group.name
|
||||
assert table["created"] =~ actor.name
|
||||
end
|
||||
|
||||
test "renders group details when group created by API", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
actor = Fixtures.Actors.create_actor(type: :api_client, account: account)
|
||||
subject = Fixtures.Auth.create_subject(account: account, actor: actor)
|
||||
group = Fixtures.Gateways.create_group(account: account, subject: subject)
|
||||
test "renders group details when group created by API", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
actor = Fixtures.Actors.create_actor(type: :api_client, account: account)
|
||||
subject = Fixtures.Auth.create_subject(account: account, actor: actor)
|
||||
group = Fixtures.Gateways.create_group(account: account, subject: subject)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
table =
|
||||
lv
|
||||
|> element("#group")
|
||||
|> render()
|
||||
|> vertical_table_to_map()
|
||||
table =
|
||||
lv
|
||||
|> element("#group")
|
||||
|> render()
|
||||
|> vertical_table_to_map()
|
||||
|
||||
assert table["name"] =~ group.name
|
||||
assert table["created"] =~ actor.name
|
||||
end
|
||||
assert table["name"] =~ group.name
|
||||
assert table["created"] =~ actor.name
|
||||
end
|
||||
|
||||
test "renders online gateways table", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
group: group,
|
||||
gateway: gateway,
|
||||
conn: conn
|
||||
} do
|
||||
:ok = Domain.Gateways.connect_gateway(gateway)
|
||||
Fixtures.Gateways.create_gateway(account: account, group: group)
|
||||
test "renders online gateways table", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
group: group,
|
||||
gateway: gateway,
|
||||
conn: conn
|
||||
} do
|
||||
:ok = Domain.Gateways.connect_gateway(gateway)
|
||||
Fixtures.Gateways.create_gateway(account: account, group: group)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
rows =
|
||||
lv
|
||||
|> element("#gateways")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
rows =
|
||||
lv
|
||||
|> element("#gateways")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|
||||
assert length(rows) == 1
|
||||
assert length(rows) == 1
|
||||
|
||||
rows
|
||||
|> with_table_row("instance", gateway.name, fn row ->
|
||||
assert gateway.last_seen_remote_ip
|
||||
assert row["remote ip"] =~ to_string(gateway.last_seen_remote_ip)
|
||||
assert row["version"] =~ gateway.last_seen_version
|
||||
assert row["status"] =~ "Online"
|
||||
end)
|
||||
end
|
||||
|
||||
test "updates online gateways table", %{
|
||||
account: account,
|
||||
group: group,
|
||||
gateway: gateway,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
:ok = Domain.Gateways.subscribe_to_gateways_presence_in_group(group)
|
||||
:ok = Domain.Gateways.connect_gateway(gateway)
|
||||
assert_receive %Phoenix.Socket.Broadcast{topic: "presences:group_gateways:" <> _}
|
||||
|
||||
wait_for(fn ->
|
||||
lv
|
||||
|> element("#gateways")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
rows
|
||||
|> with_table_row("instance", gateway.name, fn row ->
|
||||
assert gateway.last_seen_remote_ip
|
||||
assert row["remote ip"] =~ to_string(gateway.last_seen_remote_ip)
|
||||
assert row["version"] =~ gateway.last_seen_version
|
||||
assert row["status"] =~ "Online"
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
test "allows revoking all tokens", %{
|
||||
account: account,
|
||||
group: group,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
assert lv
|
||||
|> element("button[type=submit]", "Revoke All")
|
||||
|> render_click() =~ "1 token(s) were revoked."
|
||||
|
||||
assert Repo.get_by(Domain.Tokens.Token, gateway_group_id: group.id).deleted_at
|
||||
end
|
||||
|
||||
test "renders resources table", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
group: group,
|
||||
conn: conn
|
||||
} do
|
||||
resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
resource_rows =
|
||||
lv
|
||||
|> element("#resources")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|
||||
Enum.each(resource_rows, fn row ->
|
||||
assert row["name"] =~ resource.name
|
||||
assert row["address"] =~ resource.address
|
||||
assert row["authorized groups"] == "None. Create a Policy to grant access."
|
||||
end)
|
||||
end
|
||||
|
||||
test "renders authorized groups peek", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
group: group,
|
||||
conn: conn
|
||||
} do
|
||||
resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|
||||
policies =
|
||||
[
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource
|
||||
),
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource
|
||||
),
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource
|
||||
)
|
||||
]
|
||||
|> Repo.preload(:actor_group)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
resource_rows =
|
||||
lv
|
||||
|> element("#resources")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|
||||
Enum.each(resource_rows, fn row ->
|
||||
for policy <- policies do
|
||||
assert row["authorized groups"] =~ policy.actor_group.name
|
||||
end
|
||||
end)
|
||||
|
||||
Fixtures.Policies.create_policy(
|
||||
test "updates online gateways table", %{
|
||||
account: account,
|
||||
resource: resource
|
||||
)
|
||||
group: group,
|
||||
gateway: gateway,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
:ok = Domain.Gateways.subscribe_to_gateways_presence_in_group(group)
|
||||
:ok = Domain.Gateways.connect_gateway(gateway)
|
||||
assert_receive %Phoenix.Socket.Broadcast{topic: "presences:group_gateways:" <> _}
|
||||
|
||||
wait_for(fn ->
|
||||
lv
|
||||
|> element("#gateways")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|> with_table_row("instance", gateway.name, fn row ->
|
||||
assert row["status"] =~ "Online"
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
test "allows revoking all tokens", %{
|
||||
account: account,
|
||||
group: group,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
assert lv
|
||||
|> element("button[type=submit]", "Revoke All")
|
||||
|> render_click() =~ "1 token(s) were revoked."
|
||||
|
||||
assert Repo.get_by(Domain.Tokens.Token, gateway_group_id: group.id).deleted_at
|
||||
end
|
||||
|
||||
test "renders resources table", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
group: group,
|
||||
conn: conn
|
||||
} do
|
||||
resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
resource_rows =
|
||||
lv
|
||||
|> element("#resources")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|
||||
Enum.each(resource_rows, fn row ->
|
||||
assert row["name"] =~ resource.name
|
||||
assert row["address"] =~ resource.address
|
||||
assert row["authorized groups"] == "None. Create a Policy to grant access."
|
||||
end)
|
||||
end
|
||||
|
||||
test "renders authorized groups peek", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
group: group,
|
||||
conn: conn
|
||||
} do
|
||||
resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|
||||
policies =
|
||||
[
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource
|
||||
),
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource
|
||||
),
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource
|
||||
)
|
||||
]
|
||||
|> Repo.preload(:actor_group)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
resource_rows =
|
||||
lv
|
||||
|> element("#resources")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|
||||
Enum.each(resource_rows, fn row ->
|
||||
for policy <- policies do
|
||||
assert row["authorized groups"] =~ policy.actor_group.name
|
||||
end
|
||||
end)
|
||||
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource
|
||||
)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
resource_rows =
|
||||
lv
|
||||
|> element("#resources")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|
||||
Enum.each(resource_rows, fn row ->
|
||||
assert row["authorized groups"] =~ "and 1 more"
|
||||
end)
|
||||
end
|
||||
|
||||
test "allows deleting gateway groups", %{
|
||||
account: account,
|
||||
group: group,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
resource_rows =
|
||||
lv
|
||||
|> element("#resources")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|> element("button[type=submit]", "Delete")
|
||||
|> render_click()
|
||||
|
||||
Enum.each(resource_rows, fn row ->
|
||||
assert row["authorized groups"] =~ "and 1 more"
|
||||
end)
|
||||
assert_redirected(lv, ~p"/#{account}/sites")
|
||||
|
||||
assert Repo.get(Domain.Gateways.Group, group.id).deleted_at
|
||||
end
|
||||
end
|
||||
|
||||
test "allows deleting gateway groups", %{
|
||||
account: account,
|
||||
group: group,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
describe "for non-internet resources" do
|
||||
test "allows editing gateway groups", %{
|
||||
account: account,
|
||||
group: group,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
lv
|
||||
|> element("button[type=submit]", "Delete")
|
||||
|> render_click()
|
||||
assert lv
|
||||
|> element("a", "Edit Site")
|
||||
|> render_click() ==
|
||||
{:error, {:live_redirect, %{to: ~p"/#{account}/sites/#{group}/edit", kind: :push}}}
|
||||
end
|
||||
|
||||
assert_redirected(lv, ~p"/#{account}/sites")
|
||||
test "renders group details", %{
|
||||
account: account,
|
||||
actor: actor,
|
||||
identity: identity,
|
||||
group: group,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
assert Repo.get(Domain.Gateways.Group, group.id).deleted_at
|
||||
table =
|
||||
lv
|
||||
|> element("#group")
|
||||
|> render()
|
||||
|> vertical_table_to_map()
|
||||
|
||||
assert table["name"] =~ group.name
|
||||
assert table["created"] =~ actor.name
|
||||
end
|
||||
|
||||
test "renders online gateways table", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
group: group,
|
||||
gateway: gateway,
|
||||
conn: conn
|
||||
} do
|
||||
:ok = Domain.Gateways.connect_gateway(gateway)
|
||||
Fixtures.Gateways.create_gateway(account: account, group: group)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
rows =
|
||||
lv
|
||||
|> element("#gateways")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|
||||
assert length(rows) == 1
|
||||
|
||||
rows
|
||||
|> with_table_row("instance", gateway.name, fn row ->
|
||||
assert gateway.last_seen_remote_ip
|
||||
assert row["remote ip"] =~ to_string(gateway.last_seen_remote_ip)
|
||||
assert row["status"] =~ "Online"
|
||||
end)
|
||||
end
|
||||
|
||||
test "updates online gateways table", %{
|
||||
account: account,
|
||||
group: group,
|
||||
gateway: gateway,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
:ok = Domain.Gateways.subscribe_to_gateways_presence_in_group(group)
|
||||
:ok = Domain.Gateways.connect_gateway(gateway)
|
||||
assert_receive %Phoenix.Socket.Broadcast{topic: "presences:group_gateways:" <> _}
|
||||
|
||||
wait_for(fn ->
|
||||
lv
|
||||
|> element("#gateways")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|> with_table_row("instance", gateway.name, fn row ->
|
||||
assert row["status"] =~ "Online"
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
test "allows revoking all tokens", %{
|
||||
account: account,
|
||||
group: group,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
assert lv
|
||||
|> element("button[type=submit]", "Revoke All")
|
||||
|> render_click() =~ "1 token(s) were revoked."
|
||||
|
||||
assert Repo.get_by(Domain.Tokens.Token, gateway_group_id: group.id).deleted_at
|
||||
end
|
||||
|
||||
test "renders resources table", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
group: group,
|
||||
conn: conn
|
||||
} do
|
||||
resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
resource_rows =
|
||||
lv
|
||||
|> element("#resources")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|
||||
Enum.each(resource_rows, fn row ->
|
||||
assert row["name"] =~ resource.name
|
||||
assert row["address"] =~ resource.address
|
||||
assert row["authorized groups"] == "None. Create a Policy to grant access."
|
||||
end)
|
||||
end
|
||||
|
||||
test "renders authorized groups peek", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
group: group,
|
||||
conn: conn
|
||||
} do
|
||||
resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|
||||
policies =
|
||||
[
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource
|
||||
),
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource
|
||||
),
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource
|
||||
)
|
||||
]
|
||||
|> Repo.preload(:actor_group)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
resource_rows =
|
||||
lv
|
||||
|> element("#resources")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|
||||
Enum.each(resource_rows, fn row ->
|
||||
for policy <- policies do
|
||||
assert row["authorized groups"] =~ policy.actor_group.name
|
||||
end
|
||||
end)
|
||||
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource
|
||||
)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
resource_rows =
|
||||
lv
|
||||
|> element("#resources")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|
||||
Enum.each(resource_rows, fn row ->
|
||||
assert row["authorized groups"] =~ "and 1 more"
|
||||
end)
|
||||
end
|
||||
|
||||
test "allows deleting gateway groups", %{
|
||||
account: account,
|
||||
group: group,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
lv
|
||||
|> element("button[type=submit]", "Delete")
|
||||
|> render_click()
|
||||
|
||||
assert_redirected(lv, ~p"/#{account}/sites")
|
||||
|
||||
assert Repo.get(Domain.Gateways.Group, group.id).deleted_at
|
||||
end
|
||||
end
|
||||
|
||||
describe "for internet sites" do
|
||||
setup %{account: account} do
|
||||
{:ok, group} = Domain.Gateways.create_internet_group(account)
|
||||
gateway = Fixtures.Gateways.create_gateway(account: account, group: group)
|
||||
gateway = Repo.preload(gateway, :group)
|
||||
|
||||
{:ok, resource} = Domain.Resources.create_internet_resource(account, group)
|
||||
|
||||
%{
|
||||
group: group,
|
||||
gateway: gateway,
|
||||
resource: resource
|
||||
}
|
||||
end
|
||||
|
||||
test "does not allow editing", %{
|
||||
account: account,
|
||||
group: group,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
refute has_element?(lv, "a", "Edit Site")
|
||||
end
|
||||
|
||||
test "renders online gateways table", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
group: group,
|
||||
gateway: gateway,
|
||||
conn: conn
|
||||
} do
|
||||
:ok = Domain.Gateways.connect_gateway(gateway)
|
||||
Fixtures.Gateways.create_gateway(account: account, group: group)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
rows =
|
||||
lv
|
||||
|> element("#gateways")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|
||||
assert length(rows) == 1
|
||||
|
||||
rows
|
||||
|> with_table_row("instance", gateway.name, fn row ->
|
||||
assert gateway.last_seen_remote_ip
|
||||
assert row["remote ip"] =~ to_string(gateway.last_seen_remote_ip)
|
||||
assert row["status"] =~ "Online"
|
||||
end)
|
||||
end
|
||||
|
||||
test "updates online gateways table", %{
|
||||
account: account,
|
||||
group: group,
|
||||
gateway: gateway,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
:ok = Domain.Gateways.subscribe_to_gateways_presence_in_group(group)
|
||||
:ok = Domain.Gateways.connect_gateway(gateway)
|
||||
assert_receive %Phoenix.Socket.Broadcast{topic: "presences:group_gateways:" <> _}
|
||||
|
||||
wait_for(fn ->
|
||||
lv
|
||||
|> element("#gateways")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|> with_table_row("instance", gateway.name, fn row ->
|
||||
assert row["status"] =~ "Online"
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
test "allows revoking all tokens", %{
|
||||
account: account,
|
||||
group: group,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
assert lv
|
||||
|> element("button[type=submit]", "Revoke All")
|
||||
|> render_click() =~ "1 token(s) were revoked."
|
||||
|
||||
assert Repo.get_by(Domain.Tokens.Token, gateway_group_id: group.id).deleted_at
|
||||
end
|
||||
|
||||
test "does not render resources table", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
group: group,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
refute has_element?(lv, "#resources")
|
||||
end
|
||||
|
||||
test "renders policies table", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
group: group,
|
||||
resource: resource,
|
||||
conn: conn
|
||||
} do
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource
|
||||
)
|
||||
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource
|
||||
)
|
||||
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource
|
||||
)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
rows =
|
||||
lv
|
||||
|> element("#policies")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|
||||
assert Enum.all?(rows, fn row ->
|
||||
assert row["group"]
|
||||
assert row["id"]
|
||||
assert row["status"] == "Active"
|
||||
end)
|
||||
end
|
||||
|
||||
test "does not allow deleting the group", %{
|
||||
account: account,
|
||||
group: group,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/sites/#{group}")
|
||||
|
||||
refute has_element?(lv, "button[type=submit]", "Delete")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user