mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
feat(portal): Add 'temp account' feature for launch HN (#6153)
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 <img width="1459" alt="Screenshot 2024-08-02 at 11 00 15 AM" src="https://github.com/user-attachments/assets/9b4c5dd4-52ee-43dc-8b4f-d3cc6389b698"> #### Temp Account Info Page <img width="1461" alt="Screenshot 2024-08-02 at 11 00 28 AM" src="https://github.com/user-attachments/assets/7e96360d-a878-4e63-b3f6-cca29d0bd79f"> #### Temp Account Sign In <img width="1461" alt="Screenshot 2024-08-02 at 11 00 44 AM" src="https://github.com/user-attachments/assets/f812e72a-7030-4b35-9ac3-3816a056ef55"> #### Bottom Banner <img width="1462" alt="Screenshot 2024-08-02 at 11 01 02 AM" src="https://github.com/user-attachments/assets/b5e9d90f-e888-46f1-9bb6-bcc59fe2c6e6"> #### Temp Account Identity Provider <img width="1461" alt="Screenshot 2024-08-02 at 11 01 35 AM" src="https://github.com/user-attachments/assets/79b3d7c4-fe3a-45a6-b4de-56d4f2c70f8e">
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
119
elixir/apps/domain/lib/domain/auth/adapters/temp_account.ex
Normal file
119
elixir/apps/domain/lib/domain/auth/adapters/temp_account.ex
Normal file
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
##############################################
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -125,4 +125,5 @@
|
||||
</.flash>
|
||||
|
||||
<%= @inner_content %>
|
||||
<.bottom_banner :if={String.starts_with?(@account.slug, "temp_")} />
|
||||
</main>
|
||||
|
||||
@@ -3,6 +3,30 @@ defmodule Web.NavigationComponents do
|
||||
use Web, :verified_routes
|
||||
import Web.CoreComponents
|
||||
|
||||
def bottom_banner(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id="bottom-banner"
|
||||
tabindex="-1"
|
||||
class="fixed bottom-0 start-0 z-50 flex justify-between w-full p-4 bg-primary-400"
|
||||
>
|
||||
<div class="flex items-center mx-auto">
|
||||
<p class="flex items-center text-md font-normal text-neutral-800">
|
||||
<.icon name="hero-exclamation-triangle" class="w-5 h-5 mx-2" />
|
||||
<span class="mr-1">
|
||||
<b>Reminder:</b> This account is temporary and will be deleted! →
|
||||
</span>
|
||||
<span>
|
||||
<a href={url(~p"/sign_up")} class={[link_style()]}>
|
||||
Click here to create a free starter account!
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :subject, :any, required: true
|
||||
|
||||
def topbar(assigns) do
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -165,6 +165,17 @@ defmodule Web.Actors.Components do
|
||||
"""
|
||||
end
|
||||
|
||||
def provider_form(%{provider: %{adapter: :temp_account}} = assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
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 <a
|
||||
class={link_style()}
|
||||
href={url(~p"/sign_up")}
|
||||
>click here</a>.
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def next_step_path(:service_account, account) do
|
||||
~p"/#{account}/actors/service_accounts/new"
|
||||
end
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
<:item :if={adapter_enabled?(@providers_by_adapter, :temp_account)}>
|
||||
<h2 class="text-lg sm:text-xl leading-tight tracking-tight text-neutral-900">
|
||||
Sign in with a password
|
||||
</h2>
|
||||
|
||||
<.providers_group_form
|
||||
adapter="temp_account"
|
||||
provider={List.first(@providers_by_adapter[:temp_account])}
|
||||
account={@account}
|
||||
flash={@flash}
|
||||
params={@params}
|
||||
/>
|
||||
</:item>
|
||||
|
||||
<:item :if={adapter_enabled?(@providers_by_adapter, :email)}>
|
||||
<h2 class="text-lg sm:text-xl leading-tight tracking-tight text-neutral-900">
|
||||
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")}
|
||||
>
|
||||
<div class="bg-white grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
|
||||
<.input :for={{key, value} <- @params} type="hidden" name={key} value={value} />
|
||||
|
||||
<.input
|
||||
field={@temp_account_form[:secret]}
|
||||
type="password"
|
||||
label="Password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<.submit_button class="w-full" style="info" icon="hero-key">
|
||||
Sign in
|
||||
</.submit_button>
|
||||
</.form>
|
||||
<div :if={not @enabled?} class="text-center border rounded py-4">
|
||||
<span class="text-xl">
|
||||
Temporary Accounts have been disabled.
|
||||
</span>
|
||||
<p>
|
||||
<a class={link_style()} href={url(~p"/sign_up")}>Click here</a>
|
||||
to create a free starter account.
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
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")
|
||||
|
||||
@@ -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
|
||||
</div>
|
||||
|
||||
<.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"}
|
||||
>
|
||||
<p class="p-6">
|
||||
Copy-paste the code block below to a file titled
|
||||
<code class="bg-black text-white px-2">docker-compose.yml</code>
|
||||
<br /> Then run <code class="bg-black text-white px-2">docker compose up -d</code>
|
||||
</p>
|
||||
|
||||
<.code_block
|
||||
id="code-sample-docker1"
|
||||
class="w-full text-xs whitespace-pre-line"
|
||||
phx-no-format
|
||||
phx-update="ignore"
|
||||
><%= docker_compose(@env) %></.code_block>
|
||||
|
||||
<p class="p-6 pt-0">
|
||||
<strong>Important:</strong>
|
||||
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</.link>.
|
||||
</p>
|
||||
</:tab>
|
||||
<: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=<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=<name-of-gateway>
|
||||
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)
|
||||
|
||||
211
elixir/apps/web/lib/web/live/temp_accounts/index.ex
Normal file
211
elixir/apps/web/lib/web/live/temp_accounts/index.ex
Normal file
@@ -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"""
|
||||
<section>
|
||||
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto lg:py-0">
|
||||
<.hero_logo text="Welcome to Firezone" />
|
||||
|
||||
<div class="w-full col-span-6 mx-auto bg-white rounded shadow md:mt-0 sm:max-w-lg xl:p-0">
|
||||
<div class="p-6 space-y-4 lg:space-y-6 sm:p-8">
|
||||
<.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}
|
||||
/>
|
||||
<div :if={@creation_error}>
|
||||
Something went wrong!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
|
||||
def welcome(assigns) do
|
||||
~H"""
|
||||
<div class="text-center">
|
||||
<p class="mb-4">
|
||||
Interested in trying out Firezone? You've come to the right place!
|
||||
</p>
|
||||
|
||||
<p class="mb-4">
|
||||
Take Firezone for a spin with a temporary demo account.
|
||||
</p>
|
||||
<div class="py-1">
|
||||
<.button class="w-full" phx-click="start">Create Temporary Account</.button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center my-6">
|
||||
<div class="w-full h-0.5 bg-neutral-200"></div>
|
||||
<div class="px-5 text-center text-neutral-500">or</div>
|
||||
<div class="w-full h-0.5 bg-neutral-200"></div>
|
||||
</div>
|
||||
|
||||
<p class="mb-4">
|
||||
Sign up below with a free starter account to keep your data.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
class="inline-block bg-white border rounded px-3 py-2 text-sm w-full"
|
||||
href={url(~p"/sign_up")}
|
||||
>
|
||||
Create Free Starter Account
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :account, :any
|
||||
attr :password, :string
|
||||
|
||||
def account(assigns) do
|
||||
~H"""
|
||||
<div class="space-y-6">
|
||||
<div class="text-center text-neutral-900 text-xl">
|
||||
Your temporary account has been created!
|
||||
</div>
|
||||
<.flash kind={:warning}>
|
||||
<p class="flex items-center gap-1.5 text-sm font-semibold leading-6">
|
||||
<span class="hero-exclamation-triangle h-4 w-4"></span> Warning!
|
||||
</p>
|
||||
<div>Please save the following information, it will not be displayed again.</div>
|
||||
</.flash>
|
||||
<div class="text-center">
|
||||
<div>
|
||||
<.code_block
|
||||
id="code-sample-systemd0"
|
||||
class="w-full text-xs whitespace-pre-line rounded"
|
||||
phx-no-format
|
||||
><%= account_details(@account, @password) %></.code_block>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-base text-center text-neutral-900">
|
||||
<.link class={button_style("primary") ++ ["py-2"]} navigate={~p"/#{@account}"}>
|
||||
Sign In
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -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: []
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user