From e8903f52d3b2b003edd254ab50e8f6f8d7b55cc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:24:55 +0000 Subject: [PATCH 001/189] build(deps): bump codecov/codecov-action from 5.4.0 to 5.4.2 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.0 to 5.4.2. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5.4.0...v5.4.2) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: 5.4.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/coverage.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index b90ec3819..b6fdcd371 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -33,7 +33,7 @@ jobs: run: make coverage - name: Upload to codecov.io - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: policies/coverage.json @@ -60,7 +60,7 @@ jobs: run: npm run coverage - name: Upload to codecov.io - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} directory: frontend/coverage/ @@ -127,7 +127,7 @@ jobs: grcov . --binary-path ./target/debug/deps/ -s . -t lcov --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/tests.lcov - name: Upload to codecov.io - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: target/coverage/*.lcov From 78908625008e7ae27cd6f69f2d0d41c6028a7d1b Mon Sep 17 00:00:00 2001 From: Adis Veletanlic Date: Wed, 16 Apr 2025 11:44:58 +0200 Subject: [PATCH 002/189] Add client_name to static registrations function and generate new query data --- crates/cli/src/sync.rs | 2 ++ crates/config/src/sections/clients.rs | 3 +++ ...7ebe97b523b6836e1696d8b8e2a0ef70bfa44.json | 23 ------------------ ...f91ac0b326dd751c0d374d6ef4d19f671d22e.json | 24 +++++++++++++++++++ ...9c715560d011d4c01112703a9c046170c84f1.json | 2 +- crates/storage-pg/src/oauth2/client.rs | 8 +++++-- crates/storage/src/oauth2/client.rs | 2 ++ 7 files changed, 38 insertions(+), 26 deletions(-) delete mode 100644 crates/storage-pg/.sqlx/query-5236305c49b1ee99a00e32df3727ebe97b523b6836e1696d8b8e2a0ef70bfa44.json create mode 100644 crates/storage-pg/.sqlx/query-da02f93d7346992a9795f12b900f91ac0b326dd751c0d374d6ef4d19f671d22e.json diff --git a/crates/cli/src/sync.rs b/crates/cli/src/sync.rs index c66f6f004..2eb2594d0 100644 --- a/crates/cli/src/sync.rs +++ b/crates/cli/src/sync.rs @@ -356,6 +356,7 @@ pub async fn config_sync( } let client_secret = client.client_secret.as_deref(); + let client_name = client.client_name.as_ref(); let client_auth_method = client.client_auth_method(); let jwks = client.jwks.as_ref(); let jwks_uri = client.jwks_uri.as_ref(); @@ -368,6 +369,7 @@ pub async fn config_sync( repo.oauth2_client() .upsert_static( client.client_id, + client_name.cloned(), client_auth_method, encrypted_client_secret, jwks.cloned(), diff --git a/crates/config/src/sections/clients.rs b/crates/config/src/sections/clients.rs index 84aa55a22..d821c11bd 100644 --- a/crates/config/src/sections/clients.rs +++ b/crates/config/src/sections/clients.rs @@ -79,6 +79,9 @@ pub struct ClientConfig { /// Authentication method used for this client client_auth_method: ClientAuthMethodConfig, + /// Name of the OAuth2 client + pub client_name: Option, + /// The client secret, used by the `client_secret_basic`, /// `client_secret_post` and `client_secret_jwt` authentication methods #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/storage-pg/.sqlx/query-5236305c49b1ee99a00e32df3727ebe97b523b6836e1696d8b8e2a0ef70bfa44.json b/crates/storage-pg/.sqlx/query-5236305c49b1ee99a00e32df3727ebe97b523b6836e1696d8b8e2a0ef70bfa44.json deleted file mode 100644 index 23b06789c..000000000 --- a/crates/storage-pg/.sqlx/query-5236305c49b1ee99a00e32df3727ebe97b523b6836e1696d8b8e2a0ef70bfa44.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO oauth2_clients\n ( oauth2_client_id\n , encrypted_client_secret\n , redirect_uris\n , grant_type_authorization_code\n , grant_type_refresh_token\n , grant_type_client_credentials\n , grant_type_device_code\n , token_endpoint_auth_method\n , jwks\n , jwks_uri\n , is_static\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, TRUE)\n ON CONFLICT (oauth2_client_id)\n DO\n UPDATE SET encrypted_client_secret = EXCLUDED.encrypted_client_secret\n , redirect_uris = EXCLUDED.redirect_uris\n , grant_type_authorization_code = EXCLUDED.grant_type_authorization_code\n , grant_type_refresh_token = EXCLUDED.grant_type_refresh_token\n , grant_type_client_credentials = EXCLUDED.grant_type_client_credentials\n , grant_type_device_code = EXCLUDED.grant_type_device_code\n , token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method\n , jwks = EXCLUDED.jwks\n , jwks_uri = EXCLUDED.jwks_uri\n , is_static = TRUE\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Text", - "TextArray", - "Bool", - "Bool", - "Bool", - "Bool", - "Text", - "Jsonb", - "Text" - ] - }, - "nullable": [] - }, - "hash": "5236305c49b1ee99a00e32df3727ebe97b523b6836e1696d8b8e2a0ef70bfa44" -} diff --git a/crates/storage-pg/.sqlx/query-da02f93d7346992a9795f12b900f91ac0b326dd751c0d374d6ef4d19f671d22e.json b/crates/storage-pg/.sqlx/query-da02f93d7346992a9795f12b900f91ac0b326dd751c0d374d6ef4d19f671d22e.json new file mode 100644 index 000000000..378ca2d78 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-da02f93d7346992a9795f12b900f91ac0b326dd751c0d374d6ef4d19f671d22e.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO oauth2_clients\n ( oauth2_client_id\n , encrypted_client_secret\n , redirect_uris\n , grant_type_authorization_code\n , grant_type_refresh_token\n , grant_type_client_credentials\n , grant_type_device_code\n , token_endpoint_auth_method\n , jwks\n , client_name\n , jwks_uri\n , is_static\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, TRUE)\n ON CONFLICT (oauth2_client_id)\n DO\n UPDATE SET encrypted_client_secret = EXCLUDED.encrypted_client_secret\n , redirect_uris = EXCLUDED.redirect_uris\n , grant_type_authorization_code = EXCLUDED.grant_type_authorization_code\n , grant_type_refresh_token = EXCLUDED.grant_type_refresh_token\n , grant_type_client_credentials = EXCLUDED.grant_type_client_credentials\n , grant_type_device_code = EXCLUDED.grant_type_device_code\n , token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method\n , jwks = EXCLUDED.jwks\n , client_name = EXCLUDED.client_name\n , jwks_uri = EXCLUDED.jwks_uri\n , is_static = TRUE\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "TextArray", + "Bool", + "Bool", + "Bool", + "Bool", + "Text", + "Jsonb", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "da02f93d7346992a9795f12b900f91ac0b326dd751c0d374d6ef4d19f671d22e" +} diff --git a/crates/storage-pg/.sqlx/query-fcd8b4b9e003d1540357c6bf1ff9c715560d011d4c01112703a9c046170c84f1.json b/crates/storage-pg/.sqlx/query-fcd8b4b9e003d1540357c6bf1ff9c715560d011d4c01112703a9c046170c84f1.json index f5503fa0e..ef1ac0372 100644 --- a/crates/storage-pg/.sqlx/query-fcd8b4b9e003d1540357c6bf1ff9c715560d011d4c01112703a9c046170c84f1.json +++ b/crates/storage-pg/.sqlx/query-fcd8b4b9e003d1540357c6bf1ff9c715560d011d4c01112703a9c046170c84f1.json @@ -23,7 +23,7 @@ "Left": [] }, "nullable": [ - false, + true, true, null ] diff --git a/crates/storage-pg/src/oauth2/client.rs b/crates/storage-pg/src/oauth2/client.rs index 02e57a01a..60e1ebb54 100644 --- a/crates/storage-pg/src/oauth2/client.rs +++ b/crates/storage-pg/src/oauth2/client.rs @@ -554,6 +554,7 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> { async fn upsert_static( &mut self, client_id: Ulid, + client_name: Option, client_auth_method: OAuthClientAuthenticationMethod, encrypted_client_secret: Option, jwks: Option, @@ -581,11 +582,12 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> { , grant_type_device_code , token_endpoint_auth_method , jwks + , client_name , jwks_uri , is_static ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, TRUE) + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, TRUE) ON CONFLICT (oauth2_client_id) DO UPDATE SET encrypted_client_secret = EXCLUDED.encrypted_client_secret @@ -596,6 +598,7 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> { , grant_type_device_code = EXCLUDED.grant_type_device_code , token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method , jwks = EXCLUDED.jwks + , client_name = EXCLUDED.client_name , jwks_uri = EXCLUDED.jwks_uri , is_static = TRUE "#, @@ -608,6 +611,7 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> { true, client_auth_method, jwks_json, + client_name, jwks_uri.as_ref().map(Url::as_str), ) .traced() @@ -633,7 +637,7 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> { GrantType::RefreshToken, GrantType::ClientCredentials, ], - client_name: None, + client_name, logo_uri: None, client_uri: None, policy_uri: None, diff --git a/crates/storage/src/oauth2/client.rs b/crates/storage/src/oauth2/client.rs index aa5a82a2a..33b92d189 100644 --- a/crates/storage/src/oauth2/client.rs +++ b/crates/storage/src/oauth2/client.rs @@ -157,6 +157,7 @@ pub trait OAuth2ClientRepository: Send + Sync { async fn upsert_static( &mut self, client_id: Ulid, + client_name: Option, client_auth_method: OAuthClientAuthenticationMethod, encrypted_client_secret: Option, jwks: Option, @@ -237,6 +238,7 @@ repository_impl!(OAuth2ClientRepository: async fn upsert_static( &mut self, client_id: Ulid, + client_name: Option, client_auth_method: OAuthClientAuthenticationMethod, encrypted_client_secret: Option, jwks: Option, From cae3edbc54bbdaea0741ded00e18a46fa1df4f50 Mon Sep 17 00:00:00 2001 From: Adis Veletanlic Date: Wed, 16 Apr 2025 12:58:55 +0200 Subject: [PATCH 003/189] Skip serialization if not provided client_name --- crates/config/src/sections/clients.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/config/src/sections/clients.rs b/crates/config/src/sections/clients.rs index d821c11bd..cffb814d3 100644 --- a/crates/config/src/sections/clients.rs +++ b/crates/config/src/sections/clients.rs @@ -80,6 +80,7 @@ pub struct ClientConfig { client_auth_method: ClientAuthMethodConfig, /// Name of the OAuth2 client + #[serde(skip_serializing_if = "Option::is_none")] pub client_name: Option, /// The client secret, used by the `client_secret_basic`, From 5365e7a172d4e919dfcf743f8f0e5c4c8302299f Mon Sep 17 00:00:00 2001 From: Adis Veletanlic Date: Wed, 16 Apr 2025 13:04:25 +0200 Subject: [PATCH 004/189] Fix clippy error --- crates/config/src/sections/clients.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/config/src/sections/clients.rs b/crates/config/src/sections/clients.rs index cffb814d3..2a0469677 100644 --- a/crates/config/src/sections/clients.rs +++ b/crates/config/src/sections/clients.rs @@ -79,7 +79,7 @@ pub struct ClientConfig { /// Authentication method used for this client client_auth_method: ClientAuthMethodConfig, - /// Name of the OAuth2 client + /// Name of the `OAuth2` client #[serde(skip_serializing_if = "Option::is_none")] pub client_name: Option, From e495b66ad1329021569527c2258498a3a2d8b67d Mon Sep 17 00:00:00 2001 From: Adis Veletanlic Date: Wed, 16 Apr 2025 13:16:43 +0200 Subject: [PATCH 005/189] Run ./misc/update.sh --- docs/config.schema.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/config.schema.json b/docs/config.schema.json index 165cf947d..313e672c5 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -239,6 +239,10 @@ } ] }, + "client_name": { + "description": "Name of the `OAuth2` client", + "type": "string" + }, "client_secret": { "description": "The client secret, used by the `client_secret_basic`, `client_secret_post` and `client_secret_jwt` authentication methods", "type": "string" From e1631f6dfd7a3c5776e2cff12fc8fac102b3e7e1 Mon Sep 17 00:00:00 2001 From: "Kai A. Hiller" Date: Wed, 16 Apr 2025 15:05:36 +0200 Subject: [PATCH 006/189] Fix headings in config doc Signed-off-by: Kai A. Hiller --- docs/reference/configuration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 30dbbfca9..122ae1460 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -560,7 +560,7 @@ telemetry: dsn: https://public@host:port/1 ``` -### `email` +## `email` Settings related to sending emails @@ -589,13 +589,13 @@ email: #transport: aws_ses ``` -### `upstream_oauth2` +## `upstream_oauth2` Settings related to upstream OAuth 2.0/OIDC providers. Additions and modifications within this section are synced with the database on server startup. Removed entries are only removed with the [`config sync --prune`](./cli/config.md#config-sync---prune---dry-run) command. -#### `upstream_oauth2.providers` +### `upstream_oauth2.providers` A list of upstream OAuth 2.0/OIDC providers to use to authenticate users. From 57c33dde1464ddce5d404a39d28f2fcdb0fc3c87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:59:53 +0000 Subject: [PATCH 007/189] build(deps): bump swagger-ui-dist from 5.20.7 to 5.21.0 in /frontend Bumps [swagger-ui-dist](https://github.com/swagger-api/swagger-ui) from 5.20.7 to 5.21.0. - [Release notes](https://github.com/swagger-api/swagger-ui/releases) - [Changelog](https://github.com/swagger-api/swagger-ui/blob/master/.releaserc) - [Commits](https://github.com/swagger-api/swagger-ui/compare/v5.20.7...v5.21.0) --- updated-dependencies: - dependency-name: swagger-ui-dist dependency-version: 5.21.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 8 ++++---- frontend/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 78a4b18e9..1c000c64e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,7 +24,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.4.1", - "swagger-ui-dist": "^5.20.7", + "swagger-ui-dist": "^5.21.0", "valibot": "^1.0.0", "vaul": "^1.1.2" }, @@ -12905,9 +12905,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.20.7", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.20.7.tgz", - "integrity": "sha512-gLpb1wrWinUwMFKfSvDYsIlCyGQSryftzi6uWc9Qo98zO3mFT6oHOqmDUu5OoahvepuS6HGTe/3MsGUCVtpLig==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz", + "integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" diff --git a/frontend/package.json b/frontend/package.json index 782c03469..164497c70 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,7 +34,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.4.1", - "swagger-ui-dist": "^5.20.7", + "swagger-ui-dist": "^5.21.0", "valibot": "^1.0.0", "vaul": "^1.1.2" }, From 70e809b9978c6a1e80170023da1173422bd2e3c3 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 15 Apr 2025 14:01:09 +0200 Subject: [PATCH 008/189] Introduce a task-level log context --- Cargo.lock | 11 +++ Cargo.toml | 17 +++++ crates/context/Cargo.toml | 19 +++++ crates/context/src/future.rs | 59 ++++++++++++++++ crates/context/src/layer.rs | 41 +++++++++++ crates/context/src/lib.rs | 126 ++++++++++++++++++++++++++++++++++ crates/context/src/service.rs | 54 +++++++++++++++ crates/listener/Cargo.toml | 2 +- crates/tower/Cargo.toml | 2 +- 9 files changed, 329 insertions(+), 2 deletions(-) create mode 100644 crates/context/Cargo.toml create mode 100644 crates/context/src/future.rs create mode 100644 crates/context/src/layer.rs create mode 100644 crates/context/src/lib.rs create mode 100644 crates/context/src/service.rs diff --git a/Cargo.lock b/Cargo.lock index e4a179e53..86a87f359 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3246,6 +3246,17 @@ dependencies = [ "url", ] +[[package]] +name = "mas-context" +version = "0.15.0-rc.0" +dependencies = [ + "pin-project-lite", + "quanta", + "tokio", + "tower-layer", + "tower-service", +] + [[package]] name = "mas-data-model" version = "0.15.0-rc.0" diff --git a/Cargo.toml b/Cargo.toml index f2bd90196..b6fe86947 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ broken_intra_doc_links = "deny" mas-axum-utils = { path = "./crates/axum-utils/", version = "=0.15.0-rc.0" } mas-cli = { path = "./crates/cli/", version = "=0.15.0-rc.0" } mas-config = { path = "./crates/config/", version = "=0.15.0-rc.0" } +mas-context = { path = "./crates/context/", version = "=0.15.0-rc.0" } mas-data-model = { path = "./crates/data-model/", version = "=0.15.0-rc.0" } mas-email = { path = "./crates/email/", version = "=0.15.0-rc.0" } mas-graphql = { path = "./crates/graphql/", version = "=0.15.0-rc.0" } @@ -248,6 +249,10 @@ features = ["std"] version = "0.7.0" features = ["std"] +# Pin projection +[workspace.dependencies.pin-project-lite] +version = "0.2.16" + # PKCS#1 encoding [workspace.dependencies.pkcs1] version = "0.7.5" @@ -258,6 +263,10 @@ features = ["std"] version = "0.10.2" features = ["std", "pkcs5", "encryption"] +# High-precision clock +[workspace.dependencies.quanta] +version = "0.12.5" + # Random values [workspace.dependencies.rand] version = "0.8.5" @@ -374,6 +383,14 @@ features = ["rt"] version = "0.5.2" features = ["util"] +# Tower service trait +[workspace.dependencies.tower-service] +version = "0.3.3" + +# Tower layer trait +[workspace.dependencies.tower-layer] +version = "0.3.3" + # Tower HTTP layers [workspace.dependencies.tower-http] version = "0.6.2" diff --git a/crates/context/Cargo.toml b/crates/context/Cargo.toml new file mode 100644 index 000000000..b0f422b51 --- /dev/null +++ b/crates/context/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "mas-context" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +publish = false + +[lints] +workspace = true + +[dependencies] +pin-project-lite.workspace = true +quanta.workspace = true +tokio.workspace = true +tower-service.workspace = true +tower-layer.workspace = true diff --git a/crates/context/src/future.rs b/crates/context/src/future.rs new file mode 100644 index 000000000..9e93af4fa --- /dev/null +++ b/crates/context/src/future.rs @@ -0,0 +1,59 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +use std::{ + pin::Pin, + sync::atomic::Ordering, + task::{Context, Poll}, +}; + +use quanta::Instant; +use tokio::task::futures::TaskLocalFuture; + +use crate::LogContext; + +pub type LogContextFuture = TaskLocalFuture>; + +impl LogContext { + /// Wrap a future with the given log context + pub(crate) fn wrap_future(&self, future: F) -> LogContextFuture { + let future = PollRecordingFuture::new(future); + crate::CURRENT_LOG_CONTEXT.scope(self.clone(), future) + } +} + +pin_project_lite::pin_project! { + /// A future which records the elapsed time and the number of polls in the + /// active log context + pub struct PollRecordingFuture { + #[pin] + inner: F, + } +} + +impl PollRecordingFuture { + pub(crate) fn new(inner: F) -> Self { + Self { inner } + } +} + +impl Future for PollRecordingFuture { + type Output = F::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let start = Instant::now(); + let this = self.project(); + let result = this.inner.poll(cx); + + // Record the number of polls and the time we spent polling the future + let elapsed = start.elapsed().as_nanos().try_into().unwrap_or(u64::MAX); + let _ = crate::CURRENT_LOG_CONTEXT.try_with(|c| { + c.inner.polls.fetch_add(1, Ordering::Relaxed); + c.inner.cpu_time.fetch_add(elapsed, Ordering::Relaxed); + }); + + result + } +} diff --git a/crates/context/src/layer.rs b/crates/context/src/layer.rs new file mode 100644 index 000000000..0ce6e3497 --- /dev/null +++ b/crates/context/src/layer.rs @@ -0,0 +1,41 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +use std::borrow::Cow; + +use tower_layer::Layer; +use tower_service::Service; + +use crate::LogContextService; + +/// A layer which creates a log context for each request. +pub struct LogContextLayer { + tagger: fn(&R) -> Cow<'static, str>, +} + +impl Clone for LogContextLayer { + fn clone(&self) -> Self { + Self { + tagger: self.tagger, + } + } +} + +impl LogContextLayer { + pub fn new(tagger: fn(&R) -> Cow<'static, str>) -> Self { + Self { tagger } + } +} + +impl Layer for LogContextLayer +where + S: Service, +{ + type Service = LogContextService; + + fn layer(&self, inner: S) -> Self::Service { + LogContextService::new(inner, self.tagger) + } +} diff --git a/crates/context/src/lib.rs b/crates/context/src/lib.rs new file mode 100644 index 000000000..54cdff095 --- /dev/null +++ b/crates/context/src/lib.rs @@ -0,0 +1,126 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +mod future; +mod layer; +mod service; + +use std::{ + borrow::Cow, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, +}; + +use quanta::Instant; +use tokio::task_local; + +pub use self::{ + future::{LogContextFuture, PollRecordingFuture}, + layer::LogContextLayer, + service::LogContextService, +}; + +/// A counter which increments each time we create a new log context +/// It will wrap around if we create more than [`u64::MAX`] contexts +static LOG_CONTEXT_INDEX: AtomicU64 = AtomicU64::new(0); +task_local! { + pub static CURRENT_LOG_CONTEXT: LogContext; +} + +/// A log context saves informations about the current task, such as the +/// elapsed time, the number of polls, and the poll time. +#[derive(Clone)] +pub struct LogContext { + inner: Arc, +} + +struct LogContextInner { + /// A user-defined tag for the log context + tag: Cow<'static, str>, + + /// A unique index for the log context + index: u64, + + /// The time when the context was created + start: Instant, + + /// The number of [`Future::poll`] recorded + polls: AtomicU64, + + /// An approximation of the total CPU time spent in the context + cpu_time: AtomicU64, +} + +impl LogContext { + /// Create a new log context with the given tag + pub fn new(tag: impl Into>) -> Self { + let tag = tag.into(); + let inner = LogContextInner { + tag, + index: LOG_CONTEXT_INDEX.fetch_add(1, Ordering::Relaxed), + start: Instant::now(), + polls: AtomicU64::new(0), + cpu_time: AtomicU64::new(0), + }; + + Self { + inner: Arc::new(inner), + } + } + + /// Get a copy of the current log context, if any + pub fn current() -> Option { + CURRENT_LOG_CONTEXT.try_with(Self::clone).ok() + } + + /// Run the async function `f` with the given log context. It will wrap the + /// output future to record poll and CPU statistics. + pub fn run Fut, Fut: Future>(&self, f: F) -> LogContextFuture { + let future = self.run_sync(f); + self.wrap_future(future) + } + + /// Run the sync function `f` with the given log context, recording the CPU + /// time spent. + pub fn run_sync R, R>(&self, f: F) -> R { + let start = Instant::now(); + let result = CURRENT_LOG_CONTEXT.sync_scope(self.clone(), f); + let elapsed = start.elapsed().as_nanos().try_into().unwrap_or(u64::MAX); + self.inner.cpu_time.fetch_add(elapsed, Ordering::Relaxed); + result + } +} + +impl std::fmt::Display for LogContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + #[expect(clippy::cast_precision_loss)] + let elapsed = self.inner.start.elapsed().as_nanos() as f64 / 1_000_000.; + + #[expect(clippy::cast_precision_loss)] + let cpu_time_ms = self.inner.cpu_time.load(Ordering::Relaxed) as f64 / 1_000_000.; + + let polls = self.inner.polls.load(Ordering::Relaxed); + let tag = &self.inner.tag; + let index = self.inner.index; + write!( + f, + "{tag}-{index} ({polls} polls, CPU: {cpu_time_ms:.3} ms, total: {elapsed:.3} ms)" + ) + } +} + +/// A helper which implements `Display` for printing the current log context +#[derive(Debug, Clone, Copy)] +pub struct CurrentLogContext; + +impl std::fmt::Display for CurrentLogContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + CURRENT_LOG_CONTEXT + .try_with(|c| c.fmt(f)) + .unwrap_or_else(|_| "".fmt(f)) + } +} diff --git a/crates/context/src/service.rs b/crates/context/src/service.rs new file mode 100644 index 000000000..98a1d1184 --- /dev/null +++ b/crates/context/src/service.rs @@ -0,0 +1,54 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +use std::{ + borrow::Cow, + task::{Context, Poll}, +}; + +use tower_service::Service; + +use crate::{LogContext, LogContextFuture}; + +/// A service which wraps another service and creates a log context for +/// each request. +pub struct LogContextService { + inner: S, + tagger: fn(&R) -> Cow<'static, str>, +} + +impl Clone for LogContextService { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + tagger: self.tagger, + } + } +} + +impl LogContextService { + pub fn new(inner: S, tagger: fn(&R) -> Cow<'static, str>) -> Self { + Self { inner, tagger } + } +} + +impl Service for LogContextService +where + S: Service, +{ + type Response = S::Response; + type Error = S::Error; + type Future = LogContextFuture; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: R) -> Self::Future { + let tag = (self.tagger)(&req); + let log_context = LogContext::new(tag); + log_context.run(|| self.inner.call(req)) + } +} diff --git a/crates/listener/Cargo.toml b/crates/listener/Cargo.toml index 4f9056c0f..d5178c049 100644 --- a/crates/listener/Cargo.toml +++ b/crates/listener/Cargo.toml @@ -17,7 +17,7 @@ futures-util.workspace = true http-body.workspace = true hyper = { workspace = true, features = ["server"] } hyper-util.workspace = true -pin-project-lite = "0.2.16" +pin-project-lite.workspace = true socket2 = "0.5.9" thiserror.workspace = true tokio.workspace = true diff --git a/crates/tower/Cargo.toml b/crates/tower/Cargo.toml index 978eaa3c1..52ef9da13 100644 --- a/crates/tower/Cargo.toml +++ b/crates/tower/Cargo.toml @@ -19,4 +19,4 @@ tower.workspace = true opentelemetry.workspace = true opentelemetry-http.workspace = true opentelemetry-semantic-conventions.workspace = true -pin-project-lite = "0.2.16" +pin-project-lite.workspace = true From 9e4689f263fc520d5ca1b79edca3ea702f0eb777 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 15 Apr 2025 14:34:45 +0200 Subject: [PATCH 009/189] Create a few basic logging contexts --- Cargo.lock | 3 ++ crates/cli/Cargo.toml | 1 + crates/cli/src/app_state.rs | 52 ++++++++++---------- crates/cli/src/commands/server.rs | 13 +++-- crates/cli/src/server.rs | 3 ++ crates/cli/src/util.rs | 30 ++++++----- crates/handlers/Cargo.toml | 1 + crates/handlers/src/upstream_oauth2/cache.rs | 5 +- crates/tasks/Cargo.toml | 1 + crates/tasks/src/new_queue.rs | 23 +++++---- 10 files changed, 79 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86a87f359..96915d6f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3165,6 +3165,7 @@ dependencies = [ "itertools 0.14.0", "listenfd", "mas-config", + "mas-context", "mas-data-model", "mas-email", "mas-handlers", @@ -3317,6 +3318,7 @@ dependencies = [ "lettre", "mas-axum-utils", "mas-config", + "mas-context", "mas-data-model", "mas-http", "mas-i18n", @@ -3685,6 +3687,7 @@ dependencies = [ "async-trait", "chrono", "cron", + "mas-context", "mas-data-model", "mas-email", "mas-i18n", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index cc40545cb..e181930c8 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -66,6 +66,7 @@ sentry-tracing.workspace = true sentry-tower.workspace = true mas-config.workspace = true +mas-context.workspace = true mas-data-model.workspace = true mas-email.workspace = true mas-handlers.workspace = true diff --git a/crates/cli/src/app_state.rs b/crates/cli/src/app_state.rs index 0fb6064ea..cd4ae44ad 100644 --- a/crates/cli/src/app_state.rs +++ b/crates/cli/src/app_state.rs @@ -8,6 +8,7 @@ use std::{convert::Infallible, net::IpAddr, sync::Arc, time::Instant}; use axum::extract::{FromRef, FromRequestParts}; use ipnetwork::IpNetwork; +use mas_context::LogContext; use mas_data_model::SiteConfig; use mas_handlers::{ ActivityTracker, BoundActivityTracker, CookieManager, ErrorWrapper, GraphQLSchema, Limiter, @@ -92,35 +93,36 @@ impl AppState { let http_client = self.http_client.clone(); tokio::spawn( - async move { - let conn = match pool.acquire().await { - Ok(conn) => conn, - Err(e) => { + LogContext::new("metadata-cache-warmup") + .run(async move || { + let conn = match pool.acquire().await { + Ok(conn) => conn, + Err(e) => { + tracing::error!( + error = &e as &dyn std::error::Error, + "Failed to acquire a database connection" + ); + return; + } + }; + + let mut repo = PgRepository::from_conn(conn); + + if let Err(e) = metadata_cache + .warm_up_and_run( + &http_client, + std::time::Duration::from_secs(60 * 15), + &mut repo, + ) + .await + { tracing::error!( error = &e as &dyn std::error::Error, - "Failed to acquire a database connection" + "Failed to warm up the metadata cache" ); - return; } - }; - - let mut repo = PgRepository::from_conn(conn); - - if let Err(e) = metadata_cache - .warm_up_and_run( - &http_client, - std::time::Duration::from_secs(60 * 15), - &mut repo, - ) - .await - { - tracing::error!( - error = &e as &dyn std::error::Error, - "Failed to warm up the metadata cache" - ); - } - } - .instrument(tracing::info_span!("metadata_cache.background_warmup")), + }) + .instrument(tracing::info_span!("metadata_cache.background_warmup")), ); } } diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index 15074c684..37d4f5392 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -13,6 +13,7 @@ use itertools::Itertools; use mas_config::{ AppConfig, ClientsConfig, ConfigurationSection, ConfigurationSectionExt, UpstreamOAuth2Config, }; +use mas_context::LogContext; use mas_handlers::{ActivityTracker, CookieManager, Limiter, MetadataCache}; use mas_listener::server::Server; use mas_router::UrlBuilder; @@ -316,11 +317,13 @@ impl Options { shutdown .task_tracker() - .spawn(mas_listener::server::run_servers( - servers, - shutdown.soft_shutdown_token(), - shutdown.hard_shutdown_token(), - )); + .spawn(LogContext::new("run-servers").run(|| { + mas_listener::server::run_servers( + servers, + shutdown.soft_shutdown_token(), + shutdown.hard_shutdown_token(), + ) + })); let exit_code = shutdown.run().await; diff --git a/crates/cli/src/server.rs b/crates/cli/src/server.rs index b40cc0d44..ecc846f4f 100644 --- a/crates/cli/src/server.rs +++ b/crates/cli/src/server.rs @@ -277,6 +277,9 @@ pub fn build_router( span.record("otel.status_code", "OK"); }), ) + .layer(mas_context::LogContextLayer::new(|req| { + otel_http_method(req).into() + })) .layer(SentryHttpLayer::new()) .layer(NewSentryLayer::new_from_top()) .with_state(state) diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index 2daad2e91..3d74fb4bf 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -12,6 +12,7 @@ use mas_config::{ EmailTransportKind, ExperimentalConfig, HomeserverKind, MatrixConfig, PasswordsConfig, PolicyConfig, TemplatesConfig, }; +use mas_context::LogContext; use mas_data_model::{SessionExpirationConfig, SiteConfig}; use mas_email::{MailTransport, Mailer}; use mas_handlers::passwords::PasswordManager; @@ -109,20 +110,23 @@ pub fn test_mailer_in_background(mailer: &Mailer, timeout: Duration) { let mailer = mailer.clone(); let span = tracing::info_span!("cli.test_mailer"); - tokio::spawn(async move { - match tokio::time::timeout(timeout, mailer.test_connection()).await { - Ok(Ok(())) => {} - Ok(Err(err)) => { - tracing::warn!( - error = &err as &dyn std::error::Error, - "Could not connect to the mail backend, tasks sending mails may fail!" - ); + tokio::spawn( + LogContext::new("mailer-test").run(async move || { + match tokio::time::timeout(timeout, mailer.test_connection()).await { + Ok(Ok(())) => {} + Ok(Err(err)) => { + tracing::warn!( + error = &err as &dyn std::error::Error, + "Could not connect to the mail backend, tasks sending mails may fail!" + ); + } + Err(_) => { + tracing::warn!("Timed out while testing the mail backend connection, tasks sending mails may fail!"); + } } - Err(_) => { - tracing::warn!("Timed out while testing the mail backend connection, tasks sending mails may fail!"); - } - } - }.instrument(span)); + }) + .instrument(span) + ); } pub async fn policy_factory_from_config( diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index 65c7bbb6f..21cf1ded1 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -90,6 +90,7 @@ ulid.workspace = true mas-axum-utils.workspace = true mas-config.workspace = true +mas-context.workspace = true mas-data-model.workspace = true mas-http.workspace = true mas-i18n.workspace = true diff --git a/crates/handlers/src/upstream_oauth2/cache.rs b/crates/handlers/src/upstream_oauth2/cache.rs index 02a202745..54253cac9 100644 --- a/crates/handlers/src/upstream_oauth2/cache.rs +++ b/crates/handlers/src/upstream_oauth2/cache.rs @@ -6,6 +6,7 @@ use std::{collections::HashMap, sync::Arc}; +use mas_context::LogContext; use mas_data_model::{ UpstreamOAuthProvider, UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderPkceMode, }; @@ -197,7 +198,9 @@ impl MetadataCache { loop { // Re-fetch the known metadata at the given interval tokio::time::sleep(interval).await; - cache.refresh_all(&client).await; + LogContext::new("metadata-cache-refresh") + .run(|| cache.refresh_all(&client)) + .await; } })) } diff --git a/crates/tasks/Cargo.toml b/crates/tasks/Cargo.toml index 306e5cede..18eb740d5 100644 --- a/crates/tasks/Cargo.toml +++ b/crates/tasks/Cargo.toml @@ -30,6 +30,7 @@ ulid.workspace = true serde.workspace = true serde_json.workspace = true +mas-context.workspace = true mas-data-model.workspace = true mas-email.workspace = true mas-i18n.workspace = true diff --git a/crates/tasks/src/new_queue.rs b/crates/tasks/src/new_queue.rs index b78d9014a..a2021c364 100644 --- a/crates/tasks/src/new_queue.rs +++ b/crates/tasks/src/new_queue.rs @@ -8,6 +8,7 @@ use std::{collections::HashMap, sync::Arc}; use async_trait::async_trait; use chrono::{DateTime, Duration, Utc}; use cron::Schedule; +use mas_context::LogContext; use mas_storage::{ Clock, RepositoryAccess, RepositoryError, queue::{InsertableJob, Job, JobMetadata, Worker}, @@ -337,7 +338,9 @@ impl QueueWorker { self.setup_schedules().await?; while !self.cancellation_token.is_cancelled() { - self.run_loop().await?; + LogContext::new("worker-run-loop") + .run(|| self.run_loop()) + .await?; } self.shutdown().await?; @@ -771,16 +774,18 @@ impl JobTracker { fn spawn_job(&mut self, state: State, context: JobContext, payload: JobPayload) { let factory = self.factories.get(context.queue_name.as_str()).cloned(); let task = { + let log_context = LogContext::new(format!("worker-job-{}", context.queue_name)); let context = context.clone(); let span = context.span(); - async move { - // We should never crash, but in case we do, we do that in the task and - // don't crash the worker - let job = factory.expect("unknown job factory")(payload); - tracing::info!("Running job"); - job.run(&state, context).await - } - .instrument(span) + log_context + .run(async move || { + // We should never crash, but in case we do, we do that in the task and + // don't crash the worker + let job = factory.expect("unknown job factory")(payload); + tracing::info!("Running job"); + job.run(&state, context).await + }) + .instrument(span) }; self.in_flight_jobs.add( From b56b5ebd4767f09288ed79102e3001b13538708c Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 16 Apr 2025 16:04:42 +0200 Subject: [PATCH 010/189] Provide log context stats in a separate structure --- crates/context/src/lib.rs | 60 ++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/crates/context/src/lib.rs b/crates/context/src/lib.rs index 54cdff095..d189cb53e 100644 --- a/crates/context/src/lib.rs +++ b/crates/context/src/lib.rs @@ -13,6 +13,7 @@ use std::{ Arc, atomic::{AtomicU64, Ordering}, }, + time::Duration, }; use quanta::Instant; @@ -93,34 +94,53 @@ impl LogContext { self.inner.cpu_time.fetch_add(elapsed, Ordering::Relaxed); result } + + /// Create a snapshot of the log context statistics + #[must_use] + pub fn stats(&self) -> LogContextStats { + let polls = self.inner.polls.load(Ordering::Relaxed); + let cpu_time = self.inner.cpu_time.load(Ordering::Relaxed); + let cpu_time = Duration::from_nanos(cpu_time); + let elapsed = self.inner.start.elapsed(); + LogContextStats { + polls, + cpu_time, + elapsed, + } + } } impl std::fmt::Display for LogContext { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - #[expect(clippy::cast_precision_loss)] - let elapsed = self.inner.start.elapsed().as_nanos() as f64 / 1_000_000.; - - #[expect(clippy::cast_precision_loss)] - let cpu_time_ms = self.inner.cpu_time.load(Ordering::Relaxed) as f64 / 1_000_000.; - - let polls = self.inner.polls.load(Ordering::Relaxed); let tag = &self.inner.tag; let index = self.inner.index; + write!(f, "{tag}-{index}") + } +} + +/// A snapshot of a log context statistics +#[derive(Debug, Clone, Copy)] +pub struct LogContextStats { + /// How many times the context was polled + pub polls: u64, + + /// The approximate CPU time spent in the context + pub cpu_time: Duration, + + /// How much time elapsed since the context was created + pub elapsed: Duration, +} + +impl std::fmt::Display for LogContextStats { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let polls = self.polls; + #[expect(clippy::cast_precision_loss)] + let cpu_time_ms = self.cpu_time.as_nanos() as f64 / 1_000_000.; + #[expect(clippy::cast_precision_loss)] + let elapsed_ms = self.elapsed.as_nanos() as f64 / 1_000_000.; write!( f, - "{tag}-{index} ({polls} polls, CPU: {cpu_time_ms:.3} ms, total: {elapsed:.3} ms)" + "polls: {polls:>3}, cpu: {cpu_time_ms:>6.3}ms, elapsed: {elapsed_ms:>6.3}ms", ) } } - -/// A helper which implements `Display` for printing the current log context -#[derive(Debug, Clone, Copy)] -pub struct CurrentLogContext; - -impl std::fmt::Display for CurrentLogContext { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - CURRENT_LOG_CONTEXT - .try_with(|c| c.fmt(f)) - .unwrap_or_else(|_| "".fmt(f)) - } -} From aa174a5ea05eb49d3515c5c05adfdd81ee91c983 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 16 Apr 2025 16:06:06 +0200 Subject: [PATCH 011/189] Roll our own event formatter --- Cargo.lock | 5 ++ Cargo.toml | 4 ++ crates/cli/src/main.rs | 3 +- crates/context/Cargo.toml | 5 ++ crates/context/src/fmt.rs | 141 ++++++++++++++++++++++++++++++++++++++ crates/context/src/lib.rs | 2 + 6 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 crates/context/src/fmt.rs diff --git a/Cargo.lock b/Cargo.lock index 96915d6f8..ee216fcbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3251,11 +3251,16 @@ dependencies = [ name = "mas-context" version = "0.15.0-rc.0" dependencies = [ + "console", + "opentelemetry", "pin-project-lite", "quanta", "tokio", "tower-layer", "tower-service", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b6fe86947..8718e70f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,10 @@ version = "1.1.9" [workspace.dependencies.compact_str] version = "0.9.0" +# Terminal formatting +[workspace.dependencies.console] +version = "0.15.11" + # Time utilities [workspace.dependencies.chrono] version = "0.4.40" diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 85c5a89f1..1477b634a 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -91,8 +91,7 @@ async fn try_main() -> anyhow::Result { let (log_writer, _guard) = tracing_appender::non_blocking(output); let fmt_layer = tracing_subscriber::fmt::layer() .with_writer(log_writer) - .with_file(true) - .with_line_number(true) + .event_format(mas_context::EventFormatter) .with_ansi(with_ansi); let filter_layer = EnvFilter::try_from_default_env() .or_else(|_| EnvFilter::try_new("info")) diff --git a/crates/context/Cargo.toml b/crates/context/Cargo.toml index b0f422b51..762985080 100644 --- a/crates/context/Cargo.toml +++ b/crates/context/Cargo.toml @@ -12,8 +12,13 @@ publish = false workspace = true [dependencies] +console.workspace = true pin-project-lite.workspace = true quanta.workspace = true tokio.workspace = true tower-service.workspace = true tower-layer.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +tracing-opentelemetry.workspace = true +opentelemetry.workspace = true diff --git a/crates/context/src/fmt.rs b/crates/context/src/fmt.rs new file mode 100644 index 000000000..345025c10 --- /dev/null +++ b/crates/context/src/fmt.rs @@ -0,0 +1,141 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +use console::{Color, Style}; +use opentelemetry::{TraceId, trace::TraceContextExt}; +use tracing::{Level, Subscriber}; +use tracing_opentelemetry::OtelData; +use tracing_subscriber::{ + fmt::{ + FormatEvent, FormatFields, + format::{DefaultFields, Writer}, + time::{FormatTime, SystemTime}, + }, + registry::LookupSpan, +}; + +use crate::LogContext; + +/// An event formatter usable by the [`tracing-subscriber`] crate, which +/// includes the log context and the OTEL trace ID. +#[derive(Debug, Default)] +pub struct EventFormatter; + +struct FmtLevel<'a> { + level: &'a Level, + ansi: bool, +} + +impl<'a> FmtLevel<'a> { + pub(crate) fn new(level: &'a Level, ansi: bool) -> Self { + Self { level, ansi } + } +} + +const TRACE_STR: &str = "TRACE"; +const DEBUG_STR: &str = "DEBUG"; +const INFO_STR: &str = " INFO"; +const WARN_STR: &str = " WARN"; +const ERROR_STR: &str = "ERROR"; + +const TRACE_STYLE: Style = Style::new().fg(Color::Magenta); +const DEBUG_STYLE: Style = Style::new().fg(Color::Blue); +const INFO_STYLE: Style = Style::new().fg(Color::Green); +const WARN_STYLE: Style = Style::new().fg(Color::Yellow); +const ERROR_STYLE: Style = Style::new().fg(Color::Red); + +impl std::fmt::Display for FmtLevel<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let msg = match *self.level { + Level::TRACE => TRACE_STYLE.force_styling(self.ansi).apply_to(TRACE_STR), + Level::DEBUG => DEBUG_STYLE.force_styling(self.ansi).apply_to(DEBUG_STR), + Level::INFO => INFO_STYLE.force_styling(self.ansi).apply_to(INFO_STR), + Level::WARN => WARN_STYLE.force_styling(self.ansi).apply_to(WARN_STR), + Level::ERROR => ERROR_STYLE.force_styling(self.ansi).apply_to(ERROR_STR), + }; + write!(f, "{msg}") + } +} + +struct TargetFmt<'a> { + target: &'a str, + line: Option, +} + +impl<'a> TargetFmt<'a> { + pub(crate) fn new(metadata: &tracing::Metadata<'a>) -> Self { + Self { + target: metadata.target(), + line: metadata.line(), + } + } +} + +impl std::fmt::Display for TargetFmt<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.target)?; + if let Some(line) = self.line { + write!(f, ":{line}")?; + } + Ok(()) + } +} + +impl FormatEvent for EventFormatter +where + S: Subscriber + for<'a> LookupSpan<'a>, + N: for<'writer> FormatFields<'writer> + 'static, +{ + fn format_event( + &self, + ctx: &tracing_subscriber::fmt::FmtContext<'_, S, N>, + mut writer: Writer<'_>, + event: &tracing::Event<'_>, + ) -> std::fmt::Result { + let ansi = writer.has_ansi_escapes(); + let metadata = event.metadata(); + + SystemTime.format_time(&mut writer)?; + + let level = FmtLevel::new(metadata.level(), ansi); + let target = Style::new() + .dim() + .force_styling(ansi) + .apply_to(TargetFmt::new(metadata)); + write!(&mut writer, " {level} {target} ")?; + + if let Some(log_context) = LogContext::current() { + let log_context = Style::new() + .bold() + .force_styling(ansi) + .apply_to(log_context); + write!(&mut writer, "{log_context} - ")?; + } + + let field_fromatter = DefaultFields::new(); + field_fromatter.format_fields(writer.by_ref(), event)?; + + // If we have a OTEL span, we can add the trace ID to the end of the log line + if let Some(span) = ctx.lookup_current() { + if let Some(otel) = span.extensions().get::() { + // If it is the root span, the trace ID will be in the span builder. Else, it + // will be in the parent OTEL context + let trace_id = otel + .builder + .trace_id + .unwrap_or_else(|| otel.parent_cx.span().span_context().trace_id()); + if trace_id != TraceId::INVALID { + let label = Style::new() + .italic() + .force_styling(ansi) + .apply_to("trace.id"); + write!(&mut writer, " {label}={trace_id}")?; + } + } + } + + writeln!(&mut writer) + } +} diff --git a/crates/context/src/lib.rs b/crates/context/src/lib.rs index d189cb53e..b289bfcda 100644 --- a/crates/context/src/lib.rs +++ b/crates/context/src/lib.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +mod fmt; mod future; mod layer; mod service; @@ -20,6 +21,7 @@ use quanta::Instant; use tokio::task_local; pub use self::{ + fmt::EventFormatter, future::{LogContextFuture, PollRecordingFuture}, layer::LogContextLayer, service::LogContextService, From 7c9754d5378465981899ed05fded81c659ddc7b6 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 16 Apr 2025 16:39:21 +0200 Subject: [PATCH 012/189] Add context to some log messages --- crates/cli/src/commands/manage.rs | 10 ++++++++-- crates/cli/src/sync.rs | 16 ++++++++-------- crates/handlers/src/admin/v1/users/deactivate.rs | 2 +- crates/handlers/src/graphql/mutations/user.rs | 2 +- crates/tasks/src/new_queue.rs | 9 +++++++-- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index 4cf59a483..719a0953f 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -67,7 +67,7 @@ enum Subcommand { /// Add an email address to the specified user AddEmail { username: String, email: String }, - /// [DEPRECATED] Mark email address as verified + /// \[DEPRECATED\] Mark email address as verified VerifyEmail { username: String, email: String }, /// Set a user password @@ -255,7 +255,13 @@ impl Options { }; repo.into_inner().commit().await?; - info!(?email, "Email added"); + info!( + %user.id, + %user.username, + %email.id, + %email.email, + "Email added" + ); Ok(ExitCode::SUCCESS) } diff --git a/crates/cli/src/sync.rs b/crates/cli/src/sync.rs index 36f9568e8..52e0f3337 100644 --- a/crates/cli/src/sync.rs +++ b/crates/cli/src/sync.rs @@ -175,11 +175,11 @@ pub async fn config_sync( let _span = info_span!("provider", %provider.id).entered(); if existing_enabled_ids.contains(&provider.id) { - info!("Updating provider"); + info!(provider.id = %provider.id, "Updating provider"); } else if existing_disabled.contains_key(&provider.id) { - info!("Enabling and updating provider"); + info!(provider.id = %provider.id, "Enabling and updating provider"); } else { - info!("Adding provider"); + info!(provider.id = %provider.id, "Adding provider"); } if dry_run { @@ -252,15 +252,15 @@ pub async fn config_sync( if discovery_mode.is_disabled() { if provider.authorization_endpoint.is_none() { - error!("Provider has discovery disabled but no authorization endpoint set"); + error!(provider.id = %provider.id, "Provider has discovery disabled but no authorization endpoint set"); } if provider.token_endpoint.is_none() { - error!("Provider has discovery disabled but no token endpoint set"); + error!(provider.id = %provider.id, "Provider has discovery disabled but no token endpoint set"); } if provider.jwks_uri.is_none() { - warn!("Provider has discovery disabled but no JWKS URI set"); + warn!(provider.id = %provider.id, "Provider has discovery disabled but no JWKS URI set"); } } @@ -347,9 +347,9 @@ pub async fn config_sync( for client in clients_config { let _span = info_span!("client", client.id = %client.client_id).entered(); if existing_ids.contains(&client.client_id) { - info!("Updating client"); + info!(client.id = %client.client_id, "Updating client"); } else { - info!("Adding client"); + info!(client.id = %client.client_id, "Adding client"); } if dry_run { diff --git a/crates/handlers/src/admin/v1/users/deactivate.rs b/crates/handlers/src/admin/v1/users/deactivate.rs index 25d4a0339..60adad850 100644 --- a/crates/handlers/src/admin/v1/users/deactivate.rs +++ b/crates/handlers/src/admin/v1/users/deactivate.rs @@ -86,7 +86,7 @@ pub async fn handler( user = repo.user().lock(&clock, user).await?; } - info!("Scheduling deactivation of user {}", user.id); + info!(%user.id, "Scheduling deactivation of user"); repo.queue_job() .schedule_job(&mut rng, &clock, DeactivateUserJob::new(&user, true)) .await?; diff --git a/crates/handlers/src/graphql/mutations/user.rs b/crates/handlers/src/graphql/mutations/user.rs index ec9d2afe0..301307d96 100644 --- a/crates/handlers/src/graphql/mutations/user.rs +++ b/crates/handlers/src/graphql/mutations/user.rs @@ -552,7 +552,7 @@ impl UserMutations { let user = repo.user().lock(&state.clock(), user).await?; if deactivate { - info!("Scheduling deactivation of user {}", user.id); + info!(%user.id, "Scheduling deactivation of user"); repo.queue_job() .schedule_job(&mut rng, &clock, DeactivateUserJob::new(&user, deactivate)) .await?; diff --git a/crates/tasks/src/new_queue.rs b/crates/tasks/src/new_queue.rs index a2021c364..a9ba39dfb 100644 --- a/crates/tasks/src/new_queue.rs +++ b/crates/tasks/src/new_queue.rs @@ -253,7 +253,7 @@ impl QueueWorker { .await .map_err(QueueRunnerError::CommitTransaction)?; - tracing::info!("Registered worker"); + tracing::info!(worker.id = %registration.id, "Registered worker"); let now = clock.now(); let wakeup_reason = METER @@ -782,7 +782,12 @@ impl JobTracker { // We should never crash, but in case we do, we do that in the task and // don't crash the worker let job = factory.expect("unknown job factory")(payload); - tracing::info!("Running job"); + tracing::info!( + job.id = %context.id, + job.queue.name = %context.queue_name, + job.attempt = %context.attempt, + "Running job" + ); job.run(&state, context).await }) .instrument(span) From c8c63a10ad88440a89708521df5b8348ecc8f6f4 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 10:45:14 +0200 Subject: [PATCH 013/189] Log on every HTTP response --- Cargo.lock | 1 + crates/cli/Cargo.toml | 1 + crates/cli/src/server.rs | 42 +++++++++++++++++++++++++++++++++++++++ crates/context/src/fmt.rs | 17 +++++++++++----- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ee216fcbd..8d51c3a1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3159,6 +3159,7 @@ dependencies = [ "dotenvy", "figment", "futures-util", + "headers", "http-body-util", "hyper", "ipnetwork", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index e181930c8..55def8cea 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -27,6 +27,7 @@ dialoguer = { version = "0.11.0", default-features = false, features = [ dotenvy = "0.15.7" figment.workspace = true futures-util.workspace = true +headers.workspace = true http-body-util.workspace = true hyper.workspace = true ipnetwork = "0.20.0" diff --git a/crates/cli/src/server.rs b/crates/cli/src/server.rs index ecc846f4f..45fa40e6b 100644 --- a/crates/cli/src/server.rs +++ b/crates/cli/src/server.rs @@ -16,12 +16,14 @@ use axum::{ error_handling::HandleErrorLayer, extract::{FromRef, MatchedPath}, }; +use headers::{HeaderMapExt as _, UserAgent}; use hyper::{ Method, Request, Response, StatusCode, Version, header::{CACHE_CONTROL, HeaderValue, USER_AGENT}, }; use listenfd::ListenFd; use mas_config::{HttpBindConfig, HttpResource, HttpTlsConfig, UnixOrTcp}; +use mas_context::LogContext; use mas_listener::{ConnectionInfo, unix_or_tcp::UnixOrTcpListener}; use mas_router::Route; use mas_templates::Templates; @@ -170,6 +172,45 @@ fn on_http_response_labels(res: &Response) -> Vec { )] } +async fn log_response_middleware( + request: axum::extract::Request, + next: axum::middleware::Next, +) -> axum::response::Response { + let user_agent: Option = request.headers().typed_get(); + let user_agent = user_agent.as_ref().map_or("-", |u| u.as_str()); + let method = otel_http_method(&request); + let path = request.uri().path().to_owned(); + let version = otel_net_protocol_version(&request); + + let response = next.run(request).await; + + let Some(log_context) = LogContext::current() else { + tracing::error!("Missing log context for request, this is a bug!"); + return response; + }; + + let stats = log_context.stats(); + + let status_code = response.status(); + match status_code.as_u16() { + 100..=399 => tracing::info!( + name: "http.server.response", + "\"{method} {path} HTTP/{version}\" {status_code} {user_agent:?} [{stats}]", + ), + 400..=499 => tracing::warn!( + name: "http.server.response", + "\"{method} {path} HTTP/{version}\" {status_code} {user_agent:?} [{stats}]", + ), + 500..=599 => tracing::error!( + name: "http.server.response", + "\"{method} {path} HTTP/{version}\" {status_code} {user_agent:?} [{stats}]", + ), + _ => { /* This shouldn't happen */ } + } + + response +} + pub fn build_router( state: AppState, resources: &[HttpResource], @@ -277,6 +318,7 @@ pub fn build_router( span.record("otel.status_code", "OK"); }), ) + .layer(axum::middleware::from_fn(log_response_middleware)) .layer(mas_context::LogContextLayer::new(|req| { otel_http_method(req).into() })) diff --git a/crates/context/src/fmt.rs b/crates/context/src/fmt.rs index 345025c10..f2e619690 100644 --- a/crates/context/src/fmt.rs +++ b/crates/context/src/fmt.rs @@ -100,11 +100,18 @@ where SystemTime.format_time(&mut writer)?; let level = FmtLevel::new(metadata.level(), ansi); - let target = Style::new() - .dim() - .force_styling(ansi) - .apply_to(TargetFmt::new(metadata)); - write!(&mut writer, " {level} {target} ")?; + write!(&mut writer, " {level} ")?; + + // If there is no explicit 'name' set in the event macro, it will have the + // 'event {filename}:{line}' value. In this case, we want to display the target: + // the module from where it was emitted. In other cases, we want to + // display the explit name of the event we have set. + let style = Style::new().dim().force_styling(ansi); + if metadata.name().starts_with("event ") { + write!(&mut writer, "{} ", style.apply_to(TargetFmt::new(metadata)))?; + } else { + write!(&mut writer, "{} ", style.apply_to(metadata.name()))?; + } if let Some(log_context) = LogContext::current() { let log_context = Style::new() From 37a0af85a6d2f4b8191e9cf301c291f583dbaf6b Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 13:19:11 +0200 Subject: [PATCH 014/189] Macro to record an HTTP response error with the Sentry event ID attached --- crates/axum-utils/src/sentry.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/crates/axum-utils/src/sentry.rs b/crates/axum-utils/src/sentry.rs index ffa8fac17..5dd00a211 100644 --- a/crates/axum-utils/src/sentry.rs +++ b/crates/axum-utils/src/sentry.rs @@ -13,6 +13,13 @@ use sentry::types::Uuid; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct SentryEventID(Uuid); +impl SentryEventID { + /// Create a new Sentry event ID header for the last event on the hub. + pub fn for_last_event() -> Option { + sentry::last_event_id().map(Self) + } +} + impl From for SentryEventID { fn from(uuid: Uuid) -> Self { Self(uuid) @@ -28,3 +35,28 @@ impl IntoResponseParts for SentryEventID { Ok(res) } } + +/// Record an error. It will emit a tracing event with the error level if +/// matches the pattern, warning otherwise. It also returns the Sentry event ID +/// if the error was recorded. +#[macro_export] +macro_rules! record_error { + ($error:expr, !) => {{ + tracing::warn!(message = &$error as &dyn std::error::Error); + Option::<$crate::sentry::SentryEventID>::None + }}; + + ($error:expr, $pattern:pat) => { + if let $pattern = $error { + tracing::error!(message = &$error as &dyn std::error::Error); + + // With the `sentry-tracing` integration, Sentry should have + // captured an error, so let's extract the last event ID from the + // current hub + $crate::sentry::SentryEventID::for_last_event() + } else { + tracing::warn!(message = &$error as &dyn std::error::Error); + None + } + }; +} From c380cd0f987c512eb0cd12d46faa2ec21c0ef71c Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 13:19:46 +0200 Subject: [PATCH 015/189] Exclude the HTTP server response events from Sentry --- crates/cli/src/main.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 1477b634a..bd5aa2ba8 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -128,9 +128,11 @@ async fn try_main() -> anyhow::Result { let sentry_layer = sentry.is_enabled().then(|| { sentry_tracing::layer().event_filter(|md| { - // All the spans in the handlers module send their data to Sentry themselves, so - // we only create breadcrumbs for them, instead of full events - if md.target().starts_with("mas_handlers::") { + // By default, Sentry records all events as breadcrumbs, except errors. + // + // Because we're emitting error events for 5xx responses, we need to exclude + // them and also record them as breadcrumbs. + if md.name() == "http.server.response" { EventFilter::Breadcrumb } else { sentry_tracing::default_event_filter(md) From 488991f8d60e8cc751ae21786216ad9b5308f5de Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 13:20:35 +0200 Subject: [PATCH 016/189] handlers::admin: don't rely on #[instrument(err)] for logging errors --- crates/handlers/src/admin/call_context.rs | 45 +++++++++++-------- .../src/admin/v1/compat_sessions/get.rs | 9 ++-- .../src/admin/v1/compat_sessions/list.rs | 9 ++-- .../src/admin/v1/oauth2_sessions/get.rs | 6 ++- .../src/admin/v1/oauth2_sessions/list.rs | 6 ++- .../handlers/src/admin/v1/policy_data/get.rs | 6 ++- .../src/admin/v1/policy_data/get_latest.rs | 6 ++- .../handlers/src/admin/v1/policy_data/set.rs | 6 ++- .../src/admin/v1/upstream_oauth_links/add.rs | 6 ++- .../admin/v1/upstream_oauth_links/delete.rs | 6 ++- .../src/admin/v1/upstream_oauth_links/get.rs | 6 ++- .../src/admin/v1/upstream_oauth_links/list.rs | 6 ++- .../handlers/src/admin/v1/user_emails/add.rs | 6 ++- .../src/admin/v1/user_emails/delete.rs | 6 ++- .../handlers/src/admin/v1/user_emails/get.rs | 6 ++- .../handlers/src/admin/v1/user_emails/list.rs | 6 ++- .../src/admin/v1/user_sessions/get.rs | 6 ++- .../src/admin/v1/user_sessions/list.rs | 6 ++- crates/handlers/src/admin/v1/users/add.rs | 6 ++- .../src/admin/v1/users/by_username.rs | 6 ++- .../handlers/src/admin/v1/users/deactivate.rs | 6 ++- crates/handlers/src/admin/v1/users/get.rs | 6 ++- crates/handlers/src/admin/v1/users/list.rs | 6 ++- crates/handlers/src/admin/v1/users/lock.rs | 6 ++- .../handlers/src/admin/v1/users/set_admin.rs | 6 ++- .../src/admin/v1/users/set_password.rs | 6 ++- crates/handlers/src/admin/v1/users/unlock.rs | 6 ++- 27 files changed, 135 insertions(+), 72 deletions(-) diff --git a/crates/handlers/src/admin/call_context.rs b/crates/handlers/src/admin/call_context.rs index 0b812db05..95340b160 100644 --- a/crates/handlers/src/admin/call_context.rs +++ b/crates/handlers/src/admin/call_context.rs @@ -15,6 +15,7 @@ use axum::{ use axum_extra::TypedHeader; use headers::{Authorization, authorization::Bearer}; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_data_model::{Session, User}; use mas_storage::{BoxClock, BoxRepository, RepositoryError}; use ulid::Ulid; @@ -69,27 +70,35 @@ pub enum Rejection { MissingScope, } -impl Rejection { - fn status_code(&self) -> StatusCode { - match self { - Self::InvalidAuthorizationHeader | Self::MissingAuthorizationHeader => { - StatusCode::BAD_REQUEST - } - Self::UnknownAccessToken - | Self::TokenExpired - | Self::SessionRevoked - | Self::UserLocked - | Self::MissingScope => StatusCode::UNAUTHORIZED, - _ => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - impl IntoResponse for Rejection { fn into_response(self) -> Response { let response = ErrorResponse::from_error(&self); - let status = self.status_code(); - (status, Json(response)).into_response() + let sentry_event_id = record_error!( + self, + Self::RepositorySetup(_) + | Self::Repository(_) + | Self::LoadSession(_) + | Self::LoadUser(_) + ); + + let status = match &self { + Rejection::InvalidAuthorizationHeader | Rejection::MissingAuthorizationHeader => { + StatusCode::BAD_REQUEST + } + + Rejection::UnknownAccessToken + | Rejection::TokenExpired + | Rejection::SessionRevoked + | Rejection::UserLocked + | Rejection::MissingScope => StatusCode::UNAUTHORIZED, + + Rejection::RepositorySetup(_) + | Rejection::Repository(_) + | Rejection::LoadSession(_) + | Rejection::LoadUser(_) => StatusCode::INTERNAL_SERVER_ERROR, + }; + + (status, sentry_event_id, Json(response)).into_response() } } diff --git a/crates/handlers/src/admin/v1/compat_sessions/get.rs b/crates/handlers/src/admin/v1/compat_sessions/get.rs index 16a720849..f39fc79da 100644 --- a/crates/handlers/src/admin/v1/compat_sessions/get.rs +++ b/crates/handlers/src/admin/v1/compat_sessions/get.rs @@ -6,6 +6,7 @@ use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use ulid::Ulid; use crate::{ @@ -33,11 +34,13 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); - let status = match self { + let sentry_event_id = record_error!(self, RouteError::Internal(_)); + let status = match &self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + + (status, sentry_event_id, Json(error)).into_response() } } @@ -59,7 +62,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.compat_sessions.get", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.compat_sessions.get", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, id: UlidPathParam, diff --git a/crates/handlers/src/admin/v1/compat_sessions/list.rs b/crates/handlers/src/admin/v1/compat_sessions/list.rs index f08fcae1c..a882f6d56 100644 --- a/crates/handlers/src/admin/v1/compat_sessions/list.rs +++ b/crates/handlers/src/admin/v1/compat_sessions/list.rs @@ -11,6 +11,7 @@ use axum::{ }; use axum_macros::FromRequestParts; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_storage::{Page, compat::CompatSessionFilter}; use schemars::JsonSchema; use serde::Deserialize; @@ -113,12 +114,14 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); - let status = match self { + let sentry_event_id = record_error!(self, RouteError::Internal(_)); + let status = match &self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::UserNotFound(_) | Self::UserSessionNotFound(_) => StatusCode::NOT_FOUND, Self::InvalidFilter(_) => StatusCode::BAD_REQUEST, }; - (status, Json(error)).into_response() + + (status, sentry_event_id, Json(error)).into_response() } } @@ -153,7 +156,7 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p }) } -#[tracing::instrument(name = "handler.admin.v1.compat_sessions.list", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.compat_sessions.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, Pagination(pagination): Pagination, diff --git a/crates/handlers/src/admin/v1/oauth2_sessions/get.rs b/crates/handlers/src/admin/v1/oauth2_sessions/get.rs index e8fce4a0a..e5e602c62 100644 --- a/crates/handlers/src/admin/v1/oauth2_sessions/get.rs +++ b/crates/handlers/src/admin/v1/oauth2_sessions/get.rs @@ -7,6 +7,7 @@ use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use ulid::Ulid; use crate::{ @@ -34,11 +35,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, RouteError::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -60,7 +62,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.oauth2_session.get", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.oauth2_session.get", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, id: UlidPathParam, diff --git a/crates/handlers/src/admin/v1/oauth2_sessions/list.rs b/crates/handlers/src/admin/v1/oauth2_sessions/list.rs index eeccfc12b..6b75caadd 100644 --- a/crates/handlers/src/admin/v1/oauth2_sessions/list.rs +++ b/crates/handlers/src/admin/v1/oauth2_sessions/list.rs @@ -14,6 +14,7 @@ use axum::{ }; use axum_macros::FromRequestParts; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_storage::{Page, oauth2::OAuth2SessionFilter}; use oauth2_types::scope::{Scope, ScopeToken}; use schemars::JsonSchema; @@ -167,6 +168,7 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, RouteError::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::UserNotFound(_) | Self::ClientNotFound(_) | Self::UserSessionNotFound(_) => { @@ -174,7 +176,7 @@ impl IntoResponse for RouteError { } Self::InvalidScope(_) | Self::InvalidFilter(_) => StatusCode::BAD_REQUEST, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -213,7 +215,7 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p }) } -#[tracing::instrument(name = "handler.admin.v1.oauth2_sessions.list", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.oauth2_sessions.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, Pagination(pagination): Pagination, diff --git a/crates/handlers/src/admin/v1/policy_data/get.rs b/crates/handlers/src/admin/v1/policy_data/get.rs index 338c999b3..51d8c7849 100644 --- a/crates/handlers/src/admin/v1/policy_data/get.rs +++ b/crates/handlers/src/admin/v1/policy_data/get.rs @@ -5,6 +5,7 @@ use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use ulid::Ulid; use crate::{ @@ -32,11 +33,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -56,7 +58,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.policy_data.get", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.policy_data.get", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, id: UlidPathParam, diff --git a/crates/handlers/src/admin/v1/policy_data/get_latest.rs b/crates/handlers/src/admin/v1/policy_data/get_latest.rs index 7b4c0654f..f217b30dc 100644 --- a/crates/handlers/src/admin/v1/policy_data/get_latest.rs +++ b/crates/handlers/src/admin/v1/policy_data/get_latest.rs @@ -5,6 +5,7 @@ use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use crate::{ admin::{ @@ -30,11 +31,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -55,7 +57,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.policy_data.get_latest", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.policy_data.get_latest", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, ) -> Result>, RouteError> { diff --git a/crates/handlers/src/admin/v1/policy_data/set.rs b/crates/handlers/src/admin/v1/policy_data/set.rs index b857b488b..bc28e96e3 100644 --- a/crates/handlers/src/admin/v1/policy_data/set.rs +++ b/crates/handlers/src/admin/v1/policy_data/set.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use aide::{NoApi, OperationIo, transform::TransformOperation}; use axum::{Json, extract::State, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_policy::PolicyFactory; use mas_storage::BoxRng; use schemars::JsonSchema; @@ -36,11 +37,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { RouteError::InvalidPolicyData(_) => StatusCode::BAD_REQUEST, RouteError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -79,7 +81,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.policy_data.set", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.policy_data.set", skip_all)] pub async fn handler( CallContext { mut repo, clock, .. diff --git a/crates/handlers/src/admin/v1/upstream_oauth_links/add.rs b/crates/handlers/src/admin/v1/upstream_oauth_links/add.rs index a9a7c461f..3cdbc783f 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_links/add.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_links/add.rs @@ -6,6 +6,7 @@ use aide::{NoApi, OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_storage::BoxRng; use schemars::JsonSchema; use serde::Deserialize; @@ -41,12 +42,13 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::LinkAlreadyExists(_, _) => StatusCode::CONFLICT, Self::UserNotFound(_) | Self::ProviderNotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -102,7 +104,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.upstream_oauth_links.post", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.upstream_oauth_links.post", skip_all)] pub async fn handler( CallContext { mut repo, clock, .. diff --git a/crates/handlers/src/admin/v1/upstream_oauth_links/delete.rs b/crates/handlers/src/admin/v1/upstream_oauth_links/delete.rs index 51540de0f..0c270c64b 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_links/delete.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_links/delete.rs @@ -6,6 +6,7 @@ use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use ulid::Ulid; use crate::{ @@ -28,11 +29,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -49,7 +51,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.upstream_oauth_links.delete", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.upstream_oauth_links.delete", skip_all)] pub async fn handler( CallContext { mut repo, clock, .. diff --git a/crates/handlers/src/admin/v1/upstream_oauth_links/get.rs b/crates/handlers/src/admin/v1/upstream_oauth_links/get.rs index eecbb3e4e..4fd5158d3 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_links/get.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_links/get.rs @@ -6,6 +6,7 @@ use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use ulid::Ulid; use crate::{ @@ -33,11 +34,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_entry_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_entry_id, Json(error)).into_response() } } @@ -59,7 +61,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.upstream_oauth_links.get", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.upstream_oauth_links.get", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, id: UlidPathParam, diff --git a/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs b/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs index e20a975be..e46b85820 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs @@ -11,6 +11,7 @@ use axum::{ }; use axum_macros::FromRequestParts; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_storage::{Page, upstream_oauth2::UpstreamOAuthLinkFilter}; use schemars::JsonSchema; use serde::Deserialize; @@ -91,12 +92,13 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::UserNotFound(_) | Self::ProviderNotFound(_) => StatusCode::NOT_FOUND, Self::InvalidFilter(_) => StatusCode::BAD_REQUEST, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -130,7 +132,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.upstream_oauth_links.list", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.upstream_oauth_links.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, Pagination(pagination): Pagination, diff --git a/crates/handlers/src/admin/v1/user_emails/add.rs b/crates/handlers/src/admin/v1/user_emails/add.rs index 466d372be..f3a39e20a 100644 --- a/crates/handlers/src/admin/v1/user_emails/add.rs +++ b/crates/handlers/src/admin/v1/user_emails/add.rs @@ -8,6 +8,7 @@ use std::str::FromStr as _; use aide::{NoApi, OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_storage::{ BoxRng, queue::{ProvisionUserJob, QueueJobRepositoryExt as _}, @@ -52,13 +53,14 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::EmailAlreadyInUse(_) => StatusCode::CONFLICT, Self::EmailNotValid { .. } => StatusCode::BAD_REQUEST, Self::UserNotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -106,7 +108,7 @@ Note that this endpoint ignores any policy which would normally prevent the emai }) } -#[tracing::instrument(name = "handler.admin.v1.user_emails.add", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.user_emails.add", skip_all)] pub async fn handler( CallContext { mut repo, clock, .. diff --git a/crates/handlers/src/admin/v1/user_emails/delete.rs b/crates/handlers/src/admin/v1/user_emails/delete.rs index 65e000111..ad7df7acb 100644 --- a/crates/handlers/src/admin/v1/user_emails/delete.rs +++ b/crates/handlers/src/admin/v1/user_emails/delete.rs @@ -6,6 +6,7 @@ use aide::{NoApi, OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_storage::{ BoxRng, queue::{ProvisionUserJob, QueueJobRepositoryExt as _}, @@ -32,11 +33,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -52,7 +54,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.user_emails.delete", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.user_emails.delete", skip_all)] pub async fn handler( CallContext { mut repo, clock, .. diff --git a/crates/handlers/src/admin/v1/user_emails/get.rs b/crates/handlers/src/admin/v1/user_emails/get.rs index b5ddfc02a..9232b0663 100644 --- a/crates/handlers/src/admin/v1/user_emails/get.rs +++ b/crates/handlers/src/admin/v1/user_emails/get.rs @@ -6,6 +6,7 @@ use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use ulid::Ulid; use crate::{ @@ -33,11 +34,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -57,7 +59,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.user_emails.get", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.user_emails.get", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, id: UlidPathParam, diff --git a/crates/handlers/src/admin/v1/user_emails/list.rs b/crates/handlers/src/admin/v1/user_emails/list.rs index d7ffa9ce5..f7adb23da 100644 --- a/crates/handlers/src/admin/v1/user_emails/list.rs +++ b/crates/handlers/src/admin/v1/user_emails/list.rs @@ -11,6 +11,7 @@ use axum::{ }; use axum_macros::FromRequestParts; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_storage::{Page, user::UserEmailFilter}; use schemars::JsonSchema; use serde::Deserialize; @@ -78,12 +79,13 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::UserNotFound(_) => StatusCode::NOT_FOUND, Self::InvalidFilter(_) => StatusCode::BAD_REQUEST, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -116,7 +118,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.user_emails.list", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.user_emails.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, Pagination(pagination): Pagination, diff --git a/crates/handlers/src/admin/v1/user_sessions/get.rs b/crates/handlers/src/admin/v1/user_sessions/get.rs index 1396beb47..a59b10d0e 100644 --- a/crates/handlers/src/admin/v1/user_sessions/get.rs +++ b/crates/handlers/src/admin/v1/user_sessions/get.rs @@ -6,6 +6,7 @@ use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use ulid::Ulid; use crate::{ @@ -33,11 +34,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -58,7 +60,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.user_sessions.get", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.user_sessions.get", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, id: UlidPathParam, diff --git a/crates/handlers/src/admin/v1/user_sessions/list.rs b/crates/handlers/src/admin/v1/user_sessions/list.rs index 62b72cc50..a04bf057f 100644 --- a/crates/handlers/src/admin/v1/user_sessions/list.rs +++ b/crates/handlers/src/admin/v1/user_sessions/list.rs @@ -11,6 +11,7 @@ use axum::{ }; use axum_macros::FromRequestParts; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_storage::{pagination::Page, user::BrowserSessionFilter}; use schemars::JsonSchema; use serde::Deserialize; @@ -100,12 +101,13 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::UserNotFound(_) => StatusCode::NOT_FOUND, Self::InvalidFilter(_) => StatusCode::BAD_REQUEST, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -140,7 +142,7 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p }) } -#[tracing::instrument(name = "handler.admin.v1.user_sessions.list", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.user_sessions.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, Pagination(pagination): Pagination, diff --git a/crates/handlers/src/admin/v1/users/add.rs b/crates/handlers/src/admin/v1/users/add.rs index d6c83db51..9867b06ec 100644 --- a/crates/handlers/src/admin/v1/users/add.rs +++ b/crates/handlers/src/admin/v1/users/add.rs @@ -9,6 +9,7 @@ use std::sync::Arc; use aide::{NoApi, OperationIo, transform::TransformOperation}; use axum::{Json, extract::State, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_matrix::HomeserverConnection; use mas_storage::{ BoxRng, @@ -81,12 +82,13 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_) | Self::Homeserver(_)); let status = match self { Self::Internal(_) | Self::Homeserver(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::UsernameNotValid => StatusCode::BAD_REQUEST, Self::UserAlreadyExists | Self::UsernameReserved => StatusCode::CONFLICT, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -131,7 +133,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.users.add", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.users.add", skip_all)] pub async fn handler( CallContext { mut repo, clock, .. diff --git a/crates/handlers/src/admin/v1/users/by_username.rs b/crates/handlers/src/admin/v1/users/by_username.rs index 66fc6ff51..98ddb8b3c 100644 --- a/crates/handlers/src/admin/v1/users/by_username.rs +++ b/crates/handlers/src/admin/v1/users/by_username.rs @@ -7,6 +7,7 @@ use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, extract::Path, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use schemars::JsonSchema; use serde::Deserialize; @@ -34,11 +35,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -65,7 +67,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.users.by_username", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.users.by_username", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, Path(UsernamePathParam { username }): Path, diff --git a/crates/handlers/src/admin/v1/users/deactivate.rs b/crates/handlers/src/admin/v1/users/deactivate.rs index 60adad850..fad2f5257 100644 --- a/crates/handlers/src/admin/v1/users/deactivate.rs +++ b/crates/handlers/src/admin/v1/users/deactivate.rs @@ -7,6 +7,7 @@ use aide::{NoApi, OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_storage::{ BoxRng, queue::{DeactivateUserJob, QueueJobRepositoryExt as _}, @@ -39,11 +40,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -67,7 +69,7 @@ This invalidates any existing session, and will ask the homeserver to make them }) } -#[tracing::instrument(name = "handler.admin.v1.users.deactivate", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.users.deactivate", skip_all)] pub async fn handler( CallContext { mut repo, clock, .. diff --git a/crates/handlers/src/admin/v1/users/get.rs b/crates/handlers/src/admin/v1/users/get.rs index 71763b292..59d221fbb 100644 --- a/crates/handlers/src/admin/v1/users/get.rs +++ b/crates/handlers/src/admin/v1/users/get.rs @@ -7,6 +7,7 @@ use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use ulid::Ulid; use crate::{ @@ -34,11 +35,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -58,7 +60,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.users.get", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.users.get", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, id: UlidPathParam, diff --git a/crates/handlers/src/admin/v1/users/list.rs b/crates/handlers/src/admin/v1/users/list.rs index 628b56747..17bd82ede 100644 --- a/crates/handlers/src/admin/v1/users/list.rs +++ b/crates/handlers/src/admin/v1/users/list.rs @@ -12,6 +12,7 @@ use axum::{ }; use axum_macros::FromRequestParts; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_storage::{Page, user::UserFilter}; use schemars::JsonSchema; use serde::Deserialize; @@ -95,11 +96,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::InvalidFilter(_) => StatusCode::BAD_REQUEST, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -122,7 +124,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.users.list", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.users.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, Pagination(pagination): Pagination, diff --git a/crates/handlers/src/admin/v1/users/lock.rs b/crates/handlers/src/admin/v1/users/lock.rs index 99ae7c5a4..13ffdc071 100644 --- a/crates/handlers/src/admin/v1/users/lock.rs +++ b/crates/handlers/src/admin/v1/users/lock.rs @@ -7,6 +7,7 @@ use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use ulid::Ulid; use crate::{ @@ -34,11 +35,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -62,7 +64,7 @@ This DOES NOT invalidate any existing session, meaning that all their existing s }) } -#[tracing::instrument(name = "handler.admin.v1.users.lock", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.users.lock", skip_all)] pub async fn handler( CallContext { mut repo, clock, .. diff --git a/crates/handlers/src/admin/v1/users/set_admin.rs b/crates/handlers/src/admin/v1/users/set_admin.rs index fae62d5c0..72df1f71b 100644 --- a/crates/handlers/src/admin/v1/users/set_admin.rs +++ b/crates/handlers/src/admin/v1/users/set_admin.rs @@ -7,6 +7,7 @@ use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use schemars::JsonSchema; use serde::Deserialize; use ulid::Ulid; @@ -36,11 +37,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -71,7 +73,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.users.set_admin", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.users.set_admin", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, id: UlidPathParam, diff --git a/crates/handlers/src/admin/v1/users/set_password.rs b/crates/handlers/src/admin/v1/users/set_password.rs index 4a0e6d034..2d83e39d9 100644 --- a/crates/handlers/src/admin/v1/users/set_password.rs +++ b/crates/handlers/src/admin/v1/users/set_password.rs @@ -7,6 +7,7 @@ use aide::{NoApi, OperationIo, transform::TransformOperation}; use axum::{Json, extract::State, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_storage::BoxRng; use schemars::JsonSchema; use serde::Deserialize; @@ -43,13 +44,14 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_) | Self::Password(_)); let status = match self { Self::Internal(_) | Self::Password(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::PasswordAuthDisabled => StatusCode::FORBIDDEN, Self::PasswordTooWeak => StatusCode::BAD_REQUEST, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -90,7 +92,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.users.set_password", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.users.set_password", skip_all)] pub async fn handler( CallContext { mut repo, clock, .. diff --git a/crates/handlers/src/admin/v1/users/unlock.rs b/crates/handlers/src/admin/v1/users/unlock.rs index 76bb738c3..e1811378c 100644 --- a/crates/handlers/src/admin/v1/users/unlock.rs +++ b/crates/handlers/src/admin/v1/users/unlock.rs @@ -9,6 +9,7 @@ use std::sync::Arc; use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, extract::State, response::IntoResponse}; use hyper::StatusCode; +use mas_axum_utils::record_error; use mas_matrix::HomeserverConnection; use ulid::Ulid; @@ -40,11 +41,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_) | Self::Homeserver(_)); let status = match self { Self::Internal(_) | Self::Homeserver(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; - (status, Json(error)).into_response() + (status, sentry_event_id, Json(error)).into_response() } } @@ -66,7 +68,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }) } -#[tracing::instrument(name = "handler.admin.v1.users.unlock", skip_all, err)] +#[tracing::instrument(name = "handler.admin.v1.users.unlock", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, State(homeserver): State>, From feb3f6c0a841e54a3228b85dcff40fdb53dc8614 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 13:59:56 +0200 Subject: [PATCH 017/189] handlers::compat: don't rely on #[instrument(err)] for logging errors --- crates/handlers/src/compat/login.rs | 9 ++-- .../handlers/src/compat/login_sso_complete.rs | 2 - .../handlers/src/compat/login_sso_redirect.rs | 17 +++--- crates/handlers/src/compat/logout.rs | 8 +-- crates/handlers/src/compat/mod.rs | 6 +-- crates/handlers/src/compat/refresh.rs | 53 ++++++++++--------- 6 files changed, 49 insertions(+), 46 deletions(-) diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index 9c9312855..d13b50d3c 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -10,7 +10,7 @@ use axum::{Json, extract::State, response::IntoResponse}; use axum_extra::typed_header::TypedHeader; use chrono::Duration; use hyper::StatusCode; -use mas_axum_utils::sentry::SentryEventID; +use mas_axum_utils::record_error; use mas_data_model::{ CompatSession, CompatSsoLoginState, Device, SiteConfig, TokenType, User, UserAgent, }; @@ -210,7 +210,8 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = + record_error!(self, Self::Internal(_) | Self::ProvisionDeviceFailed(_)); LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); let response = match self { Self::Internal(_) | Self::ProvisionDeviceFailed(_) => MatrixError { @@ -257,11 +258,11 @@ impl IntoResponse for RouteError { }, }; - (SentryEventID::from(event_id), response).into_response() + (sentry_event_id, response).into_response() } } -#[tracing::instrument(name = "handlers.compat.login.post", skip_all, err)] +#[tracing::instrument(name = "handlers.compat.login.post", skip_all)] pub(crate) async fn post( mut rng: BoxRng, clock: BoxClock, diff --git a/crates/handlers/src/compat/login_sso_complete.rs b/crates/handlers/src/compat/login_sso_complete.rs index 8da507d70..1a5d1504c 100644 --- a/crates/handlers/src/compat/login_sso_complete.rs +++ b/crates/handlers/src/compat/login_sso_complete.rs @@ -48,7 +48,6 @@ pub struct Params { name = "handlers.compat.login_sso_complete.get", fields(compat_sso_login.id = %id), skip_all, - err, )] pub async fn get( PreferredLanguage(locale): PreferredLanguage, @@ -121,7 +120,6 @@ pub async fn get( name = "handlers.compat.login_sso_complete.post", fields(compat_sso_login.id = %id), skip_all, - err, )] pub async fn post( mut rng: BoxRng, diff --git a/crates/handlers/src/compat/login_sso_redirect.rs b/crates/handlers/src/compat/login_sso_redirect.rs index 36b065b39..583f24e9b 100644 --- a/crates/handlers/src/compat/login_sso_redirect.rs +++ b/crates/handlers/src/compat/login_sso_redirect.rs @@ -9,7 +9,7 @@ use axum::{ response::IntoResponse, }; use hyper::StatusCode; -use mas_axum_utils::sentry::SentryEventID; +use mas_axum_utils::record_error; use mas_router::{CompatLoginSsoAction, CompatLoginSsoComplete, UrlBuilder}; use mas_storage::{BoxClock, BoxRepository, BoxRng, compat::CompatSsoLoginRepository}; use rand::distributions::{Alphanumeric, DistString}; @@ -43,17 +43,16 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); - ( - StatusCode::INTERNAL_SERVER_ERROR, - SentryEventID::from(event_id), - format!("{self}"), - ) - .into_response() + let sentry_event_id = record_error!(self, Self::Internal(_)); + let status_code = match &self { + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::MissingRedirectUrl | Self::InvalidRedirectUrl => StatusCode::BAD_REQUEST, + }; + (status_code, sentry_event_id, format!("{self}")).into_response() } } -#[tracing::instrument(name = "handlers.compat.login_sso_redirect.get", skip_all, err)] +#[tracing::instrument(name = "handlers.compat.login_sso_redirect.get", skip_all)] pub async fn get( mut rng: BoxRng, clock: BoxClock, diff --git a/crates/handlers/src/compat/logout.rs b/crates/handlers/src/compat/logout.rs index 557dbaef9..7b2ea7d52 100644 --- a/crates/handlers/src/compat/logout.rs +++ b/crates/handlers/src/compat/logout.rs @@ -10,7 +10,7 @@ use axum::{Json, response::IntoResponse}; use axum_extra::typed_header::TypedHeader; use headers::{Authorization, authorization::Bearer}; use hyper::StatusCode; -use mas_axum_utils::sentry::SentryEventID; +use mas_axum_utils::record_error; use mas_data_model::TokenType; use mas_storage::{ BoxClock, BoxRepository, BoxRng, Clock, RepositoryAccess, @@ -51,7 +51,7 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); LOGOUT_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); let response = match self { Self::Internal(_) => MatrixError { @@ -71,11 +71,11 @@ impl IntoResponse for RouteError { }, }; - (SentryEventID::from(event_id), response).into_response() + (sentry_event_id, response).into_response() } } -#[tracing::instrument(name = "handlers.compat.logout.post", skip_all, err)] +#[tracing::instrument(name = "handlers.compat.logout.post", skip_all)] pub(crate) async fn post( clock: BoxClock, mut rng: BoxRng, diff --git a/crates/handlers/src/compat/mod.rs b/crates/handlers/src/compat/mod.rs index fcb45d68c..304499471 100644 --- a/crates/handlers/src/compat/mod.rs +++ b/crates/handlers/src/compat/mod.rs @@ -14,7 +14,7 @@ use axum::{ response::IntoResponse, }; use hyper::{StatusCode, header}; -use mas_axum_utils::sentry::SentryEventID; +use mas_axum_utils::record_error; use serde::{Serialize, de::DeserializeOwned}; use thiserror::Error; @@ -59,7 +59,7 @@ pub enum MatrixJsonBodyRejection { impl IntoResponse for MatrixJsonBodyRejection { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = record_error!(self, !); let response = match self { Self::InvalidContentType | Self::ContentTypeNotJson(_) => MatrixError { errcode: "M_NOT_JSON", @@ -102,7 +102,7 @@ impl IntoResponse for MatrixJsonBodyRejection { }, }; - (SentryEventID::from(event_id), response).into_response() + (sentry_event_id, response).into_response() } } diff --git a/crates/handlers/src/compat/refresh.rs b/crates/handlers/src/compat/refresh.rs index 9e3675910..7511e0e99 100644 --- a/crates/handlers/src/compat/refresh.rs +++ b/crates/handlers/src/compat/refresh.rs @@ -7,7 +7,7 @@ use axum::{Json, extract::State, response::IntoResponse}; use chrono::Duration; use hyper::StatusCode; -use mas_axum_utils::sentry::SentryEventID; +use mas_axum_utils::record_error; use mas_data_model::{SiteConfig, TokenFormatError, TokenType}; use mas_storage::{ BoxClock, BoxRepository, BoxRng, Clock, @@ -16,6 +16,7 @@ use mas_storage::{ use serde::{Deserialize, Serialize}; use serde_with::{DurationMilliSeconds, serde_as}; use thiserror::Error; +use ulid::Ulid; use super::MatrixError; use crate::{BoundActivityTracker, impl_from_error_for_route}; @@ -31,46 +32,50 @@ pub enum RouteError { Internal(Box), #[error("invalid token")] - InvalidToken, + InvalidToken(#[from] TokenFormatError), - #[error("refresh token already consumed")] - RefreshTokenConsumed, + #[error("unknown token")] + UnknownToken, - #[error("invalid session")] - InvalidSession, + #[error("invalid token type {0}, expected a compat refresh token")] + InvalidTokenType(TokenType), - #[error("unknown session")] - UnknownSession, + #[error("refresh token already consumed {0}")] + RefreshTokenConsumed(Ulid), + + #[error("invalid compat session {0}")] + InvalidSession(Ulid), + + #[error("unknown comapt session {0}")] + UnknownSession(Ulid), } impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_) | Self::UnknownSession(_)); let response = match self { - Self::Internal(_) | Self::UnknownSession => MatrixError { + Self::Internal(_) | Self::UnknownSession(_) => MatrixError { errcode: "M_UNKNOWN", error: "Internal error", status: StatusCode::INTERNAL_SERVER_ERROR, }, - Self::InvalidToken | Self::InvalidSession | Self::RefreshTokenConsumed => MatrixError { + Self::InvalidToken(_) + | Self::UnknownToken + | Self::InvalidTokenType(_) + | Self::InvalidSession(_) + | Self::RefreshTokenConsumed(_) => MatrixError { errcode: "M_UNKNOWN_TOKEN", error: "Invalid refresh token", status: StatusCode::UNAUTHORIZED, }, }; - (SentryEventID::from(event_id), response).into_response() + (sentry_event_id, response).into_response() } } impl_from_error_for_route!(mas_storage::RepositoryError); -impl From for RouteError { - fn from(_e: TokenFormatError) -> Self { - Self::InvalidToken - } -} - #[serde_as] #[derive(Debug, Serialize)] pub struct ResponseBody { @@ -80,7 +85,7 @@ pub struct ResponseBody { expires_in_ms: Duration, } -#[tracing::instrument(name = "handlers.compat.refresh.post", skip_all, err)] +#[tracing::instrument(name = "handlers.compat.refresh.post", skip_all)] pub(crate) async fn post( mut rng: BoxRng, clock: BoxClock, @@ -92,27 +97,27 @@ pub(crate) async fn post( let token_type = TokenType::check(&input.refresh_token)?; if token_type != TokenType::CompatRefreshToken { - return Err(RouteError::InvalidToken); + return Err(RouteError::InvalidTokenType(token_type)); } let refresh_token = repo .compat_refresh_token() .find_by_token(&input.refresh_token) .await? - .ok_or(RouteError::InvalidToken)?; + .ok_or(RouteError::UnknownToken)?; if !refresh_token.is_valid() { - return Err(RouteError::RefreshTokenConsumed); + return Err(RouteError::RefreshTokenConsumed(refresh_token.id)); } let session = repo .compat_session() .lookup(refresh_token.session_id) .await? - .ok_or(RouteError::UnknownSession)?; + .ok_or(RouteError::UnknownSession(refresh_token.session_id))?; if !session.is_valid() { - return Err(RouteError::InvalidSession); + return Err(RouteError::InvalidSession(refresh_token.session_id)); } activity_tracker From fb35d0e1b70efcceb9312d2e063e478df5e57b8e Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 14:47:01 +0200 Subject: [PATCH 018/189] handlers::oauth2::token: Way better error logging on the token endpoint --- crates/handlers/src/oauth2/token.rs | 160 +++++++++++++++++++--------- 1 file changed, 109 insertions(+), 51 deletions(-) diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index d361bcc99..54be27c7e 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -13,7 +13,7 @@ use headers::{CacheControl, HeaderMap, HeaderMapExt, Pragma}; use hyper::StatusCode; use mas_axum_utils::{ client_authorization::{ClientAuthorization, CredentialsVerificationError}, - sentry::SentryEventID, + record_error, }; use mas_data_model::{ AuthorizationGrantStage, Client, Device, DeviceCodeGrantState, SiteConfig, TokenType, UserAgent, @@ -42,7 +42,7 @@ use oauth2_types::{ }; use opentelemetry::{Key, KeyValue, metrics::Counter}; use thiserror::Error; -use tracing::{debug, info}; +use tracing::{debug, info, warn}; use ulid::Ulid; use super::{generate_id_token, generate_token_pair}; @@ -72,17 +72,28 @@ pub(crate) enum RouteError { #[error("client not found")] ClientNotFound, - #[error("client not allowed")] - ClientNotAllowed, + #[error("client not allowed to use the token endpoint: {0}")] + ClientNotAllowed(Ulid), - #[error("could not verify client credentials")] - ClientCredentialsVerification(#[from] CredentialsVerificationError), + #[error("invalid client credentials for client {client_id}")] + InvalidClientCredentials { + client_id: Ulid, + #[source] + source: CredentialsVerificationError, + }, + + #[error("could not verify client credentials for client {client_id}")] + ClientCredentialsVerification { + client_id: Ulid, + #[source] + source: CredentialsVerificationError, + }, #[error("grant not found")] GrantNotFound, - #[error("invalid grant")] - InvalidGrant, + #[error("invalid grant {0}")] + InvalidGrant(Ulid), #[error("refresh token not found")] RefreshTokenNotFound, @@ -96,20 +107,23 @@ pub(crate) enum RouteError { #[error("client id mismatch: expected {expected}, got {actual}")] ClientIDMismatch { expected: Ulid, actual: Ulid }, - #[error("policy denied the request")] - DeniedByPolicy(Vec), + #[error("policy denied the request: {0}")] + DeniedByPolicy(mas_policy::EvaluationResult), #[error("unsupported grant type")] UnsupportedGrantType, - #[error("unauthorized client")] - UnauthorizedClient, + #[error("client {0} is not authorized to use this grant type")] + UnauthorizedClient(Ulid), - #[error("failed to load browser session")] - NoSuchBrowserSession, + #[error("unexpected client {was} (expected {expected})")] + UnexptectedClient { was: Ulid, expected: Ulid }, - #[error("failed to load oauth session")] - NoSuchOAuthSession, + #[error("failed to load browser session {0}")] + NoSuchBrowserSession(Ulid), + + #[error("failed to load oauth session {0}")] + NoSuchOAuthSession(Ulid), #[error( "failed to load the next refresh token ({next:?}) from the previous one ({previous:?})" @@ -145,14 +159,25 @@ pub(crate) enum RouteError { impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = record_error!( + self, + Self::Internal(_) + | Self::ClientCredentialsVerification { .. } + | Self::NoSuchBrowserSession(_) + | Self::NoSuchOAuthSession(_) + | Self::ProvisionDeviceFailed(_) + | Self::NoSuchNextRefreshToken { .. } + | Self::NoSuchNextAccessToken { .. } + | Self::NoAccessTokenOnRefreshToken { .. } + ); TOKEN_REQUEST_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); let response = match self { Self::Internal(_) - | Self::NoSuchBrowserSession - | Self::NoSuchOAuthSession + | Self::ClientCredentialsVerification { .. } + | Self::NoSuchBrowserSession(_) + | Self::NoSuchOAuthSession(_) | Self::ProvisionDeviceFailed(_) | Self::NoSuchNextRefreshToken { .. } | Self::NoSuchNextAccessToken { .. } @@ -160,10 +185,12 @@ impl IntoResponse for RouteError { StatusCode::INTERNAL_SERVER_ERROR, Json(ClientError::from(ClientErrorCode::ServerError)), ), + Self::BadRequest => ( StatusCode::BAD_REQUEST, Json(ClientError::from(ClientErrorCode::InvalidRequest)), ), + Self::PkceVerification(err) => ( StatusCode::BAD_REQUEST, Json( @@ -171,19 +198,25 @@ impl IntoResponse for RouteError { .with_description(format!("PKCE verification failed: {err}")), ), ), - Self::ClientNotFound | Self::ClientCredentialsVerification(_) => ( + + Self::ClientNotFound | Self::InvalidClientCredentials { .. } => ( StatusCode::UNAUTHORIZED, Json(ClientError::from(ClientErrorCode::InvalidClient)), ), - Self::ClientNotAllowed | Self::UnauthorizedClient => ( + + Self::ClientNotAllowed(_) + | Self::UnauthorizedClient(_) + | Self::UnexptectedClient { .. } => ( StatusCode::UNAUTHORIZED, Json(ClientError::from(ClientErrorCode::UnauthorizedClient)), ), - Self::DeniedByPolicy(violations) => ( + + Self::DeniedByPolicy(evaluation) => ( StatusCode::FORBIDDEN, Json( ClientError::from(ClientErrorCode::InvalidScope).with_description( - violations + evaluation + .violations .into_iter() .map(|violation| violation.msg) .collect::>() @@ -191,19 +224,23 @@ impl IntoResponse for RouteError { ), ), ), + Self::DeviceCodeRejected => ( StatusCode::FORBIDDEN, Json(ClientError::from(ClientErrorCode::AccessDenied)), ), + Self::DeviceCodeExpired => ( StatusCode::FORBIDDEN, Json(ClientError::from(ClientErrorCode::ExpiredToken)), ), + Self::DeviceCodePending => ( StatusCode::FORBIDDEN, Json(ClientError::from(ClientErrorCode::AuthorizationPending)), ), - Self::InvalidGrant + + Self::InvalidGrant(_) | Self::DeviceCodeExchanged | Self::RefreshTokenNotFound | Self::RefreshTokenInvalid(_) @@ -213,13 +250,14 @@ impl IntoResponse for RouteError { StatusCode::BAD_REQUEST, Json(ClientError::from(ClientErrorCode::InvalidGrant)), ), + Self::UnsupportedGrantType => ( StatusCode::BAD_REQUEST, Json(ClientError::from(ClientErrorCode::UnsupportedGrantType)), ), }; - (SentryEventID::from(event_id), response).into_response() + (sentry_event_id, response).into_response() } } @@ -231,7 +269,6 @@ impl_from_error_for_route!(super::IdTokenSignatureError); name = "handlers.oauth2.token.post", fields(client.id = client_authorization.client_id()), skip_all, - err, )] pub(crate) async fn post( mut rng: BoxRng, @@ -258,12 +295,27 @@ pub(crate) async fn post( let method = client .token_endpoint_auth_method .as_ref() - .ok_or(RouteError::ClientNotAllowed)?; + .ok_or(RouteError::ClientNotAllowed(client.id))?; client_authorization .credentials .verify(&http_client, &encrypter, method, &client) - .await?; + .await + .map_err(|err| { + // Classify the error differntly, depending on whether it's an 'internal' error, + // or just because the client presented invalid credentials. + if err.is_internal() { + RouteError::ClientCredentialsVerification { + client_id: client.id, + source: err, + } + } else { + RouteError::InvalidClientCredentials { + client_id: client.id, + source: err, + } + } + })?; let form = client_authorization.form.ok_or(RouteError::BadRequest)?; @@ -367,7 +419,7 @@ async fn authorization_code_grant( ) -> Result<(AccessTokenResponse, BoxRepository), RouteError> { // Check that the client is allowed to use this grant type if !client.grant_types.contains(&GrantType::AuthorizationCode) { - return Err(RouteError::UnauthorizedClient); + return Err(RouteError::UnauthorizedClient(client.id)); } let authz_grant = repo @@ -381,40 +433,40 @@ async fn authorization_code_grant( let session_id = match authz_grant.stage { AuthorizationGrantStage::Cancelled { cancelled_at } => { debug!(%cancelled_at, "Authorization grant was cancelled"); - return Err(RouteError::InvalidGrant); + return Err(RouteError::InvalidGrant(authz_grant.id)); } AuthorizationGrantStage::Exchanged { exchanged_at, fulfilled_at, session_id, } => { - debug!(%exchanged_at, %fulfilled_at, "Authorization code was already exchanged"); + warn!(%exchanged_at, %fulfilled_at, "Authorization code was already exchanged"); // Ending the session if the token was already exchanged more than 20s ago if now - exchanged_at > Duration::microseconds(20 * 1000 * 1000) { - debug!("Ending potentially compromised session"); + warn!("Ending potentially compromised session"); let session = repo .oauth2_session() .lookup(session_id) .await? - .ok_or(RouteError::NoSuchOAuthSession)?; + .ok_or(RouteError::NoSuchOAuthSession(session_id))?; repo.oauth2_session().finish(clock, session).await?; repo.save().await?; } - return Err(RouteError::InvalidGrant); + return Err(RouteError::InvalidGrant(authz_grant.id)); } AuthorizationGrantStage::Pending => { - debug!("Authorization grant has not been fulfilled yet"); - return Err(RouteError::InvalidGrant); + warn!("Authorization grant has not been fulfilled yet"); + return Err(RouteError::InvalidGrant(authz_grant.id)); } AuthorizationGrantStage::Fulfilled { session_id, fulfilled_at, } => { if now - fulfilled_at > Duration::microseconds(10 * 60 * 1000 * 1000) { - debug!("Code exchange took more than 10 minutes"); - return Err(RouteError::InvalidGrant); + warn!("Code exchange took more than 10 minutes"); + return Err(RouteError::InvalidGrant(authz_grant.id)); } session_id @@ -425,7 +477,7 @@ async fn authorization_code_grant( .oauth2_session() .lookup(session_id) .await? - .ok_or(RouteError::NoSuchOAuthSession)?; + .ok_or(RouteError::NoSuchOAuthSession(session_id))?; if let Some(user_agent) = user_agent { session = repo @@ -435,10 +487,16 @@ async fn authorization_code_grant( } // This should never happen, since we looked up in the database using the code - let code = authz_grant.code.as_ref().ok_or(RouteError::InvalidGrant)?; + let code = authz_grant + .code + .as_ref() + .ok_or(RouteError::InvalidGrant(authz_grant.id))?; if client.id != session.client_id { - return Err(RouteError::UnauthorizedClient); + return Err(RouteError::UnexptectedClient { + was: client.id, + expected: session.client_id, + }); } match (code.pkce.as_ref(), grant.code_verifier.as_ref()) { @@ -453,14 +511,14 @@ async fn authorization_code_grant( let Some(user_session_id) = session.user_session_id else { tracing::warn!("No user session associated with this OAuth2 session"); - return Err(RouteError::InvalidGrant); + return Err(RouteError::InvalidGrant(authz_grant.id)); }; let browser_session = repo .browser_session() .lookup(user_session_id) .await? - .ok_or(RouteError::NoSuchBrowserSession)?; + .ok_or(RouteError::NoSuchBrowserSession(user_session_id))?; let last_authentication = repo .browser_session() @@ -539,7 +597,7 @@ async fn refresh_token_grant( ) -> Result<(AccessTokenResponse, BoxRepository), RouteError> { // Check that the client is allowed to use this grant type if !client.grant_types.contains(&GrantType::RefreshToken) { - return Err(RouteError::UnauthorizedClient); + return Err(RouteError::UnauthorizedClient(client.id)); } let refresh_token = repo @@ -552,7 +610,7 @@ async fn refresh_token_grant( .oauth2_session() .lookup(refresh_token.session_id) .await? - .ok_or(RouteError::NoSuchOAuthSession)?; + .ok_or(RouteError::NoSuchOAuthSession(refresh_token.session_id))?; // Let's for now record the user agent on each refresh, that should be // responsive enough and not too much of a burden on the database. @@ -692,7 +750,7 @@ async fn client_credentials_grant( ) -> Result<(AccessTokenResponse, BoxRepository), RouteError> { // Check that the client is allowed to use this grant type if !client.grant_types.contains(&GrantType::ClientCredentials) { - return Err(RouteError::UnauthorizedClient); + return Err(RouteError::UnauthorizedClient(client.id)); } // Default to an empty scope if none is provided @@ -715,7 +773,7 @@ async fn client_credentials_grant( }) .await?; if !res.valid() { - return Err(RouteError::DeniedByPolicy(res.violations)); + return Err(RouteError::DeniedByPolicy(res)); } // Start the session @@ -771,7 +829,7 @@ async fn device_code_grant( ) -> Result<(AccessTokenResponse, BoxRepository), RouteError> { // Check that the client is allowed to use this grant type if !client.grant_types.contains(&GrantType::DeviceCode) { - return Err(RouteError::UnauthorizedClient); + return Err(RouteError::UnauthorizedClient(client.id)); } let grant = repo @@ -804,14 +862,14 @@ async fn device_code_grant( } DeviceCodeGrantState::Fulfilled { browser_session_id, .. - } => browser_session_id, + } => *browser_session_id, }; let browser_session = repo .browser_session() - .lookup(*browser_session_id) + .lookup(browser_session_id) .await? - .ok_or(RouteError::NoSuchBrowserSession)?; + .ok_or(RouteError::NoSuchBrowserSession(browser_session_id))?; // Start the session let mut session = repo From e65bc17e5085848e9e28e1dec5650fc6b7b29a25 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 14:49:24 +0200 Subject: [PATCH 019/189] Better logging of client cretentials verification errors --- crates/axum-utils/src/client_authorization.rs | 80 +++++++++++++------ 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/crates/axum-utils/src/client_authorization.rs b/crates/axum-utils/src/client_authorization.rs index b3886c7b9..19d6c9e7a 100644 --- a/crates/axum-utils/src/client_authorization.rs +++ b/crates/axum-utils/src/client_authorization.rs @@ -28,6 +28,8 @@ use serde::{Deserialize, de::DeserializeOwned}; use serde_json::Value; use thiserror::Error; +use crate::record_error; + static JWT_BEARER_CLIENT_ASSERTION: &str = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; #[derive(Deserialize)] @@ -97,7 +99,7 @@ impl Credentials { /// # Errors /// /// Returns an error if the credentials are invalid. - #[tracing::instrument(skip_all, err)] + #[tracing::instrument(skip_all)] pub async fn verify( &self, http_client: &reqwest::Client, @@ -144,7 +146,7 @@ impl Credentials { let jwks = fetch_jwks(http_client, jwks) .await - .map_err(|_| CredentialsVerificationError::JwksFetchFailed)?; + .map_err(CredentialsVerificationError::JwksFetchFailed)?; jwt.verify_with_jwks(&jwks) .map_err(|_| CredentialsVerificationError::InvalidAssertionSignature)?; @@ -214,7 +216,18 @@ pub enum CredentialsVerificationError { InvalidAssertionSignature, #[error("failed to fetch jwks")] - JwksFetchFailed, + JwksFetchFailed(#[source] Box), +} + +impl CredentialsVerificationError { + /// Returns true if the error is an internal error, not caused by the client + #[must_use] + pub fn is_internal(&self) -> bool { + matches!( + self, + Self::DecryptionError | Self::InvalidClientConfig | Self::JwksFetchFailed(_) + ) + } } #[derive(Debug, PartialEq, Eq)] @@ -231,23 +244,40 @@ impl ClientAuthorization { } } -#[derive(Debug)] +#[derive(Debug, Error)] pub enum ClientAuthorizationError { + #[error("Invalid Authorization header")] InvalidHeader, - BadForm(FailedToDeserializeForm), + + #[error("Could not deserialize request body")] + BadForm(#[source] FailedToDeserializeForm), + + #[error("client_id in form ({form:?}) does not match credential ({credential:?})")] ClientIdMismatch { credential: String, form: String }, + + #[error("Unsupported client_assertion_type: {client_assertion_type}")] UnsupportedClientAssertion { client_assertion_type: String }, + + #[error("No credentials were presented")] MissingCredentials, + + #[error("Invalid request")] InvalidRequest, + + #[error("Invalid client_assertion")] InvalidAssertion, + + #[error(transparent)] Internal(Box), } impl IntoResponse for ClientAuthorizationError { fn into_response(self) -> axum::response::Response { - match self { + let sentry_event_id = record_error!(self, Self::Internal(_)); + match &self { ClientAuthorizationError::InvalidHeader => ( StatusCode::BAD_REQUEST, + sentry_event_id, Json(ClientError::new( ClientErrorCode::InvalidRequest, "Invalid Authorization header", @@ -256,39 +286,34 @@ impl IntoResponse for ClientAuthorizationError { ClientAuthorizationError::BadForm(err) => ( StatusCode::BAD_REQUEST, + sentry_event_id, Json( ClientError::from(ClientErrorCode::InvalidRequest) .with_description(format!("{err}")), ), ), - ClientAuthorizationError::ClientIdMismatch { form, credential } => { - let description = format!( - "client_id in form ({form:?}) does not match credential ({credential:?})" - ); - - ( - StatusCode::BAD_REQUEST, - Json( - ClientError::from(ClientErrorCode::InvalidGrant) - .with_description(description), - ), - ) - } - - ClientAuthorizationError::UnsupportedClientAssertion { - client_assertion_type, - } => ( + ClientAuthorizationError::ClientIdMismatch { .. } => ( StatusCode::BAD_REQUEST, + sentry_event_id, Json( - ClientError::from(ClientErrorCode::InvalidRequest).with_description(format!( - "Unsupported client_assertion_type: {client_assertion_type}", - )), + ClientError::from(ClientErrorCode::InvalidGrant) + .with_description(format!("{self}")), + ), + ), + + ClientAuthorizationError::UnsupportedClientAssertion { .. } => ( + StatusCode::BAD_REQUEST, + sentry_event_id, + Json( + ClientError::from(ClientErrorCode::InvalidRequest) + .with_description(format!("{self}")), ), ), ClientAuthorizationError::MissingCredentials => ( StatusCode::BAD_REQUEST, + sentry_event_id, Json(ClientError::new( ClientErrorCode::InvalidRequest, "No credentials were presented", @@ -297,11 +322,13 @@ impl IntoResponse for ClientAuthorizationError { ClientAuthorizationError::InvalidRequest => ( StatusCode::BAD_REQUEST, + sentry_event_id, Json(ClientError::from(ClientErrorCode::InvalidRequest)), ), ClientAuthorizationError::InvalidAssertion => ( StatusCode::BAD_REQUEST, + sentry_event_id, Json(ClientError::new( ClientErrorCode::InvalidRequest, "Invalid client_assertion", @@ -310,6 +337,7 @@ impl IntoResponse for ClientAuthorizationError { ClientAuthorizationError::Internal(e) => ( StatusCode::INTERNAL_SERVER_ERROR, + sentry_event_id, Json( ClientError::from(ClientErrorCode::ServerError) .with_description(format!("{e}")), From a1c147c43a16a55ec9098a95ad0f6a623303d13d Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 15:22:11 +0200 Subject: [PATCH 020/189] Better errors for the introspection endpoint --- crates/handlers/src/oauth2/introspection.rs | 91 +++++++++++---------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index a97c62ab6..c196ac37e 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -10,7 +10,7 @@ use axum::{Json, extract::State, http::HeaderValue, response::IntoResponse}; use hyper::{HeaderMap, StatusCode}; use mas_axum_utils::{ client_authorization::{ClientAuthorization, CredentialsVerificationError}, - sentry::SentryEventID, + record_error, }; use mas_data_model::{Device, TokenFormatError, TokenType}; use mas_iana::oauth::{OAuthClientAuthenticationMethod, OAuthTokenTypeHint}; @@ -28,6 +28,7 @@ use oauth2_types::{ }; use opentelemetry::{Key, KeyValue, metrics::Counter}; use thiserror::Error; +use ulid::Ulid; use crate::{ActivityTracker, METER, impl_from_error_for_route}; @@ -53,8 +54,8 @@ pub enum RouteError { ClientNotFound, /// The client is not allowed to introspect. - #[error("client is not allowed to introspect")] - NotAllowed, + #[error("client {0} is not allowed to introspect")] + NotAllowed(Ulid), /// The token type is not the one expected. #[error("unexpected token type")] @@ -73,30 +74,30 @@ pub enum RouteError { InvalidToken(TokenType), /// The OAuth session is not valid. - #[error("invalid oauth session")] - InvalidOAuthSession, + #[error("invalid oauth session {0}")] + InvalidOAuthSession(Ulid), /// The OAuth session could not be found in the database. - #[error("unknown oauth session")] - CantLoadOAuthSession, + #[error("unknown oauth session {0}")] + CantLoadOAuthSession(Ulid), /// The compat session is not valid. - #[error("invalid compat session")] - InvalidCompatSession, + #[error("invalid compat session {0}")] + InvalidCompatSession(Ulid), /// The compat session could not be found in the database. - #[error("unknown compat session")] - CantLoadCompatSession, + #[error("unknown compat session {0}")] + CantLoadCompatSession(Ulid), /// The Device ID in the compat session can't be encoded as a scope #[error("device ID contains characters that are not allowed in a scope")] CantEncodeDeviceID(#[from] mas_data_model::ToScopeTokenError), - #[error("invalid user")] - InvalidUser, + #[error("invalid user {0}")] + InvalidUser(Ulid), - #[error("unknown user")] - CantLoadUser, + #[error("unknown user {0}")] + CantLoadUser(Ulid), #[error("bad request")] BadRequest, @@ -107,12 +108,19 @@ pub enum RouteError { impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = record_error!( + self, + Self::Internal(_) + | Self::CantLoadCompatSession(_) + | Self::CantLoadOAuthSession(_) + | Self::CantLoadUser(_) + ); + let response = match self { e @ (Self::Internal(_) - | Self::CantLoadCompatSession - | Self::CantLoadOAuthSession - | Self::CantLoadUser) => ( + | Self::CantLoadCompatSession(_) + | Self::CantLoadOAuthSession(_) + | Self::CantLoadUser(_)) => ( StatusCode::INTERNAL_SERVER_ERROR, Json( ClientError::from(ClientErrorCode::ServerError).with_description(e.to_string()), @@ -136,9 +144,9 @@ impl IntoResponse for RouteError { Self::UnknownToken(_) | Self::UnexpectedTokenType | Self::InvalidToken(_) - | Self::InvalidUser - | Self::InvalidCompatSession - | Self::InvalidOAuthSession + | Self::InvalidUser(_) + | Self::InvalidCompatSession(_) + | Self::InvalidOAuthSession(_) | Self::InvalidTokenFormat(_) | Self::CantEncodeDeviceID(_) => { INTROSPECTION_COUNTER.add(1, &[KeyValue::new(ACTIVE.clone(), false)]); @@ -146,11 +154,12 @@ impl IntoResponse for RouteError { Json(INACTIVE).into_response() } - Self::NotAllowed => ( + Self::NotAllowed(_) => ( StatusCode::UNAUTHORIZED, Json(ClientError::from(ClientErrorCode::AccessDenied)), ) .into_response(), + Self::BadRequest => ( StatusCode::BAD_REQUEST, Json(ClientError::from(ClientErrorCode::InvalidRequest)), @@ -158,7 +167,7 @@ impl IntoResponse for RouteError { .into_response(), }; - (SentryEventID::from(event_id), response).into_response() + (sentry_event_id, response).into_response() } } @@ -208,7 +217,7 @@ pub(crate) async fn post( let method = match &client.token_endpoint_auth_method { None | Some(OAuthClientAuthenticationMethod::None) => { - return Err(RouteError::NotAllowed); + return Err(RouteError::NotAllowed(client.id)); } Some(c) => c, }; @@ -259,10 +268,10 @@ pub(crate) async fn post( .oauth2_session() .lookup(access_token.session_id) .await? - .ok_or(RouteError::InvalidOAuthSession)?; + .ok_or(RouteError::CantLoadOAuthSession(access_token.session_id))?; if !session.is_valid() { - return Err(RouteError::InvalidOAuthSession); + return Err(RouteError::InvalidOAuthSession(session.id)); } // If this is the first time we're using this token, mark it as used @@ -280,10 +289,10 @@ pub(crate) async fn post( .user() .lookup(user_id) .await? - .ok_or(RouteError::CantLoadUser)?; + .ok_or(RouteError::CantLoadUser(user_id))?; if !user.is_valid() { - return Err(RouteError::InvalidUser); + return Err(RouteError::InvalidUser(user.id)); } (Some(user.sub), Some(user.username)) @@ -338,10 +347,10 @@ pub(crate) async fn post( .oauth2_session() .lookup(refresh_token.session_id) .await? - .ok_or(RouteError::CantLoadOAuthSession)?; + .ok_or(RouteError::CantLoadOAuthSession(refresh_token.session_id))?; if !session.is_valid() { - return Err(RouteError::InvalidOAuthSession); + return Err(RouteError::InvalidOAuthSession(session.id)); } // The session might not have a user on it (for Client Credentials grants for @@ -351,10 +360,10 @@ pub(crate) async fn post( .user() .lookup(user_id) .await? - .ok_or(RouteError::CantLoadUser)?; + .ok_or(RouteError::CantLoadUser(user_id))?; if !user.is_valid() { - return Err(RouteError::InvalidUser); + return Err(RouteError::InvalidUser(user.id)); } (Some(user.sub), Some(user.username)) @@ -407,20 +416,20 @@ pub(crate) async fn post( .compat_session() .lookup(access_token.session_id) .await? - .ok_or(RouteError::CantLoadCompatSession)?; + .ok_or(RouteError::CantLoadCompatSession(access_token.session_id))?; if !session.is_valid() { - return Err(RouteError::InvalidCompatSession); + return Err(RouteError::InvalidCompatSession(session.id)); } let user = repo .user() .lookup(session.user_id) .await? - .ok_or(RouteError::CantLoadUser)?; + .ok_or(RouteError::CantLoadUser(session.user_id))?; if !user.is_valid() { - return Err(RouteError::InvalidUser)?; + return Err(RouteError::InvalidUser(user.id))?; } // Grant the synapse admin scope if the session has the admin flag set. @@ -491,20 +500,20 @@ pub(crate) async fn post( .compat_session() .lookup(refresh_token.session_id) .await? - .ok_or(RouteError::CantLoadCompatSession)?; + .ok_or(RouteError::CantLoadCompatSession(refresh_token.session_id))?; if !session.is_valid() { - return Err(RouteError::InvalidCompatSession); + return Err(RouteError::InvalidCompatSession(session.id)); } let user = repo .user() .lookup(session.user_id) .await? - .ok_or(RouteError::CantLoadUser)?; + .ok_or(RouteError::CantLoadUser(session.user_id))?; if !user.is_valid() { - return Err(RouteError::InvalidUser)?; + return Err(RouteError::InvalidUser(user.id))?; } // Grant the synapse admin scope if the session has the admin flag set. From 451fd3e2fa5f158380f69c7197fa5a8020ed66e0 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 15:28:44 +0200 Subject: [PATCH 021/189] Make the FancyError type log the error when being transformed into a response. --- crates/axum-utils/src/fancy_error.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/axum-utils/src/fancy_error.rs b/crates/axum-utils/src/fancy_error.rs index 50a573bee..19b760ff5 100644 --- a/crates/axum-utils/src/fancy_error.rs +++ b/crates/axum-utils/src/fancy_error.rs @@ -54,12 +54,13 @@ impl From for FancyError { impl IntoResponse for FancyError { fn into_response(self) -> Response { + tracing::error!(message = %self.context); let error = format!("{}", self.context); - let event_id = sentry::capture_message(&error, sentry::Level::Error); + let event_id = SentryEventID::for_last_event(); ( StatusCode::INTERNAL_SERVER_ERROR, TypedHeader(ContentType::text()), - SentryEventID::from(event_id), + event_id, Extension(self.context), error, ) From d70877a1c97bd30ce4ce46b4d42b4197ed8fbe54 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 16:02:57 +0200 Subject: [PATCH 022/189] Fix Sentry creating transactions for every request --- crates/cli/src/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cli/src/server.rs b/crates/cli/src/server.rs index 45fa40e6b..969a93e23 100644 --- a/crates/cli/src/server.rs +++ b/crates/cli/src/server.rs @@ -322,8 +322,8 @@ pub fn build_router( .layer(mas_context::LogContextLayer::new(|req| { otel_http_method(req).into() })) - .layer(SentryHttpLayer::new()) .layer(NewSentryLayer::new_from_top()) + .layer(SentryHttpLayer::with_transaction()) .with_state(state) } From ef28aa2250045ba9c8ef774964efc184a1998a5f Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 16:05:00 +0200 Subject: [PATCH 023/189] handlers::oauth2: don't rely on #[instrument(err)] for error logging --- .../src/oauth2/authorization/consent.rs | 26 +++++----- .../handlers/src/oauth2/authorization/mod.rs | 9 ++-- .../handlers/src/oauth2/device/authorize.rs | 52 ++++++++++++++----- crates/handlers/src/oauth2/device/consent.rs | 2 + crates/handlers/src/oauth2/device/link.rs | 2 +- crates/handlers/src/oauth2/introspection.rs | 1 - crates/handlers/src/oauth2/registration.rs | 27 ++++++---- crates/handlers/src/oauth2/revoke.rs | 42 +++++++++++---- crates/handlers/src/oauth2/token.rs | 5 +- crates/handlers/src/oauth2/userinfo.rs | 32 ++++++++---- 10 files changed, 133 insertions(+), 65 deletions(-) diff --git a/crates/handlers/src/oauth2/authorization/consent.rs b/crates/handlers/src/oauth2/authorization/consent.rs index 273542383..14dfd0e7f 100644 --- a/crates/handlers/src/oauth2/authorization/consent.rs +++ b/crates/handlers/src/oauth2/authorization/consent.rs @@ -13,7 +13,7 @@ use hyper::StatusCode; use mas_axum_utils::{ cookies::CookieJar, csrf::{CsrfExt, ProtectedForm}, - sentry::SentryEventID, + record_error, }; use mas_data_model::AuthorizationGrantStage; use mas_keystore::Keystore; @@ -46,11 +46,11 @@ pub enum RouteError { #[error("Authorization grant not found")] GrantNotFound, - #[error("Authorization grant already used")] - GrantNotPending, + #[error("Authorization grant {0} already used")] + GrantNotPending(Ulid), - #[error("Failed to load client")] - NoSuchClient, + #[error("Failed to load client {0}")] + NoSuchClient(Ulid), } impl_from_error_for_route!(mas_templates::TemplateError); @@ -64,10 +64,10 @@ impl_from_error_for_route!(super::callback::CallbackDestinationError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_) | Self::NoSuchClient(_)); ( StatusCode::INTERNAL_SERVER_ERROR, - SentryEventID::from(event_id), + sentry_event_id, self.to_string(), ) .into_response() @@ -78,7 +78,6 @@ impl IntoResponse for RouteError { name = "handlers.oauth2.authorization.consent.get", fields(grant.id = %grant_id), skip_all, - err, )] pub(crate) async fn get( mut rng: BoxRng, @@ -118,10 +117,10 @@ pub(crate) async fn get( .oauth2_client() .lookup(grant.client_id) .await? - .ok_or(RouteError::NoSuchClient)?; + .ok_or(RouteError::NoSuchClient(grant.client_id))?; if !matches!(grant.stage, AuthorizationGrantStage::Pending) { - return Err(RouteError::GrantNotPending); + return Err(RouteError::GrantNotPending(grant.id)); } let Some(session) = maybe_session else { @@ -172,7 +171,6 @@ pub(crate) async fn get( name = "handlers.oauth2.authorization.consent.post", fields(grant.id = %grant_id), skip_all, - err, )] pub(crate) async fn post( mut rng: BoxRng, @@ -229,7 +227,11 @@ pub(crate) async fn post( .oauth2_client() .lookup(grant.client_id) .await? - .ok_or(RouteError::NoSuchClient)?; + .ok_or(RouteError::NoSuchClient(grant.client_id))?; + + if !matches!(grant.stage, AuthorizationGrantStage::Pending) { + return Err(RouteError::GrantNotPending(grant.id)); + } let res = policy .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput { diff --git a/crates/handlers/src/oauth2/authorization/mod.rs b/crates/handlers/src/oauth2/authorization/mod.rs index 7e131f812..cde23f636 100644 --- a/crates/handlers/src/oauth2/authorization/mod.rs +++ b/crates/handlers/src/oauth2/authorization/mod.rs @@ -9,7 +9,7 @@ use axum::{ response::{IntoResponse, Response}, }; use hyper::StatusCode; -use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, sentry::SentryEventID}; +use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, record_error}; use mas_data_model::{AuthorizationCode, Pkce}; use mas_router::{PostAuthAction, UrlBuilder}; use mas_storage::{ @@ -53,7 +53,7 @@ pub enum RouteError { impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); // TODO: better error pages let response = match self { RouteError::Internal(e) => { @@ -75,7 +75,7 @@ impl IntoResponse for RouteError { .into_response(), }; - (SentryEventID::from(event_id), response).into_response() + (sentry_event_id, response).into_response() } } @@ -122,7 +122,6 @@ fn resolve_response_mode( name = "handlers.oauth2.authorization.get", fields(client.id = %params.auth.client_id), skip_all, - err, )] #[allow(clippy::too_many_lines)] pub(crate) async fn get( @@ -319,7 +318,7 @@ pub(crate) async fn get( let response = match res { Ok(r) => r, Err(err) => { - tracing::error!(%err); + tracing::error!(message = &err as &dyn std::error::Error); callback_destination.go( &templates, &locale, diff --git a/crates/handlers/src/oauth2/device/authorize.rs b/crates/handlers/src/oauth2/device/authorize.rs index e38bf17fd..dcf0316d4 100644 --- a/crates/handlers/src/oauth2/device/authorize.rs +++ b/crates/handlers/src/oauth2/device/authorize.rs @@ -11,7 +11,7 @@ use headers::{CacheControl, Pragma}; use hyper::StatusCode; use mas_axum_utils::{ client_authorization::{ClientAuthorization, CredentialsVerificationError}, - sentry::SentryEventID, + record_error, }; use mas_data_model::UserAgent; use mas_keystore::Encrypter; @@ -24,6 +24,7 @@ use oauth2_types::{ }; use rand::distributions::{Alphanumeric, DistString}; use thiserror::Error; +use ulid::Ulid; use crate::{BoundActivityTracker, impl_from_error_for_route}; @@ -35,35 +36,46 @@ pub(crate) enum RouteError { #[error("client not found")] ClientNotFound, - #[error("client not allowed")] - ClientNotAllowed, + #[error("client {0} is not allowed to use the device code grant")] + ClientNotAllowed(Ulid), - #[error("could not verify client credentials")] - ClientCredentialsVerification(#[from] CredentialsVerificationError), + #[error("invalid client credentials for client {client_id}")] + InvalidClientCredentials { + client_id: Ulid, + #[source] + source: CredentialsVerificationError, + }, + + #[error("could not verify client credentials for client {client_id}")] + ClientCredentialsVerification { + client_id: Ulid, + #[source] + source: CredentialsVerificationError, + }, } impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let response = match self { - Self::Internal(_) => ( + Self::Internal(_) | Self::ClientCredentialsVerification { .. } => ( StatusCode::INTERNAL_SERVER_ERROR, Json(ClientError::from(ClientErrorCode::ServerError)), ), - Self::ClientNotFound | Self::ClientCredentialsVerification(_) => ( + Self::ClientNotFound | Self::InvalidClientCredentials { .. } => ( StatusCode::UNAUTHORIZED, Json(ClientError::from(ClientErrorCode::InvalidClient)), ), - Self::ClientNotAllowed => ( + Self::ClientNotAllowed(_) => ( StatusCode::UNAUTHORIZED, Json(ClientError::from(ClientErrorCode::UnauthorizedClient)), ), }; - (SentryEventID::from(event_id), response).into_response() + (sentry_event_id, response).into_response() } } @@ -71,7 +83,6 @@ impl IntoResponse for RouteError { name = "handlers.oauth2.device.request.post", fields(client.id = client_authorization.client_id()), skip_all, - err, )] pub(crate) async fn post( mut rng: BoxRng, @@ -94,15 +105,28 @@ pub(crate) async fn post( let method = client .token_endpoint_auth_method .as_ref() - .ok_or(RouteError::ClientNotAllowed)?; + .ok_or(RouteError::ClientNotAllowed(client.id))?; client_authorization .credentials .verify(&http_client, &encrypter, method, &client) - .await?; + .await + .map_err(|err| { + if err.is_internal() { + RouteError::ClientCredentialsVerification { + client_id: client.id, + source: err, + } + } else { + RouteError::InvalidClientCredentials { + client_id: client.id, + source: err, + } + } + })?; if !client.grant_types.contains(&GrantType::DeviceCode) { - return Err(RouteError::ClientNotAllowed); + return Err(RouteError::ClientNotAllowed(client.id)); } let scope = client_authorization diff --git a/crates/handlers/src/oauth2/device/consent.rs b/crates/handlers/src/oauth2/device/consent.rs index 3f46c7a38..b84a9c9d9 100644 --- a/crates/handlers/src/oauth2/device/consent.rs +++ b/crates/handlers/src/oauth2/device/consent.rs @@ -41,6 +41,7 @@ pub(crate) struct ConsentForm { action: Action, } +#[tracing::instrument(name = "handlers.oauth2.device.consent.get", skip_all)] pub(crate) async fn get( mut rng: BoxRng, clock: BoxClock, @@ -136,6 +137,7 @@ pub(crate) async fn get( Ok((cookie_jar, Html(rendered)).into_response()) } +#[tracing::instrument(name = "handlers.oauth2.device.consent.post", skip_all)] pub(crate) async fn post( mut rng: BoxRng, clock: BoxClock, diff --git a/crates/handlers/src/oauth2/device/link.rs b/crates/handlers/src/oauth2/device/link.rs index 3e734cf1c..f123563ec 100644 --- a/crates/handlers/src/oauth2/device/link.rs +++ b/crates/handlers/src/oauth2/device/link.rs @@ -24,7 +24,7 @@ pub struct Params { code: Option, } -#[tracing::instrument(name = "handlers.oauth2.device.link.get", skip_all, err)] +#[tracing::instrument(name = "handlers.oauth2.device.link.get", skip_all)] pub(crate) async fn get( clock: BoxClock, mut repo: BoxRepository, diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index c196ac37e..9b3e50e7c 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -197,7 +197,6 @@ const SYNAPSE_ADMIN_SCOPE: ScopeToken = ScopeToken::from_static("urn:synapse:adm name = "handlers.oauth2.introspection.post", fields(client.id = client_authorization.client_id()), skip_all, - err, )] #[allow(clippy::too_many_lines)] pub(crate) async fn post( diff --git a/crates/handlers/src/oauth2/registration.rs b/crates/handlers/src/oauth2/registration.rs index 0b1f3515d..f3f91d754 100644 --- a/crates/handlers/src/oauth2/registration.rs +++ b/crates/handlers/src/oauth2/registration.rs @@ -9,10 +9,10 @@ use std::sync::LazyLock; use axum::{Json, extract::State, response::IntoResponse}; use axum_extra::TypedHeader; use hyper::StatusCode; -use mas_axum_utils::sentry::SentryEventID; +use mas_axum_utils::record_error; use mas_iana::oauth::OAuthClientAuthenticationMethod; use mas_keystore::Encrypter; -use mas_policy::{Policy, Violation}; +use mas_policy::{EvaluationResult, Policy}; use mas_storage::{BoxClock, BoxRepository, BoxRng, oauth2::OAuth2ClientRepository}; use oauth2_types::{ errors::{ClientError, ClientErrorCode}, @@ -55,8 +55,8 @@ pub(crate) enum RouteError { #[error("{0} is a public suffix, not a valid domain")] UrlIsPublicSuffix(&'static str), - #[error("denied by the policy: {0:?}")] - PolicyDenied(Vec), + #[error("client registration denied by the policy: {0}")] + PolicyDenied(EvaluationResult), } impl_from_error_for_route!(mas_storage::RepositoryError); @@ -67,7 +67,7 @@ impl_from_error_for_route!(serde_json::Error); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); REGISTRATION_COUNTER.add(1, &[KeyValue::new(RESULT, "denied")]); @@ -143,15 +143,20 @@ impl IntoResponse for RouteError { // For policy violations, we return an `invalid_client_metadata` error with the details // of the violations in most cases. If a violation includes `redirect_uri` in the // message, we return an `invalid_redirect_uri` error instead. - Self::PolicyDenied(violations) => { + Self::PolicyDenied(evaluation) => { // TODO: detect them better - let code = if violations.iter().any(|v| v.msg.contains("redirect_uri")) { + let code = if evaluation + .violations + .iter() + .any(|v| v.msg.contains("redirect_uri")) + { ClientErrorCode::InvalidRedirectUri } else { ClientErrorCode::InvalidClientMetadata }; - let collected = &violations + let collected = &evaluation + .violations .iter() .map(|v| v.msg.clone()) .collect::>(); @@ -165,7 +170,7 @@ impl IntoResponse for RouteError { } }; - (SentryEventID::from(event_id), response).into_response() + (sentry_event_id, response).into_response() } } @@ -207,7 +212,7 @@ fn localised_url_has_public_suffix(url: &Localized) -> bool { url.iter().any(|(_lang, url)| host_is_public_suffix(url)) } -#[tracing::instrument(name = "handlers.oauth2.registration.post", skip_all, err)] +#[tracing::instrument(name = "handlers.oauth2.registration.post", skip_all)] pub(crate) async fn post( mut rng: BoxRng, clock: BoxClock, @@ -282,7 +287,7 @@ pub(crate) async fn post( }) .await?; if !res.valid() { - return Err(RouteError::PolicyDenied(res.violations)); + return Err(RouteError::PolicyDenied(res)); } let (client_secret, encrypted_client_secret) = match metadata.token_endpoint_auth_method { diff --git a/crates/handlers/src/oauth2/revoke.rs b/crates/handlers/src/oauth2/revoke.rs index bedd8fbf7..758f0d647 100644 --- a/crates/handlers/src/oauth2/revoke.rs +++ b/crates/handlers/src/oauth2/revoke.rs @@ -8,7 +8,7 @@ use axum::{Json, extract::State, response::IntoResponse}; use hyper::StatusCode; use mas_axum_utils::{ client_authorization::{ClientAuthorization, CredentialsVerificationError}, - sentry::SentryEventID, + record_error, }; use mas_data_model::TokenType; use mas_iana::oauth::OAuthTokenTypeHint; @@ -22,6 +22,7 @@ use oauth2_types::{ requests::RevocationRequest, }; use thiserror::Error; +use ulid::Ulid; use crate::{BoundActivityTracker, impl_from_error_for_route}; @@ -39,8 +40,19 @@ pub(crate) enum RouteError { #[error("client not allowed")] ClientNotAllowed, - #[error("could not verify client credentials")] - ClientCredentialsVerification(#[from] CredentialsVerificationError), + #[error("invalid client credentials for client {client_id}")] + InvalidClientCredentials { + client_id: Ulid, + #[source] + source: CredentialsVerificationError, + }, + + #[error("could not verify client credentials for client {client_id}")] + ClientCredentialsVerification { + client_id: Ulid, + #[source] + source: CredentialsVerificationError, + }, #[error("client is unauthorized")] UnauthorizedClient, @@ -54,9 +66,9 @@ pub(crate) enum RouteError { impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let response = match self { - Self::Internal(_) => ( + Self::Internal(_) | Self::ClientCredentialsVerification { .. } => ( StatusCode::INTERNAL_SERVER_ERROR, Json(ClientError::from(ClientErrorCode::ServerError)), ) @@ -68,7 +80,7 @@ impl IntoResponse for RouteError { ) .into_response(), - Self::ClientNotFound | Self::ClientCredentialsVerification(_) => ( + Self::ClientNotFound | Self::InvalidClientCredentials { .. } => ( StatusCode::UNAUTHORIZED, Json(ClientError::from(ClientErrorCode::InvalidClient)), ) @@ -90,7 +102,7 @@ impl IntoResponse for RouteError { Self::UnknownToken => StatusCode::OK.into_response(), }; - (SentryEventID::from(event_id), response).into_response() + (sentry_event_id, response).into_response() } } @@ -106,7 +118,6 @@ impl From for RouteError { name = "handlers.oauth2.revoke.post", fields(client.id = client_authorization.client_id()), skip_all, - err, )] pub(crate) async fn post( clock: BoxClock, @@ -131,7 +142,20 @@ pub(crate) async fn post( client_authorization .credentials .verify(&http_client, &encrypter, method, &client) - .await?; + .await + .map_err(|err| { + if err.is_internal() { + RouteError::ClientCredentialsVerification { + client_id: client.id, + source: err, + } + } else { + RouteError::InvalidClientCredentials { + client_id: client.id, + source: err, + } + } + })?; let Some(form) = client_authorization.form else { return Err(RouteError::BadRequest); diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index 54be27c7e..d66d7d2f4 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -444,14 +444,17 @@ async fn authorization_code_grant( // Ending the session if the token was already exchanged more than 20s ago if now - exchanged_at > Duration::microseconds(20 * 1000 * 1000) { - warn!("Ending potentially compromised session"); + warn!(oauth_session.id = %session_id, "Ending potentially compromised session"); let session = repo .oauth2_session() .lookup(session_id) .await? .ok_or(RouteError::NoSuchOAuthSession(session_id))?; + + //if !session.is_finished() { repo.oauth2_session().finish(clock, session).await?; repo.save().await?; + //} } return Err(RouteError::InvalidGrant(authz_grant.id)); diff --git a/crates/handlers/src/oauth2/userinfo.rs b/crates/handlers/src/oauth2/userinfo.rs index 052892b33..064196e3d 100644 --- a/crates/handlers/src/oauth2/userinfo.rs +++ b/crates/handlers/src/oauth2/userinfo.rs @@ -12,7 +12,7 @@ use axum::{ use hyper::StatusCode; use mas_axum_utils::{ jwt::JwtResponse, - sentry::SentryEventID, + record_error, user_authorization::{AuthorizationVerificationError, UserAuthorization}, }; use mas_jose::{ @@ -25,6 +25,7 @@ use mas_storage::{BoxClock, BoxRepository, BoxRng, oauth2::OAuth2ClientRepositor use serde::Serialize; use serde_with::skip_serializing_none; use thiserror::Error; +use ulid::Ulid; use crate::{BoundActivityTracker, impl_from_error_for_route}; @@ -59,11 +60,11 @@ pub enum RouteError { #[error("no suitable key found for signing")] InvalidSigningKey, - #[error("failed to load client")] - NoSuchClient, + #[error("failed to load client {0}")] + NoSuchClient(Ulid), - #[error("failed to load user")] - NoSuchUser, + #[error("failed to load user {0}")] + NoSuchUser(Ulid), } impl_from_error_for_route!(mas_storage::RepositoryError); @@ -72,9 +73,18 @@ impl_from_error_for_route!(mas_jose::jwt::JwtSignatureError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = record_error!( + self, + Self::Internal(_) + | Self::InvalidSigningKey + | Self::NoSuchClient(_) + | Self::NoSuchUser(_) + ); let response = match self { - Self::Internal(_) | Self::InvalidSigningKey | Self::NoSuchClient | Self::NoSuchUser => { + Self::Internal(_) + | Self::InvalidSigningKey + | Self::NoSuchClient(_) + | Self::NoSuchUser(_) => { (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() } Self::AuthorizationVerificationError(_) | Self::Unauthorized => { @@ -82,11 +92,11 @@ impl IntoResponse for RouteError { } }; - (SentryEventID::from(event_id), response).into_response() + (sentry_event_id, response).into_response() } } -#[tracing::instrument(name = "handlers.oauth2.userinfo.get", skip_all, err)] +#[tracing::instrument(name = "handlers.oauth2.userinfo.get", skip_all)] pub async fn get( mut rng: BoxRng, clock: BoxClock, @@ -116,7 +126,7 @@ pub async fn get( .user() .lookup(user_id) .await? - .ok_or(RouteError::NoSuchUser)?; + .ok_or(RouteError::NoSuchUser(user_id))?; let user_info = UserInfo { sub: user.sub.clone(), @@ -127,7 +137,7 @@ pub async fn get( .oauth2_client() .lookup(session.client_id) .await? - .ok_or(RouteError::NoSuchClient)?; + .ok_or(RouteError::NoSuchClient(session.client_id))?; repo.save().await?; From 38b3dc6ce636610b72cb8dd7398b8e5e6d329f1e Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 16:22:31 +0200 Subject: [PATCH 024/189] handlers::upstream_oauth2: don't rely on #[instrument(err)] to capture errors --- .../handlers/src/upstream_oauth2/authorize.rs | 7 ++- crates/handlers/src/upstream_oauth2/cache.rs | 6 +-- .../handlers/src/upstream_oauth2/callback.rs | 7 ++- crates/handlers/src/upstream_oauth2/link.rs | 53 +++++++++++-------- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/crates/handlers/src/upstream_oauth2/authorize.rs b/crates/handlers/src/upstream_oauth2/authorize.rs index ca41c236b..bf1d76182 100644 --- a/crates/handlers/src/upstream_oauth2/authorize.rs +++ b/crates/handlers/src/upstream_oauth2/authorize.rs @@ -9,7 +9,7 @@ use axum::{ response::{IntoResponse, Redirect}, }; use hyper::StatusCode; -use mas_axum_utils::{cookies::CookieJar, sentry::SentryEventID}; +use mas_axum_utils::{cookies::CookieJar, record_error}; use mas_data_model::UpstreamOAuthProvider; use mas_oidc_client::requests::authorization_code::AuthorizationRequestData; use mas_router::UrlBuilder; @@ -41,13 +41,13 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let response = match self { Self::ProviderNotFound => (StatusCode::NOT_FOUND, "Provider not found").into_response(), Self::Internal(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), }; - (SentryEventID::from(event_id), response).into_response() + (sentry_event_id, response).into_response() } } @@ -55,7 +55,6 @@ impl IntoResponse for RouteError { name = "handlers.upstream_oauth2.authorize.get", fields(upstream_oauth_provider.id = %provider_id), skip_all, - err, )] pub(crate) async fn get( mut rng: BoxRng, diff --git a/crates/handlers/src/upstream_oauth2/cache.rs b/crates/handlers/src/upstream_oauth2/cache.rs index 54253cac9..9b8d6360d 100644 --- a/crates/handlers/src/upstream_oauth2/cache.rs +++ b/crates/handlers/src/upstream_oauth2/cache.rs @@ -165,7 +165,7 @@ impl MetadataCache { /// /// This spawns a background task that will refresh the cache at the given /// interval. - #[tracing::instrument(name = "metadata_cache.warm_up_and_run", skip_all, err)] + #[tracing::instrument(name = "metadata_cache.warm_up_and_run", skip_all)] pub async fn warm_up_and_run( &self, client: &reqwest::Client, @@ -205,7 +205,7 @@ impl MetadataCache { })) } - #[tracing::instrument(name = "metadata_cache.fetch", fields(%issuer), skip_all, err)] + #[tracing::instrument(name = "metadata_cache.fetch", fields(%issuer), skip_all)] async fn fetch( &self, client: &reqwest::Client, @@ -237,7 +237,7 @@ impl MetadataCache { } /// Get the metadata for the given issuer. - #[tracing::instrument(name = "metadata_cache.get", fields(%issuer), skip_all, err)] + #[tracing::instrument(name = "metadata_cache.get", fields(%issuer), skip_all)] pub async fn get( &self, client: &reqwest::Client, diff --git a/crates/handlers/src/upstream_oauth2/callback.rs b/crates/handlers/src/upstream_oauth2/callback.rs index be4b5a2d1..f9b85adc8 100644 --- a/crates/handlers/src/upstream_oauth2/callback.rs +++ b/crates/handlers/src/upstream_oauth2/callback.rs @@ -13,7 +13,7 @@ use axum::{ response::{Html, IntoResponse, Response}, }; use hyper::StatusCode; -use mas_axum_utils::{cookies::CookieJar, sentry::SentryEventID}; +use mas_axum_utils::{cookies::CookieJar, record_error}; use mas_data_model::{UpstreamOAuthProvider, UpstreamOAuthProviderResponseMode}; use mas_jose::claims::TokenHash; use mas_keystore::{Encrypter, Keystore}; @@ -153,7 +153,7 @@ impl_from_error_for_route!(super::cookie::UpstreamSessionNotFound); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); let response = match self { Self::ProviderNotFound => (StatusCode::NOT_FOUND, "Provider not found").into_response(), Self::SessionNotFound => (StatusCode::NOT_FOUND, "Session not found").into_response(), @@ -161,7 +161,7 @@ impl IntoResponse for RouteError { e => (StatusCode::BAD_REQUEST, e.to_string()).into_response(), }; - (SentryEventID::from(event_id), response).into_response() + (sentry_event_id, response).into_response() } } @@ -169,7 +169,6 @@ impl IntoResponse for RouteError { name = "handlers.upstream_oauth2.callback.handler", fields(upstream_oauth_provider.id = %provider_id), skip_all, - err, )] #[allow(clippy::too_many_lines, clippy::too_many_arguments)] pub(crate) async fn handler( diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index cacba650a..b80f3956a 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -17,7 +17,7 @@ use mas_axum_utils::{ FancyError, SessionInfoExt, cookies::CookieJar, csrf::{CsrfExt, ProtectedForm}, - sentry::SentryEventID, + record_error, }; use mas_data_model::UserAgent; use mas_jose::jwt::Jwt; @@ -77,16 +77,16 @@ pub(crate) enum RouteError { LinkNotFound, /// Couldn't find the session on the link - #[error("Session not found")] - SessionNotFound, + #[error("Session {0} not found")] + SessionNotFound(Ulid), /// Couldn't find the user - #[error("User not found")] - UserNotFound, + #[error("User {0} not found")] + UserNotFound(Ulid), /// Couldn't find upstream provider - #[error("Upstream provider not found")] - ProviderNotFound, + #[error("Upstream provider {0} not found")] + ProviderNotFound(Ulid), /// Required attribute rendered to an empty string #[error("Template {template:?} rendered to an empty string")] @@ -104,8 +104,8 @@ pub(crate) enum RouteError { }, /// Session was already consumed - #[error("Session already consumed")] - SessionConsumed, + #[error("Session {0} already consumed")] + SessionConsumed(Ulid), #[error("Missing session cookie")] MissingCookie, @@ -129,14 +129,23 @@ impl_from_error_for_route!(mas_jose::jwt::JwtDecodeError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let event_id = sentry::capture_error(&self); + let sentry_event_id = record_error!( + self, + Self::Internal(_) + | Self::RequiredAttributeEmpty { .. } + | Self::RequiredAttributeRender { .. } + | Self::SessionNotFound(_) + | Self::ProviderNotFound(_) + | Self::UserNotFound(_) + | Self::HomeserverConnection(_) + ); let response = match self { Self::LinkNotFound => (StatusCode::NOT_FOUND, "Link not found").into_response(), Self::Internal(e) => FancyError::from(e).into_response(), e => FancyError::from(e).into_response(), }; - (SentryEventID::from(event_id), response).into_response() + (sentry_event_id, response).into_response() } } @@ -209,7 +218,6 @@ impl ToFormState for FormData { name = "handlers.upstream_oauth2.link.get", fields(upstream_oauth_link.id = %link_id), skip_all, - err, )] pub(crate) async fn get( mut rng: BoxRng, @@ -245,16 +253,16 @@ pub(crate) async fn get( .upstream_oauth_session() .lookup(session_id) .await? - .ok_or(RouteError::SessionNotFound)?; + .ok_or(RouteError::SessionNotFound(session_id))?; // This checks that we're in a browser session which is allowed to consume this // link: the upstream auth session should have been started in this browser. if upstream_session.link_id() != Some(link.id) { - return Err(RouteError::SessionNotFound); + return Err(RouteError::SessionNotFound(session_id)); } if upstream_session.is_consumed() { - return Err(RouteError::SessionConsumed); + return Err(RouteError::SessionConsumed(session_id)); } let (user_session_info, cookie_jar) = cookie_jar.session_info(); @@ -289,7 +297,7 @@ pub(crate) async fn get( .user() .lookup(user_id) .await? - .ok_or(RouteError::UserNotFound)?; + .ok_or(RouteError::UserNotFound(user_id))?; let ctx = UpstreamExistingLinkContext::new(user) .with_session(user_session) @@ -315,7 +323,7 @@ pub(crate) async fn get( .user() .lookup(user_id) .await? - .ok_or(RouteError::UserNotFound)?; + .ok_or(RouteError::UserNotFound(user_id))?; // Check that the user is not locked or deactivated if user.deactivated_at.is_some() { @@ -377,7 +385,7 @@ pub(crate) async fn get( .upstream_oauth_provider() .lookup(link.provider_id) .await? - .ok_or(RouteError::ProviderNotFound)?; + .ok_or(RouteError::ProviderNotFound(link.provider_id))?; let ctx = UpstreamRegister::new(link.clone(), provider.clone()); @@ -543,7 +551,6 @@ pub(crate) async fn get( name = "handlers.upstream_oauth2.link.post", fields(upstream_oauth_link.id = %link_id), skip_all, - err, )] pub(crate) async fn post( mut rng: BoxRng, @@ -583,16 +590,16 @@ pub(crate) async fn post( .upstream_oauth_session() .lookup(session_id) .await? - .ok_or(RouteError::SessionNotFound)?; + .ok_or(RouteError::SessionNotFound(session_id))?; // This checks that we're in a browser session which is allowed to consume this // link: the upstream auth session should have been started in this browser. if upstream_session.link_id() != Some(link.id) { - return Err(RouteError::SessionNotFound); + return Err(RouteError::SessionNotFound(session_id)); } if upstream_session.is_consumed() { - return Err(RouteError::SessionConsumed); + return Err(RouteError::SessionConsumed(session_id)); } let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); @@ -637,7 +644,7 @@ pub(crate) async fn post( .upstream_oauth_provider() .lookup(link.provider_id) .await? - .ok_or(RouteError::ProviderNotFound)?; + .ok_or(RouteError::ProviderNotFound(link.provider_id))?; // Let's try to import the claims from the ID token let env = environment(); From 28026e8001dcf8bdb72b45bfcecc4c8269b7b17d Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 16:24:27 +0200 Subject: [PATCH 025/189] handlers::views: don't rely on #[instrument(err)] to capture errors --- crates/handlers/src/views/app.rs | 4 ++-- crates/handlers/src/views/index.rs | 2 +- crates/handlers/src/views/login.rs | 4 ++-- crates/handlers/src/views/logout.rs | 2 +- crates/handlers/src/views/register/mod.rs | 2 +- crates/handlers/src/views/register/password.rs | 4 ++-- crates/handlers/src/views/register/steps/display_name.rs | 2 -- crates/handlers/src/views/register/steps/finish.rs | 1 - crates/handlers/src/views/register/steps/verify_email.rs | 2 -- 9 files changed, 9 insertions(+), 14 deletions(-) diff --git a/crates/handlers/src/views/app.rs b/crates/handlers/src/views/app.rs index d8010306f..b09fc4263 100644 --- a/crates/handlers/src/views/app.rs +++ b/crates/handlers/src/views/app.rs @@ -25,7 +25,7 @@ pub struct Params { action: Option, } -#[tracing::instrument(name = "handlers.views.app.get", skip_all, err)] +#[tracing::instrument(name = "handlers.views.app.get", skip_all)] pub async fn get( PreferredLanguage(locale): PreferredLanguage, State(templates): State, @@ -74,7 +74,7 @@ pub async fn get( /// Like `get`, but allow anonymous access. /// Used for a subset of the account management paths. /// Needed for e.g. account recovery. -#[tracing::instrument(name = "handlers.views.app.get_anonymous", skip_all, err)] +#[tracing::instrument(name = "handlers.views.app.get_anonymous", skip_all)] pub async fn get_anonymous( PreferredLanguage(locale): PreferredLanguage, State(templates): State, diff --git a/crates/handlers/src/views/index.rs b/crates/handlers/src/views/index.rs index 8774b8528..e2fb7c194 100644 --- a/crates/handlers/src/views/index.rs +++ b/crates/handlers/src/views/index.rs @@ -19,7 +19,7 @@ use crate::{ session::{SessionOrFallback, load_session_or_fallback}, }; -#[tracing::instrument(name = "handlers.views.index.get", skip_all, err)] +#[tracing::instrument(name = "handlers.views.index.get", skip_all)] pub async fn get( mut rng: BoxRng, clock: BoxClock, diff --git a/crates/handlers/src/views/login.rs b/crates/handlers/src/views/login.rs index 869e9a89d..d8798fcae 100644 --- a/crates/handlers/src/views/login.rs +++ b/crates/handlers/src/views/login.rs @@ -61,7 +61,7 @@ impl ToFormState for LoginForm { type Field = LoginFormField; } -#[tracing::instrument(name = "handlers.views.login.get", skip_all, err)] +#[tracing::instrument(name = "handlers.views.login.get", skip_all)] pub(crate) async fn get( mut rng: BoxRng, clock: BoxClock, @@ -127,7 +127,7 @@ pub(crate) async fn get( .await } -#[tracing::instrument(name = "handlers.views.login.post", skip_all, err)] +#[tracing::instrument(name = "handlers.views.login.post", skip_all)] pub(crate) async fn post( mut rng: BoxRng, clock: BoxClock, diff --git a/crates/handlers/src/views/logout.rs b/crates/handlers/src/views/logout.rs index 5f717a5cf..c315d7873 100644 --- a/crates/handlers/src/views/logout.rs +++ b/crates/handlers/src/views/logout.rs @@ -18,7 +18,7 @@ use mas_storage::{BoxClock, BoxRepository, user::BrowserSessionRepository}; use crate::BoundActivityTracker; -#[tracing::instrument(name = "handlers.views.logout.post", skip_all, err)] +#[tracing::instrument(name = "handlers.views.logout.post", skip_all)] pub(crate) async fn post( clock: BoxClock, mut repo: BoxRepository, diff --git a/crates/handlers/src/views/register/mod.rs b/crates/handlers/src/views/register/mod.rs index 9afe22474..050f5d7de 100644 --- a/crates/handlers/src/views/register/mod.rs +++ b/crates/handlers/src/views/register/mod.rs @@ -20,7 +20,7 @@ mod cookie; pub(crate) mod password; pub(crate) mod steps; -#[tracing::instrument(name = "handlers.views.register.get", skip_all, err)] +#[tracing::instrument(name = "handlers.views.register.get", skip_all)] pub(crate) async fn get( mut rng: BoxRng, clock: BoxClock, diff --git a/crates/handlers/src/views/register/password.rs b/crates/handlers/src/views/register/password.rs index ee8ed7bdb..4aa0d76dd 100644 --- a/crates/handlers/src/views/register/password.rs +++ b/crates/handlers/src/views/register/password.rs @@ -66,7 +66,7 @@ pub struct QueryParams { action: OptionalPostAuthAction, } -#[tracing::instrument(name = "handlers.views.password_register.get", skip_all, err)] +#[tracing::instrument(name = "handlers.views.password_register.get", skip_all)] pub(crate) async fn get( mut rng: BoxRng, clock: BoxClock, @@ -118,7 +118,7 @@ pub(crate) async fn get( Ok((cookie_jar, Html(content)).into_response()) } -#[tracing::instrument(name = "handlers.views.password_register.post", skip_all, err)] +#[tracing::instrument(name = "handlers.views.password_register.post", skip_all)] #[allow(clippy::too_many_lines, clippy::too_many_arguments)] pub(crate) async fn post( mut rng: BoxRng, diff --git a/crates/handlers/src/views/register/steps/display_name.rs b/crates/handlers/src/views/register/steps/display_name.rs index a9ff90817..34d9d300f 100644 --- a/crates/handlers/src/views/register/steps/display_name.rs +++ b/crates/handlers/src/views/register/steps/display_name.rs @@ -49,7 +49,6 @@ impl ToFormState for DisplayNameForm { name = "handlers.views.register.steps.display_name.get", fields(user_registration.id = %id), skip_all, - err, )] pub(crate) async fn get( mut rng: BoxRng, @@ -100,7 +99,6 @@ pub(crate) async fn get( name = "handlers.views.register.steps.display_name.post", fields(user_registration.id = %id), skip_all, - err, )] pub(crate) async fn post( mut rng: BoxRng, diff --git a/crates/handlers/src/views/register/steps/finish.rs b/crates/handlers/src/views/register/steps/finish.rs index 7c73825cc..c0c0df404 100644 --- a/crates/handlers/src/views/register/steps/finish.rs +++ b/crates/handlers/src/views/register/steps/finish.rs @@ -42,7 +42,6 @@ static PASSWORD_REGISTER_COUNTER: LazyLock> = LazyLock::new(|| { name = "handlers.views.register.steps.finish.get", fields(user_registration.id = %id), skip_all, - err, )] pub(crate) async fn get( mut rng: BoxRng, diff --git a/crates/handlers/src/views/register/steps/verify_email.rs b/crates/handlers/src/views/register/steps/verify_email.rs index bba5b4728..20c3e5cb2 100644 --- a/crates/handlers/src/views/register/steps/verify_email.rs +++ b/crates/handlers/src/views/register/steps/verify_email.rs @@ -37,7 +37,6 @@ impl ToFormState for CodeForm { name = "handlers.views.register.steps.verify_email.get", fields(user_registration.id = %id), skip_all, - err, )] pub(crate) async fn get( mut rng: BoxRng, @@ -104,7 +103,6 @@ pub(crate) async fn get( name = "handlers.views.account_email_verify.post", fields(user_email.id = %id), skip_all, - err, )] pub(crate) async fn post( clock: BoxClock, From 8ba47e9b375418a3d0f04e2e333407b61b151c75 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 16:46:40 +0200 Subject: [PATCH 026/189] Create log contexts for accepted connections & log errors in them --- Cargo.lock | 1 + crates/listener/Cargo.toml | 2 ++ crates/listener/src/server.rs | 68 +++++++++++++++++++---------------- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d51c3a1b..ffc1204f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3523,6 +3523,7 @@ dependencies = [ "http-body", "hyper", "hyper-util", + "mas-context", "pin-project-lite", "rustls-pemfile", "socket2", diff --git a/crates/listener/Cargo.toml b/crates/listener/Cargo.toml index d5178c049..6d26a34c1 100644 --- a/crates/listener/Cargo.toml +++ b/crates/listener/Cargo.toml @@ -27,6 +27,8 @@ tower.workspace = true tower-http.workspace = true tracing.workspace = true +mas-context.workspace = true + [dev-dependencies] anyhow.workspace = true rustls-pemfile = "2.2.0" diff --git a/crates/listener/src/server.rs b/crates/listener/src/server.rs index b022455bb..f0b66820d 100644 --- a/crates/listener/src/server.rs +++ b/crates/listener/src/server.rs @@ -18,6 +18,7 @@ use hyper_util::{ server::conn::auto::Connection, service::TowerToHyperService, }; +use mas_context::LogContext; use pin_project_lite::pin_project; use thiserror::Error; use tokio_rustls::rustls::ServerConfig; @@ -107,12 +108,6 @@ impl Server { #[derive(Debug, Error)] #[non_exhaustive] enum AcceptError { - #[error("failed to accept connection from the underlying socket")] - Socket { - #[source] - source: std::io::Error, - }, - #[error("failed to complete the TLS handshake")] TlsHandshake { #[source] @@ -133,10 +128,6 @@ enum AcceptError { } impl AcceptError { - fn socket(source: std::io::Error) -> Self { - Self::Socket { source } - } - fn tls_handshake(source: std::io::Error) -> Self { Self::TlsHandshake { source } } @@ -164,7 +155,6 @@ impl AcceptError { network.peer.address, network.peer.port, ), - err, )] async fn accept( maybe_proxy_acceptor: &MaybeProxyAcceptor, @@ -357,12 +347,16 @@ pub async fn run_servers( // Poll on the JoinSet to collect connections to serve res = accept_tasks.join_next(), if !accept_tasks.is_empty() => { match res { - Some(Ok(Ok(connection))) => { - tracing::trace!("Accepted connection"); - let conn = AbortableConnection::new(connection, soft_shutdown_token.child_token()); - connection_tasks.spawn(conn); + Some(Ok(Some(connection))) => { + let token = soft_shutdown_token.child_token(); + connection_tasks.spawn(LogContext::new("http-serve").run(async move || { + tracing::debug!("Accepted connection"); + if let Err(e) = AbortableConnection::new(connection, token).await { + tracing::warn!(error = &*e as &dyn std::error::Error, "Failed to serve connection"); + } + })); }, - Some(Ok(Err(_e))) => { /* Connection did not finish handshake, error should be logged in `accept` */ }, + Some(Ok(None)) => { /* Connection did not finish handshake, error should be logged in `accept` */ }, Some(Err(e)) => tracing::error!(error = &e as &dyn std::error::Error, "Join error"), None => tracing::error!("Join set was polled even though it was empty"), } @@ -371,8 +365,7 @@ pub async fn run_servers( // Poll on the JoinSet to collect finished connections res = connection_tasks.join_next(), if !connection_tasks.is_empty() => { match res { - Some(Ok(Ok(()))) => tracing::trace!("Connection finished"), - Some(Ok(Err(e))) => tracing::error!(error = &*e as &dyn std::error::Error, "Error while serving connection"), + Some(Ok(())) => { /* Connection finished, any errors should be logged in in the spawned task */ }, Some(Err(e)) => tracing::error!(error = &e as &dyn std::error::Error, "Join error"), None => tracing::error!("Join set was polled even though it was empty"), } @@ -385,11 +378,23 @@ pub async fn run_servers( // Spawn the connection in the set, so we don't have to wait for the handshake to // accept the next connection. This allows us to keep track of active connections // and waiting on them for a graceful shutdown - accept_tasks.spawn(async move { - let (maybe_proxy_acceptor, maybe_tls_acceptor, service, peer_addr, stream) = res - .map_err(AcceptError::socket)?; - accept(&maybe_proxy_acceptor, &maybe_tls_acceptor, peer_addr, stream, service).await - }); + accept_tasks.spawn(LogContext::new("http-accept").run(async move || { + let (maybe_proxy_acceptor, maybe_tls_acceptor, service, peer_addr, stream) = match res { + Ok(res) => res, + Err(e) => { + tracing::warn!(error = &e as &dyn std::error::Error, "Failed to accept connection from the underlying socket"); + return None; + } + }; + + match accept(&maybe_proxy_acceptor, &maybe_tls_acceptor, peer_addr, stream, service).await { + Ok(connection) => Some(connection), + Err(e) => { + tracing::warn!(error = &e as &dyn std::error::Error, "Failed to accept connection"); + None + } + } + })); }, }; } @@ -409,12 +414,16 @@ pub async fn run_servers( // Poll on the JoinSet to collect connections to serve res = accept_tasks.join_next(), if !accept_tasks.is_empty() => { match res { - Some(Ok(Ok(connection))) => { - tracing::trace!("Accepted connection"); - let conn = AbortableConnection::new(connection, soft_shutdown_token.child_token()); - connection_tasks.spawn(conn); + Some(Ok(Some(connection))) => { + let token = soft_shutdown_token.child_token(); + connection_tasks.spawn(LogContext::new("http-serve").run(async || { + tracing::debug!("Accepted connection"); + if let Err(e) = AbortableConnection::new(connection, token).await { + tracing::warn!(error = &*e as &dyn std::error::Error, "Failed to serve connection"); + } + })); } - Some(Ok(Err(_e))) => { /* Connection did not finish handshake, error should be logged in `accept` */ }, + Some(Ok(None)) => { /* Connection did not finish handshake, error should be logged in `accept` */ }, Some(Err(e)) => tracing::error!(error = &e as &dyn std::error::Error, "Join error"), None => tracing::error!("Join set was polled even though it was empty"), } @@ -423,8 +432,7 @@ pub async fn run_servers( // Poll on the JoinSet to collect finished connections res = connection_tasks.join_next(), if !connection_tasks.is_empty() => { match res { - Some(Ok(Ok(()))) => tracing::trace!("Connection finished"), - Some(Ok(Err(e))) => tracing::error!(error = &*e as &dyn std::error::Error, "Error while serving connection"), + Some(Ok(())) => { /* Connection finished, any errors should be logged in in the spawned task */ }, Some(Err(e)) => tracing::error!(error = &e as &dyn std::error::Error, "Join error"), None => tracing::error!("Join set was polled even though it was empty"), } From 93590a0375bcbcca0fc648ba987d27b75461433f Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 16:51:37 +0200 Subject: [PATCH 027/189] Make the error wrapper log errors --- crates/axum-utils/src/error_wrapper.rs | 12 ++++++++++-- crates/axum-utils/src/sentry.rs | 19 +++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/crates/axum-utils/src/error_wrapper.rs b/crates/axum-utils/src/error_wrapper.rs index 40baf520a..1315803fe 100644 --- a/crates/axum-utils/src/error_wrapper.rs +++ b/crates/axum-utils/src/error_wrapper.rs @@ -7,6 +7,8 @@ use axum::response::{IntoResponse, Response}; use http::StatusCode; +use crate::record_error; + /// A simple wrapper around an error that implements [`IntoResponse`]. #[derive(Debug, thiserror::Error)] #[error(transparent)] @@ -14,10 +16,16 @@ pub struct ErrorWrapper(#[from] pub T); impl IntoResponse for ErrorWrapper where - T: std::error::Error, + T: std::error::Error + 'static, { fn into_response(self) -> Response { // TODO: make this a bit more user friendly - (StatusCode::INTERNAL_SERVER_ERROR, self.0.to_string()).into_response() + let sentry_event_id = record_error!(self.0); + ( + StatusCode::INTERNAL_SERVER_ERROR, + sentry_event_id, + self.0.to_string(), + ) + .into_response() } } diff --git a/crates/axum-utils/src/sentry.rs b/crates/axum-utils/src/sentry.rs index 5dd00a211..2744accff 100644 --- a/crates/axum-utils/src/sentry.rs +++ b/crates/axum-utils/src/sentry.rs @@ -46,17 +46,20 @@ macro_rules! record_error { Option::<$crate::sentry::SentryEventID>::None }}; + ($error:expr) => {{ + tracing::error!(message = &$error as &dyn std::error::Error); + + // With the `sentry-tracing` integration, Sentry should have + // captured an error, so let's extract the last event ID from the + // current hub + $crate::sentry::SentryEventID::for_last_event() + }}; + ($error:expr, $pattern:pat) => { if let $pattern = $error { - tracing::error!(message = &$error as &dyn std::error::Error); - - // With the `sentry-tracing` integration, Sentry should have - // captured an error, so let's extract the last event ID from the - // current hub - $crate::sentry::SentryEventID::for_last_event() + record_error!($error) } else { - tracing::warn!(message = &$error as &dyn std::error::Error); - None + record_error!($error, !) } }; } From 3a8d4a1e8a2c1786c7fd40ae1448e28922782579 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 16:58:00 +0200 Subject: [PATCH 028/189] Replace most remaining #[instrument(err)] annotations --- crates/cli/src/commands/config.rs | 3 ++- crates/cli/src/commands/server.rs | 3 ++- crates/cli/src/commands/syn2mas.rs | 3 ++- crates/cli/src/sync.rs | 2 +- crates/cli/src/util.rs | 13 +++++++------ crates/config/src/sections/secrets.rs | 2 +- crates/email/src/mailer.rs | 4 +--- crates/handlers/src/captcha.rs | 1 - crates/policy/src/lib.rs | 7 ++----- crates/templates/src/lib.rs | 2 -- 10 files changed, 18 insertions(+), 22 deletions(-) diff --git a/crates/cli/src/commands/config.rs b/crates/cli/src/commands/config.rs index 0a246d86c..6698416e9 100644 --- a/crates/cli/src/commands/config.rs +++ b/crates/cli/src/commands/config.rs @@ -129,7 +129,8 @@ impl Options { prune, dry_run, ) - .await?; + .await + .context("could not sync the configuration with the database")?; } } diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index 37d4f5392..de94ce7b6 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -113,7 +113,8 @@ impl Options { false, false, ) - .await?; + .await + .context("could not sync the configuration with the database")?; } // Initialize the key store diff --git a/crates/cli/src/commands/syn2mas.rs b/crates/cli/src/commands/syn2mas.rs index 473afed54..5a24ca559 100644 --- a/crates/cli/src/commands/syn2mas.rs +++ b/crates/cli/src/commands/syn2mas.rs @@ -150,7 +150,8 @@ impl Options { // Not a dry run — we do want to create the providers in the database false, ) - .await?; + .await + .context("could not sync the configuration with the database")?; } let Either::Left(mut mas_connection) = LockedMasDatabase::try_new(mas_connection) diff --git a/crates/cli/src/sync.rs b/crates/cli/src/sync.rs index 52e0f3337..303a1cf92 100644 --- a/crates/cli/src/sync.rs +++ b/crates/cli/src/sync.rs @@ -62,7 +62,7 @@ fn map_claims_imports( } } -#[tracing::instrument(name = "config.sync", skip_all, err(Debug))] +#[tracing::instrument(name = "config.sync", skip_all)] pub async fn config_sync( upstream_oauth2_config: UpstreamOAuth2Config, clients_config: ClientsConfig, diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index 3d74fb4bf..e66966103 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -22,7 +22,7 @@ use mas_policy::PolicyFactory; use mas_router::UrlBuilder; use mas_storage::RepositoryAccess; use mas_storage_pg::PgRepository; -use mas_templates::{SiteConfigExt, TemplateLoadingError, Templates}; +use mas_templates::{SiteConfigExt, Templates}; use sqlx::{ ConnectOptions, Executor, PgConnection, PgPool, postgres::{PgConnectOptions, PgPoolOptions}, @@ -226,7 +226,7 @@ pub async fn templates_from_config( config: &TemplatesConfig, site_config: &SiteConfig, url_builder: &UrlBuilder, -) -> Result { +) -> Result { Templates::load( config.path.clone(), url_builder.clone(), @@ -236,6 +236,7 @@ pub async fn templates_from_config( site_config.templates_features(), ) .await + .with_context(|| format!("Failed to load the templates at {}", config.path)) } fn database_connect_options_from_config( @@ -335,7 +336,7 @@ fn database_connect_options_from_config( } /// Create a database connection pool from the configuration -#[tracing::instrument(name = "db.connect", skip_all, err(Debug))] +#[tracing::instrument(name = "db.connect", skip_all)] pub async fn database_pool_from_config(config: &DatabaseConfig) -> Result { let options = database_connect_options_from_config(config, &DatabaseConnectOptions::default())?; PgPoolOptions::new() @@ -371,7 +372,7 @@ impl Default for DatabaseConnectOptions { } /// Create a single database connection from the configuration -#[tracing::instrument(name = "db.connect", skip_all, err(Debug))] +#[tracing::instrument(name = "db.connect", skip_all)] pub async fn database_connection_from_config( config: &DatabaseConfig, ) -> Result { @@ -383,7 +384,7 @@ pub async fn database_connection_from_config( /// Create a single database connection from the configuration, /// with specific options. -#[tracing::instrument(name = "db.connect", skip_all, err(Debug))] +#[tracing::instrument(name = "db.connect", skip_all)] pub async fn database_connection_from_config_with_options( config: &DatabaseConfig, options: &DatabaseConnectOptions, @@ -434,7 +435,7 @@ pub async fn load_policy_factory_dynamic_data_continuously( } /// Update the policy factory dynamic data from the database -#[tracing::instrument(name = "policy.load_dynamic_data", skip_all, err(Debug))] +#[tracing::instrument(name = "policy.load_dynamic_data", skip_all)] pub async fn load_policy_factory_dynamic_data( policy_factory: &PolicyFactory, pool: &PgPool, diff --git a/crates/config/src/sections/secrets.rs b/crates/config/src/sections/secrets.rs index 6a375586b..10df52e02 100644 --- a/crates/config/src/sections/secrets.rs +++ b/crates/config/src/sections/secrets.rs @@ -70,7 +70,7 @@ impl SecretsConfig { /// # Errors /// /// Returns an error when a key could not be imported - #[tracing::instrument(name = "secrets.load", skip_all, err(Debug))] + #[tracing::instrument(name = "secrets.load", skip_all)] pub async fn key_store(&self) -> anyhow::Result { let mut keys = Vec::with_capacity(self.keys.len()); for item in &self.keys { diff --git a/crates/email/src/mailer.rs b/crates/email/src/mailer.rs index ed0968269..443859a3b 100644 --- a/crates/email/src/mailer.rs +++ b/crates/email/src/mailer.rs @@ -111,7 +111,6 @@ impl Mailer { email.to = %to, email.language = %context.language(), ), - err, )] pub async fn send_verification_email( &self, @@ -137,7 +136,6 @@ impl Mailer { user.id = %context.user().id, user_recovery_session.id = %context.session().id, ), - err, )] pub async fn send_recovery_email( &self, @@ -154,7 +152,7 @@ impl Mailer { /// # Errors /// /// Returns an error if the connection failed - #[tracing::instrument(name = "email.test_connection", skip_all, err)] + #[tracing::instrument(name = "email.test_connection", skip_all)] pub async fn test_connection(&self) -> Result<(), crate::transport::Error> { self.transport.test_connection().await } diff --git a/crates/handlers/src/captcha.rs b/crates/handlers/src/captcha.rs index fec2a7573..740995145 100644 --- a/crates/handlers/src/captcha.rs +++ b/crates/handlers/src/captcha.rs @@ -156,7 +156,6 @@ impl Form { skip_all, name = "captcha.verify", fields(captcha.hostname, captcha.challenge_ts, captcha.service), - err )] pub async fn verify( &self, diff --git a/crates/policy/src/lib.rs b/crates/policy/src/lib.rs index c8b771b6d..5a714e9a2 100644 --- a/crates/policy/src/lib.rs +++ b/crates/policy/src/lib.rs @@ -197,7 +197,7 @@ pub struct PolicyFactory { } impl PolicyFactory { - #[tracing::instrument(name = "policy.load", skip(source), err)] + #[tracing::instrument(name = "policy.load", skip(source))] pub async fn load( mut source: impl AsyncRead + std::marker::Unpin, data: Data, @@ -283,7 +283,7 @@ impl PolicyFactory { Ok(true) } - #[tracing::instrument(name = "policy.instantiate", skip_all, err)] + #[tracing::instrument(name = "policy.instantiate", skip_all)] pub async fn instantiate(&self) -> Result { let data = self.dynamic_data.load(); self.instantiate_with_data(&data.merged).await @@ -342,7 +342,6 @@ impl Policy { fields( %input.email, ), - err, )] pub async fn evaluate_email( &mut self, @@ -364,7 +363,6 @@ impl Policy { input.username = input.username, input.email = input.email, ), - err, )] pub async fn evaluate_register( &mut self, @@ -402,7 +400,6 @@ impl Policy { %input.scope, %input.client.id, ), - err, )] pub async fn evaluate_authorization_grant( &mut self, diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 704c55248..4c021f87f 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -138,7 +138,6 @@ impl Templates { name = "templates.load", skip_all, fields(%path), - err, )] pub async fn load( path: Utf8PathBuf, @@ -258,7 +257,6 @@ impl Templates { name = "templates.reload", skip_all, fields(path = %self.path), - err, )] pub async fn reload(&self) -> Result<(), TemplateLoadingError> { let (translator, environment) = Self::load_( From 005c427c2f2d7e98fa819a57e9be8abea4c09607 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 17:34:17 +0200 Subject: [PATCH 029/189] Record the job result from within the job LogContext This means we can log stats about the job when it finishes, and its status will have the right log context attached to it. --- crates/tasks/src/new_queue.rs | 189 ++++++++++++++++++---------------- 1 file changed, 101 insertions(+), 88 deletions(-) diff --git a/crates/tasks/src/new_queue.rs b/crates/tasks/src/new_queue.rs index a9ba39dfb..376a2db81 100644 --- a/crates/tasks/src/new_queue.rs +++ b/crates/tasks/src/new_queue.rs @@ -184,7 +184,7 @@ fn retry_delay(attempt: usize) -> Duration { Duration::milliseconds(2_i64.saturating_pow(attempt) * 5_000) } -type JobResult = Result<(), JobError>; +type JobResult = (std::time::Duration, Result<(), JobError>); type JobFactory = Arc Box + Send + Sync>; struct ScheduleDefinition { @@ -774,7 +774,7 @@ impl JobTracker { fn spawn_job(&mut self, state: State, context: JobContext, payload: JobPayload) { let factory = self.factories.get(context.queue_name.as_str()).cloned(); let task = { - let log_context = LogContext::new(format!("worker-job-{}", context.queue_name)); + let log_context = LogContext::new(format!("job-{}", context.queue_name)); let context = context.clone(); let span = context.span(); log_context @@ -788,7 +788,70 @@ impl JobTracker { job.attempt = %context.attempt, "Running job" ); - job.run(&state, context).await + let result = job.run(&state, context.clone()).await; + + let Some(log_context) = LogContext::current() else { + // This should never happen, but if it does it's fine: we're recovering fine + // from panics in those tasks + panic!("Missing log context, this should never happen"); + }; + + let context_stats = log_context.stats(); + + // We log the result here so that it's attached to the right span & log context + match &result { + Ok(()) => { + tracing::info!( + job.id = %context.id, + job.queue.name = %context.queue_name, + job.attempt = %context.attempt, + "Job completed [{context_stats}]" + ); + } + + Err(JobError { + decision: JobErrorDecision::Fail, + error, + }) => { + tracing::error!( + error = &**error as &dyn std::error::Error, + job.id = %context.id, + job.queue.name = %context.queue_name, + job.attempt = %context.attempt, + "Job failed, not retrying [{context_stats}]" + ); + } + + Err(JobError { + decision: JobErrorDecision::Retry, + error, + }) if context.attempt < MAX_ATTEMPTS => { + let delay = retry_delay(context.attempt); + tracing::warn!( + error = &**error as &dyn std::error::Error, + job.id = %context.id, + job.queue.name = %context.queue_name, + job.attempt = %context.attempt, + "Job failed, will retry in {}s [{context_stats}]", + delay.num_seconds() + ); + } + + Err(JobError { + decision: JobErrorDecision::Retry, + error, + }) => { + tracing::error!( + error = &**error as &dyn std::error::Error, + job.id = %context.id, + job.queue.name = %context.queue_name, + job.attempt = %context.attempt, + "Job failed too many times, abandonning [{context_stats}]" + ); + } + } + + (context_stats.elapsed, result) }) .instrument(span) }; @@ -847,15 +910,10 @@ impl JobTracker { } } - // XXX: the time measurement isn't accurate, as it would include the - // time spent between the task finishing, and us processing the result. - // It's fine for now, as it at least gives us an idea of how many tasks - // we run, and what their status is - while let Some(result) = self.last_join_result.take() { match result { - // The job succeeded - Ok((id, Ok(()))) => { + // The job succeeded. The logging and time measurement is already done in the task + Ok((id, (elapsed, Ok(())))) => { let context = self .job_contexts .remove(&id) @@ -866,22 +924,9 @@ impl JobTracker { &[KeyValue::new("job.queue.name", context.queue_name.clone())], ); - let elapsed = context - .start - .elapsed() - .as_millis() - .try_into() - .unwrap_or(u64::MAX); - tracing::info!( - job.id = %context.id, - job.queue.name = %context.queue_name, - job.attempt = %context.attempt, - job.elapsed = format!("{elapsed}ms"), - "Job completed" - ); - + let elapsed_ms = elapsed.as_millis().try_into().unwrap_or(u64::MAX); self.job_processing_time.record( - elapsed, + elapsed_ms, &[ KeyValue::new("job.queue.name", context.queue_name), KeyValue::new("job.result", "success"), @@ -893,8 +938,8 @@ impl JobTracker { .await?; } - // The job failed - Ok((id, Err(e))) => { + // The job failed. The logging and time measurement is already done in the task + Ok((id, (elapsed, Err(e)))) => { let context = self .job_contexts .remove(&id) @@ -910,26 +955,11 @@ impl JobTracker { .mark_as_failed(clock, context.id, &reason) .await?; - let elapsed = context - .start - .elapsed() - .as_millis() - .try_into() - .unwrap_or(u64::MAX); - + let elapsed_ms = elapsed.as_millis().try_into().unwrap_or(u64::MAX); match e.decision { JobErrorDecision::Fail => { - tracing::error!( - error = &e as &dyn std::error::Error, - job.id = %context.id, - job.queue.name = %context.queue_name, - job.attempt = %context.attempt, - job.elapsed = format!("{elapsed}ms"), - "Job failed, not retrying" - ); - self.job_processing_time.record( - elapsed, + elapsed_ms, &[ KeyValue::new("job.queue.name", context.queue_name), KeyValue::new("job.result", "failed"), @@ -938,50 +968,31 @@ impl JobTracker { ); } + JobErrorDecision::Retry if context.attempt < MAX_ATTEMPTS => { + self.job_processing_time.record( + elapsed_ms, + &[ + KeyValue::new("job.queue.name", context.queue_name), + KeyValue::new("job.result", "failed"), + KeyValue::new("job.decision", "retry"), + ], + ); + + let delay = retry_delay(context.attempt); + repo.queue_job() + .retry(&mut *rng, clock, context.id, delay) + .await?; + } + JobErrorDecision::Retry => { - if context.attempt < MAX_ATTEMPTS { - let delay = retry_delay(context.attempt); - tracing::warn!( - error = &e as &dyn std::error::Error, - job.id = %context.id, - job.queue.name = %context.queue_name, - job.attempt = %context.attempt, - job.elapsed = format!("{elapsed}ms"), - "Job failed, will retry in {}s", - delay.num_seconds() - ); - - self.job_processing_time.record( - elapsed, - &[ - KeyValue::new("job.queue.name", context.queue_name), - KeyValue::new("job.result", "failed"), - KeyValue::new("job.decision", "retry"), - ], - ); - - repo.queue_job() - .retry(&mut *rng, clock, context.id, delay) - .await?; - } else { - tracing::error!( - error = &e as &dyn std::error::Error, - job.id = %context.id, - job.queue.name = %context.queue_name, - job.attempt = %context.attempt, - job.elapsed = format!("{elapsed}ms"), - "Job failed too many times, abandonning" - ); - - self.job_processing_time.record( - elapsed, - &[ - KeyValue::new("job.queue.name", context.queue_name), - KeyValue::new("job.result", "failed"), - KeyValue::new("job.decision", "abandon"), - ], - ); - } + self.job_processing_time.record( + elapsed_ms, + &[ + KeyValue::new("job.queue.name", context.queue_name), + KeyValue::new("job.result", "failed"), + KeyValue::new("job.decision", "abandon"), + ], + ); } } } @@ -999,6 +1010,8 @@ impl JobTracker { &[KeyValue::new("job.queue.name", context.queue_name.clone())], ); + // This measurement is not accurate as it includes the time processing the jobs, + // but it's fine, it's only for panicked tasks let elapsed = context .start .elapsed() @@ -1013,7 +1026,7 @@ impl JobTracker { if context.attempt < MAX_ATTEMPTS { let delay = retry_delay(context.attempt); - tracing::warn!( + tracing::error!( error = &e as &dyn std::error::Error, job.id = %context.id, job.queue.name = %context.queue_name, From 2e2193b806eb917b7e8c514e0765028b2603953f Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 17 Apr 2025 17:44:07 +0200 Subject: [PATCH 030/189] tasks: don't rely on #[instrument(err)] for logging errors --- crates/tasks/src/database.rs | 4 ++-- crates/tasks/src/email.rs | 2 -- crates/tasks/src/matrix.rs | 4 ---- crates/tasks/src/new_queue.rs | 11 +++++------ crates/tasks/src/recovery.rs | 1 - crates/tasks/src/user.rs | 2 -- 6 files changed, 7 insertions(+), 17 deletions(-) diff --git a/crates/tasks/src/database.rs b/crates/tasks/src/database.rs index 24cfd43d8..fa424d7df 100644 --- a/crates/tasks/src/database.rs +++ b/crates/tasks/src/database.rs @@ -17,7 +17,7 @@ use crate::{ #[async_trait] impl RunnableJob for CleanupExpiredTokensJob { - #[tracing::instrument(name = "job.cleanup_expired_tokens", skip_all, err)] + #[tracing::instrument(name = "job.cleanup_expired_tokens", skip_all)] async fn run(&self, state: &State, _context: JobContext) -> Result<(), JobError> { let clock = state.clock(); let mut repo = state.repository().await.map_err(JobError::retry)?; @@ -41,7 +41,7 @@ impl RunnableJob for CleanupExpiredTokensJob { #[async_trait] impl RunnableJob for PruneStalePolicyDataJob { - #[tracing::instrument(name = "job.prune_stale_policy_data", skip_all, err)] + #[tracing::instrument(name = "job.prune_stale_policy_data", skip_all)] async fn run(&self, state: &State, _context: JobContext) -> Result<(), JobError> { let mut repo = state.repository().await.map_err(JobError::retry)?; diff --git a/crates/tasks/src/email.rs b/crates/tasks/src/email.rs index 25ab5b7c2..4eacdfaf6 100644 --- a/crates/tasks/src/email.rs +++ b/crates/tasks/src/email.rs @@ -23,7 +23,6 @@ impl RunnableJob for VerifyEmailJob { name = "job.verify_email", fields(user_email.id = %self.user_email_id()), skip_all, - err, )] async fn run(&self, _state: &State, _context: JobContext) -> Result<(), JobError> { // This job was for the old email verification flow, which has been replaced. @@ -39,7 +38,6 @@ impl RunnableJob for SendEmailAuthenticationCodeJob { name = "job.send_email_authentication_code", fields(user_email_authentication.id = %self.user_email_authentication_id()), skip_all, - err, )] async fn run(&self, state: &State, _context: JobContext) -> Result<(), JobError> { let clock = state.clock(); diff --git a/crates/tasks/src/matrix.rs b/crates/tasks/src/matrix.rs index d65152198..3060b3d7b 100644 --- a/crates/tasks/src/matrix.rs +++ b/crates/tasks/src/matrix.rs @@ -36,7 +36,6 @@ impl RunnableJob for ProvisionUserJob { name = "job.provision_user" fields(user.id = %self.user_id()), skip_all, - err, )] async fn run(&self, state: &State, _context: JobContext) -> Result<(), JobError> { let matrix = state.matrix_connection(); @@ -103,7 +102,6 @@ impl RunnableJob for ProvisionDeviceJob { device.id = %self.device_id(), ), skip_all, - err, )] async fn run(&self, state: &State, _context: JobContext) -> Result<(), JobError> { let mut repo = state.repository().await.map_err(JobError::retry)?; @@ -140,7 +138,6 @@ impl RunnableJob for DeleteDeviceJob { device.id = %self.device_id(), ), skip_all, - err, )] async fn run(&self, state: &State, _context: JobContext) -> Result<(), JobError> { let mut rng = state.rng(); @@ -172,7 +169,6 @@ impl RunnableJob for SyncDevicesJob { name = "job.sync_devices", fields(user.id = %self.user_id()), skip_all, - err, )] async fn run(&self, state: &State, _context: JobContext) -> Result<(), JobError> { let matrix = state.matrix_connection(); diff --git a/crates/tasks/src/new_queue.rs b/crates/tasks/src/new_queue.rs index 376a2db81..81fef84d4 100644 --- a/crates/tasks/src/new_queue.rs +++ b/crates/tasks/src/new_queue.rs @@ -348,7 +348,7 @@ impl QueueWorker { Ok(()) } - #[tracing::instrument(name = "worker.setup_schedules", skip_all, err)] + #[tracing::instrument(name = "worker.setup_schedules", skip_all)] pub async fn setup_schedules(&mut self) -> Result<(), QueueRunnerError> { let schedules: Vec<_> = self.schedules.iter().map(|s| s.schedule_name).collect(); @@ -372,7 +372,7 @@ impl QueueWorker { Ok(()) } - #[tracing::instrument(name = "worker.run_loop", skip_all, err)] + #[tracing::instrument(name = "worker.run_loop", skip_all)] async fn run_loop(&mut self) -> Result<(), QueueRunnerError> { self.wait_until_wakeup().await?; @@ -393,7 +393,7 @@ impl QueueWorker { Ok(()) } - #[tracing::instrument(name = "worker.shutdown", skip_all, err)] + #[tracing::instrument(name = "worker.shutdown", skip_all)] async fn shutdown(&mut self) -> Result<(), QueueRunnerError> { tracing::info!("Shutting down worker"); @@ -438,7 +438,7 @@ impl QueueWorker { Ok(()) } - #[tracing::instrument(name = "worker.wait_until_wakeup", skip_all, err)] + #[tracing::instrument(name = "worker.wait_until_wakeup", skip_all)] async fn wait_until_wakeup(&mut self) -> Result<(), QueueRunnerError> { // This is to make sure we wake up every second to do the maintenance tasks // We add a little bit of random jitter to the duration, so that we don't get @@ -487,7 +487,6 @@ impl QueueWorker { name = "worker.tick", skip_all, fields(worker.id = %self.registration.id), - err, )] async fn tick(&mut self) -> Result<(), QueueRunnerError> { tracing::debug!("Tick"); @@ -586,7 +585,7 @@ impl QueueWorker { Ok(()) } - #[tracing::instrument(name = "worker.perform_leader_duties", skip_all, err)] + #[tracing::instrument(name = "worker.perform_leader_duties", skip_all)] async fn perform_leader_duties(&mut self) -> Result<(), QueueRunnerError> { // This should have been checked by the caller, but better safe than sorry if !self.am_i_leader { diff --git a/crates/tasks/src/recovery.rs b/crates/tasks/src/recovery.rs index 658f1b9f7..9d68dad66 100644 --- a/crates/tasks/src/recovery.rs +++ b/crates/tasks/src/recovery.rs @@ -32,7 +32,6 @@ impl RunnableJob for SendAccountRecoveryEmailsJob { user_recovery_session.email, ), skip_all, - err, )] async fn run(&self, state: &State, _context: JobContext) -> Result<(), JobError> { let clock = state.clock(); diff --git a/crates/tasks/src/user.rs b/crates/tasks/src/user.rs index 272111d17..4dfa081c4 100644 --- a/crates/tasks/src/user.rs +++ b/crates/tasks/src/user.rs @@ -27,7 +27,6 @@ impl RunnableJob for DeactivateUserJob { name = "job.deactivate_user" fields(user.id = %self.user_id(), erase = %self.hs_erase()), skip_all, - err, )] async fn run(&self, state: &State, _context: JobContext) -> Result<(), JobError> { let clock = state.clock(); @@ -118,7 +117,6 @@ impl RunnableJob for ReactivateUserJob { name = "job.reactivate_user", fields(user.id = %self.user_id()), skip_all, - err, )] async fn run(&self, state: &State, _context: JobContext) -> Result<(), JobError> { let matrix = state.matrix_connection(); From 35cd982e6f7b50ec08c22fe9067df4683aa77649 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 18 Apr 2025 11:32:26 +0200 Subject: [PATCH 031/189] syn2mas: refactor the metrics logic in the progress module We don't need to carry around the various meters. Just make them global. --- crates/syn2mas/src/migration.rs | 175 ++++---------------------------- crates/syn2mas/src/progress.rs | 108 ++++++++++++++++++-- crates/syn2mas/src/telemetry.rs | 23 +---- 3 files changed, 126 insertions(+), 180 deletions(-) diff --git a/crates/syn2mas/src/migration.rs b/crates/syn2mas/src/migration.rs index efefc25d7..37612d1d3 100644 --- a/crates/syn2mas/src/migration.rs +++ b/crates/syn2mas/src/migration.rs @@ -17,7 +17,6 @@ use chrono::{DateTime, Utc}; use compact_str::CompactString; use futures_util::{SinkExt, StreamExt as _, TryFutureExt, TryStreamExt as _}; use mas_storage::Clock; -use opentelemetry::{KeyValue, metrics::Counter}; use rand::{RngCore, SeedableRng}; use thiserror::Error; use thiserror_ext::ContextInto; @@ -33,16 +32,11 @@ use crate::{ MasNewEmailThreepid, MasNewUnsupportedThreepid, MasNewUpstreamOauthLink, MasNewUser, MasNewUserPassword, MasWriteBuffer, MasWriter, }, - progress::Progress, + progress::{EntityType, Progress}, synapse_reader::{ self, ExtractLocalpartError, FullUserId, SynapseAccessToken, SynapseDevice, SynapseExternalId, SynapseRefreshableTokenPair, SynapseThreepid, SynapseUser, }, - telemetry::{ - K_ENTITY, METER, V_ENTITY_DEVICES, V_ENTITY_EXTERNAL_IDS, - V_ENTITY_NONREFRESHABLE_ACCESS_TOKENS, V_ENTITY_REFRESHABLE_TOKEN_PAIRS, - V_ENTITY_THREEPIDS, V_ENTITY_USERS, - }, }; #[derive(Debug, Error, ContextInto)] @@ -146,7 +140,7 @@ struct MigrationState { /// /// - An underlying database access error, either to MAS or to Synapse. /// - Invalid data in the Synapse database. -#[allow(clippy::implicit_hasher, clippy::too_many_lines)] +#[allow(clippy::implicit_hasher)] pub async fn migrate( mut synapse: SynapseReader<'_>, mas: MasWriter, @@ -158,49 +152,6 @@ pub async fn migrate( ) -> Result<(), Error> { let counts = synapse.count_rows().await.into_synapse("counting users")?; - let approx_total_counter = METER - .u64_counter("syn2mas.entity.approx_total") - .with_description("Approximate number of entities of this type to be migrated") - .build(); - let migrated_otel_counter = METER - .u64_counter("syn2mas.entity.migrated") - .with_description("Number of entities of this type that have been migrated so far") - .build(); - let skipped_otel_counter = METER - .u64_counter("syn2mas.entity.skipped") - .with_description("Number of entities of this type that have been skipped so far") - .build(); - - approx_total_counter.add( - counts.users as u64, - &[KeyValue::new(K_ENTITY, V_ENTITY_USERS)], - ); - approx_total_counter.add( - counts.devices as u64, - &[KeyValue::new(K_ENTITY, V_ENTITY_DEVICES)], - ); - approx_total_counter.add( - counts.threepids as u64, - &[KeyValue::new(K_ENTITY, V_ENTITY_THREEPIDS)], - ); - approx_total_counter.add( - counts.external_ids as u64, - &[KeyValue::new(K_ENTITY, V_ENTITY_EXTERNAL_IDS)], - ); - // assume 1 refreshable access token per refresh token. - let approx_nonrefreshable_access_tokens = counts.access_tokens - counts.refresh_tokens; - approx_total_counter.add( - approx_nonrefreshable_access_tokens as u64, - &[KeyValue::new( - K_ENTITY, - V_ENTITY_NONREFRESHABLE_ACCESS_TOKENS, - )], - ); - approx_total_counter.add( - counts.refresh_tokens as u64, - &[KeyValue::new(K_ENTITY, V_ENTITY_REFRESHABLE_TOKEN_PAIRS)], - ); - let state = MigrationState { server_name, // We oversize the hashmaps, as the estimates are innaccurate, and we would like to avoid @@ -213,83 +164,32 @@ pub async fn migrate( provider_id_mapping, }; - let progress_counter = progress.migrating_data(V_ENTITY_USERS, counts.users); - let (mas, state) = migrate_users( - &mut synapse, - mas, - state, - rng, - progress_counter, - migrated_otel_counter.clone(), - skipped_otel_counter.clone(), - ) - .await?; + let progress_counter = progress.migrating_data(EntityType::Users, counts.users); + let (mas, state) = migrate_users(&mut synapse, mas, state, rng, progress_counter).await?; - let progress_counter = progress.migrating_data(V_ENTITY_THREEPIDS, counts.threepids); - let (mas, state) = migrate_threepids( - &mut synapse, - mas, - rng, - state, - progress_counter, - migrated_otel_counter.clone(), - skipped_otel_counter.clone(), - ) - .await?; + let progress_counter = progress.migrating_data(EntityType::ThreePids, counts.threepids); + let (mas, state) = migrate_threepids(&mut synapse, mas, rng, state, progress_counter).await?; - let progress_counter = progress.migrating_data(V_ENTITY_EXTERNAL_IDS, counts.external_ids); - let (mas, state) = migrate_external_ids( - &mut synapse, - mas, - rng, - state, - progress_counter, - migrated_otel_counter.clone(), - skipped_otel_counter.clone(), - ) - .await?; + let progress_counter = progress.migrating_data(EntityType::ExternalIds, counts.external_ids); + let (mas, state) = + migrate_external_ids(&mut synapse, mas, rng, state, progress_counter).await?; let progress_counter = progress.migrating_data( - V_ENTITY_NONREFRESHABLE_ACCESS_TOKENS, + EntityType::NonRefreshableAccessTokens, counts.access_tokens - counts.refresh_tokens, ); - let (mas, state) = migrate_unrefreshable_access_tokens( - &mut synapse, - mas, - clock, - rng, - state, - progress_counter, - migrated_otel_counter.clone(), - skipped_otel_counter.clone(), - ) - .await?; + let (mas, state) = + migrate_unrefreshable_access_tokens(&mut synapse, mas, clock, rng, state, progress_counter) + .await?; let progress_counter = - progress.migrating_data(V_ENTITY_REFRESHABLE_TOKEN_PAIRS, counts.refresh_tokens); - let (mas, state) = migrate_refreshable_token_pairs( - &mut synapse, - mas, - clock, - rng, - state, - progress_counter, - migrated_otel_counter.clone(), - skipped_otel_counter.clone(), - ) - .await?; + progress.migrating_data(EntityType::RefreshableTokens, counts.refresh_tokens); + let (mas, state) = + migrate_refreshable_token_pairs(&mut synapse, mas, clock, rng, state, progress_counter) + .await?; - let progress_counter = progress.migrating_data("devices", counts.devices); - let (mas, _state) = migrate_devices( - &mut synapse, - mas, - rng, - state, - progress_counter, - migrated_otel_counter.clone(), - skipped_otel_counter.clone(), - ) - .await?; + let progress_counter = progress.migrating_data(EntityType::Devices, counts.devices); + let (mas, _state) = migrate_devices(&mut synapse, mas, rng, state, progress_counter).await?; synapse .finish() @@ -310,11 +210,8 @@ async fn migrate_users( mut state: MigrationState, rng: &mut impl RngCore, progress_counter: ProgressCounter, - migrated_otel_counter: Counter, - skipped_otel_counter: Counter, ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let otel_kv = [KeyValue::new(K_ENTITY, V_ENTITY_USERS)]; let (tx, mut rx) = tokio::sync::mpsc::channel::(10 * 1024 * 1024); @@ -356,7 +253,6 @@ async fn migrate_users( if user.appservice_id.is_some() { flags |= UserFlags::IS_APPSERVICE; - skipped_otel_counter.add(1, &otel_kv); progress_counter.increment_skipped(); // Special case for appservice users: we don't insert them into the database @@ -391,7 +287,6 @@ async fn migrate_users( .into_mas("writing password")?; } - migrated_otel_counter.add(1, &otel_kv); progress_counter.increment_migrated(); } @@ -437,11 +332,8 @@ async fn migrate_threepids( rng: &mut impl RngCore, state: MigrationState, progress_counter: ProgressCounter, - migrated_otel_counter: Counter, - skipped_otel_counter: Counter, ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let otel_kv = [KeyValue::new(K_ENTITY, V_ENTITY_THREEPIDS)]; let mut email_buffer = MasWriteBuffer::new(&mas, MasWriter::write_email_threepids); let mut unsupported_buffer = MasWriteBuffer::new(&mas, MasWriter::write_unsupported_threepids); @@ -469,7 +361,6 @@ async fn migrate_threepids( let Some(mas_user_id) = user_infos.mas_user_id else { progress_counter.increment_skipped(); - skipped_otel_counter.add(1, &otel_kv); continue; }; @@ -504,7 +395,6 @@ async fn migrate_threepids( .into_mas("writing unsupported threepid")?; } - migrated_otel_counter.add(1, &otel_kv); progress_counter.increment_migrated(); } @@ -536,11 +426,8 @@ async fn migrate_external_ids( rng: &mut impl RngCore, state: MigrationState, progress_counter: ProgressCounter, - migrated_otel_counter: Counter, - skipped_otel_counter: Counter, ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let otel_kv = [KeyValue::new(K_ENTITY, V_ENTITY_EXTERNAL_IDS)]; let mut write_buffer = MasWriteBuffer::new(&mas, MasWriter::write_upstream_oauth_links); let mut extids_stream = pin!(synapse.read_user_external_ids()); @@ -564,7 +451,6 @@ async fn migrate_external_ids( let Some(mas_user_id) = user_infos.mas_user_id else { progress_counter.increment_skipped(); - skipped_otel_counter.add(1, &otel_kv); continue; }; @@ -595,7 +481,6 @@ async fn migrate_external_ids( .await .into_mas("failed to write upstream link")?; - migrated_otel_counter.add(1, &otel_kv); progress_counter.increment_migrated(); } @@ -627,11 +512,8 @@ async fn migrate_devices( rng: &mut impl RngCore, mut state: MigrationState, progress_counter: ProgressCounter, - migrated_otel_counter: Counter, - skipped_otel_counter: Counter, ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let otel_kv = [KeyValue::new(K_ENTITY, V_ENTITY_DEVICES)]; let (tx, mut rx) = tokio::sync::mpsc::channel(10 * 1024 * 1024); @@ -664,7 +546,6 @@ async fn migrate_devices( let Some(mas_user_id) = user_infos.mas_user_id else { progress_counter.increment_skipped(); - skipped_otel_counter.add(1, &otel_kv); continue; }; @@ -721,7 +602,6 @@ async fn migrate_devices( .await .into_mas("writing compat sessions")?; - migrated_otel_counter.add(1, &otel_kv); progress_counter.increment_migrated(); } @@ -759,7 +639,6 @@ async fn migrate_devices( /// Migrates unrefreshable access tokens (those without an associated refresh /// token). Some of these may be deviceless. #[tracing::instrument(skip_all, level = Level::INFO)] -#[allow(clippy::too_many_arguments)] async fn migrate_unrefreshable_access_tokens( synapse: &mut SynapseReader<'_>, mut mas: MasWriter, @@ -767,14 +646,8 @@ async fn migrate_unrefreshable_access_tokens( rng: &mut impl RngCore, mut state: MigrationState, progress_counter: ProgressCounter, - migrated_otel_counter: Counter, - skipped_otel_counter: Counter, ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let otel_kv = [KeyValue::new( - K_ENTITY, - V_ENTITY_NONREFRESHABLE_ACCESS_TOKENS, - )]; let (tx, mut rx) = tokio::sync::mpsc::channel(10 * 1024 * 1024); @@ -809,7 +682,6 @@ async fn migrate_unrefreshable_access_tokens( let Some(mas_user_id) = user_infos.mas_user_id else { progress_counter.increment_skipped(); - skipped_otel_counter.add(1, &otel_kv); continue; }; @@ -818,7 +690,6 @@ async fn migrate_unrefreshable_access_tokens( || user_infos.flags.is_appservice() { progress_counter.increment_skipped(); - skipped_otel_counter.add(1, &otel_kv); continue; } @@ -879,7 +750,6 @@ async fn migrate_unrefreshable_access_tokens( .await .into_mas("writing compat access tokens")?; - migrated_otel_counter.add(1, &otel_kv); progress_counter.increment_migrated(); } write_buffer @@ -920,7 +790,6 @@ async fn migrate_unrefreshable_access_tokens( /// Migrates (access token, refresh token) pairs. /// Does not migrate non-refreshable access tokens. #[tracing::instrument(skip_all, level = Level::INFO)] -#[allow(clippy::too_many_arguments)] async fn migrate_refreshable_token_pairs( synapse: &mut SynapseReader<'_>, mut mas: MasWriter, @@ -928,11 +797,8 @@ async fn migrate_refreshable_token_pairs( rng: &mut impl RngCore, mut state: MigrationState, progress_counter: ProgressCounter, - migrated_otel_counter: Counter, - skipped_otel_counter: Counter, ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let otel_kv = [KeyValue::new(K_ENTITY, V_ENTITY_REFRESHABLE_TOKEN_PAIRS)]; let mut token_stream = pin!(synapse.read_refreshable_token_pairs()); let mut access_token_write_buffer = @@ -963,7 +829,6 @@ async fn migrate_refreshable_token_pairs( let Some(mas_user_id) = user_infos.mas_user_id else { progress_counter.increment_skipped(); - skipped_otel_counter.add(1, &otel_kv); continue; }; @@ -972,7 +837,6 @@ async fn migrate_refreshable_token_pairs( || user_infos.flags.is_appservice() { progress_counter.increment_skipped(); - skipped_otel_counter.add(1, &otel_kv); continue; } @@ -1017,7 +881,6 @@ async fn migrate_refreshable_token_pairs( .await .into_mas("writing compat refresh tokens")?; - migrated_otel_counter.add(1, &otel_kv); progress_counter.increment_migrated(); } diff --git a/crates/syn2mas/src/progress.rs b/crates/syn2mas/src/progress.rs index e5f61d292..3c67825ce 100644 --- a/crates/syn2mas/src/progress.rs +++ b/crates/syn2mas/src/progress.rs @@ -1,6 +1,89 @@ -use std::sync::{Arc, atomic::AtomicU32}; +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +use std::sync::{Arc, LazyLock, atomic::AtomicU32}; use arc_swap::ArcSwap; +use opentelemetry::{ + KeyValue, + metrics::{Counter, Gauge}, +}; + +use crate::telemetry::METER; + +/// A gauge that tracks the approximate number of entities of a given type +/// that will be migrated. +pub static APPROX_TOTAL_GAUGE: LazyLock> = LazyLock::new(|| { + METER + .u64_gauge("syn2mas.entity.approx_total") + .with_description("Approximate number of entities of this type to be migrated") + .build() +}); + +/// A counter that tracks the number of entities of a given type that have +/// been migrated so far. +pub static MIGRATED_COUNTER: LazyLock> = LazyLock::new(|| { + METER + .u64_counter("syn2mas.entity.migrated") + .with_description("Number of entities of this type that have been migrated so far") + .build() +}); + +/// A counter that tracks the number of entities of a given type that have +/// been skipped so far. +pub static SKIPPED_COUNTER: LazyLock> = LazyLock::new(|| { + METER + .u64_counter("syn2mas.entity.skipped") + .with_description("Number of entities of this type that have been skipped so far") + .build() +}); + +/// Enum representing the different types of entities that syn2mas can migrate. +#[derive(Debug, Clone, Copy)] +pub enum EntityType { + /// Represents users + Users, + + /// Represents devices + Devices, + + /// Represents third-party IDs + ThreePids, + + /// Represents external IDs + ExternalIds, + + /// Represents non-refreshable access tokens + NonRefreshableAccessTokens, + + /// Represents refreshable access tokens + RefreshableTokens, +} + +impl std::fmt::Display for EntityType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name()) + } +} + +impl EntityType { + pub const fn name(self) -> &'static str { + match self { + Self::Users => "users", + Self::Devices => "devices", + Self::ThreePids => "threepids", + Self::ExternalIds => "external_ids", + Self::NonRefreshableAccessTokens => "nonrefreshable_access_tokens", + Self::RefreshableTokens => "refreshable_tokens", + } + } + + pub fn as_kv(self) -> KeyValue { + KeyValue::new("entity", self.name()) + } +} /// Tracker for the progress of the migration /// @@ -11,25 +94,37 @@ pub struct Progress { current_stage: Arc>, } -#[derive(Clone, Default)] +#[derive(Clone)] pub struct ProgressCounter { inner: Arc, } -#[derive(Default)] struct ProgressCounterInner { + kv: [KeyValue; 1], migrated: AtomicU32, skipped: AtomicU32, } impl ProgressCounter { + fn new(entity: EntityType) -> Self { + Self { + inner: Arc::new(ProgressCounterInner { + kv: [entity.as_kv()], + migrated: AtomicU32::new(0), + skipped: AtomicU32::new(0), + }), + } + } + pub fn increment_migrated(&self) { + MIGRATED_COUNTER.add(1, &self.inner.kv); self.inner .migrated .fetch_add(1, std::sync::atomic::Ordering::Relaxed); } pub fn increment_skipped(&self) { + SKIPPED_COUNTER.add(1, &self.inner.kv); self.inner .skipped .fetch_add(1, std::sync::atomic::Ordering::Relaxed); @@ -52,8 +147,9 @@ impl ProgressCounter { impl Progress { #[must_use] - pub fn migrating_data(&self, entity: &'static str, approx_count: usize) -> ProgressCounter { - let counter = ProgressCounter::default(); + pub fn migrating_data(&self, entity: EntityType, approx_count: usize) -> ProgressCounter { + let counter = ProgressCounter::new(entity); + APPROX_TOTAL_GAUGE.record(approx_count as u64, &[entity.as_kv()]); self.set_current_stage(ProgressStage::MigratingData { entity, counter: counter.clone(), @@ -99,7 +195,7 @@ impl Default for Progress { pub enum ProgressStage { SettingUp, MigratingData { - entity: &'static str, + entity: EntityType, counter: ProgressCounter, approx_count: u64, }, diff --git a/crates/syn2mas/src/telemetry.rs b/crates/syn2mas/src/telemetry.rs index 5c1c0a54a..e9a3385fb 100644 --- a/crates/syn2mas/src/telemetry.rs +++ b/crates/syn2mas/src/telemetry.rs @@ -1,3 +1,8 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + use std::sync::LazyLock; use opentelemetry::{InstrumentationScope, metrics::Meter}; @@ -12,21 +17,3 @@ static SCOPE: LazyLock = LazyLock::new(|| { pub static METER: LazyLock = LazyLock::new(|| opentelemetry::global::meter_with_scope(SCOPE.clone())); - -/// Attribute key for syn2mas.entity metrics representing what entity. -pub const K_ENTITY: &str = "entity"; - -/// Attribute value for syn2mas.entity metrics representing users. -pub const V_ENTITY_USERS: &str = "users"; -/// Attribute value for syn2mas.entity metrics representing devices. -pub const V_ENTITY_DEVICES: &str = "devices"; -/// Attribute value for syn2mas.entity metrics representing threepids. -pub const V_ENTITY_THREEPIDS: &str = "threepids"; -/// Attribute value for syn2mas.entity metrics representing external IDs. -pub const V_ENTITY_EXTERNAL_IDS: &str = "external_ids"; -/// Attribute value for syn2mas.entity metrics representing non-refreshable -/// access token entities. -pub const V_ENTITY_NONREFRESHABLE_ACCESS_TOKENS: &str = "nonrefreshable_access_tokens"; -/// Attribute value for syn2mas.entity metrics representing refreshable -/// access/refresh token pairs. -pub const V_ENTITY_REFRESHABLE_TOKEN_PAIRS: &str = "refreshable_token_pairs"; From b4a1e5f6913251b3241b326c515f121224c41786 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 18 Apr 2025 11:34:27 +0200 Subject: [PATCH 032/189] syn2mas: replace #[allow] annotations with #[expect] Also removes unused #[allow] annotations. --- crates/syn2mas/src/mas_writer/mod.rs | 5 +---- crates/syn2mas/src/migration.rs | 2 +- crates/syn2mas/src/synapse_reader/config.rs | 4 ++-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index 865bf02fe..fe52e0eed 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -46,7 +46,7 @@ pub enum Error { }, #[error("writer connection pool shut down due to error")] - #[allow(clippy::enum_variant_names)] + #[expect(clippy::enum_variant_names)] WriterConnectionPoolError, #[error("inconsistent database: {0}")] @@ -390,7 +390,6 @@ impl MasWriter { /// Errors are returned in the following conditions: /// /// - If the database connection experiences an error. - #[allow(clippy::missing_panics_doc)] // not real #[tracing::instrument(name = "syn2mas.mas_writer.new", skip_all)] pub async fn new( mut conn: LockedMasDatabase, @@ -632,7 +631,6 @@ impl MasWriter { /// Errors are returned in the following conditions: /// /// - If the database writer connection pool had an error. - #[allow(clippy::missing_panics_doc)] // not a real panic #[tracing::instrument(skip_all, level = Level::DEBUG)] pub fn write_users(&mut self, users: Vec) -> BoxFuture<'_, Result<(), Error>> { self.writer_pool @@ -711,7 +709,6 @@ impl MasWriter { /// Errors are returned in the following conditions: /// /// - If the database writer connection pool had an error. - #[allow(clippy::missing_panics_doc)] // not a real panic #[tracing::instrument(skip_all, level = Level::DEBUG)] pub fn write_passwords( &mut self, diff --git a/crates/syn2mas/src/migration.rs b/crates/syn2mas/src/migration.rs index 37612d1d3..6d0420077 100644 --- a/crates/syn2mas/src/migration.rs +++ b/crates/syn2mas/src/migration.rs @@ -140,7 +140,7 @@ struct MigrationState { /// /// - An underlying database access error, either to MAS or to Synapse. /// - Invalid data in the Synapse database. -#[allow(clippy::implicit_hasher)] +#[expect(clippy::implicit_hasher)] pub async fn migrate( mut synapse: SynapseReader<'_>, mas: MasWriter, diff --git a/crates/syn2mas/src/synapse_reader/config.rs b/crates/syn2mas/src/synapse_reader/config.rs index 2c413a1b9..789be6845 100644 --- a/crates/syn2mas/src/synapse_reader/config.rs +++ b/crates/syn2mas/src/synapse_reader/config.rs @@ -16,7 +16,7 @@ use sqlx::postgres::PgConnectOptions; /// /// See: #[derive(Deserialize)] -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub struct Config { pub database: DatabaseSection, @@ -239,7 +239,7 @@ mod test { #[test] fn test_to_sqlx_postgres() { #[track_caller] - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] fn assert_eq_options(config: DatabaseSection, uri: &str) { let config_connect_options = config .to_sqlx_postgres() From a061db35d7c45758ebd2cd5cda2ed2efdb0c8328 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 18 Apr 2025 18:24:35 +0200 Subject: [PATCH 033/189] Make a few password-related options public in the config crate It also adds docs to a few of those options --- crates/config/src/sections/mod.rs | 4 +++- crates/config/src/sections/passwords.rs | 30 +++++++++++++++++-------- docs/config.schema.json | 12 +++++++++- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/crates/config/src/sections/mod.rs b/crates/config/src/sections/mod.rs index d415f646a..9a9fc9de8 100644 --- a/crates/config/src/sections/mod.rs +++ b/crates/config/src/sections/mod.rs @@ -38,7 +38,9 @@ pub use self::{ Resource as HttpResource, TlsConfig as HttpTlsConfig, UnixOrTcp, }, matrix::{HomeserverKind, MatrixConfig}, - passwords::{Algorithm as PasswordAlgorithm, PasswordsConfig}, + passwords::{ + Algorithm as PasswordAlgorithm, HashingScheme as PasswordHashingScheme, PasswordsConfig, + }, policy::PolicyConfig, rate_limiting::RateLimitingConfig, secrets::SecretsConfig, diff --git a/crates/config/src/sections/passwords.rs b/crates/config/src/sections/passwords.rs index 455dbfd61..07ea71b0e 100644 --- a/crates/config/src/sections/passwords.rs +++ b/crates/config/src/sections/passwords.rs @@ -16,7 +16,7 @@ use crate::ConfigurationSection; fn default_schemes() -> Vec { vec![HashingScheme { version: 1, - algorithm: Algorithm::Argon2id, + algorithm: Algorithm::default(), cost: None, secret: None, secret_file: None, @@ -36,10 +36,14 @@ fn default_minimum_complexity() -> u8 { pub struct PasswordsConfig { /// Whether password-based authentication is enabled #[serde(default = "default_enabled")] - enabled: bool, + pub enabled: bool, + /// The hashing schemes to use for hashing and validating passwords + /// + /// The hashing scheme with the highest version number will be used for + /// hashing new passwords. #[serde(default = "default_schemes")] - schemes: Vec, + pub schemes: Vec, /// Score between 0 and 4 determining the minimum allowed password /// complexity. Scores are based on the ESTIMATED number of guesses @@ -154,23 +158,30 @@ impl PasswordsConfig { } } +/// Parameters for a password hashing scheme #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct HashingScheme { - version: u16, + /// The version of the hashing scheme. They must be unique, and the highest + /// version will be used for hashing new passwords. + pub version: u16, - algorithm: Algorithm, + /// The hashing algorithm to use + pub algorithm: Algorithm, /// Cost for the bcrypt algorithm #[serde(skip_serializing_if = "Option::is_none")] #[schemars(default = "default_bcrypt_cost")] - cost: Option, + pub cost: Option, + /// An optional secret to use when hashing passwords. This makes it harder + /// to brute-force the passwords in case of a database leak. #[serde(skip_serializing_if = "Option::is_none")] - secret: Option, + pub secret: Option, + /// Same as `secret`, but read from a file. #[serde(skip_serializing_if = "Option::is_none")] #[schemars(with = "Option")] - secret_file: Option, + pub secret_file: Option, } #[allow(clippy::unnecessary_wraps)] @@ -179,13 +190,14 @@ fn default_bcrypt_cost() -> Option { } /// A hashing algorithm -#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] #[serde(rename_all = "lowercase")] pub enum Algorithm { /// bcrypt Bcrypt, /// argon2id + #[default] Argon2id, /// PBKDF2 diff --git a/docs/config.schema.json b/docs/config.schema.json index 165cf947d..9a1184bfd 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -1566,6 +1566,7 @@ "type": "boolean" }, "schemes": { + "description": "The hashing schemes to use for hashing and validating passwords\n\nThe hashing scheme with the highest version number will be used for hashing new passwords.", "default": [ { "version": 1, @@ -1587,6 +1588,7 @@ } }, "HashingScheme": { + "description": "Parameters for a password hashing scheme", "type": "object", "required": [ "algorithm", @@ -1594,12 +1596,18 @@ ], "properties": { "version": { + "description": "The version of the hashing scheme. They must be unique, and the highest version will be used for hashing new passwords.", "type": "integer", "format": "uint16", "minimum": 0.0 }, "algorithm": { - "$ref": "#/definitions/Algorithm" + "description": "The hashing algorithm to use", + "allOf": [ + { + "$ref": "#/definitions/Algorithm" + } + ] }, "cost": { "description": "Cost for the bcrypt algorithm", @@ -1609,9 +1617,11 @@ "minimum": 0.0 }, "secret": { + "description": "An optional secret to use when hashing passwords. This makes it harder to brute-force the passwords in case of a database leak.", "type": "string" }, "secret_file": { + "description": "Same as `secret`, but read from a file.", "type": "string" } } From 0792171f91bd5b18e74f7034d07aba2741705969 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 18 Apr 2025 18:25:46 +0200 Subject: [PATCH 034/189] Move the synapse_idp_id field to the top of the provider section This means that when serializing those, it will be at a more obvious place. --- crates/config/src/sections/upstream_oauth2.rs | 34 +++++++++---------- docs/config.schema.json | 8 ++--- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/crates/config/src/sections/upstream_oauth2.rs b/crates/config/src/sections/upstream_oauth2.rs index 623a97c14..1183a6421 100644 --- a/crates/config/src/sections/upstream_oauth2.rs +++ b/crates/config/src/sections/upstream_oauth2.rs @@ -418,6 +418,23 @@ pub struct Provider { )] pub id: Ulid, + /// The ID of the provider that was used by Synapse. + /// In order to perform a Synapse-to-MAS migration, this must be specified. + /// + /// ## For providers that used OAuth 2.0 or OpenID Connect in Synapse + /// + /// ### For `oidc_providers`: + /// This should be specified as `oidc-` followed by the ID that was + /// configured as `idp_id` in one of the `oidc_providers` in the Synapse + /// configuration. + /// For example, if Synapse's configuration contained `idp_id: wombat` for + /// this provider, then specify `oidc-wombat` here. + /// + /// ### For `oidc_config` (legacy): + /// Specify `oidc` here. + #[serde(skip_serializing_if = "Option::is_none")] + pub synapse_idp_id: Option, + /// The OIDC issuer URL /// /// This is required if OIDC discovery is enabled (which is the default) @@ -548,21 +565,4 @@ pub struct Provider { /// Orders of the keys are not preserved. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub additional_authorization_parameters: BTreeMap, - - /// The ID of the provider that was used by Synapse. - /// In order to perform a Synapse-to-MAS migration, this must be specified. - /// - /// ## For providers that used OAuth 2.0 or OpenID Connect in Synapse - /// - /// ### For `oidc_providers`: - /// This should be specified as `oidc-` followed by the ID that was - /// configured as `idp_id` in one of the `oidc_providers` in the Synapse - /// configuration. - /// For example, if Synapse's configuration contained `idp_id: wombat` for - /// this provider, then specify `oidc-wombat` here. - /// - /// ### For `oidc_config` (legacy): - /// Specify `oidc` here. - #[serde(skip_serializing_if = "Option::is_none")] - pub synapse_idp_id: Option, } diff --git a/docs/config.schema.json b/docs/config.schema.json index 9a1184bfd..7906e5378 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -1983,6 +1983,10 @@ "type": "string", "pattern": "^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$" }, + "synapse_idp_id": { + "description": "The ID of the provider that was used by Synapse. In order to perform a Synapse-to-MAS migration, this must be specified.\n\n## For providers that used OAuth 2.0 or OpenID Connect in Synapse\n\n### For `oidc_providers`: This should be specified as `oidc-` followed by the ID that was configured as `idp_id` in one of the `oidc_providers` in the Synapse configuration. For example, if Synapse's configuration contained `idp_id: wombat` for this provider, then specify `oidc-wombat` here.\n\n### For `oidc_config` (legacy): Specify `oidc` here.", + "type": "string" + }, "issuer": { "description": "The OIDC issuer URL\n\nThis is required if OIDC discovery is enabled (which is the default)", "type": "string" @@ -2110,10 +2114,6 @@ "additionalProperties": { "type": "string" } - }, - "synapse_idp_id": { - "description": "The ID of the provider that was used by Synapse. In order to perform a Synapse-to-MAS migration, this must be specified.\n\n## For providers that used OAuth 2.0 or OpenID Connect in Synapse\n\n### For `oidc_providers`: This should be specified as `oidc-` followed by the ID that was configured as `idp_id` in one of the `oidc_providers` in the Synapse configuration. For example, if Synapse's configuration contained `idp_id: wombat` for this provider, then specify `oidc-wombat` here.\n\n### For `oidc_config` (legacy): Specify `oidc` here.", - "type": "string" } } }, From 1fcf6503221239a7093baf680506a5aac4fb2cfc Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 18 Apr 2025 18:26:29 +0200 Subject: [PATCH 035/189] Option to generate a MAS config from an existing Synapse config This is a best-effort conversion, which will warn about unsupported options. --- Cargo.lock | 4 + crates/cli/src/commands/config.rs | 24 +- crates/syn2mas/Cargo.toml | 4 + crates/syn2mas/src/synapse_reader/checks.rs | 2 +- .../{config.rs => config/mod.rs} | 117 +++++- .../syn2mas/src/synapse_reader/config/oidc.rs | 347 ++++++++++++++++++ 6 files changed, 475 insertions(+), 23 deletions(-) rename crates/syn2mas/src/synapse_reader/{config.rs => config/mod.rs} (70%) create mode 100644 crates/syn2mas/src/synapse_reader/config/oidc.rs diff --git a/Cargo.lock b/Cargo.lock index e4a179e53..a3170d80a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6161,14 +6161,17 @@ dependencies = [ "futures-util", "insta", "mas-config", + "mas-iana", "mas-storage", "mas-storage-pg", + "oauth2-types", "opentelemetry", "opentelemetry-semantic-conventions", "rand 0.8.5", "rand_chacha 0.3.1", "rustc-hash 2.1.1", "serde", + "serde_json", "sqlx", "thiserror 2.0.12", "thiserror-ext", @@ -6176,6 +6179,7 @@ dependencies = [ "tokio-util", "tracing", "ulid", + "url", "uuid", ] diff --git a/crates/cli/src/commands/config.rs b/crates/cli/src/commands/config.rs index 0a246d86c..26b944016 100644 --- a/crates/cli/src/commands/config.rs +++ b/crates/cli/src/commands/config.rs @@ -11,7 +11,7 @@ use camino::Utf8PathBuf; use clap::Parser; use figment::Figment; use mas_config::{ConfigurationSection, RootConfig, SyncConfig}; -use mas_storage::SystemClock; +use mas_storage::{Clock as _, SystemClock}; use mas_storage_pg::MIGRATOR; use rand::SeedableRng; use tokio::io::AsyncWriteExt; @@ -46,6 +46,10 @@ enum Subcommand { /// If not specified, the config will be written to stdout #[clap(short, long)] output: Option, + + /// Existing Synapse configuration used to generate the MAS config + #[arg(short, long, action = clap::ArgAction::Append)] + synapse_config: Vec, }, /// Sync the clients and providers from the config file to the database @@ -88,14 +92,24 @@ impl Options { info!("Configuration file looks good"); } - SC::Generate { output } => { + SC::Generate { + output, + synapse_config, + } => { let _span = info_span!("cli.config.generate").entered(); + let clock = SystemClock::default(); // XXX: we should disallow SeedableRng::from_entropy - let rng = rand_chacha::ChaChaRng::from_entropy(); - let config = RootConfig::generate(rng).await?; - let config = serde_yaml::to_string(&config)?; + let mut rng = rand_chacha::ChaChaRng::from_entropy(); + let mut config = RootConfig::generate(&mut rng).await?; + if !synapse_config.is_empty() { + info!("Adjusting MAS config to match Synapse config from {synapse_config:?}"); + let synapse_config = syn2mas::synapse_config::Config::load(&synapse_config)?; + config = synapse_config.adjust_mas_config(config, &mut rng, clock.now()); + } + + let config = serde_yaml::to_string(&config)?; if let Some(output) = output { info!("Writing configuration to {output:?}"); let mut file = tokio::fs::File::create(output).await?; diff --git a/crates/syn2mas/Cargo.toml b/crates/syn2mas/Cargo.toml index 0e82867ce..61e7ac2d5 100644 --- a/crates/syn2mas/Cargo.toml +++ b/crates/syn2mas/Cargo.toml @@ -16,6 +16,7 @@ bitflags.workspace = true camino.workspace = true figment.workspace = true serde.workspace = true +serde_json.workspace = true thiserror.workspace = true thiserror-ext.workspace = true tokio.workspace = true @@ -26,6 +27,7 @@ compact_str.workspace = true tracing.workspace = true futures-util = "0.3.31" rustc-hash = "2.1.1" +url.workspace = true rand.workspace = true rand_chacha = "0.3.1" @@ -33,7 +35,9 @@ uuid = "1.16.0" ulid = { workspace = true, features = ["uuid"] } mas-config.workspace = true +mas-iana.workspace = true mas-storage.workspace = true +oauth2-types.workspace = true opentelemetry.workspace = true opentelemetry-semantic-conventions.workspace = true diff --git a/crates/syn2mas/src/synapse_reader/checks.rs b/crates/syn2mas/src/synapse_reader/checks.rs index 360e6d38d..4dca03029 100644 --- a/crates/syn2mas/src/synapse_reader/checks.rs +++ b/crates/syn2mas/src/synapse_reader/checks.rs @@ -157,7 +157,7 @@ pub fn synapse_config_check(synapse_config: &Config) -> (Vec, Vec< )); } - if synapse_config.enable_3pid_changes { + if synapse_config.enable_3pid_changes == Some(true) { errors.push(CheckError::ThreepidChangesEnabled); } diff --git a/crates/syn2mas/src/synapse_reader/config.rs b/crates/syn2mas/src/synapse_reader/config/mod.rs similarity index 70% rename from crates/syn2mas/src/synapse_reader/config.rs rename to crates/syn2mas/src/synapse_reader/config/mod.rs index 789be6845..390dacaa8 100644 --- a/crates/syn2mas/src/synapse_reader/config.rs +++ b/crates/syn2mas/src/synapse_reader/config/mod.rs @@ -3,12 +3,21 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +mod oidc; + use std::collections::BTreeMap; use camino::Utf8PathBuf; +use chrono::{DateTime, Utc}; use figment::providers::{Format, Yaml}; +use mas_config::{PasswordAlgorithm, PasswordHashingScheme}; +use rand::Rng; use serde::Deserialize; use sqlx::postgres::PgConnectOptions; +use tracing::warn; +use url::Url; + +pub use self::oidc::OidcProvider; /// The root of a Synapse configuration. /// This struct only includes fields which the Synapse-to-MAS migration is @@ -23,6 +32,8 @@ pub struct Config { #[serde(default)] pub password_config: PasswordSection, + pub bcrypt_rounds: Option, + #[serde(default)] pub allow_guest_access: bool, @@ -31,11 +42,16 @@ pub struct Config { #[serde(default)] pub enable_registration_captcha: bool, + pub recaptcha_public_key: Option, + pub recaptcha_private_key: Option, /// Normally this defaults to true, but when MAS integration is enabled in /// Synapse it defaults to false. #[serde(default)] - pub enable_3pid_changes: bool, + pub enable_3pid_changes: Option, + + #[serde(default = "default_true")] + enable_set_display_name: bool, #[serde(default)] pub user_consent: Option, @@ -67,6 +83,8 @@ pub struct Config { pub oidc_providers: Vec, pub server_name: String, + + pub public_baseurl: Option, } impl Config { @@ -100,21 +118,97 @@ impl Config { let mut out = BTreeMap::new(); if let Some(provider) = &self.oidc_config { - if provider.issuer.is_some() { + if provider.has_required_fields() { + let mut provider = provider.clone(); // The legacy configuration has an implied IdP ID of `oidc`. - out.insert("oidc".to_owned(), provider.clone()); + let idp_id = provider.idp_id.take().unwrap_or("oidc".to_owned()); + provider.idp_id = Some(idp_id.clone()); + out.insert(idp_id, provider); } } for provider in &self.oidc_providers { - if let Some(idp_id) = &provider.idp_id { + let mut provider = provider.clone(); + let idp_id = match provider.idp_id.take() { + None => "oidc".to_owned(), + Some(idp_id) if idp_id == "oidc" => idp_id, // Synapse internally prefixes the IdP IDs with `oidc-`. - out.insert(format!("oidc-{idp_id}"), provider.clone()); - } + Some(idp_id) => format!("oidc-{idp_id}"), + }; + provider.idp_id = Some(idp_id.clone()); + out.insert(idp_id, provider); } out } + + /// Adjust a MAS configuration to match this Synapse configuration. + #[must_use] + pub fn adjust_mas_config( + self, + mut mas_config: mas_config::RootConfig, + rng: &mut impl Rng, + now: DateTime, + ) -> mas_config::RootConfig { + let providers = self.all_oidc_providers(); + for provider in providers.into_values() { + let Some(mas_provider_config) = provider.into_mas_config(rng, now) else { + // TODO: better log message + warn!("Could not convert OIDC provider to MAS config"); + continue; + }; + + mas_config + .upstream_oauth2 + .providers + .push(mas_provider_config); + } + + // TODO: manage when the option is not set + if let Some(enable_3pid_changes) = self.enable_3pid_changes { + mas_config.account.email_change_allowed = enable_3pid_changes; + } + mas_config.account.displayname_change_allowed = self.enable_set_display_name; + if self.password_config.enabled { + mas_config.passwords.enabled = true; + mas_config.passwords.schemes = vec![ + // This is the password hashing scheme synapse uses + PasswordHashingScheme { + version: 1, + algorithm: PasswordAlgorithm::Bcrypt, + cost: self.bcrypt_rounds, + secret: self.password_config.pepper, + secret_file: None, + }, + // Use the default algorithm MAS uses as a second hashing scheme, so that users + // will get their password hash upgraded to a more modern algorithm over time + PasswordHashingScheme { + version: 2, + algorithm: PasswordAlgorithm::default(), + cost: None, + secret: None, + secret_file: None, + }, + ]; + + mas_config.account.password_registration_enabled = self.enable_registration; + } else { + mas_config.passwords.enabled = false; + } + + if self.enable_registration_captcha { + mas_config.captcha.service = Some(mas_config::CaptchaServiceKind::RecaptchaV2); + mas_config.captcha.site_key = self.recaptcha_public_key; + mas_config.captcha.secret_key = self.recaptcha_private_key; + } + + mas_config.matrix.homeserver = self.server_name; + if let Some(public_baseurl) = self.public_baseurl { + mas_config.matrix.endpoint = public_baseurl; + } + + mas_config + } } /// The `database` section of the Synapse configuration. @@ -215,17 +309,6 @@ pub struct EnableableSection { pub enabled: bool, } -#[derive(Clone, Deserialize)] -pub struct OidcProvider { - /// At least for `oidc_config`, if the dict is present but left empty then - /// the config should be ignored, so this field must be optional. - pub issuer: Option, - - /// Required, except for the old `oidc_config` where this is implied to be - /// "oidc". - pub idp_id: Option, -} - fn default_true() -> bool { true } diff --git a/crates/syn2mas/src/synapse_reader/config/oidc.rs b/crates/syn2mas/src/synapse_reader/config/oidc.rs new file mode 100644 index 000000000..5a9321ce2 --- /dev/null +++ b/crates/syn2mas/src/synapse_reader/config/oidc.rs @@ -0,0 +1,347 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +use std::{collections::BTreeMap, str::FromStr as _}; + +use chrono::{DateTime, Utc}; +use mas_config::{ + UpstreamOAuth2ClaimsImports, UpstreamOAuth2DiscoveryMode, UpstreamOAuth2ImportAction, + UpstreamOAuth2PkceMethod, UpstreamOAuth2ResponseMode, UpstreamOAuth2TokenAuthMethod, +}; +use mas_iana::jose::JsonWebSignatureAlg; +use oauth2_types::scope::{OPENID, Scope, ScopeToken}; +use rand::Rng; +use serde::Deserialize; +use tracing::warn; +use ulid::Ulid; +use url::Url; + +#[derive(Clone, Deserialize, Default)] +enum UserMappingProviderModule { + #[default] + #[serde(rename = "synapse.handlers.oidc.JinjaOidcMappingProvider")] + Jinja, + + #[serde(rename = "synapse.handlers.oidc_handler.JinjaOidcMappingProvider")] + JinjaLegacy, + + #[serde(other)] + Other, +} + +#[derive(Clone, Deserialize, Default)] +struct UserMappingProviderConfig { + subject_template: Option, + subject_claim: Option, + localpart_template: Option, + display_name_template: Option, + email_template: Option, + + #[serde(default)] + confirm_localpart: bool, +} + +impl UserMappingProviderConfig { + fn into_mas_config(self) -> UpstreamOAuth2ClaimsImports { + let mut config = UpstreamOAuth2ClaimsImports::default(); + + match (self.subject_claim, self.subject_template) { + (Some(_), Some(subject_template)) => { + warn!( + "Both `subject_claim` and `subject_template` options are set, using `subject_template`." + ); + config.subject.template = Some(subject_template); + } + (None, Some(subject_template)) => { + config.subject.template = Some(subject_template); + } + (Some(subject_claim), None) => { + config.subject.template = Some(format!("{{{{ user.{subject_claim} }}}}")); + } + (None, None) => {} + } + + if let Some(localpart_template) = self.localpart_template { + config.localpart.template = Some(localpart_template); + config.localpart.action = if self.confirm_localpart { + UpstreamOAuth2ImportAction::Suggest + } else { + UpstreamOAuth2ImportAction::Require + }; + } + + if let Some(displayname_template) = self.display_name_template { + config.displayname.template = Some(displayname_template); + config.displayname.action = if self.confirm_localpart { + UpstreamOAuth2ImportAction::Suggest + } else { + UpstreamOAuth2ImportAction::Force + }; + } + + if let Some(email_template) = self.email_template { + config.email.template = Some(email_template); + config.email.action = if self.confirm_localpart { + UpstreamOAuth2ImportAction::Suggest + } else { + UpstreamOAuth2ImportAction::Force + }; + } + + config + } +} + +#[derive(Clone, Deserialize, Default)] +struct UserMappingProvider { + #[serde(default)] + module: UserMappingProviderModule, + #[serde(default)] + config: UserMappingProviderConfig, +} + +#[derive(Clone, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +enum PkceMethod { + #[default] + Auto, + Always, + Never, + #[serde(other)] + Other, +} + +#[derive(Clone, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +enum UserProfileMethod { + #[default] + Auto, + UserinfoEndpoint, + #[serde(other)] + Other, +} + +#[derive(Clone, Deserialize)] +#[expect(clippy::struct_excessive_bools)] +pub struct OidcProvider { + pub issuer: Option, + + /// Required, except for the old `oidc_config` where this is implied to be + /// "oidc". + pub idp_id: Option, + + idp_name: Option, + idp_brand: Option, + + #[serde(default = "default_true")] + discover: bool, + + client_id: Option, + client_secret: Option, + + // Unsupported, we want to shout about it + client_secret_path: Option, + + // Unsupported, we want to shout about it + client_secret_jwt_key: Option, + client_auth_method: Option, + #[serde(default)] + pkce_method: PkceMethod, + // Unsupported, we want to shout about it + id_token_signing_alg_values_supported: Option>, + scopes: Option>, + authorization_endpoint: Option, + token_endpoint: Option, + userinfo_endpoint: Option, + jwks_uri: Option, + #[serde(default)] + skip_verification: bool, + + // Unsupported, we want to shout about it + #[serde(default)] + backchannel_logout_enabled: bool, + + #[serde(default)] + user_profile_method: UserProfileMethod, + + // Unsupported, we want to shout about it + attribute_requirements: Option, + + // Unsupported, we want to shout about it + #[serde(default = "default_true")] + enable_registration: bool, + #[serde(default)] + additional_authorization_parameters: BTreeMap, + #[serde(default)] + user_mapping_provider: UserMappingProvider, +} + +fn default_true() -> bool { + true +} + +impl OidcProvider { + /// Returns true if the two 'required' fields are set. This is used to + /// ignore an empty dict on the `oidc_config` section. + #[must_use] + pub(crate) fn has_required_fields(&self) -> bool { + self.issuer.is_some() && self.client_id.is_some() + } + + /// Map this Synapse OIDC provider config to a MAS upstream provider config. + #[expect(clippy::too_many_lines)] + pub(crate) fn into_mas_config( + self, + rng: &mut impl Rng, + now: DateTime, + ) -> Option { + let client_id = self.client_id?; + + if self.client_secret_path.is_some() { + warn!( + "The `client_secret_path` option is not supported, ignoring. You *will* need to include the secret in the `client_secret` field." + ); + } + + if self.client_secret_jwt_key.is_some() { + warn!("The `client_secret_jwt_key` option is not supported, ignoring."); + } + + if self.attribute_requirements.is_some() { + warn!("The `attribute_requirements` option is not supported, ignoring."); + } + + if self.id_token_signing_alg_values_supported.is_some() { + warn!("The `id_token_signing_alg_values_supported` option is not supported, ignoring."); + } + + if self.backchannel_logout_enabled { + warn!("The `backchannel_logout_enabled` option is not supported, ignoring."); + } + + if !self.enable_registration { + warn!( + "Setting the `enable_registration` option to `false` is not supported, ignoring." + ); + } + + let scope: Scope = match self.scopes { + None => [OPENID].into_iter().collect(), // Synapse defaults to the 'openid' scope + Some(scopes) => scopes + .into_iter() + .filter_map(|scope| match ScopeToken::from_str(&scope) { + Ok(scope) => Some(scope), + Err(err) => { + warn!("OIDC provider scope '{scope}' is invalid: {err}"); + None + } + }) + .collect(), + }; + + let id = Ulid::from_datetime_with_source(now.into(), rng); + + let token_endpoint_auth_method = self.client_auth_method.unwrap_or_else(|| { + // The token auth method defaults to 'none' if no client_secret is set and + // 'client_secret_basic' otherwise + if self.client_secret.is_some() { + UpstreamOAuth2TokenAuthMethod::ClientSecretBasic + } else { + UpstreamOAuth2TokenAuthMethod::None + } + }); + + let discovery_mode = match (self.discover, self.skip_verification) { + (true, false) => UpstreamOAuth2DiscoveryMode::Oidc, + (true, true) => UpstreamOAuth2DiscoveryMode::Insecure, + (false, _) => UpstreamOAuth2DiscoveryMode::Disabled, + }; + + let pkce_method = match self.pkce_method { + PkceMethod::Auto => UpstreamOAuth2PkceMethod::Auto, + PkceMethod::Always => UpstreamOAuth2PkceMethod::Always, + PkceMethod::Never => UpstreamOAuth2PkceMethod::Never, + PkceMethod::Other => { + warn!( + "The `pkce_method` option is not supported, expected 'auto', 'always', or 'never'; assuming 'auto'." + ); + UpstreamOAuth2PkceMethod::default() + } + }; + + // "auto" doesn't mean the same thing depending on whether we request the openid + // scope or not + let has_openid_scope = scope.contains(&OPENID); + let fetch_userinfo = match self.user_profile_method { + UserProfileMethod::Auto => has_openid_scope, + UserProfileMethod::UserinfoEndpoint => true, + UserProfileMethod::Other => { + warn!( + "The `user_profile_method` option is not supported, expected 'auto' or 'userinfo_endpoint'; assuming 'auto'." + ); + has_openid_scope + } + }; + + // Check if there is a `response_mode` set in the additional authorization + // parameters + let mut additional_authorization_parameters = self.additional_authorization_parameters; + let response_mode = if let Some(response_mode) = + additional_authorization_parameters.remove("response_mode") + { + match response_mode.to_ascii_lowercase().as_str() { + "query" => Some(UpstreamOAuth2ResponseMode::Query), + "form_post" => Some(UpstreamOAuth2ResponseMode::FormPost), + _ => { + warn!( + "Invalid `response_mode` in the `additional_authorization_parameters` option, expected 'query' or 'form_post'; ignoring." + ); + None + } + } + } else { + None + }; + + let claims_imports = if matches!( + self.user_mapping_provider.module, + UserMappingProviderModule::Other + ) { + warn!( + "The `user_mapping_provider` module specified is not supported, ignoring. Please adjust the `claims_imports` to match the mapping provider behaviour." + ); + UpstreamOAuth2ClaimsImports::default() + } else { + self.user_mapping_provider.config.into_mas_config() + }; + + Some(mas_config::UpstreamOAuth2Provider { + enabled: true, + id, + synapse_idp_id: self.idp_id, + issuer: self.issuer, + human_name: self.idp_name, + brand_name: self.idp_brand, + client_id, + client_secret: self.client_secret, + token_endpoint_auth_method, + sign_in_with_apple: None, + token_endpoint_auth_signing_alg: None, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + scope: scope.to_string(), + discovery_mode, + pkce_method, + fetch_userinfo, + userinfo_signed_response_alg: None, + authorization_endpoint: self.authorization_endpoint, + userinfo_endpoint: self.userinfo_endpoint, + token_endpoint: self.token_endpoint, + jwks_uri: self.jwks_uri, + response_mode, + claims_imports, + additional_authorization_parameters, + }) + } +} From ef81b3ce4f8c721884a96c9c7bbc2b33d954fe77 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 13:40:14 +0200 Subject: [PATCH 036/189] syn2mas: add a buffered channel for writing threepids --- crates/syn2mas/src/migration.rs | 145 +++++++++++++++++++------------- 1 file changed, 85 insertions(+), 60 deletions(-) diff --git a/crates/syn2mas/src/migration.rs b/crates/syn2mas/src/migration.rs index 6d0420077..f5efaa722 100644 --- a/crates/syn2mas/src/migration.rs +++ b/crates/syn2mas/src/migration.rs @@ -335,77 +335,102 @@ async fn migrate_threepids( ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let mut email_buffer = MasWriteBuffer::new(&mas, MasWriter::write_email_threepids); - let mut unsupported_buffer = MasWriteBuffer::new(&mas, MasWriter::write_unsupported_threepids); - let mut users_stream = pin!(synapse.read_threepids()); + let (tx, mut rx) = tokio::sync::mpsc::channel::(10 * 1024 * 1024); - while let Some(threepid_res) = users_stream.next().await { - let SynapseThreepid { - user_id: synapse_user_id, - medium, - address, - added_at, - } = threepid_res.into_synapse("reading threepid")?; - let created_at: DateTime = added_at.into(); + // create a new RNG seeded from the passed RNG so that we can move it into the + // spawned task + let mut rng = rand_chacha::ChaChaRng::from_rng(rng).expect("failed to seed rng"); + let task = tokio::spawn( + async move { + let mut email_buffer = MasWriteBuffer::new(&mas, MasWriter::write_email_threepids); + let mut unsupported_buffer = + MasWriteBuffer::new(&mas, MasWriter::write_unsupported_threepids); - let username = synapse_user_id - .extract_localpart(&state.server_name) - .into_extract_localpart(synapse_user_id.clone())? - .to_owned(); - let Some(user_infos) = state.users.get(username.as_str()).copied() else { - return Err(Error::MissingUserFromDependentTable { - table: "user_threepids".to_owned(), - user: synapse_user_id, - }); - }; + while let Some(threepid) = rx.recv().await { + let SynapseThreepid { + user_id: synapse_user_id, + medium, + address, + added_at, + } = threepid; + let created_at: DateTime = added_at.into(); - let Some(mas_user_id) = user_infos.mas_user_id else { - progress_counter.increment_skipped(); - continue; - }; + let username = synapse_user_id + .extract_localpart(&state.server_name) + .into_extract_localpart(synapse_user_id.clone())? + .to_owned(); + let Some(user_infos) = state.users.get(username.as_str()).copied() else { + return Err(Error::MissingUserFromDependentTable { + table: "user_threepids".to_owned(), + user: synapse_user_id, + }); + }; + + let Some(mas_user_id) = user_infos.mas_user_id else { + progress_counter.increment_skipped(); + continue; + }; + + if medium == "email" { + email_buffer + .write( + &mut mas, + MasNewEmailThreepid { + user_id: mas_user_id, + user_email_id: Uuid::from(Ulid::from_datetime_with_source( + created_at.into(), + &mut rng, + )), + email: address, + created_at, + }, + ) + .await + .into_mas("writing email")?; + } else { + unsupported_buffer + .write( + &mut mas, + MasNewUnsupportedThreepid { + user_id: mas_user_id, + medium, + address, + created_at, + }, + ) + .await + .into_mas("writing unsupported threepid")?; + } + + progress_counter.increment_migrated(); + } - if medium == "email" { email_buffer - .write( - &mut mas, - MasNewEmailThreepid { - user_id: mas_user_id, - user_email_id: Uuid::from(Ulid::from_datetime_with_source( - created_at.into(), - rng, - )), - email: address, - created_at, - }, - ) + .finish(&mut mas) .await - .into_mas("writing email")?; - } else { + .into_mas("writing email threepids")?; unsupported_buffer - .write( - &mut mas, - MasNewUnsupportedThreepid { - user_id: mas_user_id, - medium, - address, - created_at, - }, - ) + .finish(&mut mas) .await - .into_mas("writing unsupported threepid")?; + .into_mas("writing unsupported threepids")?; + + Ok((mas, state)) } + .instrument(tracing::info_span!("ingest_task")), + ); - progress_counter.increment_migrated(); - } + // In case this has an error, we still want to join the task, so we look at the + // error later + let res = synapse + .read_threepids() + .map_err(|e| e.into_synapse("reading threepids")) + .forward(PollSender::new(tx).sink_map_err(|_| Error::ChannelClosed)) + .inspect_err(|e| tracing::error!(error = e as &dyn std::error::Error)) + .await; - email_buffer - .finish(&mut mas) - .await - .into_mas("writing email threepids")?; - unsupported_buffer - .finish(&mut mas) - .await - .into_mas("writing unsupported threepids")?; + let (mas, state) = task.await.into_join("threepid write task")??; + + res?; info!( "third-party IDs migrated in {:.1}s", From c292da7ac99604498c904a7c73c362c977077c27 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 13:44:52 +0200 Subject: [PATCH 037/189] syn2mas: add a buffered channel for writing external IDs --- crates/syn2mas/src/migration.rs | 132 +++++++++++++++++++------------- 1 file changed, 77 insertions(+), 55 deletions(-) diff --git a/crates/syn2mas/src/migration.rs b/crates/syn2mas/src/migration.rs index f5efaa722..fb2ea5487 100644 --- a/crates/syn2mas/src/migration.rs +++ b/crates/syn2mas/src/migration.rs @@ -440,10 +440,6 @@ async fn migrate_threepids( Ok((mas, state)) } -/// # Parameters -/// -/// - `provider_id_mapping`: mapping from Synapse `auth_provider` ID to UUID of -/// the upstream provider in MAS. #[tracing::instrument(skip_all, level = Level::INFO)] async fn migrate_external_ids( synapse: &mut SynapseReader<'_>, @@ -454,65 +450,91 @@ async fn migrate_external_ids( ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let mut write_buffer = MasWriteBuffer::new(&mas, MasWriter::write_upstream_oauth_links); - let mut extids_stream = pin!(synapse.read_user_external_ids()); + let (tx, mut rx) = tokio::sync::mpsc::channel::(10 * 1024 * 1024); - while let Some(extid_res) = extids_stream.next().await { - let SynapseExternalId { - user_id: synapse_user_id, - auth_provider, - external_id: subject, - } = extid_res.into_synapse("reading external ID")?; - let username = synapse_user_id - .extract_localpart(&state.server_name) - .into_extract_localpart(synapse_user_id.clone())? - .to_owned(); - let Some(user_infos) = state.users.get(username.as_str()).copied() else { - return Err(Error::MissingUserFromDependentTable { - table: "user_external_ids".to_owned(), - user: synapse_user_id, - }); - }; + // create a new RNG seeded from the passed RNG so that we can move it into the + // spawned task + let mut rng = rand_chacha::ChaChaRng::from_rng(rng).expect("failed to seed rng"); + let task = tokio::spawn( + async move { + let mut write_buffer = MasWriteBuffer::new(&mas, MasWriter::write_upstream_oauth_links); - let Some(mas_user_id) = user_infos.mas_user_id else { - progress_counter.increment_skipped(); - continue; - }; + while let Some(extid) = rx.recv().await { + let SynapseExternalId { + user_id: synapse_user_id, + auth_provider, + external_id: subject, + } = extid; + let username = synapse_user_id + .extract_localpart(&state.server_name) + .into_extract_localpart(synapse_user_id.clone())? + .to_owned(); + let Some(user_infos) = state.users.get(username.as_str()).copied() else { + return Err(Error::MissingUserFromDependentTable { + table: "user_external_ids".to_owned(), + user: synapse_user_id, + }); + }; - let Some(&upstream_provider_id) = state.provider_id_mapping.get(&auth_provider) else { - return Err(Error::MissingAuthProviderMapping { - synapse_id: auth_provider, - user: synapse_user_id, - }); - }; + let Some(mas_user_id) = user_infos.mas_user_id else { + progress_counter.increment_skipped(); + continue; + }; - // To save having to store user creation times, extract it from the ULID - // This gives millisecond precision — good enough. - let user_created_ts = Ulid::from(mas_user_id.get()).datetime(); + let Some(&upstream_provider_id) = state.provider_id_mapping.get(&auth_provider) + else { + return Err(Error::MissingAuthProviderMapping { + synapse_id: auth_provider, + user: synapse_user_id, + }); + }; - let link_id: Uuid = Ulid::from_datetime_with_source(user_created_ts, rng).into(); + // To save having to store user creation times, extract it from the ULID + // This gives millisecond precision — good enough. + let user_created_ts = Ulid::from(mas_user_id.get()).datetime(); - write_buffer - .write( - &mut mas, - MasNewUpstreamOauthLink { - link_id, - user_id: mas_user_id, - upstream_provider_id, - subject, - created_at: user_created_ts.into(), - }, - ) - .await - .into_mas("failed to write upstream link")?; + let link_id: Uuid = + Ulid::from_datetime_with_source(user_created_ts, &mut rng).into(); - progress_counter.increment_migrated(); - } + write_buffer + .write( + &mut mas, + MasNewUpstreamOauthLink { + link_id, + user_id: mas_user_id, + upstream_provider_id, + subject, + created_at: user_created_ts.into(), + }, + ) + .await + .into_mas("failed to write upstream link")?; - write_buffer - .finish(&mut mas) - .await - .into_mas("writing upstream links")?; + progress_counter.increment_migrated(); + } + + write_buffer + .finish(&mut mas) + .await + .into_mas("writing upstream links")?; + + Ok((mas, state)) + } + .instrument(tracing::info_span!("ingest_task")), + ); + + // In case this has an error, we still want to join the task, so we look at the + // error later + let res = synapse + .read_user_external_ids() + .map_err(|e| e.into_synapse("reading external ID")) + .forward(PollSender::new(tx).sink_map_err(|_| Error::ChannelClosed)) + .inspect_err(|e| tracing::error!(error = e as &dyn std::error::Error)) + .await; + + let (mas, state) = task.await.into_join("external IDs write task")??; + + res?; info!( "upstream links (external IDs) migrated in {:.1}s", From b21748c2bd6284569faf0f71dd96dbb240c89bc9 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 13:48:47 +0200 Subject: [PATCH 038/189] syn2mas: add a buffered channel for writing refreshable tokens --- crates/syn2mas/src/migration.rs | 202 ++++++++++++++++++-------------- 1 file changed, 115 insertions(+), 87 deletions(-) diff --git a/crates/syn2mas/src/migration.rs b/crates/syn2mas/src/migration.rs index fb2ea5487..3388ff387 100644 --- a/crates/syn2mas/src/migration.rs +++ b/crates/syn2mas/src/migration.rs @@ -11,7 +11,7 @@ //! This module does not implement any of the safety checks that should be run //! *before* the migration. -use std::{pin::pin, time::Instant}; +use std::time::Instant; use chrono::{DateTime, Utc}; use compact_str::CompactString; @@ -847,99 +847,127 @@ async fn migrate_refreshable_token_pairs( ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let mut token_stream = pin!(synapse.read_refreshable_token_pairs()); - let mut access_token_write_buffer = - MasWriteBuffer::new(&mas, MasWriter::write_compat_access_tokens); - let mut refresh_token_write_buffer = - MasWriteBuffer::new(&mas, MasWriter::write_compat_refresh_tokens); + let (tx, mut rx) = tokio::sync::mpsc::channel::(10 * 1024 * 1024); - while let Some(token_res) = token_stream.next().await { - let SynapseRefreshableTokenPair { - user_id: synapse_user_id, - device_id, - access_token, - refresh_token, - valid_until_ms, - last_validated, - } = token_res.into_synapse("reading Synapse refresh token")?; + // create a new RNG seeded from the passed RNG so that we can move it into the + // spawned task + let mut rng = rand_chacha::ChaChaRng::from_rng(rng).expect("failed to seed rng"); + let now = clock.now(); + let task = tokio::spawn( + async move { + let mut access_token_write_buffer = + MasWriteBuffer::new(&mas, MasWriter::write_compat_access_tokens); + let mut refresh_token_write_buffer = + MasWriteBuffer::new(&mas, MasWriter::write_compat_refresh_tokens); - let username = synapse_user_id - .extract_localpart(&state.server_name) - .into_extract_localpart(synapse_user_id.clone())? - .to_owned(); - let Some(user_infos) = state.users.get(username.as_str()).copied() else { - return Err(Error::MissingUserFromDependentTable { - table: "refresh_tokens".to_owned(), - user: synapse_user_id, - }); - }; - - let Some(mas_user_id) = user_infos.mas_user_id else { - progress_counter.increment_skipped(); - continue; - }; - - if user_infos.flags.is_deactivated() - || user_infos.flags.is_guest() - || user_infos.flags.is_appservice() - { - progress_counter.increment_skipped(); - continue; - } - - // It's not always accurate, but last_validated is *often* the creation time of - // the device If we don't have one, then use the current time as a - // fallback. - let created_at = last_validated.map_or_else(|| clock.now(), DateTime::from); - - // Use the existing device_id if this is the second token for a device - let session_id = *state - .devices_to_compat_sessions - .entry((mas_user_id, CompactString::new(&device_id))) - .or_insert_with(|| Uuid::from(Ulid::from_datetime_with_source(created_at.into(), rng))); - - let access_token_id = Uuid::from(Ulid::from_datetime_with_source(created_at.into(), rng)); - let refresh_token_id = Uuid::from(Ulid::from_datetime_with_source(created_at.into(), rng)); - - access_token_write_buffer - .write( - &mut mas, - MasNewCompatAccessToken { - token_id: access_token_id, - session_id, + while let Some(token) = rx.recv().await { + let SynapseRefreshableTokenPair { + user_id: synapse_user_id, + device_id, access_token, - created_at, - expires_at: valid_until_ms.map(DateTime::from), - }, - ) - .await - .into_mas("writing compat access tokens")?; - refresh_token_write_buffer - .write( - &mut mas, - MasNewCompatRefreshToken { - refresh_token_id, - session_id, - access_token_id, refresh_token, - created_at, - }, - ) - .await - .into_mas("writing compat refresh tokens")?; + valid_until_ms, + last_validated, + } = token; - progress_counter.increment_migrated(); - } + let username = synapse_user_id + .extract_localpart(&state.server_name) + .into_extract_localpart(synapse_user_id.clone())? + .to_owned(); + let Some(user_infos) = state.users.get(username.as_str()).copied() else { + return Err(Error::MissingUserFromDependentTable { + table: "refresh_tokens".to_owned(), + user: synapse_user_id, + }); + }; - access_token_write_buffer - .finish(&mut mas) - .await - .into_mas("writing compat access tokens")?; + let Some(mas_user_id) = user_infos.mas_user_id else { + progress_counter.increment_skipped(); + continue; + }; - refresh_token_write_buffer - .finish(&mut mas) - .await - .into_mas("writing compat refresh tokens")?; + if user_infos.flags.is_deactivated() + || user_infos.flags.is_guest() + || user_infos.flags.is_appservice() + { + progress_counter.increment_skipped(); + continue; + } + + // It's not always accurate, but last_validated is *often* the creation time of + // the device If we don't have one, then use the current time as a + // fallback. + let created_at = last_validated.map_or_else(|| now, DateTime::from); + + // Use the existing device_id if this is the second token for a device + let session_id = *state + .devices_to_compat_sessions + .entry((mas_user_id, CompactString::new(&device_id))) + .or_insert_with(|| { + Uuid::from(Ulid::from_datetime_with_source(created_at.into(), &mut rng)) + }); + + let access_token_id = + Uuid::from(Ulid::from_datetime_with_source(created_at.into(), &mut rng)); + let refresh_token_id = + Uuid::from(Ulid::from_datetime_with_source(created_at.into(), &mut rng)); + + access_token_write_buffer + .write( + &mut mas, + MasNewCompatAccessToken { + token_id: access_token_id, + session_id, + access_token, + created_at, + expires_at: valid_until_ms.map(DateTime::from), + }, + ) + .await + .into_mas("writing compat access tokens")?; + refresh_token_write_buffer + .write( + &mut mas, + MasNewCompatRefreshToken { + refresh_token_id, + session_id, + access_token_id, + refresh_token, + created_at, + }, + ) + .await + .into_mas("writing compat refresh tokens")?; + + progress_counter.increment_migrated(); + } + + access_token_write_buffer + .finish(&mut mas) + .await + .into_mas("writing compat access tokens")?; + + refresh_token_write_buffer + .finish(&mut mas) + .await + .into_mas("writing compat refresh tokens")?; + Ok((mas, state)) + } + .instrument(tracing::info_span!("ingest_task")), + ); + + // In case this has an error, we still want to join the task, so we look at the + // error later + let res = synapse + .read_refreshable_token_pairs() + .map_err(|e| e.into_synapse("reading refresh token pairs")) + .forward(PollSender::new(tx).sink_map_err(|_| Error::ChannelClosed)) + .inspect_err(|e| tracing::error!(error = e as &dyn std::error::Error)) + .await; + + let (mas, state) = task.await.into_join("refresh token write task")??; + + res?; info!( "refreshable token pairs migrated in {:.1}s", From b3538219e6343252cdc57d8c3728d43910ed7b72 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 14:14:20 +0200 Subject: [PATCH 039/189] syn2mas: introduce a WriteBatch trait to refactor how we write to MAS --- crates/syn2mas/src/mas_writer/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index fe52e0eed..33fc856e5 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -250,6 +250,13 @@ pub struct MasWriter { write_buffer_finish_checker: FinishChecker, } +trait WriteBatch: Sized { + fn write_batch( + conn: &mut PgConnection, + batch: Vec, + ) -> impl Future>; +} + pub struct MasNewUser { pub user_id: NonNilUuid, pub username: String, From 848847636866c081f236f9739081253644b5c198 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 14:14:44 +0200 Subject: [PATCH 040/189] syn2mas: implement WriteBatch for MasNewUser --- ...85277958b66e4534561686c073e282fafaf2a.json | 20 +++ ...817cb6fe71203b2d3471e838f841b53e688d1.json | 20 --- crates/syn2mas/src/mas_writer/mod.rs | 124 +++++++++--------- 3 files changed, 85 insertions(+), 79 deletions(-) create mode 100644 crates/syn2mas/.sqlx/query-207b880ec2dd484ad05a7138ba485277958b66e4534561686c073e282fafaf2a.json delete mode 100644 crates/syn2mas/.sqlx/query-f2820b3752cf66669551ef90a10817cb6fe71203b2d3471e838f841b53e688d1.json diff --git a/crates/syn2mas/.sqlx/query-207b880ec2dd484ad05a7138ba485277958b66e4534561686c073e282fafaf2a.json b/crates/syn2mas/.sqlx/query-207b880ec2dd484ad05a7138ba485277958b66e4534561686c073e282fafaf2a.json new file mode 100644 index 000000000..79688d807 --- /dev/null +++ b/crates/syn2mas/.sqlx/query-207b880ec2dd484ad05a7138ba485277958b66e4534561686c073e282fafaf2a.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO syn2mas__users (\n user_id, username,\n created_at, locked_at,\n deactivated_at,\n can_request_admin, is_guest)\n SELECT * FROM UNNEST(\n $1::UUID[], $2::TEXT[],\n $3::TIMESTAMP WITH TIME ZONE[], $4::TIMESTAMP WITH TIME ZONE[],\n $5::TIMESTAMP WITH TIME ZONE[],\n $6::BOOL[], $7::BOOL[])\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray", + "TextArray", + "TimestamptzArray", + "TimestamptzArray", + "TimestamptzArray", + "BoolArray", + "BoolArray" + ] + }, + "nullable": [] + }, + "hash": "207b880ec2dd484ad05a7138ba485277958b66e4534561686c073e282fafaf2a" +} diff --git a/crates/syn2mas/.sqlx/query-f2820b3752cf66669551ef90a10817cb6fe71203b2d3471e838f841b53e688d1.json b/crates/syn2mas/.sqlx/query-f2820b3752cf66669551ef90a10817cb6fe71203b2d3471e838f841b53e688d1.json deleted file mode 100644 index 66979a67e..000000000 --- a/crates/syn2mas/.sqlx/query-f2820b3752cf66669551ef90a10817cb6fe71203b2d3471e838f841b53e688d1.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO syn2mas__users (\n user_id, username,\n created_at, locked_at,\n deactivated_at,\n can_request_admin, is_guest)\n SELECT * FROM UNNEST(\n $1::UUID[], $2::TEXT[],\n $3::TIMESTAMP WITH TIME ZONE[], $4::TIMESTAMP WITH TIME ZONE[],\n $5::TIMESTAMP WITH TIME ZONE[],\n $6::BOOL[], $7::BOOL[])\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "UuidArray", - "TextArray", - "TimestamptzArray", - "TimestamptzArray", - "TimestamptzArray", - "BoolArray", - "BoolArray" - ] - }, - "nullable": [] - }, - "hash": "f2820b3752cf66669551ef90a10817cb6fe71203b2d3471e838f841b53e688d1" -} diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index 33fc856e5..cd64650ac 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -270,6 +270,70 @@ pub struct MasNewUser { pub is_guest: bool, } +impl WriteBatch for MasNewUser { + async fn write_batch(conn: &mut PgConnection, batch: Vec) -> Result<(), Error> { + // `UNNEST` is a fast way to do bulk inserts, as it lets us send multiple rows + // in one statement without having to change the statement + // SQL thus altering the query plan. See . + // In the future we could consider using sqlx's support for `PgCopyIn` / the + // `COPY FROM STDIN` statement, which is allegedly the best + // for insert performance, but is less simple to encode. + let mut user_ids: Vec = Vec::with_capacity(batch.len()); + let mut usernames: Vec = Vec::with_capacity(batch.len()); + let mut created_ats: Vec> = Vec::with_capacity(batch.len()); + let mut locked_ats: Vec>> = Vec::with_capacity(batch.len()); + let mut deactivated_ats: Vec>> = Vec::with_capacity(batch.len()); + let mut can_request_admins: Vec = Vec::with_capacity(batch.len()); + let mut is_guests: Vec = Vec::with_capacity(batch.len()); + for MasNewUser { + user_id, + username, + created_at, + locked_at, + deactivated_at, + can_request_admin, + is_guest, + } in batch + { + user_ids.push(user_id.get()); + usernames.push(username); + created_ats.push(created_at); + locked_ats.push(locked_at); + deactivated_ats.push(deactivated_at); + can_request_admins.push(can_request_admin); + is_guests.push(is_guest); + } + + sqlx::query!( + r#" + INSERT INTO syn2mas__users ( + user_id, username, + created_at, locked_at, + deactivated_at, + can_request_admin, is_guest) + SELECT * FROM UNNEST( + $1::UUID[], $2::TEXT[], + $3::TIMESTAMP WITH TIME ZONE[], $4::TIMESTAMP WITH TIME ZONE[], + $5::TIMESTAMP WITH TIME ZONE[], + $6::BOOL[], $7::BOOL[]) + "#, + &user_ids[..], + &usernames[..], + &created_ats[..], + // We need to override the typing for arrays of optionals (sqlx limitation) + &locked_ats[..] as &[Option>], + &deactivated_ats[..] as &[Option>], + &can_request_admins[..], + &is_guests[..], + ) + .execute(&mut *conn) + .await + .into_database("writing users to MAS")?; + + Ok(()) + } +} + pub struct MasNewUserPassword { pub user_password_id: Uuid, pub user_id: NonNilUuid, @@ -643,65 +707,7 @@ impl MasWriter { self.writer_pool .spawn_with_connection(move |conn| { Box::pin(async move { - // `UNNEST` is a fast way to do bulk inserts, as it lets us send multiple rows - // in one statement without having to change the statement - // SQL thus altering the query plan. See . - // In the future we could consider using sqlx's support for `PgCopyIn` / the - // `COPY FROM STDIN` statement, which is allegedly the best - // for insert performance, but is less simple to encode. - let mut user_ids: Vec = Vec::with_capacity(users.len()); - let mut usernames: Vec = Vec::with_capacity(users.len()); - let mut created_ats: Vec> = Vec::with_capacity(users.len()); - let mut locked_ats: Vec>> = - Vec::with_capacity(users.len()); - let mut deactivated_ats: Vec>> = - Vec::with_capacity(users.len()); - let mut can_request_admins: Vec = Vec::with_capacity(users.len()); - let mut is_guests: Vec = Vec::with_capacity(users.len()); - for MasNewUser { - user_id, - username, - created_at, - locked_at, - deactivated_at, - can_request_admin, - is_guest, - } in users - { - user_ids.push(user_id.get()); - usernames.push(username); - created_ats.push(created_at); - locked_ats.push(locked_at); - deactivated_ats.push(deactivated_at); - can_request_admins.push(can_request_admin); - is_guests.push(is_guest); - } - - sqlx::query!( - r#" - INSERT INTO syn2mas__users ( - user_id, username, - created_at, locked_at, - deactivated_at, - can_request_admin, is_guest) - SELECT * FROM UNNEST( - $1::UUID[], $2::TEXT[], - $3::TIMESTAMP WITH TIME ZONE[], $4::TIMESTAMP WITH TIME ZONE[], - $5::TIMESTAMP WITH TIME ZONE[], - $6::BOOL[], $7::BOOL[]) - "#, - &user_ids[..], - &usernames[..], - &created_ats[..], - // We need to override the typing for arrays of optionals (sqlx limitation) - &locked_ats[..] as &[Option>], - &deactivated_ats[..] as &[Option>], - &can_request_admins[..], - &is_guests[..], - ) - .execute(&mut *conn) - .await - .into_database("writing users to MAS")?; + MasNewUser::write_batch(conn, users).await?; Ok(()) }) From 4c081152a910a13e01038692e8b5a9ebaecf60e9 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 14:17:04 +0200 Subject: [PATCH 041/189] syn2mas: implement WriteBatch for MasNewUserPassword --- ...e3db8ff7a686180d71052911879f186ed1c8e.json | 18 +++++ ...7cafe3f85d639452fd0593b2773997dfc7425.json | 18 ----- crates/syn2mas/src/mas_writer/mod.rs | 80 +++++++++++-------- 3 files changed, 64 insertions(+), 52 deletions(-) create mode 100644 crates/syn2mas/.sqlx/query-24f6ce6280dc6675ab1ebdde0c5e3db8ff7a686180d71052911879f186ed1c8e.json delete mode 100644 crates/syn2mas/.sqlx/query-c6c7db1d578efc45b9e8c8bfea47cafe3f85d639452fd0593b2773997dfc7425.json diff --git a/crates/syn2mas/.sqlx/query-24f6ce6280dc6675ab1ebdde0c5e3db8ff7a686180d71052911879f186ed1c8e.json b/crates/syn2mas/.sqlx/query-24f6ce6280dc6675ab1ebdde0c5e3db8ff7a686180d71052911879f186ed1c8e.json new file mode 100644 index 000000000..d736336f2 --- /dev/null +++ b/crates/syn2mas/.sqlx/query-24f6ce6280dc6675ab1ebdde0c5e3db8ff7a686180d71052911879f186ed1c8e.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO syn2mas__user_passwords\n (user_password_id, user_id, hashed_password, created_at, version)\n SELECT * FROM UNNEST($1::UUID[], $2::UUID[], $3::TEXT[], $4::TIMESTAMP WITH TIME ZONE[], $5::INTEGER[])\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray", + "UuidArray", + "TextArray", + "TimestamptzArray", + "Int4Array" + ] + }, + "nullable": [] + }, + "hash": "24f6ce6280dc6675ab1ebdde0c5e3db8ff7a686180d71052911879f186ed1c8e" +} diff --git a/crates/syn2mas/.sqlx/query-c6c7db1d578efc45b9e8c8bfea47cafe3f85d639452fd0593b2773997dfc7425.json b/crates/syn2mas/.sqlx/query-c6c7db1d578efc45b9e8c8bfea47cafe3f85d639452fd0593b2773997dfc7425.json deleted file mode 100644 index efa2c4d24..000000000 --- a/crates/syn2mas/.sqlx/query-c6c7db1d578efc45b9e8c8bfea47cafe3f85d639452fd0593b2773997dfc7425.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO syn2mas__user_passwords\n (user_password_id, user_id, hashed_password, created_at, version)\n SELECT * FROM UNNEST($1::UUID[], $2::UUID[], $3::TEXT[], $4::TIMESTAMP WITH TIME ZONE[], $5::INTEGER[])\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "UuidArray", - "UuidArray", - "TextArray", - "TimestamptzArray", - "Int4Array" - ] - }, - "nullable": [] - }, - "hash": "c6c7db1d578efc45b9e8c8bfea47cafe3f85d639452fd0593b2773997dfc7425" -} diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index cd64650ac..866b615e9 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -341,6 +341,44 @@ pub struct MasNewUserPassword { pub created_at: DateTime, } +impl WriteBatch for MasNewUserPassword { + async fn write_batch(conn: &mut PgConnection, batch: Vec) -> Result<(), Error> { + let mut user_password_ids: Vec = Vec::with_capacity(batch.len()); + let mut user_ids: Vec = Vec::with_capacity(batch.len()); + let mut hashed_passwords: Vec = Vec::with_capacity(batch.len()); + let mut created_ats: Vec> = Vec::with_capacity(batch.len()); + let mut versions: Vec = Vec::with_capacity(batch.len()); + for MasNewUserPassword { + user_password_id, + user_id, + hashed_password, + created_at, + } in batch + { + user_password_ids.push(user_password_id); + user_ids.push(user_id.get()); + hashed_passwords.push(hashed_password); + created_ats.push(created_at); + versions.push(MIGRATED_PASSWORD_VERSION.into()); + } + + sqlx::query!( + r#" + INSERT INTO syn2mas__user_passwords + (user_password_id, user_id, hashed_password, created_at, version) + SELECT * FROM UNNEST($1::UUID[], $2::UUID[], $3::TEXT[], $4::TIMESTAMP WITH TIME ZONE[], $5::INTEGER[]) + "#, + &user_password_ids[..], + &user_ids[..], + &hashed_passwords[..], + &created_ats[..], + &versions[..], + ).execute(&mut *conn).await.into_database("writing users to MAS")?; + + Ok(()) + } +} + pub struct MasNewEmailThreepid { pub user_email_id: Uuid, pub user_id: NonNilUuid, @@ -727,41 +765,15 @@ impl MasWriter { &mut self, passwords: Vec, ) -> BoxFuture<'_, Result<(), Error>> { - self.writer_pool.spawn_with_connection(move |conn| Box::pin(async move { - let mut user_password_ids: Vec = Vec::with_capacity(passwords.len()); - let mut user_ids: Vec = Vec::with_capacity(passwords.len()); - let mut hashed_passwords: Vec = Vec::with_capacity(passwords.len()); - let mut created_ats: Vec> = Vec::with_capacity(passwords.len()); - let mut versions: Vec = Vec::with_capacity(passwords.len()); - for MasNewUserPassword { - user_password_id, - user_id, - hashed_password, - created_at, - } in passwords - { - user_password_ids.push(user_password_id); - user_ids.push(user_id.get()); - hashed_passwords.push(hashed_password); - created_ats.push(created_at); - versions.push(MIGRATED_PASSWORD_VERSION.into()); - } + self.writer_pool + .spawn_with_connection(move |conn| { + Box::pin(async move { + MasNewUserPassword::write_batch(conn, passwords).await?; - sqlx::query!( - r#" - INSERT INTO syn2mas__user_passwords - (user_password_id, user_id, hashed_password, created_at, version) - SELECT * FROM UNNEST($1::UUID[], $2::UUID[], $3::TEXT[], $4::TIMESTAMP WITH TIME ZONE[], $5::INTEGER[]) - "#, - &user_password_ids[..], - &user_ids[..], - &hashed_passwords[..], - &created_ats[..], - &versions[..], - ).execute(&mut *conn).await.into_database("writing users to MAS")?; - - Ok(()) - })).boxed() + Ok(()) + }) + }) + .boxed() } #[tracing::instrument(skip_all, level = Level::DEBUG)] From 86ff994fb39eff60e03b2b8f237f9ffc31a7ff10 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 22 Apr 2025 13:21:07 +0100 Subject: [PATCH 042/189] Remove reference to unsupported aws_ses email transport --- docs/reference/configuration.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 30dbbfca9..81a62f6b1 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -583,10 +583,6 @@ email: # Send emails by calling a local sendmail binary #transport: sendmail #command: /usr/sbin/sendmail - - # Send emails through the AWS SESv2 API - # This uses the AWS SDK, so the usual AWS environment variables are supported - #transport: aws_ses ``` ### `upstream_oauth2` From 2450e2e480f6ba49b9c060aab2640f1d604f6b02 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 14:25:13 +0200 Subject: [PATCH 043/189] syn2mas: implement WriteBatch for MasNewEmailThreepid --- ...035e9a087ff27b06e804464a432d93e5a25f1.json | 17 ++++ ...a853a8a7efccdc20b968d99d8c18deda8dd00.json | 17 ---- crates/syn2mas/src/mas_writer/mod.rs | 80 +++++++++++-------- 3 files changed, 62 insertions(+), 52 deletions(-) create mode 100644 crates/syn2mas/.sqlx/query-08ad2855f0baaaed9d6af23c8bf035e9a087ff27b06e804464a432d93e5a25f1.json delete mode 100644 crates/syn2mas/.sqlx/query-dfbd462f7874d3dae551f2a0328a853a8a7efccdc20b968d99d8c18deda8dd00.json diff --git a/crates/syn2mas/.sqlx/query-08ad2855f0baaaed9d6af23c8bf035e9a087ff27b06e804464a432d93e5a25f1.json b/crates/syn2mas/.sqlx/query-08ad2855f0baaaed9d6af23c8bf035e9a087ff27b06e804464a432d93e5a25f1.json new file mode 100644 index 000000000..545389cb6 --- /dev/null +++ b/crates/syn2mas/.sqlx/query-08ad2855f0baaaed9d6af23c8bf035e9a087ff27b06e804464a432d93e5a25f1.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO syn2mas__user_emails\n (user_email_id, user_id, email, created_at, confirmed_at)\n SELECT * FROM UNNEST($1::UUID[], $2::UUID[], $3::TEXT[], $4::TIMESTAMP WITH TIME ZONE[], $4::TIMESTAMP WITH TIME ZONE[])\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray", + "UuidArray", + "TextArray", + "TimestamptzArray" + ] + }, + "nullable": [] + }, + "hash": "08ad2855f0baaaed9d6af23c8bf035e9a087ff27b06e804464a432d93e5a25f1" +} diff --git a/crates/syn2mas/.sqlx/query-dfbd462f7874d3dae551f2a0328a853a8a7efccdc20b968d99d8c18deda8dd00.json b/crates/syn2mas/.sqlx/query-dfbd462f7874d3dae551f2a0328a853a8a7efccdc20b968d99d8c18deda8dd00.json deleted file mode 100644 index cf89130f9..000000000 --- a/crates/syn2mas/.sqlx/query-dfbd462f7874d3dae551f2a0328a853a8a7efccdc20b968d99d8c18deda8dd00.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO syn2mas__user_emails\n (user_email_id, user_id, email, created_at, confirmed_at)\n SELECT * FROM UNNEST($1::UUID[], $2::UUID[], $3::TEXT[], $4::TIMESTAMP WITH TIME ZONE[], $4::TIMESTAMP WITH TIME ZONE[])\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "UuidArray", - "UuidArray", - "TextArray", - "TimestamptzArray" - ] - }, - "nullable": [] - }, - "hash": "dfbd462f7874d3dae551f2a0328a853a8a7efccdc20b968d99d8c18deda8dd00" -} diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index 866b615e9..fe2391ae6 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -386,6 +386,44 @@ pub struct MasNewEmailThreepid { pub created_at: DateTime, } +impl WriteBatch for MasNewEmailThreepid { + async fn write_batch(conn: &mut PgConnection, batch: Vec) -> Result<(), Error> { + let mut user_email_ids: Vec = Vec::with_capacity(batch.len()); + let mut user_ids: Vec = Vec::with_capacity(batch.len()); + let mut emails: Vec = Vec::with_capacity(batch.len()); + let mut created_ats: Vec> = Vec::with_capacity(batch.len()); + + for MasNewEmailThreepid { + user_email_id, + user_id, + email, + created_at, + } in batch + { + user_email_ids.push(user_email_id); + user_ids.push(user_id.get()); + emails.push(email); + created_ats.push(created_at); + } + + // `confirmed_at` is going to get removed in a future MAS release, + // so just populate with `created_at` + sqlx::query!( + r#" + INSERT INTO syn2mas__user_emails + (user_email_id, user_id, email, created_at, confirmed_at) + SELECT * FROM UNNEST($1::UUID[], $2::UUID[], $3::TEXT[], $4::TIMESTAMP WITH TIME ZONE[], $4::TIMESTAMP WITH TIME ZONE[]) + "#, + &user_email_ids[..], + &user_ids[..], + &emails[..], + &created_ats[..], + ).execute(&mut *conn).await.into_database("writing emails to MAS")?; + + Ok(()) + } +} + pub struct MasNewUnsupportedThreepid { pub user_id: NonNilUuid, pub medium: String, @@ -781,43 +819,15 @@ impl MasWriter { &mut self, threepids: Vec, ) -> BoxFuture<'_, Result<(), Error>> { - self.writer_pool.spawn_with_connection(move |conn| { - Box::pin(async move { - let mut user_email_ids: Vec = Vec::with_capacity(threepids.len()); - let mut user_ids: Vec = Vec::with_capacity(threepids.len()); - let mut emails: Vec = Vec::with_capacity(threepids.len()); - let mut created_ats: Vec> = Vec::with_capacity(threepids.len()); + self.writer_pool + .spawn_with_connection(move |conn| { + Box::pin(async move { + MasNewEmailThreepid::write_batch(conn, threepids).await?; - for MasNewEmailThreepid { - user_email_id, - user_id, - email, - created_at, - } in threepids - { - user_email_ids.push(user_email_id); - user_ids.push(user_id.get()); - emails.push(email); - created_ats.push(created_at); - } - - // `confirmed_at` is going to get removed in a future MAS release, - // so just populate with `created_at` - sqlx::query!( - r#" - INSERT INTO syn2mas__user_emails - (user_email_id, user_id, email, created_at, confirmed_at) - SELECT * FROM UNNEST($1::UUID[], $2::UUID[], $3::TEXT[], $4::TIMESTAMP WITH TIME ZONE[], $4::TIMESTAMP WITH TIME ZONE[]) - "#, - &user_email_ids[..], - &user_ids[..], - &emails[..], - &created_ats[..], - ).execute(&mut *conn).await.into_database("writing emails to MAS")?; - - Ok(()) + Ok(()) + }) }) - }).boxed() + .boxed() } #[tracing::instrument(skip_all, level = Level::DEBUG)] From 028a993dc6b309c1867e38942e6d30263b265371 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 14:30:51 +0200 Subject: [PATCH 044/189] syn2mas: implement WriteBatch for MasNewUnsupportedThreepid --- ...a9cd62ac3c9e58155882858c6056e2ef6c30d.json | 17 ++++ ...b5b37ab50db3505712c35610b822cda322b5b.json | 17 ---- crates/syn2mas/src/mas_writer/mod.rs | 79 +++++++++++-------- 3 files changed, 63 insertions(+), 50 deletions(-) create mode 100644 crates/syn2mas/.sqlx/query-204cf4811150a7fdeafa9373647a9cd62ac3c9e58155882858c6056e2ef6c30d.json delete mode 100644 crates/syn2mas/.sqlx/query-b11590549fdd4cdcd36c937a353b5b37ab50db3505712c35610b822cda322b5b.json diff --git a/crates/syn2mas/.sqlx/query-204cf4811150a7fdeafa9373647a9cd62ac3c9e58155882858c6056e2ef6c30d.json b/crates/syn2mas/.sqlx/query-204cf4811150a7fdeafa9373647a9cd62ac3c9e58155882858c6056e2ef6c30d.json new file mode 100644 index 000000000..464dd9007 --- /dev/null +++ b/crates/syn2mas/.sqlx/query-204cf4811150a7fdeafa9373647a9cd62ac3c9e58155882858c6056e2ef6c30d.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO syn2mas__user_unsupported_third_party_ids\n (user_id, medium, address, created_at)\n SELECT * FROM UNNEST($1::UUID[], $2::TEXT[], $3::TEXT[], $4::TIMESTAMP WITH TIME ZONE[])\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray", + "TextArray", + "TextArray", + "TimestamptzArray" + ] + }, + "nullable": [] + }, + "hash": "204cf4811150a7fdeafa9373647a9cd62ac3c9e58155882858c6056e2ef6c30d" +} diff --git a/crates/syn2mas/.sqlx/query-b11590549fdd4cdcd36c937a353b5b37ab50db3505712c35610b822cda322b5b.json b/crates/syn2mas/.sqlx/query-b11590549fdd4cdcd36c937a353b5b37ab50db3505712c35610b822cda322b5b.json deleted file mode 100644 index b44dfc605..000000000 --- a/crates/syn2mas/.sqlx/query-b11590549fdd4cdcd36c937a353b5b37ab50db3505712c35610b822cda322b5b.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO syn2mas__user_unsupported_third_party_ids\n (user_id, medium, address, created_at)\n SELECT * FROM UNNEST($1::UUID[], $2::TEXT[], $3::TEXT[], $4::TIMESTAMP WITH TIME ZONE[])\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "UuidArray", - "TextArray", - "TextArray", - "TimestamptzArray" - ] - }, - "nullable": [] - }, - "hash": "b11590549fdd4cdcd36c937a353b5b37ab50db3505712c35610b822cda322b5b" -} diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index fe2391ae6..cbb4ad3b0 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -431,6 +431,45 @@ pub struct MasNewUnsupportedThreepid { pub created_at: DateTime, } +impl WriteBatch for MasNewUnsupportedThreepid { + async fn write_batch(conn: &mut PgConnection, batch: Vec) -> Result<(), Error> { + let mut user_ids: Vec = Vec::with_capacity(batch.len()); + let mut mediums: Vec = Vec::with_capacity(batch.len()); + let mut addresses: Vec = Vec::with_capacity(batch.len()); + let mut created_ats: Vec> = Vec::with_capacity(batch.len()); + + for MasNewUnsupportedThreepid { + user_id, + medium, + address, + created_at, + } in batch + { + user_ids.push(user_id.get()); + mediums.push(medium); + addresses.push(address); + created_ats.push(created_at); + } + + sqlx::query!( + r#" + INSERT INTO syn2mas__user_unsupported_third_party_ids + (user_id, medium, address, created_at) + SELECT * FROM UNNEST($1::UUID[], $2::TEXT[], $3::TEXT[], $4::TIMESTAMP WITH TIME ZONE[]) + "#, + &user_ids[..], + &mediums[..], + &addresses[..], + &created_ats[..], + ) + .execute(&mut *conn) + .await + .into_database("writing unsupported threepids to MAS")?; + + Ok(()) + } +} + pub struct MasNewUpstreamOauthLink { pub link_id: Uuid, pub user_id: NonNilUuid, @@ -835,41 +874,15 @@ impl MasWriter { &mut self, threepids: Vec, ) -> BoxFuture<'_, Result<(), Error>> { - self.writer_pool.spawn_with_connection(move |conn| { - Box::pin(async move { - let mut user_ids: Vec = Vec::with_capacity(threepids.len()); - let mut mediums: Vec = Vec::with_capacity(threepids.len()); - let mut addresses: Vec = Vec::with_capacity(threepids.len()); - let mut created_ats: Vec> = Vec::with_capacity(threepids.len()); + self.writer_pool + .spawn_with_connection(move |conn| { + Box::pin(async move { + MasNewUnsupportedThreepid::write_batch(conn, threepids).await?; - for MasNewUnsupportedThreepid { - user_id, - medium, - address, - created_at, - } in threepids - { - user_ids.push(user_id.get()); - mediums.push(medium); - addresses.push(address); - created_ats.push(created_at); - } - - sqlx::query!( - r#" - INSERT INTO syn2mas__user_unsupported_third_party_ids - (user_id, medium, address, created_at) - SELECT * FROM UNNEST($1::UUID[], $2::TEXT[], $3::TEXT[], $4::TIMESTAMP WITH TIME ZONE[]) - "#, - &user_ids[..], - &mediums[..], - &addresses[..], - &created_ats[..], - ).execute(&mut *conn).await.into_database("writing unsupported threepids to MAS")?; - - Ok(()) + Ok(()) + }) }) - }).boxed() + .boxed() } #[tracing::instrument(skip_all, level = Level::DEBUG)] From 07536cbd3b5cb8b9d9be4b43216b61773712edfa Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 14:33:16 +0200 Subject: [PATCH 045/189] syn2mas: implement WriteBatch for MasNewUpstreamOauthLink --- ...110039b9a4a0425fd566e401f56ea215de0dd.json | 18 ++++ ...096c848ae87c43b6430246ef3b6a1dc6a7a32.json | 18 ---- crates/syn2mas/src/mas_writer/mod.rs | 84 +++++++++++-------- 3 files changed, 65 insertions(+), 55 deletions(-) create mode 100644 crates/syn2mas/.sqlx/query-026adeffc646b41ebc096bb874d110039b9a4a0425fd566e401f56ea215de0dd.json delete mode 100644 crates/syn2mas/.sqlx/query-d79fd99ebed9033711f96113005096c848ae87c43b6430246ef3b6a1dc6a7a32.json diff --git a/crates/syn2mas/.sqlx/query-026adeffc646b41ebc096bb874d110039b9a4a0425fd566e401f56ea215de0dd.json b/crates/syn2mas/.sqlx/query-026adeffc646b41ebc096bb874d110039b9a4a0425fd566e401f56ea215de0dd.json new file mode 100644 index 000000000..fa5f442ed --- /dev/null +++ b/crates/syn2mas/.sqlx/query-026adeffc646b41ebc096bb874d110039b9a4a0425fd566e401f56ea215de0dd.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO syn2mas__upstream_oauth_links\n (upstream_oauth_link_id, user_id, upstream_oauth_provider_id, subject, created_at)\n SELECT * FROM UNNEST($1::UUID[], $2::UUID[], $3::UUID[], $4::TEXT[], $5::TIMESTAMP WITH TIME ZONE[])\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray", + "UuidArray", + "UuidArray", + "TextArray", + "TimestamptzArray" + ] + }, + "nullable": [] + }, + "hash": "026adeffc646b41ebc096bb874d110039b9a4a0425fd566e401f56ea215de0dd" +} diff --git a/crates/syn2mas/.sqlx/query-d79fd99ebed9033711f96113005096c848ae87c43b6430246ef3b6a1dc6a7a32.json b/crates/syn2mas/.sqlx/query-d79fd99ebed9033711f96113005096c848ae87c43b6430246ef3b6a1dc6a7a32.json deleted file mode 100644 index f6ac32781..000000000 --- a/crates/syn2mas/.sqlx/query-d79fd99ebed9033711f96113005096c848ae87c43b6430246ef3b6a1dc6a7a32.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO syn2mas__upstream_oauth_links\n (upstream_oauth_link_id, user_id, upstream_oauth_provider_id, subject, created_at)\n SELECT * FROM UNNEST($1::UUID[], $2::UUID[], $3::UUID[], $4::TEXT[], $5::TIMESTAMP WITH TIME ZONE[])\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "UuidArray", - "UuidArray", - "UuidArray", - "TextArray", - "TimestamptzArray" - ] - }, - "nullable": [] - }, - "hash": "d79fd99ebed9033711f96113005096c848ae87c43b6430246ef3b6a1dc6a7a32" -} diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index cbb4ad3b0..0f7ccf715 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -478,6 +478,46 @@ pub struct MasNewUpstreamOauthLink { pub created_at: DateTime, } +impl WriteBatch for MasNewUpstreamOauthLink { + async fn write_batch(conn: &mut PgConnection, batch: Vec) -> Result<(), Error> { + let mut link_ids: Vec = Vec::with_capacity(batch.len()); + let mut user_ids: Vec = Vec::with_capacity(batch.len()); + let mut upstream_provider_ids: Vec = Vec::with_capacity(batch.len()); + let mut subjects: Vec = Vec::with_capacity(batch.len()); + let mut created_ats: Vec> = Vec::with_capacity(batch.len()); + + for MasNewUpstreamOauthLink { + link_id, + user_id, + upstream_provider_id, + subject, + created_at, + } in batch + { + link_ids.push(link_id); + user_ids.push(user_id.get()); + upstream_provider_ids.push(upstream_provider_id); + subjects.push(subject); + created_ats.push(created_at); + } + + sqlx::query!( + r#" + INSERT INTO syn2mas__upstream_oauth_links + (upstream_oauth_link_id, user_id, upstream_oauth_provider_id, subject, created_at) + SELECT * FROM UNNEST($1::UUID[], $2::UUID[], $3::UUID[], $4::TEXT[], $5::TIMESTAMP WITH TIME ZONE[]) + "#, + &link_ids[..], + &user_ids[..], + &upstream_provider_ids[..], + &subjects[..], + &created_ats[..], + ).execute(&mut *conn).await.into_database("writing unsupported threepids to MAS")?; + + Ok(()) + } +} + pub struct MasNewCompatSession { pub session_id: Uuid, pub user_id: NonNilUuid, @@ -890,45 +930,15 @@ impl MasWriter { &mut self, links: Vec, ) -> BoxFuture<'_, Result<(), Error>> { - self.writer_pool.spawn_with_connection(move |conn| { - Box::pin(async move { - let mut link_ids: Vec = Vec::with_capacity(links.len()); - let mut user_ids: Vec = Vec::with_capacity(links.len()); - let mut upstream_provider_ids: Vec = Vec::with_capacity(links.len()); - let mut subjects: Vec = Vec::with_capacity(links.len()); - let mut created_ats: Vec> = Vec::with_capacity(links.len()); + self.writer_pool + .spawn_with_connection(move |conn| { + Box::pin(async move { + MasNewUpstreamOauthLink::write_batch(conn, links).await?; - for MasNewUpstreamOauthLink { - link_id, - user_id, - upstream_provider_id, - subject, - created_at, - } in links - { - link_ids.push(link_id); - user_ids.push(user_id.get()); - upstream_provider_ids.push(upstream_provider_id); - subjects.push(subject); - created_ats.push(created_at); - } - - sqlx::query!( - r#" - INSERT INTO syn2mas__upstream_oauth_links - (upstream_oauth_link_id, user_id, upstream_oauth_provider_id, subject, created_at) - SELECT * FROM UNNEST($1::UUID[], $2::UUID[], $3::UUID[], $4::TEXT[], $5::TIMESTAMP WITH TIME ZONE[]) - "#, - &link_ids[..], - &user_ids[..], - &upstream_provider_ids[..], - &subjects[..], - &created_ats[..], - ).execute(&mut *conn).await.into_database("writing unsupported threepids to MAS")?; - - Ok(()) + Ok(()) + }) }) - }).boxed() + .boxed() } #[tracing::instrument(skip_all, level = Level::DEBUG)] From 86000613ac2f0a578cdf1dd99b6ee51104f133a5 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 14:37:32 +0200 Subject: [PATCH 046/189] syn2mas: implement WriteBatch for MasNewCompatSession --- ...233e5c9aabfdae1f0ee9b77c909b2bb2f3e25.json | 22 +++ ...e36f4ef03e1224a0a89a921e5a3d398a5d35c.json | 22 --- crates/syn2mas/src/mas_writer/mod.rs | 134 +++++++++--------- 3 files changed, 92 insertions(+), 86 deletions(-) create mode 100644 crates/syn2mas/.sqlx/query-09db58b250c20ab9d1701653165233e5c9aabfdae1f0ee9b77c909b2bb2f3e25.json delete mode 100644 crates/syn2mas/.sqlx/query-396c97dbfbc932c73301daa7376e36f4ef03e1224a0a89a921e5a3d398a5d35c.json diff --git a/crates/syn2mas/.sqlx/query-09db58b250c20ab9d1701653165233e5c9aabfdae1f0ee9b77c909b2bb2f3e25.json b/crates/syn2mas/.sqlx/query-09db58b250c20ab9d1701653165233e5c9aabfdae1f0ee9b77c909b2bb2f3e25.json new file mode 100644 index 000000000..97e8a07a0 --- /dev/null +++ b/crates/syn2mas/.sqlx/query-09db58b250c20ab9d1701653165233e5c9aabfdae1f0ee9b77c909b2bb2f3e25.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO syn2mas__compat_sessions (\n compat_session_id, user_id,\n device_id, human_name,\n created_at, is_synapse_admin,\n last_active_at, last_active_ip,\n user_agent)\n SELECT * FROM UNNEST(\n $1::UUID[], $2::UUID[],\n $3::TEXT[], $4::TEXT[],\n $5::TIMESTAMP WITH TIME ZONE[], $6::BOOLEAN[],\n $7::TIMESTAMP WITH TIME ZONE[], $8::INET[],\n $9::TEXT[])\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray", + "UuidArray", + "TextArray", + "TextArray", + "TimestamptzArray", + "BoolArray", + "TimestamptzArray", + "InetArray", + "TextArray" + ] + }, + "nullable": [] + }, + "hash": "09db58b250c20ab9d1701653165233e5c9aabfdae1f0ee9b77c909b2bb2f3e25" +} diff --git a/crates/syn2mas/.sqlx/query-396c97dbfbc932c73301daa7376e36f4ef03e1224a0a89a921e5a3d398a5d35c.json b/crates/syn2mas/.sqlx/query-396c97dbfbc932c73301daa7376e36f4ef03e1224a0a89a921e5a3d398a5d35c.json deleted file mode 100644 index 521e4facd..000000000 --- a/crates/syn2mas/.sqlx/query-396c97dbfbc932c73301daa7376e36f4ef03e1224a0a89a921e5a3d398a5d35c.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO syn2mas__compat_sessions (\n compat_session_id, user_id,\n device_id, human_name,\n created_at, is_synapse_admin,\n last_active_at, last_active_ip,\n user_agent)\n SELECT * FROM UNNEST(\n $1::UUID[], $2::UUID[],\n $3::TEXT[], $4::TEXT[],\n $5::TIMESTAMP WITH TIME ZONE[], $6::BOOLEAN[],\n $7::TIMESTAMP WITH TIME ZONE[], $8::INET[],\n $9::TEXT[])\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "UuidArray", - "UuidArray", - "TextArray", - "TextArray", - "TimestamptzArray", - "BoolArray", - "TimestamptzArray", - "InetArray", - "TextArray" - ] - }, - "nullable": [] - }, - "hash": "396c97dbfbc932c73301daa7376e36f4ef03e1224a0a89a921e5a3d398a5d35c" -} diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index 0f7ccf715..b0b8250c1 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -530,6 +530,75 @@ pub struct MasNewCompatSession { pub user_agent: Option, } +impl WriteBatch for MasNewCompatSession { + async fn write_batch(conn: &mut PgConnection, batch: Vec) -> Result<(), Error> { + let mut session_ids: Vec = Vec::with_capacity(batch.len()); + let mut user_ids: Vec = Vec::with_capacity(batch.len()); + let mut device_ids: Vec> = Vec::with_capacity(batch.len()); + let mut human_names: Vec> = Vec::with_capacity(batch.len()); + let mut created_ats: Vec> = Vec::with_capacity(batch.len()); + let mut is_synapse_admins: Vec = Vec::with_capacity(batch.len()); + let mut last_active_ats: Vec>> = Vec::with_capacity(batch.len()); + let mut last_active_ips: Vec> = Vec::with_capacity(batch.len()); + let mut user_agents: Vec> = Vec::with_capacity(batch.len()); + + for MasNewCompatSession { + session_id, + user_id, + device_id, + human_name, + created_at, + is_synapse_admin, + last_active_at, + last_active_ip, + user_agent, + } in batch + { + session_ids.push(session_id); + user_ids.push(user_id.get()); + device_ids.push(device_id); + human_names.push(human_name); + created_ats.push(created_at); + is_synapse_admins.push(is_synapse_admin); + last_active_ats.push(last_active_at); + last_active_ips.push(last_active_ip); + user_agents.push(user_agent); + } + + sqlx::query!( + r#" + INSERT INTO syn2mas__compat_sessions ( + compat_session_id, user_id, + device_id, human_name, + created_at, is_synapse_admin, + last_active_at, last_active_ip, + user_agent) + SELECT * FROM UNNEST( + $1::UUID[], $2::UUID[], + $3::TEXT[], $4::TEXT[], + $5::TIMESTAMP WITH TIME ZONE[], $6::BOOLEAN[], + $7::TIMESTAMP WITH TIME ZONE[], $8::INET[], + $9::TEXT[]) + "#, + &session_ids[..], + &user_ids[..], + &device_ids[..] as &[Option], + &human_names[..] as &[Option], + &created_ats[..], + &is_synapse_admins[..], + // We need to override the typing for arrays of optionals (sqlx limitation) + &last_active_ats[..] as &[Option>], + &last_active_ips[..] as &[Option], + &user_agents[..] as &[Option], + ) + .execute(&mut *conn) + .await + .into_database("writing compat sessions to MAS")?; + + Ok(()) + } +} + pub struct MasNewCompatAccessToken { pub token_id: Uuid, pub session_id: Uuid, @@ -949,70 +1018,7 @@ impl MasWriter { self.writer_pool .spawn_with_connection(move |conn| { Box::pin(async move { - let mut session_ids: Vec = Vec::with_capacity(sessions.len()); - let mut user_ids: Vec = Vec::with_capacity(sessions.len()); - let mut device_ids: Vec> = Vec::with_capacity(sessions.len()); - let mut human_names: Vec> = Vec::with_capacity(sessions.len()); - let mut created_ats: Vec> = Vec::with_capacity(sessions.len()); - let mut is_synapse_admins: Vec = Vec::with_capacity(sessions.len()); - let mut last_active_ats: Vec>> = - Vec::with_capacity(sessions.len()); - let mut last_active_ips: Vec> = - Vec::with_capacity(sessions.len()); - let mut user_agents: Vec> = Vec::with_capacity(sessions.len()); - - for MasNewCompatSession { - session_id, - user_id, - device_id, - human_name, - created_at, - is_synapse_admin, - last_active_at, - last_active_ip, - user_agent, - } in sessions - { - session_ids.push(session_id); - user_ids.push(user_id.get()); - device_ids.push(device_id); - human_names.push(human_name); - created_ats.push(created_at); - is_synapse_admins.push(is_synapse_admin); - last_active_ats.push(last_active_at); - last_active_ips.push(last_active_ip); - user_agents.push(user_agent); - } - - sqlx::query!( - r#" - INSERT INTO syn2mas__compat_sessions ( - compat_session_id, user_id, - device_id, human_name, - created_at, is_synapse_admin, - last_active_at, last_active_ip, - user_agent) - SELECT * FROM UNNEST( - $1::UUID[], $2::UUID[], - $3::TEXT[], $4::TEXT[], - $5::TIMESTAMP WITH TIME ZONE[], $6::BOOLEAN[], - $7::TIMESTAMP WITH TIME ZONE[], $8::INET[], - $9::TEXT[]) - "#, - &session_ids[..], - &user_ids[..], - &device_ids[..] as &[Option], - &human_names[..] as &[Option], - &created_ats[..], - &is_synapse_admins[..], - // We need to override the typing for arrays of optionals (sqlx limitation) - &last_active_ats[..] as &[Option>], - &last_active_ips[..] as &[Option], - &user_agents[..] as &[Option], - ) - .execute(&mut *conn) - .await - .into_database("writing compat sessions to MAS")?; + MasNewCompatSession::write_batch(conn, sessions).await?; Ok(()) }) From aa2e2825fe87649a89cbb6edb245d334eac44662 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 14:39:59 +0200 Subject: [PATCH 047/189] syn2mas: implement WriteBatch for MasNewCompatAccessToken --- ...3c67bf00fd3e411f769b9f25dec27428489ed.json | 18 ++++ ...ad976c3a0ff238046872b17d3f412beda62c7.json | 18 ---- crates/syn2mas/src/mas_writer/mod.rs | 101 ++++++++++-------- 3 files changed, 72 insertions(+), 65 deletions(-) create mode 100644 crates/syn2mas/.sqlx/query-86b2b02fbb6350100d794e4d0fa3c67bf00fd3e411f769b9f25dec27428489ed.json delete mode 100644 crates/syn2mas/.sqlx/query-d55adc78a0c222e19688e6ac810ad976c3a0ff238046872b17d3f412beda62c7.json diff --git a/crates/syn2mas/.sqlx/query-86b2b02fbb6350100d794e4d0fa3c67bf00fd3e411f769b9f25dec27428489ed.json b/crates/syn2mas/.sqlx/query-86b2b02fbb6350100d794e4d0fa3c67bf00fd3e411f769b9f25dec27428489ed.json new file mode 100644 index 000000000..dd8a8e306 --- /dev/null +++ b/crates/syn2mas/.sqlx/query-86b2b02fbb6350100d794e4d0fa3c67bf00fd3e411f769b9f25dec27428489ed.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO syn2mas__compat_access_tokens (\n compat_access_token_id,\n compat_session_id,\n access_token,\n created_at,\n expires_at)\n SELECT * FROM UNNEST(\n $1::UUID[],\n $2::UUID[],\n $3::TEXT[],\n $4::TIMESTAMP WITH TIME ZONE[],\n $5::TIMESTAMP WITH TIME ZONE[])\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray", + "UuidArray", + "TextArray", + "TimestamptzArray", + "TimestamptzArray" + ] + }, + "nullable": [] + }, + "hash": "86b2b02fbb6350100d794e4d0fa3c67bf00fd3e411f769b9f25dec27428489ed" +} diff --git a/crates/syn2mas/.sqlx/query-d55adc78a0c222e19688e6ac810ad976c3a0ff238046872b17d3f412beda62c7.json b/crates/syn2mas/.sqlx/query-d55adc78a0c222e19688e6ac810ad976c3a0ff238046872b17d3f412beda62c7.json deleted file mode 100644 index eb406d23b..000000000 --- a/crates/syn2mas/.sqlx/query-d55adc78a0c222e19688e6ac810ad976c3a0ff238046872b17d3f412beda62c7.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO syn2mas__compat_access_tokens (\n compat_access_token_id,\n compat_session_id,\n access_token,\n created_at,\n expires_at)\n SELECT * FROM UNNEST(\n $1::UUID[],\n $2::UUID[],\n $3::TEXT[],\n $4::TIMESTAMP WITH TIME ZONE[],\n $5::TIMESTAMP WITH TIME ZONE[])\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "UuidArray", - "UuidArray", - "TextArray", - "TimestamptzArray", - "TimestamptzArray" - ] - }, - "nullable": [] - }, - "hash": "d55adc78a0c222e19688e6ac810ad976c3a0ff238046872b17d3f412beda62c7" -} diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index b0b8250c1..182a4795b 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -607,6 +607,59 @@ pub struct MasNewCompatAccessToken { pub expires_at: Option>, } +impl WriteBatch for MasNewCompatAccessToken { + async fn write_batch(conn: &mut PgConnection, batch: Vec) -> Result<(), Error> { + let mut token_ids: Vec = Vec::with_capacity(batch.len()); + let mut session_ids: Vec = Vec::with_capacity(batch.len()); + let mut access_tokens: Vec = Vec::with_capacity(batch.len()); + let mut created_ats: Vec> = Vec::with_capacity(batch.len()); + let mut expires_ats: Vec>> = Vec::with_capacity(batch.len()); + + for MasNewCompatAccessToken { + token_id, + session_id, + access_token, + created_at, + expires_at, + } in batch + { + token_ids.push(token_id); + session_ids.push(session_id); + access_tokens.push(access_token); + created_ats.push(created_at); + expires_ats.push(expires_at); + } + + sqlx::query!( + r#" + INSERT INTO syn2mas__compat_access_tokens ( + compat_access_token_id, + compat_session_id, + access_token, + created_at, + expires_at) + SELECT * FROM UNNEST( + $1::UUID[], + $2::UUID[], + $3::TEXT[], + $4::TIMESTAMP WITH TIME ZONE[], + $5::TIMESTAMP WITH TIME ZONE[]) + "#, + &token_ids[..], + &session_ids[..], + &access_tokens[..], + &created_ats[..], + // We need to override the typing for arrays of optionals (sqlx limitation) + &expires_ats[..] as &[Option>], + ) + .execute(&mut *conn) + .await + .into_database("writing compat access tokens to MAS")?; + + Ok(()) + } +} + pub struct MasNewCompatRefreshToken { pub refresh_token_id: Uuid, pub session_id: Uuid, @@ -1034,53 +1087,7 @@ impl MasWriter { self.writer_pool .spawn_with_connection(move |conn| { Box::pin(async move { - let mut token_ids: Vec = Vec::with_capacity(tokens.len()); - let mut session_ids: Vec = Vec::with_capacity(tokens.len()); - let mut access_tokens: Vec = Vec::with_capacity(tokens.len()); - let mut created_ats: Vec> = Vec::with_capacity(tokens.len()); - let mut expires_ats: Vec>> = - Vec::with_capacity(tokens.len()); - - for MasNewCompatAccessToken { - token_id, - session_id, - access_token, - created_at, - expires_at, - } in tokens - { - token_ids.push(token_id); - session_ids.push(session_id); - access_tokens.push(access_token); - created_ats.push(created_at); - expires_ats.push(expires_at); - } - - sqlx::query!( - r#" - INSERT INTO syn2mas__compat_access_tokens ( - compat_access_token_id, - compat_session_id, - access_token, - created_at, - expires_at) - SELECT * FROM UNNEST( - $1::UUID[], - $2::UUID[], - $3::TEXT[], - $4::TIMESTAMP WITH TIME ZONE[], - $5::TIMESTAMP WITH TIME ZONE[]) - "#, - &token_ids[..], - &session_ids[..], - &access_tokens[..], - &created_ats[..], - // We need to override the typing for arrays of optionals (sqlx limitation) - &expires_ats[..] as &[Option>], - ) - .execute(&mut *conn) - .await - .into_database("writing compat access tokens to MAS")?; + MasNewCompatAccessToken::write_batch(conn, tokens).await?; Ok(()) }) From d2f1ab08fb779f1214ac288a8a8bea81653bb1bb Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 14:42:52 +0200 Subject: [PATCH 048/189] syn2mas: implement WriteBatch for MasNewCompatRefreshToken --- ...b986b1b4864a778525d0b8b0ad6678aef3e9f.json | 18 ++++ ...e5d8cac3836701fc24922f4f0e8b98d330796.json | 18 ---- crates/syn2mas/src/mas_writer/mod.rs | 99 ++++++++++--------- 3 files changed, 71 insertions(+), 64 deletions(-) create mode 100644 crates/syn2mas/.sqlx/query-1d1004d0fb5939fbf30c1986b80b986b1b4864a778525d0b8b0ad6678aef3e9f.json delete mode 100644 crates/syn2mas/.sqlx/query-88975196c4c174d464b33aa015ce5d8cac3836701fc24922f4f0e8b98d330796.json diff --git a/crates/syn2mas/.sqlx/query-1d1004d0fb5939fbf30c1986b80b986b1b4864a778525d0b8b0ad6678aef3e9f.json b/crates/syn2mas/.sqlx/query-1d1004d0fb5939fbf30c1986b80b986b1b4864a778525d0b8b0ad6678aef3e9f.json new file mode 100644 index 000000000..c65dfb7a4 --- /dev/null +++ b/crates/syn2mas/.sqlx/query-1d1004d0fb5939fbf30c1986b80b986b1b4864a778525d0b8b0ad6678aef3e9f.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO syn2mas__compat_refresh_tokens (\n compat_refresh_token_id,\n compat_session_id,\n compat_access_token_id,\n refresh_token,\n created_at)\n SELECT * FROM UNNEST(\n $1::UUID[],\n $2::UUID[],\n $3::UUID[],\n $4::TEXT[],\n $5::TIMESTAMP WITH TIME ZONE[])\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray", + "UuidArray", + "UuidArray", + "TextArray", + "TimestamptzArray" + ] + }, + "nullable": [] + }, + "hash": "1d1004d0fb5939fbf30c1986b80b986b1b4864a778525d0b8b0ad6678aef3e9f" +} diff --git a/crates/syn2mas/.sqlx/query-88975196c4c174d464b33aa015ce5d8cac3836701fc24922f4f0e8b98d330796.json b/crates/syn2mas/.sqlx/query-88975196c4c174d464b33aa015ce5d8cac3836701fc24922f4f0e8b98d330796.json deleted file mode 100644 index cb251624d..000000000 --- a/crates/syn2mas/.sqlx/query-88975196c4c174d464b33aa015ce5d8cac3836701fc24922f4f0e8b98d330796.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO syn2mas__compat_refresh_tokens (\n compat_refresh_token_id,\n compat_session_id,\n compat_access_token_id,\n refresh_token,\n created_at)\n SELECT * FROM UNNEST(\n $1::UUID[],\n $2::UUID[],\n $3::UUID[],\n $4::TEXT[],\n $5::TIMESTAMP WITH TIME ZONE[])\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "UuidArray", - "UuidArray", - "UuidArray", - "TextArray", - "TimestamptzArray" - ] - }, - "nullable": [] - }, - "hash": "88975196c4c174d464b33aa015ce5d8cac3836701fc24922f4f0e8b98d330796" -} diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index 182a4795b..0012ef04a 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -668,6 +668,58 @@ pub struct MasNewCompatRefreshToken { pub created_at: DateTime, } +impl WriteBatch for MasNewCompatRefreshToken { + async fn write_batch(conn: &mut PgConnection, batch: Vec) -> Result<(), Error> { + let mut refresh_token_ids: Vec = Vec::with_capacity(batch.len()); + let mut session_ids: Vec = Vec::with_capacity(batch.len()); + let mut access_token_ids: Vec = Vec::with_capacity(batch.len()); + let mut refresh_tokens: Vec = Vec::with_capacity(batch.len()); + let mut created_ats: Vec> = Vec::with_capacity(batch.len()); + + for MasNewCompatRefreshToken { + refresh_token_id, + session_id, + access_token_id, + refresh_token, + created_at, + } in batch + { + refresh_token_ids.push(refresh_token_id); + session_ids.push(session_id); + access_token_ids.push(access_token_id); + refresh_tokens.push(refresh_token); + created_ats.push(created_at); + } + + sqlx::query!( + r#" + INSERT INTO syn2mas__compat_refresh_tokens ( + compat_refresh_token_id, + compat_session_id, + compat_access_token_id, + refresh_token, + created_at) + SELECT * FROM UNNEST( + $1::UUID[], + $2::UUID[], + $3::UUID[], + $4::TEXT[], + $5::TIMESTAMP WITH TIME ZONE[]) + "#, + &refresh_token_ids[..], + &session_ids[..], + &access_token_ids[..], + &refresh_tokens[..], + &created_ats[..], + ) + .execute(&mut *conn) + .await + .into_database("writing compat refresh tokens to MAS")?; + + Ok(()) + } +} + /// The 'version' of the password hashing scheme used for passwords when they /// are migrated from Synapse to MAS. /// This is version 1, as in the previous syn2mas script. @@ -1103,52 +1155,7 @@ impl MasWriter { self.writer_pool .spawn_with_connection(move |conn| { Box::pin(async move { - let mut refresh_token_ids: Vec = Vec::with_capacity(tokens.len()); - let mut session_ids: Vec = Vec::with_capacity(tokens.len()); - let mut access_token_ids: Vec = Vec::with_capacity(tokens.len()); - let mut refresh_tokens: Vec = Vec::with_capacity(tokens.len()); - let mut created_ats: Vec> = Vec::with_capacity(tokens.len()); - - for MasNewCompatRefreshToken { - refresh_token_id, - session_id, - access_token_id, - refresh_token, - created_at, - } in tokens - { - refresh_token_ids.push(refresh_token_id); - session_ids.push(session_id); - access_token_ids.push(access_token_id); - refresh_tokens.push(refresh_token); - created_ats.push(created_at); - } - - sqlx::query!( - r#" - INSERT INTO syn2mas__compat_refresh_tokens ( - compat_refresh_token_id, - compat_session_id, - compat_access_token_id, - refresh_token, - created_at) - SELECT * FROM UNNEST( - $1::UUID[], - $2::UUID[], - $3::UUID[], - $4::TEXT[], - $5::TIMESTAMP WITH TIME ZONE[]) - "#, - &refresh_token_ids[..], - &session_ids[..], - &access_token_ids[..], - &refresh_tokens[..], - &created_ats[..], - ) - .execute(&mut *conn) - .await - .into_database("writing compat refresh tokens to MAS")?; - + MasNewCompatRefreshToken::write_batch(conn, tokens).await?; Ok(()) }) }) From 47009a8800b85fe9403a0908900cadddad54b5ef Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 15:49:17 +0200 Subject: [PATCH 049/189] syn2mas: make the MasWriteBuffer use the WriteBatch trait --- crates/syn2mas/src/mas_writer/mod.rs | 25 ++++++++++++------------- crates/syn2mas/src/migration.rs | 24 ++++++++++-------------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index 0012ef04a..257e07a11 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -114,7 +114,6 @@ impl WriterConnectionPool { where F: for<'conn> FnOnce(&'conn mut PgConnection) -> BoxFuture<'conn, Result<(), Error>> + Send - + Sync + 'static, { match self.connection_rx.recv().await { @@ -250,11 +249,11 @@ pub struct MasWriter { write_buffer_finish_checker: FinishChecker, } -trait WriteBatch: Sized { +pub trait WriteBatch: Send + Sync + Sized + 'static { fn write_batch( conn: &mut PgConnection, batch: Vec, - ) -> impl Future>; + ) -> impl Future> + Send; } pub struct MasNewUser { @@ -1167,24 +1166,20 @@ impl MasWriter { // database. const WRITE_BUFFER_BATCH_SIZE: usize = 4096; -/// A function that can accept and flush buffers from a `MasWriteBuffer`. -/// Intended uses are the methods on `MasWriter` such as `write_users`. -type WriteBufferFlusher = - for<'a> fn(&'a mut MasWriter, Vec) -> BoxFuture<'a, Result<(), Error>>; - /// A buffer for writing rows to the MAS database. /// Generic over the type of rows. pub struct MasWriteBuffer { rows: Vec, - flusher: WriteBufferFlusher, finish_checker_handle: FinishCheckerHandle, } -impl MasWriteBuffer { - pub fn new(writer: &MasWriter, flusher: WriteBufferFlusher) -> Self { +impl MasWriteBuffer +where + T: WriteBatch, +{ + pub fn new(writer: &MasWriter) -> Self { MasWriteBuffer { rows: Vec::with_capacity(WRITE_BUFFER_BATCH_SIZE), - flusher, finish_checker_handle: writer.write_buffer_finish_checker.handle(), } } @@ -1201,7 +1196,11 @@ impl MasWriteBuffer { } let rows = std::mem::take(&mut self.rows); self.rows.reserve_exact(WRITE_BUFFER_BATCH_SIZE); - (self.flusher)(writer, rows).await?; + writer + .writer_pool + .spawn_with_connection(move |conn| T::write_batch(conn, rows).boxed()) + .boxed() + .await?; Ok(()) } diff --git a/crates/syn2mas/src/migration.rs b/crates/syn2mas/src/migration.rs index 3388ff387..7b34baf62 100644 --- a/crates/syn2mas/src/migration.rs +++ b/crates/syn2mas/src/migration.rs @@ -220,8 +220,8 @@ async fn migrate_users( let mut rng = rand_chacha::ChaChaRng::from_rng(rng).expect("failed to seed rng"); let task = tokio::spawn( async move { - let mut user_buffer = MasWriteBuffer::new(&mas, MasWriter::write_users); - let mut password_buffer = MasWriteBuffer::new(&mas, MasWriter::write_passwords); + let mut user_buffer = MasWriteBuffer::new(&mas); + let mut password_buffer = MasWriteBuffer::new(&mas); while let Some(user) = rx.recv().await { // Handling an edge case: some AS users may have invalid localparts containing @@ -342,9 +342,8 @@ async fn migrate_threepids( let mut rng = rand_chacha::ChaChaRng::from_rng(rng).expect("failed to seed rng"); let task = tokio::spawn( async move { - let mut email_buffer = MasWriteBuffer::new(&mas, MasWriter::write_email_threepids); - let mut unsupported_buffer = - MasWriteBuffer::new(&mas, MasWriter::write_unsupported_threepids); + let mut email_buffer = MasWriteBuffer::new(&mas); + let mut unsupported_buffer = MasWriteBuffer::new(&mas); while let Some(threepid) = rx.recv().await { let SynapseThreepid { @@ -457,7 +456,7 @@ async fn migrate_external_ids( let mut rng = rand_chacha::ChaChaRng::from_rng(rng).expect("failed to seed rng"); let task = tokio::spawn( async move { - let mut write_buffer = MasWriteBuffer::new(&mas, MasWriter::write_upstream_oauth_links); + let mut write_buffer = MasWriteBuffer::new(&mas); while let Some(extid) = rx.recv().await { let SynapseExternalId { @@ -569,7 +568,7 @@ async fn migrate_devices( let mut rng = rand_chacha::ChaChaRng::from_rng(rng).expect("failed to seed rng"); let task = tokio::spawn( async move { - let mut write_buffer = MasWriteBuffer::new(&mas, MasWriter::write_compat_sessions); + let mut write_buffer = MasWriteBuffer::new(&mas); while let Some(device) = rx.recv().await { let SynapseDevice { @@ -704,9 +703,8 @@ async fn migrate_unrefreshable_access_tokens( let mut rng = rand_chacha::ChaChaRng::from_rng(rng).expect("failed to seed rng"); let task = tokio::spawn( async move { - let mut write_buffer = MasWriteBuffer::new(&mas, MasWriter::write_compat_access_tokens); - let mut deviceless_session_write_buffer = - MasWriteBuffer::new(&mas, MasWriter::write_compat_sessions); + let mut write_buffer = MasWriteBuffer::new(&mas); + let mut deviceless_session_write_buffer = MasWriteBuffer::new(&mas); while let Some(token) = rx.recv().await { let SynapseAccessToken { @@ -855,10 +853,8 @@ async fn migrate_refreshable_token_pairs( let now = clock.now(); let task = tokio::spawn( async move { - let mut access_token_write_buffer = - MasWriteBuffer::new(&mas, MasWriter::write_compat_access_tokens); - let mut refresh_token_write_buffer = - MasWriteBuffer::new(&mas, MasWriter::write_compat_refresh_tokens); + let mut access_token_write_buffer = MasWriteBuffer::new(&mas); + let mut refresh_token_write_buffer = MasWriteBuffer::new(&mas); while let Some(token) = rx.recv().await { let SynapseRefreshableTokenPair { From 1056949149484b4f3b29436670dfb458ced15ae4 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 16:30:40 +0200 Subject: [PATCH 050/189] syn2mas: remove the `MasWriter::write_` methods and replaced them in tests --- crates/syn2mas/src/mas_writer/mod.rs | 640 ++++++++++++++------------- 1 file changed, 331 insertions(+), 309 deletions(-) diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index 257e07a11..5e3a0f0ef 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -22,7 +22,7 @@ use sqlx::{Executor, PgConnection, query, query_as}; use thiserror::Error; use thiserror_ext::{Construct, ContextInto}; use tokio::sync::mpsc::{self, Receiver, Sender}; -use tracing::{Instrument, Level, error, info, warn}; +use tracing::{Instrument, error, info, warn}; use uuid::{NonNilUuid, Uuid}; use self::{ @@ -1022,144 +1022,6 @@ impl MasWriter { Ok(conn) } - - /// Write a batch of users to the database. - /// - /// # Errors - /// - /// Errors are returned in the following conditions: - /// - /// - If the database writer connection pool had an error. - #[tracing::instrument(skip_all, level = Level::DEBUG)] - pub fn write_users(&mut self, users: Vec) -> BoxFuture<'_, Result<(), Error>> { - self.writer_pool - .spawn_with_connection(move |conn| { - Box::pin(async move { - MasNewUser::write_batch(conn, users).await?; - - Ok(()) - }) - }) - .boxed() - } - - /// Write a batch of user passwords to the database. - /// - /// # Errors - /// - /// Errors are returned in the following conditions: - /// - /// - If the database writer connection pool had an error. - #[tracing::instrument(skip_all, level = Level::DEBUG)] - pub fn write_passwords( - &mut self, - passwords: Vec, - ) -> BoxFuture<'_, Result<(), Error>> { - self.writer_pool - .spawn_with_connection(move |conn| { - Box::pin(async move { - MasNewUserPassword::write_batch(conn, passwords).await?; - - Ok(()) - }) - }) - .boxed() - } - - #[tracing::instrument(skip_all, level = Level::DEBUG)] - pub fn write_email_threepids( - &mut self, - threepids: Vec, - ) -> BoxFuture<'_, Result<(), Error>> { - self.writer_pool - .spawn_with_connection(move |conn| { - Box::pin(async move { - MasNewEmailThreepid::write_batch(conn, threepids).await?; - - Ok(()) - }) - }) - .boxed() - } - - #[tracing::instrument(skip_all, level = Level::DEBUG)] - pub fn write_unsupported_threepids( - &mut self, - threepids: Vec, - ) -> BoxFuture<'_, Result<(), Error>> { - self.writer_pool - .spawn_with_connection(move |conn| { - Box::pin(async move { - MasNewUnsupportedThreepid::write_batch(conn, threepids).await?; - - Ok(()) - }) - }) - .boxed() - } - - #[tracing::instrument(skip_all, level = Level::DEBUG)] - pub fn write_upstream_oauth_links( - &mut self, - links: Vec, - ) -> BoxFuture<'_, Result<(), Error>> { - self.writer_pool - .spawn_with_connection(move |conn| { - Box::pin(async move { - MasNewUpstreamOauthLink::write_batch(conn, links).await?; - - Ok(()) - }) - }) - .boxed() - } - - #[tracing::instrument(skip_all, level = Level::DEBUG)] - pub fn write_compat_sessions( - &mut self, - sessions: Vec, - ) -> BoxFuture<'_, Result<(), Error>> { - self.writer_pool - .spawn_with_connection(move |conn| { - Box::pin(async move { - MasNewCompatSession::write_batch(conn, sessions).await?; - - Ok(()) - }) - }) - .boxed() - } - - #[tracing::instrument(skip_all, level = Level::DEBUG)] - pub fn write_compat_access_tokens( - &mut self, - tokens: Vec, - ) -> BoxFuture<'_, Result<(), Error>> { - self.writer_pool - .spawn_with_connection(move |conn| { - Box::pin(async move { - MasNewCompatAccessToken::write_batch(conn, tokens).await?; - - Ok(()) - }) - }) - .boxed() - } - - #[tracing::instrument(skip_all, level = Level::DEBUG)] - pub fn write_compat_refresh_tokens( - &mut self, - tokens: Vec, - ) -> BoxFuture<'_, Result<(), Error>> { - self.writer_pool - .spawn_with_connection(move |conn| { - Box::pin(async move { - MasNewCompatRefreshToken::write_batch(conn, tokens).await?; - Ok(()) - }) - }) - .boxed() - } } // How many entries to buffer at once, before writing a batch of rows to the @@ -1228,7 +1090,7 @@ mod test { mas_writer::{ MasNewCompatAccessToken, MasNewCompatRefreshToken, MasNewCompatSession, MasNewEmailThreepid, MasNewUnsupportedThreepid, MasNewUpstreamOauthLink, MasNewUser, - MasNewUserPassword, + MasNewUserPassword, MasWriteBuffer, }, }; @@ -1340,20 +1202,29 @@ mod test { #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] async fn test_write_user(pool: PgPool) { let mut writer = make_mas_writer(&pool).await; + let mut buffer = MasWriteBuffer::new(&writer); - writer - .write_users(vec![MasNewUser { - user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), - username: "alice".to_owned(), - created_at: DateTime::default(), - locked_at: None, - deactivated_at: None, - can_request_admin: false, - is_guest: false, - }]) + buffer + .write( + &mut writer, + MasNewUser { + user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), + username: "alice".to_owned(), + created_at: DateTime::default(), + locked_at: None, + deactivated_at: None, + can_request_admin: false, + is_guest: false, + }, + ) .await .expect("failed to write user"); + buffer + .finish(&mut writer) + .await + .expect("failed to finish MasWriter"); + let mut conn = writer .finish(&Progress::default()) .await @@ -1369,28 +1240,47 @@ mod test { let mut writer = make_mas_writer(&pool).await; - writer - .write_users(vec![MasNewUser { - user_id: USER_ID, - username: "alice".to_owned(), - created_at: DateTime::default(), - locked_at: None, - deactivated_at: None, - can_request_admin: false, - is_guest: false, - }]) + let mut user_buffer = MasWriteBuffer::new(&writer); + let mut password_buffer = MasWriteBuffer::new(&writer); + + user_buffer + .write( + &mut writer, + MasNewUser { + user_id: USER_ID, + username: "alice".to_owned(), + created_at: DateTime::default(), + locked_at: None, + deactivated_at: None, + can_request_admin: false, + is_guest: false, + }, + ) .await .expect("failed to write user"); - writer - .write_passwords(vec![MasNewUserPassword { - user_password_id: Uuid::from_u128(42u128), - user_id: USER_ID, - hashed_password: "$bcrypt$aaaaaaaaaaa".to_owned(), - created_at: DateTime::default(), - }]) + + password_buffer + .write( + &mut writer, + MasNewUserPassword { + user_password_id: Uuid::from_u128(42u128), + user_id: USER_ID, + hashed_password: "$bcrypt$aaaaaaaaaaa".to_owned(), + created_at: DateTime::default(), + }, + ) .await .expect("failed to write password"); + user_buffer + .finish(&mut writer) + .await + .expect("failed to finish MasWriteBuffer"); + password_buffer + .finish(&mut writer) + .await + .expect("failed to finish MasWriteBuffer"); + let mut conn = writer .finish(&Progress::default()) .await @@ -1404,29 +1294,47 @@ mod test { async fn test_write_user_with_email(pool: PgPool) { let mut writer = make_mas_writer(&pool).await; - writer - .write_users(vec![MasNewUser { - user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), - username: "alice".to_owned(), - created_at: DateTime::default(), - locked_at: None, - deactivated_at: None, - can_request_admin: false, - is_guest: false, - }]) + let mut user_buffer = MasWriteBuffer::new(&writer); + let mut email_buffer = MasWriteBuffer::new(&writer); + + user_buffer + .write( + &mut writer, + MasNewUser { + user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), + username: "alice".to_owned(), + created_at: DateTime::default(), + locked_at: None, + deactivated_at: None, + can_request_admin: false, + is_guest: false, + }, + ) .await .expect("failed to write user"); - writer - .write_email_threepids(vec![MasNewEmailThreepid { - user_email_id: Uuid::from_u128(2u128), - user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), - email: "alice@example.org".to_owned(), - created_at: DateTime::default(), - }]) + email_buffer + .write( + &mut writer, + MasNewEmailThreepid { + user_email_id: Uuid::from_u128(2u128), + user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), + email: "alice@example.org".to_owned(), + created_at: DateTime::default(), + }, + ) .await .expect("failed to write e-mail"); + user_buffer + .finish(&mut writer) + .await + .expect("failed to finish user buffer"); + email_buffer + .finish(&mut writer) + .await + .expect("failed to finish email buffer"); + let mut conn = writer .finish(&Progress::default()) .await @@ -1441,29 +1349,47 @@ mod test { async fn test_write_user_with_unsupported_threepid(pool: PgPool) { let mut writer = make_mas_writer(&pool).await; - writer - .write_users(vec![MasNewUser { - user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), - username: "alice".to_owned(), - created_at: DateTime::default(), - locked_at: None, - deactivated_at: None, - can_request_admin: false, - is_guest: false, - }]) + let mut user_buffer = MasWriteBuffer::new(&writer); + let mut threepid_buffer = MasWriteBuffer::new(&writer); + + user_buffer + .write( + &mut writer, + MasNewUser { + user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), + username: "alice".to_owned(), + created_at: DateTime::default(), + locked_at: None, + deactivated_at: None, + can_request_admin: false, + is_guest: false, + }, + ) .await .expect("failed to write user"); - writer - .write_unsupported_threepids(vec![MasNewUnsupportedThreepid { - user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), - medium: "msisdn".to_owned(), - address: "441189998819991197253".to_owned(), - created_at: DateTime::default(), - }]) + threepid_buffer + .write( + &mut writer, + MasNewUnsupportedThreepid { + user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), + medium: "msisdn".to_owned(), + address: "441189998819991197253".to_owned(), + created_at: DateTime::default(), + }, + ) .await .expect("failed to write phone number (unsupported threepid)"); + user_buffer + .finish(&mut writer) + .await + .expect("failed to finish user buffer"); + threepid_buffer + .finish(&mut writer) + .await + .expect("failed to finish threepid buffer"); + let mut conn = writer .finish(&Progress::default()) .await @@ -1479,30 +1405,48 @@ mod test { async fn test_write_user_with_upstream_provider_link(pool: PgPool) { let mut writer = make_mas_writer(&pool).await; - writer - .write_users(vec![MasNewUser { - user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), - username: "alice".to_owned(), - created_at: DateTime::default(), - locked_at: None, - deactivated_at: None, - can_request_admin: false, - is_guest: false, - }]) + let mut user_buffer = MasWriteBuffer::new(&writer); + let mut link_buffer = MasWriteBuffer::new(&writer); + + user_buffer + .write( + &mut writer, + MasNewUser { + user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), + username: "alice".to_owned(), + created_at: DateTime::default(), + locked_at: None, + deactivated_at: None, + can_request_admin: false, + is_guest: false, + }, + ) .await .expect("failed to write user"); - writer - .write_upstream_oauth_links(vec![MasNewUpstreamOauthLink { - user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), - link_id: Uuid::from_u128(3u128), - upstream_provider_id: Uuid::from_u128(4u128), - subject: "12345.67890".to_owned(), - created_at: DateTime::default(), - }]) + link_buffer + .write( + &mut writer, + MasNewUpstreamOauthLink { + user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), + link_id: Uuid::from_u128(3u128), + upstream_provider_id: Uuid::from_u128(4u128), + subject: "12345.67890".to_owned(), + created_at: DateTime::default(), + }, + ) .await .expect("failed to write link"); + user_buffer + .finish(&mut writer) + .await + .expect("failed to finish user buffer"); + link_buffer + .finish(&mut writer) + .await + .expect("failed to finish link buffer"); + let mut conn = writer .finish(&Progress::default()) .await @@ -1516,34 +1460,52 @@ mod test { async fn test_write_user_with_device(pool: PgPool) { let mut writer = make_mas_writer(&pool).await; - writer - .write_users(vec![MasNewUser { - user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), - username: "alice".to_owned(), - created_at: DateTime::default(), - locked_at: None, - deactivated_at: None, - can_request_admin: false, - is_guest: false, - }]) + let mut user_buffer = MasWriteBuffer::new(&writer); + let mut session_buffer = MasWriteBuffer::new(&writer); + + user_buffer + .write( + &mut writer, + MasNewUser { + user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), + username: "alice".to_owned(), + created_at: DateTime::default(), + locked_at: None, + deactivated_at: None, + can_request_admin: false, + is_guest: false, + }, + ) .await .expect("failed to write user"); - writer - .write_compat_sessions(vec![MasNewCompatSession { - user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), - session_id: Uuid::from_u128(5u128), - created_at: DateTime::default(), - device_id: Some("ADEVICE".to_owned()), - human_name: Some("alice's pinephone".to_owned()), - is_synapse_admin: true, - last_active_at: Some(DateTime::default()), - last_active_ip: Some("203.0.113.1".parse().unwrap()), - user_agent: Some("Browser/5.0".to_owned()), - }]) + session_buffer + .write( + &mut writer, + MasNewCompatSession { + user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), + session_id: Uuid::from_u128(5u128), + created_at: DateTime::default(), + device_id: Some("ADEVICE".to_owned()), + human_name: Some("alice's pinephone".to_owned()), + is_synapse_admin: true, + last_active_at: Some(DateTime::default()), + last_active_ip: Some("203.0.113.1".parse().unwrap()), + user_agent: Some("Browser/5.0".to_owned()), + }, + ) .await .expect("failed to write compat session"); + user_buffer + .finish(&mut writer) + .await + .expect("failed to finish user buffer"); + session_buffer + .finish(&mut writer) + .await + .expect("failed to finish session buffer"); + let mut conn = writer .finish(&Progress::default()) .await @@ -1557,45 +1519,71 @@ mod test { async fn test_write_user_with_access_token(pool: PgPool) { let mut writer = make_mas_writer(&pool).await; - writer - .write_users(vec![MasNewUser { - user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), - username: "alice".to_owned(), - created_at: DateTime::default(), - locked_at: None, - deactivated_at: None, - can_request_admin: false, - is_guest: false, - }]) + let mut user_buffer = MasWriteBuffer::new(&writer); + let mut session_buffer = MasWriteBuffer::new(&writer); + let mut token_buffer = MasWriteBuffer::new(&writer); + + user_buffer + .write( + &mut writer, + MasNewUser { + user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), + username: "alice".to_owned(), + created_at: DateTime::default(), + locked_at: None, + deactivated_at: None, + can_request_admin: false, + is_guest: false, + }, + ) .await .expect("failed to write user"); - writer - .write_compat_sessions(vec![MasNewCompatSession { - user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), - session_id: Uuid::from_u128(5u128), - created_at: DateTime::default(), - device_id: Some("ADEVICE".to_owned()), - human_name: None, - is_synapse_admin: false, - last_active_at: None, - last_active_ip: None, - user_agent: None, - }]) + session_buffer + .write( + &mut writer, + MasNewCompatSession { + user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), + session_id: Uuid::from_u128(5u128), + created_at: DateTime::default(), + device_id: Some("ADEVICE".to_owned()), + human_name: None, + is_synapse_admin: false, + last_active_at: None, + last_active_ip: None, + user_agent: None, + }, + ) .await .expect("failed to write compat session"); - writer - .write_compat_access_tokens(vec![MasNewCompatAccessToken { - token_id: Uuid::from_u128(6u128), - session_id: Uuid::from_u128(5u128), - access_token: "syt_zxcvzxcvzxcvzxcv_zxcv".to_owned(), - created_at: DateTime::default(), - expires_at: None, - }]) + token_buffer + .write( + &mut writer, + MasNewCompatAccessToken { + token_id: Uuid::from_u128(6u128), + session_id: Uuid::from_u128(5u128), + access_token: "syt_zxcvzxcvzxcvzxcv_zxcv".to_owned(), + created_at: DateTime::default(), + expires_at: None, + }, + ) .await .expect("failed to write access token"); + user_buffer + .finish(&mut writer) + .await + .expect("failed to finish user buffer"); + session_buffer + .finish(&mut writer) + .await + .expect("failed to finish session buffer"); + token_buffer + .finish(&mut writer) + .await + .expect("failed to finish token buffer"); + let mut conn = writer .finish(&Progress::default()) .await @@ -1610,56 +1598,90 @@ mod test { async fn test_write_user_with_refresh_token(pool: PgPool) { let mut writer = make_mas_writer(&pool).await; - writer - .write_users(vec![MasNewUser { - user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), - username: "alice".to_owned(), - created_at: DateTime::default(), - locked_at: None, - deactivated_at: None, - can_request_admin: false, - is_guest: false, - }]) + let mut user_buffer = MasWriteBuffer::new(&writer); + let mut session_buffer = MasWriteBuffer::new(&writer); + let mut token_buffer = MasWriteBuffer::new(&writer); + let mut refresh_token_buffer = MasWriteBuffer::new(&writer); + + user_buffer + .write( + &mut writer, + MasNewUser { + user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), + username: "alice".to_owned(), + created_at: DateTime::default(), + locked_at: None, + deactivated_at: None, + can_request_admin: false, + is_guest: false, + }, + ) .await .expect("failed to write user"); - writer - .write_compat_sessions(vec![MasNewCompatSession { - user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), - session_id: Uuid::from_u128(5u128), - created_at: DateTime::default(), - device_id: Some("ADEVICE".to_owned()), - human_name: None, - is_synapse_admin: false, - last_active_at: None, - last_active_ip: None, - user_agent: None, - }]) + session_buffer + .write( + &mut writer, + MasNewCompatSession { + user_id: NonNilUuid::new(Uuid::from_u128(1u128)).unwrap(), + session_id: Uuid::from_u128(5u128), + created_at: DateTime::default(), + device_id: Some("ADEVICE".to_owned()), + human_name: None, + is_synapse_admin: false, + last_active_at: None, + last_active_ip: None, + user_agent: None, + }, + ) .await .expect("failed to write compat session"); - writer - .write_compat_access_tokens(vec![MasNewCompatAccessToken { - token_id: Uuid::from_u128(6u128), - session_id: Uuid::from_u128(5u128), - access_token: "syt_zxcvzxcvzxcvzxcv_zxcv".to_owned(), - created_at: DateTime::default(), - expires_at: None, - }]) + token_buffer + .write( + &mut writer, + MasNewCompatAccessToken { + token_id: Uuid::from_u128(6u128), + session_id: Uuid::from_u128(5u128), + access_token: "syt_zxcvzxcvzxcvzxcv_zxcv".to_owned(), + created_at: DateTime::default(), + expires_at: None, + }, + ) .await .expect("failed to write access token"); - writer - .write_compat_refresh_tokens(vec![MasNewCompatRefreshToken { - refresh_token_id: Uuid::from_u128(7u128), - session_id: Uuid::from_u128(5u128), - access_token_id: Uuid::from_u128(6u128), - refresh_token: "syr_zxcvzxcvzxcvzxcv_zxcv".to_owned(), - created_at: DateTime::default(), - }]) + refresh_token_buffer + .write( + &mut writer, + MasNewCompatRefreshToken { + refresh_token_id: Uuid::from_u128(7u128), + session_id: Uuid::from_u128(5u128), + access_token_id: Uuid::from_u128(6u128), + refresh_token: "syr_zxcvzxcvzxcvzxcv_zxcv".to_owned(), + created_at: DateTime::default(), + }, + ) .await .expect("failed to write refresh token"); + user_buffer + .finish(&mut writer) + .await + .expect("failed to finish user buffer"); + session_buffer + .finish(&mut writer) + .await + .expect("failed to finish session buffer"); + token_buffer + .finish(&mut writer) + .await + .expect("failed to finish token buffer"); + refresh_token_buffer + .finish(&mut writer) + .await + .expect("failed to finish refresh token buffer"); + let mut conn = writer .finish(&Progress::default()) .await From ad2c183c713f381eb7a47f3eea9d09e2c826c11b Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 17:35:15 +0200 Subject: [PATCH 051/189] syn2mas: reduce the channel buffer size --- crates/syn2mas/src/migration.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/syn2mas/src/migration.rs b/crates/syn2mas/src/migration.rs index 7b34baf62..b79718056 100644 --- a/crates/syn2mas/src/migration.rs +++ b/crates/syn2mas/src/migration.rs @@ -213,7 +213,7 @@ async fn migrate_users( ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let (tx, mut rx) = tokio::sync::mpsc::channel::(10 * 1024 * 1024); + let (tx, mut rx) = tokio::sync::mpsc::channel::(100 * 1024); // create a new RNG seeded from the passed RNG so that we can move it into the // spawned task @@ -335,7 +335,7 @@ async fn migrate_threepids( ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let (tx, mut rx) = tokio::sync::mpsc::channel::(10 * 1024 * 1024); + let (tx, mut rx) = tokio::sync::mpsc::channel::(100 * 1024); // create a new RNG seeded from the passed RNG so that we can move it into the // spawned task @@ -449,7 +449,7 @@ async fn migrate_external_ids( ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let (tx, mut rx) = tokio::sync::mpsc::channel::(10 * 1024 * 1024); + let (tx, mut rx) = tokio::sync::mpsc::channel::(100 * 1024); // create a new RNG seeded from the passed RNG so that we can move it into the // spawned task @@ -561,7 +561,7 @@ async fn migrate_devices( ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let (tx, mut rx) = tokio::sync::mpsc::channel(10 * 1024 * 1024); + let (tx, mut rx) = tokio::sync::mpsc::channel(100 * 1024); // create a new RNG seeded from the passed RNG so that we can move it into the // spawned task @@ -695,7 +695,7 @@ async fn migrate_unrefreshable_access_tokens( ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let (tx, mut rx) = tokio::sync::mpsc::channel(10 * 1024 * 1024); + let (tx, mut rx) = tokio::sync::mpsc::channel(100 * 1024); let now = clock.now(); // create a new RNG seeded from the passed RNG so that we can move it into the @@ -845,7 +845,7 @@ async fn migrate_refreshable_token_pairs( ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); - let (tx, mut rx) = tokio::sync::mpsc::channel::(10 * 1024 * 1024); + let (tx, mut rx) = tokio::sync::mpsc::channel::(100 * 1024); // create a new RNG seeded from the passed RNG so that we can move it into the // spawned task From 9aafc576a193763eed0d6612cac285385e99530e Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 17:38:58 +0200 Subject: [PATCH 052/189] syn2mas: log the number of entities migrated at each step --- crates/syn2mas/src/migration.rs | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/crates/syn2mas/src/migration.rs b/crates/syn2mas/src/migration.rs index b79718056..2a906d933 100644 --- a/crates/syn2mas/src/migration.rs +++ b/crates/syn2mas/src/migration.rs @@ -212,6 +212,7 @@ async fn migrate_users( progress_counter: ProgressCounter, ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); + let progress_counter_ = progress_counter.clone(); let (tx, mut rx) = tokio::sync::mpsc::channel::(100 * 1024); @@ -318,7 +319,9 @@ async fn migrate_users( res?; info!( - "users migrated in {:.1}s", + "{} users migrated ({} skipped) in {:.1}s", + progress_counter_.migrated(), + progress_counter_.skipped(), Instant::now().duration_since(start).as_secs_f64() ); @@ -334,6 +337,7 @@ async fn migrate_threepids( progress_counter: ProgressCounter, ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); + let progress_counter_ = progress_counter.clone(); let (tx, mut rx) = tokio::sync::mpsc::channel::(100 * 1024); @@ -432,7 +436,9 @@ async fn migrate_threepids( res?; info!( - "third-party IDs migrated in {:.1}s", + "{} third-party IDs migrated ({} skipped) in {:.1}s", + progress_counter_.migrated(), + progress_counter_.skipped(), Instant::now().duration_since(start).as_secs_f64() ); @@ -448,6 +454,7 @@ async fn migrate_external_ids( progress_counter: ProgressCounter, ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); + let progress_counter_ = progress_counter.clone(); let (tx, mut rx) = tokio::sync::mpsc::channel::(100 * 1024); @@ -536,7 +543,9 @@ async fn migrate_external_ids( res?; info!( - "upstream links (external IDs) migrated in {:.1}s", + "{} upstream links (external IDs) migrated ({} skipped) in {:.1}s", + progress_counter_.migrated(), + progress_counter_.skipped(), Instant::now().duration_since(start).as_secs_f64() ); @@ -560,6 +569,7 @@ async fn migrate_devices( progress_counter: ProgressCounter, ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); + let progress_counter_ = progress_counter.clone(); let (tx, mut rx) = tokio::sync::mpsc::channel(100 * 1024); @@ -675,7 +685,9 @@ async fn migrate_devices( res?; info!( - "devices migrated in {:.1}s", + "{} devices migrated ({} skipped) in {:.1}s", + progress_counter_.migrated(), + progress_counter_.skipped(), Instant::now().duration_since(start).as_secs_f64() ); @@ -694,6 +706,7 @@ async fn migrate_unrefreshable_access_tokens( progress_counter: ProgressCounter, ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); + let progress_counter_ = progress_counter.clone(); let (tx, mut rx) = tokio::sync::mpsc::channel(100 * 1024); @@ -825,7 +838,9 @@ async fn migrate_unrefreshable_access_tokens( res?; info!( - "non-refreshable access tokens migrated in {:.1}s", + "{} non-refreshable access tokens migrated ({} skipped) in {:.1}s", + progress_counter_.migrated(), + progress_counter_.skipped(), Instant::now().duration_since(start).as_secs_f64() ); @@ -844,6 +859,7 @@ async fn migrate_refreshable_token_pairs( progress_counter: ProgressCounter, ) -> Result<(MasWriter, MigrationState), Error> { let start = Instant::now(); + let progress_counter_ = progress_counter.clone(); let (tx, mut rx) = tokio::sync::mpsc::channel::(100 * 1024); @@ -966,7 +982,9 @@ async fn migrate_refreshable_token_pairs( res?; info!( - "refreshable token pairs migrated in {:.1}s", + "{} refreshable token pairs migrated ({} skipped) in {:.1}s", + progress_counter_.migrated(), + progress_counter_.skipped(), Instant::now().duration_since(start).as_secs_f64() ); From 853b987a4acc5d81297d21d7f42562c736646b21 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 17:46:12 +0200 Subject: [PATCH 053/189] syn2mas: only log once when rebuilding constraints --- crates/syn2mas/src/mas_writer/constraint_pausing.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/syn2mas/src/mas_writer/constraint_pausing.rs b/crates/syn2mas/src/mas_writer/constraint_pausing.rs index 36783215f..d350bd89f 100644 --- a/crates/syn2mas/src/mas_writer/constraint_pausing.rs +++ b/crates/syn2mas/src/mas_writer/constraint_pausing.rs @@ -123,7 +123,6 @@ pub async fn restore_constraint( table_name, definition, } = &constraint; - info!("rebuilding constraint {name}"); sqlx::query(&format!( "ALTER TABLE {table_name} ADD CONSTRAINT {name} {definition};" From 31c8a08dd1309069bb1144fe4716ad1f8eb657ea Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Apr 2025 17:57:30 +0200 Subject: [PATCH 054/189] syn2mas: spawn the writer connections in parallel Also make sure we have a single span for them --- crates/cli/src/commands/syn2mas.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/cli/src/commands/syn2mas.rs b/crates/cli/src/commands/syn2mas.rs index 473afed54..bc1ae72f1 100644 --- a/crates/cli/src/commands/syn2mas.rs +++ b/crates/cli/src/commands/syn2mas.rs @@ -231,18 +231,17 @@ impl Options { // TODO this dry-run flag should be set to false in real circumstances !!! let reader = SynapseReader::new(&mut syn_conn, true).await?; - let mut writer_mas_connections = Vec::with_capacity(NUM_WRITER_CONNECTIONS); - for _ in 0..NUM_WRITER_CONNECTIONS { - writer_mas_connections.push( + let writer_mas_connections = + futures_util::future::try_join_all((0..NUM_WRITER_CONNECTIONS).map(|_| { database_connection_from_config_with_options( &config, &DatabaseConnectOptions { log_slow_statements: false, }, ) - .await?, - ); - } + })) + .instrument(tracing::info_span!("syn2mas.mas_writer_connections")) + .await?; let writer = MasWriter::new(mas_connection, writer_mas_connections).await?; let clock = SystemClock::default(); @@ -256,7 +255,6 @@ impl Options { tokio::spawn(occasional_progress_logger(progress.clone())); let mas_matrix = MatrixConfig::extract(figment)?; - eprintln!("\n\n"); syn2mas::migrate( reader, writer, From f84f6142b9811eec1f029fba9748a2332706334b Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 23 Apr 2025 10:47:57 +0200 Subject: [PATCH 055/189] Fix starting up when no telemetry config is set --- crates/cli/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 85c5a89f1..3235cf443 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -10,7 +10,7 @@ use std::{io::IsTerminal, process::ExitCode, sync::Arc}; use anyhow::Context; use clap::Parser; -use mas_config::{ConfigurationSection, TelemetryConfig}; +use mas_config::{ConfigurationSectionExt, TelemetryConfig}; use sentry_tracing::EventFilter; use tracing_subscriber::{ EnvFilter, Layer, Registry, filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt, @@ -110,7 +110,7 @@ async fn try_main() -> anyhow::Result { let figment = opts.figment(); let telemetry_config = - TelemetryConfig::extract(&figment).context("Failed to load telemetry config")?; + TelemetryConfig::extract_or_default(&figment).context("Failed to load telemetry config")?; // Setup Sentry let sentry = sentry::init(( From ed319dc43cff91e68ebcbeae092446e4fa6b91c6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 09:22:20 +0000 Subject: [PATCH 056/189] Translations updates --- frontend/.storybook/locales.ts | 76 ++++--- frontend/locales/hu.json | 395 +++++++++++++++++++++++++++++++++ frontend/locales/nb-NO.json | 395 +++++++++++++++++++++++++++++++++ translations/hu.json | 265 ++++++++++++++++++++++ translations/nb-NO.json | 265 ++++++++++++++++++++++ 5 files changed, 1369 insertions(+), 27 deletions(-) create mode 100644 frontend/locales/hu.json create mode 100644 frontend/locales/nb-NO.json create mode 100644 translations/hu.json create mode 100644 translations/nb-NO.json diff --git a/frontend/.storybook/locales.ts b/frontend/.storybook/locales.ts index 61129bca2..41eba3c10 100644 --- a/frontend/.storybook/locales.ts +++ b/frontend/.storybook/locales.ts @@ -27,7 +27,7 @@ export type LocalazyMetadata = { }; const localazyMetadata: LocalazyMetadata = { - projectUrl: "https://localazy.com/p/matrix-authentication-service", + projectUrl: "https://localazy.com/p/matrix-authentication-service!v0.15", baseLocale: "en", languages: [ { @@ -93,6 +93,24 @@ const localazyMetadata: LocalazyMetadata = { localizedName: "Français", pluralType: (n) => { return (n===0 || n===1) ? "one" : "other"; } }, + { + language: "hu", + region: "", + script: "", + isRtl: false, + name: "Hungarian", + localizedName: "Magyar", + pluralType: (n) => { return (n===1) ? "one" : "other"; } + }, + { + language: "nb", + region: "NO", + script: "", + isRtl: false, + name: "Norwegian Bokmål (Norway)", + localizedName: "Norsk bokmål (Norge)", + pluralType: (n) => { return (n===1) ? "one" : "other"; } + }, { language: "nl", region: "", @@ -154,19 +172,21 @@ const localazyMetadata: LocalazyMetadata = { file: "frontend.json", path: "", cdnFiles: { - "cs": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/cs/frontend.json", - "da": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/da/frontend.json", - "de": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/de/frontend.json", - "en": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/en/frontend.json", - "et": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/et/frontend.json", - "fi": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fi/frontend.json", - "fr": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fr/frontend.json", - "nl": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nl/frontend.json", - "pt": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt/frontend.json", - "ru": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/ru/frontend.json", - "sv": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sv/frontend.json", - "uk": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uk/frontend.json", - "zh#Hans": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/zh-Hans/frontend.json" + "cs": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/cs/frontend.json", + "da": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/da/frontend.json", + "de": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/de/frontend.json", + "en": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/en/frontend.json", + "et": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/et/frontend.json", + "fi": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fi/frontend.json", + "fr": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fr/frontend.json", + "hu": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/hu/frontend.json", + "nb_NO": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nb-NO/frontend.json", + "nl": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nl/frontend.json", + "pt": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt/frontend.json", + "ru": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/ru/frontend.json", + "sv": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sv/frontend.json", + "uk": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uk/frontend.json", + "zh#Hans": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/zh-Hans/frontend.json" } }, { @@ -174,19 +194,21 @@ const localazyMetadata: LocalazyMetadata = { file: "file.json", path: "", cdnFiles: { - "cs": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/cs/file.json", - "da": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/da/file.json", - "de": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/de/file.json", - "en": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/en/file.json", - "et": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/et/file.json", - "fi": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fi/file.json", - "fr": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fr/file.json", - "nl": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nl/file.json", - "pt": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt/file.json", - "ru": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/ru/file.json", - "sv": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sv/file.json", - "uk": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uk/file.json", - "zh#Hans": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/zh-Hans/file.json" + "cs": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/cs/file.json", + "da": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/da/file.json", + "de": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/de/file.json", + "en": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/en/file.json", + "et": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/et/file.json", + "fi": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fi/file.json", + "fr": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fr/file.json", + "hu": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/hu/file.json", + "nb_NO": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nb-NO/file.json", + "nl": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nl/file.json", + "pt": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt/file.json", + "ru": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/ru/file.json", + "sv": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sv/file.json", + "uk": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uk/file.json", + "zh#Hans": "https://delivery.localazy.com/_a69844934179046823895c13fb73/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/zh-Hans/file.json" } } ] diff --git a/frontend/locales/hu.json b/frontend/locales/hu.json new file mode 100644 index 000000000..fc27e6b2d --- /dev/null +++ b/frontend/locales/hu.json @@ -0,0 +1,395 @@ +{ + "action": { + "back": "Vissza", + "cancel": "Mégse", + "clear": "Törlés", + "close": "Bezárás", + "collapse": "Összecsukás", + "confirm": "Megerősítés", + "continue": "Folytatás", + "edit": "Szerkesztés", + "expand": "Kibontás", + "save": "Mentés", + "save_and_continue": "Mentés és folytatás", + "sign_out": "Kijelentkezés", + "start_over": "Újrakezdés" + }, + "branding": { + "privacy_policy": { + "alt": "Hivatkozás a szolgáltatás adatvédelmi irányelveire", + "link": "Adatvédelmi irányelvek" + }, + "terms_and_conditions": { + "alt": "Hivatkozás a szolgáltatási feltételekre", + "link": "Szolgáltatási feltételek" + } + }, + "common": { + "add": "Hozzáadás", + "e2ee": "Végpontok közti titkosítás", + "error": "Hiba", + "loading": "Betöltés…", + "next": "Következő", + "password": "Jelszó", + "previous": "Előző", + "saved": "Mentve", + "saving": "Mentés…" + }, + "frontend": { + "account": { + "account_password": "Fiók jelszava", + "contact_info": "Kapcsolati információ", + "delete_account": { + "alert_description": "Ez a fiók véglegesen törölve lesz, és többé nem fog hozzáférni az üzeneteihez.", + "alert_title": "Hamarosan elveszíti az összes adatát", + "button": "Fiók törlése", + "dialog_description": "Erősítse meg, hogy törölné a fiókját:\n\n\nNem fogja tudni újraaktiválni a fiókját\nTöbbé nem fog tudni bejelentkezni\nSenki sem fogja tudni használni a felhasználónevét (MXID), Önt is beleértve\nElhagyja az összes szobáját és a közvetlen üzeneteit\nEl lesz távolítva az azonosítási kiszolgálóról, és senki sem fogja tudni megtalálni az e-mail-címe vagy telefonszáma alapján\n\nA régi üzenetei továbbra is láthatóak lesznek azok számára, akik megkapták azokat. Elrejti az elküldött üzeneteit azok elől, akik a jövőben csatlakoznak a szobákhoz?", + "dialog_title": "Törli ezt a fiókot?", + "erase_checkbox_label": "Igen, az összes üzenet elrejtése az új érkezők elől", + "incorrect_password": "Helytelen jelszó, próbálja újra", + "mxid_label": "Erősítse meg a Matrix-azonosítóját ({{ mxid }})", + "mxid_mismatch": "Ez az érték nem egyezik a Matrix-azonosítójával", + "password_label": "A folytatáshoz adja meg a jelszavát" + }, + "edit_profile": { + "display_name_help": "Ez az, amit mások látni fognak, ha be van jelentkezve.", + "display_name_label": "Megjelenítési név", + "title": "Profil szerkesztése", + "username_label": "Felhasználói név" + }, + "password": { + "change": "Jelszó módosítása", + "change_disabled": "A jelszóváltoztatást letiltotta a rendszergazda.", + "label": "Jelszó" + }, + "sign_out": { + "button": "Kijelentkezés a fiókból", + "dialog": "Kijelentkezik ebből a fiókból?" + }, + "title": "Saját fiók" + }, + "add_email_form": { + "email_denied_alert": { + "text": "A megadott e-mail-címet nem engedélyezi a kiszolgáló házirendje.", + "title": "E-mail-cím házirend alapján elutasítva" + }, + "email_denied_error": "A megadott e-mail-címet nem engedélyezi a kiszolgáló házirendje", + "email_exists_alert": { + "text": "A megadott e-mail-cím már hozzá lett adva ehhez a fiókhoz", + "title": "Az e-mail-cím már létezik" + }, + "email_exists_error": "A megadott e-mail-cím már hozzá lett adva ehhez a fiókhoz", + "email_field_help": "Alternatív e-mail-cím hozzáadása, mellyel hozzáférhet ehhez a fiókhoz.", + "email_field_label": "E-mail-cím hozzáadása", + "email_in_use_error": "A megadott e-mail-cím már használatban van", + "email_invalid_alert": { + "text": "A megadott e-mail-cím érvénytelen", + "title": "Érvénytelen e-mail-cím" + }, + "email_invalid_error": "A megadott e-mail-cím érvénytelen", + "incorrect_password_error": "Helytelen jelszó, próbálja újra", + "password_confirmation": "Erősítse meg a fiókja jelszavát az e-mail-cím hozzáadásához" + }, + "app_sessions_list": { + "error": "Az alkalmazás munkameneteinek betöltése sikertelen", + "heading": "Alkalmazások" + }, + "browser_session_details": { + "current_badge": "Jelenlegi", + "session_details_title": "Munkamenet" + }, + "browser_sessions_overview": { + "body:one": "{{count}} aktív munkamenet", + "body:other": "{{count}} aktív munkamenet", + "heading": "Böngészők", + "no_active_sessions": { + "default": "Még egyetlen webböngészőben sem jelentkezett be.", + "inactive_90_days": "Az összes munkamenete aktív volt az elmúlt 90 napban." + }, + "view_all_button": "Összes megtekintése" + }, + "compat_session_detail": { + "client_details_title": "Kliensinformációk", + "name": "Név", + "session_details_title": "Munkamenet" + }, + "device_type_icon_label": { + "desktop": "Asztali számítógép", + "mobile": "Mobil", + "pc": "Számítógép", + "tablet": "Táblagép", + "unknown": "Ismeretlen eszköztípus", + "web": "Web" + }, + "email_in_use": { + "heading": "A(z) {{email}} e-mail-cím már használatban van." + }, + "end_session_button": { + "confirmation_modal_title": "Biztos, hogy befejezi a munkamenetet?", + "text": "Eszköz eltávolítása" + }, + "error": { + "hideDetails": "Részletek elrejtése", + "showDetails": "Részletek megjelenítése", + "subtitle": "Váratlan hiba történt. Próbálja újra.", + "title": "Valamilyen hiba történt" + }, + "error_boundary_title": "Valamilyen hiba történt", + "errors": { + "field_required": "Ez a mező kötelező", + "rate_limit_exceeded": "Túl sok kérést adott fel egy rövid időszak alatt. Várjon néhány percet, és próbálja újra." + }, + "last_active": { + "active_date": "{{relativeDate}} aktív", + "active_now": "Jelenleg aktív", + "inactive_90_days": "90+ napja inaktív" + }, + "nav": { + "devices": "Eszközök", + "profile": "Profil", + "sessions": "Munkamenetek", + "settings": "Beállítások" + }, + "not_found_alert_title": "Nem található.", + "not_logged_in_alert": "Nincs bejelentkezve.", + "oauth2_client_detail": { + "details_title": "Kliensinformációk", + "id": "Kliensazonosító", + "name": "Név", + "policy": "Házirend", + "terms": "Szolgáltatás feltételei" + }, + "oauth2_session_detail": { + "client_details_name": "Név", + "client_title": "Kliensinformációk", + "session_details_title": "Munkamenet" + }, + "pagination_controls": { + "total": "Összesen: {{totalCount}}" + }, + "password_change": { + "current_password_label": "Jelenlegi jelszó", + "failure": { + "description": { + "account_locked": "A fiókja zárolva van, és jelenleg nem állítható helyre. Ha erre nem számított, akkor lépjen kapcsolatba a kiszolgáló rendszergazdájával.", + "expired_recovery_ticket": "A helyreállítási hivatkozás lejárt. Kezdje az elejéről a fiókja helyreállítási folyamatát.", + "invalid_new_password": "A választott új jelszó érvénytelen; lehet, hogy nem felel meg a beállított biztonsági házirendnek.", + "no_current_password": "Nincs jelenlegi jelszava.", + "no_such_recovery_ticket": "A helyreállítási hivatkozás érvénytelen. Ha a helyreállítási üzenetből másolta ki a hivatkozást, akkor ellenőrizze, hogy a teljes hivatkozást átmásolta-e.", + "password_changes_disabled": "A jelszómódosítás le van tiltva.", + "recovery_ticket_already_used": "A helyreállítási hivatkozás már fel lett használva. Többé nem használható.", + "unspecified": "Ez ideiglenes probléma is lehet, így próbálja újra később. Ha a probléma továbbra is fennáll, lépjen kapcsolatba a kiszolgáló rendszergazdájával.", + "wrong_password": "A jelenlegi jelszóként megadott jelszó helytelen. Próbálja újra." + }, + "title": "A jelszó frissítése sikertelen" + }, + "new_password_again_label": "Adja meg a jelszót újból", + "new_password_label": "Új jelszó", + "passwords_match": "A jelszavak megegyeznek!", + "passwords_no_match": "A jelszavak nem egyeznek", + "subtitle": "Válasszon egy új jelszót a fiókjához.", + "success": { + "description": "A jelszava sikeresen frissült.", + "title": "Jelszó frissítve" + }, + "title": "Jelszó módosítása" + }, + "password_reset": { + "consumed": { + "subtitle": "Új jelszó létrehozásához kezdje elölről, és válassza a „Elfelejtett jelszó” lehetőséget.", + "title": "A jelszóhelyreállítási hivatkozás már fel lett használva" + }, + "expired": { + "resend_email": "Levél újraküldése", + "subtitle": "Új levél kérése, amely ide lesz elküldve: {{email}}", + "title": "A jelszóvisszaállítási hivatkozás lejárt" + }, + "subtitle": "Válasszon egy új jelszót a fiókjához.", + "title": "Jelszó visszaállítása" + }, + "password_strength": { + "placeholder": "Jelszó erőssége", + "score": { + "0": "Rendkívül gyenge jelszó", + "1": "Nagyon gyenge jelszó", + "2": "Gyenge jelszó", + "3": "Erős jelszó", + "4": "Nagyon erős jelszó" + }, + "suggestion": { + "all_uppercase": "Használjon nagybetűket, de nem mindnél.", + "another_word": "Adjon hozzá néhány kevésbé gyakori szót.", + "associated_years": "Kerülje az Önhöz köthető éveket.", + "capitalization": "Ne csak az első betű legyen nagybetűs.", + "dates": "Kerülje az Önhöz köthető dátumokat és éveket.", + "l33t": "Kerülje a kiszámítható betűhelyettesítéseket, mint az „a” helyetti „@”.", + "longer_keyboard_pattern": "Használjon hosszabb billentyűzetmintát, és többször módosítsa a gépelési irányt.", + "no_need": "Anélkül is hozhat létre erős jelszavakat, hogy szimbólumokat, számokat vagy nagybetűket használna.", + "pwned": "Ha máshol is használja ezt a jelszót, akkor változtassa meg.", + "recent_years": "Kerülje a közelmúltbeli éveket.", + "repeated": "Kerülje az ismétlődő szavakat és karaktereket.", + "reverse_words": "Kerülje a gyakori szavak fordított betűzését.", + "sequences": "Kerülje a gyakori karaktersorozatokat.", + "use_words": "Használjon több szót, de kerülje a gyakori kifejezéseket." + }, + "too_weak": "Ez a jelszó túl gyenge", + "warning": { + "common": "Ez egy gyakran használt jelszó.", + "common_names": "A gyakori nevek és vezetéknevek könnyen kitalálhatóak.", + "dates": "A dátumok könnyen kitalálhatóak.", + "extended_repeat": "Az ismétlődő karaktersorozatok, mint az „abcabcabc” könnyen kitalálhatóak.", + "key_pattern": "A rövid billentyűzetminták könnyen kitalálhatóak.", + "names_by_themselves": "Az egy nevet vagy vezetéknevet tartalmazó jelszók könnyen kitalálhatók.", + "pwned": "A jelszava egy adatvédelmi incidensben kikerült az internetre.", + "recent_years": "A közelmúltbeli évek könnyen kitalálhatóak.", + "sequences": "A gyakori karaktersorozatok, mint az „abc”, könnyen kitalálhatóak.", + "similar_to_common": "Ez hasonlít egy gyakran használt jelszóhoz.", + "simple_repeat": "Az ismétlődő karakterek, mint az „aaa” könnyen kitalálhatóak.", + "straight_row": "A billentyűzeten szereplő karaktersorok könnyen kitalálhatóak.", + "top_hundred": "Ez egy gyakran használt jelszó.", + "top_ten": "Ez egy nagyon gyakran használt jelszó.", + "user_inputs": "Ne legyen benne személyes, vagy az oldallal kapcsolatos adat.", + "word_by_itself": "Az egy szavas jelszavak könnyen kitalálhatóak." + } + }, + "reset_cross_signing": { + "button": "Személyazonosság visszaállítása", + "cancelled": { + "description_1": "A folytatáshoz bezárhatja ezt az ablakot, és visszatérhet az alkalmazáshoz.", + "description_2": "Ha mindenhonnan kijelentkezett, és nem emlékszik a helyreállítási kódjára, akkor vissza kell állítania a személyazonosságát.", + "heading": "Személyazonosság visszaállítása megszakítva." + }, + "description": "Ha nincs bejelentkezve egyetlen más eszközön sem, és elvesztette a helyreállítási kulcsát, akkor az alkalmazás használatának folytatásához alaphelyzetbe kell állítania a személyazonosságát.", + "effect_list": { + "negative_1": "El fogja veszíteni a meglévő üzenetelőzményeit", + "negative_2": "Újból ellenőriznie kell az összes meglévő eszközét és kapcsolatát", + "neutral_1": "El fogja veszíteni a csak a kiszolgálón tárolt üzenetelőzményeit", + "neutral_2": "Újból ellenőriznie kell az összes meglévő eszközét és kapcsolatát", + "positive_1": "A fiókja részletei, a névjegyei, a beállításai és a csevegéslistája meg lesz tartva" + }, + "failure": { + "description": "Ez ideiglenes probléma is lehet, így próbálja újra később. Ha a probléma továbbra is fennáll, lépjen kapcsolatba a kiszolgáló rendszergazdájával.", + "heading": "A kriptográfiai személyazonossága alaphelyzetbe állításának engedélyezése sikertelen", + "title": "A kriptográfiai személyazonosságának engedélyezése sikertelen" + }, + "finish_reset": "Alaphelyzetbe állítás befejezése", + "heading": "Állítsa alaphelyzetbe a személyazonosságát, ha semmilyen más módon nem tudja megerősíteni", + "start_reset": "Alaphelyzetbe állítás elkezdése", + "success": { + "description": "A személyazonosság alaphelyzetbe állítása engedélyezve a következő {{minutes}} percre. A folytatáshoz bezárhatja azt az ablakot, és visszatérhet az alkalmazáshoz.", + "heading": "Személyazonosság sikeresen alaphelyzetbe állítva. A folyamat befejezéséhez térjen vissza az alkalmazáshoz.", + "title": "A kriptográfiai személyazonossága alaphelyzetbe állítása ideiglenesen engedélyezve" + }, + "warning": "Csak akkor állítsa alaphelyzetbe a személyazonosságát, ha nem fér hozzá más bejelentkezett eszközhöz, és elveszítette a helyreállítási kulcsát." + }, + "selectable_session": { + "label": "Munkamenet kiválasztása" + }, + "session": { + "client_id_label": "Kliensazonosító", + "current": "Jelenlegi", + "current_badge": "Jelenlegi", + "device_id_label": "Eszközazonosító", + "finished_date": "Befejezve: ", + "finished_label": "Befejezve", + "generic_browser_session": "Böngésző-munkamenet", + "id_label": "Azonosító", + "ip_label": "IP-cím", + "last_active_label": "Legutóbb aktív", + "last_auth_label": "Legutóbbi hitelesítés", + "name_for_platform": "{{name}} erre: {{platform}}", + "scopes_label": "Hatókörök", + "signed_in_date": "Bejelentkezve: ", + "signed_in_label": "Bejelentkezve", + "title": "Eszköz részletei", + "unknown_browser": "Ismeretlen böngésző", + "unknown_device": "Ismeretlen eszköz", + "uri_label": "URI", + "user_id_label": "Felhasználóazonosító", + "username_label": "Felhasználónév" + }, + "session_detail": { + "alert": { + "button": "Vissza", + "text": "Ez a munkamenet nem létezik, vagy már nem aktív.", + "title": "A munkamenet nem található: {{deviceId}}" + } + }, + "unknown_route": "Ismeretlen útvonal: {{route}}", + "unverified_email_alert": { + "button": "Áttekintés és ellenőrzés", + "text:one": "{{count}} nem ellenőrzött e-mail-címe van.", + "text:other": "{{count}} nem ellenőrzött e-mail-címe van.", + "title": "Nem megerősített e-mail-cím" + }, + "user_email": { + "cant_delete_primary": "Válasszon egy másik elsődleges e-mail-címet, hogy törölhesse ezt.", + "delete_button_confirmation_modal": { + "action": "E-mail-cím törlése", + "body": "Törli ezt az e-mail-címet?", + "incorrect_password": "Helytelen jelszó, próbálja újra", + "password_confirmation": "Erősítse meg a fiókja jelszavát az e-mail-cím törléséhez" + }, + "delete_button_title": "E-mail-cím eltávolítása", + "email": "E-mail", + "make_primary_button": "Elsődlegessé tétel", + "not_verified": "Nincs ellenőrizve", + "primary_email": "Elsődleges e-mail-cím", + "retry_button": "Kód újraküldése", + "unverified": "Ellenőrizetlen" + }, + "user_email_list": { + "heading": "Levelek", + "no_primary_email_alert": "Nincs elsődleges e-mail-cím" + }, + "user_greeting": { + "error": "A felhasználó betöltése sikertelen" + }, + "user_name": { + "display_name_field_label": "Megjelenítési név" + }, + "user_sessions_overview": { + "active_sessions:one": "{{count}} aktív munkamenet", + "active_sessions:other": "{{count}} aktív munkamenet", + "heading": "Hol jelentkezett be", + "no_active_sessions": { + "default": "Egyetlen alkalmazásba sincs bejelentkezve.", + "inactive_90_days": "Az összes munkamenete aktív volt az elmúlt 90 napban." + } + }, + "verify_email": { + "code_expired_alert": { + "description": "A kód lejárt. Kérjen egy újat.", + "title": "A kód lejárt" + }, + "code_field_error": "A kód nem ismerhető fel", + "code_field_label": "6 számjegyű kód", + "code_field_wrong_shape": "A kódnak 6 számjegyűnek kell lennie", + "email_sent_alert": { + "description": "Adja meg alább az új kódot.", + "title": "Új kód küldve" + }, + "enter_code_prompt": "Adja meg az ide küldött 6 számjegyű kódot: {{email}}", + "heading": "E-mail-cím ellenőrzése", + "invalid_code_alert": { + "description": "A folytatáshoz ellenőrizze az e-mail-címére küldött kódot, és frissítse a lenti mezőket.", + "title": "Hibás kódot adott meg" + }, + "resend_code": "Kód újraküldése", + "resend_email": "Levél újraküldése", + "sent": "Elküldve!", + "unknown_email": "Ismeretlen e-mail-cím" + } + }, + "mas": { + "scope": { + "edit_profile": "Profil és elérhetőségek szerkesztése", + "manage_sessions": "Eszközök és munkamenetek kezelése", + "mas_admin": "Bármely felhasználó kezelése a matrix-authentication-service szolgáltatásban", + "send_messages": "Új üzenetek küldése az Ön nevében", + "synapse_admin": "A Synapse Matrix-kiszolgáló kezelése", + "view_messages": "Meglévő üzenetek és adatok megtekintése", + "view_profile": "Saját profilinformációk és kapcsolati részletek megtekintése" + } + } +} \ No newline at end of file diff --git a/frontend/locales/nb-NO.json b/frontend/locales/nb-NO.json new file mode 100644 index 000000000..ad2e7b8f6 --- /dev/null +++ b/frontend/locales/nb-NO.json @@ -0,0 +1,395 @@ +{ + "action": { + "back": "Tilbake", + "cancel": "Avbryt", + "clear": "Tøm", + "close": "Lukk", + "collapse": "Skjul", + "confirm": "Bekreft", + "continue": "Fortsett", + "edit": "Rediger", + "expand": "Utvid", + "save": "Lagre", + "save_and_continue": "Lagre og fortsett", + "sign_out": "Logg ut", + "start_over": "Begynn på nytt" + }, + "branding": { + "privacy_policy": { + "alt": "Lenke til tjenestens personvernerklæring", + "link": "Personvernerklæring" + }, + "terms_and_conditions": { + "alt": "Lenke til tjenestens vilkår og betingelser", + "link": "Vilkår og betingelser" + } + }, + "common": { + "add": "Legg til", + "e2ee": "Ende-til-ende-kryptering", + "error": "Feil", + "loading": "Laster inn...", + "next": "Neste", + "password": "Passord", + "previous": "Forrige", + "saved": "Lagret", + "saving": "Lagrer…" + }, + "frontend": { + "account": { + "account_password": "Passord for konto", + "contact_info": "Kontaktopplysninger", + "delete_account": { + "alert_description": "Denne kontoen vil bli slettet permanent, og du vil ikke lenger ha tilgang til noen av meldingene dine.", + "alert_title": "Du er i ferd med å miste alle dataene dine", + "button": "Slett konto", + "dialog_description": "Bekreft at du ønsker å slette kontoen din:\n\n\nDu vil ikke kunne aktivere kontoen din på nytt\nDu vil ikke lenger kunne logge på\nIngen vil kunne gjenbruke brukernavnet ditt (MXID), heller ikke du\nDu vil forlate alle rom og direktemeldinger du er en del av\nDu vil bli fjernet fra identitetsserveren, og ingen vil kunne finne deg med e-postadressen eller telefonnummeret ditt\n\nDine gamle meldinger vil fortsatt være synlige for personer som har mottatt dem. Ønsker du å skjule dine sendte meldinger for personer som blir med i rommene i fremtiden?", + "dialog_title": "Slett denne kontoen?", + "erase_checkbox_label": "Ja, skjul alle meldingene mine for nye medlemmer", + "incorrect_password": "Feil passord, prøv igjen", + "mxid_label": "Bekreft din Matrix ID ({{ mxid }})", + "mxid_mismatch": "Denne verdien samsvarer ikke med din Matrix ID", + "password_label": "Skriv inn passordet ditt for å fortsette" + }, + "edit_profile": { + "display_name_help": "Dette er det andre vil se uansett hvor du er logget inn.", + "display_name_label": "Visningsnavn", + "title": "Rediger profil", + "username_label": "Brukernavn" + }, + "password": { + "change": "Endre passord", + "change_disabled": "Endring av passord er deaktivert av administrator.", + "label": "Passord" + }, + "sign_out": { + "button": "Logg av konto", + "dialog": "Logg ut av denne kontoen?" + }, + "title": "Din konto" + }, + "add_email_form": { + "email_denied_alert": { + "text": "Den angitte e-postadressen er ikke tillatt av serverpolicyen.", + "title": "E-post avvist av policy" + }, + "email_denied_error": "Den angitte e-postadressen er ikke tillatt av serverpolicyen", + "email_exists_alert": { + "text": "Den angitte e-postadressen er allerede lagt til denne kontoen", + "title": "E-posten finnes allerede" + }, + "email_exists_error": "Den angitte e-postadressen er allerede lagt til denne kontoen", + "email_field_help": "Legg til en alternativ e-postadresse du kan bruke for å få tilgang til denne kontoen.", + "email_field_label": "Legg til e-post", + "email_in_use_error": "Den angitte e-postadressen er allerede i bruk", + "email_invalid_alert": { + "text": "Den angitte e-postadressen er ugyldig", + "title": "Ugyldig e-post" + }, + "email_invalid_error": "Den angitte e-postadressen er ugyldig", + "incorrect_password_error": "Feil passord, prøv igjen", + "password_confirmation": "Bekreft passordet ditt for å legge til denne e-postadressen" + }, + "app_sessions_list": { + "error": "Kunne ikke laste inn appsesjoner", + "heading": "Applikasjoner" + }, + "browser_session_details": { + "current_badge": "Nåværende", + "session_details_title": "Sesjon" + }, + "browser_sessions_overview": { + "body:one": "{{count}} aktiv sesjon", + "body:other": "{{count}} aktive sesjoner", + "heading": "Nettlesere", + "no_active_sessions": { + "default": "Du er ikke logget inn på noen nettlesere.", + "inactive_90_days": "Alle sesjonene dine har vært aktive de siste 90 dagene." + }, + "view_all_button": "Vis alle" + }, + "compat_session_detail": { + "client_details_title": "Klient informasjon", + "name": "Navn", + "session_details_title": "Sesjon" + }, + "device_type_icon_label": { + "desktop": "Skrivebord", + "mobile": "Mobil", + "pc": "Datamaskin", + "tablet": "Nettbrett", + "unknown": "Ukjent enhetstype", + "web": "Web" + }, + "email_in_use": { + "heading": "E-postadressen {{email}} er allerede i bruk." + }, + "end_session_button": { + "confirmation_modal_title": "Er du sikker på at du vil avslutte denne sesjonen?", + "text": "Fjern enheten" + }, + "error": { + "hideDetails": "Skjul detaljer", + "showDetails": "Vis detaljer", + "subtitle": "Det oppstod en uventet feil. Vennligst prøv igjen.", + "title": "Noe gikk galt" + }, + "error_boundary_title": "Noe gikk galt", + "errors": { + "field_required": "Dette feltet er obligatorisk", + "rate_limit_exceeded": "Du har kommet med for mange forespørsler på kort tid. Vent noen minutter og prøv igjen." + }, + "last_active": { + "active_date": "Aktiv {{relativeDate}}", + "active_now": "Aktiv nå", + "inactive_90_days": "Inaktiv i 90+ dager" + }, + "nav": { + "devices": "Enheter", + "profile": "Profil", + "sessions": "Sesjoner", + "settings": "Innstillinger" + }, + "not_found_alert_title": "Ikke funnet.", + "not_logged_in_alert": "Du er ikke innlogget.", + "oauth2_client_detail": { + "details_title": "Klient informasjon", + "id": "Klient-ID", + "name": "Navn", + "policy": "Retningslinjer", + "terms": "Vilkår for bruk" + }, + "oauth2_session_detail": { + "client_details_name": "Navn", + "client_title": "Klient informasjon", + "session_details_title": "Sesjon" + }, + "pagination_controls": { + "total": "Totalt: {{totalCount}}" + }, + "password_change": { + "current_password_label": "Nåværende passord", + "failure": { + "description": { + "account_locked": "Kontoen din er låst og kan ikke gjenopprettes på dette tidspunktet. Hvis dette ikke er forventet, kan du kontakte serveradministratoren din.", + "expired_recovery_ticket": "Gjenopprettingslenken er utløpt. Start kontogjenopprettingsprosessen på nytt.", + "invalid_new_password": "Det nye passordet du valgte er ugyldig. Det kan hende at den ikke oppfyller den gjeldende sikkerhetspolicyen.", + "no_current_password": "Du har ikke et gjeldende passord.", + "no_such_recovery_ticket": "Gjenopprettingslenken er ugyldig. Hvis du kopierte lenken fra gjenopprettingseposten, vennligst sjekk at hele lenken ble kopiert.", + "password_changes_disabled": "Endring av passord er deaktivert.", + "recovery_ticket_already_used": "Gjenopprettingslenken er allerede brukt. Den kan ikke brukes igjen.", + "unspecified": "Dette kan være et midlertidig problem, så prøv igjen senere. Hvis problemet vedvarer, vennligst kontakt serveradministratoren din.", + "wrong_password": "Passordet du oppga som ditt nåværende passord er feil. Prøv igjen." + }, + "title": "Kunne ikke oppdatere passordet" + }, + "new_password_again_label": "Skriv inn nytt passord igjen", + "new_password_label": "Nytt passord", + "passwords_match": "Passordene stemmer overens!", + "passwords_no_match": "Passord stemmer ikke overens", + "subtitle": "Velg et nytt passord for kontoen din.", + "success": { + "description": "Passordet ditt har blitt oppdatert.", + "title": "Passord oppdatert" + }, + "title": "Bytt passordet ditt" + }, + "password_reset": { + "consumed": { + "subtitle": "For å opprette et nytt passord, start på nytt og velg «Glemt passord».", + "title": "Lenken for å tilbakestille passordet ditt har allerede blitt brukt" + }, + "expired": { + "resend_email": "Send e-post på nytt", + "subtitle": "Be om en ny e-post som vil bli sendt til: {{email}}", + "title": "Lenken for å tilbakestille passordet ditt er utløpt" + }, + "subtitle": "Velg et nytt passord for kontoen din.", + "title": "Tilbakestill passordet ditt" + }, + "password_strength": { + "placeholder": "Passordstyrke", + "score": { + "0": "Ekstremt svakt passord", + "1": "Veldig svakt passord", + "2": "Svakt passord", + "3": "Sterkt passord", + "4": "Veldig sterkt passord" + }, + "suggestion": { + "all_uppercase": "Bruk store bokstaver, men ikke for alle bokstaver.", + "another_word": "Legg til flere ord som er mindre vanlige.", + "associated_years": "Unngå år som er knyttet til deg", + "capitalization": "Bruk stor bokstav på mer enn den første bokstaven.", + "dates": "Unngå datoer og år som er knyttet til deg", + "l33t": "Unngå forutsigbare bokstavbytter som \"@\" i stedet for \"a\".", + "longer_keyboard_pattern": "Bruk lengre tastaturmønstre og endre skriveretning flere ganger.", + "no_need": "Du kan lage sterke passord uten å bruke symboler, tall eller store bokstaver.", + "pwned": "Hvis du bruker dette passordet andre steder, bør du endre det.", + "recent_years": "Unngå nylige år", + "repeated": "Unngå gjentatte ord og tegn.", + "reverse_words": "Unngå omvendt staving av vanlige ord.", + "sequences": "Unngå vanlige tegnsekvenser.", + "use_words": "Bruk flere ord, men unngå vanlige fraser." + }, + "too_weak": "Dette passordet er for svakt", + "warning": { + "common": "Dette er et ofte brukt passord.", + "common_names": "Vanlige navn og etternavn er lette å gjette seg til.", + "dates": "Datoer er enkle å gjette seg til.", + "extended_repeat": "Gjentatte tegnmønstre som \"abcabcabc\" er lette å gjette seg til.", + "key_pattern": "Korte tastaturmønstre er enkle å gjette.", + "names_by_themselves": "Enkeltnavn eller etternavn er lette å gjette.", + "pwned": "Passordet ditt ble eksponert ved et datainnbrudd på Internett.", + "recent_years": "De siste årene er enkle å gjette seg til.", + "sequences": "Vanlige tegnsekvenser som «abc» er enkle å gjette.", + "similar_to_common": "Dette ligner på et ofte brukt passord.", + "simple_repeat": "Gjentatte tegn som \"aaa\" er lette å gjette.", + "straight_row": "Rette tasterader på tastaturet er enkle å gjette seg til.", + "top_hundred": "Dette er et ofte brukt passord.", + "top_ten": "Dette er et mye brukt passord.", + "user_inputs": "Det skal ikke være noen personlige eller siderelaterte data.", + "word_by_itself": "Enkeltord er lette å gjette." + } + }, + "reset_cross_signing": { + "button": "Tilbakestill identitet", + "cancelled": { + "description_1": "Du kan lukke dette vinduet og gå tilbake til appen for å fortsette.", + "description_2": "Hvis du er logget av overalt og ikke husker gjenopprettingskoden, må du fortsatt tilbakestille identiteten din.", + "heading": "Tilbakestilling av identitet kansellert." + }, + "description": "Hvis du ikke er logget på andre enheter, og du har mistet gjenopprettingsnøkkelen, må du tilbakestille identiteten din for å fortsette å bruke appen.", + "effect_list": { + "negative_1": "Du vil miste din eksisterende meldingshistorikk", + "negative_2": "Du må bekrefte alle eksisterende enheter og kontakter på nytt", + "neutral_1": "Du vil miste all meldingshistorikk som bare er lagret på serveren", + "neutral_2": "Du må bekrefte alle eksisterende enheter og kontakter på nytt", + "positive_1": "Dine kontodetaljer, kontakter, preferanser og chatteliste vil bli beholdt" + }, + "failure": { + "description": "Dette kan være et midlertidig problem, så prøv igjen senere. Hvis problemet vedvarer, vennligst kontakt serveradministratoren din.", + "heading": "Kunne ikke tillate tilbakestilling av kryptoidentitet", + "title": "Kunne ikke tillate kryptoidentitet" + }, + "finish_reset": "Fullfør tilbakestillingen", + "heading": "Tilbakestill identiteten din i tilfelle du ikke kan bekrefte på en annen måte", + "start_reset": "Start tilbakestilling", + "success": { + "description": "Tilbakestillingen av identiteten er godkjent for de neste {{minutes}} minuttene. Du kan lukke dette vinduet og gå tilbake til appen for å fortsette.", + "heading": "Identitet tilbakestilt. Gå tilbake til appen for å fullføre prosessen.", + "title": "Tilbakestilling av kryptoidentitet midlertidig tillatt" + }, + "warning": "Tilbakestill identiteten din bare hvis du ikke har tilgang til en annen pålogget enhet og du har mistet gjenopprettingsnøkkelen." + }, + "selectable_session": { + "label": "Velg sesjon" + }, + "session": { + "client_id_label": "Klient-ID", + "current": "Nåværende", + "current_badge": "Nåværende", + "device_id_label": "Enhets-ID", + "finished_date": "Fullført ", + "finished_label": "Fullført", + "generic_browser_session": "Nettlesersesjon", + "id_label": "ID", + "ip_label": "IP-adresse", + "last_active_label": "Sist aktiv", + "last_auth_label": "Siste autentisering", + "name_for_platform": "{{name}} for {{platform}}", + "scopes_label": "Omfang", + "signed_in_date": "Logget på ", + "signed_in_label": "Logget på", + "title": "Detaljer om enheten", + "unknown_browser": "Ukjent nettleser", + "unknown_device": "Ukjent enhet", + "uri_label": "Uri", + "user_id_label": "Bruker ID", + "username_label": "Brukernavn" + }, + "session_detail": { + "alert": { + "button": "Gå tilbake", + "text": "Denne sesjonen finnes ikke, eller er ikke lenger aktiv.", + "title": "Finner ikke sesjonen: {{deviceId}}" + } + }, + "unknown_route": "Ukjent rute {{route}}", + "unverified_email_alert": { + "button": "Gjennomgå og verifiser", + "text:one": "Du har {{count}} ubekreftet e-postadresse.", + "text:other": "Du har {{count}} ubekreftede e-postadresser.", + "title": "Ubekreftet e-post" + }, + "user_email": { + "cant_delete_primary": "Velg en annen primær e-postadresse for å slette denne.", + "delete_button_confirmation_modal": { + "action": "Slett e-post", + "body": "Vil du slette denne e-posten?", + "incorrect_password": "Feil passord, prøv igjen", + "password_confirmation": "Bekreft kontopassordet ditt for å slette denne e-postadressen" + }, + "delete_button_title": "Fjern e-postadresse", + "email": "E-post", + "make_primary_button": "Gjøre til primær", + "not_verified": "Ikke verifisert", + "primary_email": "Primær e-postadresse", + "retry_button": "Send kode på nytt", + "unverified": "Ikke verifisert" + }, + "user_email_list": { + "heading": "E-poster", + "no_primary_email_alert": "Ingen primær e-postadresse" + }, + "user_greeting": { + "error": "Kunne ikke laste inn bruker" + }, + "user_name": { + "display_name_field_label": "Visningsnavn" + }, + "user_sessions_overview": { + "active_sessions:one": "{{count}} aktiv sesjon", + "active_sessions:other": "{{count}} aktive sesjoner", + "heading": "Hvor du er logget inn", + "no_active_sessions": { + "default": "Du er ikke logget på noen applikasjoner.", + "inactive_90_days": "Alle sesjonene dine har vært aktive de siste 90 dagene." + } + }, + "verify_email": { + "code_expired_alert": { + "description": "Koden er utløpt. Be om en ny kode.", + "title": "Koden er utløpt" + }, + "code_field_error": "Kode ikke gjenkjent", + "code_field_label": "6-sifret kode", + "code_field_wrong_shape": "Koden må være 6 sifre", + "email_sent_alert": { + "description": "Skriv inn den nye koden nedenfor.", + "title": "Ny kode sendt" + }, + "enter_code_prompt": "Skriv inn den 6-sifrede koden sendt til: {{email}}", + "heading": "Bekreft e-postadressen din", + "invalid_code_alert": { + "description": "Sjekk koden som er sendt til e-posten din, og oppdater feltene nedenfor for å fortsette.", + "title": "Du skrev inn feil kode" + }, + "resend_code": "Send kode på nytt", + "resend_email": "Send e-post på nytt", + "sent": "Sendt!", + "unknown_email": "Ukjent e-postadresse" + } + }, + "mas": { + "scope": { + "edit_profile": "Rediger din profil og kontaktdetaljer", + "manage_sessions": "Administrer enhetene og sesjonene dine", + "mas_admin": "Administrer alle brukere på matrix-authentication-service", + "send_messages": "Send nye meldinger på dine vegne", + "synapse_admin": "Administrer Synapse-hjemmeserveren", + "view_messages": "Se dine eksisterende meldinger og data", + "view_profile": "Se din profilinformasjon og kontaktdetaljer" + } + } +} \ No newline at end of file diff --git a/translations/hu.json b/translations/hu.json new file mode 100644 index 000000000..6fdff188b --- /dev/null +++ b/translations/hu.json @@ -0,0 +1,265 @@ +{ + "action": { + "back": "Vissza", + "cancel": "Mégse", + "continue": "Folytatás", + "create_account": "Fiók létrehozása", + "sign_in": "Bejelentkezés", + "sign_out": "Kijelentkezés", + "skip": "Kihagyás", + "start_over": "Újrakezdés", + "submit": "Elküldés" + }, + "app": { + "human_name": "Matrix hitelesítési szolgáltatás", + "name": "matrix-authentication-service", + "technical_description": "OpenID Connect felfedezési dokumentum: %(discovery_url)s" + }, + "branding": { + "privacy_policy": { + "alt": "Hivatkozás a szolgáltatás adatvédelmi irányelveire", + "link": "Adatvédelmi irányelvek" + }, + "terms_and_conditions": { + "alt": "Hivatkozás a szolgáltatási feltételekre", + "link": "Szolgáltatási feltételek" + } + }, + "common": { + "display_name": "Megjelenítési név", + "email_address": "E-mail-cím", + "loading": "Betöltés…", + "mxid": "Matrix-azonosító", + "password": "Jelszó", + "password_confirm": "Jelszó megerősítése", + "username": "Felhasználói név" + }, + "error": { + "unexpected": "Váratlan hiba" + }, + "mas": { + "account": { + "deactivated": { + "description": "Ez a fiók (%(mxid)s) törölve lett. Ha erre nem számított, vegye fel a kapcsolatot a kiszolgáló rendszergazdájával.", + "heading": "Fiók törölve" + }, + "locked": { + "description": "Ez a fiók (%(mxid)s) zárolva lett. Ha erre nem számított, vegye fel a kapcsolatot a kiszolgáló rendszergazdájával.", + "heading": "Fiók zárolva" + }, + "logged_out": { + "description": "Ez a munkamenet véget ért. Jelentkezzen ki, hogy újra bejelentkezhessen.", + "heading": "Munkamenet befejezve" + } + }, + "add_email": { + "description": "Adjon meg egy e-mail-címet a fiókja helyreállításához, arra az esetre, ha elveszítené a hozzáférését.", + "heading": "E-mail-cím hozzáadása" + }, + "back_to_homepage": "Vissza a kezdőlaphoz", + "captcha": { + "noscript": "Ezt az űrlapot CAPTCHA védi, és az elküldéséhez engedélyezni kell a JavaScriptet. Engedélyezze a böngészőben a JavaScriptet, és töltse újra az oldalt." + }, + "change_password": { + "change": "Jelszó módosítása", + "confirm": "Jelszó megerősítése", + "current": "Jelenlegi jelszó", + "description": "Ez módosítja a fiókja jelszavát.", + "heading": "Saját jelszó módosítása", + "new": "Új jelszó" + }, + "choose_display_name": { + "description": "Ez az a név, melyet a többi ember látni fog. Ezt bármikor módosíthatja.", + "headline": "Válasszon megjelenítési nevet" + }, + "consent": { + "client_wants_access": "A(z) %(client_name)s (itt: %(redirect_uri)s) hozzá akar férni a fiókjához.", + "heading": "Engedélyezi a hozzáférést a fiókjához?", + "make_sure_you_trust": "Győződjön meg arról, hogy megbízik a(z) %(client_name)s kliensben.", + "this_will_allow": "Ez lehetővé teszi, hogy a(z) %(client_name)s a következőket tegye:", + "you_may_be_sharing": "Bizalmas információkat oszthat meg ezzel az oldallal vagy alkalmazással." + }, + "device_card": { + "access_requested": "Hozzáférés kérve", + "device_code": "Kód", + "generic_device": "Eszköz", + "ip_address": "IP-cím" + }, + "device_code_link": { + "description": "Eszköz összekapcsolása", + "headline": "Adja meg az eszközén megjelenített kódot" + }, + "device_consent": { + "another_device_access": "Egy másik eszköz akarja elérni a fiókját.", + "denied": { + "description": "Elutasította a(z) %(client_name)s hozzáférését. Bezárhatja ezt az ablakot.", + "heading": "Hozzáférés megtagadva" + }, + "granted": { + "description": "Megadta a(z) %(client_name)s hozzáférését. Bezárhatja ezt az ablakot.", + "heading": "Hozzáférés megadva" + } + }, + "email_in_use": { + "description": "Ha elfelejtette a fiókja hitelesítő adatait, akkor helyreállíthatja a fiókját. Újra is kezdheti egy másik e-mail-címmel.", + "title": "A(z) %(email)s e-mail-cím már használatban van" + }, + "emails": { + "greeting": "Kedves %(username)s!", + "recovery": { + "click_button": "Kattintson a lenti gombra az új jelszava létrehozásához:", + "copy_link": "Másolja a következő hivatkozást, és illessze be a böngészőbe egy új jelszó létrehozásához:", + "create_new_password": "Új jelszó létrehozása", + "fallback": "A gomb nem működik?", + "headline": "Jelszóvisszaállítást kért a(z) %(client_name)s fiókjához.", + "subject": "Fiókjelszó visszaállítása (%(mxid)s)", + "you_can_ignore": "Ha nem kért új jelszót, akkor figyelmen kívül hagyhatja ezt a levelet. A jelenlegi jelszava továbbra is működni fog." + }, + "verify": { + "body_html": "Az ellenőrzőkódja az e-mail-címe megerősítéséhez: %(code)s", + "body_text": "Az ellenőrzőkódja az e-mail-címe megerősítéséhez: %(code)s", + "subject": "Az e-mail-elleőrzőkódja: %(code)s" + } + }, + "errors": { + "captcha": "A CAPTCHA ellenőrzés sikertelen, próbálja újra", + "denied_policy": "Házirend alapján elutasítva: %(policy)s", + "email_banned": "Az e-mail-címet a kiszolgáló-házirend tiltja", + "email_domain_banned": "Az e-mail-tartományt a kiszolgáló-házirend tiltja", + "email_domain_not_allowed": "Az e-mail-tartományt nem engedélyezi a kiszolgáló-házirend", + "email_not_allowed": "Az e-mail-címet nem engedélyezi a kiszolgáló-házirend", + "field_required": "Ez a mező kötelező", + "invalid_credentials": "Érvénytelen hitelesítő adatok", + "password_mismatch": "A jelszómezők nem egyeznek", + "rate_limit_exceeded": "Túl sok kérést adott fel egy rövid időszak alatt. Várjon néhány percet, és próbálja újra.", + "username_all_numeric": "A felhasználónév nem állhat pusztán számokból", + "username_banned": "A felhasználónevet a kiszolgáló-házirend tiltja", + "username_invalid_chars": "A felhasználónév érvénytelen karaktereket tartalmaz. Csak kisbetűket, számokat, kötőjeleket és aláhúzásokat használjon.", + "username_not_allowed": "A felhasználónevet nem engedélyezi a kiszolgáló-házirend", + "username_taken": "A felhasználónév már foglalt", + "username_too_long": "A felhasználónév túl hosszú", + "username_too_short": "A felhasználónév túl rövid" + }, + "login": { + "call_to_register": "Még nincs fiókja?", + "continue_with_provider": "Folytatás ezzel a szolgáltatóval: %(provider)s", + "description": "Jelentkezzen be a folytatáshoz:", + "forgot_password": "Elfelejtette a jelszavát?", + "headline": "Bejelentkezés", + "link": { + "description": "A(z) %(provider)s fiókjának összekötése", + "headline": "Bejelentkezés az összekötéshez" + }, + "no_login_methods": "Nem érhetőek el bejelentkezési módok.", + "separator": "vagy", + "username_or_email": "Felhasználónév vagy e-mail-cím" + }, + "navbar": { + "my_account": "Saját fiók", + "register": "Fiók létrehozása", + "signed_in_as": "Bejelentkezve mint %(username)s." + }, + "not_found": { + "description": "A keresett oldal nem létezik vagy áthelyezték", + "heading": "Az oldal nem található" + }, + "not_you": "Nem %(username)s?", + "or_separator": "vagy", + "policy_violation": { + "description": "Ez a kérést feladó kliens, a jelenleg bejelentkezett felhasználó, vagy maga a kérés miatt is lehet.", + "heading": "Az engedélyezési kérés a szolgáltatás által betartatott házirend alapján elutasítva", + "logged_as": "Bejelentkezve mint %(username)s" + }, + "recovery": { + "consumed": { + "description": "Új jelszó létrehozásához kezdje elölről, és válassza a „Elfelejtett jelszó” lehetőséget.", + "heading": "A jelszóhelyreállítási hivatkozás már fel lett használva" + }, + "disabled": { + "description": "Ha elvesztette a hitelesítő adatait, akkor a fiókja visszaszerzéséhez lépjen kapcsolatba a rendszergazdával.", + "heading": "A fiókhelyreállítás le van tiltva" + }, + "expired": { + "description": "Új e-mail kérése, amely ide lesz küldve: %(email)s.", + "heading": "A jelszóvisszaállítási hivatkozás lejárt", + "resend_email": "Levél újraküldése" + }, + "finish": { + "confirm": "Adja meg a jelszót újból", + "description": "Válasszon egy új jelszót a fiókjához.", + "heading": "Jelszó visszaállítása", + "new": "Új jelszó", + "save_and_continue": "Mentés és folytatás" + }, + "progress": { + "change_email": "Próbálkozás másik e-mail-címmel", + "description": "Egy jelszóvisszaállítási hivatkozást tartalmazó levelet küldtünk, ha van fiókja a(z) %(email)s címmel.", + "heading": "Nézze meg a leveleit", + "resend_email": "Levél újraküldése" + }, + "start": { + "description": "Egy jelszóvisszaállítási hivatkozást tartalmazó levél lesz küldve.", + "heading": "A folytatáshoz adja meg az e-mail-címét" + } + }, + "register": { + "call_to_login": "Már van fiókja?", + "continue_with_email": "Folytatás az e-mail-címével", + "create_account": { + "description": "A folytatáshoz válasszon felhasználónevet.", + "heading": "Fiók létrehozása" + }, + "sign_in_instead": "Bejelentkezés", + "terms_of_service": "Elfogadom a Szolgáltatási feltételeket" + }, + "scope": { + "edit_profile": "Profil és elérhetőségek szerkesztése", + "manage_sessions": "Eszközök és munkamenetek kezelése", + "mas_admin": "Bármely felhasználó kezelése a matrix-authentication-service szolgáltatásban", + "send_messages": "Új üzenetek küldése az Ön nevében", + "synapse_admin": "A Synapse Matrix-kiszolgáló kezelése", + "view_messages": "Meglévő üzenetek és adatok megtekintése", + "view_profile": "Saját profilinformációk és kapcsolati részletek megtekintése" + }, + "upstream_oauth2": { + "link_mismatch": { + "heading": "Ez a forrásfiók már egy másik fiókkal van összekötve." + }, + "register": { + "choose_username": { + "description": "Ez később nem változtatható meg.", + "heading": "Válasszon felhasználónevet" + }, + "create_account": "Új fiók létrehozása", + "enforced_by_policy": "Kiszolgáló-házirend által betartatva", + "forced_display_name": "A következő megjelenítési nevet fogja használni", + "forced_email": "A következő e-mail-címet fogja használni", + "forced_localpart": "A következő felhasználónevet fogja használni", + "import_data": { + "description": "Erősítse meg az információkat, melyek a(z) %(server_name)s fiókjához lesznek kapcsolva.", + "heading": "Saját adatok importálása" + }, + "imported_from_upstream": "Importálva a forrásfiókjából", + "imported_from_upstream_with_name": "Importálva a(z) %(human_name)s fiókjából", + "link_existing": "Hivatkozás egy meglévő fiókra", + "provider_name": "%(human_name)s fiók", + "signup_with_upstream": { + "heading": "Folytassa a regisztrációt a(z) %(human_name)s fiókjával" + }, + "suggested_display_name": "Megjelenítési név importálása", + "suggested_email": "E-mail-cím importálása", + "use": "Használat" + }, + "suggest_link": { + "action": "Hivatkozás", + "heading": "Meglévő fiók összekötése" + } + }, + "verify_email": { + "6_digit_code": "6 számjegyű kód", + "code": "Kód", + "description": "Adja meg az ide küldött 6 számjegyű kódot: %(email)s", + "headline": "E-mail-cím ellenőrzése" + } + } +} \ No newline at end of file diff --git a/translations/nb-NO.json b/translations/nb-NO.json new file mode 100644 index 000000000..0ac50e862 --- /dev/null +++ b/translations/nb-NO.json @@ -0,0 +1,265 @@ +{ + "action": { + "back": "Tilbake", + "cancel": "Avbryt", + "continue": "Fortsett", + "create_account": "Opprett konto", + "sign_in": "Logg inn", + "sign_out": "Logg ut", + "skip": "Hopp over", + "start_over": "Begynn på nytt", + "submit": "Send" + }, + "app": { + "human_name": "Matrix Authentication Service", + "name": "matrix-authentication-service", + "technical_description": "OpenID Connect oppdagelsesdokument: %(discovery_url)s" + }, + "branding": { + "privacy_policy": { + "alt": "Lenke til tjenestens personvernerklæring", + "link": "Personvernerklæring" + }, + "terms_and_conditions": { + "alt": "Lenke til tjenestens vilkår og betingelser", + "link": "Vilkår og betingelser" + } + }, + "common": { + "display_name": "Visningsnavn", + "email_address": "E-postadresse", + "loading": "Laster inn...", + "mxid": "Matrix ID", + "password": "Passord", + "password_confirm": "Bekreft passord", + "username": "Brukernavn" + }, + "error": { + "unexpected": "Uventet feil" + }, + "mas": { + "account": { + "deactivated": { + "description": "Denne kontoen (%(mxid)s) har blitt slettet. Hvis dette ikke er forventet, må du kontakte serveradministratoren.", + "heading": "Konto slettet" + }, + "locked": { + "description": "Denne kontoen (%(mxid)s ) har blitt låst. Hvis dette ikke er forventet, kontakt serveradministratoren din.", + "heading": "Konto låst" + }, + "logged_out": { + "description": "Denne økten har blitt avsluttet. Logg ut for å kunne logge inn igjen", + "heading": "Sesjonen er avsluttet" + } + }, + "add_email": { + "description": "Skriv inn en e-postadresse for å gjenopprette kontoen din i tilfelle du mister tilgangen til den.", + "heading": "Legg til en e-postadresse" + }, + "back_to_homepage": "Gå tilbake til hjemmesiden", + "captcha": { + "noscript": "Dette skjemaet er beskyttet av en CAPTCHA og krever at JavaScript er aktivert for å kunne sendes inn. Aktiver JavaScript i nettleseren din og last inn denne siden på nytt." + }, + "change_password": { + "change": "Endre passord", + "confirm": "Bekreft passord", + "current": "Nåværende passord", + "description": "Dette vil endre passordet på kontoen din.", + "heading": "Endre passordet mitt", + "new": "Nytt passord" + }, + "choose_display_name": { + "description": "Dette er navnet andre vil se. Du kan endre dette når som helst.", + "headline": "Velg visningsnavnet ditt" + }, + "consent": { + "client_wants_access": "%(client_name)s%(redirect_uri)s ønsker å få tilgang til kontoen din.", + "heading": "Vil du gi tilgang til kontoen din?", + "make_sure_you_trust": "Vær sikker på at du stoler på %(client_name)s.", + "this_will_allow": "Dette vil tillate %(client_name)s å:", + "you_may_be_sharing": "Det kan hende du deler sensitiv informasjon med denne siden eller appen." + }, + "device_card": { + "access_requested": "Tilgang forespurt", + "device_code": "Kode", + "generic_device": "Enhet", + "ip_address": "IP adresse" + }, + "device_code_link": { + "description": "Koble til en enhet", + "headline": "Skriv inn koden som vises på enheten din" + }, + "device_consent": { + "another_device_access": "En annen enhet vil ha tilgang til kontoen din.", + "denied": { + "description": "Du nektet tilgang til %(client_name)s. Du kan lukke dette vinduet.", + "heading": "Tilgang nektet" + }, + "granted": { + "description": "Du har gitt tilgang til %(client_name)s. Du kan lukke dette vinduet.", + "heading": "Tilgang gitt" + } + }, + "email_in_use": { + "description": "Hvis du har glemt kontolegitimasjonen din, kan du gjenopprette kontoen din. Du kan også starte på nytt og bruke en annen e-postadresse.", + "title": "E-postadressen %(email)s er allerede i bruk" + }, + "emails": { + "greeting": "Hallo %(username)s,", + "recovery": { + "click_button": "Klikk på knappen nedenfor for å opprette et nytt passord:", + "copy_link": "Kopier følgende lenke og lim den inn i en nettleser for å opprette et nytt passord:", + "create_new_password": "Opprett nytt passord", + "fallback": "Fungerer ikke knappen for deg?", + "headline": "Du har bedt om tilbakestilling av passord for %(server_name)s kontoen din.", + "subject": "Tilbakestill kontopassordet ditt (%(mxid)s)", + "you_can_ignore": "Hvis du ikke ba om et nytt passord, kan du ignorere denne e-posten. Ditt nåværende passord vil fortsette å fungere." + }, + "verify": { + "body_html": "Verifiseringskoden for å bekrefte denne e-postadressen er: %(code)s", + "body_text": "Verifiseringskoden for å bekrefte denne e-postadressen er: %(code)s", + "subject": "Verifiseringskoden for e-posten din er: %(code)s" + } + }, + "errors": { + "captcha": "CAPTCHA-verifisering mislyktes. Prøv igjen", + "denied_policy": "Avvist av policy: %(policy)s", + "email_banned": "E-post er utestengt av serverpolicyen", + "email_domain_banned": "E-postdomenet er utestengt av serverpolicyen", + "email_domain_not_allowed": "E-postdomene er ikke tillatt av serverpolicyen", + "email_not_allowed": "E-post er ikke tillatt av serverpolicyen", + "field_required": "Dette feltet er obligatorisk", + "invalid_credentials": "Ugyldig legitimasjon", + "password_mismatch": "Passordfeltene stemmer ikke overens", + "rate_limit_exceeded": "Du har kommet med for mange forespørsler på kort tid. Vent noen minutter og prøv igjen.", + "username_all_numeric": "Brukernavn kan ikke bare bestå av tall", + "username_banned": "Brukernavn er utestengt av serverpolicyen", + "username_invalid_chars": "Brukernavnet inneholder ugyldige tegn. Bruk bare små bokstaver, tall, bindestrek og understrek.", + "username_not_allowed": "Brukernavnet er ikke tillatt av serverpolicyen", + "username_taken": "Dette brukernavnet er allerede tatt", + "username_too_long": "Brukernavnet er for langt", + "username_too_short": "Brukernavnet er for kort" + }, + "login": { + "call_to_register": "Har du ikke en konto ennå?", + "continue_with_provider": "Fortsett med %(provider)s", + "description": "Logg på for å fortsette:", + "forgot_password": "Glemt passordet?", + "headline": "Logg inn", + "link": { + "description": "Kobler din %(provider)s konto", + "headline": "Logg inn for å koble" + }, + "no_login_methods": "Ingen påloggingsmetoder tilgjengelig.", + "separator": "Eller", + "username_or_email": "Brukernavn eller e-postadresse" + }, + "navbar": { + "my_account": "Min konto", + "register": "Opprett en konto", + "signed_in_as": "Logget på som %(username)s." + }, + "not_found": { + "description": "Siden du lette etter eksisterer ikke eller har blitt flyttet", + "heading": "Side ikke funnet" + }, + "not_you": "Ikke %(username)s?", + "or_separator": "Eller", + "policy_violation": { + "description": "Dette kan skyldes klienten som har opprettet forespørselen, den påloggede brukeren eller selve forespørselen.", + "heading": "Autorisasjonsforespørselen ble avvist av policyen som håndheves av denne tjenesten", + "logged_as": "Logget inn som %(username)s" + }, + "recovery": { + "consumed": { + "description": "For å opprette et nytt passord, start på nytt og velg «Glemt passord».", + "heading": "Lenken for å tilbakestille passordet ditt har allerede blitt brukt" + }, + "disabled": { + "description": "Hvis du har mistet legitimasjonen din, vennligst kontakt administratoren for å gjenopprette kontoen din.", + "heading": "Muligheten for kontogjenoppretting er skrudd av" + }, + "expired": { + "description": "Be om en ny e-post som vil bli sendt til: %(email)s.", + "heading": "Lenken for å tilbakestille passordet ditt er utløpt", + "resend_email": "Send e-post på nytt" + }, + "finish": { + "confirm": "Skriv inn nytt passord igjen", + "description": "Velg et nytt passord for kontoen din.", + "heading": "Tilbakestill passordet ditt", + "new": "Nytt passord", + "save_and_continue": "Lagre og fortsett" + }, + "progress": { + "change_email": "Prøv en annen e-postadresse", + "description": "Vi sendte en e-post med en lenke for å tilbakestille passordet ditt hvis det er en konto som bruker %(email)s.", + "heading": "Sjekk e-posten din", + "resend_email": "Send e-post på nytt" + }, + "start": { + "description": "En e-post vil bli sendt med en lenke for å tilbakestille passordet ditt.", + "heading": "Skriv inn e-postadressen din for å fortsette" + } + }, + "register": { + "call_to_login": "Har du allerede en konto?", + "continue_with_email": "Fortsett med e-postadresse", + "create_account": { + "description": "Velg et brukernavn for å fortsette.", + "heading": "Opprett en konto" + }, + "sign_in_instead": "Logg på i stedet", + "terms_of_service": "Jeg godtar vilkårene og betingelsene" + }, + "scope": { + "edit_profile": "Rediger din profil og kontaktdetaljer", + "manage_sessions": "Administrer enhetene og sesjonene dine", + "mas_admin": "Administrer alle brukere på matrix-authentication-service", + "send_messages": "Send nye meldinger på dine vegne", + "synapse_admin": "Administrer Synapse-hjemmeserveren", + "view_messages": "Se dine eksisterende meldinger og data", + "view_profile": "Se din profilinformasjon og kontaktdetaljer" + }, + "upstream_oauth2": { + "link_mismatch": { + "heading": "Denne oppstrømskontoen er allerede knyttet til en annen konto." + }, + "register": { + "choose_username": { + "description": "Dette kan ikke endres senere.", + "heading": "Velg ditt brukernavn" + }, + "create_account": "Opprett en ny konto", + "enforced_by_policy": "Håndhevet av serverpolicy", + "forced_display_name": "Vil bruke følgende visningsnavn", + "forced_email": "Vil bruke følgende e-postadresse", + "forced_localpart": "Vil bruke følgende brukernavn", + "import_data": { + "description": "Bekreft informasjonen som vil bli knyttet til din nye %(server_name)s konto.", + "heading": "Importer dataene dine" + }, + "imported_from_upstream": "Importert fra oppstrømskontoen din", + "imported_from_upstream_with_name": "Importert fra %(human_name)s kontoen din", + "link_existing": "Koble til en eksisterende konto", + "provider_name": "%(human_name)s konto", + "signup_with_upstream": { + "heading": "Fortsett å registrere deg med din %(human_name)s konto" + }, + "suggested_display_name": "Importer visningsnavn", + "suggested_email": "Importer e-postadresse", + "use": "Bruk" + }, + "suggest_link": { + "action": "Lenke", + "heading": "Koble til din eksisterende konto" + } + }, + "verify_email": { + "6_digit_code": "6-sifret kode", + "code": "Kode", + "description": "Skriv inn den 6-sifrede koden sendt til: %(email)s", + "headline": "Bekreft e-postadressen din" + } + } +} \ No newline at end of file From fa1ecc5216b8a6af1673731735b3e1cfaaeafdf5 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 23 Apr 2025 13:30:02 +0200 Subject: [PATCH 057/189] syn2mas: warn about existing oauth-delegated user_external_ids --- crates/syn2mas/src/synapse_reader/checks.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/syn2mas/src/synapse_reader/checks.rs b/crates/syn2mas/src/synapse_reader/checks.rs index 4dca03029..0969d3787 100644 --- a/crates/syn2mas/src/synapse_reader/checks.rs +++ b/crates/syn2mas/src/synapse_reader/checks.rs @@ -73,6 +73,11 @@ pub enum CheckError { )] SynapseMissingOAuthProvider { provider: String, num_users: i64 }, + #[error( + "Synapse database has {num_users} mapping entries from a previously-configured MAS instance. If this is from a previous migration attempt, run the following SQL query against the Synapse database: `DELETE FROM user_external_ids WHERE auth_provider = 'oauth-delegated';` and then run the migration again." + )] + ExistingOAuthDelegated { num_users: i64 }, + #[error( "Synapse config contains an OpenID Connect or OAuth2 provider '{provider}' (issuer: {issuer:?}) used by {num_users} users which must also be configured in the MAS configuration as an upstream provider." )] @@ -292,6 +297,14 @@ pub async fn synapse_database_check( let syn_oauth2 = synapse.all_oidc_providers(); let mas_oauth2 = UpstreamOAuth2Config::extract_or_default(mas)?; for row in oauth_provider_user_counts { + // This is a special case of a previous migration attempt to MAS + if row.auth_provider == "oauth-delegated" { + errors.push(CheckError::ExistingOAuthDelegated { + num_users: row.num_users, + }); + continue; + } + let matching_syn = syn_oauth2.get(&row.auth_provider); let Some(matching_syn) = matching_syn else { From d62fe98f5a58d9daf30ca8a1778516d76fc88b90 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 23 Apr 2025 13:34:34 +0200 Subject: [PATCH 058/189] syn2mas: provide guidance on how to re-do a fresh migration --- crates/syn2mas/src/mas_writer/checks.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/syn2mas/src/mas_writer/checks.rs b/crates/syn2mas/src/mas_writer/checks.rs index d5b51b510..6ddf7619b 100644 --- a/crates/syn2mas/src/mas_writer/checks.rs +++ b/crates/syn2mas/src/mas_writer/checks.rs @@ -16,10 +16,12 @@ use super::{MAS_TABLES_AFFECTED_BY_MIGRATION, is_syn2mas_in_progress, locking::L #[derive(Debug, Error, ContextInto)] pub enum Error { - #[error("the MAS database is not empty: rows found in at least `{table}`")] + #[error( + "The MAS database is not empty: rows found in at least `{table}`. Please drop and recreate the database, then try again." + )] MasDatabaseNotEmpty { table: &'static str }, - #[error("query against {table} failed — is this actually a MAS database?")] + #[error("Query against {table} failed — is this actually a MAS database?")] MaybeNotMas { #[source] source: sqlx::Error, @@ -29,7 +31,7 @@ pub enum Error { #[error(transparent)] Sqlx(#[from] sqlx::Error), - #[error("unable to check if syn2mas is already in progress")] + #[error("Unable to check if syn2mas is already in progress")] UnableToCheckInProgress(#[source] super::Error), } From be09f62bfa7fe80ef340538e23bb9f176c71cd10 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 23 Apr 2025 13:38:46 +0200 Subject: [PATCH 059/189] syn2mas: drop the experimental flag --- crates/cli/src/commands/syn2mas.rs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/crates/cli/src/commands/syn2mas.rs b/crates/cli/src/commands/syn2mas.rs index bc1ae72f1..ca6f1422e 100644 --- a/crates/cli/src/commands/syn2mas.rs +++ b/crates/cli/src/commands/syn2mas.rs @@ -15,7 +15,7 @@ use sqlx::{Connection, Either, PgConnection, postgres::PgConnectOptions, types:: use syn2mas::{ LockedMasDatabase, MasWriter, Progress, ProgressStage, SynapseReader, synapse_config, }; -use tracing::{Instrument, error, info, info_span, warn}; +use tracing::{Instrument, error, info, info_span}; use crate::util::{DatabaseConnectOptions, database_connection_from_config_with_options}; @@ -32,15 +32,6 @@ pub(super) struct Options { #[command(subcommand)] subcommand: Subcommand, - /// This version of the syn2mas tool is EXPERIMENTAL and INCOMPLETE. It is - /// only suitable for TESTING. If you want to use this tool anyway, - /// please pass this argument. - /// - /// If you want to migrate from Synapse to MAS today, please use the - /// Node.js-based tool in the MAS repository. - #[clap(long = "i-swear-i-am-just-testing-in-a-staging-environment")] - experimental_accepted: bool, - /// Path to the Synapse configuration (in YAML format). /// May be specified multiple times if multiple Synapse configuration files /// are in use. @@ -85,14 +76,6 @@ impl Options { #[tracing::instrument("cli.syn2mas.run", skip_all)] #[allow(clippy::too_many_lines)] pub async fn run(self, figment: &Figment) -> anyhow::Result { - warn!( - "This version of the syn2mas tool is EXPERIMENTAL and INCOMPLETE. Do not use it, except for TESTING." - ); - if !self.experimental_accepted { - error!("Please agree that you can only use this tool for testing."); - return Ok(ExitCode::FAILURE); - } - if self.synapse_configuration_files.is_empty() { error!("Please specify the path to the Synapse configuration file(s)."); return Ok(ExitCode::FAILURE); From 44727912c07c3c688ac9fc10d4d448d41acc09ac Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 23 Apr 2025 13:46:44 +0200 Subject: [PATCH 060/189] Add a few missing license headers --- crates/cli/src/commands/syn2mas.rs | 5 +++++ crates/syn2mas/src/mas_writer/checks.rs | 2 +- crates/syn2mas/src/mas_writer/constraint_pausing.rs | 2 +- .../syn2mas/src/mas_writer/fixtures/upstream_provider.sql | 5 +++++ crates/syn2mas/src/mas_writer/locking.rs | 2 +- crates/syn2mas/src/mas_writer/mod.rs | 2 +- .../src/synapse_reader/fixtures/access_token_alice.sql | 5 +++++ .../fixtures/access_token_alice_with_puppet.sql | 5 +++++ .../fixtures/access_token_alice_with_refresh_token.sql | 5 +++++ .../access_token_alice_with_unused_refresh_token.sql | 5 +++++ .../syn2mas/src/synapse_reader/fixtures/devices_alice.sql | 5 +++++ .../src/synapse_reader/fixtures/external_ids_alice.sql | 5 +++++ .../src/synapse_reader/fixtures/threepids_alice.sql | 5 +++++ crates/syn2mas/src/synapse_reader/fixtures/user_alice.sql | 7 +++++-- 14 files changed, 54 insertions(+), 6 deletions(-) diff --git a/crates/cli/src/commands/syn2mas.rs b/crates/cli/src/commands/syn2mas.rs index ca6f1422e..c7851627b 100644 --- a/crates/cli/src/commands/syn2mas.rs +++ b/crates/cli/src/commands/syn2mas.rs @@ -1,3 +1,8 @@ +// Copyright 2024, 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + use std::{collections::HashMap, process::ExitCode, time::Duration}; use anyhow::Context; diff --git a/crates/syn2mas/src/mas_writer/checks.rs b/crates/syn2mas/src/mas_writer/checks.rs index 6ddf7619b..288156d8c 100644 --- a/crates/syn2mas/src/mas_writer/checks.rs +++ b/crates/syn2mas/src/mas_writer/checks.rs @@ -1,4 +1,4 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. diff --git a/crates/syn2mas/src/mas_writer/constraint_pausing.rs b/crates/syn2mas/src/mas_writer/constraint_pausing.rs index d350bd89f..49fd4a8e3 100644 --- a/crates/syn2mas/src/mas_writer/constraint_pausing.rs +++ b/crates/syn2mas/src/mas_writer/constraint_pausing.rs @@ -1,4 +1,4 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. diff --git a/crates/syn2mas/src/mas_writer/fixtures/upstream_provider.sql b/crates/syn2mas/src/mas_writer/fixtures/upstream_provider.sql index 7d1e98bc8..9da09b174 100644 --- a/crates/syn2mas/src/mas_writer/fixtures/upstream_provider.sql +++ b/crates/syn2mas/src/mas_writer/fixtures/upstream_provider.sql @@ -1,3 +1,8 @@ +-- Copyright 2024, 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only +-- Please see LICENSE in the repository root for full details. + INSERT INTO upstream_oauth_providers ( upstream_oauth_provider_id, diff --git a/crates/syn2mas/src/mas_writer/locking.rs b/crates/syn2mas/src/mas_writer/locking.rs index 031ca9ac3..8200924d4 100644 --- a/crates/syn2mas/src/mas_writer/locking.rs +++ b/crates/syn2mas/src/mas_writer/locking.rs @@ -1,4 +1,4 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index 5e3a0f0ef..07fceb85c 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. diff --git a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice.sql b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice.sql index d9f9a4a7b..e92fd21bf 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice.sql @@ -1,3 +1,8 @@ +-- Copyright 2024, 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only +-- Please see LICENSE in the repository root for full details. + INSERT INTO access_tokens ( id, diff --git a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_puppet.sql b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_puppet.sql index 6bdfb0d9c..c8b2850ac 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_puppet.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_puppet.sql @@ -1,3 +1,8 @@ +-- Copyright 2024, 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only +-- Please see LICENSE in the repository root for full details. + INSERT INTO access_tokens ( id, diff --git a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_refresh_token.sql b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_refresh_token.sql index 554ae4458..180a58810 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_refresh_token.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_refresh_token.sql @@ -1,3 +1,8 @@ +-- Copyright 2024, 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only +-- Please see LICENSE in the repository root for full details. + INSERT INTO access_tokens ( id, diff --git a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_unused_refresh_token.sql b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_unused_refresh_token.sql index 42bfddf01..8c7d1c695 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_unused_refresh_token.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_unused_refresh_token.sql @@ -1,3 +1,8 @@ +-- Copyright 2024, 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only +-- Please see LICENSE in the repository root for full details. + INSERT INTO access_tokens ( id, diff --git a/crates/syn2mas/src/synapse_reader/fixtures/devices_alice.sql b/crates/syn2mas/src/synapse_reader/fixtures/devices_alice.sql index c7f0691d6..8eb50a3ba 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/devices_alice.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/devices_alice.sql @@ -1,3 +1,8 @@ +-- Copyright 2024, 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only +-- Please see LICENSE in the repository root for full details. + INSERT INTO devices ( user_id, diff --git a/crates/syn2mas/src/synapse_reader/fixtures/external_ids_alice.sql b/crates/syn2mas/src/synapse_reader/fixtures/external_ids_alice.sql index 5a00cebb5..a365faf05 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/external_ids_alice.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/external_ids_alice.sql @@ -1,3 +1,8 @@ +-- Copyright 2024, 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only +-- Please see LICENSE in the repository root for full details. + INSERT INTO user_external_ids ( user_id, diff --git a/crates/syn2mas/src/synapse_reader/fixtures/threepids_alice.sql b/crates/syn2mas/src/synapse_reader/fixtures/threepids_alice.sql index 526c00c2c..4bf680cce 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/threepids_alice.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/threepids_alice.sql @@ -1,3 +1,8 @@ +-- Copyright 2024, 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only +-- Please see LICENSE in the repository root for full details. + INSERT INTO user_threepids ( user_id, diff --git a/crates/syn2mas/src/synapse_reader/fixtures/user_alice.sql b/crates/syn2mas/src/synapse_reader/fixtures/user_alice.sql index bf52d6c5c..dc77d5859 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/user_alice.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/user_alice.sql @@ -1,4 +1,8 @@ --- +-- Copyright 2024, 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only +-- Please see LICENSE in the repository root for full details. + INSERT INTO users ( name, @@ -37,4 +41,3 @@ INSERT INTO users false, false ); - From 30a0e5054af12844efe4b6875862843fb67080f4 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 23 Apr 2025 14:18:59 +0200 Subject: [PATCH 061/189] Allow syn2mas arguments to be specified after the subcommand --- crates/cli/src/commands/syn2mas.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/commands/syn2mas.rs b/crates/cli/src/commands/syn2mas.rs index c7851627b..c32f49bd9 100644 --- a/crates/cli/src/commands/syn2mas.rs +++ b/crates/cli/src/commands/syn2mas.rs @@ -40,7 +40,7 @@ pub(super) struct Options { /// Path to the Synapse configuration (in YAML format). /// May be specified multiple times if multiple Synapse configuration files /// are in use. - #[clap(long = "synapse-config")] + #[clap(long = "synapse-config", global = true)] synapse_configuration_files: Vec, /// Override the Synapse database URI. @@ -60,7 +60,7 @@ pub(super) struct Options { /// environment variables `PGHOST`, `PGPORT`, `PGUSER`, `PGDATABASE`, /// `PGPASSWORD`, etc. It is valid to specify the URL `postgresql:` and /// configure all values through those environment variables. - #[clap(long = "synapse-database-uri")] + #[clap(long = "synapse-database-uri", global = true)] synapse_database_uri: Option, } From ae29ef1f9de6cded5939d2e3b2df86798c48f677 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 23 Apr 2025 14:21:15 +0200 Subject: [PATCH 062/189] syn2mas: log progress more often --- crates/cli/src/commands/syn2mas.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/cli/src/commands/syn2mas.rs b/crates/cli/src/commands/syn2mas.rs index c32f49bd9..fd84c4eca 100644 --- a/crates/cli/src/commands/syn2mas.rs +++ b/crates/cli/src/commands/syn2mas.rs @@ -262,13 +262,13 @@ impl Options { } } -/// Logs progress every 30 seconds, as a lightweight alternative to a progress -/// bar. For most deployments, the migration will not take 30 seconds so this +/// Logs progress every 5 seconds, as a lightweight alternative to a progress +/// bar. For most deployments, the migration will not take 5 seconds so this /// will not be relevant. In other cases, this will give the operator an idea of /// what's going on. async fn occasional_progress_logger(progress: Progress) { loop { - tokio::time::sleep(Duration::from_secs(30)).await; + tokio::time::sleep(Duration::from_secs(5)).await; match &**progress.get_current_stage() { ProgressStage::SettingUp => { info!(name: "progress", "still setting up"); From 348eb56344db25018135b0cebdb4a166462a77dd Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 23 Apr 2025 14:42:30 +0200 Subject: [PATCH 063/189] syn2mas: introduce a dry-run mode --- crates/cli/src/commands/syn2mas.rs | 23 +++++++++++++++------- crates/syn2mas/src/mas_writer/mod.rs | 29 +++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/crates/cli/src/commands/syn2mas.rs b/crates/cli/src/commands/syn2mas.rs index fd84c4eca..1d1fa06de 100644 --- a/crates/cli/src/commands/syn2mas.rs +++ b/crates/cli/src/commands/syn2mas.rs @@ -70,8 +70,17 @@ enum Subcommand { /// /// It is OK for Synapse to be online during these checks. Check, + /// Perform a migration. Synapse must be offline during this process. - Migrate, + Migrate { + /// Perform a dry-run migration, which is safe to run with Synapse + /// running, and will restore the MAS database to an empty state. + /// + /// This still *does* write to the MAS database, making it more + /// realistic compared to the final migration. + #[clap(long)] + dry_run: bool, + }, } /// The number of parallel writing transactions active against the MAS database. @@ -118,11 +127,10 @@ impl Options { .await .context("could not run migrations")?; - if matches!(&self.subcommand, Subcommand::Migrate) { + if matches!(&self.subcommand, Subcommand::Migrate { .. }) { // First perform a config sync // This is crucial to ensure we register upstream OAuth providers // in the MAS database - // let config = SyncConfig::extract(figment)?; let clock = SystemClock::default(); let encrypter = config.secrets.encrypter(); @@ -201,7 +209,8 @@ impl Options { Ok(ExitCode::SUCCESS) } - Subcommand::Migrate => { + + Subcommand::Migrate { dry_run } => { let provider_id_mappings: HashMap = { let mas_oauth2 = UpstreamOAuth2Config::extract_or_default(figment)?; @@ -217,8 +226,7 @@ impl Options { // TODO how should we handle warnings at this stage? - // TODO this dry-run flag should be set to false in real circumstances !!! - let reader = SynapseReader::new(&mut syn_conn, true).await?; + let reader = SynapseReader::new(&mut syn_conn, dry_run).await?; let writer_mas_connections = futures_util::future::try_join_all((0..NUM_WRITER_CONNECTIONS).map(|_| { database_connection_from_config_with_options( @@ -230,7 +238,8 @@ impl Options { })) .instrument(tracing::info_span!("syn2mas.mas_writer_connections")) .await?; - let writer = MasWriter::new(mas_connection, writer_mas_connections).await?; + let writer = + MasWriter::new(mas_connection, writer_mas_connections, dry_run).await?; let clock = SystemClock::default(); // TODO is this rng ok? diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index 07fceb85c..f36851dfd 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -242,6 +242,7 @@ impl FinishCheckerHandle { pub struct MasWriter { conn: LockedMasDatabase, writer_pool: WriterConnectionPool, + dry_run: bool, indices_to_restore: Vec, constraints_to_restore: Vec, @@ -793,6 +794,7 @@ impl MasWriter { pub async fn new( mut conn: LockedMasDatabase, mut writer_connections: Vec, + dry_run: bool, ) -> Result { // Given that we don't have any concurrent transactions here, // the READ COMMITTED isolation level is sufficient. @@ -902,7 +904,7 @@ impl MasWriter { Ok(Self { conn, - + dry_run, writer_pool: WriterConnectionPool::new(writer_connections), indices_to_restore, constraints_to_restore, @@ -987,7 +989,6 @@ impl MasWriter { // Now all the data has been migrated, finish off by restoring indices and // constraints! - query("BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;") .execute(self.conn.as_mut()) .await @@ -1009,6 +1010,28 @@ impl MasWriter { .await .into_database("could not revert temporary tables")?; + // If we're in dry-run mode, truncate all the tables we've written to + if self.dry_run { + warn!("Migration ran in dry-run mode, deleting all imported data"); + let tables = MAS_TABLES_AFFECTED_BY_MIGRATION + .iter() + .map(|table| format!("\"{table}\"")) + .collect::>() + .join(", "); + + // Note that we do that with CASCADE, because we do that *after* + // restoring the FK constraints. + // + // The alternative would be to list all the tables we have FK to + // those tables, which would be a hassle, or to do that after + // restoring the constraints, which would mean we wouldn't validate + // that we've done valid FKs in dry-run mode. + query(&format!("TRUNCATE TABLE {tables} CASCADE;")) + .execute(self.conn.as_mut()) + .await + .into_database_with(|| "failed to truncate all tables")?; + } + query("COMMIT;") .execute(self.conn.as_mut()) .await @@ -1193,7 +1216,7 @@ mod test { .await .expect("failed to lock MAS database") .expect_left("MAS database is already locked"); - MasWriter::new(locked_main_conn, writer_conns) + MasWriter::new(locked_main_conn, writer_conns, false) .await .expect("failed to construct MasWriter") } From 9bf197f176ddc54a58212e495141419eeadf0880 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 13:53:45 +0000 Subject: [PATCH 064/189] build(deps): bump sea-query from 0.32.3 to 0.32.4 in the sea-query group Bumps the sea-query group with 1 update: [sea-query](https://github.com/SeaQL/sea-query). Updates `sea-query` from 0.32.3 to 0.32.4 - [Release notes](https://github.com/SeaQL/sea-query/releases) - [Changelog](https://github.com/SeaQL/sea-query/blob/master/CHANGELOG.md) - [Commits](https://github.com/SeaQL/sea-query/compare/0.32.3...0.32.4) --- updated-dependencies: - dependency-name: sea-query dependency-version: 0.32.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: sea-query ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4a179e53..945299014 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5376,9 +5376,9 @@ dependencies = [ [[package]] name = "sea-query" -version = "0.32.3" +version = "0.32.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a24d8b9fcd2674a6c878a3d871f4f1380c6c43cc3718728ac96864d888458e" +checksum = "d99447c24da0cded00089e2021e1624af90878c65f7534319448d01da3df869d" dependencies = [ "chrono", "inherent", diff --git a/Cargo.toml b/Cargo.toml index f2bd90196..bc8c1564a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -296,7 +296,7 @@ features = ["oid"] # Query builder [workspace.dependencies.sea-query] -version = "0.32.3" +version = "0.32.4" features = ["derive", "attr", "with-uuid", "with-chrono", "postgres-array"] # Query builder From 3b84f19095bdd6fb029df8c0a4ab9874933a03e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 13:55:11 +0000 Subject: [PATCH 065/189] build(deps): bump psl from 2.1.100 to 2.1.102 Bumps [psl](https://github.com/addr-rs/psl) from 2.1.100 to 2.1.102. - [Release notes](https://github.com/addr-rs/psl/releases) - [Commits](https://github.com/addr-rs/psl/compare/v2.1.100...v2.1.102) --- updated-dependencies: - dependency-name: psl dependency-version: 2.1.102 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- crates/handlers/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4a179e53..fac2cc625 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4724,9 +4724,9 @@ dependencies = [ [[package]] name = "psl" -version = "2.1.100" +version = "2.1.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70295efe3fd3db60e81f452e2eacc407b4e6c2e1ff7f763424ae6e16105cee26" +checksum = "dee834bead8841313bb97b76af417bfd2fe326a16236d77a765a7a4621ced264" dependencies = [ "psl-types", ] diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index 65c7bbb6f..7de2fd98c 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -76,7 +76,7 @@ hex.workspace = true governor.workspace = true indexmap.workspace = true pkcs8.workspace = true -psl = "2.1.100" +psl = "2.1.102" sha2.workspace = true time = "0.3.41" url.workspace = true From 5f356b816ac44e00c8dffd39be610a94cb8f84f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:03:11 +0000 Subject: [PATCH 066/189] build(deps): bump sigstore/cosign-installer from 3.8.1 to 3.8.2 Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.8.1 to 3.8.2. - [Release notes](https://github.com/sigstore/cosign-installer/releases) - [Commits](https://github.com/sigstore/cosign-installer/compare/v3.8.1...v3.8.2) --- updated-dependencies: - dependency-name: sigstore/cosign-installer dependency-version: 3.8.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 17adc0012..75b9d9157 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -270,7 +270,7 @@ jobs: type=sha - name: Setup Cosign - uses: sigstore/cosign-installer@v3.8.1 + uses: sigstore/cosign-installer@v3.8.2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.10.0 From bd95303612462bb5534d0ff47777a61ac23f6ae0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:08:00 +0000 Subject: [PATCH 067/189] build(deps-dev): bump the vite group across 1 directory with 2 updates Bumps the vite group with 2 updates in the /frontend directory: [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). Updates `@vitejs/plugin-react` from 4.4.0 to 4.4.1 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@4.4.1/packages/plugin-react) Updates `vite` from 6.3.0 to 6.3.2 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v6.3.2/packages/vite) --- updated-dependencies: - dependency-name: "@vitejs/plugin-react" dependency-version: 4.4.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: vite - dependency-name: vite dependency-version: 6.3.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: vite ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 16 ++++++++-------- frontend/package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 78a4b18e9..f205d428f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -52,7 +52,7 @@ "@types/react": "19.1.1", "@types/react-dom": "19.1.2", "@types/swagger-ui-dist": "^3.30.5", - "@vitejs/plugin-react": "^4.4.0", + "@vitejs/plugin-react": "^4.4.1", "@vitest/coverage-v8": "^3.1.1", "autoprefixer": "^10.4.21", "browserslist-to-esbuild": "^2.1.1", @@ -70,7 +70,7 @@ "storybook-react-i18next": "^3.2.1", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", - "vite": "6.3.0", + "vite": "6.3.2", "vite-plugin-compression": "^0.5.1", "vite-plugin-graphql-codegen": "^3.5.0", "vite-plugin-manifest-sri": "^0.2.0", @@ -5954,9 +5954,9 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.0.tgz", - "integrity": "sha512-x/EztcTKVj+TDeANY1WjNeYsvZjZdfWRMP/KXi5Yn8BoTzpa13ZltaQqKfvWYbX8CE10GOHHdC5v86jY9x8i/g==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", + "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", "dev": true, "license": "MIT", "dependencies": { @@ -14328,9 +14328,9 @@ } }, "node_modules/vite": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.0.tgz", - "integrity": "sha512-9aC0n4pr6hIbvi1YOpFjwQ+QOTGssvbJKoeYkuHHGWwlXfdxQlI8L2qNMo9awEEcCPSiS+5mJZk5jH1PAqoDeQ==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz", + "integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index 782c03469..cdb0b5672 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -62,7 +62,7 @@ "@types/react": "19.1.1", "@types/react-dom": "19.1.2", "@types/swagger-ui-dist": "^3.30.5", - "@vitejs/plugin-react": "^4.4.0", + "@vitejs/plugin-react": "^4.4.1", "@vitest/coverage-v8": "^3.1.1", "autoprefixer": "^10.4.21", "browserslist-to-esbuild": "^2.1.1", @@ -80,7 +80,7 @@ "storybook-react-i18next": "^3.2.1", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", - "vite": "6.3.0", + "vite": "6.3.2", "vite-plugin-compression": "^0.5.1", "vite-plugin-graphql-codegen": "^3.5.0", "vite-plugin-manifest-sri": "^0.2.0", From ac41b41d9ca5b3970798e1b9254c82f5f2d56004 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:08:02 +0000 Subject: [PATCH 068/189] 0.15.0 --- Cargo.lock | 54 +++++++++++++++--------------- Cargo.toml | 58 ++++++++++++++++----------------- tools/syn2mas/package-lock.json | 4 +-- tools/syn2mas/package.json | 2 +- 4 files changed, 59 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d02df0709..035223777 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3114,7 +3114,7 @@ dependencies = [ [[package]] name = "mas-axum-utils" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "axum", "axum-extra", @@ -3147,7 +3147,7 @@ dependencies = [ [[package]] name = "mas-cli" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "anyhow", "axum", @@ -3218,7 +3218,7 @@ dependencies = [ [[package]] name = "mas-config" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "anyhow", "camino", @@ -3248,7 +3248,7 @@ dependencies = [ [[package]] name = "mas-data-model" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "base64ct", "chrono", @@ -3269,7 +3269,7 @@ dependencies = [ [[package]] name = "mas-email" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "async-trait", "lettre", @@ -3280,7 +3280,7 @@ dependencies = [ [[package]] name = "mas-handlers" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "aide", "anyhow", @@ -3357,7 +3357,7 @@ dependencies = [ [[package]] name = "mas-http" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "futures-util", "headers", @@ -3378,7 +3378,7 @@ dependencies = [ [[package]] name = "mas-i18n" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "camino", "icu_calendar", @@ -3400,7 +3400,7 @@ dependencies = [ [[package]] name = "mas-i18n-scan" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "camino", "clap", @@ -3414,7 +3414,7 @@ dependencies = [ [[package]] name = "mas-iana" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "schemars", "serde", @@ -3422,7 +3422,7 @@ dependencies = [ [[package]] name = "mas-iana-codegen" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "anyhow", "async-trait", @@ -3438,7 +3438,7 @@ dependencies = [ [[package]] name = "mas-jose" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "base64ct", "chrono", @@ -3468,7 +3468,7 @@ dependencies = [ [[package]] name = "mas-keystore" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "aead", "base64ct", @@ -3496,7 +3496,7 @@ dependencies = [ [[package]] name = "mas-listener" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "anyhow", "bytes", @@ -3520,7 +3520,7 @@ dependencies = [ [[package]] name = "mas-matrix" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "anyhow", "async-trait", @@ -3530,7 +3530,7 @@ dependencies = [ [[package]] name = "mas-matrix-synapse" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "anyhow", "async-trait", @@ -3547,7 +3547,7 @@ dependencies = [ [[package]] name = "mas-oidc-client" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "assert_matches", "async-trait", @@ -3583,7 +3583,7 @@ dependencies = [ [[package]] name = "mas-policy" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "anyhow", "arc-swap", @@ -3600,7 +3600,7 @@ dependencies = [ [[package]] name = "mas-router" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "axum", "serde", @@ -3611,7 +3611,7 @@ dependencies = [ [[package]] name = "mas-spa" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "camino", "serde", @@ -3620,7 +3620,7 @@ dependencies = [ [[package]] name = "mas-storage" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "async-trait", "chrono", @@ -3642,7 +3642,7 @@ dependencies = [ [[package]] name = "mas-storage-pg" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "async-trait", "chrono", @@ -3668,7 +3668,7 @@ dependencies = [ [[package]] name = "mas-tasks" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "anyhow", "async-trait", @@ -3699,7 +3699,7 @@ dependencies = [ [[package]] name = "mas-templates" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "anyhow", "arc-swap", @@ -3729,7 +3729,7 @@ dependencies = [ [[package]] name = "mas-tower" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "http", "opentelemetry", @@ -3999,7 +3999,7 @@ dependencies = [ [[package]] name = "oauth2-types" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "assert_matches", "base64ct", @@ -6149,7 +6149,7 @@ dependencies = [ [[package]] name = "syn2mas" -version = "0.15.0-rc.0" +version = "0.15.0" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index a5f1ee216..a29b83963 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["crates/*"] resolver = "2" # Updated in the CI with a `sed` command -package.version = "0.15.0-rc.0" +package.version = "0.15.0" package.license = "AGPL-3.0-only" package.authors = ["Element Backend Team"] package.edition = "2024" @@ -27,34 +27,34 @@ broken_intra_doc_links = "deny" [workspace.dependencies] # Workspace crates -mas-axum-utils = { path = "./crates/axum-utils/", version = "=0.15.0-rc.0" } -mas-cli = { path = "./crates/cli/", version = "=0.15.0-rc.0" } -mas-config = { path = "./crates/config/", version = "=0.15.0-rc.0" } -mas-data-model = { path = "./crates/data-model/", version = "=0.15.0-rc.0" } -mas-email = { path = "./crates/email/", version = "=0.15.0-rc.0" } -mas-graphql = { path = "./crates/graphql/", version = "=0.15.0-rc.0" } -mas-handlers = { path = "./crates/handlers/", version = "=0.15.0-rc.0" } -mas-http = { path = "./crates/http/", version = "=0.15.0-rc.0" } -mas-i18n = { path = "./crates/i18n/", version = "=0.15.0-rc.0" } -mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=0.15.0-rc.0" } -mas-iana = { path = "./crates/iana/", version = "=0.15.0-rc.0" } -mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=0.15.0-rc.0" } -mas-jose = { path = "./crates/jose/", version = "=0.15.0-rc.0" } -mas-keystore = { path = "./crates/keystore/", version = "=0.15.0-rc.0" } -mas-listener = { path = "./crates/listener/", version = "=0.15.0-rc.0" } -mas-matrix = { path = "./crates/matrix/", version = "=0.15.0-rc.0" } -mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=0.15.0-rc.0" } -mas-oidc-client = { path = "./crates/oidc-client/", version = "=0.15.0-rc.0" } -mas-policy = { path = "./crates/policy/", version = "=0.15.0-rc.0" } -mas-router = { path = "./crates/router/", version = "=0.15.0-rc.0" } -mas-spa = { path = "./crates/spa/", version = "=0.15.0-rc.0" } -mas-storage = { path = "./crates/storage/", version = "=0.15.0-rc.0" } -mas-storage-pg = { path = "./crates/storage-pg/", version = "=0.15.0-rc.0" } -mas-tasks = { path = "./crates/tasks/", version = "=0.15.0-rc.0" } -mas-templates = { path = "./crates/templates/", version = "=0.15.0-rc.0" } -mas-tower = { path = "./crates/tower/", version = "=0.15.0-rc.0" } -oauth2-types = { path = "./crates/oauth2-types/", version = "=0.15.0-rc.0" } -syn2mas = { path = "./crates/syn2mas", version = "=0.15.0-rc.0" } +mas-axum-utils = { path = "./crates/axum-utils/", version = "=0.15.0" } +mas-cli = { path = "./crates/cli/", version = "=0.15.0" } +mas-config = { path = "./crates/config/", version = "=0.15.0" } +mas-data-model = { path = "./crates/data-model/", version = "=0.15.0" } +mas-email = { path = "./crates/email/", version = "=0.15.0" } +mas-graphql = { path = "./crates/graphql/", version = "=0.15.0" } +mas-handlers = { path = "./crates/handlers/", version = "=0.15.0" } +mas-http = { path = "./crates/http/", version = "=0.15.0" } +mas-i18n = { path = "./crates/i18n/", version = "=0.15.0" } +mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=0.15.0" } +mas-iana = { path = "./crates/iana/", version = "=0.15.0" } +mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=0.15.0" } +mas-jose = { path = "./crates/jose/", version = "=0.15.0" } +mas-keystore = { path = "./crates/keystore/", version = "=0.15.0" } +mas-listener = { path = "./crates/listener/", version = "=0.15.0" } +mas-matrix = { path = "./crates/matrix/", version = "=0.15.0" } +mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=0.15.0" } +mas-oidc-client = { path = "./crates/oidc-client/", version = "=0.15.0" } +mas-policy = { path = "./crates/policy/", version = "=0.15.0" } +mas-router = { path = "./crates/router/", version = "=0.15.0" } +mas-spa = { path = "./crates/spa/", version = "=0.15.0" } +mas-storage = { path = "./crates/storage/", version = "=0.15.0" } +mas-storage-pg = { path = "./crates/storage-pg/", version = "=0.15.0" } +mas-tasks = { path = "./crates/tasks/", version = "=0.15.0" } +mas-templates = { path = "./crates/templates/", version = "=0.15.0" } +mas-tower = { path = "./crates/tower/", version = "=0.15.0" } +oauth2-types = { path = "./crates/oauth2-types/", version = "=0.15.0" } +syn2mas = { path = "./crates/syn2mas", version = "=0.15.0" } # OpenAPI schema generation and validation [workspace.dependencies.aide] diff --git a/tools/syn2mas/package-lock.json b/tools/syn2mas/package-lock.json index 1e9e16bf7..d85617479 100644 --- a/tools/syn2mas/package-lock.json +++ b/tools/syn2mas/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vector-im/syn2mas", - "version": "0.15.0-rc.0", + "version": "0.15.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vector-im/syn2mas", - "version": "0.15.0-rc.0", + "version": "0.15.0", "license": "AGPL-3.0-only", "dependencies": { "command-line-args": "^6.0.0", diff --git a/tools/syn2mas/package.json b/tools/syn2mas/package.json index ed777771c..6e0118152 100644 --- a/tools/syn2mas/package.json +++ b/tools/syn2mas/package.json @@ -1,6 +1,6 @@ { "name": "@vector-im/syn2mas", - "version": "0.15.0-rc.0", + "version": "0.15.0", "description": "A tool to migrate Synapse users and sessions to the Matrix Authentication Service", "license": "AGPL-3.0-only", "author": "Matrix.org", From d72e404dcd84b5aa37efba71260a4a5b4a87bb21 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:16:32 +0000 Subject: [PATCH 069/189] build(deps): bump clap from 4.5.36 to 4.5.37 Bumps [clap](https://github.com/clap-rs/clap) from 4.5.36 to 4.5.37. - [Release notes](https://github.com/clap-rs/clap/releases) - [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md) - [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.36...clap_complete-v4.5.37) --- updated-dependencies: - dependency-name: clap dependency-version: 4.5.37 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bef73983f..72777e975 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1004,9 +1004,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.36" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", @@ -1014,9 +1014,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.36" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", diff --git a/Cargo.toml b/Cargo.toml index d9d2a4182..278fff91c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,7 +119,7 @@ features = ["serde", "clock"] # CLI argument parsing [workspace.dependencies.clap] -version = "4.5.36" +version = "4.5.37" features = ["derive"] # Cron expressions From 53f1d3317162807aa97c51a7720af6176f0d1b30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:16:39 +0000 Subject: [PATCH 070/189] build(deps): bump der from 0.7.9 to 0.7.10 Bumps [der](https://github.com/RustCrypto/formats) from 0.7.9 to 0.7.10. - [Commits](https://github.com/RustCrypto/formats/compare/der/v0.7.9...der/v0.7.10) --- updated-dependencies: - dependency-name: der dependency-version: 0.7.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- crates/keystore/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bef73983f..8e3a46666 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1528,9 +1528,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", diff --git a/crates/keystore/Cargo.toml b/crates/keystore/Cargo.toml index 814ed860e..00c79ecc1 100644 --- a/crates/keystore/Cargo.toml +++ b/crates/keystore/Cargo.toml @@ -14,7 +14,7 @@ workspace = true [dependencies] aead = { version = "0.5.2", features = ["std"] } const-oid = { version = "0.9.6", features = ["std"] } -der = { version = "0.7.9", features = ["std"] } +der = { version = "0.7.10", features = ["std"] } elliptic-curve.workspace = true k256.workspace = true p256.workspace = true From 9991ebde2c5fec81393f00cbbefed93add7d2834 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 23 Apr 2025 17:24:18 +0200 Subject: [PATCH 071/189] Suggestions from code review: - what unit is cpu_time in - reduce the number of digits when logging timings --- crates/context/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/context/src/lib.rs b/crates/context/src/lib.rs index b289bfcda..e7a0f3d03 100644 --- a/crates/context/src/lib.rs +++ b/crates/context/src/lib.rs @@ -54,7 +54,7 @@ struct LogContextInner { /// The number of [`Future::poll`] recorded polls: AtomicU64, - /// An approximation of the total CPU time spent in the context + /// An approximation of the total CPU time spent in the context, in nanoseconds cpu_time: AtomicU64, } @@ -142,7 +142,7 @@ impl std::fmt::Display for LogContextStats { let elapsed_ms = self.elapsed.as_nanos() as f64 / 1_000_000.; write!( f, - "polls: {polls:>3}, cpu: {cpu_time_ms:>6.3}ms, elapsed: {elapsed_ms:>6.3}ms", + "polls: {polls}, cpu: {cpu_time_ms:.1}ms, elapsed: {elapsed_ms:.1}ms", ) } } From ae819a398f93dbcc9e6b6e065c5c67404a4dbb13 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 23 Apr 2025 17:36:48 +0200 Subject: [PATCH 072/189] Format code --- crates/context/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/context/src/lib.rs b/crates/context/src/lib.rs index e7a0f3d03..a0d6b384b 100644 --- a/crates/context/src/lib.rs +++ b/crates/context/src/lib.rs @@ -54,7 +54,8 @@ struct LogContextInner { /// The number of [`Future::poll`] recorded polls: AtomicU64, - /// An approximation of the total CPU time spent in the context, in nanoseconds + /// An approximation of the total CPU time spent in the context, in + /// nanoseconds cpu_time: AtomicU64, } From 4a430c1e352e3199373f87e3c1d58ae8bdb76552 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 23 Apr 2025 17:40:27 +0200 Subject: [PATCH 073/189] Remove the code of the old syn2mas tool --- tools/syn2mas/.gitignore | 2 - tools/syn2mas/.nvmrc | 1 - tools/syn2mas/Dockerfile | 48 - tools/syn2mas/LICENSE | 661 ---- tools/syn2mas/README.md | 16 - tools/syn2mas/package-lock.json | 3107 ----------------- tools/syn2mas/package.json | 55 - tools/syn2mas/src/advisor.mts | 251 -- tools/syn2mas/src/db.mts | 108 - tools/syn2mas/src/index.ts | 65 - tools/syn2mas/src/migrate.mts | 461 --- tools/syn2mas/src/schemas/mas.mts | 65 - tools/syn2mas/src/schemas/synapse.mts | 96 - .../syn2mas/src/types/MCompatAccessToken.d.ts | 36 - .../src/types/MCompatRefreshToken.d.ts | 37 - tools/syn2mas/src/types/MCompatSession.d.ts | 40 - .../syn2mas/src/types/MUpstreamOauthLink.d.ts | 37 - .../src/types/MUpstreamOauthProvider.d.ts | 38 - tools/syn2mas/src/types/MUser.d.ts | 17 - tools/syn2mas/src/types/MUserEmail.d.ts | 29 - tools/syn2mas/src/types/MUserPassword.d.ts | 18 - tools/syn2mas/src/types/SAccessToken.d.ts | 34 - tools/syn2mas/src/types/SRefreshToken.d.ts | 30 - tools/syn2mas/src/types/SUser.d.ts | 17 - tools/syn2mas/src/types/SUserExternalId.d.ts | 13 - tools/syn2mas/src/types/SUserThreePid.d.ts | 24 - tools/syn2mas/src/types/index.d.ts | 12 - tools/syn2mas/src/types/knex.d.ts | 13 - tools/syn2mas/tsconfig.eslint.json | 11 - tools/syn2mas/tsconfig.json | 12 - 30 files changed, 5354 deletions(-) delete mode 100644 tools/syn2mas/.gitignore delete mode 100644 tools/syn2mas/.nvmrc delete mode 100644 tools/syn2mas/Dockerfile delete mode 100644 tools/syn2mas/LICENSE delete mode 100644 tools/syn2mas/README.md delete mode 100644 tools/syn2mas/package-lock.json delete mode 100644 tools/syn2mas/package.json delete mode 100644 tools/syn2mas/src/advisor.mts delete mode 100644 tools/syn2mas/src/db.mts delete mode 100644 tools/syn2mas/src/index.ts delete mode 100644 tools/syn2mas/src/migrate.mts delete mode 100644 tools/syn2mas/src/schemas/mas.mts delete mode 100644 tools/syn2mas/src/schemas/synapse.mts delete mode 100644 tools/syn2mas/src/types/MCompatAccessToken.d.ts delete mode 100644 tools/syn2mas/src/types/MCompatRefreshToken.d.ts delete mode 100644 tools/syn2mas/src/types/MCompatSession.d.ts delete mode 100644 tools/syn2mas/src/types/MUpstreamOauthLink.d.ts delete mode 100644 tools/syn2mas/src/types/MUpstreamOauthProvider.d.ts delete mode 100644 tools/syn2mas/src/types/MUser.d.ts delete mode 100644 tools/syn2mas/src/types/MUserEmail.d.ts delete mode 100644 tools/syn2mas/src/types/MUserPassword.d.ts delete mode 100644 tools/syn2mas/src/types/SAccessToken.d.ts delete mode 100644 tools/syn2mas/src/types/SRefreshToken.d.ts delete mode 100644 tools/syn2mas/src/types/SUser.d.ts delete mode 100644 tools/syn2mas/src/types/SUserExternalId.d.ts delete mode 100644 tools/syn2mas/src/types/SUserThreePid.d.ts delete mode 100644 tools/syn2mas/src/types/index.d.ts delete mode 100644 tools/syn2mas/src/types/knex.d.ts delete mode 100644 tools/syn2mas/tsconfig.eslint.json delete mode 100644 tools/syn2mas/tsconfig.json diff --git a/tools/syn2mas/.gitignore b/tools/syn2mas/.gitignore deleted file mode 100644 index f06235c46..000000000 --- a/tools/syn2mas/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist diff --git a/tools/syn2mas/.nvmrc b/tools/syn2mas/.nvmrc deleted file mode 100644 index 8fdd954df..000000000 --- a/tools/syn2mas/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 \ No newline at end of file diff --git a/tools/syn2mas/Dockerfile b/tools/syn2mas/Dockerfile deleted file mode 100644 index 2927b1de9..000000000 --- a/tools/syn2mas/Dockerfile +++ /dev/null @@ -1,48 +0,0 @@ - -# Build Node.js app -FROM --platform=${BUILDPLATFORM} docker.io/library/node:22-bookworm AS builder - -WORKDIR /syn2mas - -COPY ./package.json ./package-lock.json ./tsconfig.json ./ -COPY ./src ./src -RUN --network=default \ - npm ci - -# Install the production dependencies for each architecture we support -FROM --platform=${BUILDPLATFORM} docker.io/library/node:22-bookworm AS deps - -WORKDIR /deps/arm64 - -COPY ./package.json ./package-lock.json ./ -# Remove the "prepare" script to avoid compiling typescript -RUN sed -i '/"prepare"/d' package.json -RUN --network=default \ - npm ci \ - --target_arch=amd64 \ - --target_platform=linux \ - --omit=dev - -WORKDIR /deps/amd64 - -COPY ./package.json ./package-lock.json ./ -# Remove the "prepare" script to avoid compiling typescript -RUN sed -i '/"prepare"/d' package.json -RUN --network=default \ - npm ci \ - --target_arch=x64 \ - --target_platform=linux \ - --omit=dev - - -# Runtime stage -FROM gcr.io/distroless/nodejs18-debian12:debug-nonroot - -WORKDIR /syn2mas -COPY ./package.json ./package-lock.json ./ -COPY --from=builder /syn2mas/dist ./dist - -ARG TARGETARCH -COPY --from=deps /deps/${TARGETARCH}/node_modules ./node_modules - -ENTRYPOINT ["/nodejs/bin/node", "--enable-source-maps", "/syn2mas/dist/index.js"] diff --git a/tools/syn2mas/LICENSE b/tools/syn2mas/LICENSE deleted file mode 100644 index be3f7b28e..000000000 --- a/tools/syn2mas/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/tools/syn2mas/README.md b/tools/syn2mas/README.md deleted file mode 100644 index c458c4247..000000000 --- a/tools/syn2mas/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# syn2mas - Synapse to Matrix Authentication Service - -Tool to help with the migration of a Matrix Synapse installation to the Matrix Authentication Service. - -The tool has two modes of operation: - -- Advisor mode: Analyses the Synapse configuration and reports on any issues that would prevent a successful migration. -- Migration mode: Performs the migration of the Synapse database into the Matrix Authentication Service database. - -## Usage - -Pre-migration advisor: - -```sh -npx @vector-im/syn2mas --command=advisor --synapseConfigFile homeserver.yaml -``` diff --git a/tools/syn2mas/package-lock.json b/tools/syn2mas/package-lock.json deleted file mode 100644 index d85617479..000000000 --- a/tools/syn2mas/package-lock.json +++ /dev/null @@ -1,3107 +0,0 @@ -{ - "name": "@vector-im/syn2mas", - "version": "0.15.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@vector-im/syn2mas", - "version": "0.15.0", - "license": "AGPL-3.0-only", - "dependencies": { - "command-line-args": "^6.0.0", - "id128": "^1.6.6", - "knex": "^3.0.1", - "log4js": "^6.9.1", - "pg": "^8.11.3", - "pg-query-stream": "^4.6.0", - "sqlite3": "^5.1.6", - "ts-command-line-args": "^2.5.1", - "yaml": "^2.3.3", - "zod": "^3.22.4" - }, - "bin": { - "syn2mas": "dist/index.js" - }, - "devDependencies": { - "@biomejs/biome": "^1.9.4", - "@tsconfig/node22": "^22.0.0", - "@tsconfig/strictest": "^2.0.2", - "@types/command-line-args": "^5.2.2", - "@types/node": "^22.0.0", - "tsx": "^4.16.2", - "typescript": "^5.2.2" - } - }, - "node_modules/@biomejs/biome": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", - "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", - "dev": true, - "hasInstallScript": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "1.9.4", - "@biomejs/cli-darwin-x64": "1.9.4", - "@biomejs/cli-linux-arm64": "1.9.4", - "@biomejs/cli-linux-arm64-musl": "1.9.4", - "@biomejs/cli-linux-x64": "1.9.4", - "@biomejs/cli-linux-x64-musl": "1.9.4", - "@biomejs/cli-win32-arm64": "1.9.4", - "@biomejs/cli-win32-x64": "1.9.4" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", - "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", - "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", - "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", - "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", - "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", - "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", - "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", - "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "license": "MIT", - "optional": true - }, - "node_modules/@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "license": "MIT", - "optional": true, - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@tsconfig/node22": { - "version": "22.0.1", - "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.1.tgz", - "integrity": "sha512-VkgOa3n6jvs1p+r3DiwBqeEwGAwEvnVCg/hIjiANl5IEcqP3G0u5m8cBJspe1t9qjZRlZ7WFgqq5bJrGdgAKMg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/strictest": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@tsconfig/strictest/-/strictest-2.0.5.tgz", - "integrity": "sha512-ec4tjL2Rr0pkZ5hww65c+EEPYwxOi4Ryv+0MtjeaSQRJyq322Q27eOQiFbuNgw2hpL4hB1/W/HBGk3VKS43osg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/command-line-args": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", - "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC", - "optional": true - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agentkeepalive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", - "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", - "license": "MIT", - "optional": true, - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "license": "MIT", - "optional": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "license": "ISC", - "optional": true - }, - "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/array-back": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", - "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "optional": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "optional": true, - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", - "license": "MIT" - }, - "node_modules/command-line-args": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz", - "integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==", - "dependencies": { - "array-back": "^6.2.2", - "find-replace": "^5.0.2", - "lodash.camelcase": "^4.3.0", - "typical": "^7.2.0" - }, - "engines": { - "node": ">=12.20" - }, - "peerDependencies": { - "@75lb/nature": "latest" - }, - "peerDependenciesMeta": { - "@75lb/nature": { - "optional": true - } - } - }, - "node_modules/command-line-usage": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", - "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", - "license": "MIT", - "dependencies": { - "array-back": "^4.0.2", - "chalk": "^2.4.2", - "table-layout": "^1.0.2", - "typical": "^5.2.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/command-line-usage/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/command-line-usage/node_modules/array-back": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", - "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/command-line-usage/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/command-line-usage/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/command-line-usage/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/command-line-usage/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/command-line-usage/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/command-line-usage/node_modules/typical": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", - "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT", - "optional": true - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC", - "optional": true - }, - "node_modules/date-format": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", - "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT", - "optional": true - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "optional": true - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "license": "MIT", - "optional": true - }, - "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/find-replace": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", - "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@75lb/nature": "latest" - }, - "peerDependenciesMeta": { - "@75lb/nature": { - "optional": true - } - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "license": "ISC" - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC", - "optional": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-tsconfig": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", - "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/getopts": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", - "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==", - "license": "MIT" - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "optional": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC", - "optional": true - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "license": "BSD-2-Clause", - "optional": true - }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/id128": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/id128/-/id128-1.6.6.tgz", - "integrity": "sha512-ExSXL9qcyQ7X/AfyO4ouARLnztm7Nmry1rwGi1nbrtSM90tjjqKzeMKqJfkw5bDdDX7XqdXIRzYYkVj5PU28Hg==", - "license": "MIT", - "engines": { - "node": ">=v6.9.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "license": "ISC", - "optional": true - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "optional": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/interpret": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", - "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "license": "MIT", - "optional": true, - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "license": "MIT", - "optional": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC", - "optional": true - }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "license": "MIT", - "optional": true - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/knex": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz", - "integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==", - "license": "MIT", - "dependencies": { - "colorette": "2.0.19", - "commander": "^10.0.0", - "debug": "4.3.4", - "escalade": "^3.1.1", - "esm": "^3.2.25", - "get-package-type": "^0.1.0", - "getopts": "2.3.0", - "interpret": "^2.2.0", - "lodash": "^4.17.21", - "pg-connection-string": "2.6.2", - "rechoir": "^0.8.0", - "resolve-from": "^5.0.0", - "tarn": "^3.0.2", - "tildify": "2.0.0" - }, - "bin": { - "knex": "bin/cli.js" - }, - "engines": { - "node": ">=16" - }, - "peerDependenciesMeta": { - "better-sqlite3": { - "optional": true - }, - "mysql": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-native": { - "optional": true - }, - "sqlite3": { - "optional": true - }, - "tedious": { - "optional": true - } - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT" - }, - "node_modules/log4js": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", - "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", - "license": "Apache-2.0", - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "flatted": "^3.2.7", - "rfdc": "^1.3.0", - "streamroller": "^3.1.5" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "license": "ISC", - "optional": true, - "dependencies": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "optional": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "optionalDependencies": { - "encoding": "^0.1.12" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "3.71.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", - "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" - }, - "node_modules/node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", - "license": "MIT", - "optional": true, - "dependencies": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": ">= 10.12.0" - } - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/pg": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz", - "integrity": "sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==", - "license": "MIT", - "dependencies": { - "pg-connection-string": "^2.7.0", - "pg-pool": "^3.8.0", - "pg-protocol": "^1.8.0", - "pg-types": "^2.1.0", - "pgpass": "1.x" - }, - "engines": { - "node": ">= 8.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.1.1" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", - "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", - "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", - "license": "MIT" - }, - "node_modules/pg-cursor": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.13.1.tgz", - "integrity": "sha512-t7niROd7/BVlRn2juI0S0MP/Ps87lNMpmnxMRQMOH0fboL0n7gH/MxpymSdR4rZRcPfoR3Sx47JG1u5JOJf6Gg==", - "license": "MIT", - "peerDependencies": { - "pg": "^8" - } - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.8.0.tgz", - "integrity": "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz", - "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==", - "license": "MIT" - }, - "node_modules/pg-query-stream": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/pg-query-stream/-/pg-query-stream-4.8.1.tgz", - "integrity": "sha512-kZo6C6HSzYFF6mlwl+etDk5QZD9CMdlHUXpof6PkK9+CHHaBLvOd2lZMwErOOpC/ldg4thrAojS8sG1B8PZ9Yw==", - "license": "MIT", - "dependencies": { - "pg-cursor": "^2.13.1" - }, - "peerDependencies": { - "pg": "^8" - } - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pg/node_modules/pg-connection-string": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", - "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", - "license": "MIT" - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", - "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "license": "ISC", - "optional": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "license": "MIT", - "optional": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/reduce-flatten": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", - "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "license": "MIT" - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "optional": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT", - "optional": true - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC", - "optional": true - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC", - "optional": true - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", - "license": "MIT", - "optional": true, - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/sqlite3": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", - "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1", - "tar": "^6.1.11" - }, - "optionalDependencies": { - "node-gyp": "8.x" - }, - "peerDependencies": { - "node-gyp": "8.x" - }, - "peerDependenciesMeta": { - "node-gyp": { - "optional": true - } - } - }, - "node_modules/ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/streamroller": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", - "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", - "license": "MIT", - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "fs-extra": "^8.1.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-format": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", - "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", - "license": "WTFPL OR MIT" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/table-layout": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", - "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", - "license": "MIT", - "dependencies": { - "array-back": "^4.0.1", - "deep-extend": "~0.6.0", - "typical": "^5.2.0", - "wordwrapjs": "^4.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/table-layout/node_modules/array-back": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", - "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/table-layout/node_modules/typical": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", - "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/tarn": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", - "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/tildify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", - "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ts-command-line-args": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", - "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", - "license": "ISC", - "dependencies": { - "chalk": "^4.1.0", - "command-line-args": "^5.1.1", - "command-line-usage": "^6.1.0", - "string-format": "^2.0.0" - }, - "bin": { - "write-markdown": "dist/write-markdown.js" - } - }, - "node_modules/ts-command-line-args/node_modules/array-back": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", - "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ts-command-line-args/node_modules/command-line-args": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", - "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", - "license": "MIT", - "dependencies": { - "array-back": "^3.1.0", - "find-replace": "^3.0.0", - "lodash.camelcase": "^4.3.0", - "typical": "^4.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/ts-command-line-args/node_modules/find-replace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", - "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", - "license": "MIT", - "dependencies": { - "array-back": "^3.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/ts-command-line-args/node_modules/typical": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", - "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tsx": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", - "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typical": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-7.2.0.tgz", - "integrity": "sha512-W1+HdVRUl8fS3MZ9ogD51GOb46xMmhAZzR0WPw5jcgIZQJVvkddYzAl4YTU6g5w33Y1iRQLdIi2/1jhi2RNL0g==", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "unique-slug": "^2.0.0" - } - }, - "node_modules/unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "license": "ISC", - "optional": true, - "dependencies": { - "imurmurhash": "^0.1.4" - } - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "optional": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wordwrapjs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", - "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", - "license": "MIT", - "dependencies": { - "reduce-flatten": "^2.0.0", - "typical": "^5.2.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/wordwrapjs/node_modules/typical": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", - "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/zod": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/tools/syn2mas/package.json b/tools/syn2mas/package.json deleted file mode 100644 index 6e0118152..000000000 --- a/tools/syn2mas/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "@vector-im/syn2mas", - "version": "0.15.0", - "description": "A tool to migrate Synapse users and sessions to the Matrix Authentication Service", - "license": "AGPL-3.0-only", - "author": "Matrix.org", - "type": "module", - "repository": { - "type": "git", - "url": "https://github.com/element-hq/matrix-authentication-service" - }, - "bin": { - "syn2mas": "dist/index.js" - }, - "files": [ - "src", - "dist", - "package.json", - "LICENSE", - "README.md" - ], - "publishConfig": { - "access": "public" - }, - "scripts": { - "prepare": "npm run build", - "build": "tsc", - "dev": "tsx src/index.ts", - "lint": "npm run lint:types && npm run lint:style", - "lint:style": "biome check", - "lint:types": "tsc --noEmit", - "start": "node dist/index.js" - }, - "devDependencies": { - "@biomejs/biome": "^1.9.4", - "@tsconfig/node22": "^22.0.0", - "@tsconfig/strictest": "^2.0.2", - "@types/command-line-args": "^5.2.2", - "@types/node": "^22.0.0", - "tsx": "^4.16.2", - "typescript": "^5.2.2" - }, - "dependencies": { - "command-line-args": "^6.0.0", - "id128": "^1.6.6", - "knex": "^3.0.1", - "log4js": "^6.9.1", - "pg": "^8.11.3", - "pg-query-stream": "^4.6.0", - "sqlite3": "^5.1.6", - "ts-command-line-args": "^2.5.1", - "yaml": "^2.3.3", - "zod": "^3.22.4" - } -} diff --git a/tools/syn2mas/src/advisor.mts b/tools/syn2mas/src/advisor.mts deleted file mode 100644 index b320c20fb..000000000 --- a/tools/syn2mas/src/advisor.mts +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import { readFile } from "node:fs/promises"; - -import type { Knex } from "knex"; -import log4js from "log4js"; -import { parse } from "ts-command-line-args"; -import yaml from "yaml"; - -import { connectToSynapseDatabase } from "./db.mjs"; -import { - type SynapseOIDCProvider, - synapseConfig as synapseConfigSchema, -} from "./schemas/synapse.mjs"; -import type { SAccessToken } from "./types/SAccessToken.d.ts"; -import type { SRefreshToken } from "./types/SRefreshToken.d.ts"; -import type { SUser } from "./types/SUser.d.ts"; -import type { SUserThreePid } from "./types/SUserThreePid.d.ts"; - -const log = log4js.getLogger("migrate"); - -interface Options { - command: string; - synapseConfigFile: string; - help?: boolean; -} - -export async function advisor(): Promise { - const args = parse( - { - command: { - type: String, - description: "Command to run", - defaultOption: true, - typeLabel: "migrate", - }, - synapseConfigFile: { - type: String, - description: "Path to synapse homeserver.yaml config file", - }, - help: { - type: Boolean, - optional: true, - alias: "h", - description: "Prints this usage guide", - }, - }, - { - helpArg: "help", - }, - ); - - const warnings: string[] = []; - function warn(message: string): void { - log.warn(message); - warnings.push(message); - } - - const errors: string[] = []; - function error(message: string): void { - log.error(message); - errors.push(message); - } - - // load synapse config - const synapseConfig = synapseConfigSchema.parse( - yaml.parse(await readFile(args.synapseConfigFile, "utf8")), - ); - - // connect to synapse databases - const synapse = await connectToSynapseDatabase(synapseConfig); - - async function count(query: Knex.QueryBuilder): Promise { - const res = await query.first(); - if (!res) { - return 0; - } - return res["count(*)"] as number; - } - - const guestUsers = await count( - synapse.count("*").from("users").where({ is_guest: 1 }), - ); - if (guestUsers > 0) { - error( - `Synapse database contains ${guestUsers} guest users which aren't supported by MAS: https://github.com/element-hq/matrix-authentication-service/issues/1445`, - ); - } - if (synapseConfig.allow_guest_access) { - if (guestUsers > 0) { - error( - "Synapse config allows guest access which isn't supported by MAS: https://github.com/element-hq/matrix-authentication-service/issues/1445", - ); - } else { - error( - "Synapse config allows guest access which isn't supported by MAS, but no guest users were found in the database so the option could be disabled: https://github.com/element-hq/matrix-authentication-service/issues/1445", - ); - } - } - - if (synapseConfig.enable_registration) { - warn( - "Synapse config has registration enabled which will need to be disabled after migration", - ); - } - if (synapseConfig.enable_registration_captcha) { - warn( - "Synapse config has registration CAPTCHA enabled which will need to configured in MAS", - ); - } - if (synapseConfig.user_consent) { - warn( - "Synapse config has user_consent configured which will need to be disabled after migration", - ); - } - - const usersWithoutEmailAddress = await count( - synapse - .count("*") - .from("users") - .leftOuterJoin( - "user_threepids", - "users.name", - "user_threepids.user_id", - ) - .whereNull("user_threepids.user_id"), - ); - if (usersWithoutEmailAddress > 0) { - warn( - `Synapse database contains ${usersWithoutEmailAddress} users without a verified email address who will need to verify their email address before they can login after migration: https://github.com/element-hq/matrix-authentication-service/issues/1505`, - ); - } - - const accessTokensWithoutDeviceId = await count( - synapse - .count("*") - .from("access_tokens") - .where({ device_id: "" }) - .orWhereNull("device_id"), - ); - if (accessTokensWithoutDeviceId > 0) { - error( - `Synapse database contains ${accessTokensWithoutDeviceId} access tokens without an associated device_id which will be skipped during migration`, - ); - } - - const nonEmailThreePids = await count( - synapse - .count("*") - .from("user_threepids") - .whereNot({ medium: "email" }), - ); - if (nonEmailThreePids > 0) { - error( - `Synapse database contains ${nonEmailThreePids} non-email 3pids which will be ignored during migration`, - ); - } - - const oidcProviders: SynapseOIDCProvider[] = [ - ...(synapseConfig.oidc_providers ?? []), - ...(synapseConfig.oidc_config ? [synapseConfig.oidc_config] : []), - ]; - for (const provider of oidcProviders) { - warn( - `Synapse config contains OIDC auth configuration which will need mapping to be manually mapped to an upstream OpenID Provider during migration: ${provider.issuer}`, - ); - } - - if (synapseConfig.cas_config?.enabled) { - warn( - "Synapse config contains CAS auth configuration which will need mapping to be manually mapped to an upstream OpenID Provider during migration", - ); - } - if (synapseConfig.saml2_config?.sp_config) { - warn( - "Synapse config contains SAML2 auth configuration which will need mapping to be manually mapped to an upstream OpenID Provider during migration", - ); - } - if (synapseConfig.jwt_config?.enabled) { - warn( - "Synapse config contains JWT auth configuration which will need mapping to be manually mapped to an upstream OpenID Provider during migration", - ); - } - if ( - synapseConfig.password_config?.enabled !== false && - synapseConfig.password_config?.localdb_enabled === false - ) { - warn( - "Synapse has a non-standard password auth enabled which won't work after migration and will need to be manually mapped to an upstream OpenID Provider during migration", - ); - } - - const externalIdAuthProviders = (await synapse - .select("auth_provider") - .count("* as Count") - .from("user_external_ids") - .groupBy("auth_provider")) as { auth_provider: string; Count: number }[]; - for (const row of externalIdAuthProviders) { - warn( - `An upstream OpenID Provider will need to be configured for the ${row.Count} users with auth provider ${row.auth_provider}`, - ); - } - - const usersWithPassword = await count( - synapse.count("*").from("users").whereNotNull("password_hash"), - ); - if (usersWithPassword > 0) { - log.info( - `Synapse database contains ${usersWithPassword} users with a password which will be migrated.`, - ); - } - - const accessTokensToImport = await count( - synapse - .count("*") - .from("access_tokens") - .whereNotNull("device_id"), - ); - if (accessTokensToImport > 0) { - log.info( - `Synapse database contains ${accessTokensToImport} access tokens which will be migrated`, - ); - } - - const synapseRefreshToken = await count( - synapse.select("*").from("refresh_tokens"), - ); - if (synapseRefreshToken > 0) { - log.info( - `Synapse database contains ${synapseRefreshToken} refresh tokens which will be migrated`, - ); - } - - if (synapseConfig.enable_3pid_changes === true) { - warn( - "Synapse config has enable_3pid_changes enabled which must to be disabled or removed after migration", - ); - } - - if (synapseConfig.login_via_existing_session?.enabled === true) { - warn( - "Synapse config has login_via_existing_session enabled which must to be disabled or removed after migration", - ); - } - - process.exit(errors.length > 0 ? 1 : 0); -} diff --git a/tools/syn2mas/src/db.mts b/tools/syn2mas/src/db.mts deleted file mode 100644 index 9d8d0d0c8..000000000 --- a/tools/syn2mas/src/db.mts +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import { readFile } from "node:fs/promises"; -import type { SecureContextOptions } from "node:tls"; - -import knex, { type Knex } from "knex"; - -import type { - MASConfig, - DatabaseConfig as MASDatabaseConfig, - URIDatabaseConfig as MASURIDatabaseConfig, -} from "./schemas/mas.mjs"; -import type { SynapseConfig } from "./schemas/synapse.mjs"; - -export async function connectToSynapseDatabase({ - database, -}: SynapseConfig): Promise { - if (!database) { - throw new Error("Synapse database not configured"); - } - - if (database.name === "sqlite3") { - return knex({ - client: "sqlite3", - connection: { filename: database.args.database }, - useNullAsDefault: true, - }); - } - - const connection: Knex.PgConnectionConfig = {}; - if (database.args.database) connection.database = database.args.database; - if (database.args.dbname) connection.database = database.args.dbname; - if (database.args.user) connection.user = database.args.user; - if (database.args.password) connection.password = database.args.password; - if (database.args.host) connection.host = database.args.host; - if (typeof database.args.port === "number") - connection.port = database.args.port; - if (typeof database.args.port === "string") - connection.port = Number.parseInt(database.args.port); - - const ssl: SecureContextOptions = {}; - if (database.args.sslcert) ssl.cert = await readFile(database.args.sslcert); - if (database.args.sslrootcert) - ssl.ca = await readFile(database.args.sslrootcert); - if (database.args.sslkey) ssl.key = await readFile(database.args.sslkey); - if (database.args.sslpassword) ssl.passphrase = database.args.sslpassword; - - if (Object.keys(ssl).length > 0) { - connection.ssl = ssl; - } - - return knex({ - client: "pg", - connection, - }); -} - -const isUriConfig = ( - database: MASDatabaseConfig, -): database is MASURIDatabaseConfig => - "uri" in database && typeof database.uri === "string"; - -export async function connectToMASDatabase({ - database, -}: MASConfig): Promise { - const connection: Knex.PgConnectionConfig = {}; - const ssl: SecureContextOptions = {}; - if (isUriConfig(database)) { - connection.connectionString = database.uri; - } else { - if (database.database) connection.database = database.database; - if (database.username) connection.user = database.username; - if (database.password) connection.password = database.password; - if (database.host) connection.host = database.host; - if (database.port) connection.port = database.port; - } - - if (database.ssl_ca) { - ssl.ca = database.ssl_ca; - } else if (database.ssl_ca_file) { - ssl.ca = await readFile(database.ssl_ca_file); - } - - if (database.ssl_certificate) { - ssl.cert = database.ssl_certificate; - } else if (database.ssl_certificate_file) { - ssl.cert = await readFile(database.ssl_certificate_file); - } - - if (database.ssl_key) { - ssl.key = database.ssl_key; - } else if (database.ssl_key_file) { - ssl.key = await readFile(database.ssl_key_file); - } - - if (Object.keys(ssl).length > 0) { - connection.ssl = ssl; - } - - return knex({ - client: "pg", - connection, - }); -} diff --git a/tools/syn2mas/src/index.ts b/tools/syn2mas/src/index.ts deleted file mode 100644 index 5d736191b..000000000 --- a/tools/syn2mas/src/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env node -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import log4js from "log4js"; -import { type ArgumentConfig, parse } from "ts-command-line-args"; - -import { advisor } from "./advisor.mjs"; -import { migrate } from "./migrate.mjs"; - -log4js.configure({ - appenders: { - console: { type: "console" }, - }, - categories: { - default: { appenders: ["console"], level: "debug" }, - }, -}); - -const log = log4js.getLogger(); - -interface MainOptions { - command: string; - help?: boolean; -} - -const mainArgOptions: ArgumentConfig = { - command: { - type: String, - description: "Command to run", - defaultOption: true, - typeLabel: "", - }, - help: { - type: Boolean, - optional: true, - alias: "h", - description: "Prints this usage guide", - }, -}; - -export const mainArgs = parse(mainArgOptions, { - stopAtFirstUnknown: true, -}); - -try { - if (mainArgs.command === "migrate") { - await migrate(); - process.exit(0); - } - - if (mainArgs.command === "advisor") { - await advisor(); - process.exit(0); - } - - parse(mainArgOptions, { helpArg: "help" }); - process.exit(1); -} catch (e) { - log.error(e); - process.exit(1); -} diff --git a/tools/syn2mas/src/migrate.mts b/tools/syn2mas/src/migrate.mts deleted file mode 100644 index 2574622e0..000000000 --- a/tools/syn2mas/src/migrate.mts +++ /dev/null @@ -1,461 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import { readFile } from "node:fs/promises"; - -import id128 from "id128"; -import log4js from "log4js"; -import { parse } from "ts-command-line-args"; -import yaml from "yaml"; - -import { connectToMASDatabase, connectToSynapseDatabase } from "./db.mjs"; -import { masConfig as masConfigSchema } from "./schemas/mas.mjs"; -import { synapseConfig as synapseConfigSchema } from "./schemas/synapse.mjs"; -import type { MCompatAccessToken } from "./types/MCompatAccessToken.d.ts"; -import type { MCompatRefreshToken } from "./types/MCompatRefreshToken.d.ts"; -import type { MCompatSession } from "./types/MCompatSession.d.ts"; -import type { MUpstreamOauthLink } from "./types/MUpstreamOauthLink.d.ts"; -import type { MUpstreamOauthProvider } from "./types/MUpstreamOauthProvider.d.ts"; -import type { MUser } from "./types/MUser.js"; -import type { MUserEmail } from "./types/MUserEmail.d.ts"; -import type { MUserPassword } from "./types/MUserPassword.d.ts"; -import type { SAccessToken } from "./types/SAccessToken.d.ts"; -import type { SRefreshToken } from "./types/SRefreshToken.d.ts"; -import type { SUser } from "./types/SUser.d.ts"; -import type { SUserExternalId } from "./types/SUserExternalId.d.ts"; -import type { SUserThreePid } from "./types/SUserThreePid.d.ts"; -import type { UUID } from "./types/index.d.ts"; - -const log = log4js.getLogger("migrate"); - -interface MigrationOptions { - command: string; - synapseConfigFile: string; - masConfigFile: string; - upstreamProviderMapping: string[]; - dryRun?: boolean; - help?: boolean; -} - -// Parses a string that is either a UUID or a ULID -// Returns [uuid, ulid] in canonical format -const parseUuidOrUlid = (input: string): [string, string] => { - let bytes: Uint8Array; - if (id128.Ulid.isCanonical(input)) { - bytes = id128.Ulid.fromCanonicalTrusted(input).bytes; - } else if (id128.Uuid.isCanonical(input)) { - bytes = id128.Uuid.fromCanonicalTrusted(input).bytes; - } else { - bytes = id128.Uuid.fromRaw(input).bytes; - } - - return [ - id128.Uuid.construct(bytes).toCanonical(), - id128.Ulid.construct(bytes).toCanonical(), - ]; -}; - -export async function migrate(): Promise { - const args = parse( - { - command: { - type: String, - description: "Command to run", - defaultOption: true, - typeLabel: "migrate", - }, - synapseConfigFile: { - type: String, - description: "Path to synapse homeserver.yaml config file", - }, - masConfigFile: { type: String, description: "Path to MAS config.yaml" }, - upstreamProviderMapping: { - type: String, - defaultValue: [], - multiple: true, - description: - "Mapping of upstream provider IDs to MAS provider IDs. Format: :", - }, - dryRun: { - type: Boolean, - optional: true, - defaultValue: false, - description: "Dry run only, do not write to database", - }, - help: { - type: Boolean, - optional: true, - alias: "h", - description: "Prints this usage guide", - }, - }, - { - helpArg: "help", - }, - ); - - const warnings: string[] = []; - function warn(message: string): void { - warnings.push(message); - } - - let fatals = 0; - function fatal(message: string): void { - log.fatal(message); - for (const w of warnings) log.warn(w); - if (!args.dryRun) { - process.exit(1); - } - fatals += 1; - } - - function makeUuid(time: Date): UUID { - return id128.Uuid.construct( - id128.Ulid.generate({ time }).bytes, - ).toCanonical(); - } - - // load synapse config - const synapseConfig = synapseConfigSchema.parse( - yaml.parse(await readFile(args.synapseConfigFile, "utf8")), - ); - - // connect to synapse database - const synapse = await connectToSynapseDatabase(synapseConfig); - - // load MAS config - const masConfig = masConfigSchema.parse( - yaml.parse(await readFile(args.masConfigFile, "utf8")), - ); - - // connect to MAS database - const mas = await connectToMASDatabase(masConfig); - - const upstreamProviders = new Map(); - - for (const mapping of args.upstreamProviderMapping) { - const [providerId, masProviderId] = mapping.split(":"); - if (!providerId || !masProviderId) { - throw new Error( - `Upstream provider mapping is not in correct format. It should be :: ${mapping}`, - ); - } - - if ( - !id128.Uuid.isRaw(masProviderId) && - !id128.Uuid.isCanonical(masProviderId) && - !id128.Ulid.isCanonical(masProviderId) - ) { - throw new Error( - `Upstream provider mapping is not in correct format. It should be a UUID or a ULID: ${masProviderId}`, - ); - } - - const [masProviderUuid, masProviderUlid] = parseUuidOrUlid(masProviderId); - - log.info( - `Loading existing upstream provider ${masProviderUlid} from MAS database as ${providerId}`, - ); - const existingProvider = await mas("upstream_oauth_providers") - .select("*") - .where({ upstream_oauth_provider_id: masProviderUuid }) - .first(); - if (!existingProvider) { - throw new Error( - `Could not find upstream provider ${masProviderUlid} in MAS database`, - ); - } - upstreamProviders.set(providerId, existingProvider); - } - - function stringifyAndRedact(input: unknown): string { - const x = JSON.stringify(input); - - return x.replace( - /("(password_hash|hashed_password|access_token|token)":")[^"]*"/, - '$1redacted"', - ); - } - - type Execution = () => Promise; - - const existingMasUsers = await mas - .count({ count: "*" }) - .from("users") - .first(); - - if (Number.parseInt(`${existingMasUsers?.count ?? 0}`) > 0) { - fatal( - `Found ${existingMasUsers?.count} existing users in MAS. Refusing to continue. Please clean MAS and try again.`, - ); - } - - async function migrateUser(user: SUser): Promise { - const localpart = user.name.split(":")[0].substring(1); - log.info(`Processing user ${user.name} as ${localpart}`); - - let warningsForUser = 0; - const executions: Execution[] = []; - - if (user.is_guest === 1) { - fatal(`Migration of guest users is not supported: ${user.name}`); - } - - // users => users - const userCreatedAt = new Date( - Number.parseInt(`${user.creation_ts}`) * 1000, - ); - const masUser: MUser = { - user_id: makeUuid(userCreatedAt), - username: localpart, - created_at: userCreatedAt, - locked_at: user.deactivated === 1 ? userCreatedAt : null, - can_request_admin: user.admin === 1, - }; - executions.push(() => mas.insert(masUser).into("users")); - log.debug(`${stringifyAndRedact(user)} => ${stringifyAndRedact(masUser)}`); - // users.password_hash => user_passwords - if (user.password_hash) { - const masUserPassword: MUserPassword = { - user_password_id: makeUuid(userCreatedAt), - user_id: masUser.user_id, - hashed_password: user.password_hash, - created_at: masUser.created_at, // TODO: should we use now() instead of created_at? - version: 1, - }; - - log.debug( - `Password ${user.password_hash.slice(-4)} => ${stringifyAndRedact( - masUserPassword, - )}`, - ); - executions.push(() => mas.insert(masUserPassword).into("user_passwords")); - } - - // user_threepids => user_emails - const synapseThreePids = await synapse - .select("*") - .from("user_threepids") - .where({ user_id: user.name }); - for (const threePid of synapseThreePids) { - if (threePid.medium !== "email") { - warningsForUser += 1; - warn( - `Skipping non-email 3pid ${threePid.medium} for user ${user.name}`, - ); - continue; - } - const threePidCreatedAt = new Date( - Number.parseInt(`${threePid.added_at}`), - ); - const masUserEmail: MUserEmail = { - user_email_id: makeUuid(threePidCreatedAt), - user_id: masUser.user_id, - email: threePid.address.toLowerCase(), - created_at: threePidCreatedAt, - }; - - if (threePid.validated_at) { - masUserEmail.confirmed_at = new Date( - Number.parseInt(`${threePid.validated_at}`), - ); - } - - log.debug( - `${stringifyAndRedact(threePid)} => ${stringifyAndRedact( - masUserEmail, - )}`, - ); - executions.push(() => mas.insert(masUserEmail).into("user_emails")); - } - - // user_external_ids => upstream_oauth_links - const synapseExternalIds = await synapse - .select("*") - .from("user_external_ids") - .where({ user_id: user.name }); - for (const externalId of synapseExternalIds) { - try { - const provider = upstreamProviders.get(externalId.auth_provider); - if (!provider) { - throw new Error( - `Unknown upstream provider ${externalId.auth_provider}`, - ); - } - const masUpstreamOauthLink: MUpstreamOauthLink = { - upstream_oauth_link_id: makeUuid(userCreatedAt), - user_id: masUser.user_id, - upstream_oauth_provider_id: provider.upstream_oauth_provider_id, - subject: externalId.external_id, - created_at: masUser.created_at, - }; - - log.debug( - `${stringifyAndRedact(synapseExternalIds)} => ${stringifyAndRedact( - masUpstreamOauthLink, - )}`, - ); - - executions.push(() => - mas.insert(masUpstreamOauthLink).into("upstream_oauth_links"), - ); - } catch (e) { - fatal( - `Failed to import external id ${externalId.external_id} with ${externalId.auth_provider} for user ${user.name}: ${e}`, - ); - } - } - - // We only import access tokens for active users - if (user.deactivated === 1) { - log.info( - `Skipping access tokens import for deactivated user ${user.name}`, - ); - } else { - // access_tokens,refresh_tokens => compat_sessions,compat_access_tokens - const synapseAccessTokens = await synapse - .select("*") - .from("access_tokens") - .where({ user_id: user.name }) - // Skip tokens without devices. - // These can be for example short-lived tokens created by puppeting a user over the Synapse admin API. - .whereNotNull("device_id"); - for (const accessToken of synapseAccessTokens) { - const tokenCreatedAt = accessToken.last_validated - ? new Date(Number.parseInt(`${accessToken.last_validated}`)) - : masUser.created_at; - const masCompatSession: MCompatSession = { - compat_session_id: makeUuid(tokenCreatedAt), - user_id: masUser.user_id, - device_id: accessToken.device_id, - created_at: tokenCreatedAt, - is_synapse_admin: user.admin === 1, - }; - log.debug( - `${stringifyAndRedact(accessToken)} => ${stringifyAndRedact( - masCompatSession, - )}`, - ); - executions.push(() => - mas.insert(masCompatSession).into("compat_sessions"), - ); - - const masCompatAccessToken: MCompatAccessToken = { - compat_access_token_id: makeUuid(tokenCreatedAt), - compat_session_id: masCompatSession.compat_session_id, - access_token: accessToken.token, - created_at: tokenCreatedAt, - }; - log.debug( - `Access token ${accessToken.id} => ${stringifyAndRedact( - masCompatAccessToken, - )}`, - ); - executions.push(() => - mas.insert(masCompatAccessToken).into("compat_access_tokens"), - ); - - if (accessToken.refresh_token_id) { - const synapseRefreshToken = await synapse - .select("*") - .from("refresh_tokens") - .where({ id: accessToken.refresh_token_id }) - .first(); - if (synapseRefreshToken) { - const masCompatRefreshToken: MCompatRefreshToken = { - compat_refresh_token_id: makeUuid(tokenCreatedAt), - compat_session_id: masCompatSession.compat_session_id, - compat_access_token_id: - masCompatAccessToken.compat_access_token_id, - refresh_token: synapseRefreshToken.token, - created_at: tokenCreatedAt, - }; - log.debug( - `Refresh token ${synapseRefreshToken.id} => ${stringifyAndRedact( - masCompatRefreshToken, - )}`, - ); - executions.push(() => - mas.insert(masCompatRefreshToken).into("compat_refresh_tokens"), - ); - } else { - warningsForUser += 1; - warn( - `Unable to locate refresh token ${accessToken.refresh_token_id} for user ${user.name}`, - ); - } - } - } - } - - if (warningsForUser > 0) { - if (!args.dryRun) { - fatal(`User ${user.name} had ${warningsForUser} warnings`); - } else { - log.warn(`User ${user.name} had ${warningsForUser} warnings`); - } - } else if (!args.dryRun) { - log.info(`Running ${executions.length} updates for user ${user.name}`); - const tx = await mas.transaction(); - try { - for (const execution of executions) { - await execution(); - } - await tx.commit(); - log.info(`Migrated user ${user.name}`); - } catch (e) { - try { - await tx.rollback(); - } catch (e2) { - log.error(`Failed to rollback transaction: ${e2}`); - } - throw e; - } - } - } - - // this is a workaround to get the list of columns that we care about from the SUser type - const SUserColumns: Record = { - name: undefined, - password_hash: undefined, - admin: undefined, - is_guest: undefined, - deactivated: undefined, - creation_ts: undefined, - appservice_id: undefined, - }; - - // Get all Synapse users, except appservice owned users who don't need to be migrated - const synapseUserQuery = synapse - .select(Object.keys(SUserColumns) as (keyof SUser)[]) - .from("users") - .whereNull("appservice_id"); - - let synapseUsers = 0; - if (synapseConfig.database.name === "sqlite3") { - // SQLite doesn't support streaming - const synapseUserRows = (await synapseUserQuery) as unknown as SUser[]; - for (const user of synapseUserRows) { - synapseUsers += 1; - await migrateUser(user); - } - } else { - // Stream users from the database - const synapseUserStream = synapseUserQuery.stream(); - for await (const user of synapseUserStream) { - synapseUsers += 1; - await migrateUser(user as unknown as SUser); - } - } - - log.info( - `Completed migration ${args.dryRun ? "dry-run " : ""}of ${synapseUsers} users with ${fatals} fatals and ${warnings.length} warnings:`, - ); - for (const w of warnings) log.warn(w); - if (fatals > 0) { - throw new Error(`Migration failed with ${fatals} fatals`); - } -} diff --git a/tools/syn2mas/src/schemas/mas.mts b/tools/syn2mas/src/schemas/mas.mts deleted file mode 100644 index dc6fbb088..000000000 --- a/tools/syn2mas/src/schemas/mas.mts +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import * as z from "zod"; - -const ssl = z - .object({ - ssl_ca: z.string().optional(), - ssl_ca_file: z.string().optional(), - ssl_certificate: z.string().optional(), - ssl_certificate_file: z.string().optional(), - ssl_key: z.string().optional(), - ssl_key_file: z.string().optional(), - }) - .refine((ssl) => { - if (ssl.ssl_ca && ssl.ssl_ca_file) { - throw new Error("Cannot specify both ssl_ca and ssl_ca_file"); - } - - if (ssl.ssl_certificate && ssl.ssl_certificate_file) { - throw new Error("Cannot specify both ssl_cert and ssl_cert_file"); - } - - if (ssl.ssl_key && ssl.ssl_key_file) { - throw new Error("Cannot specify both ssl_key and ssl_key_file"); - } - - return true; - }); - -const uriDatabaseConfig = z - .object({ - uri: z.string(), - }) - .and(ssl); - -export type URIDatabaseConfig = z.infer; - -const objectDatabaseConfig = z - .object({ - host: z.string().optional(), - port: z.number().optional(), - username: z.string().optional(), - password: z.string().optional(), - database: z.string().optional(), - }) - .and(ssl); - -const databaseConfig = z.union([uriDatabaseConfig, objectDatabaseConfig]); - -export type DatabaseConfig = z.infer; - -const secretsConfig = z.object({ - encryption: z.string(), -}); - -export const masConfig = z.object({ - database: databaseConfig, - secrets: secretsConfig, -}); - -export type MASConfig = z.infer; diff --git a/tools/syn2mas/src/schemas/synapse.mts b/tools/syn2mas/src/schemas/synapse.mts deleted file mode 100644 index 03c238990..000000000 --- a/tools/syn2mas/src/schemas/synapse.mts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import * as z from "zod"; - -const sqlite3DatabaseConfig = z.object({ - name: z.literal("sqlite3"), - args: z.object({ - database: z.string(), - }), -}); - -const psycopg2DatabaseConfig = z.object({ - name: z.literal("psycopg2"), - args: z.object({ - user: z.string().nullish(), - password: z.string().nullish(), - database: z.string().nullish(), - dbname: z.string().nullish(), - host: z.string().nullish(), - port: z.union([z.number(), z.string()]).nullish(), - sslcert: z.string().nullish(), - sslkey: z.string().nullish(), - sslpassword: z.string().nullish(), - sslrootcert: z.string().nullish(), - }), -}); - -const databaseConfig = z.union([sqlite3DatabaseConfig, psycopg2DatabaseConfig]); - -const oidcProviderConfig = z.object({ - idp_id: z.string(), - idp_name: z.string().nullish(), - issuer: z.string(), - client_id: z.string(), - scopes: z.array(z.string()), - client_auth_method: z - .union([ - z.literal("client_secret_basic"), - z.literal("client_secret_post"), - z.literal("none"), - ]) - .nullish(), - client_secret: z.string().nullish(), - client_secret_jwt_key: z.string().nullish(), -}); - -export type SynapseOIDCProvider = z.infer; - -export const synapseConfig = z.object({ - database: databaseConfig, - oidc_providers: z.array(oidcProviderConfig).nullish(), - oidc_config: oidcProviderConfig.nullish(), - allow_guest_access: z.boolean().nullish(), - cas_config: z - .object({ - enabled: z.boolean().nullish(), - }) - .nullish(), - saml2_config: z - .object({ - sp_config: z.object({}).nullish(), - }) - .nullish(), - sso: z - .object({ - client_whitelist: z.array(z.string()).nullish(), - update_profile_information: z.boolean().nullish(), - }) - .nullish(), - jwt_config: z - .object({ - enabled: z.boolean().nullish(), - }) - .nullish(), - password_config: z - .object({ - enabled: z.boolean().nullish(), - localdb_enabled: z.boolean().nullish(), - }) - .nullish(), - enable_registration_captcha: z.boolean().nullish(), - enable_registration: z.boolean().nullish(), - user_consent: z.object({}).nullish(), - enable_3pid_changes: z.boolean().nullish(), - login_via_existing_session: z - .object({ - enabled: z.boolean().nullish(), - }) - .nullish(), -}); - -export type SynapseConfig = z.infer; diff --git a/tools/syn2mas/src/types/MCompatAccessToken.d.ts b/tools/syn2mas/src/types/MCompatAccessToken.d.ts deleted file mode 100644 index 810c63541..000000000 --- a/tools/syn2mas/src/types/MCompatAccessToken.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { MCompatSession } from "./MCompatSession"; - -import type { UUID } from "./index"; - -/* -+------------------------+--------------------------+-----------+ -| Column | Type | Modifiers | -|------------------------+--------------------------+-----------| -| compat_access_token_id | uuid | not null | -| compat_session_id | uuid | not null | -| access_token | text | not null | -| created_at | timestamp with time zone | not null | -| expires_at | timestamp with time zone | | -+------------------------+--------------------------+-----------+ -Indexes: - "compat_access_tokens_pkey" PRIMARY KEY, btree (compat_access_token_id) - "compat_access_tokens_access_token_unique" UNIQUE CONSTRAINT, btree (access_token) -Foreign-key constraints: - "compat_access_tokens_compat_session_id_fkey" FOREIGN KEY (compat_session_id) REFERENCES compat_sessions(compat_session_id) -Referenced by: - TABLE "compat_refresh_tokens" CONSTRAINT "compat_refresh_tokens_compat_access_token_id_fkey" FOREIGN KEY (compat_access_token_id) REFERENCES compat_access_tokens(compat_access_toke -n_id) -*/ -export interface MCompatAccessToken { - compat_access_token_id: UUID; - compat_session_id: UUID; - access_token: string; - created_at: Date; - expires_at?: Date; -} diff --git a/tools/syn2mas/src/types/MCompatRefreshToken.d.ts b/tools/syn2mas/src/types/MCompatRefreshToken.d.ts deleted file mode 100644 index 454b811a7..000000000 --- a/tools/syn2mas/src/types/MCompatRefreshToken.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { MCompatAccessToken } from "./MCompatAccessToken"; -import type { MCompatSession } from "./MCompatSession"; - -import type { UUID } from "./index"; - -/* -+-------------------------+--------------------------+-----------+ -| Column | Type | Modifiers | -|-------------------------+--------------------------+-----------| -| compat_refresh_token_id | uuid | not null | -| compat_session_id | uuid | not null | -| compat_access_token_id | uuid | not null | -| refresh_token | text | not null | -| created_at | timestamp with time zone | not null | -| consumed_at | timestamp with time zone | | -+-------------------------+--------------------------+-----------+ -Indexes: - "compat_refresh_tokens_pkey" PRIMARY KEY, btree (compat_refresh_token_id) - "compat_refresh_tokens_refresh_token_unique" UNIQUE CONSTRAINT, btree (refresh_token) -Foreign-key constraints: - "compat_refresh_tokens_compat_access_token_id_fkey" FOREIGN KEY (compat_access_token_id) REFERENCES compat_access_tokens(compat_access_token_id) - "compat_refresh_tokens_compat_session_id_fkey" FOREIGN KEY (compat_session_id) REFERENCES compat_sessions(compat_session_id) -*/ -export interface MCompatRefreshToken { - compat_refresh_token_id: UUID; - compat_session_id: UUID; - compat_access_token_id: UUID; - refresh_token: string; - created_at: Date; - consumed_at?: Date; -} diff --git a/tools/syn2mas/src/types/MCompatSession.d.ts b/tools/syn2mas/src/types/MCompatSession.d.ts deleted file mode 100644 index 70c84e561..000000000 --- a/tools/syn2mas/src/types/MCompatSession.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { MUser } from "./MUser"; - -import type { UUID } from "./index"; - -/* -+-------------------+--------------------------+-----------+ -| Column | Type | Modifiers | -|-------------------+--------------------------+-----------| -| compat_session_id | uuid | not null | -| user_id | uuid | not null | -| device_id | text | not null | -| created_at | timestamp with time zone | not null | -| finished_at | timestamp with time zone | | -| is_synapse_admin | boolean | not null | -+-------------------+--------------------------+-----------+ -Indexes: - "compat_sessions_pkey" PRIMARY KEY, btree (compat_session_id) - "compat_sessions_device_id_unique" UNIQUE CONSTRAINT, btree (device_id) -Foreign-key constraints: - "compat_sessions_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(user_id) -Referenced by: - TABLE "compat_sso_logins" CONSTRAINT "compat_sso_logins_compat_session_id_fkey" FOREIGN KEY (compat_session_id) REFERENCES compat_sessions(compat_session_id) ON DELETE SET NULL - TABLE "compat_access_tokens" CONSTRAINT "compat_access_tokens_compat_session_id_fkey" FOREIGN KEY (compat_session_id) REFERENCES compat_sessions(compat_session_id) - TABLE "compat_refresh_tokens" CONSTRAINT "compat_refresh_tokens_compat_session_id_fkey" FOREIGN KEY (compat_session_id) REFERENCES compat_sessions(compat_session_id) -*/ - -export interface MCompatSession { - compat_session_id: UUID; - user_id: UUID; - device_id: string; - created_at: Date; - finished_at?: Date; - is_synapse_admin: boolean; -} diff --git a/tools/syn2mas/src/types/MUpstreamOauthLink.d.ts b/tools/syn2mas/src/types/MUpstreamOauthLink.d.ts deleted file mode 100644 index c44626a99..000000000 --- a/tools/syn2mas/src/types/MUpstreamOauthLink.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { MUpstreamOauthProvider } from "./MUpstreamOauthProvider"; -import type { MUser } from "./MUser"; - -import type { UUID } from "./index"; - -/* -+----------------------------+--------------------------+-----------+ -| Column | Type | Modifiers | -|----------------------------+--------------------------+-----------| -| upstream_oauth_link_id | uuid | not null | -| upstream_oauth_provider_id | uuid | not null | -| user_id | uuid | | -| subject | text | not null | -| created_at | timestamp with time zone | not null | -+----------------------------+--------------------------+-----------+ -Indexes: - "upstream_oauth_links_pkey" PRIMARY KEY, btree (upstream_oauth_link_id) - "upstream_oauth_links_subject_unique" UNIQUE CONSTRAINT, btree (upstream_oauth_provider_id, subject) -Foreign-key constraints: - "upstream_oauth_link_user_fkey" FOREIGN KEY (user_id) REFERENCES users(user_id) - "upstream_oauth_links_provider_fkey" FOREIGN KEY (upstream_oauth_provider_id) REFERENCES upstream_oauth_providers(upstream_oauth_provider_id) -Referenced by: - TABLE "upstream_oauth_authorization_sessions" CONSTRAINT "upstream_oauth_authorization_sessions_link_fkey" FOREIGN KEY (upstream_oauth_link_id) REFERENCES upstream_oauth_links(upstream_oauth_link_id) -*/ -export interface MUpstreamOauthLink { - upstream_oauth_link_id: UUID; - upstream_oauth_provider_id: UUID; - user_id?: UUID; - subject: string; - created_at: Date; -} diff --git a/tools/syn2mas/src/types/MUpstreamOauthProvider.d.ts b/tools/syn2mas/src/types/MUpstreamOauthProvider.d.ts deleted file mode 100644 index 28ae0b71f..000000000 --- a/tools/syn2mas/src/types/MUpstreamOauthProvider.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { UUID } from "./index"; - -/* -+----------------------------+--------------------------+-----------+ -| Column | Type | Modifiers | -|----------------------------+--------------------------+-----------| -| upstream_oauth_provider_id | uuid | not null | -| issuer | text | not null | -| scope | text | not null | -| client_id | text | not null | -| encrypted_client_secret | text | | -| token_endpoint_signing_alg | text | | -| token_endpoint_auth_method | text | not null | -| created_at | timestamp with time zone | not null | -+----------------------------+--------------------------+-----------+ -Indexes: - "upstream_oauth_providers_pkey" PRIMARY KEY, btree (upstream_oauth_provider_id) -Referenced by: - TABLE "upstream_oauth_links" CONSTRAINT "upstream_oauth_links_provider_fkey" FOREIGN KEY (upstream_oauth_provider_id) REFERENCES upstream_oauth_providers(upstream_oauth_provider_id) - TABLE "upstream_oauth_authorization_sessions" CONSTRAINT "upstream_oauth_authorization_sessions_provider_fkey" FOREIGN KEY (upstream_oauth_provider_id) REFERENCES upstream_oauth_providers(upstream_oauth_provider_id) -*/ - -export interface MUpstreamOauthProvider { - upstream_oauth_provider_id: UUID; - issuer: string; - scope: string; - client_id: string; - encrypted_client_secret?: string; - token_endpoint_signing_alg?: string; - token_endpoint_auth_method: string; - created_at: Date; -} diff --git a/tools/syn2mas/src/types/MUser.d.ts b/tools/syn2mas/src/types/MUser.d.ts deleted file mode 100644 index 35824f288..000000000 --- a/tools/syn2mas/src/types/MUser.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { MUserEmail } from "./MUserEmail"; - -import type { UUID } from "./index"; - -export interface MUser { - user_id: UUID; - username: string; // localpart only without @ - created_at: Date; - locked_at: Date | null; - can_request_admin: boolean; -} diff --git a/tools/syn2mas/src/types/MUserEmail.d.ts b/tools/syn2mas/src/types/MUserEmail.d.ts deleted file mode 100644 index 3b563381c..000000000 --- a/tools/syn2mas/src/types/MUserEmail.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { MUser } from "./MUser"; - -import type { UUID } from "./index"; - -/* -+---------------+--------------------------+-----------+ -| Column | Type | Modifiers | -|---------------+--------------------------+-----------| -| user_email_id | uuid | not null | -| user_id | uuid | not null | -| email | text | not null | -| created_at | timestamp with time zone | not null | -| confirmed_at | timestamp with time zone | | -+---------------+--------------------------+-----------+ -*/ - -export interface MUserEmail { - user_email_id: UUID; - user_id: UUID; - email: string; - created_at: Date; - confirmed_at?: Date; -} diff --git a/tools/syn2mas/src/types/MUserPassword.d.ts b/tools/syn2mas/src/types/MUserPassword.d.ts deleted file mode 100644 index f855c1266..000000000 --- a/tools/syn2mas/src/types/MUserPassword.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { MUser } from "./MUser"; - -import type { UUID } from "./index"; - -export interface MUserPassword { - user_password_id: UUID; - user_id: UUID; - hashed_password: string; - created_at: Date; - version: number; - upgraded_from_id?: UUID; -} diff --git a/tools/syn2mas/src/types/SAccessToken.d.ts b/tools/syn2mas/src/types/SAccessToken.d.ts deleted file mode 100644 index d25fe1180..000000000 --- a/tools/syn2mas/src/types/SAccessToken.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { SRefreshToken } from "./SRefreshToken"; - -import type { Id, SynapseUserId } from "./index"; - -/* -CREATE TABLE access_tokens ( - id bigint NOT NULL, - user_id text NOT NULL, - device_id text, - token text NOT NULL, - valid_until_ms bigint, - puppets_user_id text, - last_validated bigint, - refresh_token_id bigint, - used boolean -); -*/ -export interface SAccessToken { - id: Id; - user_id: SynapseUserId; - device_id: string; - token: string; - valid_until_ms?: number; - puppets_user_id?: SynapseUserId; - last_validated?: number; - refresh_token_id?: Id; - used: boolean; -} diff --git a/tools/syn2mas/src/types/SRefreshToken.d.ts b/tools/syn2mas/src/types/SRefreshToken.d.ts deleted file mode 100644 index 53fb0cfb9..000000000 --- a/tools/syn2mas/src/types/SRefreshToken.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { Id, SynapseUserId } from "./index"; - -/* -); -CREATE TABLE refresh_tokens ( - id bigint NOT NULL, - user_id text NOT NULL, - device_id text NOT NULL, - token text NOT NULL, - next_token_id bigint, - expiry_ts bigint, - ultimate_session_expiry_ts bigint -); -*/ - -export interface SRefreshToken { - id: Id; - user_id: SynapseUserId; - device_id: string; - token: string; - next_token_id?: number; // refresh or access? - expiry_ts?: number; - ultimate_session_expiry_ts?: number; -} diff --git a/tools/syn2mas/src/types/SUser.d.ts b/tools/syn2mas/src/types/SUser.d.ts deleted file mode 100644 index f032c8a4e..000000000 --- a/tools/syn2mas/src/types/SUser.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { SynapseUserId, UnixTimestamp } from "./index"; - -export interface SUser { - name: SynapseUserId; // '@test2:localhost:8008' - password_hash?: string; - admin: number; - is_guest: number; - deactivated: number; - creation_ts: UnixTimestamp; - appservice_id?: string; -} diff --git a/tools/syn2mas/src/types/SUserExternalId.d.ts b/tools/syn2mas/src/types/SUserExternalId.d.ts deleted file mode 100644 index 305917085..000000000 --- a/tools/syn2mas/src/types/SUserExternalId.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { SynapseUserId } from "./index"; - -export interface SUserExternalId { - auth_provider: string; - external_id: string; - user_id: SynapseUserId; -} diff --git a/tools/syn2mas/src/types/SUserThreePid.d.ts b/tools/syn2mas/src/types/SUserThreePid.d.ts deleted file mode 100644 index 134794b23..000000000 --- a/tools/syn2mas/src/types/SUserThreePid.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import type { SynapseUserId } from "./index"; - -/* -CREATE TABLE user_threepids ( - user_id text NOT NULL, - medium text NOT NULL, - address text NOT NULL, - validated_at bigint NOT NULL, - added_at bigint NOT NULL -); -*/ -export interface SUserThreePid { - user_id: SynapseUserId; - medium: string; - address: string; - validated_at: number; - added_at: number; -} diff --git a/tools/syn2mas/src/types/index.d.ts b/tools/syn2mas/src/types/index.d.ts deleted file mode 100644 index b308abe6a..000000000 --- a/tools/syn2mas/src/types/index.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -export type UnixTimestamp = number; -export type SynapseUserId = string; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export type Id<_T> = number; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export type UUID<_T> = string; diff --git a/tools/syn2mas/src/types/knex.d.ts b/tools/syn2mas/src/types/knex.d.ts deleted file mode 100644 index cb03e4882..000000000 --- a/tools/syn2mas/src/types/knex.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -import "knex/types/result"; - -declare module "knex/types/result" { - interface Registry { - Count: number; - } -} diff --git a/tools/syn2mas/tsconfig.eslint.json b/tools/syn2mas/tsconfig.eslint.json deleted file mode 100644 index 276e0ea86..000000000 --- a/tools/syn2mas/tsconfig.eslint.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": [ - "@tsconfig/strictest/tsconfig.json", - "@tsconfig/node22/tsconfig.json" - ], - "compilerOptions": { - "noEmit": true, - "allowJs": true - }, - "include": [".eslintrc.cjs", "src/**/*.mts", "src/**/*.ts"] -} diff --git a/tools/syn2mas/tsconfig.json b/tools/syn2mas/tsconfig.json deleted file mode 100644 index 7c6468dd9..000000000 --- a/tools/syn2mas/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": [ - "@tsconfig/strictest/tsconfig.json", - "@tsconfig/node22/tsconfig.json" - ], - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "sourceMap": true, - "declaration": false - } -} From afc625b192616e82d56a81e34a1d8bbdc21c0ed7 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 23 Apr 2025 17:44:30 +0200 Subject: [PATCH 074/189] Remove CI for the old syn2mas tool --- .dockerignore | 1 - .github/dependabot.yml | 17 -------- .github/scripts/commit-and-tag.cjs | 7 +--- .github/workflows/build.yaml | 63 ------------------------------ .github/workflows/ci.yaml | 29 -------------- .github/workflows/tag.yaml | 4 -- biome.json | 1 - docker-bake.hcl | 8 +--- 8 files changed, 2 insertions(+), 128 deletions(-) diff --git a/.dockerignore b/.dockerignore index bd1816b8b..e016faae8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,7 +3,6 @@ crates/*/target crates/*/node_modules frontend/node_modules frontend/dist -tools/syn2mas/** docs/ .devcontainer/ .github/ diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f2e112e4f..a98d07e09 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -106,20 +106,3 @@ updates: browser-logos: patterns: - "@browser-logos/*" - - - package-ecosystem: "npm" - directory: "/tools/syn2mas/" - labels: - - "A-Dependencies" - - "Z-Deps-Syn2Mas" - schedule: - interval: "weekly" - ignore: - # Ignore @types/node until we can upgrade to Node 20 - - dependency-name: "@types/node" - update-types: ["version-update:semver-major"] - groups: - production: - dependency-type: "production" - development: - dependency-type: "development" diff --git a/.github/scripts/commit-and-tag.cjs b/.github/scripts/commit-and-tag.cjs index 5a238b9ae..b95782541 100644 --- a/.github/scripts/commit-and-tag.cjs +++ b/.github/scripts/commit-and-tag.cjs @@ -13,12 +13,7 @@ module.exports = async ({ github, context }) => { const parent = context.sha; if (!version) throw new Error("VERSION is not defined"); - const files = [ - "Cargo.toml", - "Cargo.lock", - "tools/syn2mas/package.json", - "tools/syn2mas/package-lock.json", - ]; + const files = ["Cargo.toml", "Cargo.lock"]; /** @type {{path: string, mode: "100644", type: "blob", sha: string}[]} */ const tree = []; diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 17adc0012..979c8c9f0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -23,7 +23,6 @@ env: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" IMAGE: ghcr.io/element-hq/matrix-authentication-service - IMAGE_SYN2MAS: ghcr.io/element-hq/matrix-authentication-service/syn2mas BUILDCACHE: ghcr.io/element-hq/matrix-authentication-service/buildcache DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index @@ -253,22 +252,6 @@ jobs: type=semver,pattern={{major}} type=sha - - name: Docker meta (syn2mas) - id: meta-syn2mas - uses: docker/metadata-action@v5.7.0 - with: - images: "${{ env.IMAGE_SYN2MAS }}" - bake-target: docker-metadata-action-syn2mas - flavor: | - latest=auto - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=sha - - name: Setup Cosign uses: sigstore/cosign-installer@v3.8.1 @@ -294,7 +277,6 @@ jobs: ./docker-bake.hcl cwd://${{ steps.meta.outputs.bake-file }} cwd://${{ steps.meta-debug.outputs.bake-file }} - cwd://${{ steps.meta-syn2mas.outputs.bake-file }} set: | base.output=type=image,push=true base.cache-from=type=registry,ref=${{ env.BUILDCACHE }}:buildcache @@ -318,43 +300,11 @@ jobs: env: REGULAR_DIGEST: ${{ steps.output.outputs.metadata && fromJSON(steps.output.outputs.metadata).regular.digest }} DEBUG_DIGEST: ${{ steps.output.outputs.metadata && fromJSON(steps.output.outputs.metadata).debug.digest }} - SYN2MAS_DIGEST: ${{ steps.output.outputs.metadata && fromJSON(steps.output.outputs.metadata).syn2mas.digest }} run: |- cosign sign --yes \ "$IMAGE@$REGULAR_DIGEST" \ "$IMAGE@$DEBUG_DIGEST" \ - "$IMAGE_SYN2MAS@$SYN2MAS_DIGEST" - - syn2mas: - name: Release syn2mas on NPM - runs-on: ubuntu-24.04 - if: github.event_name != 'pull_request' - - permissions: - contents: read - id-token: write - - steps: - - name: Checkout the code - uses: actions/checkout@v4.2.2 - - - name: Install Node - uses: actions/setup-node@v4.4.0 - with: - node-version-file: ./tools/syn2mas/.nvmrc - - - name: Install Node dependencies - working-directory: ./tools/syn2mas - run: npm ci - - - name: Publish - uses: JS-DevTools/npm-publish@v3 - with: - package: ./tools/syn2mas - token: ${{ secrets.NPM_TOKEN }} - provenance: true - dry-run: ${{ !startsWith(github.ref, 'refs/tags/') }} release: name: Release @@ -363,7 +313,6 @@ jobs: needs: - assemble-archives - build-image - - syn2mas steps: - name: Download the artifacts from the previous job uses: actions/download-artifact@v4 @@ -403,18 +352,6 @@ jobs: ') }} ``` - `syn2mas` migration tool: - - - Digest: - ``` - ${{ env.IMAGE_SYN2MAS }}@${{ fromJSON(needs.build-image.outputs.metadata).syn2mas.digest }} - ``` - - Tags: - ``` - ${{ join(fromJSON(needs.build-image.outputs.metadata).syn2mas.tags, ' - ') }} - ``` - files: | artifacts/mas-cli-aarch64-linux.tar.gz artifacts/mas-cli-x86_64-linux.tar.gz diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 397de666b..b3f438f5f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -311,34 +311,6 @@ jobs: --archive-file nextest-archive.tar.zst \ --partition count:${{ matrix.partition }}/3 - syn2mas: - name: Check syn2mas - runs-on: ubuntu-24.04 - - permissions: - contents: read - - steps: - - name: Checkout the code - uses: actions/checkout@v4.2.2 - - - name: Install Node - uses: actions/setup-node@v4.4.0 - with: - node-version-file: ./tools/syn2mas/.nvmrc - - - name: Install Node dependencies - working-directory: ./tools/syn2mas - run: npm ci - - - name: Lint - working-directory: ./tools/syn2mas - run: npm run lint - - - name: Build - working-directory: ./tools/syn2mas - run: npm run build - tests-done: name: Tests done if: ${{ always() }} @@ -352,7 +324,6 @@ jobs: - clippy - check-schema - test - - syn2mas runs-on: ubuntu-24.04 steps: diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag.yaml index b86579c1d..02555f5e1 100644 --- a/.github/workflows/tag.yaml +++ b/.github/workflows/tag.yaml @@ -40,10 +40,6 @@ jobs: - name: Run `cargo metadata` to make sure the lockfile is up to date run: cargo metadata --format-version 1 - - name: Set the tools/syn2mas version - working-directory: tools/syn2mas - run: npm version "${{ inputs.version }}" --no-git-tag-version - - name: Commit and tag using the GitHub API uses: actions/github-script@v7.0.1 id: commit diff --git a/biome.json b/biome.json index af0783da6..8800caa30 100644 --- a/biome.json +++ b/biome.json @@ -21,7 +21,6 @@ "frontend/.storybook/locales.ts", "frontend/.storybook/public/mockServiceWorker.js", "frontend/locales/*.json", - "tools/syn2mas/package.json", "**/coverage/**", "**/dist/**" ] diff --git a/docker-bake.hcl b/docker-bake.hcl index 0df1c848f..cfa6b8353 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -4,12 +4,11 @@ variable "VERGEN_GIT_DESCRIBE" {} // This is what is baked by GitHub Actions -group "default" { targets = ["regular", "debug", "syn2mas"] } +group "default" { targets = ["regular", "debug"] } // Targets filled by GitHub Actions: one for the regular tag, one for the debug tag target "docker-metadata-action" {} target "docker-metadata-action-debug" {} -target "docker-metadata-action-syn2mas" {} // This sets the platforms and is further extended by GitHub Actions to set the // output and the cache locations @@ -37,8 +36,3 @@ target "debug" { inherits = ["base", "docker-metadata-action-debug"] target = "debug" } - -target "syn2mas" { - inherits = ["base", "docker-metadata-action-syn2mas"] - context = "./tools/syn2mas" -} From f47df35bde728aa4c72e7d014feb3088458d4548 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Thu, 13 Mar 2025 17:35:07 +0000 Subject: [PATCH 075/189] syn2mas: document new tool --- docs/setup/migration.md | 101 +++++++++++++++++++++++++++++----------- 1 file changed, 74 insertions(+), 27 deletions(-) diff --git a/docs/setup/migration.md b/docs/setup/migration.md index 2d3b7a0c0..a24cf3efa 100644 --- a/docs/setup/migration.md +++ b/docs/setup/migration.md @@ -18,28 +18,29 @@ There will be tools to help with the migration process itself. But these aren't The deployment is non-trivial so it is important to read through and understand the steps involved and make a plan before starting. -### Get `syn2mas` +### Is your setup ready to be migrated? -The easiest way to get `syn2mas` is through [`npm`](https://www.npmjs.com/package/@vector-im/syn2mas): +#### SAML2 and LDAP Single Sign-On Providers are not supported -```sh -npm install -g @vector-im/syn2mas -``` +A deployment which requires SAML or LDAP-based authentication should use a service like [Dex](https://github.com/dexidp/dex) to bridge between the SAML provider and the authentication service. +MAS is different from Synapse in that it does **not** have built-in support for SAML or LDAP-based providers. -### Run the migration advisor +#### Custom password providers are not supported -You can use the advisor mode of the `syn2mas` tool to identify extra configuration steps or issues with the configuration of the homeserver. +If your Synapse homeserver currently uses a custom password provider module, please note that MAS does not support these. -```sh -syn2mas --command=advisor --synapseConfigFile=homeserver.yaml -``` +#### SQLite databases are not supported -This will output `WARN` entries for any identified actions and `ERROR` entries in the case of any issues that will prevent the migration from working. +It is worth noting that MAS currently only supports PostgreSQL as a database backend. ### Install and configure MAS alongside your existing homeserver Follow the instructions in the [installation guide](installation.md) to install MAS alongside your existing homeserver. +You'll need a blank PostgreSQL database for MAS to use; it does not share the database with the homeserver. + +Set up a configuration file but don't start MAS, or create any users, yet. + #### Local passwords Synapse uses bcrypt as its password hashing scheme while MAS defaults to using the newer argon2id. @@ -52,7 +53,7 @@ Example passwords configuration: passwords: enabled: true schemes: - - version: 1 + - version: 1 # TODO I think v:2 has to come first in this list algorithm: bcrypt # Optional, must match the `password_config.pepper` in the Synapse config #secret: secretPepperValue @@ -60,57 +61,103 @@ passwords: algorithm: argon2id ``` +If you have a pepper configured in your Synapse password configuration, you'll need to match that on version 1 of the equivalent MAS configuration. + +The migration checker will inform you if this has not been configured properly. + ### Map any upstream SSO providers -If you are using an upstream SSO provider then you will need to provision the upstream provide in MAS manually. +If you are using an upstream SSO provider, then you will need to configure the upstream provider in MAS manually. -Each upstream provider will need to be given as an `--upstreamProviderMapping` command line option to the import tool. +MAS does not support SAML or LDAP upstream providers. +If you are using one of these, you will need to use an adapter such as Dex at this time, +but we have not yet documented this procedure. -### Prepare the MAS database +Each upstream provider that was used by at least one user in Synapse will need to be configured in MAS. -Once the database is created, it still needs to have its schema created and synced with the configuration. -This can be done with the following command: +Set the `synapse_idp_id` attribute on the provider to: + +- `"oidc"` if you used an OIDC provider in Synapse's legacy `oidc_config` configuration section. +- `"oidc-myprovider"` if you used an OIDC provider in Synapse's `oidc_providers` configuration list, + with a `provider` of `"myprovider"`. + (This is because Synapse prefixes the provider ID with `oidc-` internally.) + +Without the `synapse_idp_id`s being set, syn2mas does not understand which providers +in Synapse correspond to which provider in MAS. + +!!!!!!!!! TODO add an example here + +The migration checker will inform you if a provider is missing from MAS' config. + +### Run the migration checker + +You can use the `check` command of the `syn2mas` tool to identify configuration problems before starting the migration. +You do not need to stop Synapse to run this command. ```sh -mas-cli config sync +mas-cli --config mas_config.yaml syn2mas --synapse-config homeserver.yaml check ``` +This will either output a list of errors and warnings, or tell you that the check completed with no errors or warnings. + +If you have any errors, you must resolve these before starting the migration. + +If you have any warnings, please read, understand and possibly resolve them. +With that said, resolving them is not strictly required before starting the migration. + ### Do a dry-run of the import to test -```sh -syn2mas --command migrate --synapseConfigFile homeserver.yaml --masConfigFile config.yaml --dryRun -``` - -If no errors are reported then you can proceed to the next step. +!!!!!!! TODO we don't have an exact dry-run mode exposed at the moment... ## Doing the migration Having done the preparation, you can now proceed with the actual migration. Note that this will require downtime for the homeserver and is not easily reversible. -### Backup your data +### Backup your data and configuration As with any migration, it is important to backup your data before proceeding. +We also suggest making a backup copy of your homeserver's known good configuration, +before making any changes to enable MAS integration. + ### Shutdown the homeserver This is to ensure that no new sessions are created whilst the migration is in progress. -### Configure the homeserver +### Configure the homeserver to enable MAS integration Follow the instructions in the [homeserver configuration guide](homeserver.md) to configure the homeserver to use MAS. ### Do the import -Run `syn2mas` in non-dry-run mode. +Once the homeserver has been stopped, MAS has been configured (but is not running!) +and you have a successful migration check, +run `syn2mas`'s `migrate` command. + +Other than the change of command word, the syntax is exactly the same as the `check` command. ```sh -syn2mas --command migrate --synapseConfigFile homeserver.yaml --masConfigFile config.yaml --dryRun false +mas-cli --config mas_config.yaml syn2mas --synapse-config homeserver.yaml migrate ``` +#### What to do if it goes wrong + +If the migration fails with an error: + +- You can either try to fix the error and make another attempt by re-running the command; or +- you can revert your homeserver configuration (so MAS integration is disabled once more) + and abort the migration for now. In this case, you should not start MAS up. + +Please report migration failures to the developers. + ### Start up the homeserver Start up the homeserver again with the new configuration. +### Start up MAS + +Start up MAS. + ### Update or serve the .well-known The `.well-known/matrix/client` needs to be served as described [here](./well-known.md). From f94a808e7a08c18ce56b5a7762ffb149a34e36cb Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Wed, 23 Apr 2025 16:54:10 +0100 Subject: [PATCH 076/189] Fix cargo doc choking on invalid [DEPRECATED] 'link' Could have escaped with a backslash but I thought just using round brackets would be more readable than that. --- crates/cli/src/commands/manage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index 4cf59a483..92cc30884 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -67,7 +67,7 @@ enum Subcommand { /// Add an email address to the specified user AddEmail { username: String, email: String }, - /// [DEPRECATED] Mark email address as verified + /// (DEPRECATED) Mark email address as verified VerifyEmail { username: String, email: String }, /// Set a user password From b3e2cadf71aab8adffafa7eb858f9f9c2246d8ea Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 23 Apr 2025 18:45:12 +0200 Subject: [PATCH 077/189] Document the new migration tool --- docs/SUMMARY.md | 1 + docs/reference/cli/config.md | 8 ++- docs/reference/cli/syn2mas.md | 29 ++++++++ docs/setup/migration.md | 132 +++++++++++++++++++++++----------- 4 files changed, 125 insertions(+), 45 deletions(-) create mode 100644 docs/reference/cli/syn2mas.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 782d71bfd..de087925c 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -33,6 +33,7 @@ - [`database`](./reference/cli/database.md) - [`manage`](./reference/cli/manage.md) - [`server`](./reference/cli/server.md) + - [`syn2mas`](./reference/cli/syn2mas.md) - [`worker`](./reference/cli/worker.md) - [`templates`](./reference/cli/templates.md) - [`doctor`](./reference/cli/doctor.md) diff --git a/docs/reference/cli/config.md b/docs/reference/cli/config.md index 551c61d76..c624838c1 100644 --- a/docs/reference/cli/config.md +++ b/docs/reference/cli/config.md @@ -26,7 +26,7 @@ clients: # ... ``` -## `config generate` +## `config generate [--synapse-config ] [--output ]` Generate a sample configuration file. It generates random signing keys (`.secrets.keys`) and the cookie encryption secret (`.secrets.encryption`). @@ -38,6 +38,10 @@ INFO generate:rsa: mas_config::oauth2: Done generating RSA key INFO generate:ecdsa: mas_config::oauth2: Done generating ECDSA key ``` +The `--synapse-config` option can be used to migrate over configuration options from an existing Synapse configuration. + +The `--output` option can be used to specify the output file. If not specified, the output will be written to stdout. + ## `config sync [--prune] [--dry-run]` Synchronize the configuration with the database. @@ -52,4 +56,4 @@ INFO cli.config.sync: Updating provider provider.id=01H3FDH2XZJS8ADKRGWM84PZTY INFO cli.config.sync: Adding provider provider.id=01H3FDH2XZJS8ADKRGWM84PZTF INFO cli.config.sync: Deleting client client.id=01GFWRB9MYE0QYK60NZP2YF905 INFO cli.config.sync: Updating client client.id=01GFWRB9MYE0QYK60NZP2YF904 -``` \ No newline at end of file +``` diff --git a/docs/reference/cli/syn2mas.md b/docs/reference/cli/syn2mas.md new file mode 100644 index 000000000..0089cc704 --- /dev/null +++ b/docs/reference/cli/syn2mas.md @@ -0,0 +1,29 @@ +# `syn2mas` + +Tool to import data from an existing Synapse homeserver into MAS. + +Global options: +- `--config `: Path to the MAS configuration file. +- `--help`: Print help. +- `--synapse-config `: Path to the Synapse configuration file. +- `--synapse-database-uri `: Override the Synapse database URI. + +## `syn2mas check` + +Check the setup for potential problems before running a migration + +```console +$ mas-cli syn2mas check --config mas_config.yaml --synapse-config homeserver.yaml +``` + +## `syn2mas migrate [--dry-run]` + +Migrate data from the homeserver to MAS. + +The `--dry-run` option will perform a dry-run of the migration, which is safe to run without stopping Synapse. +It will perform a full data migration, but then empty the MAS database at the end to roll back. + + +```console +$ mas-cli syn2mas migrate --config mas_config.yaml --synapse-config homeserver.yaml +``` diff --git a/docs/setup/migration.md b/docs/setup/migration.md index a24cf3efa..b5f93d534 100644 --- a/docs/setup/migration.md +++ b/docs/setup/migration.md @@ -1,29 +1,25 @@ # Migrating an existing homeserver -One of the design goals of MAS has been to allow it to be used to migrate an existing homeserver to an OIDC-based architecture. +One of the design goals of MAS has been to allow it to be used to migrate an existing homeserver, specifically without requiring users to re-authenticate and ensuring that all existing clients continue to work. -Specifically without requiring users to re-authenticate and that non-OIDC clients continue to work. - -Features that are provided to support this include: +Features that support this include: - Ability to import existing password hashes from Synapse - Ability to import existing sessions and devices -- Ability to import existing access tokens linked to devices (ie not including short-lived admin puppeted access tokens) +- Ability to import existing access tokens - Ability to import existing upstream IdP subject ID mappings - Provides a compatibility layer for legacy Matrix authentication -There will be tools to help with the migration process itself. But these aren't quite ready yet. - ## Preparing for the migration -The deployment is non-trivial so it is important to read through and understand the steps involved and make a plan before starting. +The deployment is non-trivial, so it is important to read through and understand the steps involved and make a plan before starting. ### Is your setup ready to be migrated? #### SAML2 and LDAP Single Sign-On Providers are not supported -A deployment which requires SAML or LDAP-based authentication should use a service like [Dex](https://github.com/dexidp/dex) to bridge between the SAML provider and the authentication service. -MAS is different from Synapse in that it does **not** have built-in support for SAML or LDAP-based providers. +A deployment that requires SAML or LDAP-based authentication should use a service like [Dex](https://github.com/dexidp/dex) to bridge between the SAML provider and the authentication service. +MAS differs from Synapse in that it does **not** have built-in support for SAML or LDAP-based providers. #### Custom password providers are not supported @@ -32,6 +28,7 @@ If your Synapse homeserver currently uses a custom password provider module, ple #### SQLite databases are not supported It is worth noting that MAS currently only supports PostgreSQL as a database backend. +The migration tool only supports reading from PostgreSQL for the Synapse database as well. ### Install and configure MAS alongside your existing homeserver @@ -39,21 +36,26 @@ Follow the instructions in the [installation guide](installation.md) to install You'll need a blank PostgreSQL database for MAS to use; it does not share the database with the homeserver. -Set up a configuration file but don't start MAS, or create any users, yet. +MAS provides a tool to generate a configuration file based on your existing Synapse configuration. This is useful for kickstarting your new configuration. + +```sh +mas-cli config generate --synapse-config homeserver.yaml --output mas_config.yaml +``` + +When using this tool, be careful to examine the log output for any warnings about unsupported configuration options. #### Local passwords -Synapse uses bcrypt as its password hashing scheme while MAS defaults to using the newer argon2id. +Synapse uses bcrypt as its password hashing scheme, while MAS defaults to using the newer argon2id. You will have to configure the version 1 scheme as bcrypt for migrated passwords to work. -It is also recommended that you keep argon2id as version 2 so that once users log in, their hashes will be updated to the newer recommended scheme. -If you have a `pepper` set in the `password_config` section of your Synapse config, then you need to specify this `pepper` as the `secret` field for your `bcrypt` scheme. +It is also recommended that you keep argon2id as version 2 so that once users log in, their hashes will be updated to the newer, recommended scheme. Example passwords configuration: ```yml passwords: enabled: true schemes: - - version: 1 # TODO I think v:2 has to come first in this list + - version: 1 algorithm: bcrypt # Optional, must match the `password_config.pepper` in the Synapse config #secret: secretPepperValue @@ -70,22 +72,59 @@ The migration checker will inform you if this has not been configured properly. If you are using an upstream SSO provider, then you will need to configure the upstream provider in MAS manually. MAS does not support SAML or LDAP upstream providers. -If you are using one of these, you will need to use an adapter such as Dex at this time, -but we have not yet documented this procedure. +If you are using one of these, you will need to use an adapter such as Dex at this time, but we have not yet documented this procedure. Each upstream provider that was used by at least one user in Synapse will need to be configured in MAS. Set the `synapse_idp_id` attribute on the provider to: - `"oidc"` if you used an OIDC provider in Synapse's legacy `oidc_config` configuration section. -- `"oidc-myprovider"` if you used an OIDC provider in Synapse's `oidc_providers` configuration list, - with a `provider` of `"myprovider"`. +- `"oidc-myprovider"` if you used an OIDC provider in Synapse's `oidc_providers` configuration list, with a `provider` of `"myprovider"`. (This is because Synapse prefixes the provider ID with `oidc-` internally.) -Without the `synapse_idp_id`s being set, syn2mas does not understand which providers -in Synapse correspond to which provider in MAS. +Without the `synapse_idp_id`s being set, `mas-cli syn2mas` does not understand which providers in Synapse correspond to which provider in MAS. -!!!!!!!!! TODO add an example here +For example, if your Synapse configuration looked like this: + +```yaml +oidc_providers: + - idp_id: dex + idp_name: "My Dex server" + issuer: "https://example.com/dex" + client_id: "synapse" + client_secret: "supersecret" + scopes: ["openid", "profile", "email"] + user_mapping_provider: + config: + localpart_template: "{{ user.email.split('@')[0].lower() }}" + email_template: "{{ user.email }}" + display_name_template: "{{ user.name|capitalize }}" +``` + +Then the equivalent configuration in MAS would look like this: + +```yaml +upstream_oauth2: + providers: + - id: 01JSHPZHAXC50QBKH67MH33TNF + synapse_idp_id: oidc-dex + issuer: "https://example.com/dex" + human_name: "My Dex server" + client_id: "synapse" + client_secret: "supersecret" + token_endpoint_auth_method: client_secret_basic + scope: "email openid profile" + claims_imports: + localpart: + action: require + template: "{{ user.email.split('@')[0].lower() }}" + displayname: + action: force + template: "{{ user.name|capitalize }}" + email: + action: force + template: "{{ user.email }}" +``` The migration checker will inform you if a provider is missing from MAS' config. @@ -95,34 +134,41 @@ You can use the `check` command of the `syn2mas` tool to identify configuration You do not need to stop Synapse to run this command. ```sh -mas-cli --config mas_config.yaml syn2mas --synapse-config homeserver.yaml check +mas-cli syn2mas check --config mas_config.yaml --synapse-config homeserver.yaml ``` -This will either output a list of errors and warnings, or tell you that the check completed with no errors or warnings. +This will output a list of errors and warnings, or tell you that the check completed with no errors or warnings. -If you have any errors, you must resolve these before starting the migration. +If you have any errors, you must resolve them before starting the migration. -If you have any warnings, please read, understand and possibly resolve them. -With that said, resolving them is not strictly required before starting the migration. +If you have any warnings, please read and understand them, and possibly resolve them. +Resolving warnings is not strictly required before starting the migration. ### Do a dry-run of the import to test -!!!!!!! TODO we don't have an exact dry-run mode exposed at the moment... +MAS can perform a dry-run of the import, which is safe to run without stopping Synapse. +It will perform a full data migration but then empty the MAS database at the end to roll back. + +This means it is safe to run multiple times without worrying about resetting the MAS database. +It also means the time this dry-run takes is representative of the time it will take to perform the actual migration. + +```sh +mas-cli syn2mas migrate --config mas_config.yaml --synapse-config homeserver.yaml --dry-run +``` ## Doing the migration -Having done the preparation, you can now proceed with the actual migration. Note that this will require downtime for the homeserver and is not easily reversible. +Having completed the preparation, you can now proceed with the actual migration. Note that this will require downtime for the homeserver and is not easily reversible. ### Backup your data and configuration -As with any migration, it is important to backup your data before proceeding. +As with any migration, it is important to back up your data before proceeding. -We also suggest making a backup copy of your homeserver's known good configuration, -before making any changes to enable MAS integration. +We also suggest making a backup copy of your homeserver's known good configuration before making any changes to enable MAS integration. -### Shutdown the homeserver +### Shut down the homeserver -This is to ensure that no new sessions are created whilst the migration is in progress. +This ensures that no new sessions are created while the migration is in progress. ### Configure the homeserver to enable MAS integration @@ -130,23 +176,23 @@ Follow the instructions in the [homeserver configuration guide](homeserver.md) t ### Do the import -Once the homeserver has been stopped, MAS has been configured (but is not running!) -and you have a successful migration check, -run `syn2mas`'s `migrate` command. - -Other than the change of command word, the syntax is exactly the same as the `check` command. +Once the homeserver has been stopped, MAS has been configured (but is not running!), and you have a successful migration check, run `syn2mas`'s `migrate` command. ```sh -mas-cli --config mas_config.yaml syn2mas --synapse-config homeserver.yaml migrate +mas-cli syn2mas migrate --config mas_config.yaml --synapse-config homeserver.yaml ``` #### What to do if it goes wrong If the migration fails with an error: -- You can either try to fix the error and make another attempt by re-running the command; or -- you can revert your homeserver configuration (so MAS integration is disabled once more) - and abort the migration for now. In this case, you should not start MAS up. +- You can try to fix the error and make another attempt by re-running the command; or +- You can revert your homeserver configuration (so MAS integration is disabled once more) and abort the migration for now. In this case, you should not start MAS up. + +In *some cases*, MAS may have written to its own database during a failed migration, causing it to complain in subsequent runs. +In this case, you can safely delete and recreate the MAS database, then start over. + +In *any case*, the migration tool itself **will not** write to the Synapse database, so as long as MAS hasn't been started, it is safe to roll back the migration without restoring the Synapse database. Please report migration failures to the developers. From f24d94c6f5ae9d282ca0c86da46c64b6b1cfd1ba Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 23 Apr 2025 18:52:34 +0200 Subject: [PATCH 078/189] docs: stop talking about the .well-known/matrix/client changes These were relevant on an old version of the specs, and just confuses people. --- docs/SUMMARY.md | 1 - docs/setup/README.md | 34 +--------------------------------- docs/setup/migration.md | 4 ---- docs/setup/well-known.md | 23 ----------------------- 4 files changed, 1 insertion(+), 61 deletions(-) delete mode 100644 docs/setup/well-known.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index de087925c..0b623e7b2 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -12,7 +12,6 @@ - [Database setup](./setup/database.md) - [Homeserver configuration](./setup/homeserver.md) - [Configuring a reverse proxy](./setup/reverse-proxy.md) -- [Configuring .well-known](./setup/well-known.md) - [Configure an upstream SSO provider](./setup/sso.md) - [Running the service](./setup/running.md) - [Migrating an existing homeserver](./setup/migration.md) diff --git a/docs/setup/README.md b/docs/setup/README.md index 6ff08d2bf..54b8ae8b4 100644 --- a/docs/setup/README.md +++ b/docs/setup/README.md @@ -11,43 +11,11 @@ The authentication service becomes the source of truth for user accounts and acc At time of writing, the authentication service is meant to be run on a standalone domain name (e.g. `auth.example.com`), and the homeserver on another (e.g. `matrix.example.com`). This domain will be user-facing as part of the authentication flow. -When a client initiates an authentication flow, it will discover the authentication service through the deployment `.well-known/matrix/client` endpoint. -This file will refer to an `issuer`, which is the canonical name of the authentication service instance. -Out of that issuer, it will discover the rest of the endpoints by calling the `[issuer]/.well-known/openid-configuration` endpoint. -By default, the `issuer` will match the root domain where the service is deployed (e.g. `https://auth.example.com/`), but it can be configured to be different. - An example setup could look like this: - The deployment domain is `example.com`, so Matrix IDs look like `@user:example.com` - - The issuer chosen is `https://auth.example.com/` - - The homeserver is deployed on `matrix.example.com` - The authentication service is deployed on `auth.example.com` - - Calling `https://example.com/.well-known/matrix/client` returns the following JSON: - - ```json - { - "m.homeserver": { - "base_url": "https://matrix.example.com" - }, - "org.matrix.msc2965.authentication": { - "issuer": "https://auth.example.com/", - "account": "https://auth.example.com/account" - } - } - ``` - - - Calling `https://auth.example.com/.well-known/openid-configuration` returns a JSON document similar to the following: - - ```json - { - "issuer": "https://auth.example.com/", - "authorization_endpoint": "https://auth.example.com/authorize", - "token_endpoint": "https://auth.example.com/oauth2/token", - "jwks_uri": "https://auth.example.com/oauth2/keys.json", - "registration_endpoint": "https://auth.example.com/oauth2/registration", - "//": "..." - } - ``` + - The homeserver is deployed on `matrix.example.com` With the installation planned, it is time to go through the installation and configuration process. The first section focuses on [installing the service](./installation.md). diff --git a/docs/setup/migration.md b/docs/setup/migration.md index b5f93d534..69e04311f 100644 --- a/docs/setup/migration.md +++ b/docs/setup/migration.md @@ -203,7 +203,3 @@ Start up the homeserver again with the new configuration. ### Start up MAS Start up MAS. - -### Update or serve the .well-known - -The `.well-known/matrix/client` needs to be served as described [here](./well-known.md). diff --git a/docs/setup/well-known.md b/docs/setup/well-known.md deleted file mode 100644 index 65b2990b4..000000000 --- a/docs/setup/well-known.md +++ /dev/null @@ -1,23 +0,0 @@ -# .well-known configuration - -A `.well-known/matrix/client` file is required to be served to allow clients to discover the authentication service. - -If no `.well-known/matrix/client` file is served currently then this will need to be enabled. - -If the homeserver is Synapse and serving this file already then the correct values will already be included when the homeserver is [configured to use MAS](./homeserver.md). - -If the .well-known is hosted elsewhere then `org.matrix.msc2965.authentication` entries need to be included similar to the following: - -```json -{ - "m.homeserver": { - "base_url": "https://matrix.example.com" - }, - "org.matrix.msc2965.authentication": { - "issuer": "https://example.com/", - "account": "https://auth.example.com/account" - } -} -``` - -For more context on what the correct values are, see [here](./). From dbb68257fcdc99ae85b1f9a2f575f9a83c5e6ec5 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 24 Apr 2025 12:36:41 +0200 Subject: [PATCH 079/189] Compile the user-agent regexes once --- crates/data-model/src/user_agent.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/data-model/src/user_agent.rs b/crates/data-model/src/user_agent.rs index 2b02ebd48..2ac4b06bd 100644 --- a/crates/data-model/src/user_agent.rs +++ b/crates/data-model/src/user_agent.rs @@ -4,9 +4,18 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +use std::sync::LazyLock; + use serde::Serialize; use woothee::{parser::Parser, woothee::VALUE_UNKNOWN}; +static CUSTOM_USER_AGENT_REGEX: LazyLock = LazyLock::new(|| { + regex::Regex::new(r"^(?P[^/]+)/(?P[^ ]+) \((?P.+)\)$").unwrap() +}); + +static ELECTRON_USER_AGENT_REGEX: LazyLock = + LazyLock::new(|| regex::Regex::new(r"(?m)\w+/[\w.]+").unwrap()); + #[derive(Debug, Serialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum DeviceType { @@ -37,10 +46,7 @@ impl std::ops::Deref for UserAgent { impl UserAgent { fn parse_custom(user_agent: &str) -> Option<(&str, &str, &str, &str, Option<&str>)> { - let regex = regex::Regex::new(r"^(?P[^/]+)/(?P[^ ]+) \((?P.+)\)$") - .unwrap(); - - let captures = regex.captures(user_agent)?; + let captures = CUSTOM_USER_AGENT_REGEX.captures(user_agent)?; let name = captures.name("name")?.as_str(); let version = captures.name("version")?.as_str(); let segments: Vec<&str> = captures @@ -73,9 +79,8 @@ impl UserAgent { } fn parse_electron(user_agent: &str) -> Option<(&str, &str)> { - let regex = regex::Regex::new(r"(?m)\w+/[\w.]+").unwrap(); let omit_keys = ["Mozilla", "AppleWebKit", "Chrome", "Electron", "Safari"]; - return regex + return ELECTRON_USER_AGENT_REGEX .find_iter(user_agent) .map(|caps| caps.as_str().split_once('/').unwrap()) .find(|pair| !omit_keys.contains(&pair.0)); From f457bd8d350fc5c7fe9b35600795a01ef69ec112 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 24 Apr 2025 13:13:26 +0200 Subject: [PATCH 080/189] Don't parse the user agent unless we need to --- crates/data-model/src/compat/session.rs | 4 ++-- .../data-model/src/oauth2/device_code_grant.rs | 4 ++-- crates/data-model/src/oauth2/session.rs | 4 ++-- crates/data-model/src/users.rs | 12 +++++------- crates/handlers/src/admin/model.rs | 6 +++--- crates/handlers/src/compat/login.rs | 6 ++---- .../src/graphql/model/browser_sessions.rs | 6 +++++- .../src/graphql/model/compat_sessions.rs | 6 +++++- crates/handlers/src/graphql/model/oauth.rs | 6 +++++- crates/handlers/src/oauth2/device/authorize.rs | 3 +-- crates/handlers/src/oauth2/token.rs | 14 +++++++------- crates/handlers/src/upstream_oauth2/link.rs | 9 ++++----- crates/handlers/src/views/login.rs | 4 ++-- crates/handlers/src/views/recovery/start.rs | 4 ++-- crates/handlers/src/views/register/password.rs | 6 +++--- .../src/views/register/steps/finish.rs | 3 +-- crates/storage-pg/src/app_session.rs | 5 +---- crates/storage-pg/src/compat/mod.rs | 4 ++-- crates/storage-pg/src/compat/session.rs | 8 ++++---- .../storage-pg/src/oauth2/device_code_grant.rs | 4 ++-- crates/storage-pg/src/oauth2/mod.rs | 4 ++-- crates/storage-pg/src/oauth2/session.rs | 8 ++++---- crates/storage-pg/src/user/recovery.rs | 8 ++++---- crates/storage-pg/src/user/registration.rs | 18 ++++++------------ crates/storage-pg/src/user/session.rs | 6 +++--- crates/storage/src/compat/session.rs | 6 +++--- crates/storage/src/oauth2/device_code_grant.rs | 4 ++-- crates/storage/src/oauth2/session.rs | 6 +++--- crates/storage/src/user/recovery.rs | 6 +++--- crates/storage/src/user/registration.rs | 6 +++--- crates/storage/src/user/session.rs | 6 +++--- crates/templates/src/context.rs | 10 +++++----- 32 files changed, 101 insertions(+), 105 deletions(-) diff --git a/crates/data-model/src/compat/session.rs b/crates/data-model/src/compat/session.rs index fc660c3f0..91b48cea0 100644 --- a/crates/data-model/src/compat/session.rs +++ b/crates/data-model/src/compat/session.rs @@ -11,7 +11,7 @@ use serde::Serialize; use ulid::Ulid; use super::Device; -use crate::{InvalidTransitionError, UserAgent}; +use crate::InvalidTransitionError; #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] pub enum CompatSessionState { @@ -76,7 +76,7 @@ pub struct CompatSession { pub user_session_id: Option, pub created_at: DateTime, pub is_synapse_admin: bool, - pub user_agent: Option, + pub user_agent: Option, pub last_active_at: Option>, pub last_active_ip: Option, } diff --git a/crates/data-model/src/oauth2/device_code_grant.rs b/crates/data-model/src/oauth2/device_code_grant.rs index bf230b850..794cc460b 100644 --- a/crates/data-model/src/oauth2/device_code_grant.rs +++ b/crates/data-model/src/oauth2/device_code_grant.rs @@ -11,7 +11,7 @@ use oauth2_types::scope::Scope; use serde::Serialize; use ulid::Ulid; -use crate::{BrowserSession, InvalidTransitionError, Session, UserAgent}; +use crate::{BrowserSession, InvalidTransitionError, Session}; #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case", tag = "state")] @@ -192,7 +192,7 @@ pub struct DeviceCodeGrant { pub ip_address: Option, /// The user agent used to request this device code grant. - pub user_agent: Option, + pub user_agent: Option, } impl std::ops::Deref for DeviceCodeGrant { diff --git a/crates/data-model/src/oauth2/session.rs b/crates/data-model/src/oauth2/session.rs index 675701619..3024aa082 100644 --- a/crates/data-model/src/oauth2/session.rs +++ b/crates/data-model/src/oauth2/session.rs @@ -11,7 +11,7 @@ use oauth2_types::scope::Scope; use serde::Serialize; use ulid::Ulid; -use crate::{InvalidTransitionError, UserAgent}; +use crate::InvalidTransitionError; #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] pub enum SessionState { @@ -80,7 +80,7 @@ pub struct Session { pub user_session_id: Option, pub client_id: Ulid, pub scope: Scope, - pub user_agent: Option, + pub user_agent: Option, pub last_active_at: Option>, pub last_active_ip: Option, } diff --git a/crates/data-model/src/users.rs b/crates/data-model/src/users.rs index 41b6c4f70..7e40f4df2 100644 --- a/crates/data-model/src/users.rs +++ b/crates/data-model/src/users.rs @@ -12,8 +12,6 @@ use serde::Serialize; use ulid::Ulid; use url::Url; -use crate::UserAgent; - #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct User { pub id: Ulid, @@ -81,7 +79,7 @@ pub enum AuthenticationMethod { pub struct UserRecoverySession { pub id: Ulid, pub email: String, - pub user_agent: UserAgent, + pub user_agent: String, pub ip_address: Option, pub locale: String, pub created_at: DateTime, @@ -137,7 +135,7 @@ pub struct BrowserSession { pub user: User, pub created_at: DateTime, pub finished_at: Option>, - pub user_agent: Option, + pub user_agent: Option, pub last_active_at: Option>, pub last_active_ip: Option, } @@ -159,9 +157,9 @@ impl BrowserSession { user, created_at: now, finished_at: None, - user_agent: Some(UserAgent::parse( + user_agent: Some( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned() - )), + ), last_active_at: Some(now), last_active_ip: None, }) @@ -213,7 +211,7 @@ pub struct UserRegistration { pub password: Option, pub post_auth_action: Option, pub ip_address: Option, - pub user_agent: Option, + pub user_agent: Option, pub created_at: DateTime, pub completed_at: Option>, } diff --git a/crates/handlers/src/admin/model.rs b/crates/handlers/src/admin/model.rs index c3e81c627..ab3a9d8a6 100644 --- a/crates/handlers/src/admin/model.rs +++ b/crates/handlers/src/admin/model.rs @@ -206,7 +206,7 @@ impl user_session_id: session.user_session_id, redirect_uri: sso_login.map(|sso| sso.redirect_uri), created_at: session.created_at, - user_agent: session.user_agent.map(|ua| ua.raw), + user_agent: session.user_agent, last_active_at: session.last_active_at, last_active_ip: session.last_active_ip, finished_at, @@ -313,7 +313,7 @@ impl From for OAuth2Session { user_session_id: session.user_session_id, client_id: session.client_id, scope: session.scope.to_string(), - user_agent: session.user_agent.map(|ua| ua.raw), + user_agent: session.user_agent, last_active_at: session.last_active_at, last_active_ip: session.last_active_ip, } @@ -406,7 +406,7 @@ impl From for UserSession { created_at: value.created_at, finished_at: value.finished_at, user_id: value.user.id, - user_agent: value.user_agent.map(|ua| ua.raw), + user_agent: value.user_agent, last_active_at: value.last_active_at, last_active_ip: value.last_active_ip, } diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index d13b50d3c..78765b2cf 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -11,9 +11,7 @@ use axum_extra::typed_header::TypedHeader; use chrono::Duration; use hyper::StatusCode; use mas_axum_utils::record_error; -use mas_data_model::{ - CompatSession, CompatSsoLoginState, Device, SiteConfig, TokenType, User, UserAgent, -}; +use mas_data_model::{CompatSession, CompatSsoLoginState, Device, SiteConfig, TokenType, User}; use mas_matrix::HomeserverConnection; use mas_storage::{ BoxClock, BoxRepository, BoxRng, Clock, RepositoryAccess, @@ -276,7 +274,7 @@ pub(crate) async fn post( user_agent: Option>, MatrixJsonBody(input): MatrixJsonBody, ) -> Result { - let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned())); + let user_agent = user_agent.map(|ua| ua.as_str().to_owned()); let login_type = input.credentials.login_type(); let (mut session, user) = match (password_manager.is_enabled(), input.credentials) { ( diff --git a/crates/handlers/src/graphql/model/browser_sessions.rs b/crates/handlers/src/graphql/model/browser_sessions.rs index 15046ebb5..5e15644e2 100644 --- a/crates/handlers/src/graphql/model/browser_sessions.rs +++ b/crates/handlers/src/graphql/model/browser_sessions.rs @@ -81,7 +81,11 @@ impl BrowserSession { /// The user-agent with which the session was created. pub async fn user_agent(&self) -> Option { - self.0.user_agent.clone().map(UserAgent::from) + self.0 + .user_agent + .clone() + .map(mas_data_model::UserAgent::parse) + .map(UserAgent::from) } /// The last IP address used by the session. diff --git a/crates/handlers/src/graphql/model/compat_sessions.rs b/crates/handlers/src/graphql/model/compat_sessions.rs index 1ac53a558..77ed7e6cc 100644 --- a/crates/handlers/src/graphql/model/compat_sessions.rs +++ b/crates/handlers/src/graphql/model/compat_sessions.rs @@ -98,7 +98,11 @@ impl CompatSession { /// The user-agent with which the session was created. pub async fn user_agent(&self) -> Option { - self.session.user_agent.clone().map(UserAgent::from) + self.session + .user_agent + .clone() + .map(mas_data_model::UserAgent::parse) + .map(UserAgent::from) } /// The associated SSO login, if any. diff --git a/crates/handlers/src/graphql/model/oauth.rs b/crates/handlers/src/graphql/model/oauth.rs index fec318eb8..9c8dc5f1a 100644 --- a/crates/handlers/src/graphql/model/oauth.rs +++ b/crates/handlers/src/graphql/model/oauth.rs @@ -61,7 +61,11 @@ impl OAuth2Session { /// The user-agent with which the session was created. pub async fn user_agent(&self) -> Option { - self.0.user_agent.clone().map(UserAgent::from) + self.0 + .user_agent + .clone() + .map(mas_data_model::UserAgent::parse) + .map(UserAgent::from) } /// The state of the session. diff --git a/crates/handlers/src/oauth2/device/authorize.rs b/crates/handlers/src/oauth2/device/authorize.rs index dcf0316d4..1feec8c3e 100644 --- a/crates/handlers/src/oauth2/device/authorize.rs +++ b/crates/handlers/src/oauth2/device/authorize.rs @@ -13,7 +13,6 @@ use mas_axum_utils::{ client_authorization::{ClientAuthorization, CredentialsVerificationError}, record_error, }; -use mas_data_model::UserAgent; use mas_keystore::Encrypter; use mas_router::UrlBuilder; use mas_storage::{BoxClock, BoxRepository, BoxRng, oauth2::OAuth2DeviceCodeGrantParams}; @@ -137,7 +136,7 @@ pub(crate) async fn post( let expires_in = Duration::microseconds(20 * 60 * 1000 * 1000); - let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned())); + let user_agent = user_agent.map(|ua| ua.as_str().to_owned()); let ip_address = activity_tracker.ip(); let device_code = Alphanumeric.sample_string(&mut rng, 32); diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index d66d7d2f4..a47207d9b 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -16,7 +16,7 @@ use mas_axum_utils::{ record_error, }; use mas_data_model::{ - AuthorizationGrantStage, Client, Device, DeviceCodeGrantState, SiteConfig, TokenType, UserAgent, + AuthorizationGrantStage, Client, Device, DeviceCodeGrantState, SiteConfig, TokenType, }; use mas_keystore::{Encrypter, Keystore}; use mas_matrix::HomeserverConnection; @@ -285,7 +285,7 @@ pub(crate) async fn post( user_agent: Option>, client_authorization: ClientAuthorization, ) -> Result { - let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned())); + let user_agent = user_agent.map(|ua| ua.as_str().to_owned()); let client = client_authorization .credentials .fetch(&mut repo) @@ -415,7 +415,7 @@ async fn authorization_code_grant( site_config: &SiteConfig, mut repo: BoxRepository, homeserver: &Arc, - user_agent: Option, + user_agent: Option, ) -> Result<(AccessTokenResponse, BoxRepository), RouteError> { // Check that the client is allowed to use this grant type if !client.grant_types.contains(&GrantType::AuthorizationCode) { @@ -596,7 +596,7 @@ async fn refresh_token_grant( client: &Client, site_config: &SiteConfig, mut repo: BoxRepository, - user_agent: Option, + user_agent: Option, ) -> Result<(AccessTokenResponse, BoxRepository), RouteError> { // Check that the client is allowed to use this grant type if !client.grant_types.contains(&GrantType::RefreshToken) { @@ -749,7 +749,7 @@ async fn client_credentials_grant( site_config: &SiteConfig, mut repo: BoxRepository, mut policy: Policy, - user_agent: Option, + user_agent: Option, ) -> Result<(AccessTokenResponse, BoxRepository), RouteError> { // Check that the client is allowed to use this grant type if !client.grant_types.contains(&GrantType::ClientCredentials) { @@ -771,7 +771,7 @@ async fn client_credentials_grant( grant_type: mas_policy::GrantType::ClientCredentials, requester: mas_policy::Requester { ip_address: activity_tracker.ip(), - user_agent: user_agent.clone().map(|ua| ua.raw), + user_agent: user_agent.clone(), }, }) .await?; @@ -828,7 +828,7 @@ async fn device_code_grant( site_config: &SiteConfig, mut repo: BoxRepository, homeserver: &Arc, - user_agent: Option, + user_agent: Option, ) -> Result<(AccessTokenResponse, BoxRepository), RouteError> { // Check that the client is allowed to use this grant type if !client.grant_types.contains(&GrantType::DeviceCode) { diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index b80f3956a..730576b61 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -19,7 +19,6 @@ use mas_axum_utils::{ csrf::{CsrfExt, ProtectedForm}, record_error, }; -use mas_data_model::UserAgent; use mas_jose::jwt::Jwt; use mas_matrix::HomeserverConnection; use mas_policy::Policy; @@ -233,7 +232,7 @@ pub(crate) async fn get( user_agent: Option>, Path(link_id): Path, ) -> Result { - let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned())); + let user_agent = user_agent.map(|ua| ua.as_str().to_owned()); let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar); let (session_id, post_auth_action) = sessions_cookie .lookup_link(link_id) @@ -502,7 +501,7 @@ pub(crate) async fn get( email: None, requester: mas_policy::Requester { ip_address: activity_tracker.ip(), - user_agent: user_agent.clone().map(|ua| ua.raw), + user_agent: user_agent.clone(), }, }) .await?; @@ -568,7 +567,7 @@ pub(crate) async fn post( Path(link_id): Path, Form(form): Form>, ) -> Result { - let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned())); + let user_agent = user_agent.map(|ua| ua.as_str().to_owned()); let form = cookie_jar.verify_form(&clock, form)?; let sessions_cookie = UpstreamSessionsCookie::load(&cookie_jar); @@ -786,7 +785,7 @@ pub(crate) async fn post( email: email.as_deref(), requester: mas_policy::Requester { ip_address: activity_tracker.ip(), - user_agent: user_agent.clone().map(|ua| ua.raw), + user_agent: user_agent.clone(), }, }) .await?; diff --git a/crates/handlers/src/views/login.rs b/crates/handlers/src/views/login.rs index d8798fcae..e22a077d0 100644 --- a/crates/handlers/src/views/login.rs +++ b/crates/handlers/src/views/login.rs @@ -17,7 +17,7 @@ use mas_axum_utils::{ cookies::CookieJar, csrf::{CsrfExt, ProtectedForm}, }; -use mas_data_model::{UserAgent, oauth2::LoginHint}; +use mas_data_model::oauth2::LoginHint; use mas_i18n::DataLocale; use mas_matrix::HomeserverConnection; use mas_router::{UpstreamOAuth2Authorize, UrlBuilder}; @@ -146,7 +146,7 @@ pub(crate) async fn post( user_agent: Option>, Form(form): Form>, ) -> Result { - let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned())); + let user_agent = user_agent.map(|ua| ua.as_str().to_owned()); if !site_config.password_login_enabled { // XXX: is it necessary to have better errors here? return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response()); diff --git a/crates/handlers/src/views/recovery/start.rs b/crates/handlers/src/views/recovery/start.rs index 728e71834..ad87bdd17 100644 --- a/crates/handlers/src/views/recovery/start.rs +++ b/crates/handlers/src/views/recovery/start.rs @@ -18,7 +18,7 @@ use mas_axum_utils::{ cookies::CookieJar, csrf::{CsrfExt, ProtectedForm}, }; -use mas_data_model::{SiteConfig, UserAgent}; +use mas_data_model::SiteConfig; use mas_router::UrlBuilder; use mas_storage::{ BoxClock, BoxRepository, BoxRng, @@ -102,7 +102,7 @@ pub(crate) async fn post( return Ok((cookie_jar, url_builder.redirect(&mas_router::Index)).into_response()); } - let user_agent = UserAgent::parse(user_agent.as_str().to_owned()); + let user_agent = user_agent.as_str().to_owned(); let ip_address = activity_tracker.ip(); let form = cookie_jar.verify_form(&clock, form)?; diff --git a/crates/handlers/src/views/register/password.rs b/crates/handlers/src/views/register/password.rs index 4aa0d76dd..92058970a 100644 --- a/crates/handlers/src/views/register/password.rs +++ b/crates/handlers/src/views/register/password.rs @@ -18,7 +18,7 @@ use mas_axum_utils::{ cookies::CookieJar, csrf::{CsrfExt, CsrfToken, ProtectedForm}, }; -use mas_data_model::{CaptchaConfig, UserAgent}; +use mas_data_model::CaptchaConfig; use mas_i18n::DataLocale; use mas_matrix::HomeserverConnection; use mas_policy::Policy; @@ -141,7 +141,7 @@ pub(crate) async fn post( cookie_jar: CookieJar, Form(form): Form>, ) -> Result { - let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned())); + let user_agent = user_agent.map(|ua| ua.as_str().to_owned()); let ip_address = activity_tracker.ip(); if !site_config.password_registration_enabled { @@ -239,7 +239,7 @@ pub(crate) async fn post( email: Some(&form.email), requester: mas_policy::Requester { ip_address: activity_tracker.ip(), - user_agent: user_agent.clone().map(|ua| ua.raw), + user_agent: user_agent.clone(), }, }) .await?; diff --git a/crates/handlers/src/views/register/steps/finish.rs b/crates/handlers/src/views/register/steps/finish.rs index c0c0df404..afc05b65b 100644 --- a/crates/handlers/src/views/register/steps/finish.rs +++ b/crates/handlers/src/views/register/steps/finish.rs @@ -13,7 +13,6 @@ use axum::{ use axum_extra::TypedHeader; use chrono::Duration; use mas_axum_utils::{FancyError, SessionInfoExt as _, cookies::CookieJar}; -use mas_data_model::UserAgent; use mas_matrix::HomeserverConnection; use mas_router::{PostAuthAction, UrlBuilder}; use mas_storage::{ @@ -56,7 +55,7 @@ pub(crate) async fn get( cookie_jar: CookieJar, Path(id): Path, ) -> Result { - let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned())); + let user_agent = user_agent.map(|ua| ua.as_str().to_owned()); let registration = repo .user_registration() .lookup(id) diff --git a/crates/storage-pg/src/app_session.rs b/crates/storage-pg/src/app_session.rs index d54c604b3..2c747c8ca 100644 --- a/crates/storage-pg/src/app_session.rs +++ b/crates/storage-pg/src/app_session.rs @@ -7,9 +7,7 @@ //! A module containing PostgreSQL implementation of repositories for sessions use async_trait::async_trait; -use mas_data_model::{ - CompatSession, CompatSessionState, Device, Session, SessionState, User, UserAgent, -}; +use mas_data_model::{CompatSession, CompatSessionState, Device, Session, SessionState, User}; use mas_storage::{ Clock, Page, Pagination, app_session::{AppSession, AppSessionFilter, AppSessionRepository, AppSessionState}, @@ -106,7 +104,6 @@ impl TryFrom for AppSession { last_active_ip, } = value; - let user_agent = user_agent.map(UserAgent::parse); let user_session_id = user_session_id.map(Ulid::from); match ( diff --git a/crates/storage-pg/src/compat/mod.rs b/crates/storage-pg/src/compat/mod.rs index 1d0e40426..8ceb089b7 100644 --- a/crates/storage-pg/src/compat/mod.rs +++ b/crates/storage-pg/src/compat/mod.rs @@ -20,7 +20,7 @@ pub use self::{ #[cfg(test)] mod tests { use chrono::Duration; - use mas_data_model::{Device, UserAgent}; + use mas_data_model::Device; use mas_storage::{ Clock, Pagination, RepositoryAccess, clock::MockClock, @@ -125,7 +125,7 @@ mod tests { assert!(session_lookup.user_agent.is_none()); let session = repo .compat_session() - .record_user_agent(session_lookup, UserAgent::parse("Mozilla/5.0".to_owned())) + .record_user_agent(session_lookup, "Mozilla/5.0".to_owned()) .await .unwrap(); assert_eq!(session.user_agent.as_deref(), Some("Mozilla/5.0")); diff --git a/crates/storage-pg/src/compat/session.rs b/crates/storage-pg/src/compat/session.rs index 10c9fd9ad..c844be238 100644 --- a/crates/storage-pg/src/compat/session.rs +++ b/crates/storage-pg/src/compat/session.rs @@ -10,7 +10,7 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; use mas_data_model::{ BrowserSession, CompatSession, CompatSessionState, CompatSsoLogin, CompatSsoLoginState, Device, - User, UserAgent, + User, }; use mas_storage::{ Clock, Page, Pagination, @@ -77,7 +77,7 @@ impl From for CompatSession { human_name: value.human_name, created_at: value.created_at, is_synapse_admin: value.is_synapse_admin, - user_agent: value.user_agent.map(UserAgent::parse), + user_agent: value.user_agent, last_active_at: value.last_active_at, last_active_ip: value.last_active_ip, } @@ -126,7 +126,7 @@ impl TryFrom for (CompatSession, Option { async fn record_user_agent( &mut self, mut compat_session: CompatSession, - user_agent: UserAgent, + user_agent: String, ) -> Result { let res = sqlx::query!( r#" diff --git a/crates/storage-pg/src/oauth2/device_code_grant.rs b/crates/storage-pg/src/oauth2/device_code_grant.rs index 409ab3ff9..ebed4d859 100644 --- a/crates/storage-pg/src/oauth2/device_code_grant.rs +++ b/crates/storage-pg/src/oauth2/device_code_grant.rs @@ -8,7 +8,7 @@ use std::net::IpAddr; use async_trait::async_trait; use chrono::{DateTime, Utc}; -use mas_data_model::{BrowserSession, DeviceCodeGrant, DeviceCodeGrantState, Session, UserAgent}; +use mas_data_model::{BrowserSession, DeviceCodeGrant, DeviceCodeGrantState, Session}; use mas_storage::{ Clock, oauth2::{OAuth2DeviceCodeGrantParams, OAuth2DeviceCodeGrantRepository}, @@ -132,7 +132,7 @@ impl TryFrom for DeviceCodeGrant { created_at, expires_at, ip_address, - user_agent: user_agent.map(UserAgent::parse), + user_agent, }) } } diff --git a/crates/storage-pg/src/oauth2/mod.rs b/crates/storage-pg/src/oauth2/mod.rs index 5968e625d..d5e7f7694 100644 --- a/crates/storage-pg/src/oauth2/mod.rs +++ b/crates/storage-pg/src/oauth2/mod.rs @@ -24,7 +24,7 @@ pub use self::{ #[cfg(test)] mod tests { use chrono::Duration; - use mas_data_model::{AuthorizationCode, UserAgent}; + use mas_data_model::AuthorizationCode; use mas_storage::{ Clock, Pagination, clock::MockClock, @@ -351,7 +351,7 @@ mod tests { assert!(session.user_agent.is_none()); let session = repo .oauth2_session() - .record_user_agent(session, UserAgent::parse("Mozilla/5.0".to_owned())) + .record_user_agent(session, "Mozilla/5.0".to_owned()) .await .unwrap(); assert_eq!(session.user_agent.as_deref(), Some("Mozilla/5.0")); diff --git a/crates/storage-pg/src/oauth2/session.rs b/crates/storage-pg/src/oauth2/session.rs index 6b753e17d..b525e22a0 100644 --- a/crates/storage-pg/src/oauth2/session.rs +++ b/crates/storage-pg/src/oauth2/session.rs @@ -8,7 +8,7 @@ use std::net::IpAddr; use async_trait::async_trait; use chrono::{DateTime, Utc}; -use mas_data_model::{BrowserSession, Client, Session, SessionState, User, UserAgent}; +use mas_data_model::{BrowserSession, Client, Session, SessionState, User}; use mas_storage::{ Clock, Page, Pagination, oauth2::{OAuth2SessionFilter, OAuth2SessionRepository}, @@ -87,7 +87,7 @@ impl TryFrom for Session { user_id: value.user_id.map(Ulid::from), user_session_id: value.user_session_id.map(Ulid::from), scope, - user_agent: value.user_agent.map(UserAgent::parse), + user_agent: value.user_agent, last_active_at: value.last_active_at, last_active_ip: value.last_active_ip, }) @@ -490,14 +490,14 @@ impl OAuth2SessionRepository for PgOAuth2SessionRepository<'_> { %session.id, %session.scope, client.id = %session.client_id, - session.user_agent = %user_agent.raw, + session.user_agent = user_agent, ), err, )] async fn record_user_agent( &mut self, mut session: Session, - user_agent: UserAgent, + user_agent: String, ) -> Result { let res = sqlx::query!( r#" diff --git a/crates/storage-pg/src/user/recovery.rs b/crates/storage-pg/src/user/recovery.rs index d838b2531..bc108b52a 100644 --- a/crates/storage-pg/src/user/recovery.rs +++ b/crates/storage-pg/src/user/recovery.rs @@ -8,7 +8,7 @@ use std::net::IpAddr; use async_trait::async_trait; use chrono::{DateTime, Duration, Utc}; -use mas_data_model::{UserAgent, UserEmail, UserRecoverySession, UserRecoveryTicket}; +use mas_data_model::{UserEmail, UserRecoverySession, UserRecoveryTicket}; use mas_storage::{Clock, user::UserRecoveryRepository}; use rand::RngCore; use sqlx::PgConnection; @@ -45,7 +45,7 @@ impl From for UserRecoverySession { UserRecoverySession { id: row.user_recovery_session_id.into(), email: row.email, - user_agent: UserAgent::parse(row.user_agent), + user_agent: row.user_agent, ip_address: row.ip_address, locale: row.locale, created_at: row.created_at, @@ -127,7 +127,7 @@ impl UserRecoveryRepository for PgUserRecoveryRepository<'_> { db.query.text, user_recovery_session.id, user_recovery_session.email = email, - user_recovery_session.user_agent = &*user_agent, + user_recovery_session.user_agent = user_agent, user_recovery_session.ip_address = ip_address.map(|ip| ip.to_string()), ) )] @@ -136,7 +136,7 @@ impl UserRecoveryRepository for PgUserRecoveryRepository<'_> { rng: &mut (dyn RngCore + Send), clock: &dyn Clock, email: String, - user_agent: UserAgent, + user_agent: String, ip_address: Option, locale: String, ) -> Result { diff --git a/crates/storage-pg/src/user/registration.rs b/crates/storage-pg/src/user/registration.rs index 1aa2afe86..5d578ab79 100644 --- a/crates/storage-pg/src/user/registration.rs +++ b/crates/storage-pg/src/user/registration.rs @@ -7,9 +7,7 @@ use std::net::IpAddr; use async_trait::async_trait; use chrono::{DateTime, Utc}; -use mas_data_model::{ - UserAgent, UserEmailAuthentication, UserRegistration, UserRegistrationPassword, -}; +use mas_data_model::{UserEmailAuthentication, UserRegistration, UserRegistrationPassword}; use mas_storage::{Clock, user::UserRegistrationRepository}; use rand::RngCore; use sqlx::PgConnection; @@ -53,7 +51,6 @@ impl TryFrom for UserRegistration { fn try_from(value: UserRegistrationLookup) -> Result { let id = Ulid::from(value.user_registration_id); - let user_agent = value.user_agent.map(UserAgent::parse); let password = match (value.hashed_password, value.hashed_password_version) { (Some(hashed_password), Some(version)) => { @@ -91,7 +88,7 @@ impl TryFrom for UserRegistration { Ok(UserRegistration { id, ip_address: value.ip_address, - user_agent, + user_agent: value.user_agent, post_auth_action: value.post_auth_action, username: value.username, display_name: value.display_name, @@ -162,7 +159,7 @@ impl UserRegistrationRepository for PgUserRegistrationRepository<'_> { clock: &dyn Clock, username: String, ip_address: Option, - user_agent: Option, + user_agent: Option, post_auth_action: Option, ) -> Result { let created_at = clock.now(); @@ -394,7 +391,7 @@ impl UserRegistrationRepository for PgUserRegistrationRepository<'_> { mod tests { use std::net::{IpAddr, Ipv4Addr}; - use mas_data_model::{UserAgent, UserRegistrationPassword}; + use mas_data_model::UserRegistrationPassword; use mas_storage::{Clock, clock::MockClock}; use rand::SeedableRng; use rand_chacha::ChaChaRng; @@ -487,16 +484,13 @@ mod tests { &clock, "alice".to_owned(), Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), - Some(UserAgent::parse("Mozilla/5.0".to_owned())), + Some("Mozilla/5.0".to_owned()), Some(serde_json::json!({"action": "continue_compat_sso_login", "id": "01FSHN9AG0MKGTBNZ16RDR3PVY"})), ) .await .unwrap(); - assert_eq!( - registration.user_agent, - Some(UserAgent::parse("Mozilla/5.0".to_owned())) - ); + assert_eq!(registration.user_agent, Some("Mozilla/5.0".to_owned())); assert_eq!( registration.ip_address, Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))) diff --git a/crates/storage-pg/src/user/session.rs b/crates/storage-pg/src/user/session.rs index ce027afc0..fa4c69f82 100644 --- a/crates/storage-pg/src/user/session.rs +++ b/crates/storage-pg/src/user/session.rs @@ -10,7 +10,7 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; use mas_data_model::{ Authentication, AuthenticationMethod, BrowserSession, Password, - UpstreamOAuthAuthorizationSession, User, UserAgent, + UpstreamOAuthAuthorizationSession, User, }; use mas_storage::{ Clock, Page, Pagination, @@ -83,7 +83,7 @@ impl TryFrom for BrowserSession { user, created_at: value.user_session_created_at, finished_at: value.user_session_finished_at, - user_agent: value.user_session_user_agent.map(UserAgent::parse), + user_agent: value.user_session_user_agent, last_active_at: value.user_session_last_active_at, last_active_ip: value.user_session_last_active_ip, }) @@ -208,7 +208,7 @@ impl BrowserSessionRepository for PgBrowserSessionRepository<'_> { rng: &mut (dyn RngCore + Send), clock: &dyn Clock, user: &User, - user_agent: Option, + user_agent: Option, ) -> Result { let created_at = clock.now(); let id = Ulid::from_datetime_with_source(created_at.into(), rng); diff --git a/crates/storage/src/compat/session.rs b/crates/storage/src/compat/session.rs index 81a417fa6..757f5269b 100644 --- a/crates/storage/src/compat/session.rs +++ b/crates/storage/src/compat/session.rs @@ -8,7 +8,7 @@ use std::net::IpAddr; use async_trait::async_trait; use chrono::{DateTime, Utc}; -use mas_data_model::{BrowserSession, CompatSession, CompatSsoLogin, Device, User, UserAgent}; +use mas_data_model::{BrowserSession, CompatSession, CompatSsoLogin, Device, User}; use rand_core::RngCore; use ulid::Ulid; @@ -322,7 +322,7 @@ pub trait CompatSessionRepository: Send + Sync { async fn record_user_agent( &mut self, compat_session: CompatSession, - user_agent: UserAgent, + user_agent: String, ) -> Result; } @@ -367,6 +367,6 @@ repository_impl!(CompatSessionRepository: async fn record_user_agent( &mut self, compat_session: CompatSession, - user_agent: UserAgent, + user_agent: String, ) -> Result; ); diff --git a/crates/storage/src/oauth2/device_code_grant.rs b/crates/storage/src/oauth2/device_code_grant.rs index 9a85a3a75..762e854cc 100644 --- a/crates/storage/src/oauth2/device_code_grant.rs +++ b/crates/storage/src/oauth2/device_code_grant.rs @@ -8,7 +8,7 @@ use std::net::IpAddr; use async_trait::async_trait; use chrono::Duration; -use mas_data_model::{BrowserSession, Client, DeviceCodeGrant, Session, UserAgent}; +use mas_data_model::{BrowserSession, Client, DeviceCodeGrant, Session}; use oauth2_types::scope::Scope; use rand_core::RngCore; use ulid::Ulid; @@ -36,7 +36,7 @@ pub struct OAuth2DeviceCodeGrantParams<'a> { pub ip_address: Option, /// The user agent from which the request was made - pub user_agent: Option, + pub user_agent: Option, } /// An [`OAuth2DeviceCodeGrantRepository`] helps interacting with diff --git a/crates/storage/src/oauth2/session.rs b/crates/storage/src/oauth2/session.rs index d53eaa85c..7dbba4a03 100644 --- a/crates/storage/src/oauth2/session.rs +++ b/crates/storage/src/oauth2/session.rs @@ -8,7 +8,7 @@ use std::net::IpAddr; use async_trait::async_trait; use chrono::{DateTime, Utc}; -use mas_data_model::{BrowserSession, Client, Device, Session, User, UserAgent}; +use mas_data_model::{BrowserSession, Client, Device, Session, User}; use oauth2_types::scope::Scope; use rand_core::RngCore; use ulid::Ulid; @@ -428,7 +428,7 @@ pub trait OAuth2SessionRepository: Send + Sync { async fn record_user_agent( &mut self, session: Session, - user_agent: UserAgent, + user_agent: String, ) -> Result; } @@ -487,6 +487,6 @@ repository_impl!(OAuth2SessionRepository: async fn record_user_agent( &mut self, session: Session, - user_agent: UserAgent, + user_agent: String, ) -> Result; ); diff --git a/crates/storage/src/user/recovery.rs b/crates/storage/src/user/recovery.rs index 05f2d9333..a5361e795 100644 --- a/crates/storage/src/user/recovery.rs +++ b/crates/storage/src/user/recovery.rs @@ -7,7 +7,7 @@ use std::net::IpAddr; use async_trait::async_trait; -use mas_data_model::{UserAgent, UserEmail, UserRecoverySession, UserRecoveryTicket}; +use mas_data_model::{UserEmail, UserRecoverySession, UserRecoveryTicket}; use rand_core::RngCore; use ulid::Ulid; @@ -59,7 +59,7 @@ pub trait UserRecoveryRepository: Send + Sync { rng: &mut (dyn RngCore + Send), clock: &dyn Clock, email: String, - user_agent: UserAgent, + user_agent: String, ip_address: Option, locale: String, ) -> Result; @@ -131,7 +131,7 @@ repository_impl!(UserRecoveryRepository: rng: &mut (dyn RngCore + Send), clock: &dyn Clock, email: String, - user_agent: UserAgent, + user_agent: String, ip_address: Option, locale: String, ) -> Result; diff --git a/crates/storage/src/user/registration.rs b/crates/storage/src/user/registration.rs index 8bc4ddcb0..3932db622 100644 --- a/crates/storage/src/user/registration.rs +++ b/crates/storage/src/user/registration.rs @@ -6,7 +6,7 @@ use std::net::IpAddr; use async_trait::async_trait; -use mas_data_model::{UserAgent, UserEmailAuthentication, UserRegistration}; +use mas_data_model::{UserEmailAuthentication, UserRegistration}; use rand_core::RngCore; use ulid::Ulid; use url::Url; @@ -56,7 +56,7 @@ pub trait UserRegistrationRepository: Send + Sync { clock: &dyn Clock, username: String, ip_address: Option, - user_agent: Option, + user_agent: Option, post_auth_action: Option, ) -> Result; @@ -166,7 +166,7 @@ repository_impl!(UserRegistrationRepository: clock: &dyn Clock, username: String, ip_address: Option, - user_agent: Option, + user_agent: Option, post_auth_action: Option, ) -> Result; async fn set_display_name( diff --git a/crates/storage/src/user/session.rs b/crates/storage/src/user/session.rs index 355507530..2421ff009 100644 --- a/crates/storage/src/user/session.rs +++ b/crates/storage/src/user/session.rs @@ -9,7 +9,7 @@ use std::net::IpAddr; use async_trait::async_trait; use chrono::{DateTime, Utc}; use mas_data_model::{ - Authentication, BrowserSession, Password, UpstreamOAuthAuthorizationSession, User, UserAgent, + Authentication, BrowserSession, Password, UpstreamOAuthAuthorizationSession, User, }; use rand_core::RngCore; use ulid::Ulid; @@ -151,7 +151,7 @@ pub trait BrowserSessionRepository: Send + Sync { rng: &mut (dyn RngCore + Send), clock: &dyn Clock, user: &User, - user_agent: Option, + user_agent: Option, ) -> Result; /// Finish a [`BrowserSession`] @@ -296,7 +296,7 @@ repository_impl!(BrowserSessionRepository: rng: &mut (dyn RngCore + Send), clock: &dyn Clock, user: &User, - user_agent: Option, + user_agent: Option, ) -> Result; async fn finish( &mut self, diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index b6661d540..e1ca20069 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -22,7 +22,7 @@ use mas_data_model::{ AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState, DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderPkceMode, - UpstreamOAuthProviderTokenAuthMethod, User, UserAgent, UserEmailAuthentication, + UpstreamOAuthProviderTokenAuthMethod, User, UserEmailAuthentication, UserEmailAuthenticationCode, UserRecoverySession, UserRegistration, }; use mas_i18n::DataLocale; @@ -808,7 +808,7 @@ impl TemplateContext for EmailRecoveryContext { let session = UserRecoverySession { id: Ulid::from_datetime_with_source(now.into(), rng), email: "hello@example.com".to_owned(), - user_agent: UserAgent::parse("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/536.30.1 (KHTML, like Gecko) Version/6.0.5 Safari/536.30.1".to_owned()), + user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/536.30.1 (KHTML, like Gecko) Version/6.0.5 Safari/536.30.1".to_owned(), ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])), locale: "en".to_owned(), created_at: now, @@ -1106,7 +1106,7 @@ impl TemplateContext for RecoveryProgressContext { let session = UserRecoverySession { id: Ulid::from_datetime_with_source(now.into(), rng), email: "name@mail.com".to_owned(), - user_agent: UserAgent::parse("Mozilla/5.0".to_owned()), + user_agent: "Mozilla/5.0".to_owned(), ip_address: None, locale: "en".to_owned(), created_at: now, @@ -1148,7 +1148,7 @@ impl TemplateContext for RecoveryExpiredContext { let session = UserRecoverySession { id: Ulid::from_datetime_with_source(now.into(), rng), email: "name@mail.com".to_owned(), - user_agent: UserAgent::parse("Mozilla/5.0".to_owned()), + user_agent: "Mozilla/5.0".to_owned(), ip_address: None, locale: "en".to_owned(), created_at: now, @@ -1529,7 +1529,7 @@ impl TemplateContext for DeviceConsentContext { created_at: now - Duration::try_minutes(5).unwrap(), expires_at: now + Duration::try_minutes(25).unwrap(), ip_address: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), - user_agent: Some(UserAgent::parse("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned())), + user_agent: Some("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned()), }; Self { grant, client } }) From 564e70d8dc2219de64fad67c11bab8e8b9caf777 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 24 Apr 2025 14:16:26 +0200 Subject: [PATCH 081/189] perf: avoid unnecessary clones of the log context --- crates/cli/src/server.rs | 4 +--- crates/context/src/fmt.rs | 7 ++++--- crates/context/src/lib.rs | 9 ++++++--- crates/tasks/src/new_queue.rs | 6 +++--- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/crates/cli/src/server.rs b/crates/cli/src/server.rs index 969a93e23..114fb809c 100644 --- a/crates/cli/src/server.rs +++ b/crates/cli/src/server.rs @@ -184,13 +184,11 @@ async fn log_response_middleware( let response = next.run(request).await; - let Some(log_context) = LogContext::current() else { + let Some(stats) = LogContext::maybe_with(LogContext::stats) else { tracing::error!("Missing log context for request, this is a bug!"); return response; }; - let stats = log_context.stats(); - let status_code = response.status(); match status_code.as_u16() { 100..=399 => tracing::info!( diff --git a/crates/context/src/fmt.rs b/crates/context/src/fmt.rs index f2e619690..d9ae7baa5 100644 --- a/crates/context/src/fmt.rs +++ b/crates/context/src/fmt.rs @@ -113,13 +113,14 @@ where write!(&mut writer, "{} ", style.apply_to(metadata.name()))?; } - if let Some(log_context) = LogContext::current() { + LogContext::maybe_with(|log_context| { let log_context = Style::new() .bold() .force_styling(ansi) .apply_to(log_context); - write!(&mut writer, "{log_context} - ")?; - } + write!(&mut writer, "{log_context} - ") + }) + .transpose()?; let field_fromatter = DefaultFields::new(); field_fromatter.format_fields(writer.by_ref(), event)?; diff --git a/crates/context/src/lib.rs b/crates/context/src/lib.rs index a0d6b384b..655d407e9 100644 --- a/crates/context/src/lib.rs +++ b/crates/context/src/lib.rs @@ -76,9 +76,12 @@ impl LogContext { } } - /// Get a copy of the current log context, if any - pub fn current() -> Option { - CURRENT_LOG_CONTEXT.try_with(Self::clone).ok() + /// Run a closure with the current log context, if any + pub fn maybe_with(f: F) -> Option + where + F: FnOnce(&Self) -> R, + { + CURRENT_LOG_CONTEXT.try_with(f).ok() } /// Run the async function `f` with the given log context. It will wrap the diff --git a/crates/tasks/src/new_queue.rs b/crates/tasks/src/new_queue.rs index 81fef84d4..ea055e2f8 100644 --- a/crates/tasks/src/new_queue.rs +++ b/crates/tasks/src/new_queue.rs @@ -789,14 +789,14 @@ impl JobTracker { ); let result = job.run(&state, context.clone()).await; - let Some(log_context) = LogContext::current() else { + let Some(context_stats) = + LogContext::maybe_with(mas_context::LogContext::stats) + else { // This should never happen, but if it does it's fine: we're recovering fine // from panics in those tasks panic!("Missing log context, this should never happen"); }; - let context_stats = log_context.stats(); - // We log the result here so that it's attached to the right span & log context match &result { Ok(()) => { From fd258ccf2321952648ffe0c83bf83f5d282f0bb0 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 24 Apr 2025 14:59:07 +0200 Subject: [PATCH 082/189] Fix the ordering of the middlewares This was causing the number of event processors to constantly grow with each request, making the server use up more memory and CPU over time. --- crates/cli/src/server.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/server.rs b/crates/cli/src/server.rs index 969a93e23..ed4b34866 100644 --- a/crates/cli/src/server.rs +++ b/crates/cli/src/server.rs @@ -293,6 +293,7 @@ pub fn build_router( router = router.fallback(mas_handlers::fallback); router + .layer(axum::middleware::from_fn(log_response_middleware)) .layer( InFlightCounterLayer::new("http.server.active_requests").on_request(( name.map(|name| KeyValue::new(MAS_LISTENER_NAME, name.to_owned())), @@ -318,12 +319,16 @@ pub fn build_router( span.record("otel.status_code", "OK"); }), ) - .layer(axum::middleware::from_fn(log_response_middleware)) .layer(mas_context::LogContextLayer::new(|req| { otel_http_method(req).into() })) - .layer(NewSentryLayer::new_from_top()) + // Careful about the order here: the `NewSentryLayer` must be around the + // `SentryHttpLayer`. axum makes new layers wrap the existing ones, + // which is the other way around compared to `tower::ServiceBuilder`. + // So even if the Sentry docs has an example that does + // 'NewSentryHttpLayer then SentryHttpLayer', we must do the opposite. .layer(SentryHttpLayer::with_transaction()) + .layer(NewSentryLayer::new_from_top()) .with_state(state) } From f855510916a7631c7502967941be4ddc704461cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:07:26 +0000 Subject: [PATCH 083/189] build(deps): bump tokio-util from 0.7.14 to 0.7.15 Bumps [tokio-util](https://github.com/tokio-rs/tokio) from 0.7.14 to 0.7.15. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-util-0.7.14...tokio-util-0.7.15) --- updated-dependencies: - dependency-name: tokio-util dependency-version: 0.7.15 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9525fb480..3e3d09db1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6460,15 +6460,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", "futures-sink", "futures-util", - "hashbrown 0.14.5", + "hashbrown 0.15.2", "pin-project-lite", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index e5a306e0b..c75dcad70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -379,7 +379,7 @@ version = "0.1.17" # Useful async utilities [workspace.dependencies.tokio-util] -version = "0.7.14" +version = "0.7.15" features = ["rt"] # Tower services From c55d0e7c0b021e897a680f3f482caf337c630690 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 24 Apr 2025 16:44:33 +0200 Subject: [PATCH 084/189] matrix: allow setting a custom device display name --- crates/handlers/src/compat/login.rs | 4 +- .../src/graphql/mutations/oauth2_session.rs | 2 +- crates/handlers/src/oauth2/token.rs | 4 +- crates/matrix-synapse/src/lib.rs | 68 ++++++++++++++++-- crates/matrix/src/lib.rs | 70 +++++++++++++++++-- crates/matrix/src/mock.rs | 25 +++++-- crates/matrix/src/readonly.rs | 16 ++++- 7 files changed, 170 insertions(+), 19 deletions(-) diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index 78765b2cf..a06a36884 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -467,7 +467,7 @@ async fn token_login( }; let mxid = homeserver.mxid(&browser_session.user.username); homeserver - .create_device(&mxid, device.as_str()) + .create_device(&mxid, device.as_str(), None) .await .map_err(RouteError::ProvisionDeviceFailed)?; @@ -566,7 +566,7 @@ async fn user_password_login( Device::generate(&mut rng) }; homeserver - .create_device(&mxid, device.as_str()) + .create_device(&mxid, device.as_str(), None) .await .map_err(RouteError::ProvisionDeviceFailed)?; diff --git a/crates/handlers/src/graphql/mutations/oauth2_session.rs b/crates/handlers/src/graphql/mutations/oauth2_session.rs index 4278d20a2..058607536 100644 --- a/crates/handlers/src/graphql/mutations/oauth2_session.rs +++ b/crates/handlers/src/graphql/mutations/oauth2_session.rs @@ -168,7 +168,7 @@ impl OAuth2SessionMutations { for scope in &*session.scope { if let Some(device) = Device::from_scope_token(scope) { homeserver - .create_device(&mxid, device.as_str()) + .create_device(&mxid, device.as_str(), None) .await .context("Failed to provision device")?; } diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index a47207d9b..bbc088dfe 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -567,7 +567,7 @@ async fn authorization_code_grant( for scope in &*session.scope { if let Some(device) = Device::from_scope_token(scope) { homeserver - .create_device(&mxid, device.as_str()) + .create_device(&mxid, device.as_str(), None) .await .map_err(RouteError::ProvisionDeviceFailed)?; } @@ -943,7 +943,7 @@ async fn device_code_grant( for scope in &*session.scope { if let Some(device) = Device::from_scope_token(scope) { homeserver - .create_device(&mxid, device.as_str()) + .create_device(&mxid, device.as_str(), None) .await .map_err(RouteError::ProvisionDeviceFailed)?; } diff --git a/crates/matrix-synapse/src/lib.rs b/crates/matrix-synapse/src/lib.rs index 1b6627afe..b1857b7b8 100644 --- a/crates/matrix-synapse/src/lib.rs +++ b/crates/matrix-synapse/src/lib.rs @@ -133,6 +133,11 @@ struct SynapseDevice { dehydrated: Option, } +#[derive(Serialize)] +struct SynapseUpdateDeviceRequest<'a> { + display_name: Option<&'a str>, +} + #[derive(Serialize)] struct SynapseDeleteDevicesRequest { devices: Vec, @@ -312,11 +317,16 @@ impl HomeserverConnection for SynapseConnection { ), err(Debug), )] - async fn create_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> { - let mxid = urlencoding::encode(mxid); + async fn create_device( + &self, + mxid: &str, + device_id: &str, + initial_display_name: Option<&str>, + ) -> Result<(), anyhow::Error> { + let encoded_mxid = urlencoding::encode(mxid); let response = self - .post(&format!("_synapse/admin/v2/users/{mxid}/devices")) + .post(&format!("_synapse/admin/v2/users/{encoded_mxid}/devices")) .json(&SynapseDevice { device_id: device_id.to_owned(), dehydrated: None, @@ -337,6 +347,56 @@ impl HomeserverConnection for SynapseConnection { ); } + // It's annoying, but the POST endpoint doesn't let us set the display name + // of the device, so we have to do it manually. + if let Some(display_name) = initial_display_name { + self.update_device_display_name(mxid, device_id, display_name) + .await?; + } + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.update_device_display_name", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.mxid = mxid, + matrix.device_id = device_id, + ), + err(Debug), + )] + async fn update_device_display_name( + &self, + mxid: &str, + device_id: &str, + display_name: &str, + ) -> Result<(), anyhow::Error> { + let device_id = urlencoding::encode(device_id); + let response = self + .put(&format!( + "_synapse/admin/v2/users/{mxid}/devices/{device_id}" + )) + .json(&SynapseUpdateDeviceRequest { + display_name: Some(display_name), + }) + .send_traced() + .await + .context("Failed to update device display name in Synapse")?; + + let response = response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while updating device display name in Synapse")?; + + if response.status() != StatusCode::OK { + bail!( + "Unexpected HTTP code while updating device display name in Synapse: {}", + response.status() + ); + } + Ok(()) } @@ -448,7 +508,7 @@ impl HomeserverConnection for SynapseConnection { // Then, create the devices that are missing. There is no batching API to do // this, so we do this sequentially, which is fine as the API is idempotent. for device_id in devices.difference(&existing_devices) { - self.create_device(mxid, device_id).await?; + self.create_device(mxid, device_id, None).await?; } Ok(()) diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index 59cdb4880..ae8a4e563 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -254,7 +254,31 @@ pub trait HomeserverConnection: Send + Sync { /// /// Returns an error if the homeserver is unreachable or the device could /// not be created. - async fn create_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error>; + async fn create_device( + &self, + mxid: &str, + device_id: &str, + initial_display_name: Option<&str>, + ) -> Result<(), anyhow::Error>; + + /// Update the display name of a device for a user on the homeserver. + /// + /// # Parameters + /// + /// * `mxid` - The Matrix ID of the user to update a device for. + /// * `device_id` - The device ID to update. + /// * `display_name` - The new display name to set + /// + /// # Errors + /// + /// Returns an error if the homeserver is unreachable or the device could + /// not be updated. + async fn update_device_display_name( + &self, + mxid: &str, + device_id: &str, + display_name: &str, + ) -> Result<(), anyhow::Error>; /// Delete a device for a user on the homeserver. /// @@ -364,8 +388,26 @@ impl HomeserverConnection for &T (**self).is_localpart_available(localpart).await } - async fn create_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> { - (**self).create_device(mxid, device_id).await + async fn create_device( + &self, + mxid: &str, + device_id: &str, + initial_display_name: Option<&str>, + ) -> Result<(), anyhow::Error> { + (**self) + .create_device(mxid, device_id, initial_display_name) + .await + } + + async fn update_device_display_name( + &self, + mxid: &str, + device_id: &str, + display_name: &str, + ) -> Result<(), anyhow::Error> { + (**self) + .update_device_display_name(mxid, device_id, display_name) + .await } async fn delete_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> { @@ -420,8 +462,26 @@ impl HomeserverConnection for Arc { (**self).is_localpart_available(localpart).await } - async fn create_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> { - (**self).create_device(mxid, device_id).await + async fn create_device( + &self, + mxid: &str, + device_id: &str, + initial_display_name: Option<&str>, + ) -> Result<(), anyhow::Error> { + (**self) + .create_device(mxid, device_id, initial_display_name) + .await + } + + async fn update_device_display_name( + &self, + mxid: &str, + device_id: &str, + display_name: &str, + ) -> Result<(), anyhow::Error> { + (**self) + .update_device_display_name(mxid, device_id, display_name) + .await } async fn delete_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> { diff --git a/crates/matrix/src/mock.rs b/crates/matrix/src/mock.rs index 22b9a43d5..7c7973ce0 100644 --- a/crates/matrix/src/mock.rs +++ b/crates/matrix/src/mock.rs @@ -107,13 +107,30 @@ impl crate::HomeserverConnection for HomeserverConnection { Ok(!users.contains_key(&mxid)) } - async fn create_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> { + async fn create_device( + &self, + mxid: &str, + device_id: &str, + _initial_display_name: Option<&str>, + ) -> Result<(), anyhow::Error> { let mut users = self.users.write().await; let user = users.get_mut(mxid).context("User not found")?; user.devices.insert(device_id.to_owned()); Ok(()) } + async fn update_device_display_name( + &self, + mxid: &str, + device_id: &str, + _display_name: &str, + ) -> Result<(), anyhow::Error> { + let mut users = self.users.write().await; + let user = users.get_mut(mxid).context("User not found")?; + user.devices.get(device_id).context("Device not found")?; + Ok(()) + } + async fn delete_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> { let mut users = self.users.write().await; let user = users.get_mut(mxid).context("User not found")?; @@ -191,7 +208,7 @@ mod tests { assert_eq!(conn.mxid("test"), mxid); assert!(conn.query_user(mxid).await.is_err()); - assert!(conn.create_device(mxid, device).await.is_err()); + assert!(conn.create_device(mxid, device, None).await.is_err()); assert!(conn.delete_device(mxid, device).await.is_err()); let request = ProvisionRequest::new("@test:example.org", "test") @@ -222,9 +239,9 @@ mod tests { assert!(conn.delete_device(mxid, device).await.is_ok()); // Create the device - assert!(conn.create_device(mxid, device).await.is_ok()); + assert!(conn.create_device(mxid, device, None).await.is_ok()); // Create the same device again - assert!(conn.create_device(mxid, device).await.is_ok()); + assert!(conn.create_device(mxid, device, None).await.is_ok()); // XXX: there is no API to query devices yet in the trait // Delete the device diff --git a/crates/matrix/src/readonly.rs b/crates/matrix/src/readonly.rs index b51040080..530c3cd89 100644 --- a/crates/matrix/src/readonly.rs +++ b/crates/matrix/src/readonly.rs @@ -40,10 +40,24 @@ impl HomeserverConnection for ReadOnlyHomeserverConnect self.inner.is_localpart_available(localpart).await } - async fn create_device(&self, _mxid: &str, _device_id: &str) -> Result<(), anyhow::Error> { + async fn create_device( + &self, + _mxid: &str, + _device_id: &str, + _initial_display_name: Option<&str>, + ) -> Result<(), anyhow::Error> { anyhow::bail!("Device creation is not supported in read-only mode"); } + async fn update_device_display_name( + &self, + _mxid: &str, + _device_id: &str, + _display_name: &str, + ) -> Result<(), anyhow::Error> { + anyhow::bail!("Device display name update is not supported in read-only mode"); + } + async fn delete_device(&self, _mxid: &str, _device_id: &str) -> Result<(), anyhow::Error> { anyhow::bail!("Device deletion is not supported in read-only mode"); } From bcd83ef649bc911e3fac61459a885a8d15d5c8a3 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 24 Apr 2025 16:46:37 +0200 Subject: [PATCH 085/189] storage: allow setting the human_name when creating compat sessions --- crates/cli/src/commands/manage.rs | 2 +- crates/handlers/src/admin/v1/compat_sessions/get.rs | 2 +- crates/handlers/src/admin/v1/compat_sessions/list.rs | 4 ++-- crates/handlers/src/compat/login.rs | 3 ++- ...48792772b09bac77b09f67e623d5371ab4dadbe2d41fa1c.json} | 7 ++++--- crates/storage-pg/src/app_session.rs | 2 +- crates/storage-pg/src/compat/mod.rs | 9 +++++---- crates/storage-pg/src/compat/session.rs | 9 ++++++--- crates/storage/src/compat/session.rs | 5 +++++ 9 files changed, 27 insertions(+), 16 deletions(-) rename crates/storage-pg/.sqlx/{query-cf1273b8aaaccedeb212a971d5e8e0dd23bfddab0ec08ee192783e103a1c4766.json => query-e99ab37ab3e03ad9c48792772b09bac77b09f67e623d5371ab4dadbe2d41fa1c.json} (56%) diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index ba4541e2f..390897ce7 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -305,7 +305,7 @@ impl Options { let compat_session = repo .compat_session() - .add(&mut rng, &clock, &user, device, None, admin) + .add(&mut rng, &clock, &user, device, None, admin, None) .await?; let token = TokenType::CompatAccessToken.generate(&mut rng); diff --git a/crates/handlers/src/admin/v1/compat_sessions/get.rs b/crates/handlers/src/admin/v1/compat_sessions/get.rs index f39fc79da..d27146d59 100644 --- a/crates/handlers/src/admin/v1/compat_sessions/get.rs +++ b/crates/handlers/src/admin/v1/compat_sessions/get.rs @@ -107,7 +107,7 @@ mod tests { let device = Device::generate(&mut rng); let session = repo .compat_session() - .add(&mut rng, &state.clock, &user, device, None, false) + .add(&mut rng, &state.clock, &user, device, None, false, None) .await .unwrap(); repo.save().await.unwrap(); diff --git a/crates/handlers/src/admin/v1/compat_sessions/list.rs b/crates/handlers/src/admin/v1/compat_sessions/list.rs index a882f6d56..5a47b0571 100644 --- a/crates/handlers/src/admin/v1/compat_sessions/list.rs +++ b/crates/handlers/src/admin/v1/compat_sessions/list.rs @@ -251,7 +251,7 @@ mod tests { let device = Device::generate(&mut rng); repo.compat_session() - .add(&mut rng, &state.clock, &alice, device, None, false) + .add(&mut rng, &state.clock, &alice, device, None, false, None) .await .unwrap(); let device = Device::generate(&mut rng); @@ -260,7 +260,7 @@ mod tests { let session = repo .compat_session() - .add(&mut rng, &state.clock, &bob, device, None, false) + .add(&mut rng, &state.clock, &bob, device, None, false, None) .await .unwrap(); state.clock.advance(Duration::minutes(1)); diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index a06a36884..a1f7873b6 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -484,6 +484,7 @@ async fn token_login( device, Some(&browser_session), false, + None, ) .await?; @@ -576,7 +577,7 @@ async fn user_password_login( let session = repo .compat_session() - .add(&mut rng, clock, &user, device, None, false) + .add(&mut rng, clock, &user, device, None, false, None) .await?; Ok((session, user)) diff --git a/crates/storage-pg/.sqlx/query-cf1273b8aaaccedeb212a971d5e8e0dd23bfddab0ec08ee192783e103a1c4766.json b/crates/storage-pg/.sqlx/query-e99ab37ab3e03ad9c48792772b09bac77b09f67e623d5371ab4dadbe2d41fa1c.json similarity index 56% rename from crates/storage-pg/.sqlx/query-cf1273b8aaaccedeb212a971d5e8e0dd23bfddab0ec08ee192783e103a1c4766.json rename to crates/storage-pg/.sqlx/query-e99ab37ab3e03ad9c48792772b09bac77b09f67e623d5371ab4dadbe2d41fa1c.json index 35f6b5973..04ad6dd39 100644 --- a/crates/storage-pg/.sqlx/query-cf1273b8aaaccedeb212a971d5e8e0dd23bfddab0ec08ee192783e103a1c4766.json +++ b/crates/storage-pg/.sqlx/query-e99ab37ab3e03ad9c48792772b09bac77b09f67e623d5371ab4dadbe2d41fa1c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO compat_sessions\n (compat_session_id, user_id, device_id,\n user_session_id, created_at, is_synapse_admin)\n VALUES ($1, $2, $3, $4, $5, $6)\n ", + "query": "\n INSERT INTO compat_sessions\n (compat_session_id, user_id, device_id,\n user_session_id, created_at, is_synapse_admin,\n human_name)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ", "describe": { "columns": [], "parameters": { @@ -10,10 +10,11 @@ "Text", "Uuid", "Timestamptz", - "Bool" + "Bool", + "Text" ] }, "nullable": [] }, - "hash": "cf1273b8aaaccedeb212a971d5e8e0dd23bfddab0ec08ee192783e103a1c4766" + "hash": "e99ab37ab3e03ad9c48792772b09bac77b09f67e623d5371ab4dadbe2d41fa1c" } diff --git a/crates/storage-pg/src/app_session.rs b/crates/storage-pg/src/app_session.rs index 2c747c8ca..1d759c2ba 100644 --- a/crates/storage-pg/src/app_session.rs +++ b/crates/storage-pg/src/app_session.rs @@ -571,7 +571,7 @@ mod tests { let device = Device::generate(&mut rng); let compat_session = repo .compat_session() - .add(&mut rng, &clock, &user, device.clone(), None, false) + .add(&mut rng, &clock, &user, device.clone(), None, false, None) .await .unwrap(); diff --git a/crates/storage-pg/src/compat/mod.rs b/crates/storage-pg/src/compat/mod.rs index 8ceb089b7..60332fd50 100644 --- a/crates/storage-pg/src/compat/mod.rs +++ b/crates/storage-pg/src/compat/mod.rs @@ -79,7 +79,7 @@ mod tests { let device_str = device.as_str().to_owned(); let session = repo .compat_session() - .add(&mut rng, &clock, &user, device.clone(), None, false) + .add(&mut rng, &clock, &user, device.clone(), None, false, None) .await .unwrap(); assert_eq!(session.user_id, user.id); @@ -227,6 +227,7 @@ mod tests { device, Some(&browser_session), false, + None, ) .await .unwrap(); @@ -331,7 +332,7 @@ mod tests { let device = Device::generate(&mut rng); let session = repo .compat_session() - .add(&mut rng, &clock, &user, device, None, false) + .add(&mut rng, &clock, &user, device, None, false, None) .await .unwrap(); @@ -452,7 +453,7 @@ mod tests { let device = Device::generate(&mut rng); let session = repo .compat_session() - .add(&mut rng, &clock, &user, device, None, false) + .add(&mut rng, &clock, &user, device, None, false, None) .await .unwrap(); @@ -618,7 +619,7 @@ mod tests { let device = Device::generate(&mut rng); let compat_session = repo .compat_session() - .add(&mut rng, &clock, &user, device, None, false) + .add(&mut rng, &clock, &user, device, None, false, None) .await .unwrap(); diff --git a/crates/storage-pg/src/compat/session.rs b/crates/storage-pg/src/compat/session.rs index c844be238..a38b11689 100644 --- a/crates/storage-pg/src/compat/session.rs +++ b/crates/storage-pg/src/compat/session.rs @@ -305,6 +305,7 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> { device: Device, browser_session: Option<&BrowserSession>, is_synapse_admin: bool, + human_name: Option, ) -> Result { let created_at = clock.now(); let id = Ulid::from_datetime_with_source(created_at.into(), rng); @@ -314,8 +315,9 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> { r#" INSERT INTO compat_sessions (compat_session_id, user_id, device_id, - user_session_id, created_at, is_synapse_admin) - VALUES ($1, $2, $3, $4, $5, $6) + user_session_id, created_at, is_synapse_admin, + human_name) + VALUES ($1, $2, $3, $4, $5, $6, $7) "#, Uuid::from(id), Uuid::from(user.id), @@ -323,6 +325,7 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> { browser_session.map(|s| Uuid::from(s.id)), created_at, is_synapse_admin, + human_name.as_deref(), ) .traced() .execute(&mut *self.conn) @@ -333,7 +336,7 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> { state: CompatSessionState::default(), user_id: user.id, device: Some(device), - human_name: None, + human_name, user_session_id: browser_session.map(|s| s.id), created_at, is_synapse_admin, diff --git a/crates/storage/src/compat/session.rs b/crates/storage/src/compat/session.rs index 757f5269b..f8747d754 100644 --- a/crates/storage/src/compat/session.rs +++ b/crates/storage/src/compat/session.rs @@ -215,10 +215,13 @@ pub trait CompatSessionRepository: Send + Sync { /// * `device`: The device ID of this session /// * `browser_session`: The browser session which created this session /// * `is_synapse_admin`: Whether the session is a synapse admin session + /// * `human_name`: The human-readable name of the session provided by the + /// client or the user /// /// # Errors /// /// Returns [`Self::Error`] if the underlying repository fails + #[expect(clippy::too_many_arguments)] async fn add( &mut self, rng: &mut (dyn RngCore + Send), @@ -227,6 +230,7 @@ pub trait CompatSessionRepository: Send + Sync { device: Device, browser_session: Option<&BrowserSession>, is_synapse_admin: bool, + human_name: Option, ) -> Result; /// End a compat session @@ -337,6 +341,7 @@ repository_impl!(CompatSessionRepository: device: Device, browser_session: Option<&BrowserSession>, is_synapse_admin: bool, + human_name: Option, ) -> Result; async fn finish( From badaf35fcf7f85a2affe4935f01f93e209691069 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 24 Apr 2025 16:47:32 +0200 Subject: [PATCH 086/189] frontend: expose the compat session humanName --- .../src/graphql/model/compat_sessions.rs | 5 +++++ frontend/schema.graphql | 4 ++++ frontend/src/components/CompatSession.tsx | 9 ++++++--- .../SessionDetail/CompatSessionDetail.tsx | 17 ++++++++++------- frontend/src/gql/gql.ts | 12 ++++++------ frontend/src/gql/graphql.ts | 10 ++++++++-- 6 files changed, 39 insertions(+), 18 deletions(-) diff --git a/crates/handlers/src/graphql/model/compat_sessions.rs b/crates/handlers/src/graphql/model/compat_sessions.rs index 77ed7e6cc..90adb61fe 100644 --- a/crates/handlers/src/graphql/model/compat_sessions.rs +++ b/crates/handlers/src/graphql/model/compat_sessions.rs @@ -165,6 +165,11 @@ impl CompatSession { pub async fn last_active_at(&self) -> Option> { self.session.last_active_at } + + /// A human-provided name for the session. + pub async fn human_name(&self) -> Option<&str> { + self.session.human_name.as_deref() + } } /// A compat SSO login represents a login done through the legacy Matrix login diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 4fdc85332..3f5dbc8d3 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -370,6 +370,10 @@ type CompatSession implements Node & CreationEvent { The last time the session was active. """ lastActiveAt: DateTime + """ + A human-provided name for the session. + """ + humanName: String } type CompatSessionConnection { diff --git a/frontend/src/components/CompatSession.tsx b/frontend/src/components/CompatSession.tsx index 2ea3fdd60..2770993ad 100644 --- a/frontend/src/components/CompatSession.tsx +++ b/frontend/src/components/CompatSession.tsx @@ -22,6 +22,7 @@ export const FRAGMENT = graphql(/* GraphQL */ ` finishedAt lastActiveIp lastActiveAt + humanName ...EndCompatSessionButton_session userAgent { name @@ -42,9 +43,11 @@ const CompatSession: React.FC<{ const { t } = useTranslation(); const data = useFragment(FRAGMENT, session); - const clientName = data.ssoLogin?.redirectUri - ? simplifyUrl(data.ssoLogin.redirectUri) - : undefined; + const clientName = + data.humanName ?? + (data.ssoLogin?.redirectUri + ? simplifyUrl(data.ssoLogin.redirectUri) + : undefined); const deviceType = data.userAgent?.deviceType ?? "UNKNOWN"; diff --git a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx index 144e7ef37..47ab93d8d 100644 --- a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx +++ b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx @@ -23,6 +23,7 @@ export const FRAGMENT = graphql(/* GraphQL */ ` finishedAt lastActiveIp lastActiveAt + humanName ...EndCompatSessionButton_session @@ -62,11 +63,11 @@ const CompatSessionDetail: React.FC = ({ session }) => { ? simplifyUrl(data.ssoLogin.redirectUri) : data.deviceId || data.id; + const sessionName = data.humanName ?? `${clientName}: ${deviceName}`; + return (
- - {clientName}: {deviceName} - + {sessionName} {t("frontend.session.title")} @@ -141,10 +142,12 @@ const CompatSessionDetail: React.FC = ({ session }) => { {deviceName} - - {t("frontend.session.uri_label")} - {data.ssoLogin?.redirectUri} - + {data.ssoLogin && ( + + {t("frontend.session.uri_label")} + {data.ssoLogin?.redirectUri} + + )} diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index aeb68252f..0bc5e469f 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -21,7 +21,7 @@ type Documents = { "\n fragment PasswordChange_siteConfig on SiteConfig {\n passwordChangeAllowed\n }\n": typeof types.PasswordChange_SiteConfigFragmentDoc, "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n deviceType\n name\n os\n model\n }\n lastActiveAt\n }\n": typeof types.BrowserSession_SessionFragmentDoc, "\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n": typeof types.OAuth2Client_DetailFragmentDoc, - "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_SessionFragmentDoc, + "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_SessionFragmentDoc, "\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": typeof types.Footer_SiteConfigFragmentDoc, "\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": typeof types.FooterDocument, "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": typeof types.OAuth2Session_SessionFragmentDoc, @@ -33,7 +33,7 @@ type Documents = { "\n fragment EndOAuth2SessionButton_session on Oauth2Session {\n id\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": typeof types.EndOAuth2SessionButton_SessionFragmentDoc, "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n": typeof types.EndOAuth2SessionDocument, "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n": typeof types.BrowserSession_DetailFragmentDoc, - "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_DetailFragmentDoc, + "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_DetailFragmentDoc, "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": typeof types.OAuth2Session_DetailFragmentDoc, "\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": typeof types.UserEmail_EmailFragmentDoc, "\n mutation RemoveEmail($id: ID!, $password: String) {\n removeEmail(input: { userEmailId: $id, password: $password }) {\n status\n\n user {\n id\n }\n }\n }\n": typeof types.RemoveEmailDocument, @@ -75,7 +75,7 @@ const documents: Documents = { "\n fragment PasswordChange_siteConfig on SiteConfig {\n passwordChangeAllowed\n }\n": types.PasswordChange_SiteConfigFragmentDoc, "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n deviceType\n name\n os\n model\n }\n lastActiveAt\n }\n": types.BrowserSession_SessionFragmentDoc, "\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n": types.OAuth2Client_DetailFragmentDoc, - "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_SessionFragmentDoc, + "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_SessionFragmentDoc, "\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": types.Footer_SiteConfigFragmentDoc, "\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": types.FooterDocument, "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": types.OAuth2Session_SessionFragmentDoc, @@ -87,7 +87,7 @@ const documents: Documents = { "\n fragment EndOAuth2SessionButton_session on Oauth2Session {\n id\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": types.EndOAuth2SessionButton_SessionFragmentDoc, "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n": types.EndOAuth2SessionDocument, "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n": types.BrowserSession_DetailFragmentDoc, - "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_DetailFragmentDoc, + "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_DetailFragmentDoc, "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": types.OAuth2Session_DetailFragmentDoc, "\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": types.UserEmail_EmailFragmentDoc, "\n mutation RemoveEmail($id: ID!, $password: String) {\n removeEmail(input: { userEmailId: $id, password: $password }) {\n status\n\n user {\n id\n }\n }\n }\n": types.RemoveEmailDocument, @@ -150,7 +150,7 @@ export function graphql(source: "\n fragment OAuth2Client_detail on Oauth2Clien /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_SessionFragmentDoc; +export function graphql(source: "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_SessionFragmentDoc; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -198,7 +198,7 @@ export function graphql(source: "\n fragment BrowserSession_detail on BrowserSe /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_DetailFragmentDoc; +export function graphql(source: "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_DetailFragmentDoc; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index 6bdb8d33f..5cd9d7c8a 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -238,6 +238,8 @@ export type CompatSession = CreationEvent & Node & { deviceId?: Maybe; /** When the session ended. */ finishedAt?: Maybe; + /** A human-provided name for the session. */ + humanName?: Maybe; /** ID of the object. */ id: Scalars['ID']['output']; /** The last time the session was active. */ @@ -1650,7 +1652,7 @@ export type BrowserSession_SessionFragment = ( export type OAuth2Client_DetailFragment = { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, clientUri?: string | null, logoUri?: string | null, tosUri?: string | null, policyUri?: string | null, redirectUris: Array } & { ' $fragmentName'?: 'OAuth2Client_DetailFragment' }; export type CompatSession_SessionFragment = ( - { __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null, deviceType: DeviceType } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null } + { __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, humanName?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null, deviceType: DeviceType } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null } & { ' $fragmentRefs'?: { 'EndCompatSessionButton_SessionFragment': EndCompatSessionButton_SessionFragment } } ) & { ' $fragmentName'?: 'CompatSession_SessionFragment' }; @@ -1704,7 +1706,7 @@ export type BrowserSession_DetailFragment = ( ) & { ' $fragmentName'?: 'BrowserSession_DetailFragment' }; export type CompatSession_DetailFragment = ( - { __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null } + { __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, humanName?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null } & { ' $fragmentRefs'?: { 'EndCompatSessionButton_SessionFragment': EndCompatSessionButton_SessionFragment } } ) & { ' $fragmentName'?: 'CompatSession_DetailFragment' }; @@ -2056,6 +2058,7 @@ export const CompatSession_SessionFragmentDoc = new TypedDocumentString(` finishedAt lastActiveIp lastActiveAt + humanName ...EndCompatSessionButton_session userAgent { name @@ -2183,6 +2186,7 @@ export const CompatSession_DetailFragmentDoc = new TypedDocumentString(` finishedAt lastActiveIp lastActiveAt + humanName ...EndCompatSessionButton_session userAgent { name @@ -2587,6 +2591,7 @@ export const AppSessionsListDocument = new TypedDocumentString(` finishedAt lastActiveIp lastActiveAt + humanName ...EndCompatSessionButton_session userAgent { name @@ -2880,6 +2885,7 @@ fragment CompatSession_detail on CompatSession { finishedAt lastActiveIp lastActiveAt + humanName ...EndCompatSessionButton_session userAgent { name From 75cfff295e456745aadf1191f44bb9de39688f74 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 24 Apr 2025 16:49:56 +0200 Subject: [PATCH 087/189] compat: allow setting an initial_device_display_name on login --- crates/handlers/src/compat/login.rs | 43 ++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index a1f7873b6..76148df75 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -116,6 +116,9 @@ pub struct RequestBody { /// this is not specified. #[serde(default, skip_serializing_if = "Option::is_none")] device_id: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + initial_device_display_name: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -309,18 +312,20 @@ pub(crate) async fn post( user, password, input.device_id, // TODO check for validity + input.initial_device_display_name, ) .await? } (_, Credentials::Token { token }) => { token_login( - &mut repo, + &mut rng, &clock, + &mut repo, + &homeserver, &token, input.device_id, - &homeserver, - &mut rng, + input.initial_device_display_name, ) .await? } @@ -387,12 +392,13 @@ pub(crate) async fn post( } async fn token_login( - repo: &mut BoxRepository, + rng: &mut (dyn RngCore + Send), clock: &dyn Clock, + repo: &mut BoxRepository, + homeserver: &dyn HomeserverConnection, token: &str, requested_device_id: Option, - homeserver: &dyn HomeserverConnection, - rng: &mut (dyn RngCore + Send), + initial_device_display_name: Option, ) -> Result<(CompatSession, User), RouteError> { let login = repo .compat_sso_login() @@ -467,7 +473,11 @@ async fn token_login( }; let mxid = homeserver.mxid(&browser_session.user.username); homeserver - .create_device(&mxid, device.as_str(), None) + .create_device( + &mxid, + device.as_str(), + initial_device_display_name.as_deref(), + ) .await .map_err(RouteError::ProvisionDeviceFailed)?; @@ -484,7 +494,7 @@ async fn token_login( device, Some(&browser_session), false, - None, + initial_device_display_name, ) .await?; @@ -506,6 +516,7 @@ async fn user_password_login( username: String, password: String, requested_device_id: Option, + initial_device_display_name: Option, ) -> Result<(CompatSession, User), RouteError> { // Try getting the localpart out of the MXID let username = homeserver.localpart(&username).unwrap_or(&username); @@ -567,7 +578,11 @@ async fn user_password_login( Device::generate(&mut rng) }; homeserver - .create_device(&mxid, device.as_str(), None) + .create_device( + &mxid, + device.as_str(), + initial_device_display_name.as_deref(), + ) .await .map_err(RouteError::ProvisionDeviceFailed)?; @@ -577,7 +592,15 @@ async fn user_password_login( let session = repo .compat_session() - .add(&mut rng, clock, &user, device, None, false, None) + .add( + &mut rng, + clock, + &user, + device, + None, + false, + initial_device_display_name, + ) .await?; Ok((session, user)) From 234de8b73753785b63f78834fe1566a3c567374d Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 24 Apr 2025 17:19:51 +0200 Subject: [PATCH 088/189] Save the locale detected when starting an authorization grant --- crates/data-model/src/oauth2/authorization_grant.rs | 2 ++ crates/handlers/src/oauth2/authorization/mod.rs | 1 + crates/handlers/src/oauth2/token.rs | 2 ++ ...67d4cdaa59fe609112afbabcbfcc0e7f96c1e531b6567.json} | 5 +++-- ...31ad6c5fabecc18c36d8cdba8db3b47953855fa5c9035.json} | 10 ++++++++-- ...49898125979f3c78c2caca52cb4b8dc9880e669a1f23e.json} | 10 ++++++++-- .../migrations/20250424150930_oauth2_grants_locale.sql | 8 ++++++++ crates/storage-pg/src/oauth2/authorization_grant.rs | 10 +++++++++- crates/storage-pg/src/oauth2/mod.rs | 1 + crates/storage/src/oauth2/authorization_grant.rs | 4 ++++ 10 files changed, 46 insertions(+), 7 deletions(-) rename crates/storage-pg/.sqlx/{query-96208afd8b81e00f2700b0ded40c1eb529d321d0aa543be70f86ef96d0d8ff28.json => query-7a0641df5058927c5cd67d4cdaa59fe609112afbabcbfcc0e7f96c1e531b6567.json} (76%) rename crates/storage-pg/.sqlx/{query-890516adeeaf2d6a4fe83a69e42e18b8817d1bb511e08ee761a031fa12d27251.json => query-8ef27901b96b73826a431ad6c5fabecc18c36d8cdba8db3b47953855fa5c9035.json} (87%) rename crates/storage-pg/.sqlx/{query-bf6d1e3e3145438c988b1a47fc13f0168a63e278d8f8c947cb3ab65173f92ae4.json => query-c960f4f5571ee68816c49898125979f3c78c2caca52cb4b8dc9880e669a1f23e.json} (86%) create mode 100644 crates/storage-pg/migrations/20250424150930_oauth2_grants_locale.sql diff --git a/crates/data-model/src/oauth2/authorization_grant.rs b/crates/data-model/src/oauth2/authorization_grant.rs index 170e476d0..1d71f0170 100644 --- a/crates/data-model/src/oauth2/authorization_grant.rs +++ b/crates/data-model/src/oauth2/authorization_grant.rs @@ -160,6 +160,7 @@ pub struct AuthorizationGrant { pub response_type_id_token: bool, pub created_at: DateTime, pub login_hint: Option, + pub locale: Option, } impl std::ops::Deref for AuthorizationGrant { @@ -263,6 +264,7 @@ impl AuthorizationGrant { response_type_id_token: false, created_at: now, login_hint: Some(String::from("mxid:@example-user:example.com")), + locale: Some(String::from("fr")), } } } diff --git a/crates/handlers/src/oauth2/authorization/mod.rs b/crates/handlers/src/oauth2/authorization/mod.rs index cde23f636..c3b080eae 100644 --- a/crates/handlers/src/oauth2/authorization/mod.rs +++ b/crates/handlers/src/oauth2/authorization/mod.rs @@ -274,6 +274,7 @@ pub(crate) async fn get( response_mode, response_type.has_id_token(), params.auth.login_hint, + Some(locale.to_string()), ) .await?; let continue_grant = PostAuthAction::continue_grant(grant.id); diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index bbc088dfe..08cfcb1d4 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -1042,6 +1042,7 @@ mod tests { ResponseMode::Query, false, None, + None, ) .await .unwrap(); @@ -1141,6 +1142,7 @@ mod tests { ResponseMode::Query, false, None, + None, ) .await .unwrap(); diff --git a/crates/storage-pg/.sqlx/query-96208afd8b81e00f2700b0ded40c1eb529d321d0aa543be70f86ef96d0d8ff28.json b/crates/storage-pg/.sqlx/query-7a0641df5058927c5cd67d4cdaa59fe609112afbabcbfcc0e7f96c1e531b6567.json similarity index 76% rename from crates/storage-pg/.sqlx/query-96208afd8b81e00f2700b0ded40c1eb529d321d0aa543be70f86ef96d0d8ff28.json rename to crates/storage-pg/.sqlx/query-7a0641df5058927c5cd67d4cdaa59fe609112afbabcbfcc0e7f96c1e531b6567.json index 2f372898b..22c3bc0eb 100644 --- a/crates/storage-pg/.sqlx/query-96208afd8b81e00f2700b0ded40c1eb529d321d0aa543be70f86ef96d0d8ff28.json +++ b/crates/storage-pg/.sqlx/query-7a0641df5058927c5cd67d4cdaa59fe609112afbabcbfcc0e7f96c1e531b6567.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO oauth2_authorization_grants (\n oauth2_authorization_grant_id,\n oauth2_client_id,\n redirect_uri,\n scope,\n state,\n nonce,\n response_mode,\n code_challenge,\n code_challenge_method,\n response_type_code,\n response_type_id_token,\n authorization_code,\n login_hint,\n created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n ", + "query": "\n INSERT INTO oauth2_authorization_grants (\n oauth2_authorization_grant_id,\n oauth2_client_id,\n redirect_uri,\n scope,\n state,\n nonce,\n response_mode,\n code_challenge,\n code_challenge_method,\n response_type_code,\n response_type_id_token,\n authorization_code,\n login_hint,\n locale,\n created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)\n ", "describe": { "columns": [], "parameters": { @@ -18,10 +18,11 @@ "Bool", "Text", "Text", + "Text", "Timestamptz" ] }, "nullable": [] }, - "hash": "96208afd8b81e00f2700b0ded40c1eb529d321d0aa543be70f86ef96d0d8ff28" + "hash": "7a0641df5058927c5cd67d4cdaa59fe609112afbabcbfcc0e7f96c1e531b6567" } diff --git a/crates/storage-pg/.sqlx/query-890516adeeaf2d6a4fe83a69e42e18b8817d1bb511e08ee761a031fa12d27251.json b/crates/storage-pg/.sqlx/query-8ef27901b96b73826a431ad6c5fabecc18c36d8cdba8db3b47953855fa5c9035.json similarity index 87% rename from crates/storage-pg/.sqlx/query-890516adeeaf2d6a4fe83a69e42e18b8817d1bb511e08ee761a031fa12d27251.json rename to crates/storage-pg/.sqlx/query-8ef27901b96b73826a431ad6c5fabecc18c36d8cdba8db3b47953855fa5c9035.json index d8fd25487..0a5d83f0a 100644 --- a/crates/storage-pg/.sqlx/query-890516adeeaf2d6a4fe83a69e42e18b8817d1bb511e08ee761a031fa12d27251.json +++ b/crates/storage-pg/.sqlx/query-8ef27901b96b73826a431ad6c5fabecc18c36d8cdba8db3b47953855fa5c9035.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE authorization_code = $1\n ", + "query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , locale\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE authorization_code = $1\n ", "describe": { "columns": [ { @@ -90,6 +90,11 @@ }, { "ordinal": 17, + "name": "locale", + "type_info": "Text" + }, + { + "ordinal": 18, "name": "oauth2_session_id", "type_info": "Uuid" } @@ -117,8 +122,9 @@ true, true, true, + true, true ] }, - "hash": "890516adeeaf2d6a4fe83a69e42e18b8817d1bb511e08ee761a031fa12d27251" + "hash": "8ef27901b96b73826a431ad6c5fabecc18c36d8cdba8db3b47953855fa5c9035" } diff --git a/crates/storage-pg/.sqlx/query-bf6d1e3e3145438c988b1a47fc13f0168a63e278d8f8c947cb3ab65173f92ae4.json b/crates/storage-pg/.sqlx/query-c960f4f5571ee68816c49898125979f3c78c2caca52cb4b8dc9880e669a1f23e.json similarity index 86% rename from crates/storage-pg/.sqlx/query-bf6d1e3e3145438c988b1a47fc13f0168a63e278d8f8c947cb3ab65173f92ae4.json rename to crates/storage-pg/.sqlx/query-c960f4f5571ee68816c49898125979f3c78c2caca52cb4b8dc9880e669a1f23e.json index 7a52e4781..20cd2c704 100644 --- a/crates/storage-pg/.sqlx/query-bf6d1e3e3145438c988b1a47fc13f0168a63e278d8f8c947cb3ab65173f92ae4.json +++ b/crates/storage-pg/.sqlx/query-c960f4f5571ee68816c49898125979f3c78c2caca52cb4b8dc9880e669a1f23e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE oauth2_authorization_grant_id = $1\n ", + "query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , locale\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE oauth2_authorization_grant_id = $1\n ", "describe": { "columns": [ { @@ -90,6 +90,11 @@ }, { "ordinal": 17, + "name": "locale", + "type_info": "Text" + }, + { + "ordinal": 18, "name": "oauth2_session_id", "type_info": "Uuid" } @@ -117,8 +122,9 @@ true, true, true, + true, true ] }, - "hash": "bf6d1e3e3145438c988b1a47fc13f0168a63e278d8f8c947cb3ab65173f92ae4" + "hash": "c960f4f5571ee68816c49898125979f3c78c2caca52cb4b8dc9880e669a1f23e" } diff --git a/crates/storage-pg/migrations/20250424150930_oauth2_grants_locale.sql b/crates/storage-pg/migrations/20250424150930_oauth2_grants_locale.sql new file mode 100644 index 000000000..699f70cf1 --- /dev/null +++ b/crates/storage-pg/migrations/20250424150930_oauth2_grants_locale.sql @@ -0,0 +1,8 @@ +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only +-- Please see LICENSE in the repository root for full details. + +-- Track the locale of the user which asked for the authorization grant +ALTER TABLE oauth2_authorization_grants + ADD COLUMN locale TEXT; diff --git a/crates/storage-pg/src/oauth2/authorization_grant.rs b/crates/storage-pg/src/oauth2/authorization_grant.rs index d619573e7..59c5c2338 100644 --- a/crates/storage-pg/src/oauth2/authorization_grant.rs +++ b/crates/storage-pg/src/oauth2/authorization_grant.rs @@ -52,6 +52,7 @@ struct GrantLookup { code_challenge: Option, code_challenge_method: Option, login_hint: Option, + locale: Option, oauth2_client_id: Uuid, oauth2_session_id: Option, } @@ -162,6 +163,7 @@ impl TryFrom for AuthorizationGrant { created_at: value.created_at, response_type_id_token: value.response_type_id_token, login_hint: value.login_hint, + locale: value.locale, }) } } @@ -194,6 +196,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository response_mode: ResponseMode, response_type_id_token: bool, login_hint: Option, + locale: Option, ) -> Result { let code_challenge = code .as_ref() @@ -225,10 +228,11 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository response_type_id_token, authorization_code, login_hint, + locale, created_at ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) "#, Uuid::from(id), Uuid::from(client.id), @@ -243,6 +247,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository response_type_id_token, code_str, login_hint, + locale, created_at, ) .traced() @@ -262,6 +267,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository created_at, response_type_id_token, login_hint, + locale, }) } @@ -295,6 +301,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository , code_challenge , code_challenge_method , login_hint + , locale , oauth2_session_id FROM oauth2_authorization_grants @@ -344,6 +351,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository , code_challenge , code_challenge_method , login_hint + , locale , oauth2_session_id FROM oauth2_authorization_grants diff --git a/crates/storage-pg/src/oauth2/mod.rs b/crates/storage-pg/src/oauth2/mod.rs index d5e7f7694..3f70fd5cc 100644 --- a/crates/storage-pg/src/oauth2/mod.rs +++ b/crates/storage-pg/src/oauth2/mod.rs @@ -138,6 +138,7 @@ mod tests { ResponseMode::Query, true, None, + None, ) .await .unwrap(); diff --git a/crates/storage/src/oauth2/authorization_grant.rs b/crates/storage/src/oauth2/authorization_grant.rs index 7724ace87..cb4802a92 100644 --- a/crates/storage/src/oauth2/authorization_grant.rs +++ b/crates/storage/src/oauth2/authorization_grant.rs @@ -39,6 +39,8 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync { /// * `response_type_id_token`: Whether the `id_token` `response_type` was /// requested /// * `login_hint`: The login_hint the client sent, if set + /// * `locale`: The locale the detected when the user asked for the + /// authorization grant /// /// # Errors /// @@ -57,6 +59,7 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync { response_mode: ResponseMode, response_type_id_token: bool, login_hint: Option, + locale: Option, ) -> Result; /// Lookup an authorization grant by its ID @@ -140,6 +143,7 @@ repository_impl!(OAuth2AuthorizationGrantRepository: response_mode: ResponseMode, response_type_id_token: bool, login_hint: Option, + locale: Option, ) -> Result; async fn lookup(&mut self, id: Ulid) -> Result, Self::Error>; From c230deaec0ebb477fc950ddf01553407f35784fb Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 25 Apr 2025 09:44:00 +0200 Subject: [PATCH 089/189] templates: introduce a `parse_user_agent` filter and use it in the device consent page --- crates/templates/src/functions.rs | 7 +++++++ templates/pages/device_consent.html | 31 +++++++++++++++-------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/crates/templates/src/functions.rs b/crates/templates/src/functions.rs index edda9783a..3229cde28 100644 --- a/crates/templates/src/functions.rs +++ b/crates/templates/src/functions.rs @@ -40,6 +40,7 @@ pub fn register( env.add_filter("to_params", filter_to_params); env.add_filter("simplify_url", filter_simplify_url); env.add_filter("add_slashes", filter_add_slashes); + env.add_filter("parse_user_agent", filter_parse_user_agent); env.add_function("add_params_to_url", function_add_params_to_url); env.add_function("counter", || Ok(Value::from_object(Counter::default()))); env.add_global( @@ -133,6 +134,12 @@ fn filter_simplify_url(url: &str, kwargs: Kwargs) -> Result Value { + let user_agent = mas_data_model::UserAgent::parse(user_agent); + Value::from_serialize(user_agent) +} + enum ParamsWhere { Fragment, Query, diff --git a/templates/pages/device_consent.html b/templates/pages/device_consent.html index e8abbdb15..abd853976 100644 --- a/templates/pages/device_consent.html +++ b/templates/pages/device_consent.html @@ -12,6 +12,7 @@ Please see LICENSE in the repository root for full details. {% block content %} {% set client_name = client.client_name or client.client_id %} + {% set user_agent = grant.user_agent | parse_user_agent() %} {% if grant.state == "pending" %}
@@ -27,13 +28,13 @@ Please see LICENSE in the repository root for full details.

{{ _("mas.consent.heading") }}

-
+
- {% if grant.user_agent.device_type == "mobile" %} + {% if user_agent.device_type == "mobile" %} {{ icon.mobile() }} - {% elif grant.user_agent.device_type == "tablet" %} + {% elif user_agent.device_type == "tablet" %} {{ icon.web_browser() }} - {% elif grant.user_agent.device_type == "pc" %} + {% elif user_agent.device_type == "pc" %} {{ icon.computer() }} {% else %} {{ icon.unknown_solid() }} @@ -41,31 +42,31 @@ Please see LICENSE in the repository root for full details.
- {% if grant.user_agent.model %} -
{{ grant.user_agent.model }}
+ {% if user_agent.model %} +
{{ user_agent.model }}
{% endif %} - {% if grant.user_agent.os %} + {% if user_agent.os %}
- {{ grant.user_agent.os }} - {% if grant.user_agent.os_version %} - {{ grant.user_agent.os_version }} + {{ user_agent.os }} + {% if user_agent.os_version %} + {{ user_agent.os_version }} {% endif %}
{% endif %} {# If we haven't detected a model, it's probably a browser, so show the name #} - {% if not grant.user_agent.model and grant.user_agent.name %} + {% if not user_agent.model and user_agent.name %}
- {{ grant.user_agent.name }} - {% if grant.user_agent.version %} - {{ grant.user_agent.version }} + {{ user_agent.name }} + {% if user_agent.version %} + {{ user_agent.version }} {% endif %}
{% endif %} {# If we couldn't detect anything, show a generic "Device" #} - {% if not grant.user_agent.model and not grant.user_agent.name and not grant.user_agent.os %} + {% if not user_agent.model and not user_agent.name and not user_agent.os %}
{{ _("mas.device_card.generic_device") }}
{% endif %}
From cd19120b0f4ad00d1511a1d30a5c65e4c0ebee8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 13:57:13 +0000 Subject: [PATCH 090/189] build(deps): bump docker/bake-action from 6.5.0 to 6.6.0 Bumps [docker/bake-action](https://github.com/docker/bake-action) from 6.5.0 to 6.6.0. - [Release notes](https://github.com/docker/bake-action/releases) - [Commits](https://github.com/docker/bake-action/compare/v6.5.0...v6.6.0) --- updated-dependencies: - dependency-name: docker/bake-action dependency-version: 6.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 75b9d9157..bf917432b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -288,7 +288,7 @@ jobs: - name: Build and push id: bake - uses: docker/bake-action@v6.5.0 + uses: docker/bake-action@v6.6.0 with: files: | ./docker-bake.hcl From a10e517d4ea1e95d4d94ab308a7cbb32a069b6f3 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 25 Apr 2025 12:54:11 +0200 Subject: [PATCH 091/189] Generate a device name based on the client name and user agent --- crates/handlers/src/lib.rs | 1 + crates/handlers/src/oauth2/token.rs | 14 +++++++- crates/i18n/src/lib.rs | 2 +- crates/templates/src/context.rs | 33 +++++++++++++++++++ crates/templates/src/lib.rs | 18 +++++++---- templates/device_name.txt | 28 ++++++++++++++++ translations/en.json | 50 +++++++++++++++++++---------- 7 files changed, 120 insertions(+), 26 deletions(-) create mode 100644 templates/device_name.txt diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 4d610482f..6032b7e07 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -203,6 +203,7 @@ where Encrypter: FromRef, reqwest::Client: FromRef, SiteConfig: FromRef, + Templates: FromRef, Arc: FromRef, BoxClock: FromRequestParts, BoxRng: FromRequestParts, diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index 08cfcb1d4..3c8c9db20 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -18,6 +18,7 @@ use mas_axum_utils::{ use mas_data_model::{ AuthorizationGrantStage, Client, Device, DeviceCodeGrantState, SiteConfig, TokenType, }; +use mas_i18n::DataLocale; use mas_keystore::{Encrypter, Keystore}; use mas_matrix::HomeserverConnection; use mas_oidc_client::types::scope::ScopeToken; @@ -31,6 +32,7 @@ use mas_storage::{ }, user::BrowserSessionRepository, }; +use mas_templates::{DeviceNameContext, TemplateContext, Templates}; use oauth2_types::{ errors::{ClientError, ClientErrorCode}, pkce::CodeChallengeError, @@ -261,6 +263,8 @@ impl IntoResponse for RouteError { } } +impl_from_error_for_route!(mas_i18n::DataError); +impl_from_error_for_route!(mas_templates::TemplateError); impl_from_error_for_route!(mas_storage::RepositoryError); impl_from_error_for_route!(mas_policy::EvaluationError); impl_from_error_for_route!(super::IdTokenSignatureError); @@ -281,6 +285,7 @@ pub(crate) async fn post( State(homeserver): State>, State(site_config): State, State(encrypter): State, + State(templates): State, policy: Policy, user_agent: Option>, client_authorization: ClientAuthorization, @@ -334,6 +339,7 @@ pub(crate) async fn post( &site_config, repo, &homeserver, + &templates, user_agent, ) .await? @@ -415,6 +421,7 @@ async fn authorization_code_grant( site_config: &SiteConfig, mut repo: BoxRepository, homeserver: &Arc, + templates: &Templates, user_agent: Option, ) -> Result<(AccessTokenResponse, BoxRepository), RouteError> { // Check that the client is allowed to use this grant type @@ -482,6 +489,11 @@ async fn authorization_code_grant( .await? .ok_or(RouteError::NoSuchOAuthSession(session_id))?; + // Generate a device name + let lang: DataLocale = authz_grant.locale.as_deref().unwrap_or("en").parse()?; + let ctx = DeviceNameContext::new(client.clone(), user_agent.clone()).with_language(lang); + let device_name = templates.render_device_name(&ctx)?; + if let Some(user_agent) = user_agent { session = repo .oauth2_session() @@ -567,7 +579,7 @@ async fn authorization_code_grant( for scope in &*session.scope { if let Some(device) = Device::from_scope_token(scope) { homeserver - .create_device(&mxid, device.as_str(), None) + .create_device(&mxid, device.as_str(), Some(&device_name)) .await .map_err(RouteError::ProvisionDeviceFailed)?; } diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs index 200b5e1ff..44fb06a5e 100644 --- a/crates/i18n/src/lib.rs +++ b/crates/i18n/src/lib.rs @@ -11,7 +11,7 @@ mod translator; pub use icu_calendar; pub use icu_datetime; pub use icu_locid::locale; -pub use icu_provider::DataLocale; +pub use icu_provider::{DataError, DataLocale}; pub use self::{ sprintf::{Argument, ArgumentList, Message}, diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index e1ca20069..345c8bf01 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -1564,6 +1564,39 @@ impl TemplateContext for AccountInactiveContext { } } +/// Context used by the `device_name.txt` template +#[derive(Serialize)] +pub struct DeviceNameContext { + client: Client, + raw_user_agent: String, +} + +impl DeviceNameContext { + /// Constructs a new context with a client and user agent + #[must_use] + pub fn new(client: Client, user_agent: Option) -> Self { + Self { + client, + raw_user_agent: user_agent.unwrap_or_default(), + } + } +} + +impl TemplateContext for DeviceNameContext { + fn sample(now: chrono::DateTime, rng: &mut impl Rng) -> Vec + where + Self: Sized, + { + Client::samples(now, rng) + .into_iter() + .map(|client| DeviceNameContext { + client, + raw_user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned(), + }) + .collect() + } +} + /// Context used by the `form_post.html` template #[derive(Serialize)] pub struct FormPostContext { diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 4c021f87f..c5d0f05e6 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -35,13 +35,13 @@ mod macros; pub use self::{ context::{ AccountInactiveContext, ApiDocContext, AppContext, CompatSsoContext, ConsentContext, - DeviceConsentContext, DeviceLinkContext, DeviceLinkFormField, EmailRecoveryContext, - EmailVerificationContext, EmptyContext, ErrorContext, FormPostContext, IndexContext, - LoginContext, LoginFormField, NotFoundContext, PasswordRegisterContext, - PolicyViolationContext, PostAuthContext, PostAuthContextInner, RecoveryExpiredContext, - RecoveryFinishContext, RecoveryFinishFormField, RecoveryProgressContext, - RecoveryStartContext, RecoveryStartFormField, RegisterContext, RegisterFormField, - RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField, + DeviceConsentContext, DeviceLinkContext, DeviceLinkFormField, DeviceNameContext, + EmailRecoveryContext, EmailVerificationContext, EmptyContext, ErrorContext, + FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext, + PasswordRegisterContext, PolicyViolationContext, PostAuthContext, PostAuthContextInner, + RecoveryExpiredContext, RecoveryFinishContext, RecoveryFinishFormField, + RecoveryProgressContext, RecoveryStartContext, RecoveryStartFormField, RegisterContext, + RegisterFormField, RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField, RegisterStepsEmailInUseContext, RegisterStepsVerifyEmailContext, RegisterStepsVerifyEmailFormField, SiteBranding, SiteConfigExt, SiteFeatures, TemplateContext, UpstreamExistingLinkContext, UpstreamRegister, UpstreamRegisterFormField, @@ -417,6 +417,9 @@ register_templates! { /// Render the 'account logged out' page pub fn render_account_logged_out(WithLanguage>) { "pages/account/logged_out.html" } + + /// Render the automatic device name for OAuth 2.0 client + pub fn render_device_name(WithLanguage) { "device_name.txt" } } impl Templates { @@ -459,6 +462,7 @@ impl Templates { check::render_upstream_oauth2_link_mismatch(self, now, rng)?; check::render_upstream_oauth2_suggest_link(self, now, rng)?; check::render_upstream_oauth2_do_register(self, now, rng)?; + check::render_device_name(self, now, rng)?; Ok(()) } } diff --git a/templates/device_name.txt b/templates/device_name.txt new file mode 100644 index 000000000..2c5e0b16f --- /dev/null +++ b/templates/device_name.txt @@ -0,0 +1,28 @@ +{# +Copyright 2024, 2025 New Vector Ltd. +Copyright 2021-2024 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +-#} + +{%- set _ = translator(lang) -%} + +{%- set client_name = client.client_name or client.client_id -%} +{%- set user_agent = raw_user_agent | parse_user_agent() -%} + +{%- set device_name -%} + {%- if user_agent.model -%} + {{- user_agent.model -}} + {%- elif user_agent.name -%} + {%- if user_agent.os -%} + {{- _("mas.device_display_name.name_for_platform", name=user_agent.name, platform=user_agent.os) -}} + {%- else -%} + {{- user_agent.name -}} + {%- endif -%} + {%- else -%} + {{- _("mas.device_display_name.unknown_device") -}} + {%- endif -%} +{%- endset -%} + +{{- _("mas.device_display_name.client_on_device", client_name=client_name, device_name=device_name) -}} diff --git a/translations/en.json b/translations/en.json index 8c4a76e1c..5b2a5ad04 100644 --- a/translations/en.json +++ b/translations/en.json @@ -6,11 +6,11 @@ }, "cancel": "Cancel", "@cancel": { - "context": "pages/consent.html:69:11-29, pages/device_consent.html:126:13-31, pages/policy_violation.html:44:13-31" + "context": "pages/consent.html:69:11-29, pages/device_consent.html:127:13-31, pages/policy_violation.html:44:13-31" }, "continue": "Continue", "@continue": { - "context": "form_post.html:25:28-48, pages/consent.html:57:28-48, pages/device_consent.html:123:13-33, pages/device_link.html:40:26-46, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:74:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:37:28-48" + "context": "form_post.html:25:28-48, pages/consent.html:57:28-48, pages/device_consent.html:124:13-33, pages/device_link.html:40:26-46, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:74:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:37:28-48" }, "create_account": "Create Account", "@create_account": { @@ -22,7 +22,7 @@ }, "sign_out": "Sign out", "@sign_out": { - "context": "pages/account/logged_out.html:22:28-48, pages/consent.html:65:28-48, pages/device_consent.html:135:30-50, pages/index.html:28:28-48, pages/policy_violation.html:38:28-48, pages/sso.html:45:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46" + "context": "pages/account/logged_out.html:22:28-48, pages/consent.html:65:28-48, pages/device_consent.html:136:30-50, pages/index.html:28:28-48, pages/policy_violation.html:38:28-48, pages/sso.html:45:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46" }, "skip": "Skip", "@skip": { @@ -195,37 +195,37 @@ }, "heading": "Allow access to your account?", "@heading": { - "context": "pages/consent.html:25:27-51, pages/device_consent.html:27:29-53" + "context": "pages/consent.html:25:27-51, pages/device_consent.html:28:29-53" }, "make_sure_you_trust": "Make sure that you trust %(client_name)s.", "@make_sure_you_trust": { - "context": "pages/consent.html:38:81-142, pages/device_consent.html:103:83-144" + "context": "pages/consent.html:38:81-142, pages/device_consent.html:104:83-144" }, "this_will_allow": "This will allow %(client_name)s to:", "@this_will_allow": { - "context": "pages/consent.html:28:11-68, pages/device_consent.html:93:13-70" + "context": "pages/consent.html:28:11-68, pages/device_consent.html:94:13-70" }, "you_may_be_sharing": "You may be sharing sensitive information with this site or app.", "@you_may_be_sharing": { - "context": "pages/consent.html:39:7-42, pages/device_consent.html:104:9-44" + "context": "pages/consent.html:39:7-42, pages/device_consent.html:105:9-44" } }, "device_card": { "access_requested": "Access requested", "@access_requested": { - "context": "pages/device_consent.html:81:34-71" + "context": "pages/device_consent.html:82:34-71" }, "device_code": "Code", "@device_code": { - "context": "pages/device_consent.html:85:34-66" + "context": "pages/device_consent.html:86:34-66" }, "generic_device": "Device", "@generic_device": { - "context": "pages/device_consent.html:69:22-57" + "context": "pages/device_consent.html:70:22-57" }, "ip_address": "IP address", "@ip_address": { - "context": "pages/device_consent.html:76:36-67" + "context": "pages/device_consent.html:77:36-67" } }, "device_code_link": { @@ -241,29 +241,45 @@ "device_consent": { "another_device_access": "Another device wants to access your account.", "@another_device_access": { - "context": "pages/device_consent.html:92:13-58" + "context": "pages/device_consent.html:93:13-58" }, "denied": { "description": "You denied access to %(client_name)s. You can close this window.", "@description": { - "context": "pages/device_consent.html:146:27-94" + "context": "pages/device_consent.html:147:27-94" }, "heading": "Access denied", "@heading": { - "context": "pages/device_consent.html:145:29-67" + "context": "pages/device_consent.html:146:29-67" } }, "granted": { "description": "You granted access to %(client_name)s. You can close this window.", "@description": { - "context": "pages/device_consent.html:157:27-95" + "context": "pages/device_consent.html:158:27-95" }, "heading": "Access granted", "@heading": { - "context": "pages/device_consent.html:156:29-68" + "context": "pages/device_consent.html:157:29-68" } } }, + "device_display_name": { + "client_on_device": "%(client_name)s on %(device_name)s", + "@client_on_device": { + "context": "device_name.txt:28:4-99", + "description": "The automatic device name generated for a client, e.g. 'Element on iPhone'" + }, + "name_for_platform": "%(name)s for %(platform)s", + "@name_for_platform": { + "context": "device_name.txt:19:10-102", + "description": "Part of the automatic device name for the platfom, e.g. 'Safari for macOS'" + }, + "unknown_device": "Unknown device", + "@unknown_device": { + "context": "device_name.txt:24:8-51" + } + }, "email_in_use": { "description": "If you have forgotten your account credentials, you can recover your account. You can also start over and use a different email address.", "@description": { @@ -469,7 +485,7 @@ }, "not_you": "Not %(username)s?", "@not_you": { - "context": "pages/consent.html:62:11-67, pages/device_consent.html:132:13-69, pages/sso.html:42:11-67", + "context": "pages/consent.html:62:11-67, pages/device_consent.html:133:13-69, pages/sso.html:42:11-67", "description": "Suggestions for the user to log in as a different user" }, "or_separator": "Or", From 7ec87b1855a62a1a8cd7ebaefe0f12ebaba414bc Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 25 Apr 2025 13:48:18 +0200 Subject: [PATCH 092/189] storage: add a user-provided human name to OAuth 2.0 sessions --- crates/data-model/src/oauth2/session.rs | 1 + ...46dbb28c11e41d86f22b3fa899a952cad00129e59bee6.json} | 10 ++++++++-- ...57c6bf1ff9c715560d011d4c01112703a9c046170c84f1.json | 2 +- .../20250425113717_oauth2_session_human_name.sql | 8 ++++++++ crates/storage-pg/src/app_session.rs | 6 +++++- crates/storage-pg/src/iden.rs | 1 + crates/storage-pg/src/oauth2/session.rs | 8 ++++++++ 7 files changed, 32 insertions(+), 4 deletions(-) rename crates/storage-pg/.sqlx/{query-5a2e9b5002c1927c0035c22e393172b36ab46a4377b46618205151ea041886d5.json => query-6b8d28b76d7ab33178b46dbb28c11e41d86f22b3fa899a952cad00129e59bee6.json} (82%) create mode 100644 crates/storage-pg/migrations/20250425113717_oauth2_session_human_name.sql diff --git a/crates/data-model/src/oauth2/session.rs b/crates/data-model/src/oauth2/session.rs index 3024aa082..8a55aa863 100644 --- a/crates/data-model/src/oauth2/session.rs +++ b/crates/data-model/src/oauth2/session.rs @@ -83,6 +83,7 @@ pub struct Session { pub user_agent: Option, pub last_active_at: Option>, pub last_active_ip: Option, + pub human_name: Option, } impl std::ops::Deref for Session { diff --git a/crates/storage-pg/.sqlx/query-5a2e9b5002c1927c0035c22e393172b36ab46a4377b46618205151ea041886d5.json b/crates/storage-pg/.sqlx/query-6b8d28b76d7ab33178b46dbb28c11e41d86f22b3fa899a952cad00129e59bee6.json similarity index 82% rename from crates/storage-pg/.sqlx/query-5a2e9b5002c1927c0035c22e393172b36ab46a4377b46618205151ea041886d5.json rename to crates/storage-pg/.sqlx/query-6b8d28b76d7ab33178b46dbb28c11e41d86f22b3fa899a952cad00129e59bee6.json index 5fae1ffab..a7b95fc91 100644 --- a/crates/storage-pg/.sqlx/query-5a2e9b5002c1927c0035c22e393172b36ab46a4377b46618205151ea041886d5.json +++ b/crates/storage-pg/.sqlx/query-6b8d28b76d7ab33178b46dbb28c11e41d86f22b3fa899a952cad00129e59bee6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT oauth2_session_id\n , user_id\n , user_session_id\n , oauth2_client_id\n , scope_list\n , created_at\n , finished_at\n , user_agent\n , last_active_at\n , last_active_ip as \"last_active_ip: IpAddr\"\n FROM oauth2_sessions\n\n WHERE oauth2_session_id = $1\n ", + "query": "\n SELECT oauth2_session_id\n , user_id\n , user_session_id\n , oauth2_client_id\n , scope_list\n , created_at\n , finished_at\n , user_agent\n , last_active_at\n , last_active_ip as \"last_active_ip: IpAddr\"\n , human_name\n FROM oauth2_sessions\n\n WHERE oauth2_session_id = $1\n ", "describe": { "columns": [ { @@ -52,6 +52,11 @@ "ordinal": 9, "name": "last_active_ip: IpAddr", "type_info": "Inet" + }, + { + "ordinal": 10, + "name": "human_name", + "type_info": "Text" } ], "parameters": { @@ -69,8 +74,9 @@ true, true, true, + true, true ] }, - "hash": "5a2e9b5002c1927c0035c22e393172b36ab46a4377b46618205151ea041886d5" + "hash": "6b8d28b76d7ab33178b46dbb28c11e41d86f22b3fa899a952cad00129e59bee6" } diff --git a/crates/storage-pg/.sqlx/query-fcd8b4b9e003d1540357c6bf1ff9c715560d011d4c01112703a9c046170c84f1.json b/crates/storage-pg/.sqlx/query-fcd8b4b9e003d1540357c6bf1ff9c715560d011d4c01112703a9c046170c84f1.json index f5503fa0e..ef1ac0372 100644 --- a/crates/storage-pg/.sqlx/query-fcd8b4b9e003d1540357c6bf1ff9c715560d011d4c01112703a9c046170c84f1.json +++ b/crates/storage-pg/.sqlx/query-fcd8b4b9e003d1540357c6bf1ff9c715560d011d4c01112703a9c046170c84f1.json @@ -23,7 +23,7 @@ "Left": [] }, "nullable": [ - false, + true, true, null ] diff --git a/crates/storage-pg/migrations/20250425113717_oauth2_session_human_name.sql b/crates/storage-pg/migrations/20250425113717_oauth2_session_human_name.sql new file mode 100644 index 000000000..82a07c6d7 --- /dev/null +++ b/crates/storage-pg/migrations/20250425113717_oauth2_session_human_name.sql @@ -0,0 +1,8 @@ +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only +-- Please see LICENSE in the repository root for full details. + +-- Add a user-provided human name to OAuth 2.0 sessions +ALTER TABLE oauth2_sessions + ADD COLUMN human_name TEXT; diff --git a/crates/storage-pg/src/app_session.rs b/crates/storage-pg/src/app_session.rs index 1d759c2ba..cd5e40b53 100644 --- a/crates/storage-pg/src/app_session.rs +++ b/crates/storage-pg/src/app_session.rs @@ -192,6 +192,7 @@ impl TryFrom for AppSession { user_agent, last_active_at, last_active_ip, + human_name, }; Ok(AppSession::OAuth2(Box::new(session))) @@ -299,7 +300,10 @@ impl AppSessionRepository for PgAppSessionRepository<'_> { AppSessionLookupIden::ScopeList, ) .expr_as(Expr::cust("NULL"), AppSessionLookupIden::DeviceId) - .expr_as(Expr::cust("NULL"), AppSessionLookupIden::HumanName) + .expr_as( + Expr::col((OAuth2Sessions::Table, OAuth2Sessions::HumanName)), + AppSessionLookupIden::HumanName, + ) .expr_as( Expr::col((OAuth2Sessions::Table, OAuth2Sessions::CreatedAt)), AppSessionLookupIden::CreatedAt, diff --git a/crates/storage-pg/src/iden.rs b/crates/storage-pg/src/iden.rs index 71e6f7591..d64ce930e 100644 --- a/crates/storage-pg/src/iden.rs +++ b/crates/storage-pg/src/iden.rs @@ -83,6 +83,7 @@ pub enum OAuth2Sessions { UserAgent, LastActiveAt, LastActiveIp, + HumanName, } #[derive(sea_query::Iden)] diff --git a/crates/storage-pg/src/oauth2/session.rs b/crates/storage-pg/src/oauth2/session.rs index b525e22a0..feeb4a49f 100644 --- a/crates/storage-pg/src/oauth2/session.rs +++ b/crates/storage-pg/src/oauth2/session.rs @@ -55,6 +55,7 @@ struct OAuthSessionLookup { user_agent: Option, last_active_at: Option>, last_active_ip: Option, + human_name: Option, } impl TryFrom for Session { @@ -90,6 +91,7 @@ impl TryFrom for Session { user_agent: value.user_agent, last_active_at: value.last_active_at, last_active_ip: value.last_active_ip, + human_name: value.human_name, }) } } @@ -195,6 +197,7 @@ impl OAuth2SessionRepository for PgOAuth2SessionRepository<'_> { , user_agent , last_active_at , last_active_ip as "last_active_ip: IpAddr" + , human_name FROM oauth2_sessions WHERE oauth2_session_id = $1 @@ -270,6 +273,7 @@ impl OAuth2SessionRepository for PgOAuth2SessionRepository<'_> { user_agent: None, last_active_at: None, last_active_ip: None, + human_name: None, }) } @@ -392,6 +396,10 @@ impl OAuth2SessionRepository for PgOAuth2SessionRepository<'_> { Expr::col((OAuth2Sessions::Table, OAuth2Sessions::LastActiveIp)), OAuthSessionLookupIden::LastActiveIp, ) + .expr_as( + Expr::col((OAuth2Sessions::Table, OAuth2Sessions::HumanName)), + OAuthSessionLookupIden::HumanName, + ) .from(OAuth2Sessions::Table) .apply_filter(filter) .generate_pagination( From 79868c3ca38834dd93668526eaa85b2377570151 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 25 Apr 2025 13:51:36 +0200 Subject: [PATCH 093/189] graphql: expose the humanName field on OAuth 2.0 sessions --- crates/handlers/src/graphql/model/oauth.rs | 5 +++++ frontend/schema.graphql | 4 ++++ frontend/src/gql/graphql.ts | 2 ++ 3 files changed, 11 insertions(+) diff --git a/crates/handlers/src/graphql/model/oauth.rs b/crates/handlers/src/graphql/model/oauth.rs index 9c8dc5f1a..9ec94c288 100644 --- a/crates/handlers/src/graphql/model/oauth.rs +++ b/crates/handlers/src/graphql/model/oauth.rs @@ -128,6 +128,11 @@ impl OAuth2Session { pub async fn last_active_at(&self) -> Option> { self.0.last_active_at } + + /// The user-provided name for this session. + pub async fn human_name(&self) -> Option<&str> { + self.0.human_name.as_deref() + } } /// The application type advertised by the client. diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 3f5dbc8d3..43499efbc 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -1064,6 +1064,10 @@ type Oauth2Session implements Node & CreationEvent { The last time the session was active. """ lastActiveAt: DateTime + """ + The user-provided name for this session. + """ + humanName: String } type Oauth2SessionConnection { diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index 5cd9d7c8a..aaba78623 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -776,6 +776,8 @@ export type Oauth2Session = CreationEvent & Node & { createdAt: Scalars['DateTime']['output']; /** When the session ended. */ finishedAt?: Maybe; + /** The user-provided name for this session. */ + humanName?: Maybe; /** ID of the object. */ id: Scalars['ID']['output']; /** The last time the session was active. */ From 5d4c371e53acb829f58e374b84c4d1a5816da9ff Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 25 Apr 2025 13:54:49 +0200 Subject: [PATCH 094/189] admin: expose the sessions 'human_name' --- crates/handlers/src/admin/model.rs | 14 ++++++++ .../src/admin/v1/compat_sessions/get.rs | 7 ++-- .../src/admin/v1/compat_sessions/list.rs | 31 ++++++++++------- .../src/admin/v1/oauth2_sessions/get.rs | 7 ++-- .../src/admin/v1/oauth2_sessions/list.rs | 7 ++-- docs/api/spec.json | 34 ++++++++++++++----- 6 files changed, 70 insertions(+), 30 deletions(-) diff --git a/crates/handlers/src/admin/model.rs b/crates/handlers/src/admin/model.rs index ab3a9d8a6..df17e2d91 100644 --- a/crates/handlers/src/admin/model.rs +++ b/crates/handlers/src/admin/model.rs @@ -184,6 +184,9 @@ pub struct CompatSession { /// The time this session was finished pub finished_at: Option>, + + /// The user-provided name, if any + pub human_name: Option, } impl @@ -210,6 +213,7 @@ impl last_active_at: session.last_active_at, last_active_ip: session.last_active_ip, finished_at, + human_name: session.human_name, } } } @@ -237,6 +241,7 @@ impl CompatSession { last_active_at: Some(DateTime::default()), last_active_ip: Some([1, 2, 3, 4].into()), finished_at: None, + human_name: Some("Laptop".to_owned()), }, Self { id: Ulid::from_bytes([0x02; 16]), @@ -249,6 +254,7 @@ impl CompatSession { last_active_at: Some(DateTime::default()), last_active_ip: Some([1, 2, 3, 4].into()), finished_at: Some(DateTime::default()), + human_name: None, }, Self { id: Ulid::from_bytes([0x03; 16]), @@ -261,6 +267,7 @@ impl CompatSession { last_active_at: None, last_active_ip: None, finished_at: None, + human_name: None, }, ] } @@ -301,6 +308,9 @@ pub struct OAuth2Session { /// The last IP address used by the session last_active_ip: Option, + + /// The user-provided name, if any + human_name: Option, } impl From for OAuth2Session { @@ -316,6 +326,7 @@ impl From for OAuth2Session { user_agent: session.user_agent, last_active_at: session.last_active_at, last_active_ip: session.last_active_ip, + human_name: session.human_name, } } } @@ -335,6 +346,7 @@ impl OAuth2Session { user_agent: Some("Mozilla/5.0".to_owned()), last_active_at: Some(DateTime::default()), last_active_ip: Some("127.0.0.1".parse().unwrap()), + human_name: Some("Laptop".to_owned()), }, Self { id: Ulid::from_bytes([0x02; 16]), @@ -347,6 +359,7 @@ impl OAuth2Session { user_agent: None, last_active_at: None, last_active_ip: None, + human_name: None, }, Self { id: Ulid::from_bytes([0x03; 16]), @@ -359,6 +372,7 @@ impl OAuth2Session { user_agent: Some("Mozilla/5.0".to_owned()), last_active_at: Some(DateTime::default()), last_active_ip: Some("127.0.0.1".parse().unwrap()), + human_name: None, }, ] } diff --git a/crates/handlers/src/admin/v1/compat_sessions/get.rs b/crates/handlers/src/admin/v1/compat_sessions/get.rs index d27146d59..3d471d0ce 100644 --- a/crates/handlers/src/admin/v1/compat_sessions/get.rs +++ b/crates/handlers/src/admin/v1/compat_sessions/get.rs @@ -119,7 +119,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "data": { "type": "compat-session", @@ -133,7 +133,8 @@ mod tests { "user_agent": null, "last_active_at": null, "last_active_ip": null, - "finished_at": null + "finished_at": null, + "human_name": null }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHN9AG0QHEHKX2JNQ2A2D07" @@ -143,7 +144,7 @@ mod tests { "self": "/api/admin/v1/compat-sessions/01FSHN9AG0QHEHKX2JNQ2A2D07" } } - "###); + "#); } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] diff --git a/crates/handlers/src/admin/v1/compat_sessions/list.rs b/crates/handlers/src/admin/v1/compat_sessions/list.rs index 5a47b0571..adf15d190 100644 --- a/crates/handlers/src/admin/v1/compat_sessions/list.rs +++ b/crates/handlers/src/admin/v1/compat_sessions/list.rs @@ -276,7 +276,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 2 @@ -294,7 +294,8 @@ mod tests { "user_agent": null, "last_active_at": null, "last_active_ip": null, - "finished_at": null + "finished_at": null, + "human_name": null }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNB530AAPR7PEV8KNBZD5Y" @@ -312,7 +313,8 @@ mod tests { "user_agent": null, "last_active_at": null, "last_active_ip": null, - "finished_at": "2022-01-16T14:43:00Z" + "finished_at": "2022-01-16T14:43:00Z", + "human_name": null }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNCZP0PPF7X0EVMJNECPZW" @@ -325,7 +327,7 @@ mod tests { "last": "/api/admin/v1/compat-sessions?page[last]=10" } } - "###); + "#); // Filter by user let request = Request::get(format!( @@ -337,7 +339,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -355,7 +357,8 @@ mod tests { "user_agent": null, "last_active_at": null, "last_active_ip": null, - "finished_at": null + "finished_at": null, + "human_name": null }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNB530AAPR7PEV8KNBZD5Y" @@ -368,7 +371,7 @@ mod tests { "last": "/api/admin/v1/compat-sessions?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&page[last]=10" } } - "###); + "#); // Filter by status (active) let request = Request::get("/api/admin/v1/compat-sessions?filter[status]=active") @@ -377,7 +380,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -395,7 +398,8 @@ mod tests { "user_agent": null, "last_active_at": null, "last_active_ip": null, - "finished_at": null + "finished_at": null, + "human_name": null }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNB530AAPR7PEV8KNBZD5Y" @@ -408,7 +412,7 @@ mod tests { "last": "/api/admin/v1/compat-sessions?filter[status]=active&page[last]=10" } } - "###); + "#); // Filter by status (finished) let request = Request::get("/api/admin/v1/compat-sessions?filter[status]=finished") @@ -417,7 +421,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -435,7 +439,8 @@ mod tests { "user_agent": null, "last_active_at": null, "last_active_ip": null, - "finished_at": "2022-01-16T14:43:00Z" + "finished_at": "2022-01-16T14:43:00Z", + "human_name": null }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNCZP0PPF7X0EVMJNECPZW" @@ -448,6 +453,6 @@ mod tests { "last": "/api/admin/v1/compat-sessions?filter[status]=finished&page[last]=10" } } - "###); + "#); } } diff --git a/crates/handlers/src/admin/v1/oauth2_sessions/get.rs b/crates/handlers/src/admin/v1/oauth2_sessions/get.rs index e5e602c62..88f46ecff 100644 --- a/crates/handlers/src/admin/v1/oauth2_sessions/get.rs +++ b/crates/handlers/src/admin/v1/oauth2_sessions/get.rs @@ -110,7 +110,7 @@ mod tests { response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); assert_eq!(body["data"]["type"], "oauth2-session"); - insta::assert_json_snapshot!(body, @r###" + insta::assert_json_snapshot!(body, @r#" { "data": { "type": "oauth2-session", @@ -124,7 +124,8 @@ mod tests { "scope": "urn:mas:admin", "user_agent": null, "last_active_at": null, - "last_active_ip": null + "last_active_ip": null, + "human_name": null }, "links": { "self": "/api/admin/v1/oauth2-sessions/01FSHN9AG0MKGTBNZ16RDR3PVY" @@ -134,7 +135,7 @@ mod tests { "self": "/api/admin/v1/oauth2-sessions/01FSHN9AG0MKGTBNZ16RDR3PVY" } } - "###); + "#); } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] diff --git a/crates/handlers/src/admin/v1/oauth2_sessions/list.rs b/crates/handlers/src/admin/v1/oauth2_sessions/list.rs index 6b75caadd..49b429243 100644 --- a/crates/handlers/src/admin/v1/oauth2_sessions/list.rs +++ b/crates/handlers/src/admin/v1/oauth2_sessions/list.rs @@ -331,7 +331,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - insta::assert_json_snapshot!(body, @r###" + insta::assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -349,7 +349,8 @@ mod tests { "scope": "urn:mas:admin", "user_agent": null, "last_active_at": null, - "last_active_ip": null + "last_active_ip": null, + "human_name": null }, "links": { "self": "/api/admin/v1/oauth2-sessions/01FSHN9AG0MKGTBNZ16RDR3PVY" @@ -362,6 +363,6 @@ mod tests { "last": "/api/admin/v1/oauth2-sessions?page[last]=10" } } - "###); + "#); } } diff --git a/docs/api/spec.json b/docs/api/spec.json index 121022809..1c7be2995 100644 --- a/docs/api/spec.json +++ b/docs/api/spec.json @@ -132,7 +132,8 @@ "user_agent": "Mozilla/5.0", "last_active_at": "1970-01-01T00:00:00Z", "last_active_ip": "1.2.3.4", - "finished_at": null + "finished_at": null, + "human_name": "Laptop" }, "links": { "self": "/api/admin/v1/compat-sessions/01040G2081040G2081040G2081" @@ -150,7 +151,8 @@ "user_agent": "Mozilla/5.0", "last_active_at": "1970-01-01T00:00:00Z", "last_active_ip": "1.2.3.4", - "finished_at": "1970-01-01T00:00:00Z" + "finished_at": "1970-01-01T00:00:00Z", + "human_name": null }, "links": { "self": "/api/admin/v1/compat-sessions/02081040G2081040G2081040G2" @@ -168,7 +170,8 @@ "user_agent": null, "last_active_at": null, "last_active_ip": null, - "finished_at": null + "finished_at": null, + "human_name": null }, "links": { "self": "/api/admin/v1/compat-sessions/030C1G60R30C1G60R30C1G60R3" @@ -245,7 +248,8 @@ "user_agent": "Mozilla/5.0", "last_active_at": "1970-01-01T00:00:00Z", "last_active_ip": "1.2.3.4", - "finished_at": null + "finished_at": null, + "human_name": "Laptop" }, "links": { "self": "/api/admin/v1/compat-sessions/01040G2081040G2081040G2081" @@ -430,7 +434,8 @@ "scope": "openid", "user_agent": "Mozilla/5.0", "last_active_at": "1970-01-01T00:00:00Z", - "last_active_ip": "127.0.0.1" + "last_active_ip": "127.0.0.1", + "human_name": "Laptop" }, "links": { "self": "/api/admin/v1/oauth2-sessions/01040G2081040G2081040G2081" @@ -448,7 +453,8 @@ "scope": "urn:mas:admin", "user_agent": null, "last_active_at": null, - "last_active_ip": null + "last_active_ip": null, + "human_name": null }, "links": { "self": "/api/admin/v1/oauth2-sessions/02081040G2081040G2081040G2" @@ -466,7 +472,8 @@ "scope": "urn:matrix:org.matrix.msc2967.client:api:*", "user_agent": "Mozilla/5.0", "last_active_at": "1970-01-01T00:00:00Z", - "last_active_ip": "127.0.0.1" + "last_active_ip": "127.0.0.1", + "human_name": null }, "links": { "self": "/api/admin/v1/oauth2-sessions/030C1G60R30C1G60R30C1G60R3" @@ -560,7 +567,8 @@ "scope": "openid", "user_agent": "Mozilla/5.0", "last_active_at": "1970-01-01T00:00:00Z", - "last_active_ip": "127.0.0.1" + "last_active_ip": "127.0.0.1", + "human_name": "Laptop" }, "links": { "self": "/api/admin/v1/oauth2-sessions/01040G2081040G2081040G2081" @@ -2726,6 +2734,11 @@ "type": "string", "format": "date-time", "nullable": true + }, + "human_name": { + "description": "The user-provided name, if any", + "type": "string", + "nullable": true } } }, @@ -3001,6 +3014,11 @@ "type": "string", "format": "ip", "nullable": true + }, + "human_name": { + "description": "The user-provided name, if any", + "type": "string", + "nullable": true } } }, From c6075f5cc06f6f22adbc2e4a9fd535705bf2f3e7 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 25 Apr 2025 14:05:18 +0200 Subject: [PATCH 095/189] frontend: display the custom device name on OAuth 2.0 sessions --- frontend/src/components/OAuth2Session.tsx | 2 ++ .../components/SessionDetail/OAuth2SessionDetail.tsx | 2 ++ frontend/src/gql/gql.ts | 12 ++++++------ frontend/src/gql/graphql.ts | 8 ++++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/OAuth2Session.tsx b/frontend/src/components/OAuth2Session.tsx index cc92f26c6..a72fa4aba 100644 --- a/frontend/src/components/OAuth2Session.tsx +++ b/frontend/src/components/OAuth2Session.tsx @@ -16,6 +16,7 @@ export const FRAGMENT = graphql(/* GraphQL */ ` finishedAt lastActiveIp lastActiveAt + humanName ...EndOAuth2SessionButton_session @@ -72,6 +73,7 @@ const OAuth2Session: React.FC = ({ session }) => { const clientName = data.client.clientName || data.client.clientId; const deviceName = + data.humanName ?? data.userAgent?.model ?? (data.userAgent?.name ? data.userAgent?.os diff --git a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx index 656067cd0..7f2f9a94c 100644 --- a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx +++ b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx @@ -23,6 +23,7 @@ export const FRAGMENT = graphql(/* GraphQL */ ` finishedAt lastActiveIp lastActiveAt + humanName ...EndOAuth2SessionButton_session @@ -54,6 +55,7 @@ const OAuth2SessionDetail: React.FC = ({ session }) => { const clientName = data.client.clientName || data.client.clientId; const deviceName = + data.humanName ?? data.userAgent?.model ?? (data.userAgent?.name ? data.userAgent?.os diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index 0bc5e469f..9b9ff86b0 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -24,7 +24,7 @@ type Documents = { "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_SessionFragmentDoc, "\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": typeof types.Footer_SiteConfigFragmentDoc, "\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": typeof types.FooterDocument, - "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": typeof types.OAuth2Session_SessionFragmentDoc, + "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": typeof types.OAuth2Session_SessionFragmentDoc, "\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n": typeof types.PasswordCreationDoubleInput_SiteConfigFragmentDoc, "\n fragment EndBrowserSessionButton_session on BrowserSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n }\n": typeof types.EndBrowserSessionButton_SessionFragmentDoc, "\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n }\n }\n }\n": typeof types.EndBrowserSessionDocument, @@ -34,7 +34,7 @@ type Documents = { "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n": typeof types.EndOAuth2SessionDocument, "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n": typeof types.BrowserSession_DetailFragmentDoc, "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_DetailFragmentDoc, - "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": typeof types.OAuth2Session_DetailFragmentDoc, + "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": typeof types.OAuth2Session_DetailFragmentDoc, "\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": typeof types.UserEmail_EmailFragmentDoc, "\n mutation RemoveEmail($id: ID!, $password: String) {\n removeEmail(input: { userEmailId: $id, password: $password }) {\n status\n\n user {\n id\n }\n }\n }\n": typeof types.RemoveEmailDocument, "\n fragment UserGreeting_user on User {\n id\n matrix {\n mxid\n displayName\n }\n }\n": typeof types.UserGreeting_UserFragmentDoc, @@ -78,7 +78,7 @@ const documents: Documents = { "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_SessionFragmentDoc, "\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": types.Footer_SiteConfigFragmentDoc, "\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": types.FooterDocument, - "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": types.OAuth2Session_SessionFragmentDoc, + "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": types.OAuth2Session_SessionFragmentDoc, "\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n": types.PasswordCreationDoubleInput_SiteConfigFragmentDoc, "\n fragment EndBrowserSessionButton_session on BrowserSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n }\n": types.EndBrowserSessionButton_SessionFragmentDoc, "\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n }\n }\n }\n": types.EndBrowserSessionDocument, @@ -88,7 +88,7 @@ const documents: Documents = { "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n": types.EndOAuth2SessionDocument, "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n": types.BrowserSession_DetailFragmentDoc, "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_DetailFragmentDoc, - "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": types.OAuth2Session_DetailFragmentDoc, + "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": types.OAuth2Session_DetailFragmentDoc, "\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": types.UserEmail_EmailFragmentDoc, "\n mutation RemoveEmail($id: ID!, $password: String) {\n removeEmail(input: { userEmailId: $id, password: $password }) {\n status\n\n user {\n id\n }\n }\n }\n": types.RemoveEmailDocument, "\n fragment UserGreeting_user on User {\n id\n matrix {\n mxid\n displayName\n }\n }\n": types.UserGreeting_UserFragmentDoc, @@ -162,7 +162,7 @@ export function graphql(source: "\n query Footer {\n siteConfig {\n id\ /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_SessionFragmentDoc; +export function graphql(source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_SessionFragmentDoc; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -202,7 +202,7 @@ export function graphql(source: "\n fragment CompatSession_detail on CompatSess /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_DetailFragmentDoc; +export function graphql(source: "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_DetailFragmentDoc; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index aaba78623..2f2d97e4a 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -1669,7 +1669,7 @@ export type FooterQuery = { __typename?: 'Query', siteConfig: ( ) }; export type OAuth2Session_SessionFragment = ( - { __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null, deviceType: DeviceType } | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, applicationType?: Oauth2ApplicationType | null, logoUri?: string | null } } + { __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, humanName?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null, deviceType: DeviceType } | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, applicationType?: Oauth2ApplicationType | null, logoUri?: string | null } } & { ' $fragmentRefs'?: { 'EndOAuth2SessionButton_SessionFragment': EndOAuth2SessionButton_SessionFragment } } ) & { ' $fragmentName'?: 'OAuth2Session_SessionFragment' }; @@ -1713,7 +1713,7 @@ export type CompatSession_DetailFragment = ( ) & { ' $fragmentName'?: 'CompatSession_DetailFragment' }; export type OAuth2Session_DetailFragment = ( - { __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null } | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, clientUri?: string | null, logoUri?: string | null } } + { __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, humanName?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null } | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, clientUri?: string | null, logoUri?: string | null } } & { ' $fragmentRefs'?: { 'EndOAuth2SessionButton_SessionFragment': EndOAuth2SessionButton_SessionFragment } } ) & { ' $fragmentName'?: 'OAuth2Session_DetailFragment' }; @@ -2119,6 +2119,7 @@ export const OAuth2Session_SessionFragmentDoc = new TypedDocumentString(` finishedAt lastActiveIp lastActiveAt + humanName ...EndOAuth2SessionButton_session userAgent { name @@ -2221,6 +2222,7 @@ export const OAuth2Session_DetailFragmentDoc = new TypedDocumentString(` finishedAt lastActiveIp lastActiveAt + humanName ...EndOAuth2SessionButton_session userAgent { name @@ -2613,6 +2615,7 @@ fragment OAuth2Session_session on Oauth2Session { finishedAt lastActiveIp lastActiveAt + humanName ...EndOAuth2SessionButton_session userAgent { name @@ -2906,6 +2909,7 @@ fragment OAuth2Session_detail on Oauth2Session { finishedAt lastActiveIp lastActiveAt + humanName ...EndOAuth2SessionButton_session userAgent { name From 3b9d580b17195e8d4d490d7030f688eaa99a4301 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 25 Apr 2025 14:39:34 +0200 Subject: [PATCH 096/189] storage: methods to set the sessions human name --- ...0ee8fca86b5cdce9320e190e3d3b8fd9f63bc.json | 15 ++++++++ ...320760971317c4519fae7af9d44e2be50985d.json | 15 ++++++++ crates/storage-pg/src/compat/session.rs | 34 +++++++++++++++++++ crates/storage-pg/src/oauth2/session.rs | 34 +++++++++++++++++++ crates/storage/src/compat/session.rs | 22 ++++++++++++ crates/storage/src/oauth2/session.rs | 18 ++++++++++ 6 files changed, 138 insertions(+) create mode 100644 crates/storage-pg/.sqlx/query-8afada5220fefb0d01ed6f87d3d0ee8fca86b5cdce9320e190e3d3b8fd9f63bc.json create mode 100644 crates/storage-pg/.sqlx/query-eb095f64bec5ac885683a8c6708320760971317c4519fae7af9d44e2be50985d.json diff --git a/crates/storage-pg/.sqlx/query-8afada5220fefb0d01ed6f87d3d0ee8fca86b5cdce9320e190e3d3b8fd9f63bc.json b/crates/storage-pg/.sqlx/query-8afada5220fefb0d01ed6f87d3d0ee8fca86b5cdce9320e190e3d3b8fd9f63bc.json new file mode 100644 index 000000000..44352005e --- /dev/null +++ b/crates/storage-pg/.sqlx/query-8afada5220fefb0d01ed6f87d3d0ee8fca86b5cdce9320e190e3d3b8fd9f63bc.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE oauth2_sessions\n SET human_name = $2\n WHERE oauth2_session_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "8afada5220fefb0d01ed6f87d3d0ee8fca86b5cdce9320e190e3d3b8fd9f63bc" +} diff --git a/crates/storage-pg/.sqlx/query-eb095f64bec5ac885683a8c6708320760971317c4519fae7af9d44e2be50985d.json b/crates/storage-pg/.sqlx/query-eb095f64bec5ac885683a8c6708320760971317c4519fae7af9d44e2be50985d.json new file mode 100644 index 000000000..2ebaa4479 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-eb095f64bec5ac885683a8c6708320760971317c4519fae7af9d44e2be50985d.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE compat_sessions\n SET human_name = $2\n WHERE compat_session_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "eb095f64bec5ac885683a8c6708320760971317c4519fae7af9d44e2be50985d" +} diff --git a/crates/storage-pg/src/compat/session.rs b/crates/storage-pg/src/compat/session.rs index a38b11689..5c99f4551 100644 --- a/crates/storage-pg/src/compat/session.rs +++ b/crates/storage-pg/src/compat/session.rs @@ -622,4 +622,38 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> { Ok(compat_session) } + + #[tracing::instrument( + name = "repository.compat_session.set_human_name", + skip(self), + fields( + compat_session.id = %compat_session.id, + compat_session.human_name = ?human_name, + ), + err, + )] + async fn set_human_name( + &mut self, + mut compat_session: CompatSession, + human_name: Option, + ) -> Result { + let res = sqlx::query!( + r#" + UPDATE compat_sessions + SET human_name = $2 + WHERE compat_session_id = $1 + "#, + Uuid::from(compat_session.id), + human_name.as_deref(), + ) + .traced() + .execute(&mut *self.conn) + .await?; + + compat_session.human_name = human_name; + + DatabaseError::ensure_affected_rows(&res, 1)?; + + Ok(compat_session) + } } diff --git a/crates/storage-pg/src/oauth2/session.rs b/crates/storage-pg/src/oauth2/session.rs index feeb4a49f..a6e00545f 100644 --- a/crates/storage-pg/src/oauth2/session.rs +++ b/crates/storage-pg/src/oauth2/session.rs @@ -526,4 +526,38 @@ impl OAuth2SessionRepository for PgOAuth2SessionRepository<'_> { Ok(session) } + + #[tracing::instrument( + name = "repository.oauth2_session.set_human_name", + skip(self), + fields( + client.id = %session.client_id, + session.human_name = ?human_name, + ), + err, + )] + async fn set_human_name( + &mut self, + mut session: Session, + human_name: Option, + ) -> Result { + let res = sqlx::query!( + r#" + UPDATE oauth2_sessions + SET human_name = $2 + WHERE oauth2_session_id = $1 + "#, + Uuid::from(session.id), + human_name.as_deref(), + ) + .traced() + .execute(&mut *self.conn) + .await?; + + session.human_name = human_name; + + DatabaseError::ensure_affected_rows(&res, 1)?; + + Ok(session) + } } diff --git a/crates/storage/src/compat/session.rs b/crates/storage/src/compat/session.rs index f8747d754..e935e986b 100644 --- a/crates/storage/src/compat/session.rs +++ b/crates/storage/src/compat/session.rs @@ -328,6 +328,22 @@ pub trait CompatSessionRepository: Send + Sync { compat_session: CompatSession, user_agent: String, ) -> Result; + + /// Set the human name of a compat session + /// + /// # Parameters + /// + /// * `compat_session`: The compat session to set the human name for + /// * `human_name`: The human name to set + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn set_human_name( + &mut self, + compat_session: CompatSession, + human_name: Option, + ) -> Result; } repository_impl!(CompatSessionRepository: @@ -374,4 +390,10 @@ repository_impl!(CompatSessionRepository: compat_session: CompatSession, user_agent: String, ) -> Result; + + async fn set_human_name( + &mut self, + compat_session: CompatSession, + human_name: Option, + ) -> Result; ); diff --git a/crates/storage/src/oauth2/session.rs b/crates/storage/src/oauth2/session.rs index 7dbba4a03..07f91a2b0 100644 --- a/crates/storage/src/oauth2/session.rs +++ b/crates/storage/src/oauth2/session.rs @@ -430,6 +430,18 @@ pub trait OAuth2SessionRepository: Send + Sync { session: Session, user_agent: String, ) -> Result; + + /// Set the human name of a [`Session`] + /// + /// # Parameters + /// + /// * `session`: The [`Session`] to set the human name for + /// * `human_name`: The human name to set + async fn set_human_name( + &mut self, + session: Session, + human_name: Option, + ) -> Result; } repository_impl!(OAuth2SessionRepository: @@ -489,4 +501,10 @@ repository_impl!(OAuth2SessionRepository: session: Session, user_agent: String, ) -> Result; + + async fn set_human_name( + &mut self, + session: Session, + human_name: Option, + ) -> Result; ); From cf9d4599f9c96c07d9c1307500a5849e88894c4b Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 25 Apr 2025 14:50:17 +0200 Subject: [PATCH 097/189] graphql: add mutation to update device name --- .../src/graphql/mutations/compat_session.rs | 94 ++++++++++++++++++ .../src/graphql/mutations/oauth2_session.rs | 98 +++++++++++++++++++ frontend/schema.graphql | 84 ++++++++++++++++ frontend/src/gql/graphql.ts | 60 ++++++++++++ 4 files changed, 336 insertions(+) diff --git a/crates/handlers/src/graphql/mutations/compat_session.rs b/crates/handlers/src/graphql/mutations/compat_session.rs index 48bf23f81..3930b5670 100644 --- a/crates/handlers/src/graphql/mutations/compat_session.rs +++ b/crates/handlers/src/graphql/mutations/compat_session.rs @@ -64,6 +64,54 @@ impl EndCompatSessionPayload { } } +/// The input of the `setCompatSessionName` mutation. +#[derive(InputObject)] +pub struct SetCompatSessionNameInput { + /// The ID of the session to set the name of. + compat_session_id: ID, + + /// The new name of the session. + human_name: String, +} + +/// The payload of the `setCompatSessionName` mutation. +pub enum SetCompatSessionNamePayload { + /// The session was not found. + NotFound, + + /// The session was updated. + Updated(mas_data_model::CompatSession), +} + +/// The status of the `setCompatSessionName` mutation. +#[derive(Enum, Copy, Clone, PartialEq, Eq, Debug)] +enum SetCompatSessionNameStatus { + /// The session was updated. + Updated, + + /// The session was not found. + NotFound, +} + +#[Object] +impl SetCompatSessionNamePayload { + /// The status of the mutation. + async fn status(&self) -> SetCompatSessionNameStatus { + match self { + Self::Updated(_) => SetCompatSessionNameStatus::Updated, + Self::NotFound => SetCompatSessionNameStatus::NotFound, + } + } + + /// The session that was updated. + async fn oauth2_session(&self) -> Option { + match self { + Self::Updated(session) => Some(CompatSession::new(session.clone())), + Self::NotFound => None, + } + } +} + #[Object] impl CompatSessionMutations { async fn end_compat_session( @@ -105,4 +153,50 @@ impl CompatSessionMutations { Ok(EndCompatSessionPayload::Ended(Box::new(session))) } + + async fn set_compat_session_name( + &self, + ctx: &Context<'_>, + input: SetCompatSessionNameInput, + ) -> Result { + let state = ctx.state(); + let compat_session_id = NodeType::CompatSession.extract_ulid(&input.compat_session_id)?; + let requester = ctx.requester(); + + let mut repo = state.repository().await?; + let homeserver = state.homeserver_connection(); + + let session = repo.compat_session().lookup(compat_session_id).await?; + let Some(session) = session else { + return Ok(SetCompatSessionNamePayload::NotFound); + }; + + if !requester.is_owner_or_admin(&session) { + return Ok(SetCompatSessionNamePayload::NotFound); + } + + let user = repo + .user() + .lookup(session.user_id) + .await? + .context("User not found")?; + + let session = repo + .compat_session() + .set_human_name(session, Some(input.human_name.clone())) + .await?; + + // Update the device on the homeserver side + let mxid = homeserver.mxid(&user.username); + if let Some(device) = session.device.as_ref() { + homeserver + .update_device_display_name(&mxid, device.as_str(), &input.human_name) + .await + .context("Failed to provision device")?; + } + + repo.save().await?; + + Ok(SetCompatSessionNamePayload::Updated(session)) + } } diff --git a/crates/handlers/src/graphql/mutations/oauth2_session.rs b/crates/handlers/src/graphql/mutations/oauth2_session.rs index 058607536..1d0282014 100644 --- a/crates/handlers/src/graphql/mutations/oauth2_session.rs +++ b/crates/handlers/src/graphql/mutations/oauth2_session.rs @@ -110,6 +110,54 @@ impl EndOAuth2SessionPayload { } } +/// The input of the `setOauth2SessionName` mutation. +#[derive(InputObject)] +pub struct SetOAuth2SessionNameInput { + /// The ID of the session to set the name of. + oauth2_session_id: ID, + + /// The new name of the session. + human_name: String, +} + +/// The payload of the `setOauth2SessionName` mutation. +pub enum SetOAuth2SessionNamePayload { + /// The session was not found. + NotFound, + + /// The session was updated. + Updated(mas_data_model::Session), +} + +/// The status of the `setOauth2SessionName` mutation. +#[derive(Enum, Copy, Clone, PartialEq, Eq, Debug)] +enum SetOAuth2SessionNameStatus { + /// The session was updated. + Updated, + + /// The session was not found. + NotFound, +} + +#[Object] +impl SetOAuth2SessionNamePayload { + /// The status of the mutation. + async fn status(&self) -> SetOAuth2SessionNameStatus { + match self { + Self::Updated(_) => SetOAuth2SessionNameStatus::Updated, + Self::NotFound => SetOAuth2SessionNameStatus::NotFound, + } + } + + /// The session that was updated. + async fn oauth2_session(&self) -> Option { + match self { + Self::Updated(session) => Some(OAuth2Session(session.clone())), + Self::NotFound => None, + } + } +} + #[Object] impl OAuth2SessionMutations { /// Create a new arbitrary OAuth 2.0 Session. @@ -247,4 +295,54 @@ impl OAuth2SessionMutations { Ok(EndOAuth2SessionPayload::Ended(session)) } + + async fn set_oauth2_session_name( + &self, + ctx: &Context<'_>, + input: SetOAuth2SessionNameInput, + ) -> Result { + let state = ctx.state(); + let oauth2_session_id = NodeType::OAuth2Session.extract_ulid(&input.oauth2_session_id)?; + let requester = ctx.requester(); + + let mut repo = state.repository().await?; + let homeserver = state.homeserver_connection(); + + let session = repo.oauth2_session().lookup(oauth2_session_id).await?; + let Some(session) = session else { + return Ok(SetOAuth2SessionNamePayload::NotFound); + }; + + if !requester.is_owner_or_admin(&session) { + return Ok(SetOAuth2SessionNamePayload::NotFound); + } + + let user_id = session.user_id.context("Session has no user")?; + + let user = repo + .user() + .lookup(user_id) + .await? + .context("User not found")?; + + let session = repo + .oauth2_session() + .set_human_name(session, Some(input.human_name.clone())) + .await?; + + // Update the device on the homeserver side + let mxid = homeserver.mxid(&user.username); + for scope in &*session.scope { + if let Some(device) = Device::from_scope_token(scope) { + homeserver + .update_device_display_name(&mxid, device.as_str(), &input.human_name) + .await + .context("Failed to provision device")?; + } + } + + repo.save().await?; + + Ok(SetOAuth2SessionNamePayload::Updated(session)) + } } diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 43499efbc..8018e4f5b 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -941,7 +941,13 @@ type Mutation { input: CreateOAuth2SessionInput! ): CreateOAuth2SessionPayload! endOauth2Session(input: EndOAuth2SessionInput!): EndOAuth2SessionPayload! + setOauth2SessionName( + input: SetOAuth2SessionNameInput! + ): SetOAuth2SessionNamePayload! endCompatSession(input: EndCompatSessionInput!): EndCompatSessionPayload! + setCompatSessionName( + input: SetCompatSessionNameInput! + ): SetCompatSessionNamePayload! endBrowserSession(input: EndBrowserSessionInput!): EndBrowserSessionPayload! """ Set the display name of a user @@ -1434,6 +1440,45 @@ type SetCanRequestAdminPayload { user: User } +""" +The input of the `setCompatSessionName` mutation. +""" +input SetCompatSessionNameInput { + """ + The ID of the session to set the name of. + """ + compatSessionId: ID! + """ + The new name of the session. + """ + humanName: String! +} + +type SetCompatSessionNamePayload { + """ + The status of the mutation. + """ + status: SetCompatSessionNameStatus! + """ + The session that was updated. + """ + oauth2Session: CompatSession +} + +""" +The status of the `setCompatSessionName` mutation. +""" +enum SetCompatSessionNameStatus { + """ + The session was updated. + """ + UPDATED + """ + The session was not found. + """ + NOT_FOUND +} + """ The input for the `addEmail` mutation """ @@ -1476,6 +1521,45 @@ enum SetDisplayNameStatus { INVALID } +""" +The input of the `setOauth2SessionName` mutation. +""" +input SetOAuth2SessionNameInput { + """ + The ID of the session to set the name of. + """ + oauth2SessionId: ID! + """ + The new name of the session. + """ + humanName: String! +} + +type SetOAuth2SessionNamePayload { + """ + The status of the mutation. + """ + status: SetOAuth2SessionNameStatus! + """ + The session that was updated. + """ + oauth2Session: Oauth2Session +} + +""" +The status of the `setOauth2SessionName` mutation. +""" +enum SetOAuth2SessionNameStatus { + """ + The session was updated. + """ + UPDATED + """ + The session was not found. + """ + NOT_FOUND +} + """ The input for the `setPasswordByRecovery` mutation. """ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index 2f2d97e4a..d875ef2bf 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -582,8 +582,10 @@ export type Mutation = { * administrators. */ setCanRequestAdmin: SetCanRequestAdminPayload; + setCompatSessionName: SetCompatSessionNamePayload; /** Set the display name of a user */ setDisplayName: SetDisplayNamePayload; + setOauth2SessionName: SetOAuth2SessionNamePayload; /** * Set the password for a user. * @@ -691,12 +693,24 @@ export type MutationSetCanRequestAdminArgs = { }; +/** The mutations root of the GraphQL interface. */ +export type MutationSetCompatSessionNameArgs = { + input: SetCompatSessionNameInput; +}; + + /** The mutations root of the GraphQL interface. */ export type MutationSetDisplayNameArgs = { input: SetDisplayNameInput; }; +/** The mutations root of the GraphQL interface. */ +export type MutationSetOauth2SessionNameArgs = { + input: SetOAuth2SessionNameInput; +}; + + /** The mutations root of the GraphQL interface. */ export type MutationSetPasswordArgs = { input: SetPasswordInput; @@ -1086,6 +1100,29 @@ export type SetCanRequestAdminPayload = { user?: Maybe; }; +/** The input of the `setCompatSessionName` mutation. */ +export type SetCompatSessionNameInput = { + /** The ID of the session to set the name of. */ + compatSessionId: Scalars['ID']['input']; + /** The new name of the session. */ + humanName: Scalars['String']['input']; +}; + +export type SetCompatSessionNamePayload = { + __typename?: 'SetCompatSessionNamePayload'; + /** The session that was updated. */ + oauth2Session?: Maybe; + /** The status of the mutation. */ + status: SetCompatSessionNameStatus; +}; + +/** The status of the `setCompatSessionName` mutation. */ +export type SetCompatSessionNameStatus = + /** The session was not found. */ + | 'NOT_FOUND' + /** The session was updated. */ + | 'UPDATED'; + /** The input for the `addEmail` mutation */ export type SetDisplayNameInput = { /** The display name to set. If `None`, the display name will be removed. */ @@ -1110,6 +1147,29 @@ export type SetDisplayNameStatus = /** The display name was set */ | 'SET'; +/** The input of the `setOauth2SessionName` mutation. */ +export type SetOAuth2SessionNameInput = { + /** The new name of the session. */ + humanName: Scalars['String']['input']; + /** The ID of the session to set the name of. */ + oauth2SessionId: Scalars['ID']['input']; +}; + +export type SetOAuth2SessionNamePayload = { + __typename?: 'SetOAuth2SessionNamePayload'; + /** The session that was updated. */ + oauth2Session?: Maybe; + /** The status of the mutation. */ + status: SetOAuth2SessionNameStatus; +}; + +/** The status of the `setOauth2SessionName` mutation. */ +export type SetOAuth2SessionNameStatus = + /** The session was not found. */ + | 'NOT_FOUND' + /** The session was updated. */ + | 'UPDATED'; + /** The input for the `setPasswordByRecovery` mutation. */ export type SetPasswordByRecoveryInput = { /** The new password for the user. */ From b38b55a805fa66858493fa251bd6dcccdea3c5e3 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 25 Apr 2025 15:35:41 +0200 Subject: [PATCH 098/189] frontend: allow setting custom names to sessions --- frontend/locales/en.json | 5 + .../CompatSessionDetail.test.tsx | 13 +- .../SessionDetail/CompatSessionDetail.tsx | 29 ++++- .../SessionDetail/EditSessionName.tsx | 100 +++++++++++++++ .../OAuth2SessionDetail.test.tsx | 9 +- .../SessionDetail/OAuth2SessionDetail.tsx | 26 ++++ .../CompatSessionDetail.test.tsx.snap | 121 ++++++++++++++---- .../OAuth2SessionDetail.test.tsx.snap | 64 ++++++++- frontend/src/gql/gql.ts | 12 ++ frontend/src/gql/graphql.ts | 78 +++++++++++ 10 files changed, 427 insertions(+), 30 deletions(-) create mode 100644 frontend/src/components/SessionDetail/EditSessionName.tsx diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 7f0343e85..405a4bacf 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -258,6 +258,11 @@ "last_active_label": "Last Active", "name_for_platform": "{{name}} for {{platform}}", "scopes_label": "Scopes", + "set_device_name": { + "help": "Set a name that will help you identify this device.", + "label": "Device name", + "title": "Edit device name" + }, "signed_in_label": "Signed in", "title": "Device details", "unknown_browser": "Unknown browser", diff --git a/frontend/src/components/SessionDetail/CompatSessionDetail.test.tsx b/frontend/src/components/SessionDetail/CompatSessionDetail.test.tsx index 30fb72418..9645c9e71 100644 --- a/frontend/src/components/SessionDetail/CompatSessionDetail.test.tsx +++ b/frontend/src/components/SessionDetail/CompatSessionDetail.test.tsx @@ -6,6 +6,7 @@ // @vitest-environment happy-dom +import { TooltipProvider } from "@vector-im/compound-web"; import { beforeAll, describe, expect, it } from "vitest"; import { makeFragmentData } from "../../gql"; import { mockLocale } from "../../test-utils/mockLocale"; @@ -33,7 +34,9 @@ describe("", () => { const data = makeFragmentData({ ...baseSession }, FRAGMENT); const { container, getByText, queryByText } = render( - , + + + , ); expect(container).toMatchSnapshot(); @@ -51,7 +54,9 @@ describe("", () => { ); const { container, getByText, queryByText } = render( - , + + + , ); expect(container).toMatchSnapshot(); @@ -69,7 +74,9 @@ describe("", () => { ); const { container, getByText, queryByText } = render( - , + + + , ); expect(container).toMatchSnapshot(); diff --git a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx index 47ab93d8d..17101a07b 100644 --- a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx +++ b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx @@ -4,17 +4,28 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { VisualList } from "@vector-im/compound-web"; import { parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; import { type FragmentType, graphql, useFragment } from "../../gql"; +import { graphqlRequest } from "../../graphql"; import simplifyUrl from "../../utils/simplifyUrl"; import DateTime from "../DateTime"; import EndCompatSessionButton from "../Session/EndCompatSessionButton"; import LastActive from "../Session/LastActive"; +import EditSessionName from "./EditSessionName"; import SessionHeader from "./SessionHeader"; import * as Info from "./SessionInfo"; +const SET_SESSION_NAME_MUTATION = graphql(/* GraphQL */ ` + mutation SetCompatSessionName($sessionId: ID!, $displayName: String!) { + setCompatSessionName(input: { compatSessionId: $sessionId, humanName: $displayName }) { + status + } + } +`); + export const FRAGMENT = graphql(/* GraphQL */ ` fragment CompatSession_detail on CompatSession { id @@ -47,6 +58,19 @@ type Props = { const CompatSessionDetail: React.FC = ({ session }) => { const data = useFragment(FRAGMENT, session); const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const setDisplayName = useMutation({ + mutationFn: (displayName: string) => + graphqlRequest({ + query: SET_SESSION_NAME_MUTATION, + variables: { sessionId: data.id, displayName }, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["sessionDetail", data.id] }); + queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); + }, + }); const deviceName = data.userAgent?.model ?? @@ -67,7 +91,10 @@ const CompatSessionDetail: React.FC = ({ session }) => { return (
- {sessionName} + + {sessionName} + + {t("frontend.session.title")} diff --git a/frontend/src/components/SessionDetail/EditSessionName.tsx b/frontend/src/components/SessionDetail/EditSessionName.tsx new file mode 100644 index 000000000..4a57c34e8 --- /dev/null +++ b/frontend/src/components/SessionDetail/EditSessionName.tsx @@ -0,0 +1,100 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +import IconEdit from "@vector-im/compound-design-tokens/assets/web/icons/edit"; +import { Button, Form, IconButton, Tooltip } from "@vector-im/compound-web"; +import { + type ComponentPropsWithoutRef, + forwardRef, + useRef, + useState, +} from "react"; +import * as Dialog from "../Dialog"; +import LoadingSpinner from "../LoadingSpinner"; + +import type { UseMutationResult } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; + +// This needs to be its own component because else props and refs aren't passed properly in the trigger +const EditButton = forwardRef< + HTMLButtonElement, + { label: string } & ComponentPropsWithoutRef<"button"> +>(({ label, ...props }, ref) => ( + + + + + +)); + +type Props = { + mutation: UseMutationResult; + deviceName: string; +}; + +const EditSessionName: React.FC = ({ mutation, deviceName }) => { + const { t } = useTranslation(); + const fieldRef = useRef(null); + const [open, setOpen] = useState(false); + + const onSubmit = async ( + event: React.FormEvent, + ): Promise => { + event.preventDefault(); + + const form = event.currentTarget; + const formData = new FormData(form); + const displayName = formData.get("name") as string; + await mutation.mutateAsync(displayName); + setOpen(false); + }; + return ( + } + open={open} + onOpenChange={(open) => { + // Reset the form when the dialog is opened or closed + fieldRef.current?.form?.reset(); + setOpen(open); + }} + > + {t("frontend.session.set_device_name.title")} + + + + {t("frontend.session.set_device_name.label")} + + + + + {t("frontend.session.set_device_name.help")} + + + + + {mutation.isPending && } + {t("action.save")} + + + + + + + + ); +}; + +export default EditSessionName; diff --git a/frontend/src/components/SessionDetail/OAuth2SessionDetail.test.tsx b/frontend/src/components/SessionDetail/OAuth2SessionDetail.test.tsx index 7f33da2bb..8aa60c6bd 100644 --- a/frontend/src/components/SessionDetail/OAuth2SessionDetail.test.tsx +++ b/frontend/src/components/SessionDetail/OAuth2SessionDetail.test.tsx @@ -11,6 +11,7 @@ import { beforeAll, describe, expect, it } from "vitest"; import { makeFragmentData } from "../../gql"; import { mockLocale } from "../../test-utils/mockLocale"; +import { TooltipProvider } from "@vector-im/compound-web"; import render from "../../test-utils/render"; import OAuth2SessionDetail, { FRAGMENT } from "./OAuth2SessionDetail"; @@ -39,7 +40,9 @@ describe("", () => { const data = makeFragmentData(baseSession, FRAGMENT); const { asFragment, getByText, queryByText } = render( - , + + + , ); expect(asFragment()).toMatchSnapshot(); @@ -57,7 +60,9 @@ describe("", () => { ); const { asFragment, getByText, queryByText } = render( - , + + + , ); expect(asFragment()).toMatchSnapshot(); diff --git a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx index 7f2f9a94c..2dc850d43 100644 --- a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx +++ b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx @@ -4,17 +4,28 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; import { type FragmentType, graphql, useFragment } from "../../gql"; +import { graphqlRequest } from "../../graphql"; import { getDeviceIdFromScope } from "../../utils/deviceIdFromScope"; import DateTime from "../DateTime"; import ClientAvatar from "../Session/ClientAvatar"; import EndOAuth2SessionButton from "../Session/EndOAuth2SessionButton"; import LastActive from "../Session/LastActive"; +import EditSessionName from "./EditSessionName"; import SessionHeader from "./SessionHeader"; import * as Info from "./SessionInfo"; +const SET_SESSION_NAME_MUTATION = graphql(/* GraphQL */ ` + mutation SetOAuth2SessionName($sessionId: ID!, $displayName: String!) { + setOauth2SessionName(input: { oauth2SessionId: $sessionId, humanName: $displayName }) { + status + } + } +`); + export const FRAGMENT = graphql(/* GraphQL */ ` fragment OAuth2Session_detail on Oauth2Session { id @@ -50,6 +61,19 @@ type Props = { const OAuth2SessionDetail: React.FC = ({ session }) => { const data = useFragment(FRAGMENT, session); const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const setDisplayName = useMutation({ + mutationFn: (displayName: string) => + graphqlRequest({ + query: SET_SESSION_NAME_MUTATION, + variables: { sessionId: data.id, displayName }, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["sessionDetail", data.id] }); + queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] }); + }, + }); const deviceId = getDeviceIdFromScope(data.scope); const clientName = data.client.clientName || data.client.clientId; @@ -70,7 +94,9 @@ const OAuth2SessionDetail: React.FC = ({ session }) => {
{clientName}: {deviceName} + + {t("frontend.session.title")} diff --git a/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap b/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap index 2fe6298f3..fbfd98192 100644 --- a/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap +++ b/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap @@ -27,9 +27,38 @@ exports[` > renders a compatability session details 1`] = `

- element.io - : - Unknown device + element.io: Unknown device +

> renders a compatability session details 1`] = `
> renders a compatability session without an ssoL Unknown device

-
  • -
    - Uri -
    -

    -

  • > renders a finished session details 1`] = ` class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112" > Element: Unknown device +
    > renders session details 1`] = ` class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112" > Element: Unknown device +
    > renders session details 1`] = `