From fcb828e912025503aa84012f37e0db8e6f5e7234 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:19:18 +0100 Subject: [PATCH] properly handle step count change --- .../LocationCardMfaEdit/LocationCardMfaEdit.tsx | 6 +++++- new-ui/src/shared/utils/mfa.test.ts | 9 +++++++++ new-ui/src/shared/utils/mfa.ts | 9 +++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/new-ui/src/shared/components/LocationCard/components/LocationCardMfaEdit/LocationCardMfaEdit.tsx b/new-ui/src/shared/components/LocationCard/components/LocationCardMfaEdit/LocationCardMfaEdit.tsx index eaecac8ff..cacaffc2c 100644 --- a/new-ui/src/shared/components/LocationCard/components/LocationCardMfaEdit/LocationCardMfaEdit.tsx +++ b/new-ui/src/shared/components/LocationCard/components/LocationCardMfaEdit/LocationCardMfaEdit.tsx @@ -4,6 +4,7 @@ import { TooltipContent } from '../../../../providers/tooltip/TooltipContent'; import { TooltipProvider } from '../../../../providers/tooltip/TooltipContext'; import { TooltipTrigger } from '../../../../providers/tooltip/TooltipTrigger'; import type { InstanceInfo, LocationInfo } from '../../../../rust-api/types'; +import { isPresent } from '../../../../utils/isPresent'; import { ConnectionAbility, type ConnectionAbilityValue, @@ -38,10 +39,13 @@ export const LocationCardMfaEdit = ({ connectionAbility, }: Props) => { const stepCount = mfaStepCount(location); + const [method] = resolveMfaStepPlan(location, [], instance); const label = stepCount > 1 ? mfaStepsToText(stepCount) - : mfaToText(resolveMfaStepPlan(location, [], instance)[0], instance); + : isPresent(method) + ? mfaToText(method, instance) + : 'Unavailable'; const canEdit = connectionAbility === ConnectionAbility.Available && diff --git a/new-ui/src/shared/utils/mfa.test.ts b/new-ui/src/shared/utils/mfa.test.ts index 2054ab9a6..c35f45cea 100644 --- a/new-ui/src/shared/utils/mfa.test.ts +++ b/new-ui/src/shared/utils/mfa.test.ts @@ -291,6 +291,15 @@ describe('resolveMfaStepPlan', () => { expect(resolveMfaStepPlan(location)).toEqual([MfaMethod.Email]); expect(resolveMfaStepPlan(location, [], instance)).toEqual([MfaMethod.Totp]); }); + + it('is empty when a step offers no factor', () => { + const location = { + ...locationOf([step(MfaMethod.Totp), step()]), + mfa_step_plan: [MfaMethod.Totp, MfaMethod.Fido2], + }; + expect(resolveMfaStepPlan(location)).toEqual([]); + expect(connectionAbilityOf(location)).toBe(ConnectionAbility.Unavailable); + }); }); const instanceOf = (id: number, overrides: Partial = {}): InstanceInfo => ({ diff --git a/new-ui/src/shared/utils/mfa.ts b/new-ui/src/shared/utils/mfa.ts index 0942476c3..b5e3d82a6 100644 --- a/new-ui/src/shared/utils/mfa.ts +++ b/new-ui/src/shared/utils/mfa.ts @@ -143,8 +143,12 @@ export const resolveMfaStepPlan = ( location: Pick, oneOffPlan: MfaMethodValue[] = [], instance?: MfaUsabilityInstance, -): MfaMethodValue[] => - mfaStepsOf(location).map((step, index) => { +): MfaMethodValue[] => { + const steps = mfaStepsOf(location); + // Core keeps a step whose methods it filtered out (FIDO2 without a license), and no plan passes it. + if (steps.some((step) => step.methods.length === 0)) return []; + + return steps.map((step, index) => { const usableMethods = usableMfaMethods(step, instance); const isUsable = (method: MfaMethodValue) => usableMethods.some((entry) => entry.method === method); @@ -157,6 +161,7 @@ export const resolveMfaStepPlan = ( return (usableMethods[0] ?? step.methods[0]).method; }); +}; /** * Whether the user has this factor set up. The instance's own report of the