Skip to main content

synapse-oidc-upload-rules

A Synapse module that applies per-user media upload limits based on the user's Keycloak group membership.

On every media upload, Synapse asks this module which limits apply to the uploading user. The module resolves the Matrix user to their Keycloak identity (via the user_external_ids mapping created by the OIDC login flow), fetches the user's groups live from the Keycloak Admin REST API (cached with a TTL), and returns the limits of the first matching ruleset.

On Synapse ≥ 1.139.0 that means rolling time-window quotas via get_media_upload_limits_for_user. On Synapse 1.132.0–1.138.x the same rulesets are enforced as a per-upload size cap via is_user_allowed_to_upload_media_of_size (and advertised as m.upload.size).

sequenceDiagram
    participant Client
    participant Synapse
    participant Module
    participant Keycloak

    Client->>Synapse: upload media
    Synapse->>Module: media repository callback
    Module->>Synapse: 1. mxid → OIDC `sub` (user_external_ids)
    Module->>Keycloak: 2. GET /admin/realms/{realm}/users/{sub}/groups\n(client-credentials token, TTL-cached)
    Keycloak-->>Module: groups for user
    Module->>Module: 3. first matching ruleset wins
    Module-->>Synapse: full: MediaUploadLimit list / legacy: allow-or-deny by size

Requirements

  • Synapse ≥ 1.132.0 (hard floor — see Synapse version support below).
  • Python ≥ 3.9 (same floor as recent Synapse releases; the package itself only needs attrs, which Synapse already ships).
  • Users must log in via Synapse's OIDC provider pointing at Keycloak, so that user_external_ids contains the mapping from Matrix IDs to Keycloak user IDs.

Synapse version support

Per-user media upload hooks only exist in Synapse from 1.132.0 onwards. There is no module API on older homeservers that can deny or reshape uploads by user, so this package cannot usefully run below that version (startup fails with a clear ConfigError).

