Compare commits

...

16 Commits

Author SHA1 Message Date
lebaudantoine c7b05a4487 ⚡️(devx) switch devstack to node:22-alpine
The devstack was pulling the full `node:22` image, around 1.6 GB.
Switch to `node:22-alpine`, which is much smaller, to speed up the
devstack bootstrap time and reduce disk usage.
2026-10-02 15:46:34 +02:00
lebaudantoine 971a7295ca ⚡️(devx) use a single Redis image version in the devstack
The devstack was pulling two different versions of the Redis image.
Align everything on a single version to save a few MB of network
bandwidth when bootstrapping the stack.

Late-night minor optimization.
2026-10-01 16:38:31 +02:00
lebaudantoine bd0329d162 🔒️(agents) upgrade libpcre2-8-0 to fix CVE-2026-103111
The python:3.14.7-slim base image ships libpcre2-8-0
10.46-1~deb13u2, which is affected by CVE-2026-103111 (HIGH):
an out-of-bounds write triggered by a crafted regular expression.

Explicitly install libpcre2-8-0 in the base stage so apt pulls
the patched 10.46-1~deb13u3 from trixie-security. All stages
(builder, development, production) inherit the fix.

This line can be dropped once an upstream python slim image
ships the patched package.
2026-10-01 16:37:29 +02:00
lebaudantoine cedaa32ab7 🔒️(backend) fix HIGH CVEs in Django and urllib3
Address the following HIGH severity CVEs reported by Trivy on the
backend image:

* Django 5.2.16 → 5.2.17
  - CVE-2026-15307 — remote code execution via GeoDjango spatial
    lookups.

* urllib3 2.7.0 → 2.8.0
  - CVE-2026-97687 — traffic interception via HTTPS proxy TLS
    configuration override.
  - CVE-2026-97689 — denial of service via unbounded memory
    allocation in the chunk parser.
2026-10-01 16:37:29 +02:00
lebaudantoine 22adccb353 👷(ci) ignore unfixed Debian CVEs in trivy scans
The trivy scan fails on HIGH vulnerabilities found in the Debian 13
base images (util-linux, acl, ncurses, systemd and perl-base). None
of them has a fixed version available yet, so there is nothing we
can upgrade to clear them.

List these CVEs in a shared .github/.trivyignore and pass it to
every image scan, so the scan stays blocking for any new HIGH or
CRITICAL vulnerability. Remove the entries once Debian ships a fix.
2026-10-01 00:13:36 +02:00
lebaudantoine 3ab651d6c7 👷(ci) pin the shared CI repo to v0.0.1
Instead of referencing the shared CI repo on `main`, pin it to the
initial tagged version `v0.0.1`, so the CI behavior is stable and
does not silently change when the shared repo is updated.
2026-10-01 00:13:36 +02:00
lebaudantoine 919af928aa 🚨(ci) fix the shellcheck job
Get the shellcheck CI job to pass again by addressing the issues it
flagged across our shell scripts.

Note: I am not 100% sure of every fix applied here. Reviewers should
feel free to challenge specific changes and suggest better ones
where relevant.
2026-10-01 00:13:36 +02:00
lebaudantoine 3ed38f1c48 🚨(ci) fix the spellcheck job
Get the spellcheck CI job to pass again by:

* Fixing the actual spelling issues it caught in the project.
* Excluding generated files from the scan, since they are not
  written by us.
* Excluding translation files, whose content is not necessarily in
  English and would trigger false positives.
2026-10-01 00:13:36 +02:00
lebaudantoine f172c5795e 👷(ci) add Menshen scan for GitHub Actions vulnerabilities
Wire Menshen into the CI to scan the GitHub Actions we use and flag
vulnerable ones, following the same approach as other projects that
recently adopted it.

Note: I am not fully sure about the current setup. Reviewers should
feel free to adjust the configuration or the integration point as
they see fit.
2026-10-01 00:13:36 +02:00
lebaudantoine 262b168414 🔥(ci) drop unused Crowdin workflows
The Crowdin workflows were not used by the project and had turned
into dead CI code.

Remove them to reduce noise and keep the CI configuration limited
to what is actually running.
2026-10-01 00:13:36 +02:00
lebaudantoine 6c371c8cb3 👷(ci) migrate CI to the shared workflows repository
First iteration of a migration toward a centralized repository
containing our shared CI logic.

Goals:

* Manage GitHub Actions version upgrades in one place.
* Make it easier to audit what actually runs in CI from a security
  perspective.
* Avoid duplicating CI logic across projects and having each
  repository slowly diverge over time.
* Centralize as many of our custom GitHub Actions as possible,
  including some that still live in the old `numerique-gouv`
  organization.
* Centralize the Renovate configuration alongside the workflows.

Inspired by the Accounts project, which recently simplified and
reorganized its CI setup.

This PR starts moving meet's CI to the shared repository so we can
validate the approach on a real project. For now, reusable
workflows are pinned to `main`; once we agree on the structure and
content of the central repository, they should be pinned to a
specific commit SHA instead.
2026-10-01 00:13:36 +02:00
lebaudantoine 1a8906c0a1 🔒️(agents) fix util-linux CVEs reported by Cyberwatch
Address the following CVEs in util-linux, reported by Cyberwatch on
the agents image. The python:3.14.6-slim tag is no longer rebuilt
and still ships util-linux 2.41-5. Bump the base image to
python:3.14.7-slim, which ships the patched 2.41.5-0+deb13u1
(DSA-6442-1), to cover them all:

* CVE-2026-53612 (7.0) — TOCTOU in mount post-mount ownership/mode
  changes.
* CVE-2026-53613 (7.0) — TOCTOU in mount via ancestor directory swap.
* CVE-2026-53614 (7.0) — SUID mount(8) nosuid/noexec bypass via
  `LIBMOUNT_FORCE_MOUNT2`.
* CVE-2026-13595 (5.3) — flaw in the libblkid library.
* CVE-2026-27456 (4.7) — TOCTOU in SUID mount(8) when setting up
  loop devices.
2026-09-30 16:21:22 +02:00
lebaudantoine 99ba8e330e 🐛(frontend) enforce recording-mode permissions on the checkboxes
Permissions on the recording panel checkboxes were not properly
enforced. A user with only partial access to some recording modes
could still tick a mode's checkbox and, for example, launch a
transcription from the recording panel even though they were not
authorized to.

Gate each checkbox on the user's actual permissions so that only
authorized modes can be started from the panel.
2026-09-30 15:15:19 +02:00
lebaudantoine 059e5f1ec4 🔒️(backend) add a daily cap on room creation
The per-minute room creation throttle absorbs bursts but does not stop
a compromised account from steadily creating rooms over hours or days.

Add RoomCreationDailyUserRateThrottle, a per-user throttle with its own
"room_creation_daily" scope, applied to room creation only alongside
the existing short-term throttle. It defaults to 1000 rooms per day and
is configurable via ROOM_CREATION_DAILY_THROTTLE_RATES.

Tests use a controllable clock and patch rates with monkeypatch so they
are restored after each test.
2026-09-30 14:57:10 +02:00
Lebaud Antoine 39ab9359e4 🔒️(backend) throttle meeting link generation
Add throttling on the endpoint used to generate meeting links, so a
compromised authenticated account cannot silently generate thousands
of links without hitting any suspicious errors or alerts.

