diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb315f4..c199c0e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to - ✨(backend) allow searching the recording admin table by owner email - ✨(frontend) add participant color gradient when camera is off #1490 +- ✨(all) allow forcing SSO display name for authenticated users ### Changed diff --git a/src/backend/core/api/__init__.py b/src/backend/core/api/__init__.py index 5cf1c671..f9460024 100644 --- a/src/backend/core/api/__init__.py +++ b/src/backend/core/api/__init__.py @@ -68,6 +68,9 @@ def get_frontend_configuration(request): "enable_firefox_proxy_workaround": settings.LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND, "default_sources": settings.LIVEKIT_DEFAULT_SOURCES, }, + "authenticated_users_can_edit_display_name": ( + settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME + ), } frontend_configuration.update(settings.FRONTEND_CONFIGURATION) return Response(frontend_configuration) diff --git a/src/backend/core/tests/test_utils.py b/src/backend/core/tests/test_utils.py index 907a7143..db4cdb38 100644 --- a/src/backend/core/tests/test_utils.py +++ b/src/backend/core/tests/test_utils.py @@ -7,6 +7,7 @@ import json from unittest import mock from django.conf import settings +from django.contrib.auth.models import AnonymousUser import jwt import pytest @@ -66,6 +67,46 @@ def test_generate_token_explicit_username_overrides_default(): assert claims["name"] == "Custom Name" +def test_authenticated_username_ignored_when_editing_disabled(settings): + """With editing disabled, an authenticated user's username is ignored.""" + settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False + user = UserFactory(full_name="Jane Doe") + token = generate_token(room="my-room", user=user, username="Custom Name") + claims = decode_token(token) + assert claims["name"] == "Jane Doe" + + +def test_authenticated_default_name_unaffected_when_editing_disabled(settings): + """Disabling editing doesn't disturb the default full-name path.""" + settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False + user = UserFactory(full_name="Jane Doe") + token = generate_token(room="my-room", user=user) + claims = decode_token(token) + assert claims["name"] == "Jane Doe" + + +def test_anonymous_uses_username_when_provided(): + """An anonymous user's provided username is used as the display name.""" + token = generate_token(room="my-room", user=AnonymousUser(), username="Guest42") + claims = decode_token(token) + assert claims["name"] == "Guest42" + + +def test_anonymous_username_used_even_when_editing_disabled(settings): + """The setting governs authenticated users only; anonymous can still set a name.""" + settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False + token = generate_token(room="my-room", user=AnonymousUser(), username="Guest42") + claims = decode_token(token) + assert claims["name"] == "Guest42" + + +def test_anonymous_falls_back_to_anonymous_label(): + """With no username, an anonymous user is labelled 'Anonymous'.""" + token = generate_token(room="my-room", user=AnonymousUser()) + claims = decode_token(token) + assert claims["name"] == "Anonymous" + + @mock.patch("asyncio.get_running_loop") @mock.patch("core.utils.LiveKitAPI") def test_create_livekit_client_ssl_enabled( diff --git a/src/backend/core/utils.py b/src/backend/core/utils.py index 88980eed..6d843e17 100644 --- a/src/backend/core/utils.py +++ b/src/backend/core/utils.py @@ -114,6 +114,11 @@ def generate_token( if color is None: color = generate_color(identity) + can_edit = ( + settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME or user.is_anonymous + ) + display_name = (username or default_username) if can_edit else default_username + token = ( AccessToken( api_key=settings.LIVEKIT_CONFIGURATION["api_key"], @@ -121,7 +126,7 @@ def generate_token( ) .with_grants(video_grants) .with_identity(identity) - .with_name(username or default_username) + .with_name(display_name) .with_attributes( {"color": color, "room_admin": "true" if is_admin_or_owner else "false"} ) diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index 7faba84e..681ad1e1 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -671,6 +671,11 @@ class Base(Configuration): environ_name="PARTICIPANT_FORBIDDEN_PERMISSION_FIELDS", environ_prefix=None, ) + AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = values.BooleanValue( + True, + environ_name="AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME", + environ_prefix=None, + ) # Recording settings RECORDING_ENABLE = values.BooleanValue( diff --git a/src/frontend/src/api/useConfig.ts b/src/frontend/src/api/useConfig.ts index 7a3bf320..e6b758e8 100644 --- a/src/frontend/src/api/useConfig.ts +++ b/src/frontend/src/api/useConfig.ts @@ -58,6 +58,7 @@ export interface ApiConfig { transcription_destination?: string max_participants_for_sound: number auto_mute_on_join_threshold: number + authenticated_users_can_edit_display_name: boolean } const fetchConfig = (): Promise => { diff --git a/src/frontend/src/features/rooms/components/Join.tsx b/src/frontend/src/features/rooms/components/Join.tsx index 606d96f3..938398fc 100644 --- a/src/frontend/src/features/rooms/components/Join.tsx +++ b/src/frontend/src/features/rooms/components/Join.tsx @@ -51,6 +51,8 @@ import { saveUsername, userStore } from '@/stores/user' import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice' import { useSnapshot } from 'valtio' +import { useUser } from '@/features/auth/api/useUser' +import { useConfig } from '@/api/useConfig' const onError = (e: Error) => console.error('ERROR', e) @@ -115,6 +117,9 @@ export const Join = ({ }) => { const { t } = useTranslation('rooms', { keyPrefix: 'join' }) + const { data: configData } = useConfig() + const { isLoggedIn } = useUser() + const { audioEnabled, videoEnabled, @@ -449,20 +454,23 @@ export const Join = ({ {t('heading')} - !value && t('errors.usernameEmpty')} - wrapperProps={{ - noMargin: true, - fullWidth: true, - }} - autoComplete="name" - maxLength={50} - /> + {(!isLoggedIn || + configData?.authenticated_users_can_edit_display_name) && ( + !value && t('errors.usernameEmpty')} + wrapperProps={{ + noMargin: true, + fullWidth: true, + }} + autoComplete="name" + maxLength={50} + /> + )} ) diff --git a/src/frontend/src/features/settings/components/tabs/AccountTab.tsx b/src/frontend/src/features/settings/components/tabs/AccountTab.tsx index 9cc59fc2..0563b732 100644 --- a/src/frontend/src/features/settings/components/tabs/AccountTab.tsx +++ b/src/frontend/src/features/settings/components/tabs/AccountTab.tsx @@ -10,6 +10,7 @@ import { LoginButton } from '@/components/LoginButton' import { useRenameParticipant } from '@/features/rooms/api/renameParticipant' import { saveUsername } from '@/stores/user' import { logout } from '@/features/auth/utils/logout' +import { useConfig } from '@/api/useConfig' export type AccountTabProps = Pick & Pick @@ -17,6 +18,7 @@ export type AccountTabProps = Pick & export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => { const { t } = useTranslation('settings') const room = useRoomContext() + const { data } = useConfig() const { user, isLoggedIn } = useUser() const { renameParticipant } = useRenameParticipant() @@ -45,15 +47,17 @@ export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => { return ( {t('account.heading')} - { - return !value ?

{t('account.nameError')}

: null - }} - /> + {(!isLoggedIn || data?.authenticated_users_can_edit_display_name) && ( + { + return !value ?

{t('account.nameError')}

: null + }} + /> + )} {t('account.authentication')} {isLoggedIn ? ( <>