Synapse version Mode What is enforced
≥ 1.139.0 Full Rolling time-window quotas via get_media_upload_limits_for_user (matches Synapse's media_upload_limits semantics).
1.132.0 – 1.138.x Legacy Per-upload size cap via is_user_allowed_to_upload_media_of_size, also advertised to clients as m.upload.size through get_media_config_for_user. Rolling quotas are not available.
< 1.132.0 Unsupported No media repository callbacks; the module refuses to start.

The mode is detected at startup from the signature of register_media_repository_callbacks. On legacy Synapse the module logs a warning and falls back automatically — no config change needed when you later upgrade past 1.139.0.

All of these callbacks are still marked experimental upstream and may change in future Synapse releases.

Installation

Install the package into the same Python environment as Synapse:

pip install synapse-oidc-upload-rules
# or from a checkout / local build:
uv build && pip install dist/synapse_oidc_upload_rules-*.whl

PyPI: synapse-oidc-upload-rules

Keycloak setup

The module authenticates to the Keycloak Admin REST API with the client-credentials grant, so it needs a confidential client with a service account:

  1. In your realm, create a client (e.g. synapse-upload-rules) with:
    • Client authentication: on (confidential client)
    • Service accounts roles: enabled
    • All login flows (standard, direct access, …): off
  2. On the client's Service accounts roles tab, assign the following roles from the realm-management client:
    • view-users
    • query-groups
  3. Copy the client secret from the Credentials tab into the module config (or a file referenced by client_secret_path).

Configuration (homeserver.yaml)

modules:
  - module: synapse_oidc_upload_rules.OidcUploadRulesModule
    config:
      auth_provider_id: "oidc-keycloak"   # idp_id as stored in user_external_ids
      keycloak:
        server_url: "https://keycloak.example.com"
        realm: "my-realm"
        client_id: "synapse-upload-rules"
        client_secret: "..."              # or client_secret_path
        timeout_s: 5
      cache_ttl: "5m"                     # group-membership cache TTL
      on_error: "fallback"                # "fallback" or "deny" (see below)
      rulesets:                           # first match wins, evaluated in order
        - name: "power-users"
          groups: ["/staff", "/premium"]  # Keycloak group paths
          require_all: false              # match ANY listed group (default)
          limits:
            - time_period: "1d"
              max_size: "10G"
        - name: "restricted"
          groups: ["/guests"]
          limits:
            - time_period: "1w"
              max_size: "100M"
      default_limits:                     # used when no ruleset matches;
        - time_period: "1d"               # omit to fall back to the homeserver's
          max_size: "500M"                # own media_upload_limits

Reference

Key Required Default Description
auth_provider_id yes The idp_id under which OIDC mappings are stored in user_external_ids (Synapse prefixes OIDC providers with oidc-).
keycloak.server_url yes Base URL of Keycloak (no /auth suffix on modern Keycloak).
keycloak.realm yes Realm containing the users and the service-account client.
keycloak.client_id yes Client ID of the confidential service-account client.
keycloak.client_secret yes* Client secret. Mutually exclusive with client_secret_path.
keycloak.client_secret_path yes* Path to a file containing the client secret (trailing whitespace is stripped).
keycloak.timeout_s no 5 Intended HTTP timeout for Keycloak requests.
cache_ttl no "5m" How long group memberships are cached per user.
on_error no "fallback" What to do when Keycloak/DB lookups fail: fallback returns None (homeserver config applies), deny returns the strictest configured limits.
rulesets no** [] Ordered list of rulesets; the first whose groups match wins.
rulesets[].groups yes Keycloak group paths (e.g. /staff), compared exactly (no prefix matching).
rulesets[].require_all no false If true, the user must be in all listed groups; otherwise any suffices.
rulesets[].limits yes List of {time_period, max_size} entries, same semantics as Synapse's media_upload_limits.
default_limits no** unset Limits applied when no ruleset matches (or the user has no OIDC mapping). If unset, None is returned and the homeserver config applies.

* exactly one of client_secret / client_secret_path is required. ** at least one of rulesets / default_limits must be configured.

Durations accept an integer (milliseconds) or a string with an s/m/h/d/w/y suffix. Sizes accept an integer (bytes) or a string with a K/M/G/T suffix (powers of 1024) — the same semantics as Synapse's own config parser.

Behavior notes

  • Users without an OIDC mapping for the configured provider (e.g. local admin accounts) get default_limits if configured, otherwise the homeserver config applies. The same applies to users whose mapping exists but who have been deleted from Keycloak.
  • First match wins: rulesets are evaluated strictly in configuration order; put the most privileged rules first.
  • Caching / staleness: group memberships are cached for cache_ttl per user, so group changes in Keycloak can take up to that long to affect upload limits. Access tokens are cached until shortly before expiry and refreshed automatically (including a one-shot retry when Keycloak rejects a token).
  • Error policy: Keycloak or database failures never crash the upload path; they are logged with context. With on_error: fallback the homeserver's own media_upload_limits apply; with on_error: deny the strictest configured limits apply (the ruleset — or default_limits — with the lowest allowed bytes-per-time rate).
  • Legacy mode semantics: on Synapse 1.132–1.138, each ruleset's smallest max_size is treated as a per-file upload cap (rolling time_period windows cannot be enforced until 1.139+). Prefer upgrading to ≥ 1.139.0 if you rely on time-window quotas.
  • Experimental callbacks: the underlying Synapse media repository callbacks are experimental; this module is tested against the 1.132 / 1.139 callback shapes.

Development

The project uses uv with a src/ layout. matrix-synapse is a dev/test dependency only (the module imports synapse.module_api at runtime inside the homeserver); tests mock the ModuleApi and the Keycloak HTTP layer, so no running Synapse or Keycloak is needed.

uv sync                  # create the venv and install all dependency groups
uv run pytest            # run the test suite
uv run ruff check .      # lint
uv run ruff format .     # format

Releasing

Releases are published to PyPI by .github/workflows/release.yml on a pushed semver tag (or via Actions → Release → Run workflow against an existing tag).

Cut a release:

  1. Bump the version in both pyproject.toml and src/synapse_oidc_upload_rules/__init__.py (they must match).

  2. Commit the bump, then tag and push:

    git tag -a v0.1.0 -m "v0.1.0"
    git push origin v0.1.0
    
  3. The Release workflow will lint, test, build, publish to PyPI via OIDC Trusted Publishing, and create a GitHub Release with the wheel and sdist attached.

Supported tag shapes: vX.Y.Z, vX.Y.ZaN, vX.Y.ZbN, vX.Y.ZrcN.

License

Copyright (c) 2026 Lukas 'dotWee' Wolfsteiner lukas@wolfsteiner.media

Licensed under the MIT License. See the LICENSE file for details.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

synapse_oidc_upload_rules-0.1.0.tar.gz (15.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

synapse_oidc_upload_rules-0.1.0-py3-none-any.whl (18.3 kB view details)

Uploaded Python 3

File details

Details for the file synapse_oidc_upload_rules-0.1.0.tar.gz.

File metadata

  • Download URL: synapse_oidc_upload_rules-0.1.0.tar.gz
  • Upload date:
  • Size: 15.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.31 {"installer":{"name":"uv","version":"0.11.31","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for synapse_oidc_upload_rules-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b72e7b2bd7bb32a0670710ec2e3aadb16ee04e6783b3bf51feb1ce33f7ce4713
MD5 cf9a44aabf1e62c8c252c99bc24c1de5
BLAKE2b-256 1b41f4f007e2bdcd113017017cf819e8a23e7c845eb6e3b97c9ee32b1dbd5346

See more details on using hashes here.

File details

Details for the file synapse_oidc_upload_rules-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: synapse_oidc_upload_rules-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.31 {"installer":{"name":"uv","version":"0.11.31","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for synapse_oidc_upload_rules-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 45d7cafcf59b4ee1c02a6e117e4725c9ee810ce76dd88218764abcf598e898d5
MD5 d0e3bc52cf27fc42f5b04eece74a1b89
BLAKE2b-256 937ecd2b413c857f3f437501c2fafb039ca59d974e3f4da261ff157e2c967111

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page