(ci) try to pass more checks with rabbitai + remove no longer used advanced encryption client in keycloak

This commit is contained in:
Thomas Ramé
2026-05-18 11:23:49 +02:00
parent e2a3b286ca
commit a357fb04d5
11 changed files with 91 additions and 42 deletions
+1 -1
View File
@@ -117,7 +117,7 @@ We hope to see many more, here is an incomplete list of public La Suite Meet ins
| [visio.numerique.gouv.fr](https://visio.numerique.gouv.fr/) | DINUM | French public agents working for the central administration and the extended public sphere. ProConnect is required to login in or sign up |
| [visio.suite.anct.gouv.fr](https://visio.suite.anct.gouv.fr/) | ANCT | French public agents working for the territorial administration and the extended public sphere. ProConnect is required to login in or sign up |
| [visio.lasuite.coop](https://visio.lasuite.coop/) | lasuite.coop | Free and open demo to all. Content and accounts are reset after one month |
| [mosacloud.cloud](https://mosa.cloud/) | mosa.cloud | Demo instance of mosa.cloud, a dutch company providing services around La Suite apps. |
| [mosa.cloud](https://mosa.cloud/) | mosa.cloud | Demo instance of mosa.cloud, a dutch company providing services around La Suite apps. |
## Contributing
-17
View File
@@ -845,23 +845,6 @@
"offline_access",
"microprofile-jwt"
]
},
{
"clientId": "encryption",
"name": "Encryption Service",
"enabled": true,
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": [
"http://encryption.localhost:7200/auth/callback"
],
"webOrigins": [
"http://encryption.localhost:7200",
"http://data.encryption.localhost:7200"
],
"protocol": "openid-connect",
"fullScopeAllowed": true
}
],
"clientScopes": [
+13
View File
@@ -41,6 +41,19 @@ class UserSerializer(serializers.ModelSerializer):
]
read_only_fields = ["id", "email", "full_name", "short_name"]
def validate_default_encryption_mode(self, value):
"""Reject a non-none default when the server has encryption disabled.
Keeps the user preference DB in sync with the deployment's posture:
if an operator flips ENCRYPTION_ENABLED off, no client should be able
to keep persisting `basic` as their default behind their back.
"""
if value != models.EncryptionMode.NONE and not settings.ENCRYPTION_ENABLED:
raise serializers.ValidationError(
_("End-to-end encryption is disabled on this server.")
)
return value
class UserLightSerializer(serializers.ModelSerializer):
"""Serialize users with limited fields."""
+40
View File
@@ -448,7 +448,12 @@ class Room(Resource):
always reject calls to them (no way to derive the key), and the
PIN namespace is finite (10**length): no point burning slots that
can never be dialed.
Also run `clean()` so the encryption invariants are enforced on
every save path (ORM, admin, shell), not only via the DRF
serializer.
"""
self.clean()
if (
settings.ROOM_TELEPHONY_ENABLED
and not self.pk
@@ -460,6 +465,41 @@ class Room(Resource):
)
super().save(*args, **kwargs)
def clean(self):
"""Enforce encryption-mode invariants outside DRF.
Two rules:
- `encryption_mode` is set at creation and never mutated afterwards
(the URL-hash passphrase encodes assumptions about it).
- An encrypted room must be at the RESTRICTED access level so the
host vets joiners before they ever see the in-URL key.
"""
super().clean()
if self.pk is not None:
previous = Room.objects.filter(pk=self.pk).only("encryption_mode").first()
if (
previous is not None
and previous.encryption_mode != self.encryption_mode
):
raise ValidationError(
{
"encryption_mode": _(
"Encryption mode cannot be changed after room creation."
)
}
)
if (
self.encryption_mode != EncryptionMode.NONE
and self.access_level != RoomAccessLevel.RESTRICTED
):
raise ValidationError(
{
"access_level": _(
"Encrypted rooms must use the 'restricted' access level."
)
}
)
def clean_fields(self, exclude=None):
"""
Automatically generate the slug from the name and make sure it does not look like a UUID.
+9 -2
View File
@@ -147,9 +147,16 @@ class LobbyService:
# In encrypted rooms, authenticated users cannot pick an arbitrary
# display name — server enforces the OIDC name so a tampered client
# can't impersonate someone else with their account.
# can't impersonate someone else with their account. If both
# full_name and email are absent (degenerate OIDC payload), fall
# back to a server-controlled technical name rather than trusting
# whatever the client posted.
if room.is_encrypted and request.user.is_authenticated:
username = request.user.full_name or request.user.email or username
username = (
request.user.full_name
or request.user.email
or f"noname-{request.user.id}"
)
participant_id = self._get_or_create_participant_id(request)
participant = self._get_participant(room.id, participant_id)
+3 -1
View File
@@ -90,7 +90,9 @@ def generate_token(
# Local import: core.models loads core.utils mid-import (see models.py:28),
# so importing EncryptionMode at module top would deadlock the bootstrap.
from core.models import EncryptionMode # pylint: disable=import-outside-toplevel
from core.models import ( # noqa: PLC0415 pylint: disable=import-outside-toplevel
EncryptionMode,
)
if is_admin_or_owner or sources is None:
sources = settings.LIVEKIT_DEFAULT_SOURCES
+2 -2
View File
@@ -561,12 +561,12 @@ class Base(Configuration):
"returnTo", environ_name="OIDC_REDIRECT_FIELD_NAME", environ_prefix=None
)
OIDC_USERINFO_FULLNAME_FIELDS = values.ListValue(
default=["first_name", "last_name"],
default=["given_name", "usual_name"],
environ_name="OIDC_USERINFO_FULLNAME_FIELDS",
environ_prefix=None,
)
OIDC_USERINFO_SHORTNAME_FIELD = values.Value(
default="first_name",
default="given_name",
environ_name="OIDC_USERINFO_SHORTNAME_FIELD",
environ_prefix=None,
)
@@ -255,7 +255,7 @@ export const Conference = ({
}, [room, isEncrypted, roomWithE2EE])
useEffect(() => {
if (!isEncrypted) return
if (!data) return
let currentHash = getPassphraseFromHash()
const onHashChange = () => {
const next = getPassphraseFromHash()
@@ -265,22 +265,23 @@ export const Conference = ({
}
window.addEventListener('hashchange', onHashChange)
return () => window.removeEventListener('hashchange', onHashChange)
}, [isEncrypted])
}, [data])
useEffect(() => {
/**
* Warm up connection to LiveKit server before joining room
* Warm up connection to LiveKit server before joining room.
* Use the normalized `serverUrl` (the same value the LiveKitRoom
* will connect to after `force_wss_protocol`) so the warm-up matches
* the actual connection target.
*/
const prepareConnection = async () => {
if (!apiConfig || isConnectionWarmedUp) return
await room.prepareConnection(apiConfig.livekit.url)
if (!apiConfig || !serverUrl || isConnectionWarmedUp) return
await room.prepareConnection(serverUrl)
if (isFireFox() && apiConfig.livekit.enable_firefox_proxy_workaround) {
try {
const wssUrl =
apiConfig.livekit.url
.replace('https://', 'wss://')
.replace(/\/$/, '') + '/rtc'
serverUrl.replace('https://', 'wss://').replace(/\/$/, '') + '/rtc'
/**
* FIREFOX + PROXY WORKAROUND — see livekit-examples/meet/issues/466
@@ -295,7 +296,7 @@ export const Conference = ({
setIsConnectionWarmedUp(true)
}
prepareConnection()
}, [room, apiConfig, isConnectionWarmedUp])
}, [room, apiConfig, serverUrl, isConnectionWarmedUp])
const [showInviteDialog, setShowInviteDialog] = useState(mode === 'create')
const [mediaDeviceError, setMediaDeviceError] = useState<{
@@ -125,7 +125,16 @@ export const Join = ({
// re-enforces this when minting the JWT.
const isNameLocked = isEncryptedRoom && !!isLoggedIn
const lockedName = user?.full_name || user?.email || ''
const passphrase = getPassphraseFromHash()
// Keep the passphrase in state and refresh on `hashchange` so the
// mismatch screen recovers immediately when the user pastes the
// correct hash into the address bar.
const [passphrase, setPassphrase] = useState(getPassphraseFromHash)
useEffect(() => {
const onHashChange = () => setPassphrase(getPassphraseFromHash())
window.addEventListener('hashchange', onHashChange)
return () => window.removeEventListener('hashchange', onHashChange)
}, [])
const hasValidPassphrase = isEncryptedRoom
? isValidPassphrase(passphrase)
: true
@@ -56,7 +56,7 @@ export const useLobby = ({
enabled: status === ApiLobbyStatus.WAITING,
})
const startWaiting = useCallback(async () => {
const startWaiting = useCallback(() => {
setStatus(ApiLobbyStatus.WAITING)
startWaitingTimeout()
}, [startWaitingTimeout])
@@ -4,13 +4,12 @@ import { Separator as RACSeparator } from 'react-aria-components'
import { RiAlertFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { usePatchRoom } from '@/features/rooms/api/patchRoom'
import { fetchRoom } from '@/features/rooms/api/fetchRoom'
import { ApiAccessLevel, ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
import { queryClient } from '@/api/queryClient'
import { keys } from '@/api/queryKeys'
import { useQuery } from '@tanstack/react-query'
import { useParams } from 'wouter'
import { usePublishSourcesManager } from '@/features/rooms/livekit/hooks/usePublishSourcesManager'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
export const Admin = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'admin' })
@@ -23,12 +22,7 @@ export const Admin = () => {
const { mutateAsync: patchRoom } = usePatchRoom()
const { data: readOnlyData } = useQuery({
queryKey: [keys.room, roomId],
queryFn: () => fetchRoom({ roomId }),
retry: false,
enabled: false,
})
const readOnlyData = useRoomData()
const {
toggleMicrophone,