From 0d5136206f9200f2d83782b174d88a98d7423738 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Fri, 10 Jul 2026 15:49:25 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(all)=20allow=20forcing=20SSO=20displa?= =?UTF-8?q?y=20name=20for=20authenticated=20users?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new setting that controls whether an authenticated user can rename or modify their display name, or if they must use the one returned by the SSO. When the setting is disabled, only anonymous users can set a display name freely; authenticated users always use their SSO display name. Requested by many self-hosters. --- CHANGELOG.md | 1 + src/backend/core/api/__init__.py | 3 ++ src/backend/core/tests/test_utils.py | 41 +++++++++++++++++++ src/backend/core/utils.py | 7 +++- src/backend/meet/settings.py | 5 +++ src/frontend/src/api/useConfig.ts | 1 + .../src/features/rooms/components/Join.tsx | 36 +++++++++------- .../settings/components/tabs/AccountTab.tsx | 22 ++++++---- 8 files changed, 92 insertions(+), 24 deletions(-) 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 ? ( <>