Merge remote-tracking branch 'origin/main' into quenting/schemars-0.9
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
[build]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
comment: false
|
||||
|
||||
flag_management:
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
[profile.default]
|
||||
retries = 1
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
target/
|
||||
crates/*/target
|
||||
crates/*/node_modules
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
*.wasm binary
|
||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @element-hq/mas-maintainers
|
||||
7
.github/actions/build-frontend/action.yml
vendored
7
.github/actions/build-frontend/action.yml
vendored
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
name: Build the frontend assets
|
||||
description: Installs Node.js and builds the frontend assets from the frontend directory
|
||||
|
||||
@@ -7,7 +12,7 @@ runs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: '22'
|
||||
node-version: "22"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
8
.github/actions/build-policies/action.yml
vendored
8
.github/actions/build-policies/action.yml
vendored
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
name: Build the Open Policy Agent policies
|
||||
description: Installs OPA and builds the policies
|
||||
|
||||
@@ -7,7 +12,8 @@ runs:
|
||||
- name: Install Open Policy Agent
|
||||
uses: open-policy-agent/setup-opa@v2.2.0
|
||||
with:
|
||||
version: 1.1.0
|
||||
# Keep in sync with the Dockerfile and policies/Makefile
|
||||
version: 1.8.0
|
||||
|
||||
- name: Build the policies
|
||||
run: make
|
||||
|
||||
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
|
||||
5
.github/release.yml
vendored
5
.github/release.yml
vendored
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
changelog:
|
||||
categories:
|
||||
- title: Bug Fixes
|
||||
|
||||
5
.github/scripts/.gitignore
vendored
5
.github/scripts/.gitignore
vendored
@@ -1,2 +1,7 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
4
.github/scripts/cleanup-pr.cjs
vendored
4
.github/scripts/cleanup-pr.cjs
vendored
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
// @ts-check
|
||||
|
||||
|
||||
4
.github/scripts/commit-and-tag.cjs
vendored
4
.github/scripts/commit-and-tag.cjs
vendored
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
// @ts-check
|
||||
|
||||
|
||||
4
.github/scripts/create-release-branch.cjs
vendored
4
.github/scripts/create-release-branch.cjs
vendored
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
// @ts-check
|
||||
|
||||
|
||||
4
.github/scripts/create-version-tag.cjs
vendored
4
.github/scripts/create-version-tag.cjs
vendored
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
// @ts-check
|
||||
|
||||
|
||||
4
.github/scripts/merge-back.cjs
vendored
4
.github/scripts/merge-back.cjs
vendored
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
// @ts-check
|
||||
|
||||
|
||||
4
.github/scripts/update-release-branch.cjs
vendored
4
.github/scripts/update-release-branch.cjs
vendored
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
// @ts-check
|
||||
|
||||
|
||||
4
.github/scripts/update-unstable-tag.cjs
vendored
4
.github/scripts/update-unstable-tag.cjs
vendored
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
// @ts-check
|
||||
|
||||
|
||||
53
.github/workflows/build.yaml
vendored
53
.github/workflows/build.yaml
vendored
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
name: Build
|
||||
|
||||
on:
|
||||
@@ -41,7 +46,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
# Need a full clone so that `git describe` reports the right version
|
||||
fetch-depth: 0
|
||||
@@ -62,7 +67,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- uses: ./.github/actions/build-frontend
|
||||
- uses: ./.github/actions/build-policies
|
||||
@@ -79,7 +84,7 @@ jobs:
|
||||
chmod -R u=rwX,go=rX assets-dist/
|
||||
|
||||
- name: Upload assets
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
uses: actions/upload-artifact@v5.0.0
|
||||
with:
|
||||
name: assets
|
||||
path: assets-dist
|
||||
@@ -107,7 +112,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -138,7 +143,7 @@ jobs:
|
||||
-p mas-cli
|
||||
|
||||
- name: Upload binary artifact
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
uses: actions/upload-artifact@v5.0.0
|
||||
with:
|
||||
name: binary-${{ matrix.target }}
|
||||
path: target/${{ matrix.target }}/release/mas-cli
|
||||
@@ -157,19 +162,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download assets
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: assets
|
||||
path: assets-dist
|
||||
|
||||
- name: Download binary x86_64
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: binary-x86_64-unknown-linux-gnu
|
||||
path: binary-x86_64
|
||||
|
||||
- name: Download binary aarch64
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: binary-aarch64-unknown-linux-gnu
|
||||
path: binary-aarch64
|
||||
@@ -187,13 +192,13 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Upload aarch64 archive
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
uses: actions/upload-artifact@v5.0.0
|
||||
with:
|
||||
name: mas-cli-aarch64-linux
|
||||
path: mas-cli-aarch64-linux.tar.gz
|
||||
|
||||
- name: Upload x86_64 archive
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
uses: actions/upload-artifact@v5.0.0
|
||||
with:
|
||||
name: mas-cli-x86_64-linux
|
||||
path: mas-cli-x86_64-linux.tar.gz
|
||||
@@ -221,7 +226,7 @@ jobs:
|
||||
steps:
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5.7.0
|
||||
uses: docker/metadata-action@v5.9.0
|
||||
with:
|
||||
images: "${{ env.IMAGE }}"
|
||||
bake-target: docker-metadata-action
|
||||
@@ -237,7 +242,7 @@ jobs:
|
||||
|
||||
- name: Docker meta (debug variant)
|
||||
id: meta-debug
|
||||
uses: docker/metadata-action@v5.7.0
|
||||
uses: docker/metadata-action@v5.9.0
|
||||
with:
|
||||
images: "${{ env.IMAGE }}"
|
||||
bake-target: docker-metadata-action-debug
|
||||
@@ -253,17 +258,17 @@ jobs:
|
||||
type=sha
|
||||
|
||||
- name: Setup Cosign
|
||||
uses: sigstore/cosign-installer@v3.8.2
|
||||
uses: sigstore/cosign-installer@v4.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.10.0
|
||||
uses: docker/setup-buildx-action@v3.11.1
|
||||
with:
|
||||
buildkitd-config-inline: |
|
||||
[registry."docker.io"]
|
||||
mirrors = ["mirror.gcr.io"]
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -271,7 +276,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: bake
|
||||
uses: docker/bake-action@v6.8.0
|
||||
uses: docker/bake-action@v6.9.0
|
||||
with:
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
@@ -315,14 +320,14 @@ jobs:
|
||||
- build-image
|
||||
steps:
|
||||
- name: Download the artifacts from the previous job
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: mas-cli-*
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare a release
|
||||
uses: softprops/action-gh-release@v2.3.2
|
||||
uses: softprops/action-gh-release@v2.4.1
|
||||
with:
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
@@ -371,27 +376,27 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
|
||||
- name: Download the artifacts from the previous job
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: mas-cli-*
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Update unstable git tag
|
||||
uses: actions/github-script@v7.0.1
|
||||
uses: actions/github-script@v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/scripts/update-unstable-tag.cjs');
|
||||
await script({ core, github, context });
|
||||
|
||||
- name: Update unstable release
|
||||
uses: softprops/action-gh-release@v2.3.2
|
||||
uses: softprops/action-gh-release@v2.4.1
|
||||
with:
|
||||
name: "Unstable build"
|
||||
tag_name: unstable
|
||||
@@ -449,13 +454,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
|
||||
- name: Remove label and comment
|
||||
uses: actions/github-script@v7.0.1
|
||||
uses: actions/github-script@v8.0.0
|
||||
env:
|
||||
BUILD_IMAGE_MANIFEST: ${{ needs.build-image.outputs.metadata }}
|
||||
with:
|
||||
|
||||
42
.github/workflows/ci.yaml
vendored
42
.github/workflows/ci.yaml
vendored
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
@@ -29,14 +34,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- uses: ./.github/actions/build-policies
|
||||
|
||||
- name: Setup Regal
|
||||
uses: StyraInc/setup-regal@v1
|
||||
with:
|
||||
version: 0.29.2
|
||||
# Keep in sync with policies/Makefile
|
||||
version: 0.36.1
|
||||
|
||||
- name: Lint policies
|
||||
working-directory: ./policies
|
||||
@@ -55,10 +61,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v6.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
@@ -79,10 +85,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v6.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
@@ -103,10 +109,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v6.0.0
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
@@ -127,7 +133,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
@@ -150,10 +156,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Run `cargo-deny`
|
||||
uses: EmbarkStudios/cargo-deny-action@v2.0.11
|
||||
uses: EmbarkStudios/cargo-deny-action@v2.0.13
|
||||
with:
|
||||
rust-version: stable
|
||||
|
||||
@@ -166,7 +172,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust toolchain
|
||||
run: |
|
||||
@@ -207,10 +213,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@1.86.0
|
||||
uses: dtolnay/rust-toolchain@1.89.0
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
@@ -232,7 +238,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -251,7 +257,7 @@ jobs:
|
||||
SQLX_OFFLINE: "1"
|
||||
|
||||
- name: Upload archive to workflow
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
uses: actions/upload-artifact@v5.0.0
|
||||
with:
|
||||
name: nextest-archive
|
||||
path: nextest-archive.tar.zst
|
||||
@@ -285,7 +291,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -299,7 +305,7 @@ jobs:
|
||||
- uses: ./.github/actions/build-policies
|
||||
|
||||
- name: Download archive
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: nextest-archive
|
||||
|
||||
|
||||
17
.github/workflows/coverage.yaml
vendored
17
.github/workflows/coverage.yaml
vendored
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
name: Coverage
|
||||
|
||||
on:
|
||||
@@ -24,7 +29,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- uses: ./.github/actions/build-policies
|
||||
|
||||
@@ -33,7 +38,7 @@ jobs:
|
||||
run: make coverage
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
uses: codecov/codecov-action@v5.5.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: policies/coverage.json
|
||||
@@ -49,7 +54,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- uses: ./.github/actions/build-frontend
|
||||
env:
|
||||
@@ -60,7 +65,7 @@ jobs:
|
||||
run: npm run coverage
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
uses: codecov/codecov-action@v5.5.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
directory: frontend/coverage/
|
||||
@@ -94,7 +99,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -127,7 +132,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.3
|
||||
uses: codecov/codecov-action@v5.5.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: target/coverage/*.lcov
|
||||
|
||||
11
.github/workflows/docs.yaml
vendored
11
.github/workflows/docs.yaml
vendored
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
name: Build and deploy the documentation
|
||||
|
||||
on:
|
||||
@@ -20,7 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -34,7 +39,7 @@ jobs:
|
||||
tool: mdbook
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v6.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
@@ -48,7 +53,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Upload GitHub Pages artifacts
|
||||
uses: actions/upload-pages-artifact@v3.0.1
|
||||
uses: actions/upload-pages-artifact@v4.0.0
|
||||
with:
|
||||
path: target/book/
|
||||
|
||||
|
||||
9
.github/workflows/merge-back.yaml
vendored
9
.github/workflows/merge-back.yaml
vendored
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
name: Merge back a reference to main
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -19,13 +24,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
|
||||
- name: Push branch and open a PR
|
||||
uses: actions/github-script@v7.0.1
|
||||
uses: actions/github-script@v8.0.0
|
||||
env:
|
||||
SHA: ${{ inputs.sha }}
|
||||
with:
|
||||
|
||||
15
.github/workflows/release-branch.yaml
vendored
15
.github/workflows/release-branch.yaml
vendored
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
name: Create a new release branch
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -29,7 +34,7 @@ jobs:
|
||||
run: exit 1
|
||||
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -56,10 +61,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v6.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
@@ -101,13 +106,13 @@ jobs:
|
||||
needs: [tag, compute-version, localazy]
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
|
||||
- name: Create a new release branch
|
||||
uses: actions/github-script@v7.0.1
|
||||
uses: actions/github-script@v8.0.0
|
||||
env:
|
||||
BRANCH: release/v${{ needs.compute-version.outputs.short }}
|
||||
SHA: ${{ needs.tag.outputs.sha }}
|
||||
|
||||
11
.github/workflows/release-bump.yaml
vendored
11
.github/workflows/release-bump.yaml
vendored
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
name: Bump the version on a release branch
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -28,7 +33,7 @@ jobs:
|
||||
run: exit 1
|
||||
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -71,13 +76,13 @@ jobs:
|
||||
needs: [tag, compute-version]
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
|
||||
- name: Update the release branch
|
||||
uses: actions/github-script@v7.0.1
|
||||
uses: actions/github-script@v8.0.0
|
||||
env:
|
||||
BRANCH: "${{ github.ref_name }}"
|
||||
SHA: ${{ needs.tag.outputs.sha }}
|
||||
|
||||
11
.github/workflows/tag.yaml
vendored
11
.github/workflows/tag.yaml
vendored
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
name: Tag a new version
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -25,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -41,7 +46,7 @@ jobs:
|
||||
run: cargo metadata --format-version 1
|
||||
|
||||
- name: Commit and tag using the GitHub API
|
||||
uses: actions/github-script@v7.0.1
|
||||
uses: actions/github-script@v8.0.0
|
||||
id: commit
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
@@ -53,7 +58,7 @@ jobs:
|
||||
return await script({ core, github, context });
|
||||
|
||||
- name: Update the refs
|
||||
uses: actions/github-script@v7.0.1
|
||||
uses: actions/github-script@v8.0.0
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
TAG_SHA: ${{ fromJSON(steps.commit.outputs.result).tag }}
|
||||
|
||||
9
.github/workflows/translations-download.yaml
vendored
9
.github/workflows/translations-download.yaml
vendored
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
name: Download translation files from Localazy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -14,10 +19,10 @@ jobs:
|
||||
run: exit 1
|
||||
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v6.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
|
||||
9
.github/workflows/translations-upload.yaml
vendored
9
.github/workflows/translations-upload.yaml
vendored
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
name: Upload translation files to Localazy
|
||||
on:
|
||||
push:
|
||||
@@ -13,10 +18,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v6.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,5 +1,10 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Rust
|
||||
target/
|
||||
target
|
||||
|
||||
# Editors
|
||||
.idea
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
max_width = 100
|
||||
comment_width = 80
|
||||
wrap_comments = true
|
||||
|
||||
2075
Cargo.lock
generated
2075
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
204
Cargo.toml
204
Cargo.toml
@@ -1,11 +1,16 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
[workspace]
|
||||
default-members = ["crates/cli"]
|
||||
members = ["crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
# Updated in the CI with a `sed` command
|
||||
package.version = "0.17.0-rc.0"
|
||||
package.license = "AGPL-3.0-only"
|
||||
package.version = "1.6.0-rc.0"
|
||||
package.license = "AGPL-3.0-only OR LicenseRef-Element-Commercial"
|
||||
package.authors = ["Element Backend Team"]
|
||||
package.edition = "2024"
|
||||
package.homepage = "https://element-hq.github.io/matrix-authentication-service/"
|
||||
@@ -21,6 +26,7 @@ all = { level = "deny", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
|
||||
str_to_string = "deny"
|
||||
too_many_lines = "allow"
|
||||
|
||||
[workspace.lints.rustdoc]
|
||||
broken_intra_doc_links = "deny"
|
||||
@@ -28,40 +34,40 @@ broken_intra_doc_links = "deny"
|
||||
[workspace.dependencies]
|
||||
|
||||
# Workspace crates
|
||||
mas-axum-utils = { path = "./crates/axum-utils/", version = "=0.17.0-rc.0" }
|
||||
mas-cli = { path = "./crates/cli/", version = "=0.17.0-rc.0" }
|
||||
mas-config = { path = "./crates/config/", version = "=0.17.0-rc.0" }
|
||||
mas-context = { path = "./crates/context/", version = "=0.17.0-rc.0" }
|
||||
mas-data-model = { path = "./crates/data-model/", version = "=0.17.0-rc.0" }
|
||||
mas-email = { path = "./crates/email/", version = "=0.17.0-rc.0" }
|
||||
mas-graphql = { path = "./crates/graphql/", version = "=0.17.0-rc.0" }
|
||||
mas-handlers = { path = "./crates/handlers/", version = "=0.17.0-rc.0" }
|
||||
mas-http = { path = "./crates/http/", version = "=0.17.0-rc.0" }
|
||||
mas-i18n = { path = "./crates/i18n/", version = "=0.17.0-rc.0" }
|
||||
mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=0.17.0-rc.0" }
|
||||
mas-iana = { path = "./crates/iana/", version = "=0.17.0-rc.0" }
|
||||
mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=0.17.0-rc.0" }
|
||||
mas-jose = { path = "./crates/jose/", version = "=0.17.0-rc.0" }
|
||||
mas-keystore = { path = "./crates/keystore/", version = "=0.17.0-rc.0" }
|
||||
mas-listener = { path = "./crates/listener/", version = "=0.17.0-rc.0" }
|
||||
mas-matrix = { path = "./crates/matrix/", version = "=0.17.0-rc.0" }
|
||||
mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=0.17.0-rc.0" }
|
||||
mas-oidc-client = { path = "./crates/oidc-client/", version = "=0.17.0-rc.0" }
|
||||
mas-policy = { path = "./crates/policy/", version = "=0.17.0-rc.0" }
|
||||
mas-router = { path = "./crates/router/", version = "=0.17.0-rc.0" }
|
||||
mas-spa = { path = "./crates/spa/", version = "=0.17.0-rc.0" }
|
||||
mas-storage = { path = "./crates/storage/", version = "=0.17.0-rc.0" }
|
||||
mas-storage-pg = { path = "./crates/storage-pg/", version = "=0.17.0-rc.0" }
|
||||
mas-tasks = { path = "./crates/tasks/", version = "=0.17.0-rc.0" }
|
||||
mas-templates = { path = "./crates/templates/", version = "=0.17.0-rc.0" }
|
||||
mas-tower = { path = "./crates/tower/", version = "=0.17.0-rc.0" }
|
||||
oauth2-types = { path = "./crates/oauth2-types/", version = "=0.17.0-rc.0" }
|
||||
syn2mas = { path = "./crates/syn2mas", version = "=0.17.0-rc.0" }
|
||||
mas-axum-utils = { path = "./crates/axum-utils/", version = "=1.6.0-rc.0" }
|
||||
mas-cli = { path = "./crates/cli/", version = "=1.6.0-rc.0" }
|
||||
mas-config = { path = "./crates/config/", version = "=1.6.0-rc.0" }
|
||||
mas-context = { path = "./crates/context/", version = "=1.6.0-rc.0" }
|
||||
mas-data-model = { path = "./crates/data-model/", version = "=1.6.0-rc.0" }
|
||||
mas-email = { path = "./crates/email/", version = "=1.6.0-rc.0" }
|
||||
mas-graphql = { path = "./crates/graphql/", version = "=1.6.0-rc.0" }
|
||||
mas-handlers = { path = "./crates/handlers/", version = "=1.6.0-rc.0" }
|
||||
mas-http = { path = "./crates/http/", version = "=1.6.0-rc.0" }
|
||||
mas-i18n = { path = "./crates/i18n/", version = "=1.6.0-rc.0" }
|
||||
mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=1.6.0-rc.0" }
|
||||
mas-iana = { path = "./crates/iana/", version = "=1.6.0-rc.0" }
|
||||
mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=1.6.0-rc.0" }
|
||||
mas-jose = { path = "./crates/jose/", version = "=1.6.0-rc.0" }
|
||||
mas-keystore = { path = "./crates/keystore/", version = "=1.6.0-rc.0" }
|
||||
mas-listener = { path = "./crates/listener/", version = "=1.6.0-rc.0" }
|
||||
mas-matrix = { path = "./crates/matrix/", version = "=1.6.0-rc.0" }
|
||||
mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=1.6.0-rc.0" }
|
||||
mas-oidc-client = { path = "./crates/oidc-client/", version = "=1.6.0-rc.0" }
|
||||
mas-policy = { path = "./crates/policy/", version = "=1.6.0-rc.0" }
|
||||
mas-router = { path = "./crates/router/", version = "=1.6.0-rc.0" }
|
||||
mas-spa = { path = "./crates/spa/", version = "=1.6.0-rc.0" }
|
||||
mas-storage = { path = "./crates/storage/", version = "=1.6.0-rc.0" }
|
||||
mas-storage-pg = { path = "./crates/storage-pg/", version = "=1.6.0-rc.0" }
|
||||
mas-tasks = { path = "./crates/tasks/", version = "=1.6.0-rc.0" }
|
||||
mas-templates = { path = "./crates/templates/", version = "=1.6.0-rc.0" }
|
||||
mas-tower = { path = "./crates/tower/", version = "=1.6.0-rc.0" }
|
||||
oauth2-types = { path = "./crates/oauth2-types/", version = "=1.6.0-rc.0" }
|
||||
syn2mas = { path = "./crates/syn2mas", version = "=1.6.0-rc.0" }
|
||||
|
||||
# OpenAPI schema generation and validation
|
||||
[workspace.dependencies.aide]
|
||||
version = "0.15.0"
|
||||
features = ["axum", "axum-extra", "axum-json", "axum-query", "macros"]
|
||||
features = ["axum", "axum-extra", "axum-extra-query", "axum-json", "macros"]
|
||||
|
||||
# An `Arc` that can be atomically updated
|
||||
[workspace.dependencies.arc-swap]
|
||||
@@ -78,11 +84,11 @@ version = "0.3.6"
|
||||
|
||||
# Utility to write and implement async traits
|
||||
[workspace.dependencies.async-trait]
|
||||
version = "0.1.88"
|
||||
version = "0.1.89"
|
||||
|
||||
# High-level error handling
|
||||
[workspace.dependencies.anyhow]
|
||||
version = "1.0.98"
|
||||
version = "1.0.100"
|
||||
|
||||
# Assert that a value matches a pattern
|
||||
[workspace.dependencies.assert_matches]
|
||||
@@ -90,12 +96,12 @@ version = "1.5.0"
|
||||
|
||||
# HTTP router
|
||||
[workspace.dependencies.axum]
|
||||
version = "0.8.4"
|
||||
version = "0.8.6"
|
||||
|
||||
# Extra utilities for Axum
|
||||
[workspace.dependencies.axum-extra]
|
||||
version = "0.10.1"
|
||||
features = ["cookie-private", "cookie-key-expansion", "typed-header"]
|
||||
version = "0.10.3"
|
||||
features = ["cookie-private", "cookie-key-expansion", "typed-header", "query"]
|
||||
|
||||
# Axum macros
|
||||
[workspace.dependencies.axum-macros]
|
||||
@@ -118,12 +124,12 @@ features = ["std"]
|
||||
|
||||
# Bcrypt password hashing
|
||||
[workspace.dependencies.bcrypt]
|
||||
version = "0.17.0"
|
||||
version = "0.17.1"
|
||||
default-features = true
|
||||
|
||||
# Packed bitfields
|
||||
[workspace.dependencies.bitflags]
|
||||
version = "2.9.1"
|
||||
version = "2.9.4"
|
||||
|
||||
# Bytes
|
||||
[workspace.dependencies.bytes]
|
||||
@@ -131,7 +137,7 @@ version = "1.10.1"
|
||||
|
||||
# UTF-8 paths
|
||||
[workspace.dependencies.camino]
|
||||
version = "1.1.10"
|
||||
version = "1.2.1"
|
||||
features = ["serde1"]
|
||||
|
||||
# ChaCha20Poly1305 AEAD
|
||||
@@ -149,19 +155,19 @@ version = "0.15.11"
|
||||
|
||||
# Cookie store
|
||||
[workspace.dependencies.cookie_store]
|
||||
version = "0.21.1"
|
||||
version = "0.22.0"
|
||||
default-features = false
|
||||
features = ["serde_json"]
|
||||
|
||||
# Time utilities
|
||||
[workspace.dependencies.chrono]
|
||||
version = "0.4.41"
|
||||
version = "0.4.42"
|
||||
default-features = false
|
||||
features = ["serde", "clock"]
|
||||
|
||||
# CLI argument parsing
|
||||
[workspace.dependencies.clap]
|
||||
version = "4.5.40"
|
||||
version = "4.5.50"
|
||||
features = ["derive"]
|
||||
|
||||
# Object Identifiers (OIDs) as constants
|
||||
@@ -221,7 +227,7 @@ features = ["env", "yaml", "test"]
|
||||
|
||||
# URL form encoding
|
||||
[workspace.dependencies.form_urlencoded]
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
|
||||
# Utilities for dealing with futures
|
||||
[workspace.dependencies.futures-util]
|
||||
@@ -233,7 +239,7 @@ version = "0.14.7"
|
||||
|
||||
# Rate-limiting
|
||||
[workspace.dependencies.governor]
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
default-features = false
|
||||
features = ["std", "dashmap", "quanta"]
|
||||
|
||||
@@ -263,12 +269,12 @@ version = "0.1.3"
|
||||
|
||||
# HTTP client and server
|
||||
[workspace.dependencies.hyper]
|
||||
version = "1.6.0"
|
||||
version = "1.7.0"
|
||||
features = ["client", "server", "http1", "http2"]
|
||||
|
||||
# Additional Hyper utilties
|
||||
[workspace.dependencies.hyper-util]
|
||||
version = "0.1.14"
|
||||
version = "0.1.17"
|
||||
features = [
|
||||
"client",
|
||||
"server",
|
||||
@@ -315,7 +321,7 @@ features = ["std"]
|
||||
|
||||
# HashMap which preserves insertion order
|
||||
[workspace.dependencies.indexmap]
|
||||
version = "2.9.0"
|
||||
version = "2.11.4"
|
||||
features = ["serde"]
|
||||
|
||||
# Indented string literals
|
||||
@@ -324,7 +330,7 @@ version = "2.0.6"
|
||||
|
||||
# Snapshot testing
|
||||
[workspace.dependencies.insta]
|
||||
version = "1.43.1"
|
||||
version = "1.43.2"
|
||||
features = ["yaml", "json"]
|
||||
|
||||
# IP network address types
|
||||
@@ -348,10 +354,12 @@ features = ["serde"]
|
||||
|
||||
# Email sending
|
||||
[workspace.dependencies.lettre]
|
||||
version = "0.11.15"
|
||||
version = "0.11.19"
|
||||
default-features = false
|
||||
features = [
|
||||
"tokio1-rustls-tls",
|
||||
"tokio1-rustls",
|
||||
"rustls-platform-verifier",
|
||||
"aws-lc-rs",
|
||||
"hostname",
|
||||
"builder",
|
||||
"tracing",
|
||||
@@ -370,12 +378,12 @@ version = "0.3.17"
|
||||
|
||||
# Templates
|
||||
[workspace.dependencies.minijinja]
|
||||
version = "2.10.2"
|
||||
features = ["loader", "json", "speedups", "unstable_machinery"]
|
||||
version = "2.12.0"
|
||||
features = ["urlencode", "loader", "json", "speedups", "unstable_machinery"]
|
||||
|
||||
# Additional filters for minijinja
|
||||
[workspace.dependencies.minijinja-contrib]
|
||||
version = "2.10.2"
|
||||
version = "2.12.0"
|
||||
features = ["pycompat"]
|
||||
|
||||
# Utilities to deal with non-zero values
|
||||
@@ -384,40 +392,40 @@ version = "0.3.0"
|
||||
|
||||
# Open Policy Agent support through WASM
|
||||
[workspace.dependencies.opa-wasm]
|
||||
version = "0.1.5"
|
||||
version = "0.1.7"
|
||||
|
||||
# OpenTelemetry
|
||||
[workspace.dependencies.opentelemetry]
|
||||
version = "0.29.1"
|
||||
version = "0.31.0"
|
||||
features = ["trace", "metrics"]
|
||||
[workspace.dependencies.opentelemetry-http]
|
||||
version = "0.29.0"
|
||||
version = "0.31.0"
|
||||
features = ["reqwest"]
|
||||
[workspace.dependencies.opentelemetry-jaeger-propagator]
|
||||
version = "0.29.0"
|
||||
version = "0.31.0"
|
||||
[workspace.dependencies.opentelemetry-otlp]
|
||||
version = "0.29.0"
|
||||
version = "0.31.0"
|
||||
default-features = false
|
||||
features = ["trace", "metrics", "http-proto"]
|
||||
[workspace.dependencies.opentelemetry-prometheus]
|
||||
version = "0.29.1"
|
||||
[workspace.dependencies.opentelemetry-prometheus-text-exporter]
|
||||
version = "0.2.1"
|
||||
[workspace.dependencies.opentelemetry-resource-detectors]
|
||||
version = "0.8.0"
|
||||
version = "0.10.0"
|
||||
[workspace.dependencies.opentelemetry-semantic-conventions]
|
||||
version = "0.29.0"
|
||||
version = "0.31.0"
|
||||
features = ["semconv_experimental"]
|
||||
[workspace.dependencies.opentelemetry-stdout]
|
||||
version = "0.29.0"
|
||||
version = "0.31.0"
|
||||
features = ["trace", "metrics"]
|
||||
[workspace.dependencies.opentelemetry_sdk]
|
||||
version = "0.29.0"
|
||||
version = "0.31.0"
|
||||
features = [
|
||||
"experimental_trace_batch_span_processor_with_async_runtime",
|
||||
"experimental_metrics_periodicreader_with_async_runtime",
|
||||
"rt-tokio",
|
||||
]
|
||||
[workspace.dependencies.tracing-opentelemetry]
|
||||
version = "0.30.0"
|
||||
version = "0.32.0"
|
||||
default-features = false
|
||||
|
||||
# P256 elliptic curve
|
||||
@@ -446,11 +454,11 @@ features = ["std"]
|
||||
|
||||
# Parser generator
|
||||
[workspace.dependencies.pest]
|
||||
version = "2.8.0"
|
||||
version = "2.8.3"
|
||||
|
||||
# Pest derive macros
|
||||
[workspace.dependencies.pest_derive]
|
||||
version = "2.8.0"
|
||||
version = "2.8.3"
|
||||
|
||||
# Pin projection
|
||||
[workspace.dependencies.pin-project-lite]
|
||||
@@ -468,11 +476,7 @@ features = ["std", "pkcs5", "encryption"]
|
||||
|
||||
# Public Suffix List
|
||||
[workspace.dependencies.psl]
|
||||
version = "2.1.120"
|
||||
|
||||
# Prometheus metrics
|
||||
[workspace.dependencies.prometheus]
|
||||
version = "0.14.0"
|
||||
version = "2.1.148"
|
||||
|
||||
# High-precision clock
|
||||
[workspace.dependencies.quanta]
|
||||
@@ -488,13 +492,19 @@ version = "0.6.4"
|
||||
|
||||
# Regular expressions
|
||||
[workspace.dependencies.regex]
|
||||
version = "1.11.1"
|
||||
version = "1.12.2"
|
||||
|
||||
# High-level HTTP client
|
||||
[workspace.dependencies.reqwest]
|
||||
version = "0.12.20"
|
||||
version = "0.12.24"
|
||||
default-features = false
|
||||
features = ["http2", "rustls-tls-manual-roots", "charset", "json", "socks"]
|
||||
features = [
|
||||
"http2",
|
||||
"rustls-tls-manual-roots-no-provider",
|
||||
"charset",
|
||||
"json",
|
||||
"socks",
|
||||
]
|
||||
|
||||
# RSA cryptography
|
||||
[workspace.dependencies.rsa]
|
||||
@@ -507,11 +517,11 @@ version = "2.1.1"
|
||||
|
||||
# Matrix-related types
|
||||
[workspace.dependencies.ruma-common]
|
||||
version = "0.15.2"
|
||||
version = "0.16.0"
|
||||
|
||||
# TLS stack
|
||||
[workspace.dependencies.rustls]
|
||||
version = "0.23.27"
|
||||
version = "0.23.34"
|
||||
|
||||
# PEM parsing for rustls
|
||||
[workspace.dependencies.rustls-pemfile]
|
||||
@@ -523,7 +533,7 @@ version = "1.12.0"
|
||||
|
||||
# Use platform-specific verifier for TLS
|
||||
[workspace.dependencies.rustls-platform-verifier]
|
||||
version = "0.5.3"
|
||||
version = "0.6.1"
|
||||
|
||||
# systemd service status notification
|
||||
[workspace.dependencies.sd-notify]
|
||||
@@ -541,7 +551,7 @@ features = ["std"]
|
||||
|
||||
# Query builder
|
||||
[workspace.dependencies.sea-query]
|
||||
version = "0.32.6"
|
||||
version = "0.32.7"
|
||||
features = ["derive", "attr", "with-uuid", "with-chrono", "postgres-array"]
|
||||
|
||||
# Query builder
|
||||
@@ -557,27 +567,27 @@ features = [
|
||||
|
||||
# Sentry error tracking
|
||||
[workspace.dependencies.sentry]
|
||||
version = "0.37.0"
|
||||
version = "0.45.0"
|
||||
default-features = false
|
||||
features = ["backtrace", "contexts", "panic", "tower", "reqwest"]
|
||||
|
||||
# Sentry tower layer
|
||||
[workspace.dependencies.sentry-tower]
|
||||
version = "0.37.0"
|
||||
version = "0.45.0"
|
||||
features = ["http", "axum-matched-path"]
|
||||
|
||||
# Sentry tracing integration
|
||||
[workspace.dependencies.sentry-tracing]
|
||||
version = "0.37.0"
|
||||
version = "0.45.0"
|
||||
|
||||
# Serialization and deserialization
|
||||
[workspace.dependencies.serde]
|
||||
version = "1.0.219"
|
||||
version = "1.0.228"
|
||||
features = ["derive"] # Most of the time, if we need serde, we need derive
|
||||
|
||||
# JSON serialization and deserialization
|
||||
[workspace.dependencies.serde_json]
|
||||
version = "1.0.140"
|
||||
version = "1.0.145"
|
||||
features = ["preserve_order"]
|
||||
|
||||
# URL encoded form serialization
|
||||
@@ -586,7 +596,7 @@ version = "0.7.1"
|
||||
|
||||
# Custom serialization helpers
|
||||
[workspace.dependencies.serde_with]
|
||||
version = "3.12.0"
|
||||
version = "3.14.0"
|
||||
features = ["hex", "chrono"]
|
||||
|
||||
# YAML serialization
|
||||
@@ -604,7 +614,7 @@ version = "2.2.0"
|
||||
|
||||
# Low-level socket manipulation
|
||||
[workspace.dependencies.socket2]
|
||||
version = "0.5.10"
|
||||
version = "0.6.1"
|
||||
|
||||
# Subject Public Key Info
|
||||
[workspace.dependencies.spki]
|
||||
@@ -627,14 +637,14 @@ features = [
|
||||
|
||||
# Custom error types
|
||||
[workspace.dependencies.thiserror]
|
||||
version = "2.0.12"
|
||||
version = "2.0.17"
|
||||
|
||||
[workspace.dependencies.thiserror-ext]
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
|
||||
# Async runtime
|
||||
[workspace.dependencies.tokio]
|
||||
version = "1.45.1"
|
||||
version = "1.48.0"
|
||||
features = ["full"]
|
||||
|
||||
[workspace.dependencies.tokio-stream]
|
||||
@@ -642,7 +652,7 @@ version = "0.1.17"
|
||||
|
||||
# Tokio rustls integration
|
||||
[workspace.dependencies.tokio-rustls]
|
||||
version = "0.26.2"
|
||||
version = "0.26.4"
|
||||
|
||||
# Tokio test utilities
|
||||
[workspace.dependencies.tokio-test]
|
||||
@@ -650,7 +660,7 @@ version = "0.4.4"
|
||||
|
||||
# Useful async utilities
|
||||
[workspace.dependencies.tokio-util]
|
||||
version = "0.7.15"
|
||||
version = "0.7.16"
|
||||
features = ["rt"]
|
||||
|
||||
# Tower services
|
||||
@@ -675,14 +685,14 @@ features = ["cors", "fs", "add-extension", "set-header"]
|
||||
[workspace.dependencies.tracing]
|
||||
version = "0.1.41"
|
||||
[workspace.dependencies.tracing-subscriber]
|
||||
version = "0.3.19"
|
||||
version = "0.3.20"
|
||||
features = ["env-filter"]
|
||||
[workspace.dependencies.tracing-appender]
|
||||
version = "0.2.3"
|
||||
|
||||
# URL manipulation
|
||||
[workspace.dependencies.url]
|
||||
version = "2.5.4"
|
||||
version = "2.5.7"
|
||||
features = ["serde"]
|
||||
|
||||
# URL encoding
|
||||
@@ -696,7 +706,7 @@ features = ["serde", "uuid"]
|
||||
|
||||
# UUID support
|
||||
[workspace.dependencies.uuid]
|
||||
version = "1.17.0"
|
||||
version = "1.18.1"
|
||||
|
||||
# HTML escaping
|
||||
[workspace.dependencies.v_htmlescape]
|
||||
@@ -713,7 +723,7 @@ version = "2.5.0"
|
||||
|
||||
# HTTP mock server
|
||||
[workspace.dependencies.wiremock]
|
||||
version = "0.6.3"
|
||||
version = "0.6.5"
|
||||
|
||||
# User-agent parser
|
||||
[workspace.dependencies.woothee]
|
||||
@@ -725,7 +735,7 @@ version = "0.5.5"
|
||||
|
||||
# Zero memory after use
|
||||
[workspace.dependencies.zeroize]
|
||||
version = "1.8.1"
|
||||
version = "1.8.2"
|
||||
|
||||
# Password strength estimation
|
||||
[workspace.dependencies.zxcvbn]
|
||||
|
||||
15
Dockerfile
15
Dockerfile
@@ -1,4 +1,8 @@
|
||||
# syntax = docker/dockerfile:1.7.1
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Builds a minimal image with the binary only. It is multi-arch capable,
|
||||
# cross-building to aarch64 and x86_64. When cross-compiling, Docker sets two
|
||||
@@ -8,10 +12,11 @@
|
||||
# The Debian version and version name must be in sync
|
||||
ARG DEBIAN_VERSION=12
|
||||
ARG DEBIAN_VERSION_NAME=bookworm
|
||||
ARG RUSTC_VERSION=1.86.0
|
||||
ARG NODEJS_VERSION=20.15.0
|
||||
ARG OPA_VERSION=1.1.0
|
||||
ARG CARGO_AUDITABLE_VERSION=0.6.6
|
||||
ARG RUSTC_VERSION=1.89.0
|
||||
ARG NODEJS_VERSION=22.19.0
|
||||
# Keep in sync with .github/actions/build-policies/action.yml and policies/Makefile
|
||||
ARG OPA_VERSION=1.8.0
|
||||
ARG CARGO_AUDITABLE_VERSION=0.7.0
|
||||
|
||||
##########################################
|
||||
## Build stage that builds the frontend ##
|
||||
@@ -20,7 +25,7 @@ FROM --platform=${BUILDPLATFORM} docker.io/library/node:${NODEJS_VERSION}-${DEBI
|
||||
|
||||
WORKDIR /app/frontend
|
||||
|
||||
COPY ./frontend/package.json ./frontend/package-lock.json /app/frontend/
|
||||
COPY ./frontend/.npmrc ./frontend/package.json ./frontend/package-lock.json /app/frontend/
|
||||
# Network access: to fetch dependencies
|
||||
RUN --network=default \
|
||||
npm ci
|
||||
|
||||
6
LICENSE-COMMERCIAL
Normal file
6
LICENSE-COMMERCIAL
Normal file
@@ -0,0 +1,6 @@
|
||||
Licensees holding a valid commercial license with Element may use this
|
||||
software in accordance with the terms contained in a written agreement
|
||||
between you and Element.
|
||||
|
||||
To purchase a commercial license please contact our sales team at
|
||||
licensing@element.io
|
||||
12
README.md
12
README.md
@@ -27,3 +27,15 @@ Anyone can contribute to translations through [Localazy](https://localazy.com/el
|
||||
## 🏗️ Contributing
|
||||
|
||||
See the [contribution guidelines](https://element-hq.github.io/matrix-authentication-service/development/contributing.html) for information on how to contribute to this project.
|
||||
|
||||
## ⚖️ Copyright & License
|
||||
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2021-2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
This software is dual-licensed by New Vector Ltd (Element). It can be used either:
|
||||
|
||||
(1) for free 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); OR
|
||||
|
||||
(2) under the terms of a paid-for Element Commercial License agreement between you and Element (the terms of which may vary depending on what you and Element have agreed to).
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses.
|
||||
|
||||
54
biome.json
54
biome.json
@@ -1,28 +1,27 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
|
||||
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignore": [
|
||||
".devcontainer/**",
|
||||
"docs/**",
|
||||
"translations/**",
|
||||
"policies/**",
|
||||
"crates/**",
|
||||
"frontend/package.json",
|
||||
"frontend/src/gql/**",
|
||||
"frontend/src/routeTree.gen.ts",
|
||||
"frontend/.storybook/locales.ts",
|
||||
"frontend/.storybook/public/mockServiceWorker.js",
|
||||
"frontend/locales/*.json",
|
||||
"**/coverage/**",
|
||||
"**/dist/**"
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/.devcontainer/**",
|
||||
"!**/docs/**",
|
||||
"!**/translations/**",
|
||||
"!**/policies/**",
|
||||
"!**/crates/**",
|
||||
"!**/frontend/package.json",
|
||||
"!**/frontend/src/gql/**",
|
||||
"!**/frontend/src/routeTree.gen.ts",
|
||||
"!**/frontend/.storybook/locales.ts",
|
||||
"!**/frontend/.storybook/public/mockServiceWorker.js",
|
||||
"!**/frontend/locales/**/*.json",
|
||||
"!**/coverage/**",
|
||||
"!**/dist/**"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
@@ -33,9 +32,28 @@
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"complexity": {
|
||||
"noImportantStyles": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noUnknownAtRules": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedImports": "warn",
|
||||
"noUnusedVariables": "warn"
|
||||
},
|
||||
"style": {
|
||||
"noParameterAssign": "error",
|
||||
"useAsConstAssertion": "error",
|
||||
"useDefaultParameterLast": "error",
|
||||
"useEnumInitializers": "error",
|
||||
"useSelfClosingElements": "error",
|
||||
"useSingleVarDeclarator": "error",
|
||||
"noUnusedTemplateLiteral": "error",
|
||||
"useNumberNamespace": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noUselessElse": "error",
|
||||
"noDescendingSpecificity": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Documentation for possible options in this file is at
|
||||
# https://rust-lang.github.io/mdBook/format/config.html
|
||||
[book]
|
||||
|
||||
10
clippy.toml
10
clippy.toml
@@ -1,4 +1,9 @@
|
||||
doc-valid-idents = ["OpenID", "OAuth", "..", "PostgreSQL", "SQLite"]
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
doc-valid-idents = ["OpenID", "OAuth", "UserInfo", "..", "PostgreSQL", "SQLite"]
|
||||
|
||||
disallowed-methods = [
|
||||
{ path = "rand::thread_rng", reason = "do not create rngs on the fly, pass them as parameters" },
|
||||
@@ -10,7 +15,8 @@ disallowed-methods = [
|
||||
]
|
||||
|
||||
disallowed-types = [
|
||||
"rand::OsRng",
|
||||
{ path = "std::path::PathBuf", reason = "use camino::Utf8PathBuf instead" },
|
||||
{ path = "std::path::Path", reason = "use camino::Utf8Path instead" },
|
||||
{ path = "axum::extract::Query", reason = "use axum_extra::extract::Query instead. The built-in version doesn't deserialise lists."},
|
||||
{ path = "axum::extract::rejection::QueryRejection", reason = "use axum_extra::extract::QueryRejection instead"}
|
||||
]
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
[package]
|
||||
name = "mas-axum-utils"
|
||||
version.workspace = true
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use axum::{
|
||||
BoxError, Json,
|
||||
extract::{
|
||||
Form, FromRequest, FromRequestParts,
|
||||
Form, FromRequest,
|
||||
rejection::{FailedToDeserializeForm, FormRejection},
|
||||
},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use axum_extra::typed_header::{TypedHeader, TypedHeaderRejectionReason};
|
||||
use headers::{Authorization, authorization::Basic};
|
||||
use headers::authorization::{Basic, Bearer, Credentials as _};
|
||||
use http::{Request, StatusCode};
|
||||
use mas_data_model::{Client, JwksOrJwksUri};
|
||||
use mas_http::RequestBuilderExt;
|
||||
@@ -60,17 +59,30 @@ pub enum Credentials {
|
||||
client_id: String,
|
||||
jwt: Box<Jwt<'static, HashMap<String, serde_json::Value>>>,
|
||||
},
|
||||
BearerToken {
|
||||
token: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Credentials {
|
||||
/// Get the `client_id` of the credentials
|
||||
#[must_use]
|
||||
pub fn client_id(&self) -> &str {
|
||||
pub fn client_id(&self) -> Option<&str> {
|
||||
match self {
|
||||
Credentials::None { client_id }
|
||||
| Credentials::ClientSecretBasic { client_id, .. }
|
||||
| Credentials::ClientSecretPost { client_id, .. }
|
||||
| Credentials::ClientAssertionJwtBearer { client_id, .. } => client_id,
|
||||
| Credentials::ClientAssertionJwtBearer { client_id, .. } => Some(client_id),
|
||||
Credentials::BearerToken { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the bearer token from the credentials.
|
||||
#[must_use]
|
||||
pub fn bearer_token(&self) -> Option<&str> {
|
||||
match self {
|
||||
Credentials::BearerToken { token } => Some(token),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +101,7 @@ impl Credentials {
|
||||
| Credentials::ClientSecretBasic { client_id, .. }
|
||||
| Credentials::ClientSecretPost { client_id, .. }
|
||||
| Credentials::ClientAssertionJwtBearer { client_id, .. } => client_id,
|
||||
Credentials::BearerToken { .. } => return Ok(None),
|
||||
};
|
||||
|
||||
repo.oauth2_client().find_by_client_id(client_id).await
|
||||
@@ -239,7 +252,7 @@ pub struct ClientAuthorization<F = ()> {
|
||||
impl<F> ClientAuthorization<F> {
|
||||
/// Get the `client_id` from the credentials.
|
||||
#[must_use]
|
||||
pub fn client_id(&self) -> &str {
|
||||
pub fn client_id(&self) -> Option<&str> {
|
||||
self.credentials.client_id()
|
||||
}
|
||||
}
|
||||
@@ -355,30 +368,40 @@ where
|
||||
{
|
||||
type Rejection = ClientAuthorizationError;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn from_request(
|
||||
req: Request<axum::body::Body>,
|
||||
state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
// Split the request into parts so we can extract some headers
|
||||
let (mut parts, body) = req.into_parts();
|
||||
enum Authorization {
|
||||
Basic(String, String),
|
||||
Bearer(String),
|
||||
}
|
||||
|
||||
let header =
|
||||
TypedHeader::<Authorization<Basic>>::from_request_parts(&mut parts, state).await;
|
||||
|
||||
// Take the Authorization header
|
||||
let credentials_from_header = match header {
|
||||
Ok(header) => Some((header.username().to_owned(), header.password().to_owned())),
|
||||
Err(err) => match err.reason() {
|
||||
// If it's missing it is fine
|
||||
TypedHeaderRejectionReason::Missing => None,
|
||||
// If the header could not be parsed, return the error
|
||||
_ => return Err(ClientAuthorizationError::InvalidHeader),
|
||||
},
|
||||
// Sadly, the typed-header 'Authorization' doesn't let us check for both
|
||||
// Basic and Bearer at the same time, so we need to parse them manually
|
||||
let authorization = if let Some(header) = req.headers().get(http::header::AUTHORIZATION) {
|
||||
let bytes = header.as_bytes();
|
||||
if bytes.len() >= 6 && bytes[..6].eq_ignore_ascii_case(b"Basic ") {
|
||||
let Some(decoded) = Basic::decode(header) else {
|
||||
return Err(ClientAuthorizationError::InvalidHeader);
|
||||
};
|
||||
|
||||
// Reconstruct the request from the parts
|
||||
let req = Request::from_parts(parts, body);
|
||||
Some(Authorization::Basic(
|
||||
decoded.username().to_owned(),
|
||||
decoded.password().to_owned(),
|
||||
))
|
||||
} else if bytes.len() >= 7 && bytes[..7].eq_ignore_ascii_case(b"Bearer ") {
|
||||
let Some(decoded) = Bearer::decode(header) else {
|
||||
return Err(ClientAuthorizationError::InvalidHeader);
|
||||
};
|
||||
|
||||
Some(Authorization::Bearer(decoded.token().to_owned()))
|
||||
} else {
|
||||
return Err(ClientAuthorizationError::InvalidHeader);
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Take the form value
|
||||
let (
|
||||
@@ -407,13 +430,19 @@ where
|
||||
|
||||
// And now, figure out the actual auth method
|
||||
let credentials = match (
|
||||
credentials_from_header,
|
||||
authorization,
|
||||
client_id_from_form,
|
||||
client_secret_from_form,
|
||||
client_assertion_type,
|
||||
client_assertion,
|
||||
) {
|
||||
(Some((client_id, client_secret)), client_id_from_form, None, None, None) => {
|
||||
(
|
||||
Some(Authorization::Basic(client_id, client_secret)),
|
||||
client_id_from_form,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
) => {
|
||||
if let Some(client_id_from_form) = client_id_from_form {
|
||||
// If the client_id was in the body, verify it matches with the header
|
||||
if client_id != client_id_from_form {
|
||||
@@ -483,6 +512,11 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
(Some(Authorization::Bearer(token)), None, None, None, None) => {
|
||||
// Got a bearer token
|
||||
Credentials::BearerToken { token }
|
||||
}
|
||||
|
||||
(None, None, None, None, None) => {
|
||||
// Special case when there are no credentials anywhere
|
||||
return Err(ClientAuthorizationError::MissingCredentials);
|
||||
@@ -677,4 +711,29 @@ mod tests {
|
||||
jwt.verify_with_shared_secret(b"client-secret".to_vec())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bearer_token_test() {
|
||||
let req = Request::builder()
|
||||
.method(Method::POST)
|
||||
.header(
|
||||
http::header::CONTENT_TYPE,
|
||||
mime::APPLICATION_WWW_FORM_URLENCODED.as_ref(),
|
||||
)
|
||||
.header(http::header::AUTHORIZATION, "Bearer token")
|
||||
.body(Body::new("foo=bar".to_owned()))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
ClientAuthorization::<serde_json::Value>::from_request(req, &())
|
||||
.await
|
||||
.unwrap(),
|
||||
ClientAuthorization {
|
||||
credentials: Credentials::BearerToken {
|
||||
token: "token".to_owned(),
|
||||
},
|
||||
form: Some(serde_json::json!({"foo": "bar"})),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
//! Private (encrypted) cookie jar, based on axum-extra's cookie jar
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use base64ct::{Base64UrlUnpadded, Encoding};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use mas_storage::Clock;
|
||||
use mas_data_model::Clock;
|
||||
use rand::{Rng, RngCore, distributions::Standard, prelude::Distribution as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{TimestampSeconds, serde_as};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use axum::response::{IntoResponse, Response};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use axum::{
|
||||
Extension,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum_extra::typed_header::TypedHeader;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::cmp::Reverse;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
#![deny(clippy::future_not_send)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::convert::Infallible;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use mas_data_model::BrowserSession;
|
||||
use mas_storage::RepositoryAccess;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::{collections::HashMap, error::Error};
|
||||
|
||||
@@ -16,9 +16,9 @@ use axum::{
|
||||
use axum_extra::typed_header::{TypedHeader, TypedHeaderRejectionReason};
|
||||
use headers::{Authorization, Header, HeaderMapExt, HeaderName, authorization::Bearer};
|
||||
use http::{HeaderMap, HeaderValue, Request, StatusCode, header::WWW_AUTHENTICATE};
|
||||
use mas_data_model::Session;
|
||||
use mas_data_model::{Clock, Session};
|
||||
use mas_storage::{
|
||||
Clock, RepositoryAccess,
|
||||
RepositoryAccess,
|
||||
oauth2::{OAuth2AccessTokenRepository, OAuth2SessionRepository},
|
||||
};
|
||||
use serde::{Deserialize, de::DeserializeOwned};
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
[package]
|
||||
name = "mas-cli"
|
||||
version.workspace = true
|
||||
@@ -54,12 +59,11 @@ opentelemetry.workspace = true
|
||||
opentelemetry-http.workspace = true
|
||||
opentelemetry-jaeger-propagator.workspace = true
|
||||
opentelemetry-otlp.workspace = true
|
||||
opentelemetry-prometheus.workspace = true
|
||||
opentelemetry-prometheus-text-exporter.workspace = true
|
||||
opentelemetry-resource-detectors.workspace = true
|
||||
opentelemetry-semantic-conventions.workspace = true
|
||||
opentelemetry-stdout.workspace = true
|
||||
opentelemetry_sdk.workspace = true
|
||||
prometheus.workspace = true
|
||||
sentry.workspace = true
|
||||
sentry-tracing.workspace = true
|
||||
sentry-tower.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use vergen_gitcl::{Emitter, GitclBuilder, RustcBuilder};
|
||||
|
||||
@@ -12,15 +12,15 @@ fn main() -> anyhow::Result<()> {
|
||||
// At build time, we override the version through the environment variable
|
||||
// VERGEN_GIT_DESCRIBE. In some contexts, it means this variable is set but
|
||||
// empty, so we unset it here.
|
||||
if let Ok(ver) = std::env::var("VERGEN_GIT_DESCRIBE") {
|
||||
if ver.is_empty() {
|
||||
if let Ok(ver) = std::env::var("VERGEN_GIT_DESCRIBE")
|
||||
&& ver.is_empty()
|
||||
{
|
||||
#[allow(unsafe_code)]
|
||||
// SAFETY: This is safe because the build script is running a single thread
|
||||
unsafe {
|
||||
std::env::remove_var("VERGEN_GIT_DESCRIBE");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let gitcl = GitclBuilder::default()
|
||||
.describe(true, false, Some("v*.*.*"))
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::{convert::Infallible, net::IpAddr, sync::Arc};
|
||||
|
||||
use axum::extract::{FromRef, FromRequestParts};
|
||||
use ipnetwork::IpNetwork;
|
||||
use mas_context::LogContext;
|
||||
use mas_data_model::SiteConfig;
|
||||
use mas_data_model::{AppVersion, BoxClock, BoxRng, SiteConfig, SystemClock};
|
||||
use mas_handlers::{
|
||||
ActivityTracker, BoundActivityTracker, CookieManager, ErrorWrapper, GraphQLSchema, Limiter,
|
||||
MetadataCache, RequesterFingerprint, passwords::PasswordManager,
|
||||
@@ -19,9 +19,7 @@ use mas_keystore::{Encrypter, Keystore};
|
||||
use mas_matrix::HomeserverConnection;
|
||||
use mas_policy::{Policy, PolicyFactory};
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_storage::{
|
||||
BoxClock, BoxRepository, BoxRepositoryFactory, BoxRng, RepositoryFactory, SystemClock,
|
||||
};
|
||||
use mas_storage::{BoxRepository, BoxRepositoryFactory, RepositoryFactory};
|
||||
use mas_storage_pg::PgRepositoryFactory;
|
||||
use mas_templates::Templates;
|
||||
use opentelemetry::KeyValue;
|
||||
@@ -29,7 +27,7 @@ use rand::SeedableRng;
|
||||
use sqlx::PgPool;
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::telemetry::METER;
|
||||
use crate::{VERSION, telemetry::METER};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
@@ -216,6 +214,12 @@ impl FromRef<AppState> for Arc<dyn HomeserverConnection> {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for AppVersion {
|
||||
fn from_ref(_input: &AppState) -> Self {
|
||||
AppVersion(VERSION)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequestParts<AppState> for BoxClock {
|
||||
type Rejection = Infallible;
|
||||
|
||||
@@ -275,11 +279,11 @@ fn infer_client_ip(
|
||||
|
||||
let peer = if let Some(info) = connection_info {
|
||||
// We can always trust the proxy protocol to give us the correct IP address
|
||||
if let Some(proxy) = info.get_proxy_ref() {
|
||||
if let Some(source) = proxy.source() {
|
||||
if let Some(proxy) = info.get_proxy_ref()
|
||||
&& let Some(source) = proxy.source()
|
||||
{
|
||||
return Some(source.ip());
|
||||
}
|
||||
}
|
||||
|
||||
info.get_peer_addr().map(|addr| addr.ip())
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
@@ -11,7 +11,7 @@ use camino::Utf8PathBuf;
|
||||
use clap::Parser;
|
||||
use figment::Figment;
|
||||
use mas_config::{ConfigurationSection, RootConfig, SyncConfig};
|
||||
use mas_storage::{Clock as _, SystemClock};
|
||||
use mas_data_model::{Clock as _, SystemClock};
|
||||
use mas_storage_pg::MIGRATOR;
|
||||
use rand::SeedableRng;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -72,7 +72,7 @@ impl Options {
|
||||
SC::Dump { output } => {
|
||||
let _span = info_span!("cli.config.dump").entered();
|
||||
|
||||
let config = RootConfig::extract(figment)?;
|
||||
let config = RootConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
let config = serde_yaml::to_string(&config)?;
|
||||
|
||||
if let Some(output) = output {
|
||||
@@ -88,7 +88,7 @@ impl Options {
|
||||
SC::Check => {
|
||||
let _span = info_span!("cli.config.check").entered();
|
||||
|
||||
let _config = RootConfig::extract(figment)?;
|
||||
let _config = RootConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
info!("Configuration file looks good");
|
||||
}
|
||||
|
||||
@@ -105,7 +105,8 @@ impl Options {
|
||||
|
||||
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)?;
|
||||
let synapse_config = syn2mas::synapse_config::Config::load(&synapse_config)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
config = synapse_config.adjust_mas_config(config, &mut rng, clock.now());
|
||||
}
|
||||
|
||||
@@ -121,7 +122,7 @@ impl Options {
|
||||
}
|
||||
|
||||
SC::Sync { prune, dry_run } => {
|
||||
let config = SyncConfig::extract(figment)?;
|
||||
let config = SyncConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
let clock = SystemClock::default();
|
||||
let encrypter = config.secrets.encrypter().await?;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
@@ -30,7 +30,8 @@ enum Subcommand {
|
||||
impl Options {
|
||||
pub async fn run(self, figment: &Figment) -> anyhow::Result<ExitCode> {
|
||||
let _span = info_span!("cli.database.migrate").entered();
|
||||
let config = DatabaseConfig::extract_or_default(figment)?;
|
||||
let config =
|
||||
DatabaseConfig::extract_or_default(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
let mut conn = database_connection_from_config(&config).await?;
|
||||
|
||||
// Run pending migrations
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
@@ -41,13 +41,16 @@ impl Options {
|
||||
match self.subcommand {
|
||||
SC::Policy { with_dynamic_data } => {
|
||||
let _span = info_span!("cli.debug.policy").entered();
|
||||
let config = PolicyConfig::extract_or_default(figment)?;
|
||||
let matrix_config = MatrixConfig::extract(figment)?;
|
||||
let config =
|
||||
PolicyConfig::extract_or_default(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
let matrix_config =
|
||||
MatrixConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
info!("Loading and compiling the policy module");
|
||||
let policy_factory = policy_factory_from_config(&config, &matrix_config).await?;
|
||||
|
||||
if with_dynamic_data {
|
||||
let database_config = DatabaseConfig::extract(figment)?;
|
||||
let database_config =
|
||||
DatabaseConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
let pool = database_pool_from_config(&database_config).await?;
|
||||
let repository_factory = PgRepositoryFactory::new(pool.clone());
|
||||
load_policy_factory_dynamic_data(&policy_factory, &repository_factory).await?;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
//! Diagnostic utility to check the health of the deployment
|
||||
//!
|
||||
@@ -14,6 +14,7 @@ use std::process::ExitCode;
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use figment::Figment;
|
||||
use hyper::StatusCode;
|
||||
use mas_config::{ConfigurationSection, RootConfig};
|
||||
use mas_http::RequestBuilderExt;
|
||||
use tracing::{error, info, info_span, warn};
|
||||
@@ -26,14 +27,13 @@ const DOCS_BASE: &str = "https://element-hq.github.io/matrix-authentication-serv
|
||||
pub(super) struct Options {}
|
||||
|
||||
impl Options {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn run(self, figment: &Figment) -> anyhow::Result<ExitCode> {
|
||||
let _span = info_span!("cli.doctor").entered();
|
||||
info!(
|
||||
"💡 Running diagnostics, make sure that both MAS and Synapse are running, and that MAS is using the same configuration files as this tool."
|
||||
);
|
||||
|
||||
let config = RootConfig::extract(figment)?;
|
||||
let config = RootConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
|
||||
// We'll need an HTTP client
|
||||
let http_client = mas_http::reqwest_client();
|
||||
@@ -44,8 +44,8 @@ impl Options {
|
||||
r"The homeserver host in the config (`matrix.homeserver`) is not a valid domain.
|
||||
See {DOCS_BASE}/setup/homeserver.html",
|
||||
)?;
|
||||
let secret = config.matrix.secret().await?;
|
||||
let hs_api = config.matrix.endpoint;
|
||||
let admin_token = config.matrix.secret;
|
||||
|
||||
if !issuer.starts_with("https://") {
|
||||
warn!(
|
||||
@@ -100,16 +100,13 @@ Make sure that the MAS config contains:
|
||||
|
||||
http:
|
||||
public_base: {issuer:?}
|
||||
# Or, if the issuer is different from the public base:
|
||||
issuer: {issuer:?}
|
||||
|
||||
And in the Synapse config:
|
||||
|
||||
experimental_features:
|
||||
msc3861:
|
||||
matrix_authentication_service:
|
||||
enabled: true
|
||||
# This must exactly match:
|
||||
issuer: {issuer:?}
|
||||
# This must point to where MAS is reachable by Synapse
|
||||
endpoint: {issuer:?}
|
||||
# ...
|
||||
|
||||
See {DOCS_BASE}/setup/homeserver.html
|
||||
@@ -129,10 +126,9 @@ Check the well-known document at "{well_known_uri}"
|
||||
Check the well-known document at "{well_known_uri}"
|
||||
Make sure Synapse has delegated auth enabled:
|
||||
|
||||
experimental_features:
|
||||
msc3861:
|
||||
matrix_authentication_service:
|
||||
enabled: true
|
||||
issuer: {issuer:?}
|
||||
endpoint: {issuer:?}
|
||||
# ...
|
||||
|
||||
If it is not Synapse handling the well-known document, update it to include the following:
|
||||
@@ -284,70 +280,50 @@ Error details: {e}
|
||||
),
|
||||
}
|
||||
|
||||
// Try to reach the admin API on an unauthorized endpoint
|
||||
let server_version = hs_api.join("/_synapse/admin/v1/server_version")?;
|
||||
let result = http_client.get(server_version.as_str()).send_traced().await;
|
||||
match result {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
info!(r#"✅ The Synapse admin API is reachable at "{server_version}"."#);
|
||||
} else {
|
||||
error!(
|
||||
r#"❌ A Synapse admin API endpoint at "{server_version}" replied with {status}.
|
||||
Make sure MAS can reach the admin API, and that the homeserver is running.
|
||||
"#
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => error!(
|
||||
r#"❌ Can't reach the Synapse admin API at "{server_version}".
|
||||
Make sure MAS can reach the admin API, and that the homeserver is running.
|
||||
|
||||
Error details: {e}
|
||||
"#
|
||||
),
|
||||
}
|
||||
|
||||
// Try to reach an authenticated admin API endpoint
|
||||
let background_updates = hs_api.join("/_synapse/admin/v1/background_updates/status")?;
|
||||
// Try to reach an authenticated MAS API endpoint
|
||||
let mas_api = hs_api.join("/_synapse/mas/is_localpart_available")?;
|
||||
let result = http_client
|
||||
.get(background_updates.as_str())
|
||||
.bearer_auth(&admin_token)
|
||||
.get(mas_api.as_str())
|
||||
.bearer_auth(&secret)
|
||||
.send_traced()
|
||||
.await;
|
||||
match result {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
// We intentionally omit the required 'localpart' parameter
|
||||
// in this request. If authentication is successful, Synapse
|
||||
// returns a 400 Bad Request because of the missing
|
||||
// parameter. If authentication fails, Synapse will return a
|
||||
// 403 Forbidden. If the MAS integration isn't enabled,
|
||||
// Synapse will return a 404 Not found.
|
||||
if status == StatusCode::BAD_REQUEST {
|
||||
info!(
|
||||
r#"✅ The Synapse admin API is reachable with authentication at "{background_updates}"."#
|
||||
r#"✅ The Synapse MAS API is reachable with authentication at "{mas_api}"."#
|
||||
);
|
||||
} else {
|
||||
error!(
|
||||
r#"❌ A Synapse admin API endpoint at "{background_updates}" replied with {status}.
|
||||
r#"❌ A Synapse MAS API endpoint at "{mas_api}" replied with {status}.
|
||||
Make sure the homeserver is running, and that the MAS config has the correct `matrix.secret`.
|
||||
It should match the `admin_token` set in the Synapse config.
|
||||
It should match the `secret` set in the Synapse config.
|
||||
|
||||
experimental_features:
|
||||
msc3861:
|
||||
matrix_authentication_service:
|
||||
enabled: true
|
||||
issuer: {issuer}
|
||||
endpoint: {issuer:?}
|
||||
# This must exactly match the secret in the MAS config:
|
||||
admin_token: {admin_token:?}
|
||||
secret: {secret:?}
|
||||
|
||||
And in the MAS config:
|
||||
|
||||
matrix:
|
||||
homeserver: "{matrix_domain}"
|
||||
endpoint: "{hs_api}"
|
||||
secret: {admin_token:?}
|
||||
secret: {secret:?}
|
||||
"#
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => error!(
|
||||
r#"❌ Can't reach the Synapse admin API at "{background_updates}".
|
||||
r#"❌ Can't reach the Synapse MAS API at "{mas_api}".
|
||||
Make sure the homeserver is running, and that the MAS config has the correct `matrix.secret`.
|
||||
|
||||
Error details: {e}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::{collections::BTreeMap, process::ExitCode};
|
||||
|
||||
@@ -15,18 +15,21 @@ use figment::Figment;
|
||||
use mas_config::{
|
||||
ConfigurationSection, ConfigurationSectionExt, DatabaseConfig, MatrixConfig, PasswordsConfig,
|
||||
};
|
||||
use mas_data_model::{Device, TokenType, Ulid, UpstreamOAuthProvider, User};
|
||||
use mas_data_model::{Clock, Device, SystemClock, TokenType, Ulid, UpstreamOAuthProvider, User};
|
||||
use mas_email::Address;
|
||||
use mas_matrix::HomeserverConnection;
|
||||
use mas_storage::{
|
||||
Clock, RepositoryAccess, SystemClock,
|
||||
Pagination, RepositoryAccess,
|
||||
compat::{CompatAccessTokenRepository, CompatSessionFilter, CompatSessionRepository},
|
||||
oauth2::OAuth2SessionFilter,
|
||||
queue::{
|
||||
DeactivateUserJob, ProvisionUserJob, QueueJobRepositoryExt as _, ReactivateUserJob,
|
||||
SyncDevicesJob,
|
||||
},
|
||||
user::{BrowserSessionFilter, UserEmailRepository, UserPasswordRepository, UserRepository},
|
||||
user::{
|
||||
BrowserSessionFilter, UserEmailRepository, UserFilter, UserPasswordRepository,
|
||||
UserRepository,
|
||||
},
|
||||
};
|
||||
use mas_storage_pg::{DatabaseError, PgRepository};
|
||||
use rand::{
|
||||
@@ -85,6 +88,15 @@ enum Subcommand {
|
||||
ignore_complexity: bool,
|
||||
},
|
||||
|
||||
/// Make a user admin
|
||||
PromoteAdmin { username: String },
|
||||
|
||||
/// Make a user non-admin
|
||||
DemoteAdmin { username: String },
|
||||
|
||||
/// List all users with admin privileges
|
||||
ListAdminUsers,
|
||||
|
||||
/// Issue a compatibility token
|
||||
IssueCompatibilityToken {
|
||||
/// User for which to issue the token
|
||||
@@ -149,6 +161,10 @@ enum Subcommand {
|
||||
UnlockUser {
|
||||
/// User to unlock
|
||||
username: String,
|
||||
|
||||
/// Whether to reactivate the user if it had been deactivated
|
||||
#[arg(long)]
|
||||
reactivate: bool,
|
||||
},
|
||||
|
||||
/// Register a user
|
||||
@@ -203,7 +219,6 @@ enum Subcommand {
|
||||
}
|
||||
|
||||
impl Options {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn run(self, figment: &Figment) -> anyhow::Result<ExitCode> {
|
||||
use Subcommand as SC;
|
||||
let clock = SystemClock::default();
|
||||
@@ -219,8 +234,10 @@ impl Options {
|
||||
let _span =
|
||||
info_span!("cli.manage.set_password", user.username = %username).entered();
|
||||
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)?;
|
||||
let passwords_config = PasswordsConfig::extract_or_default(figment)?;
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let passwords_config = PasswordsConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
|
||||
let mut conn = database_connection_from_config(&database_config).await?;
|
||||
let password_manager = password_manager_from_config(&passwords_config).await?;
|
||||
@@ -260,7 +277,8 @@ impl Options {
|
||||
)
|
||||
.entered();
|
||||
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)?;
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let mut conn = database_connection_from_config(&database_config).await?;
|
||||
let txn = conn.begin().await?;
|
||||
let mut repo = PgRepository::from_conn(txn);
|
||||
@@ -309,12 +327,95 @@ impl Options {
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
SC::PromoteAdmin { username } => {
|
||||
let _span =
|
||||
info_span!("cli.manage.promote_admin", user.username = username,).entered();
|
||||
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let mut conn = database_connection_from_config(&database_config).await?;
|
||||
let txn = conn.begin().await?;
|
||||
let mut repo = PgRepository::from_conn(txn);
|
||||
|
||||
let user = repo
|
||||
.user()
|
||||
.find_by_username(&username)
|
||||
.await?
|
||||
.context("User not found")?;
|
||||
|
||||
let user = repo.user().set_can_request_admin(user, true).await?;
|
||||
|
||||
repo.into_inner().commit().await?;
|
||||
info!(%user.id, %user.username, "User promoted to admin");
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
SC::DemoteAdmin { username } => {
|
||||
let _span =
|
||||
info_span!("cli.manage.demote_admin", user.username = username,).entered();
|
||||
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let mut conn = database_connection_from_config(&database_config).await?;
|
||||
let txn = conn.begin().await?;
|
||||
let mut repo = PgRepository::from_conn(txn);
|
||||
|
||||
let user = repo
|
||||
.user()
|
||||
.find_by_username(&username)
|
||||
.await?
|
||||
.context("User not found")?;
|
||||
|
||||
let user = repo.user().set_can_request_admin(user, false).await?;
|
||||
|
||||
repo.into_inner().commit().await?;
|
||||
info!(%user.id, %user.username, "User is no longer admin");
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
SC::ListAdminUsers => {
|
||||
let _span = info_span!("cli.manage.list_admins").entered();
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let mut conn = database_connection_from_config(&database_config).await?;
|
||||
let txn = conn.begin().await?;
|
||||
let mut repo = PgRepository::from_conn(txn);
|
||||
|
||||
let mut cursor = Pagination::first(1000);
|
||||
let filter = UserFilter::new().can_request_admin_only();
|
||||
let total = repo.user().count(filter).await?;
|
||||
|
||||
info!("The following users can request admin privileges ({total} total):");
|
||||
loop {
|
||||
let page = repo.user().list(filter, cursor).await?;
|
||||
for edge in page.edges {
|
||||
let user = edge.node;
|
||||
info!(%user.id, username = %user.username);
|
||||
cursor = cursor.after(edge.cursor);
|
||||
}
|
||||
|
||||
if !page.has_next_page {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
SC::IssueCompatibilityToken {
|
||||
username,
|
||||
admin,
|
||||
device_id,
|
||||
} => {
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)?;
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let matrix_config =
|
||||
MatrixConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
let http_client = mas_http::reqwest_client();
|
||||
let homeserver =
|
||||
homeserver_connection_from_config(&matrix_config, http_client).await?;
|
||||
let mut conn = database_connection_from_config(&database_config).await?;
|
||||
let txn = conn.begin().await?;
|
||||
let mut repo = PgRepository::from_conn(txn);
|
||||
@@ -331,6 +432,24 @@ impl Options {
|
||||
Device::generate(&mut rng)
|
||||
};
|
||||
|
||||
if let Err(e) = homeserver
|
||||
.upsert_device(&user.username, device.as_str(), None)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
error = &*e,
|
||||
"Could not create the device on the homeserver, aborting"
|
||||
);
|
||||
|
||||
// Schedule a device sync job to remove the potential leftover device
|
||||
repo.queue_job()
|
||||
.schedule_job(&mut rng, &clock, SyncDevicesJob::new(&user))
|
||||
.await?;
|
||||
|
||||
repo.into_inner().commit().await?;
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
|
||||
let compat_session = repo
|
||||
.compat_session()
|
||||
.add(&mut rng, &clock, &user, device, None, admin, None)
|
||||
@@ -372,7 +491,8 @@ impl Options {
|
||||
(Some(_), true) => unreachable!(), // This should be handled by the clap group
|
||||
};
|
||||
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)?;
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let mut conn = database_connection_from_config(&database_config).await?;
|
||||
let txn = conn.begin().await?;
|
||||
let mut repo = PgRepository::from_conn(txn);
|
||||
@@ -399,7 +519,8 @@ impl Options {
|
||||
|
||||
SC::ProvisionAllUsers => {
|
||||
let _span = info_span!("cli.manage.provision_all_users").entered();
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)?;
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let mut conn = database_connection_from_config(&database_config).await?;
|
||||
let mut txn = conn.begin().await?;
|
||||
|
||||
@@ -425,7 +546,8 @@ impl Options {
|
||||
SC::KillSessions { username, dry_run } => {
|
||||
let _span =
|
||||
info_span!("cli.manage.kill_sessions", user.username = username).entered();
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)?;
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let mut conn = database_connection_from_config(&database_config).await?;
|
||||
let txn = conn.begin().await?;
|
||||
let mut repo = PgRepository::from_conn(txn);
|
||||
@@ -497,7 +619,8 @@ impl Options {
|
||||
deactivate,
|
||||
} => {
|
||||
let _span = info_span!("cli.manage.lock_user", user.username = username).entered();
|
||||
let config = DatabaseConfig::extract_or_default(figment)?;
|
||||
let config = DatabaseConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let mut conn = database_connection_from_config(&config).await?;
|
||||
let txn = conn.begin().await?;
|
||||
let mut repo = PgRepository::from_conn(txn);
|
||||
@@ -527,9 +650,14 @@ impl Options {
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
SC::UnlockUser { username } => {
|
||||
let _span = info_span!("cli.manage.lock_user", user.username = username).entered();
|
||||
let config = DatabaseConfig::extract_or_default(figment)?;
|
||||
SC::UnlockUser {
|
||||
username,
|
||||
reactivate,
|
||||
} => {
|
||||
let _span =
|
||||
info_span!("cli.manage.unlock_user", user.username = username).entered();
|
||||
let config = DatabaseConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let mut conn = database_connection_from_config(&config).await?;
|
||||
let txn = conn.begin().await?;
|
||||
let mut repo = PgRepository::from_conn(txn);
|
||||
@@ -540,10 +668,14 @@ impl Options {
|
||||
.await?
|
||||
.context("User not found")?;
|
||||
|
||||
warn!(%user.id, "User scheduling user reactivation");
|
||||
if reactivate {
|
||||
warn!(%user.id, "Scheduling user reactivation");
|
||||
repo.queue_job()
|
||||
.schedule_job(&mut rng, &clock, ReactivateUserJob::new(&user))
|
||||
.await?;
|
||||
} else {
|
||||
repo.user().unlock(user).await?;
|
||||
}
|
||||
|
||||
repo.into_inner().commit().await?;
|
||||
|
||||
@@ -562,24 +694,27 @@ impl Options {
|
||||
ignore_password_complexity,
|
||||
} => {
|
||||
let http_client = mas_http::reqwest_client();
|
||||
let password_config = PasswordsConfig::extract_or_default(figment)?;
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)?;
|
||||
let matrix_config = MatrixConfig::extract(figment)?;
|
||||
let password_config = PasswordsConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let database_config = DatabaseConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let matrix_config =
|
||||
MatrixConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
|
||||
let password_manager = password_manager_from_config(&password_config).await?;
|
||||
let homeserver = homeserver_connection_from_config(&matrix_config, http_client);
|
||||
let homeserver =
|
||||
homeserver_connection_from_config(&matrix_config, http_client).await?;
|
||||
let mut conn = database_connection_from_config(&database_config).await?;
|
||||
let txn = conn.begin().await?;
|
||||
let mut repo = PgRepository::from_conn(txn);
|
||||
|
||||
if let Some(password) = &password {
|
||||
if !ignore_password_complexity
|
||||
if let Some(password) = &password
|
||||
&& !ignore_password_complexity
|
||||
&& !password_manager.is_password_complex_enough(password)?
|
||||
{
|
||||
error!("That password is too weak.");
|
||||
return Ok(ExitCode::from(1));
|
||||
}
|
||||
}
|
||||
|
||||
// If the username is provided, check if it's available and normalize it.
|
||||
let localpart = if let Some(username) = username {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::{collections::BTreeSet, process::ExitCode, sync::Arc, time::Duration};
|
||||
|
||||
@@ -14,10 +14,10 @@ use mas_config::{
|
||||
AppConfig, ClientsConfig, ConfigurationSection, ConfigurationSectionExt, UpstreamOAuth2Config,
|
||||
};
|
||||
use mas_context::LogContext;
|
||||
use mas_data_model::SystemClock;
|
||||
use mas_handlers::{ActivityTracker, CookieManager, Limiter, MetadataCache};
|
||||
use mas_listener::server::Server;
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_storage::SystemClock;
|
||||
use mas_storage_pg::{MIGRATOR, PgRepositoryFactory};
|
||||
use sqlx::migrate::Migrate;
|
||||
use tracing::{Instrument, info, info_span, warn};
|
||||
@@ -55,11 +55,10 @@ pub(super) struct Options {
|
||||
}
|
||||
|
||||
impl Options {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn run(self, figment: &Figment) -> anyhow::Result<ExitCode> {
|
||||
let span = info_span!("cli.run.init").entered();
|
||||
let mut shutdown = LifecycleManager::new()?;
|
||||
let config = AppConfig::extract(figment)?;
|
||||
let config = AppConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
|
||||
info!(version = crate::VERSION, "Starting up");
|
||||
|
||||
@@ -101,8 +100,10 @@ impl Options {
|
||||
} else {
|
||||
// Sync the configuration with the database
|
||||
let mut conn = pool.acquire().await?;
|
||||
let clients_config = ClientsConfig::extract_or_default(figment)?;
|
||||
let upstream_oauth2_config = UpstreamOAuth2Config::extract_or_default(figment)?;
|
||||
let clients_config =
|
||||
ClientsConfig::extract_or_default(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
let upstream_oauth2_config = UpstreamOAuth2Config::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
|
||||
crate::sync::config_sync(
|
||||
upstream_oauth2_config,
|
||||
@@ -159,22 +160,29 @@ impl Options {
|
||||
)?;
|
||||
|
||||
// Load and compile the templates
|
||||
let templates =
|
||||
templates_from_config(&config.templates, &site_config, &url_builder).await?;
|
||||
let templates = templates_from_config(
|
||||
&config.templates,
|
||||
&site_config,
|
||||
&url_builder,
|
||||
// Don't use strict mode in production yet
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
shutdown.register_reloadable(&templates);
|
||||
|
||||
let http_client = mas_http::reqwest_client();
|
||||
|
||||
let homeserver_connection =
|
||||
homeserver_connection_from_config(&config.matrix, http_client.clone());
|
||||
homeserver_connection_from_config(&config.matrix, http_client.clone()).await?;
|
||||
|
||||
if !self.no_worker {
|
||||
let mailer = mailer_from_config(&config.email, &templates)?;
|
||||
test_mailer_in_background(&mailer, Duration::from_secs(30));
|
||||
|
||||
info!("Starting task worker");
|
||||
mas_tasks::init(
|
||||
mas_tasks::init_and_run(
|
||||
PgRepositoryFactory::new(pool.clone()),
|
||||
SystemClock::default(),
|
||||
&mailer,
|
||||
homeserver_connection.clone(),
|
||||
url_builder.clone(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::{collections::HashMap, process::ExitCode, time::Duration};
|
||||
|
||||
@@ -13,7 +13,7 @@ use mas_config::{
|
||||
ConfigurationSection, ConfigurationSectionExt, DatabaseConfig, MatrixConfig, SyncConfig,
|
||||
UpstreamOAuth2Config,
|
||||
};
|
||||
use mas_storage::SystemClock;
|
||||
use mas_data_model::SystemClock;
|
||||
use mas_storage_pg::MIGRATOR;
|
||||
use rand::thread_rng;
|
||||
use sqlx::{Connection, Either, PgConnection, postgres::PgConnectOptions, types::Uuid};
|
||||
@@ -88,7 +88,6 @@ const NUM_WRITER_CONNECTIONS: usize = 8;
|
||||
|
||||
impl Options {
|
||||
#[tracing::instrument("cli.syn2mas.run", skip_all)]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn run(self, figment: &Figment) -> anyhow::Result<ExitCode> {
|
||||
if self.synapse_configuration_files.is_empty() {
|
||||
error!("Please specify the path to the Synapse configuration file(s).");
|
||||
@@ -96,6 +95,7 @@ impl Options {
|
||||
}
|
||||
|
||||
let synapse_config = synapse_config::Config::load(&self.synapse_configuration_files)
|
||||
.map_err(anyhow::Error::from_boxed)
|
||||
.context("Failed to load Synapse configuration")?;
|
||||
|
||||
// Establish a connection to Synapse's Postgres database
|
||||
@@ -111,7 +111,8 @@ impl Options {
|
||||
.await
|
||||
.context("could not connect to Synapse Postgres database")?;
|
||||
|
||||
let config = DatabaseConfig::extract_or_default(figment)?;
|
||||
let config =
|
||||
DatabaseConfig::extract_or_default(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
|
||||
let mut mas_connection = database_connection_from_config_with_options(
|
||||
&config,
|
||||
@@ -131,7 +132,7 @@ impl Options {
|
||||
// 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 config = SyncConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
let clock = SystemClock::default();
|
||||
let encrypter = config.secrets.encrypter().await?;
|
||||
|
||||
@@ -213,7 +214,8 @@ impl Options {
|
||||
|
||||
Subcommand::Migrate { dry_run } => {
|
||||
let provider_id_mappings: HashMap<String, Uuid> = {
|
||||
let mas_oauth2 = UpstreamOAuth2Config::extract_or_default(figment)?;
|
||||
let mas_oauth2 = UpstreamOAuth2Config::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
|
||||
mas_oauth2
|
||||
.providers
|
||||
@@ -252,7 +254,8 @@ impl Options {
|
||||
let occasional_progress_logger_task =
|
||||
tokio::spawn(occasional_progress_logger(progress.clone()));
|
||||
|
||||
let mas_matrix = MatrixConfig::extract(figment)?;
|
||||
let mas_matrix =
|
||||
MatrixConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
syn2mas::migrate(
|
||||
reader,
|
||||
writer,
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::process::ExitCode;
|
||||
use std::{fmt::Write, process::ExitCode};
|
||||
|
||||
use anyhow::{Context as _, bail};
|
||||
use camino::Utf8PathBuf;
|
||||
use clap::Parser;
|
||||
use figment::Figment;
|
||||
use mas_config::{
|
||||
AccountConfig, BrandingConfig, CaptchaConfig, ConfigurationSection, ConfigurationSectionExt,
|
||||
ExperimentalConfig, MatrixConfig, PasswordsConfig, TemplatesConfig,
|
||||
};
|
||||
use mas_storage::{Clock, SystemClock};
|
||||
use mas_data_model::{Clock, SystemClock};
|
||||
use rand::SeedableRng;
|
||||
use tracing::info_span;
|
||||
|
||||
@@ -27,23 +29,35 @@ pub(super) struct Options {
|
||||
#[derive(Parser, Debug)]
|
||||
enum Subcommand {
|
||||
/// Check that the templates specified in the config are valid
|
||||
Check,
|
||||
Check {
|
||||
/// If set, templates will be rendered to this directory.
|
||||
/// The directory must either not exist or be empty.
|
||||
#[arg(long = "out-dir")]
|
||||
out_dir: Option<Utf8PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub async fn run(self, figment: &Figment) -> anyhow::Result<ExitCode> {
|
||||
use Subcommand as SC;
|
||||
match self.subcommand {
|
||||
SC::Check => {
|
||||
SC::Check { out_dir } => {
|
||||
let _span = info_span!("cli.templates.check").entered();
|
||||
|
||||
let template_config = TemplatesConfig::extract_or_default(figment)?;
|
||||
let branding_config = BrandingConfig::extract_or_default(figment)?;
|
||||
let matrix_config = MatrixConfig::extract(figment)?;
|
||||
let experimental_config = ExperimentalConfig::extract_or_default(figment)?;
|
||||
let password_config = PasswordsConfig::extract_or_default(figment)?;
|
||||
let account_config = AccountConfig::extract_or_default(figment)?;
|
||||
let captcha_config = CaptchaConfig::extract_or_default(figment)?;
|
||||
let template_config = TemplatesConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let branding_config = BrandingConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let matrix_config =
|
||||
MatrixConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
let experimental_config = ExperimentalConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let password_config = PasswordsConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let account_config = AccountConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
let captcha_config = CaptchaConfig::extract_or_default(figment)
|
||||
.map_err(anyhow::Error::from_boxed)?;
|
||||
|
||||
let clock = SystemClock::default();
|
||||
// XXX: we should disallow SeedableRng::from_entropy
|
||||
@@ -58,9 +72,54 @@ impl Options {
|
||||
&account_config,
|
||||
&captcha_config,
|
||||
)?;
|
||||
let templates =
|
||||
templates_from_config(&template_config, &site_config, &url_builder).await?;
|
||||
templates.check_render(clock.now(), &mut rng)?;
|
||||
let templates = templates_from_config(
|
||||
&template_config,
|
||||
&site_config,
|
||||
&url_builder, // Use strict mode in template checks
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let all_renders = templates.check_render(clock.now(), &mut rng)?;
|
||||
|
||||
if let Some(out_dir) = out_dir {
|
||||
// Save renders to disk.
|
||||
if out_dir.exists() {
|
||||
let mut read_dir =
|
||||
tokio::fs::read_dir(&out_dir).await.with_context(|| {
|
||||
format!("could not read {out_dir} to check it's empty")
|
||||
})?;
|
||||
if read_dir.next_entry().await?.is_some() {
|
||||
bail!("Render directory {out_dir} is not empty, refusing to write.");
|
||||
}
|
||||
} else {
|
||||
tokio::fs::create_dir(&out_dir)
|
||||
.await
|
||||
.with_context(|| format!("could not create {out_dir}"))?;
|
||||
}
|
||||
|
||||
for ((template, sample_identifier), template_render) in &all_renders {
|
||||
let (template_filename_base, template_ext) =
|
||||
template.rsplit_once('.').unwrap_or((template, "txt"));
|
||||
let template_filename_base = template_filename_base.replace('/', "_");
|
||||
|
||||
// Make a string like `-index=0-browser-session=0-locale=fr`
|
||||
let sample_suffix = {
|
||||
let mut s = String::new();
|
||||
for (k, v) in &sample_identifier.components {
|
||||
write!(s, "-{k}={v}")?;
|
||||
}
|
||||
s
|
||||
};
|
||||
|
||||
let render_path = out_dir.join(format!(
|
||||
"{template_filename_base}{sample_suffix}.{template_ext}"
|
||||
));
|
||||
|
||||
tokio::fs::write(&render_path, template_render.as_bytes())
|
||||
.await
|
||||
.with_context(|| format!("could not write render to {render_path}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::{process::ExitCode, time::Duration};
|
||||
|
||||
use clap::Parser;
|
||||
use figment::Figment;
|
||||
use mas_config::{AppConfig, ConfigurationSection};
|
||||
use mas_data_model::SystemClock;
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_storage_pg::PgRepositoryFactory;
|
||||
use tracing::{info, info_span};
|
||||
@@ -28,7 +29,7 @@ impl Options {
|
||||
pub async fn run(self, figment: &Figment) -> anyhow::Result<ExitCode> {
|
||||
let shutdown = LifecycleManager::new()?;
|
||||
let span = info_span!("cli.worker.init").entered();
|
||||
let config = AppConfig::extract(figment)?;
|
||||
let config = AppConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
|
||||
|
||||
// Connect to the database
|
||||
info!("Connecting to the database");
|
||||
@@ -51,20 +52,27 @@ impl Options {
|
||||
)?;
|
||||
|
||||
// Load and compile the templates
|
||||
let templates =
|
||||
templates_from_config(&config.templates, &site_config, &url_builder).await?;
|
||||
let templates = templates_from_config(
|
||||
&config.templates,
|
||||
&site_config,
|
||||
&url_builder,
|
||||
// Don't use strict mode on task workers for now
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mailer = mailer_from_config(&config.email, &templates)?;
|
||||
test_mailer_in_background(&mailer, Duration::from_secs(30));
|
||||
|
||||
let http_client = mas_http::reqwest_client();
|
||||
let conn = homeserver_connection_from_config(&config.matrix, http_client);
|
||||
let conn = homeserver_connection_from_config(&config.matrix, http_client).await?;
|
||||
|
||||
drop(config);
|
||||
|
||||
info!("Starting task scheduler");
|
||||
mas_tasks::init(
|
||||
mas_tasks::init_and_run(
|
||||
PgRepositoryFactory::new(pool.clone()),
|
||||
SystemClock::default(),
|
||||
&mailer,
|
||||
conn,
|
||||
url_builder,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::{process::ExitCode, time::Duration};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
@@ -115,8 +115,9 @@ async fn try_main() -> anyhow::Result<ExitCode> {
|
||||
// Load the base configuration files
|
||||
let figment = opts.figment();
|
||||
|
||||
let telemetry_config =
|
||||
TelemetryConfig::extract_or_default(&figment).context("Failed to load telemetry config")?;
|
||||
let telemetry_config = TelemetryConfig::extract_or_default(&figment)
|
||||
.map_err(anyhow::Error::from_boxed)
|
||||
.context("Failed to load telemetry config")?;
|
||||
|
||||
// Setup Sentry
|
||||
let sentry = sentry::init((
|
||||
@@ -127,8 +128,6 @@ async fn try_main() -> anyhow::Result<ExitCode> {
|
||||
release: Some(VERSION.into()),
|
||||
sample_rate: telemetry_config.sentry.sample_rate.unwrap_or(1.0),
|
||||
traces_sample_rate: telemetry_config.sentry.traces_sample_rate.unwrap_or(0.0),
|
||||
auto_session_tracking: true,
|
||||
session_mode: sentry::SessionMode::Request,
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
@@ -150,12 +149,14 @@ async fn try_main() -> anyhow::Result<ExitCode> {
|
||||
// Setup OpenTelemetry tracing and metrics
|
||||
self::telemetry::setup(&telemetry_config).context("failed to setup OpenTelemetry")?;
|
||||
|
||||
let telemetry_layer = self::telemetry::TRACER.get().map(|tracer| {
|
||||
tracing_opentelemetry::layer()
|
||||
let tracer = self::telemetry::TRACER
|
||||
.get()
|
||||
.context("TRACER was not set")?;
|
||||
|
||||
let telemetry_layer = tracing_opentelemetry::layer()
|
||||
.with_tracer(tracer.clone())
|
||||
.with_tracked_inactivity(false)
|
||||
.with_filter(LevelFilter::INFO)
|
||||
});
|
||||
.with_filter(LevelFilter::INFO);
|
||||
|
||||
let subscriber = Registry::default()
|
||||
.with(suppress_layer)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener, ToSocketAddrs},
|
||||
@@ -143,7 +143,12 @@ fn make_http_span<B>(req: &Request<B>) -> Span {
|
||||
propagator.extract_with_context(&context, &extractor)
|
||||
});
|
||||
|
||||
span.set_parent(parent_context);
|
||||
if let Err(err) = span.set_parent(parent_context) {
|
||||
tracing::error!(
|
||||
error = &err as &dyn std::error::Error,
|
||||
"Failed to set parent context on span"
|
||||
);
|
||||
}
|
||||
|
||||
span
|
||||
}
|
||||
@@ -205,7 +210,6 @@ async fn log_response_middleware(
|
||||
response
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn build_router(
|
||||
state: AppState,
|
||||
resources: &[HttpResource],
|
||||
@@ -269,7 +273,7 @@ pub fn build_router(
|
||||
}
|
||||
mas_config::HttpResource::OAuth => router.merge(mas_handlers::api_router::<AppState>()),
|
||||
mas_config::HttpResource::Compat => {
|
||||
router.merge(mas_handlers::compat_router::<AppState>())
|
||||
router.merge(mas_handlers::compat_router::<AppState>(templates.clone()))
|
||||
}
|
||||
mas_config::HttpResource::AdminApi => {
|
||||
let (_, api_router) = mas_handlers::admin_api_router::<AppState>();
|
||||
@@ -333,7 +337,7 @@ pub fn build_router(
|
||||
// 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(SentryHttpLayer::new().enable_transaction())
|
||||
.layer(NewSentryLayer::new_from_top())
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
//! Utilities to synchronize the configuration file with the database.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use mas_config::{ClientsConfig, UpstreamOAuth2Config};
|
||||
use mas_data_model::Clock;
|
||||
use mas_keystore::Encrypter;
|
||||
use mas_storage::{
|
||||
Clock, Pagination, RepositoryAccess,
|
||||
Pagination, RepositoryAccess,
|
||||
upstream_oauth2::{UpstreamOAuthProviderFilter, UpstreamOAuthProviderParams},
|
||||
};
|
||||
use mas_storage_pg::PgRepository;
|
||||
@@ -37,6 +38,19 @@ fn map_import_action(
|
||||
}
|
||||
}
|
||||
|
||||
fn map_import_on_conflict(
|
||||
config: mas_config::UpstreamOAuth2OnConflict,
|
||||
) -> mas_data_model::UpstreamOAuthProviderOnConflict {
|
||||
match config {
|
||||
mas_config::UpstreamOAuth2OnConflict::Add => {
|
||||
mas_data_model::UpstreamOAuthProviderOnConflict::Add
|
||||
}
|
||||
mas_config::UpstreamOAuth2OnConflict::Fail => {
|
||||
mas_data_model::UpstreamOAuthProviderOnConflict::Fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_claims_imports(
|
||||
config: &mas_config::UpstreamOAuth2ClaimsImports,
|
||||
) -> mas_data_model::UpstreamOAuthProviderClaimsImports {
|
||||
@@ -44,9 +58,10 @@ fn map_claims_imports(
|
||||
subject: mas_data_model::UpstreamOAuthProviderSubjectPreference {
|
||||
template: config.subject.template.clone(),
|
||||
},
|
||||
localpart: mas_data_model::UpstreamOAuthProviderImportPreference {
|
||||
localpart: mas_data_model::UpstreamOAuthProviderLocalpartPreference {
|
||||
action: map_import_action(config.localpart.action),
|
||||
template: config.localpart.template.clone(),
|
||||
on_conflict: map_import_on_conflict(config.localpart.on_conflict),
|
||||
},
|
||||
displayname: mas_data_model::UpstreamOAuthProviderImportPreference {
|
||||
action: map_import_action(config.displayname.action),
|
||||
@@ -117,7 +132,8 @@ pub async fn config_sync(
|
||||
let mut existing_enabled_ids = BTreeSet::new();
|
||||
let mut existing_disabled = BTreeMap::new();
|
||||
// Process the existing providers
|
||||
for provider in page.edges {
|
||||
for edge in page.edges {
|
||||
let provider = edge.node;
|
||||
if provider.enabled() {
|
||||
if config_ids.contains(&provider.id) {
|
||||
existing_enabled_ids.insert(provider.id);
|
||||
@@ -194,12 +210,12 @@ pub async fn config_sync(
|
||||
// private key to hold the content of the private key file.
|
||||
// private key (raw) takes precedence so both can be defined
|
||||
// without issues
|
||||
if siwa.private_key.is_none() {
|
||||
if let Some(private_key_file) = siwa.private_key_file.take() {
|
||||
if siwa.private_key.is_none()
|
||||
&& let Some(private_key_file) = siwa.private_key_file.take()
|
||||
{
|
||||
let key = tokio::fs::read_to_string(private_key_file).await?;
|
||||
siwa.private_key = Some(key);
|
||||
}
|
||||
}
|
||||
let encoded = serde_json::to_vec(&siwa)?;
|
||||
Some(encrypter.encrypt_to_string(&encoded)?)
|
||||
} else {
|
||||
@@ -276,6 +292,18 @@ pub async fn config_sync(
|
||||
}
|
||||
};
|
||||
|
||||
let on_backchannel_logout = match provider.on_backchannel_logout {
|
||||
mas_config::UpstreamOAuth2OnBackchannelLogout::DoNothing => {
|
||||
mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing
|
||||
}
|
||||
mas_config::UpstreamOAuth2OnBackchannelLogout::LogoutBrowserOnly => {
|
||||
mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::LogoutBrowserOnly
|
||||
}
|
||||
mas_config::UpstreamOAuth2OnBackchannelLogout::LogoutAll => {
|
||||
mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::LogoutAll
|
||||
}
|
||||
};
|
||||
|
||||
repo.upstream_oauth_provider()
|
||||
.upsert(
|
||||
clock,
|
||||
@@ -306,6 +334,7 @@ pub async fn config_sync(
|
||||
.collect(),
|
||||
forward_login_hint: provider.forward_login_hint,
|
||||
ui_order,
|
||||
on_backchannel_logout,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -357,7 +386,7 @@ pub async fn config_sync(
|
||||
continue;
|
||||
}
|
||||
|
||||
let client_secret = client.client_secret.as_deref();
|
||||
let client_secret = client.client_secret().await?;
|
||||
let client_name = client.client_name.as_ref();
|
||||
let client_auth_method = client.client_auth_method();
|
||||
let jwks = client.jwks.as_ref();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
mod tokio;
|
||||
|
||||
@@ -23,18 +23,17 @@ use opentelemetry::{
|
||||
trace::TracerProvider as _,
|
||||
};
|
||||
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
|
||||
use opentelemetry_prometheus::PrometheusExporter;
|
||||
use opentelemetry_prometheus_text_exporter::PrometheusExporter;
|
||||
use opentelemetry_sdk::{
|
||||
Resource,
|
||||
metrics::{ManualReader, SdkMeterProvider, periodic_reader_with_async_runtime::PeriodicReader},
|
||||
propagation::{BaggagePropagator, TraceContextPropagator},
|
||||
trace::{
|
||||
Sampler, SdkTracerProvider, Tracer, span_processor_with_async_runtime::BatchSpanProcessor,
|
||||
IdGenerator, Sampler, SdkTracerProvider, Tracer,
|
||||
span_processor_with_async_runtime::BatchSpanProcessor,
|
||||
},
|
||||
};
|
||||
use opentelemetry_semantic_conventions as semcov;
|
||||
use prometheus::Registry;
|
||||
use url::Url;
|
||||
|
||||
static SCOPE: LazyLock<InstrumentationScope> = LazyLock::new(|| {
|
||||
InstrumentationScope::builder(env!("CARGO_PKG_NAME"))
|
||||
@@ -49,7 +48,7 @@ pub static METER: LazyLock<Meter> =
|
||||
pub static TRACER: OnceLock<Tracer> = OnceLock::new();
|
||||
static METER_PROVIDER: OnceLock<SdkMeterProvider> = OnceLock::new();
|
||||
static TRACER_PROVIDER: OnceLock<SdkTracerProvider> = OnceLock::new();
|
||||
static PROMETHEUS_REGISTRY: OnceLock<Registry> = OnceLock::new();
|
||||
static PROMETHEUS_EXPORTER: OnceLock<PrometheusExporter> = OnceLock::new();
|
||||
|
||||
pub fn setup(config: &TelemetryConfig) -> anyhow::Result<()> {
|
||||
let propagator = propagator(&config.tracing.propagators);
|
||||
@@ -95,22 +94,51 @@ fn propagator(propagators: &[Propagator]) -> TextMapCompositePropagator {
|
||||
TextMapCompositePropagator::new(propagators)
|
||||
}
|
||||
|
||||
fn stdout_tracer_provider() -> SdkTracerProvider {
|
||||
/// An [`IdGenerator`] which always returns an invalid trace ID and span ID
|
||||
///
|
||||
/// This is used when no exporter is being used, so that we don't log the trace
|
||||
/// ID when we're not tracing.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct InvalidIdGenerator;
|
||||
impl IdGenerator for InvalidIdGenerator {
|
||||
fn new_trace_id(&self) -> opentelemetry::TraceId {
|
||||
opentelemetry::TraceId::INVALID
|
||||
}
|
||||
fn new_span_id(&self) -> opentelemetry::SpanId {
|
||||
opentelemetry::SpanId::INVALID
|
||||
}
|
||||
}
|
||||
|
||||
fn init_tracer(config: &TracingConfig) -> anyhow::Result<()> {
|
||||
let sample_rate = config.sample_rate.unwrap_or(1.0);
|
||||
|
||||
// We sample traces based on the parent if we have one, and if not, we
|
||||
// sample a ratio based on the configured sample rate
|
||||
let sampler = Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(sample_rate)));
|
||||
|
||||
let tracer_provider_builder = SdkTracerProvider::builder()
|
||||
.with_resource(resource())
|
||||
.with_sampler(sampler);
|
||||
|
||||
let tracer_provider = match config.exporter {
|
||||
TracingExporterKind::None => tracer_provider_builder
|
||||
.with_id_generator(InvalidIdGenerator)
|
||||
.with_sampler(Sampler::AlwaysOff)
|
||||
.build(),
|
||||
|
||||
TracingExporterKind::Stdout => {
|
||||
let exporter = opentelemetry_stdout::SpanExporter::default();
|
||||
SdkTracerProvider::builder()
|
||||
tracer_provider_builder
|
||||
.with_simple_exporter(exporter)
|
||||
.build()
|
||||
}
|
||||
|
||||
fn otlp_tracer_provider(
|
||||
endpoint: Option<&Url>,
|
||||
sample_rate: f64,
|
||||
) -> anyhow::Result<SdkTracerProvider> {
|
||||
TracingExporterKind::Otlp => {
|
||||
let mut exporter = opentelemetry_otlp::SpanExporter::builder()
|
||||
.with_http()
|
||||
.with_http_client(mas_http::reqwest_client());
|
||||
if let Some(endpoint) = endpoint {
|
||||
exporter = exporter.with_endpoint(endpoint.to_string());
|
||||
if let Some(endpoint) = &config.endpoint {
|
||||
exporter = exporter.with_endpoint(endpoint.as_str());
|
||||
}
|
||||
let exporter = exporter
|
||||
.build()
|
||||
@@ -119,26 +147,12 @@ fn otlp_tracer_provider(
|
||||
let batch_processor =
|
||||
BatchSpanProcessor::builder(exporter, opentelemetry_sdk::runtime::Tokio).build();
|
||||
|
||||
// We sample traces based on the parent if we have one, and if not, we
|
||||
// sample a ratio based on the configured sample rate
|
||||
let sampler = Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(sample_rate)));
|
||||
|
||||
let tracer_provider = SdkTracerProvider::builder()
|
||||
tracer_provider_builder
|
||||
.with_span_processor(batch_processor)
|
||||
.with_resource(resource())
|
||||
.with_sampler(sampler)
|
||||
.build();
|
||||
|
||||
Ok(tracer_provider)
|
||||
.build()
|
||||
}
|
||||
|
||||
fn init_tracer(config: &TracingConfig) -> anyhow::Result<()> {
|
||||
let sample_rate = config.sample_rate.unwrap_or(1.0);
|
||||
let tracer_provider = match config.exporter {
|
||||
TracingExporterKind::None => return Ok(()),
|
||||
TracingExporterKind::Stdout => stdout_tracer_provider(),
|
||||
TracingExporterKind::Otlp => otlp_tracer_provider(config.endpoint.as_ref(), sample_rate)?,
|
||||
};
|
||||
|
||||
TRACER_PROVIDER
|
||||
.set(tracer_provider.clone())
|
||||
.map_err(|_| anyhow::anyhow!("TRACER_PROVIDER was set twice"))?;
|
||||
@@ -180,21 +194,30 @@ type PromServiceFuture =
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn prometheus_service_fn<T>(_req: T) -> PromServiceFuture {
|
||||
use prometheus::{Encoder, TextEncoder};
|
||||
let response = if let Some(exporter) = PROMETHEUS_EXPORTER.get() {
|
||||
// We'll need some space for this, so we preallocate a bit
|
||||
let mut buffer = Vec::with_capacity(1024);
|
||||
|
||||
let response = if let Some(registry) = PROMETHEUS_REGISTRY.get() {
|
||||
let mut buffer = Vec::new();
|
||||
let encoder = TextEncoder::new();
|
||||
let metric_families = registry.gather();
|
||||
|
||||
// That shouldn't panic, unless we're constructing invalid labels
|
||||
encoder.encode(&metric_families, &mut buffer).unwrap();
|
||||
if let Err(err) = exporter.export(&mut buffer) {
|
||||
tracing::error!(
|
||||
error = &err as &dyn std::error::Error,
|
||||
"Failed to export Prometheus metrics"
|
||||
);
|
||||
|
||||
Response::builder()
|
||||
.status(500)
|
||||
.header(CONTENT_TYPE, "text/plain")
|
||||
.body(Full::new(Bytes::from_static(
|
||||
b"Failed to export Prometheus metrics, see logs for details",
|
||||
)))
|
||||
.unwrap()
|
||||
} else {
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.header(CONTENT_TYPE, encoder.format_type())
|
||||
.header(CONTENT_TYPE, "text/plain;version=1.0.0")
|
||||
.body(Full::new(Bytes::from(buffer)))
|
||||
.unwrap()
|
||||
}
|
||||
} else {
|
||||
Response::builder()
|
||||
.status(500)
|
||||
@@ -209,7 +232,7 @@ fn prometheus_service_fn<T>(_req: T) -> PromServiceFuture {
|
||||
}
|
||||
|
||||
pub fn prometheus_service<T>() -> tower::util::ServiceFn<fn(T) -> PromServiceFuture> {
|
||||
if PROMETHEUS_REGISTRY.get().is_none() {
|
||||
if PROMETHEUS_EXPORTER.get().is_none() {
|
||||
tracing::warn!(
|
||||
"A Prometheus resource was mounted on a listener, but the Prometheus exporter was not setup in the config"
|
||||
);
|
||||
@@ -219,16 +242,11 @@ pub fn prometheus_service<T>() -> tower::util::ServiceFn<fn(T) -> PromServiceFut
|
||||
}
|
||||
|
||||
fn prometheus_metric_reader() -> anyhow::Result<PrometheusExporter> {
|
||||
let registry = Registry::new();
|
||||
let exporter = PrometheusExporter::builder().without_scope_info().build();
|
||||
|
||||
PROMETHEUS_REGISTRY
|
||||
.set(registry.clone())
|
||||
.map_err(|_| anyhow::anyhow!("PROMETHEUS_REGISTRY was set twice"))?;
|
||||
|
||||
let exporter = opentelemetry_prometheus::exporter()
|
||||
.with_registry(registry)
|
||||
.without_scope_info()
|
||||
.build()?;
|
||||
PROMETHEUS_EXPORTER
|
||||
.set(exporter.clone())
|
||||
.map_err(|_| anyhow::anyhow!("PROMETHEUS_EXPORTER was set twice"))?;
|
||||
|
||||
Ok(exporter)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use opentelemetry::KeyValue;
|
||||
use tokio::runtime::RuntimeMetrics;
|
||||
@@ -9,7 +9,6 @@ use tokio::runtime::RuntimeMetrics;
|
||||
use super::METER;
|
||||
|
||||
/// Install metrics for the tokio runtime.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn observe(metrics: RuntimeMetrics) {
|
||||
{
|
||||
let metrics = metrics.clone();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
@@ -17,7 +17,7 @@ use mas_data_model::{SessionExpirationConfig, SiteConfig};
|
||||
use mas_email::{MailTransport, Mailer};
|
||||
use mas_handlers::passwords::PasswordManager;
|
||||
use mas_matrix::{HomeserverConnection, ReadOnlyHomeserverConnection};
|
||||
use mas_matrix_synapse::SynapseConnection;
|
||||
use mas_matrix_synapse::{LegacySynapseConnection, SynapseConnection};
|
||||
use mas_policy::PolicyFactory;
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_storage::{BoxRepositoryFactory, RepositoryAccess, RepositoryFactory};
|
||||
@@ -211,6 +211,7 @@ pub fn site_config_from_config(
|
||||
password_login_enabled: password_config.enabled(),
|
||||
password_registration_enabled: password_config.enabled()
|
||||
&& account_config.password_registration_enabled,
|
||||
password_registration_email_required: account_config.password_registration_email_required,
|
||||
registration_token_required: account_config.registration_token_required,
|
||||
email_change_allowed: account_config.email_change_allowed,
|
||||
displayname_change_allowed: account_config.displayname_change_allowed,
|
||||
@@ -231,6 +232,7 @@ pub async fn templates_from_config(
|
||||
config: &TemplatesConfig,
|
||||
site_config: &SiteConfig,
|
||||
url_builder: &UrlBuilder,
|
||||
strict: bool,
|
||||
) -> Result<Templates, anyhow::Error> {
|
||||
Templates::load(
|
||||
config.path.clone(),
|
||||
@@ -239,6 +241,7 @@ pub async fn templates_from_config(
|
||||
config.translations_path.clone(),
|
||||
site_config.templates_branding(),
|
||||
site_config.templates_features(),
|
||||
strict,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Failed to load the templates at {}", config.path))
|
||||
@@ -464,28 +467,36 @@ pub async fn load_policy_factory_dynamic_data(
|
||||
|
||||
/// Create a clonable, type-erased [`HomeserverConnection`] from the
|
||||
/// configuration
|
||||
pub fn homeserver_connection_from_config(
|
||||
pub async fn homeserver_connection_from_config(
|
||||
config: &MatrixConfig,
|
||||
http_client: reqwest::Client,
|
||||
) -> Arc<dyn HomeserverConnection> {
|
||||
match config.kind {
|
||||
HomeserverKind::Synapse => Arc::new(SynapseConnection::new(
|
||||
) -> anyhow::Result<Arc<dyn HomeserverConnection>> {
|
||||
Ok(match config.kind {
|
||||
HomeserverKind::Synapse | HomeserverKind::SynapseModern => {
|
||||
Arc::new(SynapseConnection::new(
|
||||
config.homeserver.clone(),
|
||||
config.endpoint.clone(),
|
||||
config.secret.clone(),
|
||||
config.secret().await?,
|
||||
http_client,
|
||||
))
|
||||
}
|
||||
HomeserverKind::SynapseLegacy => Arc::new(LegacySynapseConnection::new(
|
||||
config.homeserver.clone(),
|
||||
config.endpoint.clone(),
|
||||
config.secret().await?,
|
||||
http_client,
|
||||
)),
|
||||
HomeserverKind::SynapseReadOnly => {
|
||||
let connection = SynapseConnection::new(
|
||||
config.homeserver.clone(),
|
||||
config.endpoint.clone(),
|
||||
config.secret.clone(),
|
||||
config.secret().await?,
|
||||
http_client,
|
||||
);
|
||||
let readonly = ReadOnlyHomeserverConnection::new(connection);
|
||||
Arc::new(readonly)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
[package]
|
||||
name = "mas-config"
|
||||
version.workspace = true
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use schemars::{
|
||||
generate::SchemaSettings,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
#![deny(missing_docs, rustdoc::missing_crate_level_docs)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
//! Useful JSON Schema definitions
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -50,6 +50,13 @@ pub struct AccountConfig {
|
||||
#[serde(default = "default_false", skip_serializing_if = "is_default_false")]
|
||||
pub password_registration_enabled: bool,
|
||||
|
||||
/// Whether self-service password registrations require a valid email.
|
||||
/// Defaults to `true`.
|
||||
///
|
||||
/// This has no effect if password registration is disabled.
|
||||
#[serde(default = "default_true", skip_serializing_if = "is_default_true")]
|
||||
pub password_registration_email_required: bool,
|
||||
|
||||
/// Whether users are allowed to change their passwords. Defaults to `true`.
|
||||
///
|
||||
/// This has no effect if password login is disabled.
|
||||
@@ -89,6 +96,7 @@ impl Default for AccountConfig {
|
||||
email_change_allowed: default_true(),
|
||||
displayname_change_allowed: default_true(),
|
||||
password_registration_enabled: default_false(),
|
||||
password_registration_email_required: default_true(),
|
||||
password_change_allowed: default_true(),
|
||||
password_recovery_enabled: default_false(),
|
||||
account_deactivation_allowed: default_true(),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de::Error};
|
||||
@@ -51,7 +51,10 @@ impl CaptchaConfig {
|
||||
impl ConfigurationSection for CaptchaConfig {
|
||||
const PATH: Option<&'static str> = Some("captcha");
|
||||
|
||||
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
|
||||
fn validate(
|
||||
&self,
|
||||
figment: &figment::Figment,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
let metadata = figment.find_metadata(Self::PATH.unwrap());
|
||||
|
||||
let error_on_field = |mut error: figment::error::Error, field: &'static str| {
|
||||
@@ -67,11 +70,11 @@ impl ConfigurationSection for CaptchaConfig {
|
||||
|
||||
if let Some(CaptchaServiceKind::RecaptchaV2) = self.service {
|
||||
if self.site_key.is_none() {
|
||||
return Err(missing_field("site_key"));
|
||||
return Err(missing_field("site_key").into());
|
||||
}
|
||||
|
||||
if self.secret_key.is_none() {
|
||||
return Err(missing_field("secret_key"));
|
||||
return Err(missing_field("secret_key").into());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
use figment::Figment;
|
||||
use anyhow::bail;
|
||||
use camino::Utf8PathBuf;
|
||||
use mas_iana::oauth::OAuthClientAuthenticationMethod;
|
||||
use mas_jose::jwk::PublicJsonWebKeySet;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de::Error};
|
||||
use serde_with::serde_as;
|
||||
use ulid::Ulid;
|
||||
use url::Url;
|
||||
|
||||
@@ -29,6 +31,66 @@ impl From<PublicJsonWebKeySet> for JwksOrJwksUri {
|
||||
}
|
||||
}
|
||||
|
||||
/// Client secret config option.
|
||||
///
|
||||
/// It either holds the client secret value directly or references a file where
|
||||
/// the client secret is stored.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ClientSecret {
|
||||
File(Utf8PathBuf),
|
||||
Value(String),
|
||||
}
|
||||
|
||||
/// Client secret fields as serialized in JSON.
|
||||
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
|
||||
struct ClientSecretRaw {
|
||||
/// Path to the file containing the client secret. The client secret is used
|
||||
/// by the `client_secret_basic`, `client_secret_post` and
|
||||
/// `client_secret_jwt` authentication methods.
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
client_secret_file: Option<Utf8PathBuf>,
|
||||
|
||||
/// Alternative to `client_secret_file`: Reads the client secret directly
|
||||
/// from the config.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
client_secret: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<ClientSecretRaw> for Option<ClientSecret> {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: ClientSecretRaw) -> Result<Self, Self::Error> {
|
||||
match (value.client_secret, value.client_secret_file) {
|
||||
(None, None) => Ok(None),
|
||||
(None, Some(path)) => Ok(Some(ClientSecret::File(path))),
|
||||
(Some(client_secret), None) => Ok(Some(ClientSecret::Value(client_secret))),
|
||||
(Some(_), Some(_)) => {
|
||||
bail!("Cannot specify both `client_secret` and `client_secret_file`")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<ClientSecret>> for ClientSecretRaw {
|
||||
fn from(value: Option<ClientSecret>) -> Self {
|
||||
match value {
|
||||
Some(ClientSecret::File(path)) => ClientSecretRaw {
|
||||
client_secret_file: Some(path),
|
||||
client_secret: None,
|
||||
},
|
||||
Some(ClientSecret::Value(client_secret)) => ClientSecretRaw {
|
||||
client_secret_file: None,
|
||||
client_secret: Some(client_secret),
|
||||
},
|
||||
None => ClientSecretRaw {
|
||||
client_secret_file: None,
|
||||
client_secret: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Authentication method used by clients
|
||||
#[derive(JsonSchema, Serialize, Deserialize, Copy, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -66,6 +128,7 @@ impl std::fmt::Display for ClientAuthMethodConfig {
|
||||
}
|
||||
|
||||
/// An OAuth 2.0 client configuration
|
||||
#[serde_as]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ClientConfig {
|
||||
/// The client ID
|
||||
@@ -85,8 +148,10 @@ pub struct ClientConfig {
|
||||
|
||||
/// 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")]
|
||||
pub client_secret: Option<String>,
|
||||
#[schemars(with = "ClientSecretRaw")]
|
||||
#[serde_as(as = "serde_with::TryFromInto<ClientSecretRaw>")]
|
||||
#[serde(flatten)]
|
||||
pub client_secret: Option<ClientSecret>,
|
||||
|
||||
/// The JSON Web Key Set (JWKS) used by the `private_key_jwt` authentication
|
||||
/// method. Mutually exclusive with `jwks_uri`
|
||||
@@ -104,7 +169,7 @@ pub struct ClientConfig {
|
||||
}
|
||||
|
||||
impl ClientConfig {
|
||||
fn validate(&self) -> Result<(), figment::error::Error> {
|
||||
fn validate(&self) -> Result<(), Box<figment::error::Error>> {
|
||||
let auth_method = self.client_auth_method;
|
||||
match self.client_auth_method {
|
||||
ClientAuthMethodConfig::PrivateKeyJwt => {
|
||||
@@ -112,20 +177,20 @@ impl ClientConfig {
|
||||
let error = figment::error::Error::custom(
|
||||
"jwks or jwks_uri is required for private_key_jwt",
|
||||
);
|
||||
return Err(error.with_path("client_auth_method"));
|
||||
return Err(Box::new(error.with_path("client_auth_method")));
|
||||
}
|
||||
|
||||
if self.jwks.is_some() && self.jwks_uri.is_some() {
|
||||
let error =
|
||||
figment::error::Error::custom("jwks and jwks_uri are mutually exclusive");
|
||||
return Err(error.with_path("jwks"));
|
||||
return Err(Box::new(error.with_path("jwks")));
|
||||
}
|
||||
|
||||
if self.client_secret.is_some() {
|
||||
let error = figment::error::Error::custom(
|
||||
"client_secret is not allowed with private_key_jwt",
|
||||
);
|
||||
return Err(error.with_path("client_secret"));
|
||||
return Err(Box::new(error.with_path("client_secret")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,21 +201,21 @@ impl ClientConfig {
|
||||
let error = figment::error::Error::custom(format!(
|
||||
"client_secret is required for {auth_method}"
|
||||
));
|
||||
return Err(error.with_path("client_auth_method"));
|
||||
return Err(Box::new(error.with_path("client_auth_method")));
|
||||
}
|
||||
|
||||
if self.jwks.is_some() {
|
||||
let error = figment::error::Error::custom(format!(
|
||||
"jwks is not allowed with {auth_method}"
|
||||
));
|
||||
return Err(error.with_path("jwks"));
|
||||
return Err(Box::new(error.with_path("jwks")));
|
||||
}
|
||||
|
||||
if self.jwks_uri.is_some() {
|
||||
let error = figment::error::Error::custom(format!(
|
||||
"jwks_uri is not allowed with {auth_method}"
|
||||
));
|
||||
return Err(error.with_path("jwks_uri"));
|
||||
return Err(Box::new(error.with_path("jwks_uri")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,21 +224,21 @@ impl ClientConfig {
|
||||
let error = figment::error::Error::custom(
|
||||
"client_secret is not allowed with none authentication method",
|
||||
);
|
||||
return Err(error.with_path("client_secret"));
|
||||
return Err(Box::new(error.with_path("client_secret")));
|
||||
}
|
||||
|
||||
if self.jwks.is_some() {
|
||||
let error = figment::error::Error::custom(
|
||||
"jwks is not allowed with none authentication method",
|
||||
);
|
||||
return Err(error);
|
||||
return Err(Box::new(error));
|
||||
}
|
||||
|
||||
if self.jwks_uri.is_some() {
|
||||
let error = figment::error::Error::custom(
|
||||
"jwks_uri is not allowed with none authentication method",
|
||||
);
|
||||
return Err(error);
|
||||
return Err(Box::new(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,6 +263,21 @@ impl ClientConfig {
|
||||
ClientAuthMethodConfig::PrivateKeyJwt => OAuthClientAuthenticationMethod::PrivateKeyJwt,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the client secret.
|
||||
///
|
||||
/// If `client_secret_file` was given, the secret is read from that file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when the client secret could not be read from file.
|
||||
pub async fn client_secret(&self) -> anyhow::Result<Option<String>> {
|
||||
Ok(match &self.client_secret {
|
||||
Some(ClientSecret::File(path)) => Some(tokio::fs::read_to_string(path).await?),
|
||||
Some(ClientSecret::Value(client_secret)) => Some(client_secret.clone()),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// List of OAuth 2.0/OIDC clients config
|
||||
@@ -232,7 +312,10 @@ impl IntoIterator for ClientsConfig {
|
||||
impl ConfigurationSection for ClientsConfig {
|
||||
const PATH: Option<&'static str> = Some("clients");
|
||||
|
||||
fn validate(&self, figment: &Figment) -> Result<(), figment::error::Error> {
|
||||
fn validate(
|
||||
&self,
|
||||
figment: &figment::Figment,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
for (index, client) in self.0.iter().enumerate() {
|
||||
client.validate().map_err(|mut err| {
|
||||
// Save the error location information in the error
|
||||
@@ -256,11 +339,13 @@ mod tests {
|
||||
Figment, Jail,
|
||||
providers::{Format, Yaml},
|
||||
};
|
||||
use tokio::{runtime::Handle, task};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn load_config() {
|
||||
#[tokio::test]
|
||||
async fn load_config() {
|
||||
task::spawn_blocking(|| {
|
||||
Jail::expect_with(|jail| {
|
||||
jail.create_file(
|
||||
"config.yaml",
|
||||
@@ -273,15 +358,15 @@ mod tests {
|
||||
|
||||
- client_id: 01GFWR32NCQ12B8Z0J8CPXRRB6
|
||||
client_auth_method: client_secret_basic
|
||||
client_secret: hello
|
||||
client_secret_file: secret
|
||||
|
||||
- client_id: 01GFWR3WHR93Y5HK389H28VHZ9
|
||||
client_auth_method: client_secret_post
|
||||
client_secret: hello
|
||||
client_secret: c1!3n753c237
|
||||
|
||||
- client_id: 01GFWR43R2ZZ8HX9CVBNW9TJWG
|
||||
client_auth_method: client_secret_jwt
|
||||
client_secret: hello
|
||||
client_secret_file: secret
|
||||
|
||||
- client_id: 01GFWR4BNFDCC4QDG6AMSP1VRR
|
||||
client_auth_method: private_key_jwt
|
||||
@@ -302,6 +387,7 @@ mod tests {
|
||||
n: "0hukqytPwrj1RbMYhYoepCi3CN5k7DwYkTe_Cmb7cP9_qv4ok78KdvFXt5AnQxCRwBD7-qTNkkfMWO2RxUMBdQD0ED6tsSb1n5dp0XY8dSWiBDCX8f6Hr-KolOpvMLZKRy01HdAWcM6RoL9ikbjYHUEW1C8IJnw3MzVHkpKFDL354aptdNLaAdTCBvKzU9WpXo10g-5ctzSlWWjQuecLMQ4G1mNdsR1LHhUENEnOvgT8cDkX0fJzLbEbyBYkdMgKggyVPEB1bg6evG4fTKawgnf0IDSPxIU-wdS9wdSP9ZCJJPLi5CEp-6t6rE_sb2dGcnzjCGlembC57VwpkUvyMw"
|
||||
"#,
|
||||
)?;
|
||||
jail.create_file("secret", r"c1!3n753c237")?;
|
||||
|
||||
let config = Figment::new()
|
||||
.merge(Yaml::file("config.yaml"))
|
||||
@@ -324,7 +410,20 @@ mod tests {
|
||||
);
|
||||
assert_eq!(config.0[1].redirect_uris, Vec::new());
|
||||
|
||||
assert!(config.0[0].client_secret.is_none());
|
||||
assert!(matches!(config.0[1].client_secret, Some(ClientSecret::File(ref p)) if p == "secret"));
|
||||
assert!(matches!(config.0[2].client_secret, Some(ClientSecret::Value(ref v)) if v == "c1!3n753c237"));
|
||||
assert!(matches!(config.0[3].client_secret, Some(ClientSecret::File(ref p)) if p == "secret"));
|
||||
assert!(config.0[4].client_secret.is_none());
|
||||
|
||||
Handle::current().block_on(async move {
|
||||
assert_eq!(config.0[1].client_secret().await.unwrap().unwrap(), "c1!3n753c237");
|
||||
assert_eq!(config.0[2].client_secret().await.unwrap().unwrap(), "c1!3n753c237");
|
||||
assert_eq!(config.0[3].client_secret().await.unwrap().unwrap(), "c1!3n753c237");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::{num::NonZeroU32, time::Duration};
|
||||
|
||||
@@ -222,13 +222,16 @@ pub struct DatabaseConfig {
|
||||
impl ConfigurationSection for DatabaseConfig {
|
||||
const PATH: Option<&'static str> = Some("database");
|
||||
|
||||
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::error::Error> {
|
||||
fn validate(
|
||||
&self,
|
||||
figment: &figment::Figment,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
let metadata = figment.find_metadata(Self::PATH.unwrap());
|
||||
let annotate = |mut error: figment::Error| {
|
||||
error.metadata = metadata.cloned();
|
||||
error.profile = Some(figment::Profile::Default);
|
||||
error.path = vec![Self::PATH.unwrap().to_owned()];
|
||||
Err(error)
|
||||
error
|
||||
};
|
||||
|
||||
// Check that the user did not specify both `uri` and the split options at the
|
||||
@@ -241,37 +244,41 @@ impl ConfigurationSection for DatabaseConfig {
|
||||
|| self.database.is_some();
|
||||
|
||||
if self.uri.is_some() && has_split_options {
|
||||
return annotate(figment::error::Error::from(
|
||||
return Err(annotate(figment::error::Error::from(
|
||||
"uri must not be specified if host, port, socket, username, password, or database are specified".to_owned(),
|
||||
));
|
||||
)).into());
|
||||
}
|
||||
|
||||
if self.ssl_ca.is_some() && self.ssl_ca_file.is_some() {
|
||||
return annotate(figment::error::Error::from(
|
||||
return Err(annotate(figment::error::Error::from(
|
||||
"ssl_ca must not be specified if ssl_ca_file is specified".to_owned(),
|
||||
));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
if self.ssl_certificate.is_some() && self.ssl_certificate_file.is_some() {
|
||||
return annotate(figment::error::Error::from(
|
||||
return Err(annotate(figment::error::Error::from(
|
||||
"ssl_certificate must not be specified if ssl_certificate_file is specified"
|
||||
.to_owned(),
|
||||
));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
if self.ssl_key.is_some() && self.ssl_key_file.is_some() {
|
||||
return annotate(figment::error::Error::from(
|
||||
return Err(annotate(figment::error::Error::from(
|
||||
"ssl_key must not be specified if ssl_key_file is specified".to_owned(),
|
||||
));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
if (self.ssl_key.is_some() || self.ssl_key_file.is_some())
|
||||
^ (self.ssl_certificate.is_some() || self.ssl_certificate_file.is_some())
|
||||
{
|
||||
return annotate(figment::error::Error::from(
|
||||
return Err(annotate(figment::error::Error::from(
|
||||
"both a ssl_certificate and a ssl_key must be set at the same time or none of them"
|
||||
.to_owned(),
|
||||
));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
#![allow(deprecated)]
|
||||
|
||||
@@ -175,7 +175,10 @@ impl Default for EmailConfig {
|
||||
impl ConfigurationSection for EmailConfig {
|
||||
const PATH: Option<&'static str> = Some("email");
|
||||
|
||||
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::error::Error> {
|
||||
fn validate(
|
||||
&self,
|
||||
figment: &figment::Figment,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
let metadata = figment.find_metadata(Self::PATH.unwrap());
|
||||
|
||||
let error_on_field = |mut error: figment::error::Error, field: &'static str| {
|
||||
@@ -201,29 +204,29 @@ impl ConfigurationSection for EmailConfig {
|
||||
|
||||
EmailTransportKind::Smtp => {
|
||||
if let Err(e) = Mailbox::from_str(&self.from) {
|
||||
return Err(error_on_field(figment::error::Error::custom(e), "from"));
|
||||
return Err(error_on_field(figment::error::Error::custom(e), "from").into());
|
||||
}
|
||||
|
||||
if let Err(e) = Mailbox::from_str(&self.reply_to) {
|
||||
return Err(error_on_field(figment::error::Error::custom(e), "reply_to"));
|
||||
return Err(error_on_field(figment::error::Error::custom(e), "reply_to").into());
|
||||
}
|
||||
|
||||
match (self.username.is_some(), self.password.is_some()) {
|
||||
(true, true) | (false, false) => {}
|
||||
(true, false) => {
|
||||
return Err(missing_field("password"));
|
||||
return Err(missing_field("password").into());
|
||||
}
|
||||
(false, true) => {
|
||||
return Err(missing_field("username"));
|
||||
return Err(missing_field("username").into());
|
||||
}
|
||||
}
|
||||
|
||||
if self.mode.is_none() {
|
||||
return Err(missing_field("mode"));
|
||||
return Err(missing_field("mode").into());
|
||||
}
|
||||
|
||||
if self.hostname.is_none() {
|
||||
return Err(missing_field("hostname"));
|
||||
return Err(missing_field("hostname").into());
|
||||
}
|
||||
|
||||
if self.command.is_some() {
|
||||
@@ -239,7 +242,8 @@ impl ConfigurationSection for EmailConfig {
|
||||
"username",
|
||||
"password",
|
||||
],
|
||||
));
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,35 +251,35 @@ impl ConfigurationSection for EmailConfig {
|
||||
let expected_fields = &["from", "reply_to", "transport", "command"];
|
||||
|
||||
if let Err(e) = Mailbox::from_str(&self.from) {
|
||||
return Err(error_on_field(figment::error::Error::custom(e), "from"));
|
||||
return Err(error_on_field(figment::error::Error::custom(e), "from").into());
|
||||
}
|
||||
|
||||
if let Err(e) = Mailbox::from_str(&self.reply_to) {
|
||||
return Err(error_on_field(figment::error::Error::custom(e), "reply_to"));
|
||||
return Err(error_on_field(figment::error::Error::custom(e), "reply_to").into());
|
||||
}
|
||||
|
||||
if self.command.is_none() {
|
||||
return Err(missing_field("command"));
|
||||
return Err(missing_field("command").into());
|
||||
}
|
||||
|
||||
if self.mode.is_some() {
|
||||
return Err(unexpected_field("mode", expected_fields));
|
||||
return Err(unexpected_field("mode", expected_fields).into());
|
||||
}
|
||||
|
||||
if self.hostname.is_some() {
|
||||
return Err(unexpected_field("hostname", expected_fields));
|
||||
return Err(unexpected_field("hostname", expected_fields).into());
|
||||
}
|
||||
|
||||
if self.port.is_some() {
|
||||
return Err(unexpected_field("port", expected_fields));
|
||||
return Err(unexpected_field("port", expected_fields).into());
|
||||
}
|
||||
|
||||
if self.username.is_some() {
|
||||
return Err(unexpected_field("username", expected_fields));
|
||||
return Err(unexpected_field("username", expected_fields).into());
|
||||
}
|
||||
|
||||
if self.password.is_some() {
|
||||
return Err(unexpected_field("password", expected_fields));
|
||||
return Err(unexpected_field("password", expected_fields).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use chrono::Duration;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
#![allow(deprecated)]
|
||||
|
||||
@@ -400,7 +400,10 @@ impl Default for HttpConfig {
|
||||
impl ConfigurationSection for HttpConfig {
|
||||
const PATH: Option<&'static str> = Some("http");
|
||||
|
||||
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
|
||||
fn validate(
|
||||
&self,
|
||||
figment: &figment::Figment,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
for (index, listener) in self.listeners.iter().enumerate() {
|
||||
let annotate = |mut error: figment::Error| {
|
||||
error.metadata = figment
|
||||
@@ -412,49 +415,57 @@ impl ConfigurationSection for HttpConfig {
|
||||
"listeners".to_owned(),
|
||||
index.to_string(),
|
||||
];
|
||||
Err(error)
|
||||
error
|
||||
};
|
||||
|
||||
if listener.resources.is_empty() {
|
||||
return annotate(figment::Error::from("listener has no resources".to_owned()));
|
||||
return Err(
|
||||
annotate(figment::Error::from("listener has no resources".to_owned())).into(),
|
||||
);
|
||||
}
|
||||
|
||||
if listener.binds.is_empty() {
|
||||
return annotate(figment::Error::from(
|
||||
return Err(annotate(figment::Error::from(
|
||||
"listener does not bind to any address".to_owned(),
|
||||
));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
if let Some(tls_config) = &listener.tls {
|
||||
if tls_config.certificate.is_some() && tls_config.certificate_file.is_some() {
|
||||
return annotate(figment::Error::from(
|
||||
return Err(annotate(figment::Error::from(
|
||||
"Only one of `certificate` or `certificate_file` can be set at a time"
|
||||
.to_owned(),
|
||||
));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
if tls_config.certificate.is_none() && tls_config.certificate_file.is_none() {
|
||||
return annotate(figment::Error::from(
|
||||
return Err(annotate(figment::Error::from(
|
||||
"TLS configuration is missing a certificate".to_owned(),
|
||||
));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
if tls_config.key.is_some() && tls_config.key_file.is_some() {
|
||||
return annotate(figment::Error::from(
|
||||
return Err(annotate(figment::Error::from(
|
||||
"Only one of `key` or `key_file` can be set at a time".to_owned(),
|
||||
));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
if tls_config.key.is_none() && tls_config.key_file.is_none() {
|
||||
return annotate(figment::Error::from(
|
||||
return Err(annotate(figment::Error::from(
|
||||
"TLS configuration is missing a private key".to_owned(),
|
||||
));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
if tls_config.password.is_some() && tls_config.password_file.is_some() {
|
||||
return annotate(figment::Error::from(
|
||||
return Err(annotate(figment::Error::from(
|
||||
"Only one of `password` or `password_file` can be set at a time".to_owned(),
|
||||
));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use anyhow::bail;
|
||||
use camino::Utf8PathBuf;
|
||||
use rand::{
|
||||
Rng,
|
||||
distributions::{Alphanumeric, DistString},
|
||||
@@ -27,15 +29,69 @@ fn default_endpoint() -> Url {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HomeserverKind {
|
||||
/// Homeserver is Synapse
|
||||
/// Homeserver is Synapse, version 1.135.0 or newer
|
||||
#[default]
|
||||
Synapse,
|
||||
|
||||
/// Homeserver is Synapse, in read-only mode
|
||||
/// Homeserver is Synapse, version 1.135.0 or newer, in read-only mode
|
||||
///
|
||||
/// This is meant for testing rolling out Matrix Authentication Service with
|
||||
/// no risk of writing data to the homeserver.
|
||||
SynapseReadOnly,
|
||||
|
||||
/// Homeserver is Synapse, using the legacy API
|
||||
SynapseLegacy,
|
||||
|
||||
/// Homeserver is Synapse, with the modern API available (>= 1.135.0)
|
||||
SynapseModern,
|
||||
}
|
||||
|
||||
/// Shared secret between MAS and the homeserver.
|
||||
///
|
||||
/// It either holds the secret value directly or references a file where the
|
||||
/// secret is stored.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Secret {
|
||||
File(Utf8PathBuf),
|
||||
Value(String),
|
||||
}
|
||||
|
||||
/// Secret fields as serialized in JSON.
|
||||
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
|
||||
struct SecretRaw {
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
secret_file: Option<Utf8PathBuf>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
secret: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<SecretRaw> for Secret {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: SecretRaw) -> Result<Self, Self::Error> {
|
||||
match (value.secret, value.secret_file) {
|
||||
(None, None) => bail!("Missing `secret` or `secret_file`"),
|
||||
(None, Some(path)) => Ok(Secret::File(path)),
|
||||
(Some(secret), None) => Ok(Secret::Value(secret)),
|
||||
(Some(_), Some(_)) => bail!("Cannot specify both `secret` and `secret_file`"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Secret> for SecretRaw {
|
||||
fn from(value: Secret) -> Self {
|
||||
match value {
|
||||
Secret::File(path) => SecretRaw {
|
||||
secret_file: Some(path),
|
||||
secret: None,
|
||||
},
|
||||
Secret::Value(secret) => SecretRaw {
|
||||
secret_file: None,
|
||||
secret: Some(secret),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration related to the Matrix homeserver
|
||||
@@ -51,7 +107,10 @@ pub struct MatrixConfig {
|
||||
pub homeserver: String,
|
||||
|
||||
/// Shared secret to use for calls to the admin API
|
||||
pub secret: String,
|
||||
#[schemars(with = "SecretRaw")]
|
||||
#[serde_as(as = "serde_with::TryFromInto<SecretRaw>")]
|
||||
#[serde(flatten)]
|
||||
pub secret: Secret,
|
||||
|
||||
/// The base URL of the homeserver's client API
|
||||
#[serde(default = "default_endpoint")]
|
||||
@@ -63,6 +122,24 @@ impl ConfigurationSection for MatrixConfig {
|
||||
}
|
||||
|
||||
impl MatrixConfig {
|
||||
/// Returns the shared secret.
|
||||
///
|
||||
/// If `secret_file` was given, the secret is read from that file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when the shared secret could not be read from file.
|
||||
pub async fn secret(&self) -> anyhow::Result<String> {
|
||||
Ok(match &self.secret {
|
||||
Secret::File(path) => {
|
||||
let raw = tokio::fs::read_to_string(path).await?;
|
||||
// Trim the secret when read from file to match Synapse's behaviour
|
||||
raw.trim().to_string()
|
||||
}
|
||||
Secret::Value(secret) => secret.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn generate<R>(mut rng: R) -> Self
|
||||
where
|
||||
R: Rng + Send,
|
||||
@@ -70,7 +147,7 @@ impl MatrixConfig {
|
||||
Self {
|
||||
kind: HomeserverKind::default(),
|
||||
homeserver: default_homeserver(),
|
||||
secret: Alphanumeric.sample_string(&mut rng, 32),
|
||||
secret: Secret::Value(Alphanumeric.sample_string(&mut rng, 32)),
|
||||
endpoint: default_endpoint(),
|
||||
}
|
||||
}
|
||||
@@ -79,7 +156,7 @@ impl MatrixConfig {
|
||||
Self {
|
||||
kind: HomeserverKind::default(),
|
||||
homeserver: default_homeserver(),
|
||||
secret: "test".to_owned(),
|
||||
secret: Secret::Value("test".to_owned()),
|
||||
endpoint: default_endpoint(),
|
||||
}
|
||||
}
|
||||
@@ -91,18 +168,51 @@ mod tests {
|
||||
Figment, Jail,
|
||||
providers::{Format, Yaml},
|
||||
};
|
||||
use tokio::{runtime::Handle, task};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn load_config() {
|
||||
#[tokio::test]
|
||||
async fn load_config() {
|
||||
task::spawn_blocking(|| {
|
||||
Jail::expect_with(|jail| {
|
||||
jail.create_file(
|
||||
"config.yaml",
|
||||
r"
|
||||
matrix:
|
||||
homeserver: matrix.org
|
||||
secret: test
|
||||
secret_file: secret
|
||||
",
|
||||
)?;
|
||||
jail.create_file("secret", r"m472!x53c237")?;
|
||||
|
||||
let config = Figment::new()
|
||||
.merge(Yaml::file("config.yaml"))
|
||||
.extract_inner::<MatrixConfig>("matrix")?;
|
||||
|
||||
Handle::current().block_on(async move {
|
||||
assert_eq!(&config.homeserver, "matrix.org");
|
||||
assert!(matches!(config.secret, Secret::File(ref p) if p == "secret"));
|
||||
assert_eq!(config.secret().await.unwrap(), "m472!x53c237");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
});
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_inline_secrets() {
|
||||
task::spawn_blocking(|| {
|
||||
Jail::expect_with(|jail| {
|
||||
jail.create_file(
|
||||
"config.yaml",
|
||||
r"
|
||||
matrix:
|
||||
homeserver: matrix.org
|
||||
secret: m472!x53c237
|
||||
",
|
||||
)?;
|
||||
|
||||
@@ -110,10 +220,16 @@ mod tests {
|
||||
.merge(Yaml::file("config.yaml"))
|
||||
.extract_inner::<MatrixConfig>("matrix")?;
|
||||
|
||||
Handle::current().block_on(async move {
|
||||
assert_eq!(&config.homeserver, "matrix.org");
|
||||
assert_eq!(&config.secret, "test");
|
||||
assert!(matches!(config.secret, Secret::Value(ref v) if v == "m472!x53c237"));
|
||||
assert_eq!(config.secret().await.unwrap(), "m472!x53c237");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
});
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use rand::Rng;
|
||||
use schemars::JsonSchema;
|
||||
@@ -52,7 +52,9 @@ pub use self::{
|
||||
upstream_oauth2::{
|
||||
ClaimsImports as UpstreamOAuth2ClaimsImports, DiscoveryMode as UpstreamOAuth2DiscoveryMode,
|
||||
EmailImportPreference as UpstreamOAuth2EmailImportPreference,
|
||||
ImportAction as UpstreamOAuth2ImportAction, PkceMethod as UpstreamOAuth2PkceMethod,
|
||||
ImportAction as UpstreamOAuth2ImportAction,
|
||||
OnBackchannelLogout as UpstreamOAuth2OnBackchannelLogout,
|
||||
OnConflict as UpstreamOAuth2OnConflict, PkceMethod as UpstreamOAuth2PkceMethod,
|
||||
Provider as UpstreamOAuth2Provider, ResponseMode as UpstreamOAuth2ResponseMode,
|
||||
TokenAuthMethod as UpstreamOAuth2TokenAuthMethod, UpstreamOAuth2Config,
|
||||
},
|
||||
@@ -128,7 +130,10 @@ pub struct RootConfig {
|
||||
}
|
||||
|
||||
impl ConfigurationSection for RootConfig {
|
||||
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
|
||||
fn validate(
|
||||
&self,
|
||||
figment: &figment::Figment,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
self.clients.validate(figment)?;
|
||||
self.http.validate(figment)?;
|
||||
self.database.validate(figment)?;
|
||||
@@ -247,7 +252,10 @@ pub struct AppConfig {
|
||||
}
|
||||
|
||||
impl ConfigurationSection for AppConfig {
|
||||
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
|
||||
fn validate(
|
||||
&self,
|
||||
figment: &figment::Figment,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
self.http.validate(figment)?;
|
||||
self.database.validate(figment)?;
|
||||
self.templates.validate(figment)?;
|
||||
@@ -283,7 +291,10 @@ pub struct SyncConfig {
|
||||
}
|
||||
|
||||
impl ConfigurationSection for SyncConfig {
|
||||
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
|
||||
fn validate(
|
||||
&self,
|
||||
figment: &figment::Figment,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
self.database.validate(figment)?;
|
||||
self.secrets.validate(figment)?;
|
||||
self.clients.validate(figment)?;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::cmp::Reverse;
|
||||
|
||||
@@ -72,12 +72,15 @@ impl Default for PasswordsConfig {
|
||||
impl ConfigurationSection for PasswordsConfig {
|
||||
const PATH: Option<&'static str> = Some("passwords");
|
||||
|
||||
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
|
||||
fn validate(
|
||||
&self,
|
||||
figment: &figment::Figment,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
let annotate = |mut error: figment::Error| {
|
||||
error.metadata = figment.find_metadata(Self::PATH.unwrap()).cloned();
|
||||
error.profile = Some(figment::Profile::Default);
|
||||
error.path = vec![Self::PATH.unwrap().to_owned()];
|
||||
Err(error)
|
||||
error
|
||||
};
|
||||
|
||||
if !self.enabled {
|
||||
@@ -86,16 +89,18 @@ impl ConfigurationSection for PasswordsConfig {
|
||||
}
|
||||
|
||||
if self.schemes.is_empty() {
|
||||
return annotate(figment::Error::from(
|
||||
return Err(annotate(figment::Error::from(
|
||||
"Requires at least one password scheme in the config".to_owned(),
|
||||
));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
for scheme in &self.schemes {
|
||||
if scheme.secret.is_some() && scheme.secret_file.is_some() {
|
||||
return annotate(figment::Error::from(
|
||||
return Err(annotate(figment::Error::from(
|
||||
"Cannot specify both `secret` and `secret_file`".to_owned(),
|
||||
));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use camino::Utf8PathBuf;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::{num::NonZeroU32, time::Duration};
|
||||
|
||||
@@ -117,7 +117,10 @@ pub struct RateLimiterConfiguration {
|
||||
impl ConfigurationSection for RateLimitingConfig {
|
||||
const PATH: Option<&'static str> = Some("rate_limiting");
|
||||
|
||||
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
|
||||
fn validate(
|
||||
&self,
|
||||
figment: &figment::Figment,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
let metadata = figment.find_metadata(Self::PATH.unwrap());
|
||||
|
||||
let error_on_field = |mut error: figment::error::Error, field: &'static str| {
|
||||
@@ -154,25 +157,21 @@ impl ConfigurationSection for RateLimitingConfig {
|
||||
};
|
||||
|
||||
if let Some(error) = error_on_limiter(&self.account_recovery.per_ip) {
|
||||
return Err(error_on_nested_field(error, "account_recovery", "per_ip"));
|
||||
return Err(error_on_nested_field(error, "account_recovery", "per_ip").into());
|
||||
}
|
||||
if let Some(error) = error_on_limiter(&self.account_recovery.per_address) {
|
||||
return Err(error_on_nested_field(
|
||||
error,
|
||||
"account_recovery",
|
||||
"per_address",
|
||||
));
|
||||
return Err(error_on_nested_field(error, "account_recovery", "per_address").into());
|
||||
}
|
||||
|
||||
if let Some(error) = error_on_limiter(&self.registration) {
|
||||
return Err(error_on_field(error, "registration"));
|
||||
return Err(error_on_field(error, "registration").into());
|
||||
}
|
||||
|
||||
if let Some(error) = error_on_limiter(&self.login.per_ip) {
|
||||
return Err(error_on_nested_field(error, "login", "per_ip"));
|
||||
return Err(error_on_nested_field(error, "login", "per_ip").into());
|
||||
}
|
||||
if let Some(error) = error_on_limiter(&self.login.per_account) {
|
||||
return Err(error_on_nested_field(error, "login", "per_account"));
|
||||
return Err(error_on_nested_field(error, "login", "per_account").into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use anyhow::{Context, bail};
|
||||
use camino::Utf8PathBuf;
|
||||
use futures_util::future::{try_join, try_join_all};
|
||||
use mas_jose::jwk::{JsonWebKey, JsonWebKeySet};
|
||||
use mas_jose::jwk::{JsonWebKey, JsonWebKeySet, Thumbprint};
|
||||
use mas_keystore::{Encrypter, Keystore, PrivateKey};
|
||||
use rand::{
|
||||
Rng, SeedableRng,
|
||||
distributions::{Alphanumeric, DistString, Standard},
|
||||
prelude::Distribution as _,
|
||||
};
|
||||
use rand::{Rng, SeedableRng, distributions::Standard, prelude::Distribution as _};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
@@ -128,7 +124,11 @@ impl From<Key> for KeyRaw {
|
||||
#[serde_as]
|
||||
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct KeyConfig {
|
||||
kid: String,
|
||||
/// The key ID `kid` of the key as used by JWKs.
|
||||
///
|
||||
/// If not given, `kid` will be the key’s RFC 7638 JWK Thumbprint.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
kid: Option<String>,
|
||||
|
||||
#[schemars(with = "PasswordRaw")]
|
||||
#[serde_as(as = "serde_with::TryFromInto<PasswordRaw>")]
|
||||
@@ -145,10 +145,10 @@ impl KeyConfig {
|
||||
/// Returns the password in case any is provided.
|
||||
///
|
||||
/// If `password_file` was given, the password is read from that file.
|
||||
async fn password(&self) -> anyhow::Result<Option<Cow<String>>> {
|
||||
async fn password(&self) -> anyhow::Result<Option<Cow<'_, [u8]>>> {
|
||||
Ok(match &self.password {
|
||||
Some(Password::File(path)) => Some(Cow::Owned(tokio::fs::read_to_string(path).await?)),
|
||||
Some(Password::Value(password)) => Some(Cow::Borrowed(password)),
|
||||
Some(Password::File(path)) => Some(Cow::Owned(tokio::fs::read(path).await?)),
|
||||
Some(Password::Value(password)) => Some(Cow::Borrowed(password.as_bytes())),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
@@ -156,10 +156,10 @@ impl KeyConfig {
|
||||
/// Returns the key.
|
||||
///
|
||||
/// If `key_file` was given, the key is read from that file.
|
||||
async fn key(&self) -> anyhow::Result<Cow<String>> {
|
||||
async fn key(&self) -> anyhow::Result<Cow<'_, [u8]>> {
|
||||
Ok(match &self.key {
|
||||
Key::File(path) => Cow::Owned(tokio::fs::read_to_string(path).await?),
|
||||
Key::Value(key) => Cow::Borrowed(key),
|
||||
Key::File(path) => Cow::Owned(tokio::fs::read(path).await?),
|
||||
Key::Value(key) => Cow::Borrowed(key.as_bytes()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -170,12 +170,17 @@ impl KeyConfig {
|
||||
let (key, password) = try_join(self.key(), self.password()).await?;
|
||||
|
||||
let private_key = match password {
|
||||
Some(password) => PrivateKey::load_encrypted(key.as_bytes(), password.as_bytes())?,
|
||||
None => PrivateKey::load(key.as_bytes())?,
|
||||
Some(password) => PrivateKey::load_encrypted(&key, password)?,
|
||||
None => PrivateKey::load(&key)?,
|
||||
};
|
||||
|
||||
let kid = match self.kid.clone() {
|
||||
Some(kid) => kid,
|
||||
None => private_key.thumbprint_sha256_base64(),
|
||||
};
|
||||
|
||||
Ok(JsonWebKey::new(private_key)
|
||||
.with_kid(self.kid.clone())
|
||||
.with_kid(kid)
|
||||
.with_use(mas_iana::jose::JsonWebKeyUse::Sig))
|
||||
}
|
||||
}
|
||||
@@ -299,6 +304,7 @@ impl ConfigurationSection for SecretsConfig {
|
||||
}
|
||||
|
||||
impl SecretsConfig {
|
||||
#[expect(clippy::similar_names, reason = "Key type names are very similar")]
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub(crate) async fn generate<R>(mut rng: R) -> anyhow::Result<Self>
|
||||
where
|
||||
@@ -317,7 +323,7 @@ impl SecretsConfig {
|
||||
.await
|
||||
.context("could not join blocking task")?;
|
||||
let rsa_key = KeyConfig {
|
||||
kid: Alphanumeric.sample_string(&mut rng, 10),
|
||||
kid: None,
|
||||
password: None,
|
||||
key: Key::Value(rsa_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
|
||||
};
|
||||
@@ -333,7 +339,7 @@ impl SecretsConfig {
|
||||
.await
|
||||
.context("could not join blocking task")?;
|
||||
let ec_p256_key = KeyConfig {
|
||||
kid: Alphanumeric.sample_string(&mut rng, 10),
|
||||
kid: None,
|
||||
password: None,
|
||||
key: Key::Value(ec_p256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
|
||||
};
|
||||
@@ -343,13 +349,13 @@ impl SecretsConfig {
|
||||
let ec_p384_key = task::spawn_blocking(move || {
|
||||
let _entered = span.enter();
|
||||
let ret = PrivateKey::generate_ec_p384(key_rng);
|
||||
info!("Done generating EC P-256 key");
|
||||
info!("Done generating EC P-384 key");
|
||||
ret
|
||||
})
|
||||
.await
|
||||
.context("could not join blocking task")?;
|
||||
let ec_p384_key = KeyConfig {
|
||||
kid: Alphanumeric.sample_string(&mut rng, 10),
|
||||
kid: None,
|
||||
password: None,
|
||||
key: Key::Value(ec_p384_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
|
||||
};
|
||||
@@ -365,7 +371,7 @@ impl SecretsConfig {
|
||||
.await
|
||||
.context("could not join blocking task")?;
|
||||
let ec_k256_key = KeyConfig {
|
||||
kid: Alphanumeric.sample_string(&mut rng, 10),
|
||||
kid: None,
|
||||
password: None,
|
||||
key: Key::Value(ec_k256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
|
||||
};
|
||||
@@ -378,7 +384,7 @@ impl SecretsConfig {
|
||||
|
||||
pub(crate) fn test() -> Self {
|
||||
let rsa_key = KeyConfig {
|
||||
kid: "abcdef".to_owned(),
|
||||
kid: None,
|
||||
password: None,
|
||||
key: Key::Value(
|
||||
indoc::indoc! {r"
|
||||
@@ -397,7 +403,7 @@ impl SecretsConfig {
|
||||
),
|
||||
};
|
||||
let ecdsa_key = KeyConfig {
|
||||
kid: "ghijkl".to_owned(),
|
||||
kid: None,
|
||||
password: None,
|
||||
key: Key::Value(
|
||||
indoc::indoc! {r"
|
||||
@@ -417,3 +423,68 @@ impl SecretsConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use figment::{
|
||||
Figment, Jail,
|
||||
providers::{Format, Yaml},
|
||||
};
|
||||
use mas_jose::constraints::Constrainable;
|
||||
use tokio::{runtime::Handle, task};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_inline_secrets() {
|
||||
task::spawn_blocking(|| {
|
||||
Jail::expect_with(|jail| {
|
||||
jail.create_file(
|
||||
"config.yaml",
|
||||
indoc::indoc! {r"
|
||||
secrets:
|
||||
encryption: >-
|
||||
0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff
|
||||
keys:
|
||||
- kid: lekid0
|
||||
key: |
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIOtZfDuXZr/NC0V3sisR4Chf7RZg6a2dpZesoXMlsPeRoAoGCCqGSM49
|
||||
AwEHoUQDQgAECfpqx64lrR85MOhdMxNmIgmz8IfmM5VY9ICX9aoaArnD9FjgkBIl
|
||||
fGmQWxxXDSWH6SQln9tROVZaduenJqDtDw==
|
||||
-----END EC PRIVATE KEY-----
|
||||
- key: |
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIKlZz/GnH0idVH1PnAF4HQNwRafgBaE2tmyN1wjfdOQqoAoGCCqGSM49
|
||||
AwEHoUQDQgAEHrgPeG+Mt8eahih1h4qaPjhl7jT25cdzBkg3dbVks6gBR2Rx4ug9
|
||||
h27LAir5RqxByHvua2XsP46rSTChof78uw==
|
||||
-----END EC PRIVATE KEY-----
|
||||
"},
|
||||
)?;
|
||||
|
||||
let config = Figment::new()
|
||||
.merge(Yaml::file("config.yaml"))
|
||||
.extract_inner::<SecretsConfig>("secrets")?;
|
||||
|
||||
Handle::current().block_on(async move {
|
||||
assert_eq!(
|
||||
config.encryption().await.unwrap(),
|
||||
[
|
||||
0, 0, 17, 17, 34, 34, 51, 51, 68, 68, 85, 85, 102, 102, 119, 119, 136,
|
||||
136, 153, 153, 170, 170, 187, 187, 204, 204, 221, 221, 238, 238, 255,
|
||||
255
|
||||
]
|
||||
);
|
||||
|
||||
let key_store = config.key_store().await.unwrap();
|
||||
assert!(key_store.iter().any(|k| k.kid() == Some("lekid0")));
|
||||
assert!(key_store.iter().any(|k| k.kid() == Some("ONUCn80fsiISFWKrVMEiirNVr-QEvi7uQI0QH9q9q4o")));
|
||||
});
|
||||
|
||||
Ok(())
|
||||
});
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de::Error as _};
|
||||
@@ -182,32 +182,38 @@ impl TelemetryConfig {
|
||||
impl ConfigurationSection for TelemetryConfig {
|
||||
const PATH: Option<&'static str> = Some("telemetry");
|
||||
|
||||
fn validate(&self, _figment: &figment::Figment) -> Result<(), figment::Error> {
|
||||
if let Some(sample_rate) = self.sentry.sample_rate {
|
||||
if !(0.0..=1.0).contains(&sample_rate) {
|
||||
fn validate(
|
||||
&self,
|
||||
_figment: &figment::Figment,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
if let Some(sample_rate) = self.sentry.sample_rate
|
||||
&& !(0.0..=1.0).contains(&sample_rate)
|
||||
{
|
||||
return Err(figment::error::Error::custom(
|
||||
"Sentry sample rate must be between 0.0 and 1.0",
|
||||
)
|
||||
.with_path("sentry.sample_rate"));
|
||||
}
|
||||
.with_path("sentry.sample_rate")
|
||||
.into());
|
||||
}
|
||||
|
||||
if let Some(sample_rate) = self.sentry.traces_sample_rate {
|
||||
if !(0.0..=1.0).contains(&sample_rate) {
|
||||
if let Some(sample_rate) = self.sentry.traces_sample_rate
|
||||
&& !(0.0..=1.0).contains(&sample_rate)
|
||||
{
|
||||
return Err(figment::error::Error::custom(
|
||||
"Sentry sample rate must be between 0.0 and 1.0",
|
||||
)
|
||||
.with_path("sentry.traces_sample_rate"));
|
||||
}
|
||||
.with_path("sentry.traces_sample_rate")
|
||||
.into());
|
||||
}
|
||||
|
||||
if let Some(sample_rate) = self.tracing.sample_rate {
|
||||
if !(0.0..=1.0).contains(&sample_rate) {
|
||||
if let Some(sample_rate) = self.tracing.sample_rate
|
||||
&& !(0.0..=1.0).contains(&sample_rate)
|
||||
{
|
||||
return Err(figment::error::Error::custom(
|
||||
"Tracing sample rate must be between 0.0 and 1.0",
|
||||
)
|
||||
.with_path("tracing.sample_rate"));
|
||||
}
|
||||
.with_path("tracing.sample_rate")
|
||||
.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use camino::Utf8PathBuf;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2024, 2025 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -33,7 +33,10 @@ impl UpstreamOAuth2Config {
|
||||
impl ConfigurationSection for UpstreamOAuth2Config {
|
||||
const PATH: Option<&'static str> = Some("upstream_oauth2");
|
||||
|
||||
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
|
||||
fn validate(
|
||||
&self,
|
||||
figment: &figment::Figment,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
for (index, provider) in self.providers.iter().enumerate() {
|
||||
let annotate = |mut error: figment::Error| {
|
||||
error.metadata = figment
|
||||
@@ -45,15 +48,16 @@ impl ConfigurationSection for UpstreamOAuth2Config {
|
||||
"providers".to_owned(),
|
||||
index.to_string(),
|
||||
];
|
||||
Err(error)
|
||||
error
|
||||
};
|
||||
|
||||
if !matches!(provider.discovery_mode, DiscoveryMode::Disabled)
|
||||
&& provider.issuer.is_none()
|
||||
{
|
||||
return annotate(figment::Error::custom(
|
||||
return Err(annotate(figment::Error::custom(
|
||||
"The `issuer` field is required when discovery is enabled",
|
||||
));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
match provider.token_endpoint_auth_method {
|
||||
@@ -61,16 +65,16 @@ impl ConfigurationSection for UpstreamOAuth2Config {
|
||||
| TokenAuthMethod::PrivateKeyJwt
|
||||
| TokenAuthMethod::SignInWithApple => {
|
||||
if provider.client_secret.is_some() {
|
||||
return annotate(figment::Error::custom(
|
||||
return Err(annotate(figment::Error::custom(
|
||||
"Unexpected field `client_secret` for the selected authentication method",
|
||||
));
|
||||
)).into());
|
||||
}
|
||||
}
|
||||
TokenAuthMethod::ClientSecretBasic
|
||||
| TokenAuthMethod::ClientSecretPost
|
||||
| TokenAuthMethod::ClientSecretJwt => {
|
||||
if provider.client_secret.is_none() {
|
||||
return annotate(figment::Error::missing_field("client_secret"));
|
||||
return Err(annotate(figment::Error::missing_field("client_secret")).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,16 +85,17 @@ impl ConfigurationSection for UpstreamOAuth2Config {
|
||||
| TokenAuthMethod::ClientSecretPost
|
||||
| TokenAuthMethod::SignInWithApple => {
|
||||
if provider.token_endpoint_auth_signing_alg.is_some() {
|
||||
return annotate(figment::Error::custom(
|
||||
return Err(annotate(figment::Error::custom(
|
||||
"Unexpected field `token_endpoint_auth_signing_alg` for the selected authentication method",
|
||||
));
|
||||
)).into());
|
||||
}
|
||||
}
|
||||
TokenAuthMethod::ClientSecretJwt | TokenAuthMethod::PrivateKeyJwt => {
|
||||
if provider.token_endpoint_auth_signing_alg.is_none() {
|
||||
return annotate(figment::Error::missing_field(
|
||||
return Err(annotate(figment::Error::missing_field(
|
||||
"token_endpoint_auth_signing_alg",
|
||||
));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,18 +103,32 @@ impl ConfigurationSection for UpstreamOAuth2Config {
|
||||
match provider.token_endpoint_auth_method {
|
||||
TokenAuthMethod::SignInWithApple => {
|
||||
if provider.sign_in_with_apple.is_none() {
|
||||
return annotate(figment::Error::missing_field("sign_in_with_apple"));
|
||||
return Err(
|
||||
annotate(figment::Error::missing_field("sign_in_with_apple")).into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
if provider.sign_in_with_apple.is_some() {
|
||||
return annotate(figment::Error::custom(
|
||||
return Err(annotate(figment::Error::custom(
|
||||
"Unexpected field `sign_in_with_apple` for the selected authentication method",
|
||||
));
|
||||
)).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
provider.claims_imports.localpart.on_conflict,
|
||||
OnConflict::Add
|
||||
) && !matches!(
|
||||
provider.claims_imports.localpart.action,
|
||||
ImportAction::Force | ImportAction::Require
|
||||
) {
|
||||
return Err(annotate(figment::Error::custom(
|
||||
"The field `action` must be either `force` or `require` when `on_conflict` is set to `add`",
|
||||
)).into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -183,6 +202,26 @@ impl ImportAction {
|
||||
}
|
||||
}
|
||||
|
||||
/// How to handle an existing localpart claim
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OnConflict {
|
||||
/// Fails the sso login on conflict
|
||||
#[default]
|
||||
Fail,
|
||||
|
||||
/// Adds the oauth identity link, regardless of whether there is an existing
|
||||
/// link or not
|
||||
Add,
|
||||
}
|
||||
|
||||
impl OnConflict {
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
const fn is_default(&self) -> bool {
|
||||
matches!(self, OnConflict::Fail)
|
||||
}
|
||||
}
|
||||
|
||||
/// What should be done for the subject attribute
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
|
||||
pub struct SubjectImportPreference {
|
||||
@@ -211,6 +250,10 @@ pub struct LocalpartImportPreference {
|
||||
/// If not provided, the default template is `{{ user.preferred_username }}`
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub template: Option<String>,
|
||||
|
||||
/// How to handle conflicts on the claim, default value is `Fail`
|
||||
#[serde(default, skip_serializing_if = "OnConflict::is_default")]
|
||||
pub on_conflict: OnConflict,
|
||||
}
|
||||
|
||||
impl LocalpartImportPreference {
|
||||
@@ -408,6 +451,29 @@ fn is_default_scope(scope: &str) -> bool {
|
||||
scope == default_scope()
|
||||
}
|
||||
|
||||
/// What to do when receiving an OIDC Backchannel logout request.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OnBackchannelLogout {
|
||||
/// Do nothing
|
||||
#[default]
|
||||
DoNothing,
|
||||
|
||||
/// Only log out the MAS 'browser session' started by this OIDC session
|
||||
LogoutBrowserOnly,
|
||||
|
||||
/// Log out all sessions started by this OIDC session, including MAS
|
||||
/// 'browser sessions' and client sessions
|
||||
LogoutAll,
|
||||
}
|
||||
|
||||
impl OnBackchannelLogout {
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
const fn is_default(&self) -> bool {
|
||||
matches!(self, OnBackchannelLogout::DoNothing)
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for one upstream OAuth 2 provider.
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -583,4 +649,10 @@ pub struct Provider {
|
||||
/// Defaults to `false`.
|
||||
#[serde(default)]
|
||||
pub forward_login_hint: bool,
|
||||
|
||||
/// What to do when receiving an OIDC Backchannel logout request.
|
||||
///
|
||||
/// Defaults to `do_nothing`.
|
||||
#[serde(default, skip_serializing_if = "OnBackchannelLogout::is_default")]
|
||||
pub on_backchannel_logout: OnBackchannelLogout,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// 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.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use figment::{Figment, error::Error as FigmentError};
|
||||
use figment::Figment;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
/// Trait implemented by all configuration section to help loading specific part
|
||||
@@ -18,7 +18,10 @@ pub trait ConfigurationSection: Sized + DeserializeOwned {
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the configuration is invalid
|
||||
fn validate(&self, _figment: &Figment) -> Result<(), FigmentError> {
|
||||
fn validate(
|
||||
&self,
|
||||
_figment: &Figment,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -27,7 +30,9 @@ pub trait ConfigurationSection: Sized + DeserializeOwned {
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the configuration could not be loaded
|
||||
fn extract(figment: &Figment) -> Result<Self, FigmentError> {
|
||||
fn extract(
|
||||
figment: &Figment,
|
||||
) -> Result<Self, Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
let this: Self = if let Some(path) = Self::PATH {
|
||||
figment.extract_inner(path)?
|
||||
} else {
|
||||
@@ -49,7 +54,9 @@ pub trait ConfigurationSectionExt: ConfigurationSection + Default {
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the configuration section is invalid.
|
||||
fn extract_or_default(figment: &Figment) -> Result<Self, figment::Error> {
|
||||
fn extract_or_default(
|
||||
figment: &Figment,
|
||||
) -> Result<Self, Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
let this: Self = if let Some(path) = Self::PATH {
|
||||
// If the configuration section is not present, we return the default value
|
||||
if !figment.contains(path) {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
[package]
|
||||
name = "mas-context"
|
||||
version.workspace = true
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use console::{Color, Style};
|
||||
use opentelemetry::{
|
||||
TraceId,
|
||||
trace::{SamplingDecision, TraceContextExt},
|
||||
};
|
||||
use opentelemetry::TraceId;
|
||||
use tracing::{Level, Subscriber};
|
||||
use tracing_opentelemetry::OtelData;
|
||||
use tracing_subscriber::{
|
||||
@@ -129,34 +126,17 @@ where
|
||||
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::<OtelData>() {
|
||||
let parent_cx_span = otel.parent_cx.span();
|
||||
let sc = parent_cx_span.span_context();
|
||||
|
||||
// Check if the span is sampled, first from the span builder,
|
||||
// then from the parent context if nothing is set there
|
||||
if otel
|
||||
.builder
|
||||
.sampling_result
|
||||
.as_ref()
|
||||
.map_or(sc.is_sampled(), |r| {
|
||||
r.decision == SamplingDecision::RecordAndSample
|
||||
})
|
||||
if let Some(span) = ctx.lookup_current()
|
||||
&& let Some(otel) = span.extensions().get::<OtelData>()
|
||||
&& let Some(trace_id) = otel.trace_id()
|
||||
&& trace_id != TraceId::INVALID
|
||||
{
|
||||
// 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(sc.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)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::{
|
||||
pin::Pin,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
mod fmt;
|
||||
mod future;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
[package]
|
||||
name = "mas-data-model"
|
||||
version.workspace = true
|
||||
@@ -21,10 +26,13 @@ url.workspace = true
|
||||
crc.workspace = true
|
||||
ulid.workspace = true
|
||||
rand.workspace = true
|
||||
rand_chacha.workspace = true
|
||||
regex.workspace = true
|
||||
woothee.workspace = true
|
||||
ruma-common.workspace = true
|
||||
lettre.workspace = true
|
||||
|
||||
mas-iana.workspace = true
|
||||
mas-jose.workspace = true
|
||||
oauth2-types.workspace = true
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user