mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
refactor(portal): Update IDP creation flow (#4984)
Why: * The new flow for creating an identity provider in Firezone allows the user to not have to worry what features their plan has enabled. It will allow the user to select which identity provider they use and will take them to the appropriate form depending on the features they have enabled on their plan. ## Screenshots ### Selecting an identity provider <img width="937" alt="Screenshot 2024-05-14 at 11 53 17 AM" src="https://github.com/firezone/firezone/assets/2646332/31337ad9-13c8-43a2-942c-adb0a951167c"> ### New OIDC form when a custom provider is selected but IDP sync is not enabled for account <img width="903" alt="Screenshot 2024-05-14 at 11 54 58 AM" src="https://github.com/firezone/firezone/assets/2646332/2e18d788-60c3-4fad-b749-351559a24aca">
This commit is contained in:
@@ -110,7 +110,8 @@ defmodule Domain.Auth do
|
||||
|> Enum.map(fn adapter ->
|
||||
capabilities = Adapters.fetch_capabilities!(adapter)
|
||||
requires_idp_sync_feature? = capabilities[:default_provisioner] == :custom
|
||||
{adapter, enabled: idp_sync_enabled? or not requires_idp_sync_feature?}
|
||||
enabled_for_account? = idp_sync_enabled? or not requires_idp_sync_feature?
|
||||
{adapter, enabled: enabled_for_account?, sync: requires_idp_sync_feature?}
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
@@ -11,19 +11,19 @@ defmodule Domain.AuthTest do
|
||||
account = Fixtures.Accounts.create_account(features: %{idp_sync: true})
|
||||
|
||||
assert Enum.sort(all_user_provisioned_provider_adapters!(account)) == [
|
||||
google_workspace: [enabled: true],
|
||||
microsoft_entra: [enabled: true],
|
||||
okta: [enabled: true],
|
||||
openid_connect: [enabled: true]
|
||||
google_workspace: [enabled: true, sync: true],
|
||||
microsoft_entra: [enabled: true, sync: true],
|
||||
okta: [enabled: true, sync: true],
|
||||
openid_connect: [enabled: true, sync: false]
|
||||
]
|
||||
|
||||
account = Fixtures.Accounts.create_account(features: %{idp_sync: false})
|
||||
|
||||
assert Enum.sort(all_user_provisioned_provider_adapters!(account)) == [
|
||||
google_workspace: [enabled: false],
|
||||
microsoft_entra: [enabled: false],
|
||||
okta: [enabled: false],
|
||||
openid_connect: [enabled: true]
|
||||
google_workspace: [enabled: false, sync: true],
|
||||
microsoft_entra: [enabled: false, sync: true],
|
||||
okta: [enabled: false, sync: true],
|
||||
openid_connect: [enabled: true, sync: false]
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,6 +18,10 @@ defmodule Web.Settings.IdentityProviders.New do
|
||||
{:noreply, push_navigate(socket, to: next)}
|
||||
end
|
||||
|
||||
def handle_event("next_step", %{"next" => next}, socket) do
|
||||
{:noreply, push_navigate(socket, to: next_step_path(next, socket.assigns.account))}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.breadcrumbs account={@account}>
|
||||
@@ -32,135 +36,95 @@ defmodule Web.Settings.IdentityProviders.New do
|
||||
<:title>
|
||||
Add a new Identity Provider
|
||||
</:title>
|
||||
<:help>
|
||||
Set up SSO authentication using your own identity provider. Directory sync
|
||||
also available for certain providers. <br /> Learn more about
|
||||
<.website_link href="/kb/authenticate/oidc">SSO authentication</.website_link>
|
||||
and
|
||||
<.website_link href="/kb/authenticate/directory-sync">directory sync</.website_link>
|
||||
in our docs.
|
||||
</:help>
|
||||
<:content>
|
||||
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
|
||||
<h2 class="mb-4 text-xl text-neutral-900">Choose type</h2>
|
||||
<.form id="identity-provider-type-form" for={%{}} phx-submit="submit">
|
||||
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
|
||||
<fieldset>
|
||||
<legend class="sr-only">Identity Provider Type</legend>
|
||||
|
||||
<.adapter
|
||||
:for={{adapter, opts} <- @adapters}
|
||||
adapter={adapter}
|
||||
opts={opts}
|
||||
account={@account}
|
||||
/>
|
||||
</fieldset>
|
||||
<div class="container mx-auto">
|
||||
<div class="max-w-sm mb-8 mx-auto">
|
||||
<div class="flex flex-col gap-4">
|
||||
<%= for {adapter, opts} <- @adapters, opts[:sync] == true do %>
|
||||
<.provider_card adapter={adapter} opts={opts} account={@account} />
|
||||
<% end %>
|
||||
<%= for {adapter, opts} <- @adapters, opts[:sync] == false do %>
|
||||
<.provider_card adapter={adapter} opts={opts} account={@account} />
|
||||
<% end %>
|
||||
</div>
|
||||
<.submit_button>
|
||||
Next: Configure
|
||||
</.submit_button>
|
||||
</.form>
|
||||
</div>
|
||||
</div>
|
||||
</:content>
|
||||
</.section>
|
||||
"""
|
||||
end
|
||||
|
||||
def adapter(%{adapter: :google_workspace} = assigns) do
|
||||
~H"""
|
||||
<.adapter_item
|
||||
adapter={@adapter}
|
||||
account={@account}
|
||||
opts={@opts}
|
||||
name="Google Workspace"
|
||||
description="Authenticate users and synchronize users and groups with a custom Google Workspace connector."
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
def adapter(%{adapter: :microsoft_entra} = assigns) do
|
||||
~H"""
|
||||
<.adapter_item
|
||||
adapter={@adapter}
|
||||
account={@account}
|
||||
opts={@opts}
|
||||
name="Microsoft Entra"
|
||||
description="Authenticate users and synchronize users and groups with a custom Microsoft Entra ID connector."
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
def adapter(%{adapter: :okta} = assigns) do
|
||||
~H"""
|
||||
<.adapter_item
|
||||
adapter={@adapter}
|
||||
account={@account}
|
||||
opts={@opts}
|
||||
name="Okta"
|
||||
description="Authenticate users and synchronize users and groups with a custom Okta connector."
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
def adapter(%{adapter: :openid_connect} = assigns) do
|
||||
~H"""
|
||||
<.adapter_item
|
||||
adapter={@adapter}
|
||||
account={@account}
|
||||
opts={@opts}
|
||||
name="OpenID Connect"
|
||||
description="Authenticate users with a universal OpenID Connect adapter and manager users and groups manually."
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :adapter, :any
|
||||
attr :account, :any
|
||||
attr :opts, :any
|
||||
attr :name, :string
|
||||
attr :description, :string
|
||||
|
||||
def adapter_item(assigns) do
|
||||
def provider_card(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<div class="flex items-center mb-4">
|
||||
<input
|
||||
id={"idp-option-#{@adapter}"}
|
||||
type="radio"
|
||||
name="next"
|
||||
value={next_step_path(@adapter, @account)}
|
||||
class={[
|
||||
"w-4 h-4 border-neutral-300",
|
||||
@opts[:enabled] == false && "cursor-not-allowed"
|
||||
]}
|
||||
disabled={@opts[:enabled] == false}
|
||||
required
|
||||
/>
|
||||
<.provider_icon adapter={@adapter} class="w-8 h-8 ml-4" />
|
||||
<label for={"idp-option-#{@adapter}"} class="block ml-2 text-lg text-neutral-900">
|
||||
<%= @name %>
|
||||
</label>
|
||||
|
||||
<%= if @opts[:enabled] == false do %>
|
||||
<.link navigate={~p"/#{@account}/settings/billing"} class="ml-2 text-sm text-primary-500">
|
||||
<.badge class="ml-2" type="primary" title="Feature available on a higher pricing plan">
|
||||
<.icon name="hero-lock-closed" class="w-3.5 h-3.5 mr-1" /> UPGRADE TO UNLOCK
|
||||
</.badge>
|
||||
</.link>
|
||||
<% end %>
|
||||
<div
|
||||
id={"idp-option-#{@adapter}"}
|
||||
class={[
|
||||
"component bg-white rounded",
|
||||
"px-4 py-2 flex items-center",
|
||||
"cursor-pointer hover:bg-gray-50",
|
||||
"border border-neutral-200"
|
||||
]}
|
||||
phx-click="next_step"
|
||||
phx-value-next={@adapter}
|
||||
>
|
||||
<div class="w-full">
|
||||
<.provider_icon adapter={@adapter} class="w-10 h-10 inline-block mr-2" />
|
||||
<span class="inline-block"><%= pretty_print_provider(@adapter) %></span>
|
||||
</div>
|
||||
|
||||
<div :if={@opts[:sync] == true} class="w-1/2 flex justify-end">
|
||||
<.icon name="hero-arrow-path" class="w-5 h-5 text-neutral-400" />
|
||||
</div>
|
||||
<p class="ml-6 mb-6 text-sm text-neutral-500">
|
||||
<%= @description %>
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def next_step_path(:openid_connect, account) do
|
||||
def next_step_path("openid_connect", account) do
|
||||
~p"/#{account}/settings/identity_providers/openid_connect/new"
|
||||
end
|
||||
|
||||
def next_step_path(:google_workspace, account) do
|
||||
~p"/#{account}/settings/identity_providers/google_workspace/new"
|
||||
def next_step_path("google_workspace" = provider, account) do
|
||||
if Domain.Accounts.idp_sync_enabled?(account) do
|
||||
~p"/#{account}/settings/identity_providers/google_workspace/new"
|
||||
else
|
||||
~p"/#{account}/settings/identity_providers/openid_connect/new?provider=#{provider}"
|
||||
end
|
||||
end
|
||||
|
||||
def next_step_path(:microsoft_entra, account) do
|
||||
~p"/#{account}/settings/identity_providers/microsoft_entra/new"
|
||||
def next_step_path("microsoft_entra" = provider, account) do
|
||||
if Domain.Accounts.idp_sync_enabled?(account) do
|
||||
~p"/#{account}/settings/identity_providers/microsoft_entra/new"
|
||||
else
|
||||
~p"/#{account}/settings/identity_providers/openid_connect/new?provider=#{provider}"
|
||||
end
|
||||
end
|
||||
|
||||
def next_step_path(:okta, account) do
|
||||
~p"/#{account}/settings/identity_providers/okta/new"
|
||||
def next_step_path("okta" = provider, account) do
|
||||
if Domain.Accounts.idp_sync_enabled?(account) do
|
||||
~p"/#{account}/settings/identity_providers/okta/new"
|
||||
else
|
||||
~p"/#{account}/settings/identity_providers/openid_connect/new?provider=#{provider}"
|
||||
end
|
||||
end
|
||||
|
||||
def pretty_print_provider(adapter) do
|
||||
case adapter do
|
||||
:openid_connect -> "OpenID Connect"
|
||||
:google_workspace -> "Google Workspace"
|
||||
:microsoft_entra -> "Microsoft EntraID"
|
||||
:okta -> "Okta"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
defmodule Web.Settings.IdentityProviders.OpenIDConnect.Components do
|
||||
use Web, :component_library
|
||||
|
||||
attr :id, :string
|
||||
attr :account, :any
|
||||
attr :form, :any
|
||||
attr :show_sync_msg, :boolean, default: false
|
||||
|
||||
def provider_form(assigns) do
|
||||
~H"""
|
||||
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-12">
|
||||
@@ -95,12 +100,47 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.Components do
|
||||
</div>
|
||||
</.inputs_for>
|
||||
</div>
|
||||
|
||||
<.submit_button>
|
||||
Connect Identity Provider
|
||||
</.submit_button>
|
||||
</:content>
|
||||
</.step>
|
||||
<.step :if={@show_sync_msg}>
|
||||
<:title>
|
||||
Step 3. Enable Directory Sync <.icon name="hero-arrow-path" class="w-5 h-5 ml-2" />
|
||||
<span class="inline-flex w-1/2 justify-end">
|
||||
<.link
|
||||
navigate={~p"/#{@account}/settings/billing"}
|
||||
class="text-sm text-primary-500 ml-2"
|
||||
>
|
||||
<.badge type="primary" title="Feature available on a higher pricing plan">
|
||||
<.icon name="hero-lock-closed" class="w-3.5 h-3.5 mr-1" /> UPGRADE TO UNLOCK
|
||||
</.badge>
|
||||
</.link>
|
||||
</span>
|
||||
</:title>
|
||||
<:content>
|
||||
Directory sync is not enabled on your current plan.
|
||||
<div class="blur-sm">
|
||||
<p class="mb-4">
|
||||
Ensure the following scopes are added to the OAuth application:
|
||||
</p>
|
||||
<.code_block
|
||||
id="scope-fake"
|
||||
class="w-full text-xs mb-4 whitespace-pre-line rounded"
|
||||
phx-no-format
|
||||
><%= "placeholder\nscopes\nfor\nsync" %></.code_block>
|
||||
<p class="mb-4">
|
||||
Placeholder for any additional instructions here:
|
||||
</p>
|
||||
<.code_block
|
||||
id="placeholder-instructions"
|
||||
class="w-full text-xs mb-4 whitespace-pre-line rounded"
|
||||
phx-no-format
|
||||
>placeholder instructions here</.code_block>
|
||||
</div>
|
||||
</:content>
|
||||
</.step>
|
||||
<.submit_button>
|
||||
Connect Identity Provider
|
||||
</.submit_button>
|
||||
</.form>
|
||||
</div>
|
||||
"""
|
||||
|
||||
@@ -3,7 +3,7 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.New do
|
||||
import Web.Settings.IdentityProviders.OpenIDConnect.Components
|
||||
alias Domain.Auth
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
def mount(params, _session, socket) do
|
||||
id = Ecto.UUID.generate()
|
||||
account = socket.assigns.account
|
||||
|
||||
@@ -17,7 +17,8 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.New do
|
||||
assign(socket,
|
||||
id: id,
|
||||
form: to_form(changeset),
|
||||
page_title: "New Identity Provider"
|
||||
page_title: "New Identity Provider",
|
||||
provider: params["provider"]
|
||||
)
|
||||
|
||||
{:ok, socket, temporary_assigns: [form: %Phoenix.HTML.Form{}]}
|
||||
@@ -41,7 +42,7 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.New do
|
||||
Add a new OpenID Connect Identity Provider
|
||||
</:title>
|
||||
<:content>
|
||||
<.provider_form account={@account} id={@id} form={@form} />
|
||||
<.provider_form account={@account} id={@id} form={@form} show_sync_msg={!!@provider} />
|
||||
</:content>
|
||||
</.section>
|
||||
"""
|
||||
|
||||
@@ -44,8 +44,6 @@ defmodule Web.Live.Settings.IdentityProviders.NewTest do
|
||||
|
||||
assert has_element?(lv, "#idp-option-google_workspace")
|
||||
assert html =~ "Google Workspace"
|
||||
assert html =~ "Feature available on a higher pricing plan"
|
||||
assert html =~ "UPGRADE TO UNLOCK"
|
||||
|
||||
assert has_element?(lv, "#idp-option-microsoft_entra")
|
||||
assert html =~ "Microsoft Entra"
|
||||
@@ -56,4 +54,53 @@ defmodule Web.Live.Settings.IdentityProviders.NewTest do
|
||||
assert has_element?(lv, "#idp-option-openid_connect")
|
||||
assert html =~ "OpenID Connect"
|
||||
end
|
||||
|
||||
test "next step for non-idp-sync plans is OIDC form", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/identity_providers/new")
|
||||
|
||||
lv
|
||||
|> element("#idp-option-google_workspace")
|
||||
|> render_click()
|
||||
|
||||
assert_redirect(
|
||||
lv,
|
||||
~p"/#{account}/settings/identity_providers/openid_connect/new?provider=google_workspace"
|
||||
)
|
||||
end
|
||||
|
||||
test "next step for idp-sync plans is to custom adapter form", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
Domain.Config.feature_flag_override(:idp_sync, true)
|
||||
|
||||
{:ok, account} =
|
||||
Domain.Accounts.update_account(account, %{
|
||||
features: %{
|
||||
idp_sync: true
|
||||
}
|
||||
})
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/identity_providers/new")
|
||||
|
||||
lv
|
||||
|> element("#idp-option-google_workspace")
|
||||
|> render_click()
|
||||
|
||||
assert_redirect(
|
||||
lv,
|
||||
~p"/#{account}/settings/identity_providers/google_workspace/new"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,7 +35,7 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.NewTest do
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} =
|
||||
{:ok, lv, html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/identity_providers/openid_connect/new")
|
||||
@@ -51,6 +51,36 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.NewTest do
|
||||
"provider[adapter_config][scope]",
|
||||
"provider[name]"
|
||||
]
|
||||
|
||||
refute html =~ "Enable Directory Sync"
|
||||
end
|
||||
|
||||
test "renders provider creation form with blurred sync step", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(
|
||||
~p"/#{account}/settings/identity_providers/openid_connect/new?provider=google_workspace"
|
||||
)
|
||||
|
||||
form = form(lv, "form")
|
||||
|
||||
assert find_inputs(form) == [
|
||||
"provider[adapter_config][_persistent_id]",
|
||||
"provider[adapter_config][client_id]",
|
||||
"provider[adapter_config][client_secret]",
|
||||
"provider[adapter_config][discovery_document_uri]",
|
||||
"provider[adapter_config][response_type]",
|
||||
"provider[adapter_config][scope]",
|
||||
"provider[name]"
|
||||
]
|
||||
|
||||
assert html =~ "Enable Directory Sync"
|
||||
assert html =~ "UPGRADE TO UNLOCK"
|
||||
end
|
||||
|
||||
test "creates a new provider on valid attrs", %{
|
||||
|
||||
Reference in New Issue
Block a user