From f0cacd6223626c201753912e2edd6aa44f398795 Mon Sep 17 00:00:00 2001 From: Aaron Campbell <51107902+sleepdotexe@users.noreply.github.com> Date: Fri, 2 Aug 2024 17:39:16 +1000 Subject: [PATCH] Add an Authenticator Assurance Level toggle for impersonating users in Studio (#28332) * Add AAL2 toggle for user impersonation * Move AAL toggle into user selector & tweak styles - Since the AAL level can only be toggled before a user is selected, move the toggle into the user selection area - Also display the AAL status in the UserImpersonatingRow and the RoleImpersonationPopover - Improve display styles * Run prettier * - fixes light mode text issue - adds "Advanced options" collapsible - adds info tooltip linking to MFA docs --------- Co-authored-by: Alaister Young Co-authored-by: Alaister Young --- .../RoleImpersonationPopover.tsx | 7 +- .../UserImpersonationSelector.tsx | 79 +++++++++++++++++-- apps/studio/lib/role-impersonation.ts | 3 +- 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover.tsx b/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover.tsx index 2cdf1ec43a6..e9c6d946901 100644 --- a/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover.tsx +++ b/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover.tsx @@ -41,7 +41,12 @@ const RoleImpersonationPopover = ({ role {currentRole} {state.role?.type === 'postgrest' && state.role.role === 'authenticated' && ( - + <> + + + {state.role.aal === 'aal2' ? 'AAL2' : 'AAL1'} + + )} diff --git a/apps/studio/components/interfaces/RoleImpersonationSelector/UserImpersonationSelector.tsx b/apps/studio/components/interfaces/RoleImpersonationSelector/UserImpersonationSelector.tsx index fe801037454..5f6eb2d0e96 100644 --- a/apps/studio/components/interfaces/RoleImpersonationSelector/UserImpersonationSelector.tsx +++ b/apps/studio/components/interfaces/RoleImpersonationSelector/UserImpersonationSelector.tsx @@ -1,16 +1,27 @@ import { useDebounce } from '@uidotdev/usehooks' +import { ChevronDown, ExternalLink, User as IconUser, Loader2, Search, X } from 'lucide-react' import { useState } from 'react' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import AlertError from 'components/ui/AlertError' import { User, useUsersQuery } from 'data/auth/users-query' -import { User as IconUser, Loader2, Search, X } from 'lucide-react' import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state' -import { Button, Input } from 'ui' +import { + Button, + Collapsible_Shadcn_, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, + Input, + Switch, +} from 'ui' +import { InfoTooltip } from 'ui-patterns/info-tooltip' import { getAvatarUrl, getDisplayName } from '../Auth/Users/UserListItem.utils' +type AuthenticatorAssuranceLevels = 'aal1' | 'aal2' + const UserImpersonationSelector = () => { const [searchText, setSearchText] = useState('') + const [aal, setAal] = useState('aal1') const debouncedSearchText = useDebounce(searchText, 300) const { project } = useProjectContext() @@ -37,6 +48,7 @@ const UserImpersonationSelector = () => { type: 'postgrest', role: 'authenticated', user, + aal, }) } @@ -44,6 +56,10 @@ const UserImpersonationSelector = () => { state.setRole(undefined) } + function toggleAalState() { + setAal((prev) => (prev === 'aal2' ? 'aal1' : 'aal2')) + } + return (

@@ -91,6 +107,48 @@ const UserImpersonationSelector = () => { } /> + + +
+

+ Advanced options +

+ +
+
+ +
+

MFA assurance level

+ +

+ AAL1 verifies users via standard login methods, while AAL2 adds a second + authentication factor. +
+ If you're not using MFA, you can leave this on AAL1. +

+ + Learn more about MFA + +
+
+ +
+

AAL1

+ +

AAL2

+
+
+
+ {isLoading && (
@@ -122,6 +180,7 @@ const UserImpersonationSelector = () => { user={impersonatingUser} onClick={stopImpersonating} isImpersonating={true} + aal={aal} /> )}
@@ -136,7 +195,12 @@ interface UserRowProps { isImpersonating?: boolean } -const UserImpersonatingRow = ({ user, onClick, isImpersonating = false }: UserRowProps) => { +const UserImpersonatingRow = ({ + user, + onClick, + isImpersonating = false, + aal, +}: UserRowProps & { aal: AuthenticatorAssuranceLevels }) => { const avatarUrl = getAvatarUrl(user) const displayName = getDisplayName(user, user.email ?? user.phone ?? user.id ?? 'Unknown') + @@ -144,7 +208,7 @@ const UserImpersonatingRow = ({ user, onClick, isImpersonating = false }: UserRo return (
-
+
{avatarUrl ? ( {displayName} ) : ( @@ -153,7 +217,12 @@ const UserImpersonatingRow = ({ user, onClick, isImpersonating = false }: UserRo
)} - {displayName} + + {displayName}{' '} + + {aal === 'aal2' ? 'AAL2' : 'AAL1'} + +