refactor(portal): Enforce internet resource site exclusion (#8448)

Finishes up the Internet Resource migration by enforcing:

- No internet resources in non-internet sites
- No regular resources in internet sites
- Removing the prompt to migrate

~~I've already migrated the existing internet resources in customer's
accounts. No one that was using the internet resource hadn't already
migrated.~~

Edit: I started to head down that path, then decided doing this here in
a data migration was going to be a better approach.

Fixes #8212
This commit is contained in:
Jamil
2025-03-15 18:25:32 -05:00
committed by GitHub
parent 03b6e443f7
commit 43d084f97f
8 changed files with 176 additions and 134 deletions

View File

@@ -28,6 +28,14 @@ defmodule API.Client.ChannelTest do
gateway_group_token = Fixtures.Gateways.create_token(account: account, group: gateway_group)
gateway = Fixtures.Gateways.create_gateway(account: account, group: gateway_group)
internet_gateway_group = Fixtures.Gateways.create_internet_group(account: account)
internet_gateway_group_token =
Fixtures.Gateways.create_token(account: account, group: internet_gateway_group)
internet_gateway =
Fixtures.Gateways.create_gateway(account: account, group: internet_gateway_group)
dns_resource =
Fixtures.Resources.create_resource(
account: account,
@@ -51,10 +59,9 @@ defmodule API.Client.ChannelTest do
)
internet_resource =
Fixtures.Resources.create_resource(
type: :internet,
Fixtures.Resources.create_internet_resource(
account: account,
connections: [%{gateway_group_id: gateway_group.id}]
connections: [%{gateway_group_id: internet_gateway_group.id}]
)
unauthorized_resource =
@@ -142,6 +149,9 @@ defmodule API.Client.ChannelTest do
gateway_group_token: gateway_group_token,
gateway_group: gateway_group,
gateway: gateway,
internet_gateway_group: internet_gateway_group,
internet_gateway_group_token: internet_gateway_group_token,
internet_gateway: internet_gateway,
dns_resource: dns_resource,
cidr_resource: cidr_resource,
ip_resource: ip_resource,
@@ -255,6 +265,7 @@ defmodule API.Client.ChannelTest do
test "sends list of available resources after join", %{
client: client,
internet_gateway_group: internet_gateway_group,
gateway_group: gateway_group,
dns_resource: dns_resource,
cidr_resource: cidr_resource,
@@ -337,8 +348,8 @@ defmodule API.Client.ChannelTest do
type: :internet,
gateway_groups: [
%{
id: gateway_group.id,
name: gateway_group.name
id: internet_gateway_group.id,
name: internet_gateway_group.name
}
],
can_be_disabled: true
@@ -1215,10 +1226,10 @@ defmodule API.Client.ChannelTest do
test "returns online gateway connected to an internet resource", %{
account: account,
internet_gateway_group_token: gateway_group_token,
internet_gateway: gateway,
internet_resource: resource,
client: client,
gateway_group_token: gateway_group_token,
gateway: gateway,
socket: socket
} do
Fixtures.Accounts.update_account(account,
@@ -1722,8 +1733,8 @@ defmodule API.Client.ChannelTest do
test "returns gateway that support Internet resources", %{
account: account,
internet_gateway_group: internet_gateway_group,
internet_resource: resource,
subject: subject,
socket: socket
} do
account =
@@ -1742,24 +1753,15 @@ defmodule API.Client.ChannelTest do
last_seen_at: DateTime.utc_now() |> DateTime.add(-10, :second)
)
gateway_group = Fixtures.Gateways.create_group(account: account)
gateway =
Fixtures.Gateways.create_gateway(
account: account,
group: gateway_group,
group: internet_gateway_group,
context: %{
user_agent: "iOS/12.5 (iPhone) connlib/1.2.0"
}
)
{:updated, resource} =
Domain.Resources.update_resource(
resource,
%{connections: [%{gateway_group_id: gateway_group.id}]},
subject
)
:ok = Domain.Gateways.connect_gateway(gateway)
ref = push(socket, "prepare_connection", %{"resource_id" => resource.id})
@@ -1770,7 +1772,7 @@ defmodule API.Client.ChannelTest do
gateway =
Fixtures.Gateways.create_gateway(
account: account,
group: gateway_group,
group: internet_gateway_group,
context: %{
user_agent: "iOS/12.5 (iPhone) connlib/1.3.0"
}

View File

@@ -134,10 +134,12 @@ defmodule API.Gateway.ChannelTest do
relay: relay,
socket: socket
} do
internet_gateway_group = Fixtures.Gateways.create_internet_group(account: account)
resource =
Fixtures.Resources.create_resource(
type: :internet,
account: account
Fixtures.Resources.create_internet_resource(
account: account,
connections: [%{gateway_group_id: internet_gateway_group.id}]
)
channel_pid = self()

View File

@@ -23,6 +23,13 @@ defmodule Domain.Resources do
end
end
def fetch_internet_resource(%Accounts.Account{} = account) do
Resource.Query.all()
|> Resource.Query.by_account_id(account.id)
|> Resource.Query.by_type(:internet)
|> Repo.fetch(Resource.Query)
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()

View File

@@ -0,0 +1,99 @@
defmodule Domain.Repo.Migrations.MigrateInternetResources do
use Ecto.Migration
def change do
# Step 1
#
# Delete all connections where the resource is an internet resource and the gateway group is not the internet site.
# The Internet resource is / will not be a multi-site resource.
execute("""
DELETE FROM resource_connections rc
WHERE EXISTS (
SELECT 1 FROM resources r
WHERE r.id = rc.resource_id AND r.type = 'internet'
)
AND NOT EXISTS (
SELECT 1 FROM gateway_groups gg
WHERE gg.id = rc.gateway_group_id
AND gg.name = 'Internet'
AND gg.managed_by = 'system'
)
""")
# Step 2
#
# Insert a connection to the internet site for each internet resource that does not already have one
execute("""
INSERT INTO resource_connections (
resource_id,
gateway_group_id,
account_id,
created_by
)
SELECT r.id, gg.id, r.account_id, 'system'
FROM resources r
JOIN gateway_groups gg ON r.account_id = gg.account_id
WHERE r.type = 'internet'
AND gg.name = 'Internet'
AND gg.managed_by = 'system'
AND NOT EXISTS (
SELECT 1 FROM resource_connections rc
WHERE rc.resource_id = r.id
AND rc.gateway_group_id = gg.id
)
""")
# Step 3
#
# Recreate existing constraint so that both hold true:
# - only internet resources can be in the internet site
# - on non-internet resources can be in other sites
# See 20250224205226_require_internet_resource_in_internet_site for the previous migration.
# We want to now enforce that only the Internet Resource can be in the Internet Site.
execute("""
DROP TRIGGER IF EXISTS internet_resource_in_internet_gg ON resource_connections;
""")
execute("""
DROP FUNCTION IF EXISTS enforce_internet_resource_in_internet_gg();
""")
execute("""
CREATE OR REPLACE FUNCTION enforce_internet_resource_in_internet_gg()
RETURNS TRIGGER AS $$
DECLARE
resource_type text;
site_name text;
site_managed_by text;
BEGIN
-- Fetch the resource type and gateway group details
SELECT r.type INTO resource_type
FROM resources r
WHERE r.id = NEW.resource_id;
SELECT gg.name, gg.managed_by INTO site_name, site_managed_by
FROM gateway_groups gg
WHERE gg.id = NEW.gateway_group_id;
-- Rule: Prevent non-'internet' resources in the 'Internet' gateway group
IF (site_name = 'Internet' AND site_managed_by = 'system' AND resource_type != 'internet')
OR (resource_type = 'internet' AND (site_name != 'Internet' OR site_managed_by != 'system')) THEN
RAISE EXCEPTION 'Only internet resource type is allowed in the Internet site'
USING ERRCODE = '23514', CONSTRAINT = 'internet_resource_in_internet_site';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
""")
execute("""
CREATE TRIGGER internet_resource_in_internet_gg
BEFORE INSERT OR UPDATE OF resource_id, gateway_group_id
ON resource_connections
FOR EACH ROW
EXECUTE FUNCTION enforce_internet_resource_in_internet_gg()
""")
end
end

View File

@@ -28,6 +28,19 @@ defmodule Domain.Fixtures.Gateways do
group
end
def create_internet_group(attrs \\ %{}) do
attrs = group_attrs(attrs)
{account, _attrs} =
pop_assoc_fixture(attrs, :account, fn assoc_attrs ->
Fixtures.Accounts.create_account(assoc_attrs)
end)
{:ok, group} = Gateways.create_internet_group(account)
group
end
def delete_group(group) do
group = Repo.preload(group, :account)

View File

@@ -49,6 +49,29 @@ defmodule Domain.Fixtures.Resources do
resource
end
def create_internet_resource(attrs \\ %{}) do
attrs = resource_attrs(attrs)
{account, attrs} =
pop_assoc_fixture(attrs, :account, fn assoc_attrs ->
Fixtures.Accounts.create_account(assoc_attrs)
end)
{subject, attrs} =
pop_assoc_fixture(attrs, :subject, fn assoc_attrs ->
assoc_attrs
|> Enum.into(%{account: account, actor: [type: :account_admin_user]})
|> Fixtures.Auth.create_subject()
end)
{:ok, resource} =
attrs
|> Map.put(:type, :internet)
|> Domain.Resources.create_resource(subject)
resource
end
def delete_resource(resource) do
resource = Repo.preload(resource, :account)

View File

@@ -243,57 +243,7 @@ defmodule Web.Sites.Index do
<: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/migrate-your-internet-resource">
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>
<:content></:content>
</.section>
"""
end
@@ -331,64 +281,4 @@ defmodule Web.Sites.Index do
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_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

View File

@@ -163,7 +163,13 @@ defmodule Web.Live.Policies.NewTest do
identity: identity,
conn: conn
} do
resource = Fixtures.Resources.create_resource(account: account, type: :internet)
internet_gateway_group = Fixtures.Gateways.create_internet_group(account: account)
resource =
Fixtures.Resources.create_internet_resource(
account: account,
connections: [%{gateway_group_id: internet_gateway_group.id}]
)
{:ok, lv, _html} =
conn