diff --git a/elixir/apps/api/test/api/client/channel_test.exs b/elixir/apps/api/test/api/client/channel_test.exs index 27f119908..c587f5121 100644 --- a/elixir/apps/api/test/api/client/channel_test.exs +++ b/elixir/apps/api/test/api/client/channel_test.exs @@ -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" } diff --git a/elixir/apps/api/test/api/gateway/channel_test.exs b/elixir/apps/api/test/api/gateway/channel_test.exs index a333c2648..a4535058f 100644 --- a/elixir/apps/api/test/api/gateway/channel_test.exs +++ b/elixir/apps/api/test/api/gateway/channel_test.exs @@ -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() diff --git a/elixir/apps/domain/lib/domain/resources.ex b/elixir/apps/domain/lib/domain/resources.ex index 9a5fe0a89..328bb0fbc 100644 --- a/elixir/apps/domain/lib/domain/resources.ex +++ b/elixir/apps/domain/lib/domain/resources.ex @@ -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() diff --git a/elixir/apps/domain/priv/repo/migrations/20250315163223_migrate_internet_resources.exs b/elixir/apps/domain/priv/repo/migrations/20250315163223_migrate_internet_resources.exs new file mode 100644 index 000000000..28f660fdb --- /dev/null +++ b/elixir/apps/domain/priv/repo/migrations/20250315163223_migrate_internet_resources.exs @@ -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 diff --git a/elixir/apps/domain/test/support/fixtures/gateways.ex b/elixir/apps/domain/test/support/fixtures/gateways.ex index 34ffe03cc..dbc0aaa73 100644 --- a/elixir/apps/domain/test/support/fixtures/gateways.ex +++ b/elixir/apps/domain/test/support/fixtures/gateways.ex @@ -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) diff --git a/elixir/apps/domain/test/support/fixtures/resources.ex b/elixir/apps/domain/test/support/fixtures/resources.ex index f452a5c9d..8d0b000d3 100644 --- a/elixir/apps/domain/test/support/fixtures/resources.ex +++ b/elixir/apps/domain/test/support/fixtures/resources.ex @@ -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) diff --git a/elixir/apps/web/lib/web/live/sites/index.ex b/elixir/apps/web/lib/web/live/sites/index.ex index e043caab6..8983316b2 100644 --- a/elixir/apps/web/lib/web/live/sites/index.ex +++ b/elixir/apps/web/lib/web/live/sites/index.ex @@ -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. - - <:content :if={ - Domain.Accounts.internet_resource_enabled?(@account) && - needs_internet_resource_migration?(@internet_resource, @internet_gateway_group) - }> -
-

- ACTION REQUIRED: Please migrate your existing Internet Resource to this Site before March 15, 2025. -

-

- <.website_link path="/blog/migrate-your-internet-resource"> - Read more about why this is necessary. - -

- <.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_content> -

- <.icon name="hero-exclamation-triangle-solid" class="w-16 h-16 text-primary-500" /> -

-

- Migrating the Internet Resource will permanently - move it from the {@existing_internet_resource_group_name} - Site to the Internet - Site. This cannot be reversed. -

-

- Any Clients connected to this Resource will be immediately disconnected. -

-

- To minimize downtime, it is recommended to deploy new Gateways in the Internet Site before completing the migration of the Internet Resource. -

- - <:dialog_confirm_button> - Migrate Internet Resource - - <:dialog_cancel_button> - Cancel - - Migrate Internet Resource - -
- + <:content> """ 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 diff --git a/elixir/apps/web/test/web/live/policies/new_test.exs b/elixir/apps/web/test/web/live/policies/new_test.exs index 700365521..80ed805fa 100644 --- a/elixir/apps/web/test/web/live/policies/new_test.exs +++ b/elixir/apps/web/test/web/live/policies/new_test.exs @@ -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