mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
feat(portal): Add sign up override in portal (#3739)
Why: * In order to allow easy testing of billing / Stripe integration, the staging environment needs to allow members of the Firezone team access to create new accounts, while disallowing the general public to create accounts. The account creation override functionality allows for multiple domains to be set by ENV variable by passing a comma separated string of domains. --------- Co-authored-by: Andrew Dryga <andrew@dryga.com>
This commit is contained in:
@@ -562,6 +562,18 @@ defmodule Domain.Config.Definitions do
|
||||
"""
|
||||
defconfig(:feature_sign_up_enabled, :boolean, default: true)
|
||||
|
||||
@doc """
|
||||
List of email domains allowed to signup from. Leave empty to allow signing up from any domain.
|
||||
"""
|
||||
defconfig(:sign_up_whitelisted_domains, {:array, ",", :string},
|
||||
default: [],
|
||||
changeset: fn changeset, key ->
|
||||
changeset
|
||||
|> Ecto.Changeset.validate_required(key)
|
||||
|> Domain.Repo.Changeset.validate_fqdn(key)
|
||||
end
|
||||
)
|
||||
|
||||
@doc """
|
||||
Boolean flag to turn IdP sync on/off for all accounts.
|
||||
"""
|
||||
|
||||
@@ -314,6 +314,65 @@ defmodule Domain.Repo.Changeset do
|
||||
end)
|
||||
end
|
||||
|
||||
def validate_fqdn(changeset, field, opts \\ []) do
|
||||
allow_port = Keyword.get(opts, :allow_port, false)
|
||||
|
||||
validate_change(changeset, field, fn _current_field, value ->
|
||||
{fqdn, port} = split_port(value)
|
||||
fqdn_validation_errors = fqdn_validation_errors(field, fqdn)
|
||||
port_validation_errors = port_validation_errors(field, port, allow_port)
|
||||
fqdn_validation_errors ++ port_validation_errors
|
||||
end)
|
||||
end
|
||||
|
||||
defp fqdn_validation_errors(field, fqdn) do
|
||||
if Regex.match?(~r/^([a-zA-Z0-9._-])+$/i, fqdn) do
|
||||
[]
|
||||
else
|
||||
[{field, "#{fqdn} is not a valid FQDN"}]
|
||||
end
|
||||
end
|
||||
|
||||
defp split_port(value) do
|
||||
case String.split(value, ":", parts: 2) do
|
||||
[prefix, port] ->
|
||||
case Integer.parse(port) do
|
||||
{port, ""} ->
|
||||
{prefix, port}
|
||||
|
||||
_ ->
|
||||
{value, nil}
|
||||
end
|
||||
|
||||
[value] ->
|
||||
{value, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp port_validation_errors(_field, nil, _allow?),
|
||||
do: []
|
||||
|
||||
defp port_validation_errors(field, _port, false),
|
||||
do: [{field, "setting port is not allowed"}]
|
||||
|
||||
defp port_validation_errors(_field, port, _allow?) when 0 < port and port <= 65_535,
|
||||
do: []
|
||||
|
||||
defp port_validation_errors(field, _port, _allow?),
|
||||
do: [{field, "port is not a number between 0 and 65535"}]
|
||||
|
||||
def validate_ip_type_inclusion(changeset, field, types) do
|
||||
validate_change(changeset, field, fn _current_field, %{address: address} ->
|
||||
type = if tuple_size(address) == 4, do: :ipv4, else: :ipv6
|
||||
|
||||
if type in types do
|
||||
[]
|
||||
else
|
||||
[{field, "is not a supported IP type"}]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# Polymorphic embeds
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -5,7 +5,7 @@ defmodule Domain.Fixtures.Auth do
|
||||
def user_password, do: "Hello w0rld!"
|
||||
def remote_ip, do: {100, 64, 100, 58}
|
||||
def user_agent, do: "iOS/12.5 (iPhone) connlib/0.7.412"
|
||||
def email, do: "user-#{unique_integer()}@example.com"
|
||||
def email(domain \\ "example.com"), do: "user-#{unique_integer()}@#{domain}"
|
||||
|
||||
def random_provider_identifier(%Domain.Auth.Provider{adapter: :email, name: name}) do
|
||||
"user-#{unique_integer()}@#{String.downcase(name)}.com"
|
||||
|
||||
@@ -17,10 +17,13 @@ defmodule Web.SignUp do
|
||||
end
|
||||
|
||||
def changeset(attrs) do
|
||||
whitelisted_domains = Domain.Config.get_env(:domain, :sign_up_whitelisted_domains)
|
||||
|
||||
%Registration{}
|
||||
|> Ecto.Changeset.cast(attrs, [:email])
|
||||
|> Ecto.Changeset.validate_required([:email])
|
||||
|> Ecto.Changeset.validate_format(:email, ~r/.+@.+/)
|
||||
|> validate_email_allowed(whitelisted_domains)
|
||||
|> Ecto.Changeset.validate_confirmation(:email,
|
||||
required: true,
|
||||
message: "email does not match"
|
||||
@@ -32,11 +35,32 @@ defmodule Web.SignUp do
|
||||
with: fn _account, attrs -> Actors.Actor.Changeset.create(attrs) end
|
||||
)
|
||||
end
|
||||
|
||||
defp validate_email_allowed(changeset, []) do
|
||||
changeset
|
||||
end
|
||||
|
||||
defp validate_email_allowed(changeset, whitelisted_domains) do
|
||||
Ecto.Changeset.validate_change(changeset, :email, fn :email, email ->
|
||||
if email_allowed?(email, whitelisted_domains),
|
||||
do: [],
|
||||
else: [email: "email domain is not allowed at this time"]
|
||||
end)
|
||||
end
|
||||
|
||||
defp email_allowed?(email, whitelisted_domains) do
|
||||
with [_, domain] <- String.split(email, "@", parts: 2) do
|
||||
Enum.member?(whitelisted_domains, domain)
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
user_agent = Phoenix.LiveView.get_connect_info(socket, :user_agent)
|
||||
real_ip = Web.Auth.real_ip(socket)
|
||||
sign_up_enabled? = Config.sign_up_enabled?()
|
||||
|
||||
changeset =
|
||||
Registration.changeset(%{
|
||||
@@ -46,15 +70,15 @@ defmodule Web.SignUp do
|
||||
|
||||
socket =
|
||||
assign(socket,
|
||||
page_title: "Sign Up",
|
||||
form: to_form(changeset),
|
||||
account: nil,
|
||||
provider: nil,
|
||||
user_agent: user_agent,
|
||||
real_ip: real_ip,
|
||||
sign_up_enabled?: Config.sign_up_enabled?(),
|
||||
sign_up_enabled?: sign_up_enabled?,
|
||||
account_name_changed?: false,
|
||||
actor_name_changed?: false,
|
||||
page_title: "Sign Up"
|
||||
actor_name_changed?: false
|
||||
)
|
||||
|
||||
{:ok, socket, temporary_assigns: [form: %Phoenix.HTML.Form{}]}
|
||||
@@ -290,49 +314,10 @@ defmodule Web.SignUp do
|
||||
|> Registration.changeset()
|
||||
|> Map.put(:action, :insert)
|
||||
|
||||
if changeset.valid? && socket.assigns.sign_up_enabled? do
|
||||
if changeset.valid? and socket.assigns.sign_up_enabled? do
|
||||
registration = Ecto.Changeset.apply_changes(changeset)
|
||||
|
||||
multi =
|
||||
Ecto.Multi.new()
|
||||
|> Ecto.Multi.run(
|
||||
:account,
|
||||
fn _repo, _changes ->
|
||||
Accounts.create_account(%{
|
||||
name: registration.account.name
|
||||
})
|
||||
end
|
||||
)
|
||||
|> Ecto.Multi.run(
|
||||
:provider,
|
||||
fn _repo, %{account: account} ->
|
||||
Auth.create_provider(account, %{
|
||||
name: "Email",
|
||||
adapter: :email,
|
||||
adapter_config: %{}
|
||||
})
|
||||
end
|
||||
)
|
||||
|> Ecto.Multi.run(
|
||||
:actor,
|
||||
fn _repo, %{account: account} ->
|
||||
Actors.create_actor(account, %{
|
||||
type: :account_admin_user,
|
||||
name: registration.actor.name
|
||||
})
|
||||
end
|
||||
)
|
||||
|> Ecto.Multi.run(
|
||||
:identity,
|
||||
fn _repo, %{actor: actor, provider: provider} ->
|
||||
Auth.create_identity(actor, provider, %{
|
||||
provider_identifier: registration.email,
|
||||
provider_identifier_confirmation: registration.email
|
||||
})
|
||||
end
|
||||
)
|
||||
|
||||
case Domain.Repo.transaction(multi) do
|
||||
case register_account(registration) do
|
||||
{:ok, %{account: account, provider: provider, identity: identity}} ->
|
||||
{:ok, account} = Domain.Billing.provision_account(account)
|
||||
|
||||
@@ -385,4 +370,47 @@ defmodule Web.SignUp do
|
||||
[default_name | _] = String.split(attrs["email"], "@", parts: 2)
|
||||
put_in(attrs, ["actor", "name"], default_name)
|
||||
end
|
||||
|
||||
defp register_account(registration) do
|
||||
multi =
|
||||
Ecto.Multi.new()
|
||||
|> Ecto.Multi.run(
|
||||
:account,
|
||||
fn _repo, _changes ->
|
||||
Accounts.create_account(%{
|
||||
name: registration.account.name
|
||||
})
|
||||
end
|
||||
)
|
||||
|> Ecto.Multi.run(
|
||||
:provider,
|
||||
fn _repo, %{account: account} ->
|
||||
Auth.create_provider(account, %{
|
||||
name: "Email",
|
||||
adapter: :email,
|
||||
adapter_config: %{}
|
||||
})
|
||||
end
|
||||
)
|
||||
|> Ecto.Multi.run(
|
||||
:actor,
|
||||
fn _repo, %{account: account} ->
|
||||
Actors.create_actor(account, %{
|
||||
type: :account_admin_user,
|
||||
name: registration.actor.name
|
||||
})
|
||||
end
|
||||
)
|
||||
|> Ecto.Multi.run(
|
||||
:identity,
|
||||
fn _repo, %{actor: actor, provider: provider} ->
|
||||
Auth.create_identity(actor, provider, %{
|
||||
provider_identifier: registration.email,
|
||||
provider_identifier_confirmation: registration.email
|
||||
})
|
||||
end
|
||||
)
|
||||
|
||||
Domain.Repo.transaction(multi)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -68,6 +68,70 @@ defmodule Web.Live.SignUpTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "allows whitelisted domains to create new account", %{conn: conn} do
|
||||
whitelisted_domain = "example.com"
|
||||
Domain.Config.put_env_override(:outbound_email_adapter_configured?, true)
|
||||
Domain.Config.put_env_override(:sign_up_whitelisted_domains, [whitelisted_domain])
|
||||
|
||||
account_name = "FooBar"
|
||||
|
||||
attrs = %{
|
||||
account: %{name: account_name},
|
||||
actor: %{name: "John Doe"},
|
||||
email: Fixtures.Auth.email(whitelisted_domain)
|
||||
}
|
||||
|
||||
Bypass.open()
|
||||
|> Domain.Mocks.Stripe.mock_create_customer_endpoint(%{
|
||||
id: Ecto.UUID.generate(),
|
||||
name: account_name
|
||||
})
|
||||
|> Domain.Mocks.Stripe.mock_create_subscription_endpoint()
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/sign_up")
|
||||
|
||||
html =
|
||||
lv
|
||||
|> form("form", registration: attrs)
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "Your account has been created!"
|
||||
assert html =~ account_name
|
||||
end
|
||||
|
||||
test "does not show account creation form when sign ups are disabled", %{conn: conn} do
|
||||
Domain.Config.put_env_override(:outbound_email_adapter_configured?, true)
|
||||
Domain.Config.feature_flag_override(:sign_up, false)
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/sign_up")
|
||||
refute has_element?(lv, "form")
|
||||
end
|
||||
|
||||
test "does not allow to create account from not whitelisted domain", %{conn: conn} do
|
||||
Domain.Config.put_env_override(:outbound_email_adapter_configured?, true)
|
||||
Domain.Config.feature_flag_override(:sign_up, true)
|
||||
Domain.Config.put_env_override(:sign_up_whitelisted_domains, ["firezone.dev"])
|
||||
|
||||
account_name = "FooBar"
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/sign_up")
|
||||
|
||||
email = Fixtures.Auth.email()
|
||||
|
||||
attrs = %{
|
||||
account: %{name: account_name},
|
||||
actor: %{name: "John Doe"},
|
||||
email: email
|
||||
}
|
||||
|
||||
assert html =
|
||||
lv
|
||||
|> form("form", registration: attrs)
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "email domain is not allowed at this time"
|
||||
end
|
||||
|
||||
test "renders changeset errors on input change", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/sign_up")
|
||||
|
||||
@@ -108,8 +172,19 @@ defmodule Web.Live.SignUpTest do
|
||||
}
|
||||
end
|
||||
|
||||
test "renders changeset errors on submit when sign up disabled", %{
|
||||
conn: conn
|
||||
} do
|
||||
Domain.Config.feature_flag_override(:sign_up, false)
|
||||
Domain.Config.put_env_override(:sign_up_whitelisted_domains, ["foo.com"])
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/sign_up")
|
||||
assert html =~ "Sign-ups are currently disabled."
|
||||
end
|
||||
|
||||
test "renders sign up disabled message", %{conn: conn} do
|
||||
Domain.Config.feature_flag_override(:sign_up, false)
|
||||
Domain.Config.put_env_override(:sign_up_whitelisted_domains, [])
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/sign_up")
|
||||
|
||||
|
||||
@@ -87,6 +87,8 @@ config :domain, :enabled_features,
|
||||
self_hosted_relays: true,
|
||||
multi_site_resources: true
|
||||
|
||||
config :domain, sign_up_whitelisted_domains: []
|
||||
|
||||
config :domain, docker_registry: "us-east1-docker.pkg.dev/firezone-staging/firezone"
|
||||
|
||||
config :domain, outbound_email_adapter_configured?: false
|
||||
|
||||
@@ -61,6 +61,8 @@ if config_env() == :prod do
|
||||
self_hosted_relays: compile_config!(:feature_self_hosted_relays_enabled),
|
||||
multi_site_resources: compile_config!(:feature_multi_site_resources_enabled)
|
||||
|
||||
config :domain, sign_up_whitelisted_domains: compile_config!(:sign_up_whitelisted_domains)
|
||||
|
||||
config :domain, docker_registry: compile_config!(:docker_registry)
|
||||
|
||||
config :domain, outbound_email_adapter_configured?: !!compile_config!(:outbound_email_adapter)
|
||||
|
||||
Reference in New Issue
Block a user