Skip to content
94 changes: 19 additions & 75 deletions .github/workflows/release-artifacts.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# GitHub actions workflow which builds the release artifacts.

name: Build release artifacts
name: Build Tchap release artifacts

on:
# we build on PRs and develop to (hopefully) get early warning
# of things breaking (but only build one set of debs). PRs skip
# building wheels on ARM.
pull_request:
push:
branches: ["develop", "release-*"]
branches: [tchap, develop_tchap]

# we do the full build on releases.
release:
types: [published]

# we do the full build on tags.
tags: ["v*"]
merge_group:
workflow_dispatch:

Expand Down Expand Up @@ -44,74 +46,17 @@ jobs:
outputs:
distros: ${{ steps.set-distros.outputs.distros }}

# now build the packages with a matrix build.
build-debs:
needs: get-distros
name: "Build .deb packages"
runs-on: ubuntu-latest
strategy:
matrix:
distro: ${{ fromJson(needs.get-distros.outputs.distros) }}

steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: src

- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0

- name: Set up docker layer caching
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-

- name: Set up python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.x"

- name: Build the packages
# see https://github.com/docker/build-push-action/issues/252
# for the cache magic here
run: |
./src/scripts-dev/build_debian_packages.py \
--docker-build-arg=--cache-from=type=local,src=/tmp/.buildx-cache \
--docker-build-arg=--cache-to=type=local,mode=max,dest=/tmp/.buildx-cache-new \
--docker-build-arg=--progress=plain \
--docker-build-arg=--load \
"${{ matrix.distro }}"
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

- name: Artifact name
id: artifact-name
# We can't have colons in the upload name of the artifact, so we convert
# e.g. `debian:sid` to `sid`.
env:
DISTRO: ${{ matrix.distro }}
run: |
echo "ARTIFACT_NAME=${DISTRO#*:}" >> "$GITHUB_OUTPUT"

- name: Upload debs as artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: debs-${{ steps.artifact-name.outputs.ARTIFACT_NAME }}
path: debs/*
### Deactivate build-debs pipeline : not required for Tchap Synapse ###

build-wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-24.04
- ubuntu-24.04-arm
### Deactivate macos pipeline : not required for Tchap Synapse ###
os: [ubuntu-24.04]
### Deactivate aarch64 pipeline : not required for Tchap Synapse ###
arch: [x86_64]
# is_pr is a flag used to exclude certain jobs from the matrix on PRs.
# It is not read by the rest of the workflow.
is_pr:
Expand Down Expand Up @@ -144,6 +89,9 @@ jobs:
# The platforms that we build for are determined by the
# `tool.cibuildwheel.skip` option in `pyproject.toml`.

### Deactivate other version : not required for Tchap Synapse ###
CIBW_BUILD: cp310-manylinux_x86_64

# We skip testing wheels for the following platforms in CI:
#
# pp3*-* (PyPy wheels) broke in CI (TODO: investigate).
Expand Down Expand Up @@ -181,26 +129,22 @@ jobs:
name: "Attach assets to release"
if: ${{ !failure() && !cancelled() && startsWith(github.ref, 'refs/tags/') }}
needs:
- build-debs
### Deactivate pipeline : not required for Tchap Synapse ###
# - build-debs
- build-wheels
- build-sdist
runs-on: ubuntu-latest
steps:
- name: Download all workflow run artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
- name: Build a tarball for the debs
# We need to merge all the debs uploads into one folder, then compress
# that.
run: |
mkdir debs
mv debs*/* debs/
tar -cvJf debs.tar.xz debs
### Deactivate debs archiving : not required for Tchap Synapse ###
- name: Attach to release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "${{ github.ref_name }}" \
Sdist/* \
Wheel*/* \
debs.tar.xz \
--repo ${{ github.repository }}
### Deactivate debs archiving : not required for Tchap Synapse ###
# debs.tar.xz \
41 changes: 41 additions & 0 deletions build_conf.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/bin/sh
set -eu

DOCKER_DEMO_REPO="/Users/olivier/workspace/tchap/tchap-docker-integration"
SYNAPSE_DATA=$(pwd)
OUT="homeserver.yaml"
OUT2="log_config.yaml"

