Skip to content

Conversation

@ricardogarim
Copy link
Contributor

@ricardogarim ricardogarim commented Sep 12, 2025

Works along with RocketChat/Rocket.Chat#36926.

For anyone testing with Synapse as HS1, remember to add these two properties to the YAML spec file:

  • enable_media_repo: true
  • enable_authenticated_media: true

Summary by CodeRabbit

  • New Features

    • Media access control for federated requests: validates signed requests and enforces room ACLs, including world-readable history.
    • Resolves media to originating rooms across bridged Matrix/Rocket.Chat mappings.
    • Adds media/upload mappings and repositories to support lookups.
    • Binary/media fetch support with multipart and CDN-redirect handling.
  • Improvements

    • Clearer authorization errors include origin and destination.
    • Streamlined signature verification and reduced noisy logging.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 12, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Refactors core auth signature verification; replaces fetch with richer content-type and multipart handling; adds Upload and MatrixBridgedRoom collections and repositories; extends federation request flow to return binary/multipart results and adds requestBinaryData; updates media download to use requestBinaryData; extends EventAuthorizationService with media-access checks and auth-header entrypoint.

Changes

Cohort / File(s) Summary
Core Auth Refactor
packages/core/src/utils/authentication.ts
Splits verification into named byte computations (signingKeyBytes, messageBytes); stores result in isValid; updates invalid-signature error to include origin and destination; signature of exported function unchanged.
Core Fetch API
packages/core/src/utils/fetch.ts
Replaces simple fetch with generic fetch<T> returning status, headers and accessors: buffer(), json(), text(), multipart(); adds MultipartResult type and multipart parsing/redirect handling; request header handling adjusted (no automatic JSON content-type).
Container Wiring
packages/federation-sdk/src/container.ts
Added collection tokens UploadCollection (rocketchat_uploads) and MatrixBridgedRoomCollection (rocketchat_matrix_bridged_rooms); registered UploadRepository and MatrixBridgedRoomRepository singletons; exported new types/repositories.
Repositories
packages/federation-sdk/src/repositories/matrix-bridged-room.repository.ts, packages/federation-sdk/src/repositories/upload.repository.ts
New exported types MatrixBridgedRoom and Upload. New singleton repositories MatrixBridgedRoomRepository (method findMatrixRoomId(rocketChatRoomId)) and UploadRepository (method findRocketChatRoomIdByMediaId(mediaId)), both inject typed Mongo collections.
Federation Request & Binary Handling
packages/federation-sdk/src/services/federation-request.service.ts
makeSignedRequest<T> now may return `T
Media Service
packages/federation-sdk/src/services/media.service.ts
Replaces legacy multipart/download helpers with downloadFromRemoteServer that iterates endpoints and uses federationRequest.requestBinaryData to fetch binary media; simplified logging and failure behavior.
Event Authorization Service
packages/federation-sdk/src/services/event-authorization.service.ts
Constructor now injects UploadRepository and MatrixBridgedRoomRepository; added canAccessMedia(mediaId, serverName) and canAccessMediaFromAuthorizationHeader(...); verifyRequestSignature uses computed destination and simplified logging; minor ACL variable renames only.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant EventAuth as EventAuthorizationService
  participant SigVerify as SignatureVerificationService
  participant UploadRepo as UploadRepository
  participant BridgeRepo as MatrixBridgedRoomRepository
  participant State as StateService
  participant Events as EventService
  participant FedReq as FederationRequestService
  participant Remote as RemoteServer

  rect rgb(237,246,255)
  note right of Client: Client requests media with Authorization header
  Client->>EventAuth: canAccessMediaFromAuthorizationHeader(mediaId, auth, method, uri, body?)
  EventAuth->>SigVerify: verifyRequestSignature(auth, method, uri, body, destination=serverName)
  SigVerify-->>EventAuth: { origin } / error
  end

  alt Signature invalid
    EventAuth-->>Client: { authorized: false, errorCode: M_UNAUTHORIZED }
  else Signature valid
    EventAuth->>UploadRepo: findRocketChatRoomIdByMediaId(mediaId)
    UploadRepo-->>EventAuth: rid|null
    alt rid found
      EventAuth->>BridgeRepo: findMatrixRoomId(rid)
      BridgeRepo-->>EventAuth: mri|null
      alt mri found
        EventAuth->>State: checkServerAcl(mri)
        State-->>EventAuth: acl
        EventAuth->>Events: isServerInRoom(origin, mri)?
        Events-->>EventAuth: true/false
        alt allowed
          EventAuth-->>Client: { authorized: true }
          Client->>FedReq: requestBinaryData(GET, serverName, endpoint)
          FedReq->>Remote: signed HTTP request
          Remote-->>FedReq: multipart/ok/json or redirect or binary
          FedReq-->>Client: Buffer | JSON | null
        else forbidden
          EventAuth-->>Client: { authorized: false, errorCode: M_FORBIDDEN }
        end
      else mri missing
        EventAuth-->>Client: { authorized: false, errorCode: M_FORBIDDEN }
      end
    else rid missing
      EventAuth-->>Client: { authorized: false, errorCode: M_FORBIDDEN }
    end
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

I hop through bytes with whiskered cheer,
Keys decoded, signatures clear.
Repos burrow where uploads hide,
Rooms link paths both far and wide.
A thunk, a thump — the fetching blooms,
Rabbity logs light up the rooms. 🥕

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly describes the primary changes — the addition of canAccessMedia middleware and an Upload repository — and directly corresponds to the new methods in packages/federation-sdk/src/services/event-authorization.service.ts and the UploadRepository added in packages/federation-sdk/src/repositories/upload.repository.ts; it is specific, concise, and not vague.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 210821e and 9722726.

📒 Files selected for processing (3)
  • packages/core/src/utils/fetch.ts (2 hunks)
  • packages/federation-sdk/src/services/federation-request.service.ts (8 hunks)
  • packages/federation-sdk/src/services/media.service.ts (1 hunks)

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link

codecov-commenter commented Sep 12, 2025

Codecov Report

❌ Patch coverage is 9.31677% with 146 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.03%. Comparing base (3f41231) to head (210821e).

Files with missing lines Patch % Lines
...on-sdk/src/services/event-authorization.service.ts 2.58% 113 Missing ⚠️
packages/core/src/utils/authentication.ts 0.00% 14 Missing ⚠️
...src/repositories/matrix-bridged-room.repository.ts 37.50% 10 Missing ⚠️
...deration-sdk/src/repositories/upload.repository.ts 40.00% 9 Missing ⚠️
Additional details and impacted files
@@                    Coverage Diff                     @@
##           refactor/media-service     #185      +/-   ##
==========================================================
- Coverage                   44.53%   44.03%   -0.50%     
==========================================================
  Files                          80       82       +2     
  Lines                        9720     9862     +142     
==========================================================
+ Hits                         4329     4343      +14     
- Misses                       5391     5519     +128     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ricardogarim ricardogarim marked this pull request as ready for review September 12, 2025 03:43
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/utils/authentication.ts (1)

37-39: Destination must be optional per Matrix spec; don’t reject headers missing it

extractSignaturesFromHeader throws when destination is absent, but verifyRequestSignature treats it as optional. Make destination optional here to avoid spurious 401s.

Apply:

-	if ([origin, destination, key, signature].some((value) => !value)) {
+	if ([origin, key, signature].some((value) => !value)) {
 		throw new Error('Invalid authorization header');
 	}
🧹 Nitpick comments (8)
packages/core/src/utils/authentication.ts (1)

72-80: Rename parameter hash to signatureBase64 for clarity

It carries the sig from the header, not a hash. Improves readability across callers.

packages/federation-sdk/src/repositories/upload.repository.ts (2)

19-25: Project only needed fields and prepare for scale (add index externally)

Avoid pulling full upload docs; fetch only rid. Also ensure an index on federation.mediaId exists (migration/init).

Apply:

-		const upload = await this.collection.findOne({
-			'federation.mediaId': mediaId,
-		});
+		const upload = await this.collection.findOne(
+			{ 'federation.mediaId': mediaId },
+			{ projection: { rid: 1 } },
+		);

Migration/index suggestion (outside this file):

await db.collection('rocketchat_uploads')
  .createIndex({ 'federation.mediaId': 1 }, { name: 'idx_uploads_fed_mediaId', unique: true });

19-25: Optional: support lookup by MXC URI as fallback

If historical records lack mediaId but have federation.mxcUri, consider a fallback query.

packages/federation-sdk/src/repositories/matrix-bridged-room.repository.ts (1)

17-23: Project only mri and index rid

Trim the payload and ensure a supporting index (rid:1) via migration.

Apply:

-		const bridgedRoom = await this.collection.findOne({
-			rid: rocketChatRoomId,
-		});
+		const bridgedRoom = await this.collection.findOne(
+			{ rid: rocketChatRoomId },
+			{ projection: { mri: 1 } },
+		);

Migration/index suggestion (outside this file):

await db.collection('rocketchat_matrix_bridged_rooms')
  .createIndex({ rid: 1 }, { name: 'idx_mbr_rid' });
packages/federation-sdk/src/container.ts (1)

89-101: Create required indexes at startup/migration time

Registering collections is fine; add migration/initialization to create:

  • rocketchat_uploads: {'federation.mediaId': 1} unique
  • rocketchat_matrix_bridged_rooms: {rid: 1}

Would you like me to open a follow-up PR with a small migration to create these indexes?

packages/federation-sdk/src/services/event-authorization.service.ts (3)

154-167: Remove dead code: validateAuthorizationHeader throws on invalid; the !isValid path is unreachable

Simplify and rely on exceptions.

Apply:

-			const isValid = await validateAuthorizationHeader(
+			await validateAuthorizationHeader(
 				origin,
 				publicKey.verify_keys[key].key,
-				actualDestination,
+				actualDestination,
 				method,
 				uri,
 				signature,
 				body,
 			);
-			if (!isValid) {
-				this.logger.warn(`Invalid signature from ${origin}`);
-				return;
-			}
 
 			return origin;

237-285: Improve patch coverage with focused tests for auth header and ACL paths

Add unit tests for:

  • accept header without destination (using actual server name)
  • reject non-ed25519 keys
  • invalid signature → M_UNAUTHORIZED
  • canAccessMedia: room not found, bridged room missing, ACL deny, server in room, world_readable

I can scaffold tests with DI mocks for StateService/EventService/UploadRepository/MatrixBridgedRoomRepository.


369-475: Minor: dedupe “not found in any room” logs and include room IDs for quicker tracing

Current messages for missing rcRoomId/matrixRoomId are identical; include which lookup failed and the IDs when available.

-				this.logger.debug(`Media ${mediaId} not found in any room`);
+				this.logger.debug(`Media ${mediaId}: no Upload.rid found for mediaId`);
...
-				this.logger.debug(`Media ${mediaId} not found in any room`);
+				this.logger.debug(`Media ${mediaId}: no Matrix room mapping found for rid ${rcRoomId}`);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f41231 and 210821e.

📒 Files selected for processing (5)
  • packages/core/src/utils/authentication.ts (1 hunks)
  • packages/federation-sdk/src/container.ts (2 hunks)
  • packages/federation-sdk/src/repositories/matrix-bridged-room.repository.ts (1 hunks)
  • packages/federation-sdk/src/repositories/upload.repository.ts (1 hunks)
  • packages/federation-sdk/src/services/event-authorization.service.ts (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
packages/core/src/utils/authentication.ts (1)
packages/room/src/manager/event-wrapper.ts (1)
  • origin (82-84)
packages/federation-sdk/src/repositories/matrix-bridged-room.repository.ts (2)
packages/federation-sdk/src/repositories/upload.repository.ts (1)
  • singleton (13-26)
packages/federation-sdk/src/services/event-authorization.service.ts (1)
  • singleton (16-476)
packages/federation-sdk/src/repositories/upload.repository.ts (2)
packages/federation-sdk/src/repositories/matrix-bridged-room.repository.ts (1)
  • singleton (10-24)
packages/federation-sdk/src/services/event-authorization.service.ts (1)
  • singleton (16-476)
packages/federation-sdk/src/container.ts (2)
packages/federation-sdk/src/repositories/upload.repository.ts (1)
  • Upload (4-11)
packages/federation-sdk/src/repositories/matrix-bridged-room.repository.ts (1)
  • MatrixBridgedRoom (4-8)
packages/federation-sdk/src/services/event-authorization.service.ts (3)
packages/core/src/utils/authentication.ts (1)
  • validateAuthorizationHeader (72-109)
packages/room/src/manager/event-wrapper.ts (2)
  • origin (82-84)
  • roomId (72-74)
packages/federation-sdk/src/services/config.service.ts (1)
  • serverName (92-94)
🔇 Additional comments (2)
packages/federation-sdk/src/services/event-authorization.service.ts (2)

292-346: ACL: empty allow list -> deny (spec-compliant)

Matrix spec's m.room.server_acl decision order (no ACL → allow; check allow_ip_literals; deny list; allow list; otherwise deny) means a present ACL with an empty allow list denies all — your implementation (deny when allow.length === 0) matches the spec. (spec.matrix.org)


116-136: Upstream bug dependency: header parser currently rejects missing destination

extractSignaturesFromHeader (upstream/core) requires a destination while this code treats it as optional — repo search shows the only call is packages/federation-sdk/src/services/event-authorization.service.ts:129; this will work once the core parser accepts headers without destination.

Comment on lines 89 to +100
const signature = Uint8Array.from(atob(hash as string), (c) =>
c.charCodeAt(0),
);
const signingKeyBytes = Uint8Array.from(atob(signingKey as string), (c) =>
c.charCodeAt(0),
);
const messageBytes = new TextEncoder().encode(canonicalJson);
const isValid = nacl.sign.detached.verify(
messageBytes,
signature,
signingKeyBytes,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use Node-safe base64 decoding and support base64url (Matrix uses unpadded/base64url)

atob isn’t guaranteed in Node and doesn’t handle base64url. Normalize to base64 and decode with Buffer.

Apply:

-	const signature = Uint8Array.from(atob(hash as string), (c) =>
-		c.charCodeAt(0),
-	);
-	const signingKeyBytes = Uint8Array.from(atob(signingKey as string), (c) =>
-		c.charCodeAt(0),
-	);
-	const messageBytes = new TextEncoder().encode(canonicalJson);
-	const isValid = nacl.sign.detached.verify(
-		messageBytes,
-		signature,
-		signingKeyBytes,
-	);
+	const toB64 = (s: string) =>
+		s.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(s.length / 4) * 4, '=');
+	const signature = Uint8Array.from(Buffer.from(toB64(hash), 'base64'));
+	const signingKeyBytes = Uint8Array.from(Buffer.from(toB64(signingKey), 'base64'));
+	const messageBytes = new TextEncoder().encode(canonicalJson);
+	const isValid = nacl.sign.detached.verify(messageBytes, signature, signingKeyBytes);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const signature = Uint8Array.from(atob(hash as string), (c) =>
c.charCodeAt(0),
);
const signingKeyBytes = Uint8Array.from(atob(signingKey as string), (c) =>
c.charCodeAt(0),
);
const messageBytes = new TextEncoder().encode(canonicalJson);
const isValid = nacl.sign.detached.verify(
messageBytes,
signature,
signingKeyBytes,
);
const toB64 = (s: string) =>
s.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(s.length / 4) * 4, '=');
const signature = Uint8Array.from(Buffer.from(toB64(hash), 'base64'));
const signingKeyBytes = Uint8Array.from(Buffer.from(toB64(signingKey), 'base64'));
const messageBytes = new TextEncoder().encode(canonicalJson);
const isValid = nacl.sign.detached.verify(messageBytes, signature, signingKeyBytes);
🤖 Prompt for AI Agents
In packages/core/src/utils/authentication.ts around lines 89 to 100, the code
uses atob for base64 decoding which is not Node-safe and does not support
base64url; normalize the incoming hash and signingKey strings to standard base64
by replacing '-' with '+' and '_' with '/' and padding with '=' to a multiple of
4, then decode using Buffer.from(..., 'base64') and convert the resulting Buffer
to Uint8Array for signature and signing key bytes before calling
nacl.sign.detached.verify; update both decodes (signature and signingKey) this
way so Node and unpadded base64url inputs are handled correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants