From 023d05ece12727fdaed79429e14222c8a6bcb904 Mon Sep 17 00:00:00 2001 From: Brian Manifold Date: Mon, 5 Aug 2024 08:45:22 -0700 Subject: [PATCH] feat(portal): Add 'temp account' feature for launch HN (#6153) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: * As part of our Launch HN, it was recommended to have a way to allow people to try Firezone without needing to sign up. This commit adds the changes need to create temporary accounts that are intended to be deleted after the Launch HN is complete. ## Screenshots #### Start Page Screenshot 2024-08-02 at 11 00 15 AM #### Temp Account Info Page Screenshot 2024-08-02 at 11 00 28 AM #### Temp Account Sign In Screenshot 2024-08-02 at 11 00 44 AM #### Bottom Banner Screenshot 2024-08-02 at 11 01 02 AM #### Temp Account Identity Provider Screenshot 2024-08-02 at 11 01 35 AM --- elixir/apps/domain/lib/domain/auth.ex | 6 +- elixir/apps/domain/lib/domain/auth/adapter.ex | 2 +- .../apps/domain/lib/domain/auth/adapters.ex | 5 +- .../lib/domain/auth/adapters/temp_account.ex | 119 ++++++++++ .../auth/adapters/temp_account/password.ex | 10 + .../temp_account/password/changeset.ex | 29 +++ .../apps/domain/lib/domain/auth/provider.ex | 3 +- .../domain/lib/domain/config/definitions.ex | 10 +- .../web/lib/web/components/core_components.ex | 14 +- .../lib/web/components/layouts/app.html.heex | 1 + .../web/components/navigation_components.ex | 24 ++ .../lib/web/controllers/auth_controller.ex | 35 ++- .../web/lib/web/live/actors/components.ex | 11 + .../settings/identity_providers/components.ex | 3 +- elixir/apps/web/lib/web/live/sign_in.ex | 64 +++++- .../apps/web/lib/web/live/sites/new_token.ex | 102 ++++++++- .../web/lib/web/live/temp_accounts/index.ex | 211 ++++++++++++++++++ elixir/apps/web/lib/web/router.ex | 8 +- elixir/config/config.exs | 3 +- elixir/config/runtime.exs | 3 +- 20 files changed, 644 insertions(+), 19 deletions(-) create mode 100644 elixir/apps/domain/lib/domain/auth/adapters/temp_account.ex create mode 100644 elixir/apps/domain/lib/domain/auth/adapters/temp_account/password.ex create mode 100644 elixir/apps/domain/lib/domain/auth/adapters/temp_account/password/changeset.ex create mode 100644 elixir/apps/web/lib/web/live/temp_accounts/index.ex diff --git a/elixir/apps/domain/lib/domain/auth.ex b/elixir/apps/domain/lib/domain/auth.ex index 431495a44..bc2da4bac 100644 --- a/elixir/apps/domain/lib/domain/auth.ex +++ b/elixir/apps/domain/lib/domain/auth.ex @@ -72,7 +72,7 @@ defmodule Domain.Auth do alias Domain.Auth.Identity # This session duration is used when IdP doesn't return the token expiration date, - # or no IdP is used (eg. sign in via email or userpass). + # or no IdP is used (eg. sign in via email, userpass, or temp_account). @default_session_duration_hours [ browser: [ account_admin_user: 10, @@ -143,7 +143,7 @@ defmodule Domain.Auth do This functions allows to fetch singleton providers like `email` or `token`. """ def fetch_active_provider_by_adapter(adapter, %Subject{} = subject, opts \\ []) - when adapter in [:email, :userpass] do + when adapter in [:email, :userpass, :temp_account] do with :ok <- ensure_has_permissions(subject, Authorizer.manage_providers_permission()) do Provider.Query.not_disabled() |> Provider.Query.by_adapter(adapter) @@ -170,7 +170,7 @@ defmodule Domain.Auth do def all_third_party_providers!(%Subject{} = subject) do Provider.Query.not_deleted() |> Provider.Query.by_account_id(subject.account.id) - |> Provider.Query.by_adapter({:not_in, [:email, :userpass]}) + |> Provider.Query.by_adapter({:not_in, [:email, :userpass, :temp_account]}) |> Authorizer.for_subject(Provider, subject) |> Repo.all() end diff --git a/elixir/apps/domain/lib/domain/auth/adapter.ex b/elixir/apps/domain/lib/domain/auth/adapter.ex index 42721a74f..4349b009f 100644 --- a/elixir/apps/domain/lib/domain/auth/adapter.ex +++ b/elixir/apps/domain/lib/domain/auth/adapter.ex @@ -72,7 +72,7 @@ defmodule Domain.Auth.Adapter do A callback invoked during sign-in, should verify the secret and return the identity if it's valid, or an error otherwise. - Used by secret-based providers, eg.: UserPass, Email. + Used by secret-based providers, eg.: UserPass, Email, TempAccount. """ @callback verify_secret(%Identity{}, %Context{}, secret :: term()) :: {:ok, %Identity{}, expires_at :: %DateTime{} | nil} diff --git a/elixir/apps/domain/lib/domain/auth/adapters.ex b/elixir/apps/domain/lib/domain/auth/adapters.ex index 23ff288be..61c29c3df 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters.ex @@ -9,7 +9,8 @@ defmodule Domain.Auth.Adapters do microsoft_entra: Domain.Auth.Adapters.MicrosoftEntra, okta: Domain.Auth.Adapters.Okta, jumpcloud: Domain.Auth.Adapters.JumpCloud, - userpass: Domain.Auth.Adapters.UserPass + userpass: Domain.Auth.Adapters.UserPass, + temp_account: Domain.Auth.Adapters.TempAccount } @adapter_names Map.keys(@adapters) @@ -29,7 +30,7 @@ defmodule Domain.Auth.Adapters do def list_user_provisioned_adapters! do enabled_adapters = Domain.Config.compile_config!(:auth_provider_adapters) - enabled_idp_adapters = enabled_adapters -- ~w[email userpass]a + enabled_idp_adapters = enabled_adapters -- ~w[email userpass temp_account]a Map.take(@adapters, enabled_idp_adapters) end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/temp_account.ex b/elixir/apps/domain/lib/domain/auth/adapters/temp_account.ex new file mode 100644 index 000000000..f235db85b --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/temp_account.ex @@ -0,0 +1,119 @@ +defmodule Domain.Auth.Adapters.TempAccount do + @moduledoc """ + This is only being used for Launch HN and will be removed + shortly after. + """ + use Supervisor + alias Domain.Repo + alias Domain.Auth.{Identity, Provider, Adapter, Context} + alias Domain.Auth.Adapters.TempAccount.Password + + @behaviour Adapter + @behaviour Adapter.Local + + def start_link(_init_arg) do + Supervisor.start_link(__MODULE__, nil, name: __MODULE__) + end + + @impl true + def init(_init_arg) do + children = [] + + Supervisor.init(children, strategy: :one_for_one) + end + + @impl true + def capabilities do + [ + provisioners: [:manual], + default_provisioner: :manual, + parent_adapter: nil + ] + end + + @impl true + def identity_changeset(%Provider{} = _provider, %Ecto.Changeset{} = changeset) do + changeset + |> Domain.Repo.Changeset.trim_change(:provider_identifier) + |> validate_password() + end + + defp validate_password(changeset) do + data = Map.get(changeset.data, :provider_virtual_state) || %{} + attrs = Ecto.Changeset.get_change(changeset, :provider_virtual_state) || %{} + + Ecto.embedded_load(Password, data, :json) + |> Password.Changeset.changeset(attrs) + |> case do + %{valid?: false} = nested_changeset -> + {changeset, _original_type} = + Repo.Changeset.inject_embedded_changeset( + changeset, + :provider_virtual_state, + nested_changeset + ) + + changeset + + %{valid?: true} = nested_changeset -> + password_hash = Ecto.Changeset.fetch_change!(nested_changeset, :password_hash) + + {changeset, _original_type} = + changeset + |> Ecto.Changeset.put_change(:provider_state, %{"password_hash" => password_hash}) + |> Repo.Changeset.inject_embedded_changeset(:provider_virtual_state, nested_changeset) + + changeset + end + end + + @impl true + def provider_changeset(%Ecto.Changeset{} = changeset) do + changeset + end + + @impl true + def ensure_provisioned(%Provider{} = provider) do + {:ok, provider} + end + + @impl true + def ensure_deprovisioned(%Provider{} = provider) do + {:ok, provider} + end + + @impl true + def sign_out(%Provider{} = _provider, %Identity{} = identity, redirect_url) do + {:ok, identity, redirect_url} + end + + @impl true + def verify_secret(%Identity{} = identity, %Context{} = _context, password) + when is_binary(password) do + Identity.Query.not_disabled() + |> Identity.Query.by_id(identity.id) + |> Repo.fetch_and_update(Identity.Query, + with: fn identity -> + password_hash = identity.provider_state["password_hash"] + + cond do + is_nil(password_hash) -> + :invalid_secret + + not Domain.Crypto.equal?(:argon2, password, password_hash) -> + :invalid_secret + + true -> + Ecto.Changeset.change(identity) + end + end + ) + |> case do + {:ok, identity} -> + {:ok, identity, nil} + + {:error, reason} -> + {:error, reason} + end + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/temp_account/password.ex b/elixir/apps/domain/lib/domain/auth/adapters/temp_account/password.ex new file mode 100644 index 000000000..b7ac7879b --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/temp_account/password.ex @@ -0,0 +1,10 @@ +defmodule Domain.Auth.Adapters.TempAccount.Password do + use Domain, :schema + + @primary_key false + embedded_schema do + field :password, :string, virtual: true, redact: true + field :password_hash, :string + field :password_confirmation, :string, virtual: true, redact: true + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/temp_account/password/changeset.ex b/elixir/apps/domain/lib/domain/auth/adapters/temp_account/password/changeset.ex new file mode 100644 index 000000000..47f9b2ef2 --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/temp_account/password/changeset.ex @@ -0,0 +1,29 @@ +defmodule Domain.Auth.Adapters.TempAccount.Password.Changeset do + use Domain, :changeset + alias Domain.Auth.Adapters.TempAccount.Password + + @fields ~w[password password_confirmation]a + @min_password_length 12 + @max_password_length 72 + + def changeset(%Password{} = struct, attrs) do + struct + |> cast(attrs, @fields) + |> validate_required(@fields) + |> validate_confirmation(:password, required: true) + |> validate_length(:password, + min: @min_password_length, + max: @max_password_length, + count: :bytes + ) + # We can improve password strength checks later if we decide to run this provider in production. + # |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character") + # |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character") + # |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character") + # |> validate_no_repetitive_characters(:password) + # |> validate_no_sequential_characters(:password) + # |> validate_no_public_context(:password) + |> put_hash(:password, :argon2, to: :password_hash) + |> validate_required([:password_hash]) + end +end diff --git a/elixir/apps/domain/lib/domain/auth/provider.ex b/elixir/apps/domain/lib/domain/auth/provider.ex index 514638b2a..11dd97288 100644 --- a/elixir/apps/domain/lib/domain/auth/provider.ex +++ b/elixir/apps/domain/lib/domain/auth/provider.ex @@ -5,7 +5,8 @@ defmodule Domain.Auth.Provider do field :name, :string field :adapter, Ecto.Enum, - values: ~w[email openid_connect google_workspace microsoft_entra okta jumpcloud userpass]a + values: + ~w[email openid_connect google_workspace microsoft_entra okta jumpcloud userpass temp_account]a field :provisioner, Ecto.Enum, values: ~w[manual just_in_time custom]a field :adapter_config, :map, redact: true diff --git a/elixir/apps/domain/lib/domain/config/definitions.ex b/elixir/apps/domain/lib/domain/config/definitions.ex index 96fee049d..e914b8330 100644 --- a/elixir/apps/domain/lib/domain/config/definitions.ex +++ b/elixir/apps/domain/lib/domain/config/definitions.ex @@ -100,6 +100,7 @@ defmodule Domain.Config.Definitions do * `openid_connect` is used to authenticate users via OpenID Connect, this is recommended for production use; * `email` is used to authenticate users via sign in tokens sent to the email; * `token` is used to authenticate service accounts using an API token; + * `temp_account` is used to authenticate users with a password, only used for Launch HN; * `userpass` is used to authenticate users with username and password, should be used with extreme care and is not recommended for production use. """, @@ -442,9 +443,11 @@ defmodule Domain.Config.Definitions do okta jumpcloud userpass + temp_account token ]a)}}, - default: ~w[email openid_connect google_workspace microsoft_entra okta jumpcloud token]a + default: + ~w[email openid_connect google_workspace microsoft_entra okta jumpcloud token temp_account]a ) ############################################## @@ -669,6 +672,11 @@ defmodule Domain.Config.Definitions do """ defconfig(:feature_rest_api_enabled, :boolean, default: false) + @doc """ + Boolean flag to turn 'Try Firezone' / temporary accounts functionality on/off + """ + defconfig(:feature_temp_accounts, :boolean, default: false) + ############################################## ## Analytics ############################################## diff --git a/elixir/apps/web/lib/web/components/core_components.ex b/elixir/apps/web/lib/web/components/core_components.ex index aa041a292..4fa25bd2d 100644 --- a/elixir/apps/web/lib/web/components/core_components.ex +++ b/elixir/apps/web/lib/web/components/core_components.ex @@ -285,10 +285,10 @@ defmodule Web.CoreComponents do id={@id} class={[ "p-4 text-sm flash-#{@kind}", - @kind == :success && "text-green-800 bg-green-50", - @kind == :info && "text-blue-800 bg-blue-50", - @kind == :warning && "text-yellow-800 bg-yellow-50", - @kind == :error && "text-red-800 bg-red-50", + @kind == :success && "text-green-800 bg-green-100", + @kind == :info && "text-blue-800 bg-blue-100", + @kind == :warning && "text-yellow-800 bg-yellow-100", + @kind == :error && "text-red-800 bg-red-100", @style != "wide" && "mb-4 rounded" ]} role="alert" @@ -1251,6 +1251,12 @@ defmodule Web.CoreComponents do """ end + def provider_icon(%{adapter: :temp_account} = assigns) do + ~H""" + <.icon name="hero-key" {@rest} /> + """ + end + def provider_icon(assigns), do: ~H"" def feature_name(%{feature: :idp_sync} = assigns) do diff --git a/elixir/apps/web/lib/web/components/layouts/app.html.heex b/elixir/apps/web/lib/web/components/layouts/app.html.heex index 5fd597446..36e61f29c 100644 --- a/elixir/apps/web/lib/web/components/layouts/app.html.heex +++ b/elixir/apps/web/lib/web/components/layouts/app.html.heex @@ -125,4 +125,5 @@ <%= @inner_content %> + <.bottom_banner :if={String.starts_with?(@account.slug, "temp_")} /> diff --git a/elixir/apps/web/lib/web/components/navigation_components.ex b/elixir/apps/web/lib/web/components/navigation_components.ex index fb5b61d6f..6900834eb 100644 --- a/elixir/apps/web/lib/web/components/navigation_components.ex +++ b/elixir/apps/web/lib/web/components/navigation_components.ex @@ -3,6 +3,30 @@ defmodule Web.NavigationComponents do use Web, :verified_routes import Web.CoreComponents + def bottom_banner(assigns) do + ~H""" +
+
+

+ <.icon name="hero-exclamation-triangle" class="w-5 h-5 mx-2" /> + + Reminder: This account is temporary and will be deleted! → + + + + Click here to create a free starter account! + + +

+
+
+ """ + end + attr :subject, :any, required: true def topbar(assigns) do diff --git a/elixir/apps/web/lib/web/controllers/auth_controller.ex b/elixir/apps/web/lib/web/controllers/auth_controller.ex index c351846fb..d791fb7c6 100644 --- a/elixir/apps/web/lib/web/controllers/auth_controller.ex +++ b/elixir/apps/web/lib/web/controllers/auth_controller.ex @@ -22,7 +22,7 @@ defmodule Web.AuthController do action_fallback Web.FallbackController @doc """ - This is a callback for the UserPass provider which checks login and password to authenticate the user. + This is a callback for the UserPass/TempAccount provider which checks login and password to authenticate the user. """ def verify_credentials( conn, @@ -59,6 +59,39 @@ defmodule Web.AuthController do end end + def verify_credentials( + conn, + %{ + "account_id_or_slug" => account_id_or_slug, + "provider_id" => provider_id, + "temp_account" => %{ + "secret" => secret + } + } = params + ) do + redirect_params = Web.Auth.take_sign_in_params(params) + context_type = Web.Auth.fetch_auth_context_type!(redirect_params) + context = Web.Auth.get_auth_context(conn, context_type) + nonce = Web.Auth.fetch_token_nonce!(redirect_params) + + with {:ok, provider} <- + Domain.Auth.fetch_active_provider_by_id(provider_id, preload: :account), + provider_identifier = provider_identifier(provider), + {:ok, identity, encoded_fragment} <- + Domain.Auth.sign_in(provider, provider_identifier, nonce, secret, context) do + Web.Auth.signed_in(conn, provider, identity, context, encoded_fragment, redirect_params) + else + {:error, _reason} -> + conn + |> put_flash(:error, "Invalid password.") + |> redirect(to: ~p"/#{account_id_or_slug}?#{redirect_params}") + end + end + + defp provider_identifier(provider) do + "admin_" <> provider.account.slug <> "@firezonedemo.com" + end + @doc """ This is a callback for the Email provider which sends login link. """ diff --git a/elixir/apps/web/lib/web/live/actors/components.ex b/elixir/apps/web/lib/web/live/actors/components.ex index 4b10f4591..7e990d1e9 100644 --- a/elixir/apps/web/lib/web/live/actors/components.ex +++ b/elixir/apps/web/lib/web/live/actors/components.ex @@ -165,6 +165,17 @@ defmodule Web.Actors.Components do """ end + def provider_form(%{provider: %{adapter: :temp_account}} = assigns) do + ~H""" +
+ No other identities can be created using this provider. The temporary account is intended to be used as a brief trial of Firezone. To create a free starter account click here. +
+ """ + end + def next_step_path(:service_account, account) do ~p"/#{account}/actors/service_accounts/new" end diff --git a/elixir/apps/web/lib/web/live/settings/identity_providers/components.ex b/elixir/apps/web/lib/web/live/settings/identity_providers/components.ex index 1df6774cd..daaa362c4 100644 --- a/elixir/apps/web/lib/web/live/settings/identity_providers/components.ex +++ b/elixir/apps/web/lib/web/live/settings/identity_providers/components.ex @@ -264,6 +264,7 @@ defmodule Web.Settings.IdentityProviders.Components do def adapter_name(:email), do: "Email" def adapter_name(:userpass), do: "Username & Password" + def adapter_name(:temp_account), do: "Temporary Account" def adapter_name(:google_workspace), do: "Google Workspace" def adapter_name(:microsoft_entra), do: "Microsoft Entra" def adapter_name(:okta), do: "Okta" @@ -271,7 +272,7 @@ defmodule Web.Settings.IdentityProviders.Components do def adapter_name(:openid_connect), do: "OpenID Connect" def view_provider(account, %{adapter: adapter} = provider) - when adapter in [:email, :userpass], + when adapter in [:email, :userpass, :temp_account], do: ~p"/#{account}/settings/identity_providers/system/#{provider}" def view_provider(account, %{adapter: :openid_connect} = provider), diff --git a/elixir/apps/web/lib/web/live/sign_in.ex b/elixir/apps/web/lib/web/live/sign_in.ex index 6768ca059..b26ab5cc8 100644 --- a/elixir/apps/web/lib/web/live/sign_in.ex +++ b/elixir/apps/web/lib/web/live/sign_in.ex @@ -2,7 +2,7 @@ defmodule Web.SignIn do use Web, {:live_view, layout: {Web.Layouts, :public}} alias Domain.{Auth, Accounts} - @root_adapters_whitelist [:email, :userpass, :openid_connect] + @root_adapters_whitelist [:email, :userpass, :openid_connect, :temp_account] def mount(%{"account_id_or_slug" => account_id_or_slug} = params, _session, socket) do with {:ok, account} <- Accounts.fetch_account_by_id_or_slug(account_id_or_slug), @@ -88,6 +88,20 @@ defmodule Web.SignIn do /> + <:item :if={adapter_enabled?(@providers_by_adapter, :temp_account)}> +

+ Sign in with a password +

+ + <.providers_group_form + adapter="temp_account" + provider={List.first(@providers_by_adapter[:temp_account])} + account={@account} + flash={@flash} + params={@params} + /> + + <:item :if={adapter_enabled?(@providers_by_adapter, :email)}>

Sign in with email @@ -194,6 +208,54 @@ defmodule Web.SignIn do """ end + def providers_group_form(%{adapter: "temp_account"} = assigns) do + provider_identifier = Phoenix.Flash.get(assigns.flash, :userpass_provider_identifier) + form = to_form(%{"provider_identifier" => provider_identifier}, as: "temp_account") + + assigns = + assigns + |> Map.put(:temp_account_form, form) + |> Map.put(:enabled?, Domain.Config.global_feature_enabled?(:temp_accounts)) + + ~H""" + <.form + :if={@enabled?} + for={@temp_account_form} + action={~p"/#{@account}/sign_in/providers/#{@provider.id}/verify_credentials"} + class="space-y-4 lg:space-y-6" + id="temp_account_form" + phx-update="ignore" + phx-hook="AttachDisableSubmit" + phx-submit={JS.dispatch("form:disable_and_submit", to: "#temp_account_form")} + > +
+ <.input :for={{key, value} <- @params} type="hidden" name={key} value={value} /> + + <.input + field={@temp_account_form[:secret]} + type="password" + label="Password" + placeholder="••••••••" + required + /> +
+ + <.submit_button class="w-full" style="info" icon="hero-key"> + Sign in + + +
+ + Temporary Accounts have been disabled. + +

+ Click here + to create a free starter account. +

+
+ """ + end + def providers_group_form(%{adapter: "email"} = assigns) do provider_identifier = Phoenix.Flash.get(assigns.flash, :email_provider_identifier) form = to_form(%{"provider_identifier" => provider_identifier}, as: "email") diff --git a/elixir/apps/web/lib/web/live/sites/new_token.ex b/elixir/apps/web/lib/web/live/sites/new_token.ex index 8da774f22..8d1e6b4fc 100644 --- a/elixir/apps/web/lib/web/live/sites/new_token.ex +++ b/elixir/apps/web/lib/web/live/sites/new_token.ex @@ -32,10 +32,17 @@ defmodule Web.Sites.NewToken do end def handle_params(params, uri, socket) do + selected_tab = + if String.starts_with?(socket.assigns.account.slug, "temp_") do + "docker-compose-instructions" + else + "systemd-instructions" + end + {:noreply, assign(socket, uri: uri, - selected_tab: Map.get(params, "method", "systemd-instructions") + selected_tab: Map.get(params, "method", selected_tab) )} end @@ -72,6 +79,36 @@ defmodule Web.Sites.NewToken do <.tabs :if={@env} id="deployment-instructions"> + <:tab + :if={String.starts_with?(@account.slug, "temp_")} + id="docker-compose-instructions" + icon="docker" + label="Docker Compose" + phx_click="tab_selected" + selected={@selected_tab == "docker-compose-instructions"} + > +

+ Copy-paste the code block below to a file titled + docker-compose.yml +
Then run docker compose up -d +

+ + <.code_block + id="code-sample-docker1" + class="w-full text-xs whitespace-pre-line" + phx-no-format + phx-update="ignore" + ><%= docker_compose(@env) %> + +

+ Important: + If you need IPv6 support, you must <.link + href="https://docs.docker.com/config/daemon/ipv6" + class={link_style()} + target="_blank" + >enable IPv6 in the Docker daemon. +

+ <:tab id="systemd-instructions" icon="hero-command-line" @@ -321,6 +358,69 @@ defmodule Web.Sites.NewToken do """ end + defp docker_compose(env) do + env = Enum.into(env, %{}) + + """ + services: + firezone-gateway: + image: "ghcr.io/firezone/gateway:1" + init: true + environment: + # Use a unique ID for each Gateway in your Firezone account. If left blank, + # the Gateway will generate a random ID saved in /var/lib/firezone + # - FIREZONE_ID= + + # REQUIRED. The token shown when deploying a Gateway in the admin portal. + - FIREZONE_TOKEN=#{env["FIREZONE_TOKEN"]} + + # REQUIRED. Firezone URL + - FIREZONE_API_URL=#{env["FIREZONE_API_URL"]} + + # Configure log output. Other options are "trace", "debug", "info", "warn", "error", and "off". + # See https://docs.rs/env_logger/latest/env_logger/ for more information. + - RUST_LOG=str0m=warn,info + + # Enable or disable masquerading. Default enabled. Disabling this can prevent + # the Gateway from reaching other hosts in your subnet or the internet. + - FIREZONE_ENABLE_MASQUERADE=1 + + # Human-friendly name to use for this Gateway in the admin portal. + # $(hostname) is used by default if not set. + # - FIREZONE_NAME= + volumes: + # Persist the FIREZONE_ID. Can be omitted if FIREZONE_ID is set above. + - /var/lib/firezone:/var/lib/firezone + cap_add: + - NET_ADMIN + sysctls: + # Enable IP forwarding + - net.ipv4.ip_forward=1 + - net.ipv4.conf.all.src_valid_mark=1 + - net.ipv6.conf.all.disable_ipv6=0 + - net.ipv6.conf.all.forwarding=1 + - net.ipv6.conf.default.forwarding=1 + healthcheck: + test: ["CMD", "ip", "link", "|", "grep", "tun-firezone"] + interval: 5s + timeout: 10s + retries: 3 + start_period: 1m + devices: + - /dev/net/tun:/dev/net/tun + networks: + fz-net: + + httpbin: + image: "kong/httpbin" + networks: + fz-net: + + networks: + fz-net: + """ + end + def handle_event("tab_selected", %{"id" => id}, socket) do socket |> assign(selected_tab: id) diff --git a/elixir/apps/web/lib/web/live/temp_accounts/index.ex b/elixir/apps/web/lib/web/live/temp_accounts/index.ex new file mode 100644 index 000000000..091e2daf5 --- /dev/null +++ b/elixir/apps/web/lib/web/live/temp_accounts/index.ex @@ -0,0 +1,211 @@ +defmodule Web.TempAccounts.Index do + use Web, {:live_view, layout: {Web.Layouts, :public}} + alias Domain.{Accounts, Actors, Auth, Config} + + def mount(_params, _session, socket) do + if Config.global_feature_enabled?(:temp_accounts) do + socket = + assign(socket, + page_title: "Try Firezone", + account_info: nil, + creation_error: false + ) + + {:ok, socket} + else + raise(Web.LiveErrors.NotFoundError) + end + end + + def render(assigns) do + ~H""" +
+
+ <.hero_logo text="Welcome to Firezone" /> + +
+
+ <.flash flash={@flash} kind={:error} /> + <.flash flash={@flash} kind={:info} /> + <.welcome :if={is_nil(@account_info) and @creation_error == false} /> + <.account + :if={not is_nil(@account_info)} + account={@account_info.account} + password={@account_info.password} + /> +
+ Something went wrong! +
+
+
+
+
+ """ + end + + def welcome(assigns) do + ~H""" +
+

+ Interested in trying out Firezone? You've come to the right place! +

+ +

+ Take Firezone for a spin with a temporary demo account. +

+
+ <.button class="w-full" phx-click="start">Create Temporary Account +
+ +
+
+
or
+
+
+ +

+ Sign up below with a free starter account to keep your data. +

+

+ + Create Free Starter Account + +

+
+ """ + end + + attr :account, :any + attr :password, :string + + def account(assigns) do + ~H""" +
+
+ Your temporary account has been created! +
+ <.flash kind={:warning}> +

+ Warning! +

+
Please save the following information, it will not be displayed again.
+ +
+
+ <.code_block + id="code-sample-systemd0" + class="w-full text-xs whitespace-pre-line rounded" + phx-no-format + ><%= account_details(@account, @password) %> +
+
+
+ <.link class={button_style("primary") ++ ["py-2"]} navigate={~p"/#{@account}"}> + Sign In + +
+
+ """ + end + + def handle_event("start", _params, socket) do + case register_temp_account() do + {:ok, account_info} -> + socket = + socket + |> assign(:account_info, account_info) + + {:noreply, socket} + + {:error, _} -> + socket = + socket + |> assign(:creation_error, true) + + {:noreply, socket} + end + end + + defp register_temp_account do + account_name = random_string(12) + account_slug = "temp_" <> account_name + admin_email = "admin_#{account_slug}@firezonedemo.com" + admin_password = random_string(16) + + Ecto.Multi.new() + |> Ecto.Multi.run( + :account, + fn _repo, _changes -> + Accounts.create_account(%{ + name: "Temp Account #{account_name}", + slug: account_slug, + metadata: %{stripe: %{billing_email: admin_email}} + }) + end + ) + |> Ecto.Multi.run(:everyone_group, fn _repo, %{account: account} -> + Domain.Actors.create_managed_group(account, %{ + name: "Everyone", + membership_rules: [%{operator: true}] + }) + end) + |> Ecto.Multi.run( + :provider, + fn _repo, %{account: account} -> + Auth.create_provider(account, %{ + name: "Temp Account Password", + adapter: :temp_account, + adapter_config: %{} + }) + end + ) + |> Ecto.Multi.run( + :actor, + fn _repo, %{account: account} -> + Actors.create_actor(account, %{ + type: :account_admin_user, + name: "Admin #{account_slug}" + }) + end + ) + |> Ecto.Multi.run( + :identity, + fn _repo, %{actor: actor, provider: provider} -> + Auth.create_identity(actor, provider, %{ + provider_identifier: admin_email, + provider_virtual_state: %{ + "password" => admin_password, + "password_confirmation" => admin_password + } + }) + end + ) + |> Ecto.Multi.run( + :password, + fn _repo, %{} -> {:ok, admin_password} end + ) + |> Domain.Repo.transaction() + end + + defp random_string(length) do + :crypto.strong_rand_bytes(length) + |> Base.encode32() + |> binary_part(0, length) + |> String.downcase() + end + + defp account_details(account, password) do + """ + Account Name: #{account.name} + + Account Slug: #{account.slug} + + Account URL: #{url(~p"/#{account}")} + + Account Password: #{password} + """ + end +end diff --git a/elixir/apps/web/lib/web/router.ex b/elixir/apps/web/lib/web/router.ex index a27e39ae1..91344631a 100644 --- a/elixir/apps/web/lib/web/router.ex +++ b/elixir/apps/web/lib/web/router.ex @@ -66,6 +66,12 @@ defmodule Web.Router do live "/", SignUp end + scope "/try", Web do + pipe_through :public + + live "/", TempAccounts.Index + end + scope "/:account_id_or_slug", Web do pipe_through [:public, :account, :redirect_if_user_is_authenticated] @@ -89,7 +95,7 @@ defmodule Web.Router do get "/sign_in/client_auth_error", SignInController, :client_auth_error scope "/sign_in/providers/:provider_id" do - # UserPass + # UserPass / Temp Account post "/verify_credentials", AuthController, :verify_credentials # Email diff --git a/elixir/config/config.exs b/elixir/config/config.exs index b12cbc6d8..5ea7d1a31 100644 --- a/elixir/config/config.exs +++ b/elixir/config/config.exs @@ -90,7 +90,8 @@ config :domain, :enabled_features, self_hosted_relays: true, policy_conditions: true, multi_site_resources: true, - rest_api: true + rest_api: true, + temp_accounts: true config :domain, sign_up_whitelisted_domains: [] diff --git a/elixir/config/runtime.exs b/elixir/config/runtime.exs index 208ee2357..c977d7d22 100644 --- a/elixir/config/runtime.exs +++ b/elixir/config/runtime.exs @@ -64,7 +64,8 @@ if config_env() == :prod do self_hosted_relays: compile_config!(:feature_self_hosted_relays_enabled), policy_conditions: compile_config!(:feature_policy_conditions_enabled), multi_site_resources: compile_config!(:feature_multi_site_resources_enabled), - rest_api: compile_config!(:feature_rest_api_enabled) + rest_api: compile_config!(:feature_rest_api_enabled), + temp_accounts: compile_config!(:feature_temp_accounts) config :domain, sign_up_whitelisted_domains: compile_config!(:sign_up_whitelisted_domains)