# 1/7 Copy the base homeserver.yaml
cp docs/sample_config.yaml "$OUT"
cp docs/sample_log_config.yaml "$OUT2"

# 2/7 Merge $DOCKER_DEMO_REPO configuration into the base (overlays)
yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' "$OUT" "$DOCKER_DEMO_REPO/data/synapse/homeserver.yaml" -i

# 3/7 Force SQLite with a configurable path
yq eval -i ".database = {\"name\":\"sqlite3\",\"args\":{\"database\":\"${SYNAPSE_DATA}/data/sqlite3/homeserver.db\"}}" "$OUT"

# 4/7 Force paths relative to SYNAPSE_DATA
yq eval -i ".pid_file = \"${SYNAPSE_DATA}/data/homeserver.pid\"" "$OUT"
yq eval -i ".log_config = \"${SYNAPSE_DATA}/log_config.yaml\"" "$OUT"
yq eval -i ".media_store_path = \"${SYNAPSE_DATA}/data/media_store\"" "$OUT"
yq eval -i ".signing_key_path = \"${SYNAPSE_DATA}/data/tchapgouv.com.signing.key\"" "$OUT"

# 5/7 Add bind_addresses to the main listener
yq eval -i '.listeners[0].bind_addresses = ["::1","127.0.0.1"]' "$OUT"

# 6/7 Unused configuration
yq eval -i 'del(.instance_map)' "$OUT"
yq eval -i 'del(.redis)' "$OUT"
yq eval -i 'del(.background_updates)' "$OUT"
yq eval -i '.send_federation = false' "$OUT"
yq eval -i '.report_stats = false' "$OUT"
yq eval -i '.suppress_key_server_warning = true' "$OUT"

echo "==> homeserver.yaml generated!"

# 7/7 Build log_config.yaml
yq eval -i ".handlers.file.filename = \"${SYNAPSE_DATA}/data/homeserver.log\"" "$OUT2"

