mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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 <alaister@users.noreply.github.com> Co-authored-by: Alaister Young <a@alaisteryoung.com>
This commit is contained in:
3 files changed
+82
-7
No files matched your search
+6
-1
@@ -41,7 +41,12 @@ const RoleImpersonationPopover = ({
|
||||
<span className="text-foreground-muted">role</span>
|
||||
<span>{currentRole}</span>
|
||||
{state.role?.type === 'postgrest' && state.role.role === 'authenticated' && (
|
||||
<UserRoleButtonSection user={state.role.user} />
|
||||
<>
|
||||
<UserRoleButtonSection user={state.role.user} />
|
||||
<span className="text-xs text-foreground-lighter font-light">
|
||||
{state.role.aal === 'aal2' ? 'AAL2' : 'AAL1'}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<ChevronDown className="text-muted" strokeWidth={1} size={12} />
|
||||
</div>
|
||||
|
||||
+74
-5
@@ -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<AuthenticatorAssuranceLevels>('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 (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="text-foreground text-base">
|
||||
@@ -91,6 +107,48 @@ const UserImpersonationSelector = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<Collapsible_Shadcn_>
|
||||
<CollapsibleTrigger_Shadcn_ className="group font-normal p-0 [&[data-state=open]>div>svg]:!-rotate-180">
|
||||
<div className="flex items-center gap-x-1 w-full">
|
||||
<p className="text-xs text-foreground-light group-hover:text-foreground transition">
|
||||
Advanced options
|
||||
</p>
|
||||
<ChevronDown
|
||||
className="transition-transform duration-200"
|
||||
strokeWidth={1.5}
|
||||
size={14}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleTrigger_Shadcn_>
|
||||
<CollapsibleContent_Shadcn_ className="mt-1 flex flex-row items-center gap-x-4 text-sm text-foreground-light">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<h3>MFA assurance level</h3>
|
||||
<InfoTooltip side="top" className="flex flex-col gap-1 max-w-96">
|
||||
<p>
|
||||
AAL1 verifies users via standard login methods, while AAL2 adds a second
|
||||
authentication factor.
|
||||
<br />
|
||||
If you're not using MFA, you can leave this on AAL1.
|
||||
</p>
|
||||
<a
|
||||
href="/docs/guides/auth/auth-mfa"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-x-1 opacity-50 hover:opacity-100 transition"
|
||||
>
|
||||
Learn more about MFA <ExternalLink size={14} strokeWidth={2} />
|
||||
</a>
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center gap-x-2 text-xs font-bold">
|
||||
<p className={aal === 'aal1' ? undefined : 'text-foreground-lighter'}>AAL1</p>
|
||||
<Switch checked={aal === 'aal2'} onCheckedChange={toggleAalState} />
|
||||
<p className={aal === 'aal2' ? undefined : 'text-foreground-lighter'}>AAL2</p>
|
||||
</div>
|
||||
</CollapsibleContent_Shadcn_>
|
||||
</Collapsible_Shadcn_>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex flex-col gap-2 items-center justify-center h-24">
|
||||
<Loader2 className="animate-spin" size={24} />
|
||||
@@ -122,6 +180,7 @@ const UserImpersonationSelector = () => {
|
||||
user={impersonatingUser}
|
||||
onClick={stopImpersonating}
|
||||
isImpersonating={true}
|
||||
aal={aal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -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 (
|
||||
<div className="flex items-center gap-3 py-2 text-foreground">
|
||||
<div className="flex items-center gap-4 bg-surface-200 pr-5 pl-0.5 py-0.5 border rounded-full max-w-l">
|
||||
<div className="flex items-center gap-4 bg-surface-200 pr-4 pl-0.5 py-0.5 border rounded-full max-w-l">
|
||||
{avatarUrl ? (
|
||||
<img className="rounded-full w-5 h-5" src={avatarUrl} alt={displayName} />
|
||||
) : (
|
||||
@@ -153,7 +217,12 @@ const UserImpersonatingRow = ({ user, onClick, isImpersonating = false }: UserRo
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="text-sm truncate">{displayName}</span>
|
||||
<span className="text-sm truncate">
|
||||
{displayName}{' '}
|
||||
<span className="ml-2 text-foreground-lighter text-xs font-light">
|
||||
{aal === 'aal2' ? 'AAL2' : 'AAL1'}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -14,6 +14,7 @@ type PostgrestImpersonationRole =
|
||||
type: 'postgrest'
|
||||
role: 'authenticated'
|
||||
user: User
|
||||
aal?: 'aal1' | 'aal2'
|
||||
}
|
||||
|
||||
export type PostgrestRole = PostgrestImpersonationRole['role']
|
||||
@@ -36,7 +37,7 @@ function getPostgrestClaims(projectRef: string, role: PostgrestImpersonationRole
|
||||
const user = role.user
|
||||
|
||||
return {
|
||||
aal: 'aal1',
|
||||
aal: role.aal ?? 'aal1',
|
||||
amr: [{ method: 'password', timestamp: nowTimestamp }],
|
||||
app_metadata: user.raw_app_meta_data,
|
||||
aud: 'authenticated',
|
||||
|
||||
Reference in new issue
Block a user