The limits are set high enough not to affect legitimate usage, while
capping the damage a leaked account can do.
2026-09-30 14:57:10 +02:00
lebaudantoine d0a0d60ece 🔖(minor) bump release to 1.33.0 2026-09-30 13:03:41 +02:00
54 changed files with 609 additions and 709 deletions
+9
View File
@@ -0,0 +1,9 @@
[codespell]
# Files that are not English, or generated
skip = ./.git,*.pdf,*.po,*.pot,*.json,*.lock,package-lock.json,
./LICENSES,
./src/summary/summary/core/locales,
./src/summary/summary/core/prompt.py
# Valid words in French (connexion) or in the code (statics)
ignore-words-list = connexion,statics
check-filenames = true
+20
View File
@@ -0,0 +1,20 @@
# Debian 13 base image (python:3.14-slim): no fixed version available yet.
# Review regularly and remove entries once Debian ships a fix.
# util-linux
CVE-2026-76642
CVE-2026-78408
CVE-2026-78409
CVE-2026-78410
# acl
CVE-2026-54369
# ncurses
CVE-2025-69720
# systemd
CVE-2026-16742
# perl-base (fix deferred by Debian)
CVE-2026-9538
+19
View File
@@ -0,0 +1,19 @@
name: Changelog Workflow
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
changelog:
uses: suitenumerique/ci/.github/workflows/_changelog.yml@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
+22 -176
View File
@@ -11,180 +11,30 @@ permissions:
contents: read contents: read
jobs: jobs:
lint-git:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' # Makes sense only for pull requests
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: 0
- name: show
run: git log
- name: Enforce absence of print statements in code
if: always()
run: |
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude).github/workflows/**' | grep "print("
- name: Check absence of fixup commits
if: always()
run: |
! git log | grep 'fixup!'
- name: Install uv
if: always()
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Lint commit messages added to main
if: always()
run: uvx --no-build --from gitlint-core==0.19.1 gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
check-changelog: lint-python:
runs-on: ubuntu-latest name: lint ${{ matrix.service }}
if: | strategy:
contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false && fail-fast: false
github.event_name == 'pull_request' matrix:
permissions: include:
contents: read - service: backend
steps: working_directory: src/backend
- name: Checkout repository pylint_targets: meet demo core
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - service: agents
with: working_directory: src/agents
fetch-depth: 50 pylint_targets: ""
- name: Check that the CHANGELOG has been modified in the current branch - service: summary
run: git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.after }} | grep 'CHANGELOG.md' working_directory: src/summary
pylint_targets: ""
lint-changelog: uses: suitenumerique/ci/.github/workflows/_python-lint.yml@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
runs-on: ubuntu-latest with:
permissions: working_directory: ${{ matrix.working_directory }}
contents: read python_version: "3.13"
steps: pylint_targets: ${{ matrix.pylint_targets }}
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Check CHANGELOG max line length
run: |
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
if [ $max_line_length -ge 80 ]; then
echo "ERROR: CHANGELOG has lines longer than 80 characters."
exit 1
fi
build-mails:
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: src/mail
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Install Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: "22"
- name: Restore the mail templates
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
- name: Install yarn
if: steps.mail-templates.outputs.cache-hit != 'true'
run: npm install -g --ignore-scripts yarn@1.22.22
- name: Install node dependencies
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile --ignore-scripts
- name: Build mails
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn build
- name: Cache mail templates
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
lint-back:
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: src/backend
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Install Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.13"
- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Install the project
run: uv sync --locked --all-extras
- name: Check code formatting with ruff
run: uv run --no-sync --no-build ruff format . --diff
- name: Lint code with ruff
run: uv run --no-sync --no-build ruff check .
- name: Lint code with pylint
run: uv run --no-sync --no-build pylint meet demo core
lint-agents:
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: src/agents
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Install Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.13"
- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Install the project
run: uv sync --locked --all-extras --no-build
- name: Check code formatting with ruff
run: uv run --no-sync --no-build ruff format . --diff
- name: Lint code with ruff
run: uv run --no-sync --no-build ruff check .
lint-summary:
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: src/summary
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Install Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.13"
- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Install the project
run: uv sync --locked --all-extras
- name: Check code formatting with ruff
run: uv run --no-sync --no-build ruff format . --diff
- name: Lint code with ruff
run: uv run --no-sync --no-build ruff check .
test-back: test-back:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build-mails
permissions: permissions:
contents: read contents: read
defaults: defaults:
@@ -244,12 +94,8 @@ jobs:
sudo mkdir -p /data/media && \ sudo mkdir -p /data/media && \
sudo mkdir -p /data/static sudo mkdir -p /data/static
- name: Restore the mail templates - name: Build or restore the mail templates
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 uses: suitenumerique/ci/actions/mail-templates@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
# Creates the access key and the bucket on startup # Creates the access key and the bucket on startup
- name: Start Garage - name: Start Garage
+14
View File
@@ -0,0 +1,14 @@
name: Project quality Workflow
on:
pull_request:
permissions:
contents: read
jobs:
quality:
uses: suitenumerique/ci/.github/workflows/_project-quality.yml@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
with:
print_check_paths: src/backend src/summary src/agents
codespell_ignore_words: "unsecure"
-33
View File
@@ -1,33 +0,0 @@
name: Download Crowdin translations
on:
workflow_dispatch:
types: [file-fully-translated]
permissions:
contents: write
pull-requests: write
jobs:
crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Download Crowdin files
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0
with:
upload_sources: false
upload_translations: false
download_translations: true
localization_branch_name: l10n_crowdin_translations
create_pull_request: true
pull_request_title: "New Crowdin translations"
pull_request_body: "New Crowdin pull request with translations"
pull_request_base_branch_name: "main"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
CROWDIN_BASE_PATH: ${{ github.workspace }}
+49 -252
View File
@@ -1,5 +1,5 @@
name: Docker Hub Workflow name: Docker images
run-name: Docker Hub Workflow run-name: Docker images
on: on:
workflow_dispatch: workflow_dispatch:
@@ -15,265 +15,62 @@ on:
permissions: permissions:
contents: read contents: read
env:
DOCKER_USER: 1001:127
DOCKER_CONTAINER_REGISTRY_HOSTNAME: docker.io
DOCKER_CONTAINER_REGISTRY_NAMESPACE: lasuite
IS_MULTI_PLATFORM_BUILD: ${{ startsWith(github.ref, 'refs/tags/v') }}
BUILD_PLATFORMS: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
jobs: jobs:
build-and-push-backend: build-and-push:
runs-on: ubuntu-latest name: ${{ matrix.service }}
permissions: strategy:
contents: read fail-fast: false
steps: matrix:
- include:
name: Checkout repository - service: backend
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 image_name: lasuite/meet-backend
- context: .
name: Set up QEMU file: ./Dockerfile
if: env.IS_MULTI_PLATFORM_BUILD == 'true' target: backend-production
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - service: frontend
- image_name: lasuite/meet-frontend
name: Set up Docker Buildx context: .
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 file: ./src/frontend/Dockerfile
- target: frontend-production
name: Docker meta - service: frontend-dinum
id: meta image_name: lasuite/meet-frontend-dinum
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 context: .
with: file: ./docker/dinum-frontend/Dockerfile
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend' target: frontend-production
- - service: summary
name: Login to DockerHub image_name: lasuite/meet-summary
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') context: ./src/summary
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 file: ./src/summary/Dockerfile
with: target: production
username: ${{ secrets.DOCKER_HUB_USER }} - service: agents
password: ${{ secrets.DOCKER_HUB_PASSWORD }} image_name: lasuite/meet-agents
- context: ./src/agents
name: Run trivy scan file: ./src/agents/Dockerfile
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main target: production
with: uses: suitenumerique/ci/.github/workflows/_docker-publish.yml@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
docker-build-args: '--target backend-production -f Dockerfile' with:
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}' image_name: ${{ matrix.image_name }}
- context: ${{ matrix.context }}
name: Build and push file: ${{ matrix.file }}
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 target: ${{ matrix.target }}
with: docker_user: "1001:127"
context: . is_multi_platform: ${{ startsWith(github.ref, 'refs/tags/v') }}
target: backend-production should_push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
platforms: ${{ env.BUILD_PLATFORMS }} trivy_scan: true
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000 trivy_ignore_files: ./.github/.trivyignore
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }} secrets:
tags: ${{ steps.meta.outputs.tags }} DOCKER_HUB_USER: ${{ secrets.DOCKER_HUB_USER }}
labels: ${{ steps.meta.outputs.labels }} DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
build-and-push-frontend-generic:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
-
name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
-
name: Set up QEMU
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
-
name: Docker meta
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend'
-
name: Login to DockerHub
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
with:
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}'
-
name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: ./src/frontend/Dockerfile
target: frontend-production
platforms: ${{ env.BUILD_PLATFORMS }}
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-frontend-dinum:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
-
name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
-
name: Set up QEMU
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
-
name: Docker meta
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum'
-
name: Login to DockerHub
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
with:
docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}'
-
name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: ./docker/dinum-frontend/Dockerfile
target: frontend-production
platforms: ${{ env.BUILD_PLATFORMS }}
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-summary:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
-
name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
-
name: Set up QEMU
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
-
name: Docker meta
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary'
-
name: Login to DockerHub
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
continue-on-error: true
with:
docker-build-args: '-f src/summary/Dockerfile --target production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}'
docker-context: './src/summary'
-
name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: ./src/summary
file: ./src/summary/Dockerfile
target: production
platforms: ${{ env.BUILD_PLATFORMS }}
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-agents:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
-
name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
-
name: Set up QEMU
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
-
name: Docker meta
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: lasuite/meet-agents
-
name: Login to DockerHub
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
continue-on-error: true
with:
docker-build-args: '-f src/agents/Dockerfile --target production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}'
docker-context: './src/agents'
-
name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: ./src/agents
file: ./src/agents/Dockerfile
target: production
platforms: ${{ env.BUILD_PLATFORMS }}
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
notify-argocd: notify-argocd:
permissions: permissions:
contents: read contents: read
needs: needs:
- build-and-push-frontend-generic - build-and-push
- build-and-push-frontend-dinum
- build-and-push-backend
- build-and-push-summary
- build-and-push-agents
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
steps: steps:
- uses: numerique-gouv/action-argocd-webhook-notification@cac2ee67896eb13e84e804f60c4271370424eaa8 # main - uses: suitenumerique/ci/actions/argocd-webhook-notification@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
id: notify id: notify
with: with:
deployment_repo_path: "${{ secrets.DEPLOYMENT_REPO_URL }}" deployment_repo_path: "${{ secrets.DEPLOYMENT_REPO_URL }}"
+7 -23
View File
@@ -1,33 +1,17 @@
name: Release Chart name: Release Helm chart
run-name: Release Chart
on: on:
push: push:
branches:
- main
paths: paths:
- src/helm/meet/** - src/helm/meet/**
permissions:
contents: read
jobs: jobs:
release: release:
permissions: permissions:
contents: write contents: write
runs-on: ubuntu-latest uses: suitenumerique/ci/.github/workflows/_release-helm-chart.yml@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: 0
- name: Cleanup
run: rm -rf ./src/helm/extra
- name: Install Helm
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Publish Helm charts
uses: numerique-gouv/helm-gh-pages@2cf477ae49d7c70037ceb1685803f4f7bad9b981 # add-overwrite-option
with:
charts_dir: ./src/helm
linting: on
token: ${{ secrets.GITHUB_TOKEN }}
+21
View File
@@ -0,0 +1,21 @@
name: Security analysis
on:
push:
branches:
- main
pull_request:
branches:
- "**"
permissions: {}
jobs:
zizmor:
permissions:
contents: read
actions: read
security-events: write
uses: suitenumerique/ci/.github/workflows/_zizmor.yml@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
with:
config: .github/zizmor.yml
+5
View File
@@ -0,0 +1,5 @@
rules:
unpinned-uses:
config:
policies:
"suitenumerique/*": ref-pin
+15 -1
View File
@@ -10,6 +10,20 @@ and this project adheres to
### Added ### Added
- 🔒(backend) throttle meeting link generation
- 🔒️(backend) add a daily cap on room creation
### Fixed
- 🐛(frontend) enforce recording-mode permissions on the checkboxes
- 🔒️(agents) fix util-linux CVEs reported by Cyberwatch
- 🔒️(backend) fix HIGH CVEs in Django and urllib3
- 🔒️(agents) upgrade libpcre2-8-0 to fix CVE-2026-103111
## [1.33.0] - 2026-09-30
### Added
- ✨(backend) purge rooms inactive for a configurable period - ✨(backend) purge rooms inactive for a configurable period
- 🔨(makefile) add targets to list and download files stored in Garage - 🔨(makefile) add targets to list and download files stored in Garage
@@ -393,7 +407,7 @@ and this project adheres to
### Fixed ### Fixed
- ♿️(frontend) improve accessibilty of the Effects panel #1401 - ♿️(frontend) improve accessibility of the Effects panel #1401
## [1.20.0] - 2026-06-12 ## [1.20.0] - 2026-06-12
+2 -3
View File
@@ -37,14 +37,13 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev uv sync --locked --no-dev
# ---- mails ---- # ---- mails ----
FROM node:22 AS mail-builder FROM node:22-alpine AS mail-builder
COPY ./src/mail /mail/app COPY ./src/mail /mail/app
WORKDIR /mail/app WORKDIR /mail/app
RUN yarn install --frozen-lockfile && \ RUN npm ci --ignore-scripts && npm run build
yarn build
# ---- static link collector ---- # ---- static link collector ----
+8 -8
View File
@@ -55,12 +55,12 @@ function _docker_compose() {
function _dc_run() { function _dc_run() {
_set_user _set_user
user_args="--user=$USER_ID" user_args=()
if [ -z $USER_ID ]; then if [ -n "$USER_ID" ]; then
user_args="" user_args=("--user=$USER_ID")
fi fi
_docker_compose run --rm $user_args "$@" _docker_compose run --rm "${user_args[@]}" "$@"
} }
# _dc_exec: wrap docker compose exec command # _dc_exec: wrap docker compose exec command
@@ -74,12 +74,12 @@ function _dc_exec() {
echo "🐳(compose) exec command: '\$@'" echo "🐳(compose) exec command: '\$@'"
user_args="--user=$USER_ID" user_args=()
if [ -z $USER_ID ]; then if [ -n "$USER_ID" ]; then
user_args="" user_args=("--user=$USER_ID")
fi fi
_docker_compose exec $user_args "$@" _docker_compose exec "${user_args[@]}" "$@"
} }
# _django_manage: wrap django's manage.py command with docker compose # _django_manage: wrap django's manage.py command with docker compose
+1 -1
View File
@@ -40,7 +40,7 @@ if [ -n "$CUSTOM_LOGO_URL" ]; then
[[ "$IS_SVG" == false ]] && echo "[custom-logo] ERROR: not a valid SVG file" >&2 && exit 1 [[ "$IS_SVG" == false ]] && echo "[custom-logo] ERROR: not a valid SVG file" >&2 && exit 1
mv -f "$TMP_FILE" "$LOGO_FILE" mv -f "$TMP_FILE" "$LOGO_FILE"
echo "[custom-logo] INFO: Custom logo downloaded successfuly" echo "[custom-logo] INFO: Custom logo downloaded successfully"
fi fi
mv src/backend/* ./ mv src/backend/* ./
+1 -1
View File
@@ -7,7 +7,7 @@ gunicorn -b 0.0.0.0:8000 meet.wsgi:application --log-file - &
bin/run & bin/run &
# if the current shell is killed, also terminate all its children # if the current shell is killed, also terminate all its children
trap "pkill SIGTERM -P $$" SIGTERM trap 'pkill -TERM -P $$' SIGTERM
# wait for a single child to finish, # wait for a single child to finish,
wait -n wait -n
+4 -5
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -o errexit set -o errexit
CURRENT_DIR=$(pwd)
NAMESPACE=${1:-meet} NAMESPACE=${1:-meet}
SECRET_NAME=${2:-bitwarden-cli-meet} SECRET_NAME=${2:-bitwarden-cli-meet}
TEMP_SECRET_FILE=$(mktemp) TEMP_SECRET_FILE=$(mktemp)
@@ -30,10 +29,10 @@ check_secret_exists() {
# Collect user input securely # Collect user input securely
get_user_input() { get_user_input() {
echo "Please provide the following information:" echo "Please provide the following information:"
read -p "Enter your Vaultwarden email login: " LOGIN read -r -p "Enter your Vaultwarden email login: " LOGIN
read -s -p "Enter your Vaultwarden password: " PASSWORD read -r -s -p "Enter your Vaultwarden password: " PASSWORD
echo echo
read -p "Enter your Vaultwarden server url: " URL read -r -p "Enter your Vaultwarden server url: " URL
} }
# Create and apply the secret # Create and apply the secret
@@ -77,7 +76,7 @@ main() {
exit 0 exit 0
fi fi
echo -e ${TEMP_SECRET_FILE} echo -e "${TEMP_SECRET_FILE}"
get_user_input get_user_input
echo -e "\nCreating Vaultwarden secret…" echo -e "\nCreating Vaultwarden secret…"
+2 -2
View File
@@ -3,7 +3,7 @@
mkdir -p "$(dirname -- "${BASH_SOURCE[0]}")/../.git/hooks/" mkdir -p "$(dirname -- "${BASH_SOURCE[0]}")/../.git/hooks/"
PRE_COMMIT_FILE="$(dirname -- "${BASH_SOURCE[0]}")/../.git/hooks/pre-commit" PRE_COMMIT_FILE="$(dirname -- "${BASH_SOURCE[0]}")/../.git/hooks/pre-commit"
cat <<'EOF' >$PRE_COMMIT_FILE cat <<'EOF' >"$PRE_COMMIT_FILE"
#!/bin/bash #!/bin/bash
# directories containing potential secrets # directories containing potential secrets
@@ -27,4 +27,4 @@ for d in $DIRS; do
done done
EOF EOF
chmod +x $PRE_COMMIT_FILE chmod +x "$PRE_COMMIT_FILE"
+1 -1
View File
@@ -68,7 +68,7 @@ fi
# Ask user for release version number # Ask user for release version number
echo "" echo ""
read -p "Enter release version number (e.g., 1.2.3): " VERSION read -r -p "Enter release version number (e.g., 1.2.3): " VERSION
# Validate version format (basic semver check) # Validate version format (basic semver check)
if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+1
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
git submodule update --init --recursive git submodule update --init --recursive
# shellcheck disable=SC2016
git submodule foreach 'git fetch origin; git checkout $(git rev-parse --abbrev-ref HEAD); git reset --hard origin/$(git rev-parse --abbrev-ref HEAD); git submodule update --recursive; git clean -dfx' git submodule foreach 'git fetch origin; git checkout $(git rev-parse --abbrev-ref HEAD); git reset --hard origin/$(git rev-parse --abbrev-ref HEAD); git submodule update --recursive; git clean -dfx'
+1 -1
View File
@@ -8,6 +8,6 @@ environments=$(awk '/environments:/ {flag=1; next} flag && NF {print} !NF {flag=
for env in $environments; do for env in $environments; do
echo "################### $env lint ###################" echo "################### $env lint ###################"
helmfile -e $env -f src/helm/helmfile.yaml lint || exit 1 helmfile -e "$env" -f src/helm/helmfile.yaml lint || exit 1
echo -e "\n" echo -e "\n"
done done
+2 -2
View File
@@ -172,7 +172,7 @@ services:
working_dir: /app working_dir: /app
node: node:
image: node:22 image: node:22-alpine
user: "${DOCKER_USER:-1000}" user: "${DOCKER_USER:-1000}"
environment: environment:
HOME: /tmp HOME: /tmp
@@ -271,7 +271,7 @@ services:
- /app/.venv - /app/.venv
redis-summary: redis-summary:
image: redis image: redis:5
ports: ports:
- "6379:6379" - "6379:6379"
+1 -1
View File
@@ -1,7 +1,7 @@
# Bureautix proxy overrides # Bureautix proxy overrides
# #
# Builds submitted through the Docker API of the Podman service get none of # Builds submitted through the Docker API of the Podman service get none of
# the proxy variables in their RUN steps. We need to pass the http_proxy explicitely # the proxy variables in their RUN steps. We need to pass the http_proxy explicitly
# otherwise all connections fail during the build. # otherwise all connections fail during the build.
x-proxy-vars: &proxy-vars x-proxy-vars: &proxy-vars
+119 -117
View File
@@ -14,7 +14,7 @@ This document is a step-by-step guide that describes how to install LaSuite Meet
If you do not have a kubernetes test cluster, you can install everything on a local kind cluster. In this case, the simplest way is to use our script located in this repo under **bin/start-kind.sh**. If you do not have a kubernetes test cluster, you can install everything on a local kind cluster. In this case, the simplest way is to use our script located in this repo under **bin/start-kind.sh**.
IMPORTANT: The kind method will only deploy meet as a local instance(127.0.0.1) that can only be accessed from the device where it has been deployed. IMPORTANT: The kind method will only deploy meet as a local instance(127.0.0.1) that can only be accessed from the device where it has been deployed.
To be able to use the script, you will need to install the following components: To be able to use the script, you will need to install the following components:
@@ -311,120 +311,122 @@ frontend:
These are the environmental options available on meet backend. These are the environmental options available on meet backend.
| Option | Description | default | | Option | Description | default |
|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| |-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DATA_DIR | Data directory location | /data | | DATA_DIR | Data directory location | /data |
| DJANGO_ALLOWED_HOSTS | Hosts that are allowed | [] | | DJANGO_ALLOWED_HOSTS | Hosts that are allowed | [] |
| DJANGO_SECRET_KEY | Secret key used for Django security | | | DJANGO_SECRET_KEY | Secret key used for Django security | |
| DJANGO_SILENCED_SYSTEM_CHECKS | Silence Django system checks | [] | | DJANGO_SILENCED_SYSTEM_CHECKS | Silence Django system checks | [] |
| DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow unsecure user listing | false | | DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow unsecure user listing | false |
| DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 | | DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 |
| DB_NAME | Name of the database | meet | | DB_NAME | Name of the database | meet |
| DB_USER | User used to connect to database | dinum | | DB_USER | User used to connect to database | dinum |
| DB_PASSWORD | Password used to connect to the database | pass | | DB_PASSWORD | Password used to connect to the database | pass |
| DB_HOST | Hostname of the database | localhost | | DB_HOST | Hostname of the database | localhost |
| DB_PORT | Port to connect to database | 5432 | | DB_PORT | Port to connect to database | 5432 |
| STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage | | STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage |
| AWS_S3_ENDPOINT_URL | S3 host endpoint | | | AWS_S3_ENDPOINT_URL | S3 host endpoint | |
| AWS_S3_ACCESS_KEY_ID | S3 access key | | | AWS_S3_ACCESS_KEY_ID | S3 access key | |
| AWS_S3_SECRET_ACCESS_KEY | S3 secret key | | | AWS_S3_SECRET_ACCESS_KEY | S3 secret key | |
| AWS_S3_REGION_NAME | S3 region | | | AWS_S3_REGION_NAME | S3 region | |
| AWS_STORAGE_BUCKET_NAME | S3 bucket name | meet-media-storage | | AWS_STORAGE_BUCKET_NAME | S3 bucket name | meet-media-storage |
| DJANGO_LANGUAGE_CODE | Default language | en-us | | DJANGO_LANGUAGE_CODE | Default language | en-us |
| REDIS_URL | Redis endpoint | redis://redis:6379/1 | | REDIS_URL | Redis endpoint | redis://redis:6379/1 |
| SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) | | SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) |
| REQUEST_ENTRY_THROTTLE_RATES | Entry request throttle rates | 150/minute | | ROOM_CREATION_THROTTLE_RATES | Room creation throttle rate per authenticated user | 50/minute | 50/minute |
| CREATION_CALLBACK_THROTTLE_RATES | Creation callback throttle rates | 600/minute | | ROOM_CREATION_DAILY_THROTTLE_RATES | Daily room creation cap per authenticated user | 1000/day |
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | Enable Django deploy check | false | | REQUEST_ENTRY_THROTTLE_RATES | Entry request throttle rates | 150/minute |
| CSRF_TRUSTED_ORIGINS | CSRF trusted origins list | [] | | CREATION_CALLBACK_THROTTLE_RATES | Creation callback throttle rates | 600/minute |
| FRONTEND_CUSTOM_CSS_URL | URL of an additional CSS file to load in the frontend app. If set, a `<link>` tag with this URL as href is added to the `<head>` of the frontend app | | | SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | Enable Django deploy check | false |
| FRONTEND_ANALYTICS | Analytics information | {} | | CSRF_TRUSTED_ORIGINS | CSRF trusted origins list | [] |
| FRONTEND_SUPPORT | Crisp frontend support configuration, also you can pass help articles, with `help_article_transcript`, `help_article_recording`, `help_article_more_tools` | {} | | FRONTEND_CUSTOM_CSS_URL | URL of an additional CSS file to load in the frontend app. If set, a `<link>` tag with this URL as href is added to the `<head>` of the frontend app | |
| FRONTEND_MANIFEST_LINK | Link to the "Learn more" button on the homepage | {} | | FRONTEND_ANALYTICS | Analytics information | {} |
| FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false | | FRONTEND_SUPPORT | Crisp frontend support configuration, also you can pass help articles, with `help_article_transcript`, `help_article_recording`, `help_article_more_tools` | {} |
| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true | | FRONTEND_MANIFEST_LINK | Link to the "Learn more" button on the homepage | {} |
| FRONTEND_FEEDBACK | Frontend feedback configuration | {} | | FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false |
| FRONTEND_DOCUMENTATION_URL | URL of the documentation opened from the room options menu. If unset, the documentation menu item is hidden | | | FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true |
| FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false | | FRONTEND_FEEDBACK | Frontend feedback configuration | {} |
| FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false | | FRONTEND_DOCUMENTATION_URL | URL of the documentation opened from the room options menu. If unset, the documentation menu item is hidden | |
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend | | FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false |
| DJANGO_EMAIL_HOST | Host of the email server | | | FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false |
| DJANGO_EMAIL_HOST_USER | User to connect to the email server | | | DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
| DJANGO_EMAIL_HOST_PASSWORD | Password to connect to the email server | | | DJANGO_EMAIL_HOST | Host of the email server | |
| DJANGO_EMAIL_PORT | Port to connect to the email server | | | DJANGO_EMAIL_HOST_USER | User to connect to the email server | |
| DJANGO_EMAIL_USE_TLS | Enable TLS on email connection | false | | DJANGO_EMAIL_HOST_PASSWORD | Password to connect to the email server | |
| DJANGO_EMAIL_USE_SSL | Enable SSL on email connection | false | | DJANGO_EMAIL_PORT | Port to connect to the email server | |
| DJANGO_EMAIL_FROM | Email from account | from@example.com | | DJANGO_EMAIL_USE_TLS | Enable TLS on email connection | false |
| EMAIL_BRAND_NAME | Email branding name | | | DJANGO_EMAIL_USE_SSL | Enable SSL on email connection | false |
| EMAIL_SUPPORT_EMAIL | Support email address | | | DJANGO_EMAIL_FROM | Email from account | from@example.com |
| EMAIL_LOGO_IMG | Email logo image | | | EMAIL_BRAND_NAME | Email branding name | |
| EMAIL_DOMAIN | Email domain | | | EMAIL_SUPPORT_EMAIL | Support email address | |
| EMAIL_APP_BASE_URL | Email app base URL | | | EMAIL_LOGO_IMG | Email logo image | |
| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false | | EMAIL_DOMAIN | Email domain | |
| DJANGO_CORS_ALLOWED_ORIGINS | Origins to allow (string list) | [] | | EMAIL_APP_BASE_URL | Email app base URL | |
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | Origins to allow (regex patterns) | [] | | DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false |
| SENTRY_DSN | Sentry server DSN | | | DJANGO_CORS_ALLOWED_ORIGINS | Origins to allow (string list) | [] |
| DJANGO_CELERY_BROKER_URL | Celery broker host | redis://redis:6379/0 | | DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | Origins to allow (regex patterns) | [] |
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker options | {} | | SENTRY_DSN | Sentry server DSN | |
| OIDC_CREATE_USER | Create OIDC user if not exists | true | | DJANGO_CELERY_BROKER_URL | Celery broker host | redis://redis:6379/0 |
| OIDC_VERIFY_SSL | Verify SSL for OIDC | true | | DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker options | {} |
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | false | | OIDC_CREATE_USER | Create OIDC user if not exists | true |
| OIDC_RP_SIGN_ALGO | Token verification algorithm used by OIDC | RS256 | | OIDC_VERIFY_SSL | Verify SSL for OIDC | true |
| OIDC_RP_CLIENT_ID | OIDC client ID | meet | | OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | false |
| OIDC_RP_CLIENT_SECRET | OIDC client secret | | | OIDC_RP_SIGN_ALGO | Token verification algorithm used by OIDC | RS256 |
| OIDC_OP_JWKS_ENDPOINT | OIDC endpoint for JWKS | | | OIDC_RP_CLIENT_ID | OIDC client ID | meet |
| OIDC_OP_AUTHORIZATION_ENDPOINT | OIDC endpoint for authorization | | | OIDC_RP_CLIENT_SECRET | OIDC client secret | |
| OIDC_OP_TOKEN_ENDPOINT | OIDC endpoint for token | | | OIDC_OP_JWKS_ENDPOINT | OIDC endpoint for JWKS | |
| OIDC_OP_USER_ENDPOINT | OIDC endpoint for user | | | OIDC_OP_AUTHORIZATION_ENDPOINT | OIDC endpoint for authorization | |
| OIDC_OP_USER_ENDPOINT_FORMAT | OIDC endpoint format (AUTO, JWT, JSON) | AUTO | | OIDC_OP_TOKEN_ENDPOINT | OIDC endpoint for token | |
| OIDC_OP_LOGOUT_ENDPOINT | OIDC endpoint for logout | | | OIDC_OP_USER_ENDPOINT | OIDC endpoint for user | |
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | Extra parameters for OIDC request | {} | | OIDC_OP_USER_ENDPOINT_FORMAT | OIDC endpoint format (AUTO, JWT, JSON) | AUTO |
| OIDC_RP_SCOPES | OIDC scopes | openid email | | OIDC_OP_LOGOUT_ENDPOINT | OIDC endpoint for logout | |
| OIDC_USE_NONCE | Use nonce for OIDC | true | | OIDC_AUTH_REQUEST_EXTRA_PARAMS | Extra parameters for OIDC request | {} |
| OIDC_REDIRECT_REQUIRE_HTTPS | Require HTTPS for OIDC | false | | OIDC_RP_SCOPES | OIDC scopes | openid email |
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed redirect hosts for OIDC | [] | | OIDC_USE_NONCE | Use nonce for OIDC | true |
| OIDC_STORE_ID_TOKEN | Store OIDC ID token | true | | OIDC_REDIRECT_REQUIRE_HTTPS | Require HTTPS for OIDC | false |
| OIDC_REDIRECT_FIELD_NAME | Redirect field for OIDC | returnTo | | OIDC_REDIRECT_ALLOWED_HOSTS | Allowed redirect hosts for OIDC | [] |
| OIDC_USERINFO_FULLNAME_FIELDS | Full name claim from OIDC token | ["given_name", "usual_name"] | | OIDC_STORE_ID_TOKEN | Store OIDC ID token | true |
| OIDC_USERINFO_SHORTNAME_FIELD | Short name claim from OIDC token | given_name | | OIDC_REDIRECT_FIELD_NAME | Redirect field for OIDC | returnTo |
| OIDC_USERINFO_ESSENTIAL_CLAIMS | Required claims from OIDC token | [] | | OIDC_USERINFO_FULLNAME_FIELDS | Full name claim from OIDC token | ["given_name", "usual_name"] |
| OIDC_USE_PKCE | Enable the use of PKCE (Proof Key for Code Exchange) during the OAuth 2.0 authorization code flow. Recommended for enhanced security. | False | | OIDC_USERINFO_SHORTNAME_FIELD | Short name claim from OIDC token | given_name |
| OIDC_PKCE_CODE_CHALLENGE_METHOD | Method used to generate the PKCE code challenge. Common values include S256 and plain. Refer to the mozilla-django-oidc documentation for supported options. | S256 | | OIDC_USERINFO_ESSENTIAL_CLAIMS | Required claims from OIDC token | [] |
| OIDC_PKCE_CODE_VERIFIER_SIZE | Length of the random string used as the PKCE code verifier. Must be an integer between 43 and 128, inclusive. | 64 | | OIDC_USE_PKCE | Enable the use of PKCE (Proof Key for Code Exchange) during the OAuth 2.0 authorization code flow. Recommended for enhanced security. | False |
| LOGIN_REDIRECT_URL | Login redirect URL | | | OIDC_PKCE_CODE_CHALLENGE_METHOD | Method used to generate the PKCE code challenge. Common values include S256 and plain. Refer to the mozilla-django-oidc documentation for supported options. | S256 |
| LOGIN_REDIRECT_URL_FAILURE | Login redirect URL for failure | | | OIDC_PKCE_CODE_VERIFIER_SIZE | Length of the random string used as the PKCE code verifier. Must be an integer between 43 and 128, inclusive. | 64 |
| LOGOUT_REDIRECT_URL | URL to redirect to on logout | | | LOGIN_REDIRECT_URL | Login redirect URL | |
| ALLOW_LOGOUT_GET_METHOD | Allow logout through GET method | true | | LOGIN_REDIRECT_URL_FAILURE | Login redirect URL for failure | |
| LIVEKIT_API_KEY | LiveKit API key | | | LOGOUT_REDIRECT_URL | URL to redirect to on logout | |
| LIVEKIT_API_SECRET | LiveKit API secret | | | ALLOW_LOGOUT_GET_METHOD | Allow logout through GET method | true |
| LIVEKIT_API_URL | LiveKit API URL | | | LIVEKIT_API_KEY | LiveKit API key | |
| LIVEKIT_VERIFY_SSL | Verify SSL for LiveKit connections | true | | LIVEKIT_API_SECRET | LiveKit API secret | |
| LIVEKIT_FORCE_WSS_PROTOCOL | Enables WSS protocol conversion for legacy browser compatibility (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs fail in WebSocket() constructor. | false | | LIVEKIT_API_URL | LiveKit API URL | |
| LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND | Firefox-only connection warmup: pre-calls WebSocket endpoint (expecting 401) to initialize cache, resolving proxy/network connectivity issues. | false | | LIVEKIT_VERIFY_SSL | Verify SSL for LiveKit connections | true |
| RESOURCE_DEFAULT_ACCESS_LEVEL | Default resource access level for rooms | public | | LIVEKIT_FORCE_WSS_PROTOCOL | Enables WSS protocol conversion for legacy browser compatibility (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs fail in WebSocket() constructor. | false |
| ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true | | LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND | Firefox-only connection warmup: pre-calls WebSocket endpoint (expecting 401) to initialize cache, resolving proxy/network connectivity issues. | false |
| ROOM_INACTIVITY_DELETION_DAYS | Days without being started after which a room is purged. Unset to never purge | | | RESOURCE_DEFAULT_ACCESS_LEVEL | Default resource access level for rooms | public |
| RECORDING_ENABLE | Record meeting option | false | | ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true |
| RECORDING_OUTPUT_FOLDER | Folder to store meetings | recordings | | ROOM_INACTIVITY_DELETION_DAYS | Days without being started after which a room is purged. Unset to never purge | |
| RECORDING_ENABLE | Record meeting option | false |
| RECORDING_OUTPUT_FOLDER | Folder to store meetings | recordings |
| RECORDING_WORKER_CLASSES | Worker classes for recording | {"screen_recording": "core.recording.worker.services.VideoCompositeEgressService","transcript": "core.recording.worker.services.AudioCompositeEgressService"} | | RECORDING_WORKER_CLASSES | Worker classes for recording | {"screen_recording": "core.recording.worker.services.VideoCompositeEgressService","transcript": "core.recording.worker.services.AudioCompositeEgressService"} |
| RECORDING_EXPIRATION_DAYS | Recording expiration in days | | | RECORDING_EXPIRATION_DAYS | Recording expiration in days | |
| RECORDING_MAX_DURATION | Maximum recording duration in milliseconds. Must match LiveKit Egress configuration exactly. | | | RECORDING_MAX_DURATION | Maximum recording duration in milliseconds. Must match LiveKit Egress configuration exactly. | |
| SCREEN_RECORDING_BASE_URL | Screen recording base URL | | | SCREEN_RECORDING_BASE_URL | Screen recording base URL | |
| SUMMARY_SERVICE_ENDPOINT | Summary service endpoint | | | SUMMARY_SERVICE_ENDPOINT | Summary service endpoint | |
| SUMMARY_SERVICE_API_TOKEN | API token for summary service | | | SUMMARY_SERVICE_API_TOKEN | API token for summary service | |
| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Signup users to marketing emails | false | | SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Signup users to marketing emails | false |
| MARKETING_SERVICE_CLASS | Marketing service class | core.services.marketing.BrevoMarketingService | | MARKETING_SERVICE_CLASS | Marketing service class | core.services.marketing.BrevoMarketingService |
| BREVO_API_KEY | Brevo API key for marketing emails | | | BREVO_API_KEY | Brevo API key for marketing emails | |
| BREVO_API_CONTACT_LIST_IDS | Brevo API contact list IDs | [] | | BREVO_API_CONTACT_LIST_IDS | Brevo API contact list IDs | [] |
| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | Brevo contact attributes | {"VISIO_USER": true} | | DJANGO_BREVO_API_CONTACT_ATTRIBUTES | Brevo contact attributes | {"VISIO_USER": true} |
| BREVO_API_TIMEOUT | Brevo timeout in seconds | 1 | | BREVO_API_TIMEOUT | Brevo timeout in seconds | 1 |
| LOBBY_KEY_PREFIX | Lobby key prefix | room_lobby | | LOBBY_KEY_PREFIX | Lobby key prefix | room_lobby |
| LOBBY_WAITING_TIMEOUT | Lobby waiting timeout in seconds | 3 | | LOBBY_WAITING_TIMEOUT | Lobby waiting timeout in seconds | 3 |
| LOBBY_DENIED_TIMEOUT | Lobby deny timeout in seconds | 5 | | LOBBY_DENIED_TIMEOUT | Lobby deny timeout in seconds | 5 |
| LOBBY_ACCEPTED_TIMEOUT | Lobby accept timeout in seconds | 21600 (6 hours) | | LOBBY_ACCEPTED_TIMEOUT | Lobby accept timeout in seconds | 21600 (6 hours) |
| LOBBY_NOTIFICATION_TYPE | Lobby notification types | participantWaiting | | LOBBY_NOTIFICATION_TYPE | Lobby notification types | participantWaiting |
| LOBBY_COOKIE_NAME | Lobby cookie name | lobbyParticipantId | | LOBBY_COOKIE_NAME | Lobby cookie name | lobbyParticipantId |
| ROOM_CREATION_CALLBACK_CACHE_TIMEOUT | Room creation callback cache timeout | 600 (10 minutes) | | ROOM_CREATION_CALLBACK_CACHE_TIMEOUT | Room creation callback cache timeout | 600 (10 minutes) |
| ROOM_TELEPHONY_ENABLED | Enable SIP telephony feature | false | | ROOM_TELEPHONY_ENABLED | Enable SIP telephony feature | false |
| ROOM_TELEPHONY_PIN_LENGTH | Telephony PIN length | 10 | | ROOM_TELEPHONY_PIN_LENGTH | Telephony PIN length | 10 |
| ROOM_TELEPHONY_PIN_MAX_RETRIES | Telephony PIN maximum retries | 5 | | ROOM_TELEPHONY_PIN_MAX_RETRIES | Telephony PIN maximum retries | 5 |
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"extends": ["github>numerique-gouv/renovate-configuration"], "extends": ["github>suitenumerique/ci//renovate/default"],
"dependencyDashboard": true, "dependencyDashboard": true,
"labels": ["dependencies", "noChangeLog"], "labels": ["dependencies", "noChangeLog"],
"packageRules": [ "packageRules": [
+1 -1
View File
@@ -21,7 +21,7 @@ const { initI18n, translateUI } = require("../common/i18n");
document.querySelector("#close-msg").style.display = "block"; document.querySelector("#close-msg").style.display = "block";
}) })
.catch((e) => { .catch((e) => {
console.error(`Error occured: ${e}`); console.error(`Error occurred: ${e}`);
}) })
.finally(() => { .finally(() => {
// NOTE: doesn't work with the desktop client — the browser considers // NOTE: doesn't work with the desktop client — the browser considers
+2 -1
View File
@@ -1,10 +1,11 @@
FROM python:3.14.6-slim AS base FROM python:3.14.7-slim AS base
# Install system dependencies required by LiveKit, fetching packages over HTTPS only for Bureautix proxy # Install system dependencies required by LiveKit, fetching packages over HTTPS only for Bureautix proxy
RUN sed -i "s|^URIs: http://|URIs: https://|" /etc/apt/sources.list.d/debian.sources \ RUN sed -i "s|^URIs: http://|URIs: https://|" /etc/apt/sources.list.d/debian.sources \
&& apt-get update && apt-get install -y --no-install-recommends \ && apt-get update && apt-get install -y --no-install-recommends \
libglib2.0-0 \ libglib2.0-0 \
libgobject-2.0-0 \ libgobject-2.0-0 \
libpcre2-8-0 \
libssl3t64 \ libssl3t64 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "agents" name = "agents"
version = "1.32.1" version = "1.33.0"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"livekit-agents==1.7.0", "livekit-agents==1.7.0",
+1 -1
View File
@@ -9,7 +9,7 @@ resolution-markers = [
[[package]] [[package]]
name = "agents" name = "agents"
version = "1.32.1" version = "1.33.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "boto3" }, { name = "boto3" },
+27
View File
@@ -20,6 +20,33 @@ class MonitoredUserRateThrottle(MonitoredThrottleMixin, UserRateThrottle):
"""Throttle for the monitored scoped rate throttle.""" """Throttle for the monitored scoped rate throttle."""
class RoomCreationUserRateThrottle(MonitoredUserRateThrottle):
"""Throttle room creation per authenticated user.
Can be declared at the viewset level: every action other than "create"
is left unthrottled, so the same class can be reused on any viewset
exposing a room creation endpoint.
"""
scope = "room_creation"
def get_cache_key(self, request, view):
"""Throttle only room creations."""
if getattr(view, "action", None) != "create":
return None
return super().get_cache_key(request, view)
class RoomCreationDailyUserRateThrottle(RoomCreationUserRateThrottle):
"""Cap room creation per authenticated user over a day.
Complements the short-term RoomCreationUserRateThrottle, which absorbs
bursts but lets a user steadily create rooms over hours or days.
"""
scope = "room_creation_daily"
class RequestEntryAuthenticatedUserRateThrottle(MonitoredUserRateThrottle): class RequestEntryAuthenticatedUserRateThrottle(MonitoredUserRateThrottle):
"""Throttle authenticated user requesting room entry""" """Throttle authenticated user requesting room entry"""
+4
View File
@@ -180,6 +180,10 @@ class RoomViewSet(
permission_classes = [permissions.RoomPermissions] permission_classes = [permissions.RoomPermissions]
queryset = models.Room.objects.all() queryset = models.Room.objects.all()
serializer_class = serializers.RoomSerializer serializer_class = serializers.RoomSerializer
throttle_classes = [
throttling.RoomCreationUserRateThrottle,
throttling.RoomCreationDailyUserRateThrottle,
]
def get_object(self): def get_object(self):
"""Allow getting a room by its slug.""" """Allow getting a room by its slug."""
@@ -24,7 +24,7 @@ class BaseEgressService:
def _get_filepath(self, filename: str, extension: str) -> str: def _get_filepath(self, filename: str, extension: str) -> str:
"""Construct the file path for a given filename and extension. """Construct the file path for a given filename and extension.
Unsecure method, doesn't handle paths robustly and securely. Insecure method, doesn't handle paths robustly and securely.
""" """
return f"{self._config.output_folder}/{filename}.{extension}" return f"{self._config.output_folder}/{filename}.{extension}"
@@ -27,7 +27,7 @@ def test_api_files_list_anonymous_not_allowed():
def test_api_files_list_authentificated_user_allowed(): def test_api_files_list_authentificated_user_allowed():
""" """
Authentificated users should be allowed to list files Authenticated users should be allowed to list files
""" """
user = factories.UserFactory() user = factories.UserFactory()
client = APIClient() client = APIClient()
@@ -9,6 +9,10 @@ from django.core.cache import cache
import pytest import pytest
from rest_framework.test import APIClient from rest_framework.test import APIClient
from ...api.throttling import (
RoomCreationDailyUserRateThrottle,
RoomCreationUserRateThrottle,
)
from ...factories import RoomFactory, UserFactory from ...factories import RoomFactory, UserFactory
from ...models import Room, RoomAccessLevel from ...models import Room, RoomAccessLevel
@@ -312,3 +316,145 @@ def test_api_rooms_create_authenticated_blank_user_default_access_level():
assert response.status_code == 201 assert response.status_code == 201
room = Room.objects.get() room = Room.objects.get()
assert room.access_level == settings.RESOURCE_DEFAULT_ACCESS_LEVEL assert room.access_level == settings.RESOURCE_DEFAULT_ACCESS_LEVEL
@pytest.fixture
def room_creation_throttle(monkeypatch):
"""Lower the room creation rate for the duration of a test."""
monkeypatch.setitem(
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"], "room_creation", "2/minute"
)
def test_api_rooms_create_throttled(room_creation_throttle):
"""Excess requests are rejected and create no room."""
client = APIClient()
client.force_login(UserFactory())
for index in range(2):
response = client.post("/api/v1.0/rooms/", {"name": f"Room {index}"})
assert response.status_code == 201
response = client.post("/api/v1.0/rooms/", {"name": "Blocked room"})
assert response.status_code == 429
assert 0 < int(response["Retry-After"]) <= 60
assert Room.objects.count() == 2
def test_api_rooms_create_throttle_per_user(room_creation_throttle):
"""Users sharing an IP have independent creation limits."""
client = APIClient()
client.force_login(UserFactory())
for index in range(2):
response = client.post("/api/v1.0/rooms/", {"name": f"First user room {index}"})
assert response.status_code == 201
response = client.post("/api/v1.0/rooms/", {"name": "Blocked room"})
assert response.status_code == 429
client.force_login(UserFactory())
response = client.post("/api/v1.0/rooms/", {"name": "Second user room"})
assert response.status_code == 201
def test_api_rooms_create_throttle_does_not_limit_other_actions(room_creation_throttle):
"""Exhausting creation capacity leaves listing and updating available."""
client = APIClient()
client.force_login(UserFactory())
for index in range(2):
response = client.post("/api/v1.0/rooms/", {"name": f"Room {index}"})
assert response.status_code == 201
room_id = response.json()["id"]
assert client.post("/api/v1.0/rooms/", {"name": "Blocked room"}).status_code == 429
assert client.get("/api/v1.0/rooms/").status_code == 200
assert (
client.patch(
f"/api/v1.0/rooms/{room_id}/", {"name": "Renamed room"}
).status_code
== 200
)
@pytest.fixture
def daily_room_creation_throttle(monkeypatch):
"""Use a tiny daily cap, a loose burst limit and a controllable clock.
Rates are patched with monkeypatch.setitem so they are restored after the
test. Returns a one-item list holding the current fake timestamp.
"""
rates = RoomCreationDailyUserRateThrottle.THROTTLE_RATES
monkeypatch.setitem(rates, "room_creation", "100/minute")
monkeypatch.setitem(rates, "room_creation_daily", "3/day")
now = [1_000_000.0]
monkeypatch.setattr(RoomCreationUserRateThrottle, "timer", lambda self: now[0])
return now
def test_api_rooms_create_daily_throttled(daily_room_creation_throttle):
"""The daily cap still applies once the short-term window has elapsed."""
now = daily_room_creation_throttle
client = APIClient()
client.force_login(UserFactory())
for index in range(3):
response = client.post("/api/v1.0/rooms/", {"name": f"Room {index}"})
assert response.status_code == 201
now[0] += 120 # Spread creations beyond the short-term window.
response = client.post("/api/v1.0/rooms/", {"name": "Blocked room"})
assert response.status_code == 429
assert int(response["Retry-After"]) > 60
assert Room.objects.count() == 3
def test_api_rooms_create_daily_throttle_resets(daily_room_creation_throttle):
"""Room creation is allowed again once a day has passed."""
now = daily_room_creation_throttle
client = APIClient()
client.force_login(UserFactory())
for index in range(3):
response = client.post("/api/v1.0/rooms/", {"name": f"Room {index}"})
assert response.status_code == 201
response = client.post("/api/v1.0/rooms/", {"name": "Blocked room"})
assert response.status_code == 429
now[0] += 24 * 60 * 60 + 1
response = client.post("/api/v1.0/rooms/", {"name": "Next day room"})
assert response.status_code == 201
def test_api_rooms_create_daily_throttle_per_user(daily_room_creation_throttle):
"""Each user has its own daily cap."""
client = APIClient()
client.force_login(UserFactory())
for index in range(3):
response = client.post("/api/v1.0/rooms/", {"name": f"Room {index}"})
assert response.status_code == 201
assert client.post("/api/v1.0/rooms/", {"name": "Blocked"}).status_code == 429
client.force_login(UserFactory())
response = client.post("/api/v1.0/rooms/", {"name": "Other user room"})
assert response.status_code == 201
def test_api_rooms_create_daily_throttle_does_not_limit_other_actions(
daily_room_creation_throttle,
):
"""Reaching the daily cap leaves listing and updating available."""
client = APIClient()
client.force_login(UserFactory())
for index in range(3):
response = client.post("/api/v1.0/rooms/", {"name": f"Room {index}"})
assert response.status_code == 201
room_id = response.json()["id"]
assert client.post("/api/v1.0/rooms/", {"name": "Blocked"}).status_code == 429
assert client.get("/api/v1.0/rooms/").status_code == 200
response = client.patch(f"/api/v1.0/rooms/{room_id}/", {"name": "Renamed"})
assert response.status_code == 200
@@ -458,7 +458,7 @@ def test_mute_participant_livekit_token_presence_check_twirp_error_forbidden(
room = RoomFactory() room = RoomFactory()
mock_livekit_client.room.get_participant.side_effect = TwirpError( mock_livekit_client.room.get_participant.side_effect = TwirpError(
msg="an error occured", code="not_found", status=500 msg="an error occurred", code="not_found", status=500
) )
user = AnonymousUser() user = AnonymousUser()
@@ -1,5 +1,5 @@
""" """
Test SIP mamagement service. Test SIP management service.
""" """
# pylint: disable=W0212 # pylint: disable=W0212
+10
View File
@@ -361,6 +361,16 @@ class Base(Configuration):
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning", "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_THROTTLE_RATES": { "DEFAULT_THROTTLE_RATES": {
"room_creation": values.Value(
default="50/minute",
environ_name="ROOM_CREATION_THROTTLE_RATES",
environ_prefix=None,
),
"room_creation_daily": values.Value(
default="1000/day",
environ_name="ROOM_CREATION_DAILY_THROTTLE_RATES",
environ_prefix=None,
),
"request_entry": values.Value( "request_entry": values.Value(
default="150/minute", default="150/minute",
environ_name="REQUEST_ENTRY_THROTTLE_RATES", environ_name="REQUEST_ENTRY_THROTTLE_RATES",
+3 -3
View File
@@ -7,7 +7,7 @@ build-backend = "uv_build"
[project] [project]
name = "meet" name = "meet"
version = "1.32.1" version = "1.33.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }] authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
@@ -40,7 +40,7 @@ dependencies = [
"django-storages[s3]==1.14.6", "django-storages[s3]==1.14.6",
"django-timezone-field>=5.1", "django-timezone-field>=5.1",
"django-pydantic-field==0.5.4", "django-pydantic-field==0.5.4",
"django==5.2.16", "django==5.2.17",
"djangorestframework==3.18.0", "djangorestframework==3.18.0",
"drf_spectacular==0.30.0", "drf_spectacular==0.30.0",
"dockerflow==2026.3.4", "dockerflow==2026.3.4",
@@ -62,7 +62,7 @@ dependencies = [
"mozilla-django-oidc==5.0.2", "mozilla-django-oidc==5.0.2",
"livekit-api==1.2.0", "livekit-api==1.2.0",
"aiohttp==3.14.3", "aiohttp==3.14.3",
"urllib3==2.7.0", "urllib3==2.8.0",
"phonenumbers==9.0.37", "phonenumbers==9.0.37",
"cryptography==50.0.1", # CVE-2026-69247 "cryptography==50.0.1", # CVE-2026-69247
] ]
+9 -9
View File
@@ -700,16 +700,16 @@ wheels = [
[[package]] [[package]]
name = "django" name = "django"
version = "5.2.16" version = "5.2.17"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "asgiref" }, { name = "asgiref" },
{ name = "sqlparse" }, { name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/a9/26/889449d521ae508b26de715954faecd8bcf3f740affb81b2d146a83b42a5/django-5.2.16.tar.gz", hash = "sha256:59ea02020c3136fce14bef0bbece21a10a4febef5eed1c51c22ae468efa22200", size = 10890894, upload-time = "2026-07-07T13:52:17.005Z" } sdist = { url = "https://files.pythonhosted.org/packages/d5/d8/43e9d000519adceb189620b6869ff88031e046df91c2e9da72f8f6918399/django-5.2.17.tar.gz", hash = "sha256:9d4d93be539a18ab80d058eb515900e10951e04c537c5a6b394fc49528d3251f", size = 10889740, upload-time = "2026-08-04T15:04:03.173Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/13/1e5e3e4c15dcecb04281b3cb2a46a4670e1cef131068e202f6040df19224/django-5.2.16-py3-none-any.whl", hash = "sha256:04f354bf9d807a86ad1a8392fe3808d362358a8eafc322848e0e43e59b24371d", size = 8311943, upload-time = "2026-07-07T13:52:11.223Z" }, { url = "https://files.pythonhosted.org/packages/df/f8/ce120525ca78f12b07daf65786679c5d0b54a75285a8958d3ae55e39da35/django-5.2.17-py3-none-any.whl", hash = "sha256:f04fb3b36ee119e1af4fa1d397d5fd6cf12700f49321e84d4f4c642c5b1973db", size = 8315563, upload-time = "2026-08-04T15:03:59.1Z" },
] ]
[[package]] [[package]]
@@ -1297,7 +1297,7 @@ wheels = [
[[package]] [[package]]
name = "meet" name = "meet"
version = "1.32.1" version = "1.33.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiohttp" }, { name = "aiohttp" },
@@ -1372,7 +1372,7 @@ requires-dist = [
{ name = "celery", extras = ["redis"], specifier = "==5.6.3" }, { name = "celery", extras = ["redis"], specifier = "==5.6.3" },
{ name = "cryptography", specifier = "==50.0.1" }, { name = "cryptography", specifier = "==50.0.1" },
{ name = "dj-database-url", specifier = "==3.1.2" }, { name = "dj-database-url", specifier = "==3.1.2" },
{ name = "django", specifier = "==5.2.16" }, { name = "django", specifier = "==5.2.17" },
{ name = "django-configurations", specifier = "==2.5.1" }, { name = "django-configurations", specifier = "==2.5.1" },
{ name = "django-cors-headers", specifier = "==4.9.0" }, { name = "django-cors-headers", specifier = "==4.9.0" },
{ name = "django-countries", specifier = "==9.0.0" }, { name = "django-countries", specifier = "==9.0.0" },
@@ -1404,7 +1404,7 @@ requires-dist = [
{ name = "redis", specifier = "==5.2.1" }, { name = "redis", specifier = "==5.2.1" },
{ name = "requests", specifier = "==2.34.2" }, { name = "requests", specifier = "==2.34.2" },
{ name = "sentry-sdk", specifier = "==2.68.1" }, { name = "sentry-sdk", specifier = "==2.68.1" },
{ name = "urllib3", specifier = "==2.7.0" }, { name = "urllib3", specifier = "==2.8.0" },
{ name = "whitenoise", specifier = "==6.12.0" }, { name = "whitenoise", specifier = "==6.12.0" },
] ]
@@ -2529,11 +2529,11 @@ wheels = [
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.7.0" version = "2.8.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" },
] ]
[[package]] [[package]]
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "meet", "name": "meet",
"version": "1.32.1", "version": "1.33.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "meet", "name": "meet",
"version": "1.32.1", "version": "1.33.0",
"dependencies": { "dependencies": {
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0", "@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
"@fontsource-variable/lexend": "5.3.0", "@fontsource-variable/lexend": "5.3.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "meet", "name": "meet",
"private": true, "private": true,
"version": "1.32.1", "version": "1.33.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "panda codegen && vite", "dev": "panda codegen && vite",
+1 -1
View File
@@ -121,7 +121,7 @@ const config: Config = {
}, },
tokens: defineTokens({ tokens: defineTokens({
/* we take a few things from the panda preset but for now we clear out some stuff. /* we take a few things from the panda preset but for now we clear out some stuff.
* This way we'll only add the things we need step by step and prevent using lots of differents things. * This way we'll only add the things we need step by step and prevent using lots of different things.
*/ */
...pandaPreset.theme.tokens, ...pandaPreset.theme.tokens,
colors: defineTokens.colors({ colors: defineTokens.colors({
@@ -45,6 +45,10 @@ export const ScreenRecordingSidePanel = () => {
FeatureFlags.ScreenRecording FeatureFlags.ScreenRecording
) )
const hasTranscriptAccess = useHasRecordingAccess(
RecordingMode.Transcript,
FeatureFlags.Transcript
)
const { notifyParticipants } = useNotifyParticipants() const { notifyParticipants } = useNotifyParticipants()
const { selectedLanguageKey, isLanguageSetToAuto } = const { selectedLanguageKey, isLanguageSetToAuto } =
useTranscriptionLanguage() useTranscriptionLanguage()
@@ -88,7 +92,7 @@ export const ScreenRecordingSidePanel = () => {
...(!isLanguageSetToAuto && { ...(!isLanguageSetToAuto && {
language: selectedLanguageKey, language: selectedLanguageKey,
}), }),
...(includeTranscript && { transcribe: true }), ...(includeTranscript && hasTranscriptAccess && { transcribe: true }),
} }
await startRecording({ await startRecording({
@@ -182,24 +186,26 @@ export const ScreenRecordingSidePanel = () => {
<RowWrapper iconName="mail" position="last"> <RowWrapper iconName="mail" position="last">
<Text variant="sm">{t('details.receiver')}</Text> <Text variant="sm">{t('details.receiver')}</Text>
</RowWrapper> </RowWrapper>
{hasTranscriptAccess && (
<div className={css({ height: '15px' })} /> <>
<div className={css({ height: '15px' })} />
<div <div
className={css({ className={css({
width: '100%', width: '100%',
marginLeft: '20px', marginLeft: '20px',
})} })}
> >
<Checkbox <Checkbox
size="sm" size="sm"
isSelected={includeTranscript} isSelected={includeTranscript}
onChange={setIncludeTranscript} onChange={setIncludeTranscript}
isDisabled={statuses.isActive || isPendingToStart} isDisabled={statuses.isActive || isPendingToStart}
> >
<Text variant="sm">{t('details.transcription')}</Text> <Text variant="sm">{t('details.transcription')}</Text>
</Checkbox> </Checkbox>
</div> </div>
</>
)}
</VStack> </VStack>
<ControlsButton <ControlsButton
i18nKeyPrefix={keyPrefix} i18nKeyPrefix={keyPrefix}
@@ -53,6 +53,10 @@ export const TranscriptSidePanel = () => {
FeatureFlags.Transcript FeatureFlags.Transcript
) )
const hasScreenRecordingAccess = useHasRecordingAccess(
RecordingMode.ScreenRecording,
FeatureFlags.ScreenRecording
)
const hasFeatureWithoutAdminRights = useHasFeatureWithoutAdminRights( const hasFeatureWithoutAdminRights = useHasFeatureWithoutAdminRights(
RecordingMode.Transcript, RecordingMode.Transcript,
FeatureFlags.Transcript FeatureFlags.Transcript
@@ -97,7 +101,9 @@ export const TranscriptSidePanel = () => {
room.localParticipant room.localParticipant
) )
} else { } else {
const recordingMode = includeScreenRecording const withScreenRecording =
includeScreenRecording && hasScreenRecordingAccess
const recordingMode = withScreenRecording
? RecordingMode.ScreenRecording ? RecordingMode.ScreenRecording
: RecordingMode.Transcript : RecordingMode.Transcript
@@ -105,7 +111,7 @@ export const TranscriptSidePanel = () => {
...(!isLanguageSetToAuto && { ...(!isLanguageSetToAuto && {
language: selectedLanguageKey, language: selectedLanguageKey,
}), }),
...(includeScreenRecording && { ...(withScreenRecording && {
transcribe: true, transcribe: true,
original_mode: RecordingMode.Transcript, original_mode: RecordingMode.Transcript,
}), }),
@@ -122,7 +128,7 @@ export const TranscriptSidePanel = () => {
type: NotificationType.TranscriptionStarted, type: NotificationType.TranscriptionStarted,
}) })
captureEvent('transcript-started', { captureEvent('transcript-started', {
includeScreenRecording: includeScreenRecording, includeScreenRecording: withScreenRecording,
language: selectedLanguageKey, language: selectedLanguageKey,
}) })
} }
@@ -234,22 +240,26 @@ export const TranscriptSidePanel = () => {
</Button> </Button>
</Text> </Text>
</RowWrapper> </RowWrapper>
<div className={css({ height: '15px' })} /> {hasScreenRecordingAccess && (
<div <>
className={css({ <div className={css({ height: '15px' })} />
width: '100%', <div
marginLeft: '20px', className={css({
})} width: '100%',
> marginLeft: '20px',
<Checkbox })}
size="sm" >
isSelected={includeScreenRecording} <Checkbox
onChange={setIncludeScreenRecording} size="sm"
isDisabled={statuses.isActive || isPendingToStart} isSelected={includeScreenRecording}
> onChange={setIncludeScreenRecording}
<Text variant="sm">{t('details.recording')}</Text> isDisabled={statuses.isActive || isPendingToStart}
</Checkbox> >
</div> <Text variant="sm">{t('details.recording')}</Text>
</Checkbox>
</div>
</>
)}
</VStack> </VStack>
<ControlsButton <ControlsButton
i18nKeyPrefix={keyPrefix} i18nKeyPrefix={keyPrefix}
@@ -158,7 +158,7 @@ export const Conference = ({
* *
* Issue: On Firefox behind proxy configurations, WebSocket signaling fails to establish. * Issue: On Firefox behind proxy configurations, WebSocket signaling fails to establish.
* Symptom: Client receives HTTP 200 instead of expected 101 (Switching Protocols). * Symptom: Client receives HTTP 200 instead of expected 101 (Switching Protocols).
* Root Cause: Certificate/security issue where the initial request is considered unsecure. * Root Cause: Certificate/security issue where the initial request is considered insecure.
* *
* Solution: Pre-establish a WebSocket connection to the signaling server, which fails. * Solution: Pre-establish a WebSocket connection to the signaling server, which fails.
* This "primes" the connection, allowing subsequent WebSocket establishments to work correctly. * This "primes" the connection, allowing subsequent WebSocket establishments to work correctly.
+3 -4
View File
@@ -1,10 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
docker image ls | grep readme-generator-for-helm if ! docker image ls | grep readme-generator-for-helm; then
if [ "$?" -ne "0" ]; then
git clone https://github.com/bitnami/readme-generator-for-helm.git /tmp/readme-generator-for-helm git clone https://github.com/bitnami/readme-generator-for-helm.git /tmp/readme-generator-for-helm
cd /tmp/readme-generator-for-helm cd /tmp/readme-generator-for-helm || exit 1
docker build -t readme-generator-for-helm:latest . docker build -t readme-generator-for-helm:latest .
cd $(dirname -- "${BASH_SOURCE[0]}") cd "$(dirname -- "${BASH_SOURCE[0]}")" || exit 1
fi fi
docker run --rm -it -v .:/source -w /source readme-generator-for-helm:latest readme-generator -v values.yaml -r README.md docker run --rm -it -v .:/source -w /source readme-generator-for-helm:latest readme-generator -v values.yaml -r README.md
+1 -1
View File
@@ -51,7 +51,7 @@ app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }} {{- end }}
{{/* {{/*
transform dictionnary of environment variables transform dictionary of environment variables
Usage : {{ include "meet.env.transformDict" .Values.envVars }} Usage : {{ include "meet.env.transformDict" .Values.envVars }}
Example: Example:
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/bin/sh
set -eo pipefail set -e
# Run html-to-text to convert all html files to text files # Run html-to-text to convert all html files to text files
DIR_MAILS="../backend/core/templates/mail/" DIR_MAILS="../backend/core/templates/mail/"
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/bin/sh
# Run mjml command to convert all mjml templates to html files # Run mjml command to convert all mjml templates to html files
DIR_MAILS="../backend/core/templates/mail/html/" DIR_MAILS="../backend/core/templates/mail/html/"
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mail_mjml", "name": "mail_mjml",
"version": "1.32.1", "version": "1.33.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mail_mjml", "name": "mail_mjml",
"version": "1.32.1", "version": "1.33.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@html-to/text-cli": "0.6.1", "@html-to/text-cli": "0.6.1",
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "mail_mjml", "name": "mail_mjml",
"version": "1.32.1", "version": "1.33.0",
"description": "An util to generate html and text django's templates from mjml templates", "description": "An util to generate html and text django's templates from mjml templates",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
@@ -9,8 +9,8 @@
}, },
"private": true, "private": true,
"scripts": { "scripts": {
"build-mjml-to-html": "bash ./bin/mjml-to-html", "build-mjml-to-html": "sh ./bin/mjml-to-html",
"build-html-to-plain-text": "bash ./bin/html-to-plain-text", "build-html-to-plain-text": "sh ./bin/html-to-plain-text",
"build": "npm run build-mjml-to-html && npm run build-html-to-plain-text" "build": "npm run build-mjml-to-html && npm run build-html-to-plain-text"
}, },
"volta": { "volta": {
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "sdk", "name": "sdk",
"version": "1.32.1", "version": "1.33.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sdk", "name": "sdk",
"version": "1.32.1", "version": "1.33.0",
"license": "ISC", "license": "ISC",
"workspaces": [ "workspaces": [
"./library", "./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "sdk", "name": "sdk",
"version": "1.32.1", "version": "1.33.0",
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"description": "", "description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "summary" name = "summary"
version = "1.32.1" version = "1.33.0"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"fastapi[standard]>=0.105.0", "fastapi[standard]>=0.105.0",
+1 -1
View File
@@ -1484,7 +1484,7 @@ wheels = [
[[package]] [[package]]
name = "summary" name = "summary"
version = "1.32.1" version = "1.33.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "boto3" }, { name = "boto3" },