mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-31 04:37:57 +00:00
✅ (ci) try to pass more checks with rabbitai + remove no longer used advanced encryption client in keycloak
This commit is contained in:
@@ -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.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.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 |
|
| [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
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -845,23 +845,6 @@
|
|||||||
"offline_access",
|
"offline_access",
|
||||||
"microprofile-jwt"
|
"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": [
|
"clientScopes": [
|
||||||
|
|||||||
@@ -41,6 +41,19 @@ class UserSerializer(serializers.ModelSerializer):
|
|||||||
]
|
]
|
||||||
read_only_fields = ["id", "email", "full_name", "short_name"]
|
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):
|
class UserLightSerializer(serializers.ModelSerializer):
|
||||||
"""Serialize users with limited fields."""
|
"""Serialize users with limited fields."""
|
||||||
|
|||||||
@@ -448,7 +448,12 @@ class Room(Resource):
|
|||||||
always reject calls to them (no way to derive the key), and the
|
always reject calls to them (no way to derive the key), and the
|
||||||
PIN namespace is finite (10**length): no point burning slots that
|
PIN namespace is finite (10**length): no point burning slots that
|
||||||
can never be dialed.
|
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 (
|
if (
|
||||||
settings.ROOM_TELEPHONY_ENABLED
|
settings.ROOM_TELEPHONY_ENABLED
|
||||||
and not self.pk
|
and not self.pk
|
||||||
@@ -460,6 +465,41 @@ class Room(Resource):
|
|||||||
)
|
)
|
||||||
super().save(*args, **kwargs)
|
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):
|
def clean_fields(self, exclude=None):
|
||||||
"""
|
"""
|
||||||
Automatically generate the slug from the name and make sure it does not look like a UUID.
|
Automatically generate the slug from the name and make sure it does not look like a UUID.
|
||||||
|
|||||||
@@ -147,9 +147,16 @@ class LobbyService:
|
|||||||
|
|
||||||
# In encrypted rooms, authenticated users cannot pick an arbitrary
|
# In encrypted rooms, authenticated users cannot pick an arbitrary
|
||||||
# display name — server enforces the OIDC name so a tampered client
|
# 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:
|
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_id = self._get_or_create_participant_id(request)
|
||||||
participant = self._get_participant(room.id, participant_id)
|
participant = self._get_participant(room.id, participant_id)
|
||||||
|
|||||||
@@ -90,7 +90,9 @@ def generate_token(
|
|||||||
|
|
||||||
# Local import: core.models loads core.utils mid-import (see models.py:28),
|
# Local import: core.models loads core.utils mid-import (see models.py:28),
|
||||||
# so importing EncryptionMode at module top would deadlock the bootstrap.
|
# 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:
|
if is_admin_or_owner or sources is None:
|
||||||
sources = settings.LIVEKIT_DEFAULT_SOURCES
|
sources = settings.LIVEKIT_DEFAULT_SOURCES
|
||||||
|
|||||||
@@ -561,12 +561,12 @@ class Base(Configuration):
|
|||||||
"returnTo", environ_name="OIDC_REDIRECT_FIELD_NAME", environ_prefix=None
|
"returnTo", environ_name="OIDC_REDIRECT_FIELD_NAME", environ_prefix=None
|
||||||
)
|
)
|
||||||
OIDC_USERINFO_FULLNAME_FIELDS = values.ListValue(
|
OIDC_USERINFO_FULLNAME_FIELDS = values.ListValue(
|
||||||
default=["first_name", "last_name"],
|
default=["given_name", "usual_name"],
|
||||||
environ_name="OIDC_USERINFO_FULLNAME_FIELDS",
|
environ_name="OIDC_USERINFO_FULLNAME_FIELDS",
|
||||||
environ_prefix=None,
|
environ_prefix=None,
|
||||||
)
|
)
|
||||||
OIDC_USERINFO_SHORTNAME_FIELD = values.Value(
|
OIDC_USERINFO_SHORTNAME_FIELD = values.Value(
|
||||||
default="first_name",
|
default="given_name",
|
||||||
environ_name="OIDC_USERINFO_SHORTNAME_FIELD",
|
environ_name="OIDC_USERINFO_SHORTNAME_FIELD",
|
||||||
environ_prefix=None,
|
environ_prefix=None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ export const Conference = ({
|
|||||||
}, [room, isEncrypted, roomWithE2EE])
|
}, [room, isEncrypted, roomWithE2EE])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEncrypted) return
|
if (!data) return
|
||||||
let currentHash = getPassphraseFromHash()
|
let currentHash = getPassphraseFromHash()
|
||||||
const onHashChange = () => {
|
const onHashChange = () => {
|
||||||
const next = getPassphraseFromHash()
|
const next = getPassphraseFromHash()
|
||||||
@@ -265,22 +265,23 @@ export const Conference = ({
|
|||||||
}
|
}
|
||||||
window.addEventListener('hashchange', onHashChange)
|
window.addEventListener('hashchange', onHashChange)
|
||||||
return () => window.removeEventListener('hashchange', onHashChange)
|
return () => window.removeEventListener('hashchange', onHashChange)
|
||||||
}, [isEncrypted])
|
}, [data])
|
||||||
|
|
||||||
useEffect(() => {
|
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 () => {
|
const prepareConnection = async () => {
|
||||||
if (!apiConfig || isConnectionWarmedUp) return
|
if (!apiConfig || !serverUrl || isConnectionWarmedUp) return
|
||||||
await room.prepareConnection(apiConfig.livekit.url)
|
await room.prepareConnection(serverUrl)
|
||||||
|
|
||||||
if (isFireFox() && apiConfig.livekit.enable_firefox_proxy_workaround) {
|
if (isFireFox() && apiConfig.livekit.enable_firefox_proxy_workaround) {
|
||||||
try {
|
try {
|
||||||
const wssUrl =
|
const wssUrl =
|
||||||
apiConfig.livekit.url
|
serverUrl.replace('https://', 'wss://').replace(/\/$/, '') + '/rtc'
|
||||||
.replace('https://', 'wss://')
|
|
||||||
.replace(/\/$/, '') + '/rtc'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FIREFOX + PROXY WORKAROUND — see livekit-examples/meet/issues/466
|
* FIREFOX + PROXY WORKAROUND — see livekit-examples/meet/issues/466
|
||||||
@@ -295,7 +296,7 @@ export const Conference = ({
|
|||||||
setIsConnectionWarmedUp(true)
|
setIsConnectionWarmedUp(true)
|
||||||
}
|
}
|
||||||
prepareConnection()
|
prepareConnection()
|
||||||
}, [room, apiConfig, isConnectionWarmedUp])
|
}, [room, apiConfig, serverUrl, isConnectionWarmedUp])
|
||||||
|
|
||||||
const [showInviteDialog, setShowInviteDialog] = useState(mode === 'create')
|
const [showInviteDialog, setShowInviteDialog] = useState(mode === 'create')
|
||||||
const [mediaDeviceError, setMediaDeviceError] = useState<{
|
const [mediaDeviceError, setMediaDeviceError] = useState<{
|
||||||
|
|||||||
@@ -125,7 +125,16 @@ export const Join = ({
|
|||||||
// re-enforces this when minting the JWT.
|
// re-enforces this when minting the JWT.
|
||||||
const isNameLocked = isEncryptedRoom && !!isLoggedIn
|
const isNameLocked = isEncryptedRoom && !!isLoggedIn
|
||||||
const lockedName = user?.full_name || user?.email || ''
|
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
|
const hasValidPassphrase = isEncryptedRoom
|
||||||
? isValidPassphrase(passphrase)
|
? isValidPassphrase(passphrase)
|
||||||
: true
|
: true
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const useLobby = ({
|
|||||||
enabled: status === ApiLobbyStatus.WAITING,
|
enabled: status === ApiLobbyStatus.WAITING,
|
||||||
})
|
})
|
||||||
|
|
||||||
const startWaiting = useCallback(async () => {
|
const startWaiting = useCallback(() => {
|
||||||
setStatus(ApiLobbyStatus.WAITING)
|
setStatus(ApiLobbyStatus.WAITING)
|
||||||
startWaitingTimeout()
|
startWaitingTimeout()
|
||||||
}, [startWaitingTimeout])
|
}, [startWaitingTimeout])
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import { Separator as RACSeparator } from 'react-aria-components'
|
|||||||
import { RiAlertFill } from '@remixicon/react'
|
import { RiAlertFill } from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { usePatchRoom } from '@/features/rooms/api/patchRoom'
|
import { usePatchRoom } from '@/features/rooms/api/patchRoom'
|
||||||
import { fetchRoom } from '@/features/rooms/api/fetchRoom'
|
|
||||||
import { ApiAccessLevel, ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
import { ApiAccessLevel, ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||||
import { queryClient } from '@/api/queryClient'
|
import { queryClient } from '@/api/queryClient'
|
||||||
import { keys } from '@/api/queryKeys'
|
import { keys } from '@/api/queryKeys'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
import { useParams } from 'wouter'
|
import { useParams } from 'wouter'
|
||||||
import { usePublishSourcesManager } from '@/features/rooms/livekit/hooks/usePublishSourcesManager'
|
import { usePublishSourcesManager } from '@/features/rooms/livekit/hooks/usePublishSourcesManager'
|
||||||
|
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||||
|
|
||||||
export const Admin = () => {
|
export const Admin = () => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'admin' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'admin' })
|
||||||
@@ -23,12 +22,7 @@ export const Admin = () => {
|
|||||||
|
|
||||||
const { mutateAsync: patchRoom } = usePatchRoom()
|
const { mutateAsync: patchRoom } = usePatchRoom()
|
||||||
|
|
||||||
const { data: readOnlyData } = useQuery({
|
const readOnlyData = useRoomData()
|
||||||
queryKey: [keys.room, roomId],
|
|
||||||
queryFn: () => fetchRoom({ roomId }),
|
|
||||||
retry: false,
|
|
||||||
enabled: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
toggleMicrophone,
|
toggleMicrophone,
|
||||||
|
|||||||
Reference in New Issue
Block a user