echo "==> log_config.yaml generated!"
20 changes: 19 additions & 1 deletion synapse/api/auth/mas.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from synapse.api.auth.base import BaseAuth
from synapse.api.errors import (
AuthError,
Codes, #:tchap:
HttpResponseException,
InvalidClientTokenError,
SynapseError,
Expand Down Expand Up @@ -106,6 +107,7 @@ def __init__(self, hs: "HomeServer"):
self.server_name = hs.hostname
self._clock = hs.get_clock()
self._config = hs.config.mas
self._account_validity_handler = hs.get_account_validity_handler() #:tchap:

self._http_client = hs.get_proxied_http_client()
self._rust_http_client = HttpClient(
Expand Down Expand Up @@ -140,7 +142,7 @@ def __init__(self, hs: "HomeServer"):
clock=self._clock,
name="mas_token_introspection",
server_name=self.server_name,
timeout=Duration(minutes=2),
timeout=Duration(minutes=20),
# don't log because the keys are access tokens
enable_logging=False,
)
Expand Down Expand Up @@ -291,6 +293,22 @@ async def get_user_by_req(
allow_expired=allow_expired,
)

#:tchap:
# Deny the request if the user account has expired.
if not allow_expired:
if await self._account_validity_handler.is_user_expired(
requester.user.to_string()
):
# Raise the error if either an account validity module has determined
# the account has expired, or the legacy account validity
# implementation is enabled and determined the account has expired
raise AuthError(
403,
"User account has expired",
errcode=Codes.EXPIRED_ACCOUNT,
)
#:tchap: end

await self._record_request(request, requester)

request.requester = requester
Expand Down
2 changes: 1 addition & 1 deletion synapse/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
#
# in short, we somewhat arbitrarily limit requests to 200 * 64K (about 12.5M)
#
MAX_REQUEST_SIZE = 200 * MAX_PDU_SIZE
MAX_REQUEST_SIZE = 1000 * MAX_PDU_SIZE

# Max/min size of ints in canonical JSON
CANONICALJSON_MAX_INT = (2**53) - 1
Expand Down
7 changes: 7 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,3 +611,10 @@ def read_config(
# Note that sticky events persisted before this feature is enabled will not be
# considered sticky by the local homeserver.
self.msc4354_enabled: bool = experimental.get("msc4354_enabled", False)

# MSC4258: Federated User Search
self.msc4258_enabled: bool = experimental.get("msc4258_enabled", False)

self.msc4258_federation_search_timeout: int = experimental.get(
"msc4258_federation_search_timeout", 2000
)
5 changes: 5 additions & 0 deletions synapse/config/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,11 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
):
raise ConfigError("allowed_avatar_mimetypes must be a list")

# The events to exclude from join state updates.
self.join_states_update_excludes = config.get("join_states_update_excludes", [])
if not isinstance(self.join_states_update_excludes, list):
raise ConfigError("join_states_update_excludes must be a list")

listeners = config.get("listeners", [])
if not isinstance(listeners, list):
raise ConfigError("Expected a list", ("listeners",))
Expand Down
99 changes: 99 additions & 0 deletions synapse/federation/federation_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
import attr
from prometheus_client import Counter

from twisted.internet import defer

from synapse.api.constants import Direction, EventContentFields, EventTypes, Membership
from synapse.api.errors import (
CodeMessageException,
Expand Down Expand Up @@ -69,6 +71,7 @@
from synapse.federation.transport.client import SendJoinResponse
from synapse.http.client import is_unknown_endpoint
from synapse.http.types import QueryParams
from synapse.logging.context import make_deferred_yieldable
from synapse.logging.opentracing import SynapseTags, log_kv, set_tag, tag_args, trace
from synapse.metrics import SERVER_NAME_LABEL
from synapse.types import JsonDict, StrCollection, UserID, get_domain_from_id
Expand Down Expand Up @@ -136,6 +139,9 @@ def __init__(self, hs: "HomeServer"):
self._clock.looping_call(self._clear_tried_cache, Duration(minutes=1))
self.state = hs.get_state_handler()
self.transport_layer = hs.get_federation_transport_client()
self.user_directory_search_timeout = (
hs.config.experimental.msc4258_federation_search_timeout
)

self.server_name = hs.hostname
self.signing_key = hs.signing_key
Expand Down Expand Up @@ -1977,6 +1983,99 @@ def filter_user_id(user_id: str) -> bool:

return filtered_statuses, filtered_failures

async def user_directory_search(
self,
requester: str,
destination: str,
search_term: str,
timeout: int,
limit: int = 10,
) -> JsonDict:
"""Search for users in the user directory of a remote server.
Args:
requester: The user that initiated the search.
destination: The server to query.
search_term: The search term to look for.
limit: Maximum number of results to return.
Returns:
The search results containing a list of users matching the search term.
"""
try:
response = await self.transport_layer.user_directory_search(
requester, destination, search_term, limit, timeout
)
return response
except Exception as e:
# If something goes wrong, we still want to return what we have
logger.exception(
"Error searching user directory across federation[destination=%s] : %s",
destination,
e,
)
return {"limited": False, "results": []}

async def search_user_directory_across_federation(
self,
requester: str,
destinations: Collection[str],
search_term: str,
limit: int = 10,
) -> JsonDict:
"""Search for users across multiple federated servers.
Args:
requester: The user that initiated the search.
destinations: The servers to query.
search_term: The search term to look for.
limit: Maximum number of results to return per server.
Returns:
Combined search results from all servers.
"""

if not destinations:
return {"limited": False, "results": []}

# Query each server individually and collect results
combined_results = []
limited = False

# Create a list of deferreds to query each server
query_tasks = []
for destination in destinations:
if not self._is_mine_server_name(destination):
# Convert coroutine to Deferred
deferred = defer.ensureDeferred(
self.user_directory_search(
requester,
destination,
search_term,
self.user_directory_search_timeout,
limit,
)
)
query_tasks.append(deferred)

# Execute all queries in parallel
if query_tasks:
server_results = await make_deferred_yieldable(
defer.gatherResults(
query_tasks,
consumeErrors=True,
)
)

# Process results from each server
for result in server_results:
if result.get("limited", False):
limited = True
combined_results.extend(result.get("results", []))

# Limit the total number of results
if len(combined_results) > limit:
combined_results = combined_results[:limit]
limited = True

return {"limited": limited, "results": combined_results}

async def federation_download_media(
self,
destination: str,
Expand Down
Loading
Loading