fix(portal): Prevent additional email identities per actor (#8888)

This is a UI-only change for now to serve as a stop-gap while we work to
overhaul the identity domain model.

Related: #6294
This commit is contained in:
Jamil
2025-04-22 14:13:37 -07:00
committed by GitHub
parent 8293e6c440
commit 0a2a393d4c
3 changed files with 53 additions and 9 deletions

View File

@@ -8,13 +8,27 @@ defmodule Web.Actors.Show do
def mount(%{"id" => id}, _session, socket) do
with {:ok, actor} <-
Actors.fetch_actor_by_id(id, socket.assigns.subject,
preload: [:last_seen_at, groups: [:provider]]
preload: [:identities, :last_seen_at, groups: [:provider]]
) do
:ok = Clients.subscribe_to_clients_presence_for_actor(actor)
available_providers =
Auth.all_active_providers_for_account!(socket.assigns.account)
|> Enum.filter(fn provider ->
# TODO: This will be refactored to enforce with a DB constraint, but for now
# we don't allow creating multiple identities per actor for the same provider.
Enum.all?(actor.identities, fn identity ->
identity.provider_id != provider.id
end) and
Auth.fetch_provider_capabilities!(provider)
|> Keyword.fetch!(:provisioners)
|> Enum.member?(:manual)
end)
socket =
socket
|> assign(
available_providers: available_providers,
page_title: "Actor #{actor.name}",
flow_activities_enabled?: Accounts.flow_activities_enabled?(socket.assigns.account),
actor: actor
@@ -232,7 +246,7 @@ defmodule Web.Actors.Show do
Each authentication identity is associated with an identity provider and is used to identify the actor upon successful authentication.
</:help>
<:action :if={is_nil(@actor.deleted_at)}>
<:action :if={is_nil(@actor.deleted_at) and Enum.any?(@available_providers)}>
<.add_button
:if={@actor.type != :service_account}
navigate={~p"/#{@account}/actors/users/#{@actor}/new_identity"}
@@ -664,8 +678,27 @@ defmodule Web.Actors.Show do
{:ok, identity} = Auth.fetch_identity_by_id(id, socket.assigns.subject)
{:ok, _identity} = Auth.delete_identity(identity, socket.assigns.subject)
{:ok, actor} =
Actors.fetch_actor_by_id(socket.assigns.actor.id, socket.assigns.subject,
preload: [:identities]
)
available_providers =
Auth.all_active_providers_for_account!(socket.assigns.account)
|> Enum.filter(fn provider ->
# TODO: This will be refactored to enforce with a DB constraint, but for now
# we don't allow creating multiple identities per actor for the same provider.
Enum.all?(actor.identities, fn identity ->
identity.provider_id != provider.id
end) and
Auth.fetch_provider_capabilities!(provider)
|> Keyword.fetch!(:provisioners)
|> Enum.member?(:manual)
end)
socket =
socket
|> assign(actor: actor, available_providers: available_providers)
|> reload_live_table!("identities")
|> put_flash(:info, "Identity was deleted.")

View File

@@ -6,7 +6,7 @@ defmodule Web.Actors.Users.NewIdentity do
def mount(%{"id" => id} = params, _session, socket) do
with {:ok, actor} <-
Actors.fetch_actor_by_id(id, socket.assigns.subject,
preload: [:memberships],
preload: [:memberships, :identities],
filter: [
deleted?: false,
types: ["account_user", "account_admin_user"]
@@ -15,9 +15,14 @@ defmodule Web.Actors.Users.NewIdentity do
providers =
Auth.all_active_providers_for_account!(socket.assigns.account)
|> Enum.filter(fn provider ->
Auth.fetch_provider_capabilities!(provider)
|> Keyword.fetch!(:provisioners)
|> Enum.member?(:manual)
# TODO: This will be refactored to enforce with a DB constraint, but for now
# we don't allow creating multiple identities per actor for the same provider.
Enum.all?(actor.identities, fn identity ->
identity.provider_id != provider.id
end) and
Auth.fetch_provider_capabilities!(provider)
|> Keyword.fetch!(:provisioners)
|> Enum.member?(:manual)
end)
provider = List.first(providers)

View File

@@ -5,16 +5,22 @@ defmodule Web.Live.Actors.User.NewIdentityTest do
Domain.Config.put_env_override(:outbound_email_adapter_configured?, true)
account = Fixtures.Accounts.create_account()
provider = Fixtures.Auth.create_email_provider(account: account)
_provider = Fixtures.Auth.create_email_provider(account: account)
# TODO: Users won't be able to naturally arrive at some of the routes tested on this page without another
# manual provisioning provider like OIDC, so we add it here. Clean this up when identities are refactored.
{oidc_provider, _bypass} =
Fixtures.Auth.start_and_create_openid_connect_provider(account: account)
actor =
Fixtures.Actors.create_actor(
type: :account_admin_user,
account: account,
provider: provider
provider: oidc_provider
)
identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor)
identity =
Fixtures.Auth.create_identity(account: account, provider: oidc_provider, actor: actor)
%{
account: account,