Merge branch 'main' into rei/reapply_5297
This commit is contained in:
20
.github/workflows/build.yaml
vendored
20
.github/workflows/build.yaml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# Need a full clone so that `git describe` reports the right version
|
||||
fetch-depth: 0
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: ./.github/actions/build-frontend
|
||||
- uses: ./.github/actions/build-policies
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -226,7 +226,7 @@ jobs:
|
||||
steps:
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5.9.0
|
||||
uses: docker/metadata-action@v5.10.0
|
||||
with:
|
||||
images: "${{ env.IMAGE }}"
|
||||
bake-target: docker-metadata-action
|
||||
@@ -242,7 +242,7 @@ jobs:
|
||||
|
||||
- name: Docker meta (debug variant)
|
||||
id: meta-debug
|
||||
uses: docker/metadata-action@v5.9.0
|
||||
uses: docker/metadata-action@v5.10.0
|
||||
with:
|
||||
images: "${{ env.IMAGE }}"
|
||||
bake-target: docker-metadata-action-debug
|
||||
@@ -276,7 +276,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: bake
|
||||
uses: docker/bake-action@v6.9.0
|
||||
uses: docker/bake-action@v6.10.0
|
||||
with:
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
@@ -327,7 +327,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare a release
|
||||
uses: softprops/action-gh-release@v2.4.2
|
||||
uses: softprops/action-gh-release@v2.5.0
|
||||
with:
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
@@ -376,7 +376,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
@@ -396,7 +396,7 @@ jobs:
|
||||
await script({ core, github, context });
|
||||
|
||||
- name: Update unstable release
|
||||
uses: softprops/action-gh-release@v2.4.2
|
||||
uses: softprops/action-gh-release@v2.5.0
|
||||
with:
|
||||
name: "Unstable build"
|
||||
tag_name: unstable
|
||||
@@ -454,7 +454,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
|
||||
28
.github/workflows/ci.yaml
vendored
28
.github/workflows/ci.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: ./.github/actions/build-policies
|
||||
|
||||
@@ -61,10 +61,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6.0.0
|
||||
uses: actions/setup-node@v6.1.0
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -85,10 +85,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6.0.0
|
||||
uses: actions/setup-node@v6.1.0
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -109,10 +109,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6.0.0
|
||||
uses: actions/setup-node@v6.1.0
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
@@ -156,10 +156,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run `cargo-deny`
|
||||
uses: EmbarkStudios/cargo-deny-action@v2.0.13
|
||||
uses: EmbarkStudios/cargo-deny-action@v2.0.14
|
||||
with:
|
||||
rust-version: stable
|
||||
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
run: |
|
||||
@@ -213,7 +213,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@1.89.0
|
||||
@@ -238,7 +238,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -291,7 +291,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
6
.github/workflows/coverage.yaml
vendored
6
.github/workflows/coverage.yaml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: ./.github/actions/build-policies
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: ./.github/actions/build-frontend
|
||||
env:
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
4
.github/workflows/docs.yaml
vendored
4
.github/workflows/docs.yaml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
tool: mdbook
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6.0.0
|
||||
uses: actions/setup-node@v6.1.0
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
|
||||
2
.github/workflows/merge-back.yaml
vendored
2
.github/workflows/merge-back.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
|
||||
8
.github/workflows/release-branch.yaml
vendored
8
.github/workflows/release-branch.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
run: exit 1
|
||||
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -61,10 +61,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6.0.0
|
||||
uses: actions/setup-node@v6.1.0
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
needs: [tag, compute-version, localazy]
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
|
||||
4
.github/workflows/release-bump.yaml
vendored
4
.github/workflows/release-bump.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
run: exit 1
|
||||
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
needs: [tag, compute-version]
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
|
||||
2
.github/workflows/tag.yaml
vendored
2
.github/workflows/tag.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
6
.github/workflows/translations-download.yaml
vendored
6
.github/workflows/translations-download.yaml
vendored
@@ -19,10 +19,10 @@ jobs:
|
||||
run: exit 1
|
||||
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6.0.0
|
||||
uses: actions/setup-node@v6.1.0
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v7.0.8
|
||||
uses: peter-evans/create-pull-request@v8.0.0
|
||||
with:
|
||||
sign-commits: true
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
|
||||
4
.github/workflows/translations-upload.yaml
vendored
4
.github/workflows/translations-upload.yaml
vendored
@@ -18,10 +18,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6.0.0
|
||||
uses: actions/setup-node@v6.1.0
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
|
||||
101
Cargo.lock
generated
101
Cargo.lock
generated
@@ -1089,9 +1089,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
|
||||
checksum = "db05ffb6856bf0ecdf6367558a76a0e8a77b1713044eb92845c692100ed50190"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
@@ -3097,7 +3097,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-axum-utils"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -3131,7 +3131,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-cli"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -3204,7 +3204,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-config"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
@@ -3236,7 +3236,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-context"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"console",
|
||||
"opentelemetry",
|
||||
@@ -3252,7 +3252,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-data-model"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"chrono",
|
||||
@@ -3275,7 +3275,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-email"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"lettre",
|
||||
@@ -3286,7 +3286,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-handlers"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"aide",
|
||||
"anyhow",
|
||||
@@ -3366,7 +3366,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-http"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"headers",
|
||||
@@ -3387,7 +3387,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-i18n"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"icu_calendar",
|
||||
@@ -3409,7 +3409,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-i18n-scan"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"clap",
|
||||
@@ -3423,7 +3423,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-iana"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"schemars 0.9.0",
|
||||
"serde",
|
||||
@@ -3431,7 +3431,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-iana-codegen"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3448,7 +3448,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-jose"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"chrono",
|
||||
@@ -3478,7 +3478,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-keystore"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"base64ct",
|
||||
@@ -3506,7 +3506,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-listener"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -3531,7 +3531,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-matrix"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3541,7 +3541,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-matrix-synapse"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3558,7 +3558,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-oidc-client"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"async-trait",
|
||||
@@ -3594,7 +3594,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-policy"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -3611,7 +3611,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-router"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"serde",
|
||||
@@ -3622,7 +3622,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-spa"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"serde",
|
||||
@@ -3631,7 +3631,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-storage"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -3653,10 +3653,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-storage-pg"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"crc",
|
||||
"futures-util",
|
||||
"mas-data-model",
|
||||
"mas-iana",
|
||||
@@ -3673,6 +3674,7 @@ dependencies = [
|
||||
"sha2",
|
||||
"sqlx",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"ulid",
|
||||
"url",
|
||||
@@ -3681,7 +3683,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-tasks"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3713,7 +3715,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-templates"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -3723,6 +3725,7 @@ dependencies = [
|
||||
"mas-data-model",
|
||||
"mas-i18n",
|
||||
"mas-iana",
|
||||
"mas-policy",
|
||||
"mas-router",
|
||||
"mas-spa",
|
||||
"minijinja",
|
||||
@@ -3744,7 +3747,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-tower"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"http",
|
||||
"opentelemetry",
|
||||
@@ -4014,7 +4017,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oauth2-types"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"base64ct",
|
||||
@@ -5383,9 +5386,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "sentry"
|
||||
version = "0.45.0"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48b85e25e8a1fc13928885e8bf13abe8a09e15c46993aed05d6405f7755d6e20"
|
||||
checksum = "d9794f69ad475e76c057e326175d3088509649e3aed98473106b9fe94ba59424"
|
||||
dependencies = [
|
||||
"httpdate",
|
||||
"reqwest",
|
||||
@@ -5400,9 +5403,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-backtrace"
|
||||
version = "0.45.0"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3253a495ab536f6de1746a58d5d7824b77d75e08e1a4b8ca6fb356839077ae0"
|
||||
checksum = "e81137ad53b8592bd0935459ad74c0376053c40084aa170451e74eeea8dbc6c3"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"regex",
|
||||
@@ -5411,9 +5414,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-contexts"
|
||||
version = "0.45.0"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "027f81a728836e66b88c07666a10f5ed5a35e2695b04eb7aa0fcbed93f814900"
|
||||
checksum = "cfb403c66cc2651a01b9bacda2e7c22cd51f7e8f56f206aa4310147eb3259282"
|
||||
dependencies = [
|
||||
"hostname",
|
||||
"libc",
|
||||
@@ -5425,9 +5428,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-core"
|
||||
version = "0.45.0"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3b6729c8e71ac968edbe9bf2dd4109c162e552b52bacd2b07e24ede1aba84a5"
|
||||
checksum = "cfc409727ae90765ca8ea76fe6c949d6f159a11d02e130b357fa652ee9efcada"
|
||||
dependencies = [
|
||||
"rand 0.9.2",
|
||||
"sentry-types",
|
||||
@@ -5438,9 +5441,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-panic"
|
||||
version = "0.45.0"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ac0471f04f8f97af0c17eeca2c516e23faa1c0271a55bc64371d9ce488c2d40"
|
||||
checksum = "3df79f4e1e72b2a8b75a0ebf49e78709ceb9b3f0b451f13adc92a0361b0aaabe"
|
||||
dependencies = [
|
||||
"sentry-backtrace",
|
||||
"sentry-core",
|
||||
@@ -5448,9 +5451,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-tower"
|
||||
version = "0.45.0"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "417bd48071863a65ca5f33d15af9aabd49a5cee7f97415d3f08ce8c90ed2c531"
|
||||
checksum = "7eec9885bceb8ba374858d015bb6fa39dbb341d94ca088bc8f13bee2e64e2c68"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"http",
|
||||
@@ -5463,9 +5466,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-tracing"
|
||||
version = "0.45.0"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "428f780866a613142dcc81b7f8551ae4d1c056f4df22b6d7ddd9154a9974eb03"
|
||||
checksum = "ff2046f527fd4b75e0b6ab3bd656c67dce42072f828dc4d03c206d15dca74a93"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"sentry-backtrace",
|
||||
@@ -5476,9 +5479,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-types"
|
||||
version = "0.45.0"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c19d1d1967b55659c358886d0f1aa3076488d445f84c7d727d384c675adaec1"
|
||||
checksum = "c7b9b4e4c03a4d3643c18c78b8aa91d2cbee5da047d2fa0ca4bb29bc67e6c55c"
|
||||
dependencies = [
|
||||
"debugid",
|
||||
"hex",
|
||||
@@ -6085,7 +6088,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn2mas"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -6508,12 +6511,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-appender"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
|
||||
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
70
Cargo.toml
70
Cargo.toml
@@ -9,7 +9,7 @@ members = ["crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
# Updated in the CI with a `sed` command
|
||||
package.version = "1.7.0"
|
||||
package.version = "1.8.0"
|
||||
package.license = "AGPL-3.0-only OR LicenseRef-Element-Commercial"
|
||||
package.authors = ["Element Backend Team"]
|
||||
package.edition = "2024"
|
||||
@@ -34,35 +34,35 @@ broken_intra_doc_links = "deny"
|
||||
[workspace.dependencies]
|
||||
|
||||
# Workspace crates
|
||||
mas-axum-utils = { path = "./crates/axum-utils/", version = "=1.7.0" }
|
||||
mas-cli = { path = "./crates/cli/", version = "=1.7.0" }
|
||||
mas-config = { path = "./crates/config/", version = "=1.7.0" }
|
||||
mas-context = { path = "./crates/context/", version = "=1.7.0" }
|
||||
mas-data-model = { path = "./crates/data-model/", version = "=1.7.0" }
|
||||
mas-email = { path = "./crates/email/", version = "=1.7.0" }
|
||||
mas-graphql = { path = "./crates/graphql/", version = "=1.7.0" }
|
||||
mas-handlers = { path = "./crates/handlers/", version = "=1.7.0" }
|
||||
mas-http = { path = "./crates/http/", version = "=1.7.0" }
|
||||
mas-i18n = { path = "./crates/i18n/", version = "=1.7.0" }
|
||||
mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=1.7.0" }
|
||||
mas-iana = { path = "./crates/iana/", version = "=1.7.0" }
|
||||
mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=1.7.0" }
|
||||
mas-jose = { path = "./crates/jose/", version = "=1.7.0" }
|
||||
mas-keystore = { path = "./crates/keystore/", version = "=1.7.0" }
|
||||
mas-listener = { path = "./crates/listener/", version = "=1.7.0" }
|
||||
mas-matrix = { path = "./crates/matrix/", version = "=1.7.0" }
|
||||
mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=1.7.0" }
|
||||
mas-oidc-client = { path = "./crates/oidc-client/", version = "=1.7.0" }
|
||||
mas-policy = { path = "./crates/policy/", version = "=1.7.0" }
|
||||
mas-router = { path = "./crates/router/", version = "=1.7.0" }
|
||||
mas-spa = { path = "./crates/spa/", version = "=1.7.0" }
|
||||
mas-storage = { path = "./crates/storage/", version = "=1.7.0" }
|
||||
mas-storage-pg = { path = "./crates/storage-pg/", version = "=1.7.0" }
|
||||
mas-tasks = { path = "./crates/tasks/", version = "=1.7.0" }
|
||||
mas-templates = { path = "./crates/templates/", version = "=1.7.0" }
|
||||
mas-tower = { path = "./crates/tower/", version = "=1.7.0" }
|
||||
oauth2-types = { path = "./crates/oauth2-types/", version = "=1.7.0" }
|
||||
syn2mas = { path = "./crates/syn2mas", version = "=1.7.0" }
|
||||
mas-axum-utils = { path = "./crates/axum-utils/", version = "=1.8.0" }
|
||||
mas-cli = { path = "./crates/cli/", version = "=1.8.0" }
|
||||
mas-config = { path = "./crates/config/", version = "=1.8.0" }
|
||||
mas-context = { path = "./crates/context/", version = "=1.8.0" }
|
||||
mas-data-model = { path = "./crates/data-model/", version = "=1.8.0" }
|
||||
mas-email = { path = "./crates/email/", version = "=1.8.0" }
|
||||
mas-graphql = { path = "./crates/graphql/", version = "=1.8.0" }
|
||||
mas-handlers = { path = "./crates/handlers/", version = "=1.8.0" }
|
||||
mas-http = { path = "./crates/http/", version = "=1.8.0" }
|
||||
mas-i18n = { path = "./crates/i18n/", version = "=1.8.0" }
|
||||
mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=1.8.0" }
|
||||
mas-iana = { path = "./crates/iana/", version = "=1.8.0" }
|
||||
mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=1.8.0" }
|
||||
mas-jose = { path = "./crates/jose/", version = "=1.8.0" }
|
||||
mas-keystore = { path = "./crates/keystore/", version = "=1.8.0" }
|
||||
mas-listener = { path = "./crates/listener/", version = "=1.8.0" }
|
||||
mas-matrix = { path = "./crates/matrix/", version = "=1.8.0" }
|
||||
mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=1.8.0" }
|
||||
mas-oidc-client = { path = "./crates/oidc-client/", version = "=1.8.0" }
|
||||
mas-policy = { path = "./crates/policy/", version = "=1.8.0" }
|
||||
mas-router = { path = "./crates/router/", version = "=1.8.0" }
|
||||
mas-spa = { path = "./crates/spa/", version = "=1.8.0" }
|
||||
mas-storage = { path = "./crates/storage/", version = "=1.8.0" }
|
||||
mas-storage-pg = { path = "./crates/storage-pg/", version = "=1.8.0" }
|
||||
mas-tasks = { path = "./crates/tasks/", version = "=1.8.0" }
|
||||
mas-templates = { path = "./crates/templates/", version = "=1.8.0" }
|
||||
mas-tower = { path = "./crates/tower/", version = "=1.8.0" }
|
||||
oauth2-types = { path = "./crates/oauth2-types/", version = "=1.8.0" }
|
||||
syn2mas = { path = "./crates/syn2mas", version = "=1.8.0" }
|
||||
|
||||
# OpenAPI schema generation and validation
|
||||
[workspace.dependencies.aide]
|
||||
@@ -177,7 +177,7 @@ features = ["std"]
|
||||
|
||||
# Utility for converting between different cases
|
||||
[workspace.dependencies.convert_case]
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
|
||||
# CRC calculation
|
||||
[workspace.dependencies.crc]
|
||||
@@ -567,18 +567,18 @@ features = [
|
||||
|
||||
# Sentry error tracking
|
||||
[workspace.dependencies.sentry]
|
||||
version = "0.45.0"
|
||||
version = "0.46.0"
|
||||
default-features = false
|
||||
features = ["backtrace", "contexts", "panic", "tower", "reqwest"]
|
||||
|
||||
# Sentry tower layer
|
||||
[workspace.dependencies.sentry-tower]
|
||||
version = "0.45.0"
|
||||
version = "0.46.0"
|
||||
features = ["http", "axum-matched-path"]
|
||||
|
||||
# Sentry tracing integration
|
||||
[workspace.dependencies.sentry-tracing]
|
||||
version = "0.45.0"
|
||||
version = "0.46.0"
|
||||
|
||||
# Serialization and deserialization
|
||||
[workspace.dependencies.serde]
|
||||
@@ -688,7 +688,7 @@ version = "0.1.41"
|
||||
version = "0.3.20"
|
||||
features = ["env-filter"]
|
||||
[workspace.dependencies.tracing-appender]
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
|
||||
# URL manipulation
|
||||
[workspace.dependencies.url]
|
||||
|
||||
@@ -12,10 +12,9 @@ use clap::Parser;
|
||||
use figment::Figment;
|
||||
use mas_config::{ConfigurationSection, RootConfig, SyncConfig};
|
||||
use mas_data_model::{Clock as _, SystemClock};
|
||||
use mas_storage_pg::MIGRATOR;
|
||||
use rand::SeedableRng;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tracing::{Instrument, info, info_span};
|
||||
use tracing::{info, info_span};
|
||||
|
||||
use crate::util::database_connection_from_config;
|
||||
|
||||
@@ -129,9 +128,7 @@ impl Options {
|
||||
// Grab a connection to the database
|
||||
let mut conn = database_connection_from_config(&config.database).await?;
|
||||
|
||||
MIGRATOR
|
||||
.run(&mut conn)
|
||||
.instrument(info_span!("db.migrate"))
|
||||
mas_storage_pg::migrate(&mut conn)
|
||||
.await
|
||||
.context("could not run migrations")?;
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use figment::Figment;
|
||||
use mas_config::{ConfigurationSectionExt, DatabaseConfig};
|
||||
use mas_storage_pg::MIGRATOR;
|
||||
use tracing::{Instrument, info_span};
|
||||
use tracing::info_span;
|
||||
|
||||
use crate::util::database_connection_from_config;
|
||||
|
||||
@@ -35,9 +34,7 @@ impl Options {
|
||||
let mut conn = database_connection_from_config(&config).await?;
|
||||
|
||||
// Run pending migrations
|
||||
MIGRATOR
|
||||
.run(&mut conn)
|
||||
.instrument(info_span!("db.migrate"))
|
||||
mas_storage_pg::migrate(&mut conn)
|
||||
.await
|
||||
.context("could not run migrations")?;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// 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};
|
||||
use std::{process::ExitCode, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
@@ -18,9 +18,8 @@ use mas_data_model::SystemClock;
|
||||
use mas_handlers::{ActivityTracker, CookieManager, Limiter, MetadataCache};
|
||||
use mas_listener::server::Server;
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_storage_pg::{MIGRATOR, PgRepositoryFactory};
|
||||
use sqlx::migrate::Migrate;
|
||||
use tracing::{Instrument, info, info_span, warn};
|
||||
use mas_storage_pg::PgRepositoryFactory;
|
||||
use tracing::{info, info_span, warn};
|
||||
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
@@ -73,24 +72,20 @@ impl Options {
|
||||
let pool = database_pool_from_config(&config.database).await?;
|
||||
|
||||
if self.no_migrate {
|
||||
// Check that we applied all the migrations
|
||||
let mut conn = pool.acquire().await?;
|
||||
let applied = conn.list_applied_migrations().await?;
|
||||
let applied: BTreeSet<_> = applied.into_iter().map(|m| m.version).collect();
|
||||
let has_missing_migrations = MIGRATOR.iter().any(|m| !applied.contains(&m.version));
|
||||
if has_missing_migrations {
|
||||
let pending_migrations = mas_storage_pg::pending_migrations(&mut conn).await?;
|
||||
if !pending_migrations.is_empty() {
|
||||
// Refuse to start if there are pending migrations
|
||||
return Err(anyhow::anyhow!(
|
||||
"The server is running with `--no-migrate` but there are pending. Please run them first with `mas-cli database migrate`, or omit the `--no-migrate` flag to apply them automatically on startup."
|
||||
"The server is running with `--no-migrate` but there are pending migrations. Please run them first with `mas-cli database migrate`, or omit the `--no-migrate` flag to apply them automatically on startup."
|
||||
));
|
||||
}
|
||||
} else {
|
||||
info!("Running pending database migrations");
|
||||
MIGRATOR
|
||||
.run(&pool)
|
||||
.instrument(info_span!("db.migrate"))
|
||||
let mut conn = pool.acquire().await?;
|
||||
mas_storage_pg::migrate(&mut conn)
|
||||
.await
|
||||
.context("could not run database migrations")?;
|
||||
.context("could not run migrations")?;
|
||||
}
|
||||
|
||||
let encrypter = config.secrets.encrypter().await?;
|
||||
|
||||
@@ -14,13 +14,12 @@ use mas_config::{
|
||||
UpstreamOAuth2Config,
|
||||
};
|
||||
use mas_data_model::SystemClock;
|
||||
use mas_storage_pg::MIGRATOR;
|
||||
use rand::thread_rng;
|
||||
use sqlx::{Connection, Either, PgConnection, postgres::PgConnectOptions, types::Uuid};
|
||||
use syn2mas::{
|
||||
LockedMasDatabase, MasWriter, Progress, ProgressStage, SynapseReader, synapse_config,
|
||||
};
|
||||
use tracing::{Instrument, error, info, info_span};
|
||||
use tracing::{Instrument, error, info};
|
||||
|
||||
use crate::util::{DatabaseConnectOptions, database_connection_from_config_with_options};
|
||||
|
||||
@@ -122,9 +121,7 @@ impl Options {
|
||||
)
|
||||
.await?;
|
||||
|
||||
MIGRATOR
|
||||
.run(&mut mas_connection)
|
||||
.instrument(info_span!("db.migrate"))
|
||||
mas_storage_pg::migrate(&mut mas_connection)
|
||||
.await
|
||||
.context("could not run migrations")?;
|
||||
|
||||
|
||||
@@ -45,6 +45,12 @@ fn map_import_on_conflict(
|
||||
mas_config::UpstreamOAuth2OnConflict::Add => {
|
||||
mas_data_model::UpstreamOAuthProviderOnConflict::Add
|
||||
}
|
||||
mas_config::UpstreamOAuth2OnConflict::Replace => {
|
||||
mas_data_model::UpstreamOAuthProviderOnConflict::Replace
|
||||
}
|
||||
mas_config::UpstreamOAuth2OnConflict::Set => {
|
||||
mas_data_model::UpstreamOAuthProviderOnConflict::Set
|
||||
}
|
||||
mas_config::UpstreamOAuth2OnConflict::Fail => {
|
||||
mas_data_model::UpstreamOAuthProviderOnConflict::Fail
|
||||
}
|
||||
@@ -58,6 +64,7 @@ fn map_claims_imports(
|
||||
subject: mas_data_model::UpstreamOAuthProviderSubjectPreference {
|
||||
template: config.subject.template.clone(),
|
||||
},
|
||||
skip_confirmation: config.skip_confirmation,
|
||||
localpart: mas_data_model::UpstreamOAuthProviderLocalpartPreference {
|
||||
action: map_import_action(config.localpart.action),
|
||||
template: config.localpart.template.clone(),
|
||||
|
||||
@@ -145,6 +145,7 @@ pub async fn policy_factory_from_config(
|
||||
register: config.register_entrypoint.clone(),
|
||||
client_registration: config.client_registration_entrypoint.clone(),
|
||||
authorization_grant: config.authorization_grant_entrypoint.clone(),
|
||||
compat_login: config.compat_login_entrypoint.clone(),
|
||||
email: config.email_entrypoint.clone(),
|
||||
};
|
||||
|
||||
|
||||
@@ -62,6 +62,14 @@ fn is_default_password_entrypoint(value: &String) -> bool {
|
||||
*value == default_password_entrypoint()
|
||||
}
|
||||
|
||||
fn default_compat_login_entrypoint() -> String {
|
||||
"compat_login/violation".to_owned()
|
||||
}
|
||||
|
||||
fn is_default_compat_login_entrypoint(value: &String) -> bool {
|
||||
*value == default_compat_login_entrypoint()
|
||||
}
|
||||
|
||||
fn default_email_entrypoint() -> String {
|
||||
"email/violation".to_owned()
|
||||
}
|
||||
@@ -111,6 +119,13 @@ pub struct PolicyConfig {
|
||||
)]
|
||||
pub authorization_grant_entrypoint: String,
|
||||
|
||||
/// Entrypoint to use when evaluating compatibility logins
|
||||
#[serde(
|
||||
default = "default_compat_login_entrypoint",
|
||||
skip_serializing_if = "is_default_compat_login_entrypoint"
|
||||
)]
|
||||
pub compat_login_entrypoint: String,
|
||||
|
||||
/// Entrypoint to use when changing password
|
||||
#[serde(
|
||||
default = "default_password_entrypoint",
|
||||
@@ -137,6 +152,7 @@ impl Default for PolicyConfig {
|
||||
client_registration_entrypoint: default_client_registration_entrypoint(),
|
||||
register_entrypoint: default_register_entrypoint(),
|
||||
authorization_grant_entrypoint: default_authorization_grant_entrypoint(),
|
||||
compat_login_entrypoint: default_compat_login_entrypoint(),
|
||||
password_entrypoint: default_password_entrypoint(),
|
||||
email_entrypoint: default_email_entrypoint(),
|
||||
data: default_data(),
|
||||
|
||||
@@ -118,16 +118,36 @@ impl ConfigurationSection for UpstreamOAuth2Config {
|
||||
}
|
||||
}
|
||||
|
||||
if provider.claims_imports.skip_confirmation {
|
||||
if provider.claims_imports.localpart.action != ImportAction::Require {
|
||||
return Err(annotate(figment::Error::custom(
|
||||
"The field `action` must be `require` when `skip_confirmation` is set to `true`",
|
||||
)).with_path("claims_imports.localpart").into());
|
||||
}
|
||||
|
||||
if provider.claims_imports.email.action == ImportAction::Suggest {
|
||||
return Err(annotate(figment::Error::custom(
|
||||
"The field `action` must not be `suggest` when `skip_confirmation` is set to `true`",
|
||||
)).with_path("claims_imports.email").into());
|
||||
}
|
||||
|
||||
if provider.claims_imports.displayname.action == ImportAction::Suggest {
|
||||
return Err(annotate(figment::Error::custom(
|
||||
"The field `action` must not be `suggest` when `skip_confirmation` is set to `true`",
|
||||
)).with_path("claims_imports.displayname").into());
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
provider.claims_imports.localpart.on_conflict,
|
||||
OnConflict::Add
|
||||
OnConflict::Add | OnConflict::Replace | OnConflict::Set
|
||||
) && !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());
|
||||
"The field `action` must be either `force` or `require` when `on_conflict` is set to `add`, `replace` or `set`",
|
||||
)).with_path("claims_imports.localpart").into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,13 +226,20 @@ impl ImportAction {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OnConflict {
|
||||
/// Fails the sso login on conflict
|
||||
/// Fails the upstream OAuth 2.0 login on conflict
|
||||
#[default]
|
||||
Fail,
|
||||
|
||||
/// Adds the oauth identity link, regardless of whether there is an existing
|
||||
/// link or not
|
||||
/// Adds the upstream OAuth 2.0 identity link, regardless of whether there
|
||||
/// is an existing link or not
|
||||
Add,
|
||||
|
||||
/// Replace any existing upstream OAuth 2.0 identity link
|
||||
Replace,
|
||||
|
||||
/// Adds the upstream OAuth 2.0 identity link *only* if there is no existing
|
||||
/// link for this provider on the matching user
|
||||
Set,
|
||||
}
|
||||
|
||||
impl OnConflict {
|
||||
@@ -326,6 +353,13 @@ pub struct ClaimsImports {
|
||||
#[serde(default, skip_serializing_if = "SubjectImportPreference::is_default")]
|
||||
pub subject: SubjectImportPreference,
|
||||
|
||||
/// Whether to skip the interactive screen prompting the user to confirm the
|
||||
/// attributes that are being imported. This requires `localpart.action` to
|
||||
/// be `require` and other attribute actions to be either `ignore`, `force`
|
||||
/// or `require`
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub skip_confirmation: bool,
|
||||
|
||||
/// Import the localpart of the MXID
|
||||
#[serde(default, skip_serializing_if = "LocalpartImportPreference::is_default")]
|
||||
pub localpart: LocalpartImportPreference,
|
||||
@@ -337,8 +371,7 @@ pub struct ClaimsImports {
|
||||
)]
|
||||
pub displayname: DisplaynameImportPreference,
|
||||
|
||||
/// Import the email address of the user based on the `email` and
|
||||
/// `email_verified` claims
|
||||
/// Import the email address of the user
|
||||
#[serde(default, skip_serializing_if = "EmailImportPreference::is_default")]
|
||||
pub email: EmailImportPreference,
|
||||
|
||||
@@ -354,8 +387,10 @@ impl ClaimsImports {
|
||||
const fn is_default(&self) -> bool {
|
||||
self.subject.is_default()
|
||||
&& self.localpart.is_default()
|
||||
&& !self.skip_confirmation
|
||||
&& self.displayname.is_default()
|
||||
&& self.email.is_default()
|
||||
&& self.account_name.is_default()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ pub use self::{
|
||||
},
|
||||
user_agent::{DeviceType, UserAgent},
|
||||
users::{
|
||||
Authentication, AuthenticationMethod, BrowserSession, Password, User, UserEmail,
|
||||
UserEmailAuthentication, UserEmailAuthenticationCode, UserRecoverySession,
|
||||
Authentication, AuthenticationMethod, BrowserSession, MatrixUser, Password, User,
|
||||
UserEmail, UserEmailAuthentication, UserEmailAuthenticationCode, UserRecoverySession,
|
||||
UserRecoveryTicket, UserRegistration, UserRegistrationPassword, UserRegistrationToken,
|
||||
},
|
||||
utils::{BoxClock, BoxRng},
|
||||
|
||||
@@ -312,6 +312,9 @@ pub struct ClaimsImports {
|
||||
#[serde(default)]
|
||||
pub subject: SubjectPreference,
|
||||
|
||||
#[serde(default)]
|
||||
pub skip_confirmation: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub localpart: LocalpartPreference,
|
||||
|
||||
@@ -415,11 +418,18 @@ impl ImportAction {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OnConflict {
|
||||
/// Fails the upstream OAuth 2.0 login
|
||||
/// Fails the upstream OAuth 2.0 login on conflict
|
||||
#[default]
|
||||
Fail,
|
||||
|
||||
/// Adds the upstream account link, regardless of whether there is an
|
||||
/// existing link or not
|
||||
/// Adds the upstream OAuth 2.0 identity link, regardless of whether there
|
||||
/// is an existing link or not
|
||||
Add,
|
||||
|
||||
/// Replace any existing upstream OAuth 2.0 identity link
|
||||
Replace,
|
||||
|
||||
/// Adds the upstream OAuth 2.0 identity link *only* if there is no existing
|
||||
/// link for this provider on the matching user
|
||||
Set,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ use serde::Serialize;
|
||||
use ulid::Ulid;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct MatrixUser {
|
||||
pub mxid: String,
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct User {
|
||||
pub id: Ulid,
|
||||
|
||||
@@ -16,6 +16,7 @@ use mas_data_model::{
|
||||
User,
|
||||
};
|
||||
use mas_matrix::HomeserverConnection;
|
||||
use mas_policy::{Policy, Requester, ViolationCode, model::CompatLogin};
|
||||
use mas_storage::{
|
||||
BoxRepository, BoxRepositoryFactory, RepositoryAccess,
|
||||
compat::{
|
||||
@@ -37,6 +38,7 @@ use crate::{
|
||||
BoundActivityTracker, Limiter, METER, RequesterFingerprint, impl_from_error_for_route,
|
||||
passwords::{PasswordManager, PasswordVerificationResult},
|
||||
rate_limit::PasswordCheckLimitedError,
|
||||
session::count_user_sessions_for_limiting,
|
||||
};
|
||||
|
||||
static LOGIN_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
|
||||
@@ -213,9 +215,16 @@ pub enum RouteError {
|
||||
|
||||
#[error("failed to provision device")]
|
||||
ProvisionDeviceFailed(#[source] anyhow::Error),
|
||||
|
||||
#[error("login rejected by policy")]
|
||||
PolicyRejected,
|
||||
|
||||
#[error("login rejected by policy (hard session limit reached)")]
|
||||
PolicyHardSessionLimitReached,
|
||||
}
|
||||
|
||||
impl_from_error_for_route!(mas_storage::RepositoryError);
|
||||
impl_from_error_for_route!(mas_policy::EvaluationError);
|
||||
|
||||
impl From<anyhow::Error> for RouteError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
@@ -274,6 +283,16 @@ impl IntoResponse for RouteError {
|
||||
error: "User account has been locked",
|
||||
status: StatusCode::UNAUTHORIZED,
|
||||
},
|
||||
Self::PolicyRejected => MatrixError {
|
||||
errcode: "M_FORBIDDEN",
|
||||
error: "Login denied by the policy enforced by this service",
|
||||
status: StatusCode::FORBIDDEN,
|
||||
},
|
||||
Self::PolicyHardSessionLimitReached => MatrixError {
|
||||
errcode: "M_FORBIDDEN",
|
||||
error: "You have reached your hard device limit. Please visit your account page to sign some out.",
|
||||
status: StatusCode::FORBIDDEN,
|
||||
},
|
||||
};
|
||||
|
||||
(sentry_event_id, response).into_response()
|
||||
@@ -290,6 +309,7 @@ pub(crate) async fn post(
|
||||
State(homeserver): State<Arc<dyn HomeserverConnection>>,
|
||||
State(site_config): State<SiteConfig>,
|
||||
State(limiter): State<Limiter>,
|
||||
mut policy: Policy,
|
||||
requester: RequesterFingerprint,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
MatrixJsonBody(input): MatrixJsonBody<RequestBody>,
|
||||
@@ -329,6 +349,11 @@ pub(crate) async fn post(
|
||||
&limiter,
|
||||
requester,
|
||||
&mut repo,
|
||||
&mut policy,
|
||||
Requester {
|
||||
ip_address: activity_tracker.ip(),
|
||||
user_agent: user_agent.clone(),
|
||||
},
|
||||
username,
|
||||
password,
|
||||
input.device_id, // TODO check for validity
|
||||
@@ -342,6 +367,11 @@ pub(crate) async fn post(
|
||||
&mut rng,
|
||||
&clock,
|
||||
&mut repo,
|
||||
&mut policy,
|
||||
Requester {
|
||||
ip_address: activity_tracker.ip(),
|
||||
user_agent: user_agent.clone(),
|
||||
},
|
||||
&token,
|
||||
input.device_id,
|
||||
input.initial_device_display_name,
|
||||
@@ -459,6 +489,8 @@ async fn token_login(
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
repo: &mut BoxRepository,
|
||||
policy: &mut Policy,
|
||||
requester: Requester,
|
||||
token: &str,
|
||||
requested_device_id: Option<String>,
|
||||
initial_device_display_name: Option<String>,
|
||||
@@ -544,10 +576,38 @@ async fn token_login(
|
||||
Device::generate(rng)
|
||||
};
|
||||
|
||||
repo.app_session()
|
||||
let session_replaced = repo
|
||||
.app_session()
|
||||
.finish_sessions_to_replace_device(clock, &browser_session.user, &device)
|
||||
.await?;
|
||||
|
||||
let session_counts = count_user_sessions_for_limiting(repo, &browser_session.user).await?;
|
||||
|
||||
let res = policy
|
||||
.evaluate_compat_login(mas_policy::CompatLoginInput {
|
||||
user: &browser_session.user,
|
||||
login: CompatLogin::Token,
|
||||
session_replaced,
|
||||
session_counts,
|
||||
requester,
|
||||
})
|
||||
.await?;
|
||||
if !res.valid() {
|
||||
// If the only violation is that we have too many sessions, then handle that
|
||||
// separately.
|
||||
// In the future, we intend to evict some sessions automatically instead. We
|
||||
// don't trigger this if there was some other violation anyway, since that means
|
||||
// that removing a session wouldn't actually unblock the login.
|
||||
if res.violations.len() == 1 {
|
||||
let violation = &res.violations[0];
|
||||
if violation.code == Some(ViolationCode::TooManySessions) {
|
||||
// The only violation is having reached the session limit.
|
||||
return Err(RouteError::PolicyHardSessionLimitReached);
|
||||
}
|
||||
}
|
||||
return Err(RouteError::PolicyRejected);
|
||||
}
|
||||
|
||||
// We first create the session in the database, commit the transaction, then
|
||||
// create it on the homeserver, scheduling a device sync job afterwards to
|
||||
// make sure we don't end up in an inconsistent state.
|
||||
@@ -578,6 +638,8 @@ async fn user_password_login(
|
||||
limiter: &Limiter,
|
||||
requester: RequesterFingerprint,
|
||||
repo: &mut BoxRepository,
|
||||
policy: &mut Policy,
|
||||
policy_requester: Requester,
|
||||
username: &str,
|
||||
password: String,
|
||||
requested_device_id: Option<String>,
|
||||
@@ -647,10 +709,38 @@ async fn user_password_login(
|
||||
Device::generate(&mut rng)
|
||||
};
|
||||
|
||||
repo.app_session()
|
||||
let session_replaced = repo
|
||||
.app_session()
|
||||
.finish_sessions_to_replace_device(clock, &user, &device)
|
||||
.await?;
|
||||
|
||||
let session_counts = count_user_sessions_for_limiting(repo, &user).await?;
|
||||
|
||||
let res = policy
|
||||
.evaluate_compat_login(mas_policy::CompatLoginInput {
|
||||
user: &user,
|
||||
login: CompatLogin::Password,
|
||||
session_replaced,
|
||||
session_counts,
|
||||
requester: policy_requester,
|
||||
})
|
||||
.await?;
|
||||
if !res.valid() {
|
||||
// If the only violation is that we have too many sessions, then handle that
|
||||
// separately.
|
||||
// In the future, we intend to evict some sessions automatically instead. We
|
||||
// don't trigger this if there was some other violation anyway, since that means
|
||||
// that removing a session wouldn't actually unblock the login.
|
||||
if res.violations.len() == 1 {
|
||||
let violation = &res.violations[0];
|
||||
if violation.code == Some(ViolationCode::TooManySessions) {
|
||||
// The only violation is having reached the session limit.
|
||||
return Err(RouteError::PolicyHardSessionLimitReached);
|
||||
}
|
||||
}
|
||||
return Err(RouteError::PolicyRejected);
|
||||
}
|
||||
|
||||
let session = repo
|
||||
.compat_session()
|
||||
.add(
|
||||
|
||||
@@ -4,30 +4,35 @@
|
||||
// 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 std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
extract::{Form, Path, State},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::Query;
|
||||
use axum_extra::{TypedHeader, extract::Query};
|
||||
use chrono::Duration;
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::{
|
||||
InternalError,
|
||||
cookies::CookieJar,
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
};
|
||||
use mas_data_model::{BoxClock, BoxRng, Clock};
|
||||
use mas_data_model::{BoxClock, BoxRng, Clock, MatrixUser};
|
||||
use mas_matrix::HomeserverConnection;
|
||||
use mas_policy::{Policy, model::CompatLogin};
|
||||
use mas_router::{CompatLoginSsoAction, UrlBuilder};
|
||||
use mas_storage::{BoxRepository, RepositoryAccess, compat::CompatSsoLoginRepository};
|
||||
use mas_templates::{CompatSsoContext, ErrorContext, TemplateContext, Templates};
|
||||
use mas_templates::{
|
||||
CompatLoginPolicyViolationContext, CompatSsoContext, ErrorContext, TemplateContext, Templates,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{
|
||||
PreferredLanguage,
|
||||
session::{SessionOrFallback, load_session_or_fallback},
|
||||
BoundActivityTracker, PreferredLanguage,
|
||||
session::{SessionOrFallback, count_user_sessions_for_limiting, load_session_or_fallback},
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -56,10 +61,16 @@ pub async fn get(
|
||||
mut repo: BoxRepository,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
State(homeserver): State<Arc<dyn HomeserverConnection>>,
|
||||
mut policy: Policy,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
cookie_jar: CookieJar,
|
||||
Path(id): Path<Ulid>,
|
||||
Query(params): Query<Params>,
|
||||
) -> Result<Response, InternalError> {
|
||||
let user_agent = user_agent.map(|ua| ua.to_string());
|
||||
|
||||
let (cookie_jar, maybe_session) = match load_session_or_fallback(
|
||||
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
|
||||
)
|
||||
@@ -107,7 +118,69 @@ pub async fn get(
|
||||
return Ok((cookie_jar, Html(content)).into_response());
|
||||
}
|
||||
|
||||
let ctx = CompatSsoContext::new(login)
|
||||
let session_counts = count_user_sessions_for_limiting(&mut repo, &session.user).await?;
|
||||
|
||||
// We can close the repository early, we don't need it at this point
|
||||
repo.save().await?;
|
||||
|
||||
let res = policy
|
||||
.evaluate_compat_login(mas_policy::CompatLoginInput {
|
||||
user: &session.user,
|
||||
login: CompatLogin::Sso {
|
||||
redirect_uri: login.redirect_uri.to_string(),
|
||||
},
|
||||
// We don't know if there's going to be a replacement until we received the device ID,
|
||||
// which happens too late.
|
||||
session_replaced: false,
|
||||
session_counts,
|
||||
requester: mas_policy::Requester {
|
||||
ip_address: activity_tracker.ip(),
|
||||
user_agent,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
if !res.valid() {
|
||||
let ctx = CompatLoginPolicyViolationContext::for_violations(res.violations)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_compat_login_policy_violation(&ctx)?;
|
||||
|
||||
return Ok((StatusCode::FORBIDDEN, cookie_jar, Html(content)).into_response());
|
||||
}
|
||||
|
||||
// Fetch informations about the user. This is purely cosmetic, so we let it
|
||||
// fail and put a 1s timeout to it in case we fail to query it
|
||||
// XXX: we're likely to need this in other places
|
||||
let localpart = &session.user.username;
|
||||
let display_name = match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(1),
|
||||
homeserver.query_user(localpart),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(user)) => user.displayname,
|
||||
Ok(Err(err)) => {
|
||||
tracing::warn!(
|
||||
error = &*err as &dyn std::error::Error,
|
||||
localpart,
|
||||
"Failed to query user"
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!(localpart, "Timed out while querying user");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let matrix_user = MatrixUser {
|
||||
mxid: homeserver.mxid(localpart),
|
||||
display_name,
|
||||
};
|
||||
|
||||
let ctx = CompatSsoContext::new(login, matrix_user)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
@@ -129,11 +202,16 @@ pub async fn post(
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
mut policy: Policy,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
cookie_jar: CookieJar,
|
||||
Path(id): Path<Ulid>,
|
||||
Query(params): Query<Params>,
|
||||
Form(form): Form<ProtectedForm<()>>,
|
||||
) -> Result<Response, InternalError> {
|
||||
let user_agent = user_agent.map(|ua| ua.to_string());
|
||||
|
||||
let (cookie_jar, maybe_session) = match load_session_or_fallback(
|
||||
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
|
||||
)
|
||||
@@ -200,6 +278,37 @@ pub async fn post(
|
||||
redirect_uri
|
||||
};
|
||||
|
||||
let session_counts = count_user_sessions_for_limiting(&mut repo, &session.user).await?;
|
||||
|
||||
let res = policy
|
||||
.evaluate_compat_login(mas_policy::CompatLoginInput {
|
||||
user: &session.user,
|
||||
login: CompatLogin::Sso {
|
||||
redirect_uri: login.redirect_uri.to_string(),
|
||||
},
|
||||
session_counts,
|
||||
// We don't know if there's going to be a replacement until we received the device ID,
|
||||
// which happens too late.
|
||||
session_replaced: false,
|
||||
requester: mas_policy::Requester {
|
||||
ip_address: activity_tracker.ip(),
|
||||
user_agent,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
if !res.valid() {
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||
let ctx = CompatLoginPolicyViolationContext::for_violations(res.violations)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_compat_login_policy_violation(&ctx)?;
|
||||
|
||||
return Ok((StatusCode::FORBIDDEN, cookie_jar, Html(content)).into_response());
|
||||
}
|
||||
|
||||
// Note that if the login is not Pending,
|
||||
// this fails and aborts the transaction.
|
||||
repo.compat_sso_login()
|
||||
|
||||
@@ -272,6 +272,7 @@ where
|
||||
BoxRepository: FromRequestParts<S>,
|
||||
BoxClock: FromRequestParts<S>,
|
||||
BoxRng: FromRequestParts<S>,
|
||||
Policy: FromRequestParts<S>,
|
||||
{
|
||||
// A sub-router for human-facing routes with error handling
|
||||
let human_router = Router::new()
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
// 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};
|
||||
|
||||
use axum::{
|
||||
extract::{Form, Path, State},
|
||||
response::{Html, IntoResponse, Response},
|
||||
@@ -15,8 +17,9 @@ use mas_axum_utils::{
|
||||
cookies::CookieJar,
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
};
|
||||
use mas_data_model::{AuthorizationGrantStage, BoxClock, BoxRng};
|
||||
use mas_data_model::{AuthorizationGrantStage, BoxClock, BoxRng, MatrixUser};
|
||||
use mas_keystore::Keystore;
|
||||
use mas_matrix::HomeserverConnection;
|
||||
use mas_policy::Policy;
|
||||
use mas_router::{PostAuthAction, UrlBuilder};
|
||||
use mas_storage::{
|
||||
@@ -87,6 +90,7 @@ pub(crate) async fn get(
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
State(homeserver): State<Arc<dyn HomeserverConnection>>,
|
||||
mut policy: Policy,
|
||||
mut repo: BoxRepository,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
@@ -138,6 +142,9 @@ pub(crate) async fn get(
|
||||
|
||||
let session_counts = count_user_sessions_for_limiting(&mut repo, &session.user).await?;
|
||||
|
||||
// We can close the repository early, we don't need it at this point
|
||||
repo.save().await?;
|
||||
|
||||
let res = policy
|
||||
.evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
|
||||
user: Some(&session.user),
|
||||
@@ -162,7 +169,37 @@ pub(crate) async fn get(
|
||||
return Ok((cookie_jar, Html(content)).into_response());
|
||||
}
|
||||
|
||||
let ctx = ConsentContext::new(grant, client)
|
||||
// Fetch informations about the user. This is purely cosmetic, so we let it
|
||||
// fail and put a 1s timeout to it in case we fail to query it
|
||||
// XXX: we're likely to need this in other places
|
||||
let localpart = &session.user.username;
|
||||
let display_name = match tokio::time::timeout(
|
||||
Duration::from_secs(1),
|
||||
homeserver.query_user(localpart),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(user)) => user.displayname,
|
||||
Ok(Err(err)) => {
|
||||
tracing::warn!(
|
||||
error = &*err as &dyn std::error::Error,
|
||||
localpart,
|
||||
"Failed to query user"
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!(localpart, "Timed out while querying user");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let matrix_user = MatrixUser {
|
||||
mxid: homeserver.mxid(localpart),
|
||||
display_name,
|
||||
};
|
||||
|
||||
let ctx = ConsentContext::new(grant, client, matrix_user)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
// 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};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
Form,
|
||||
@@ -16,7 +18,8 @@ use mas_axum_utils::{
|
||||
cookies::CookieJar,
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
};
|
||||
use mas_data_model::{BoxClock, BoxRng};
|
||||
use mas_data_model::{BoxClock, BoxRng, MatrixUser};
|
||||
use mas_matrix::HomeserverConnection;
|
||||
use mas_policy::Policy;
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_storage::BoxRepository;
|
||||
@@ -49,6 +52,7 @@ pub(crate) async fn get(
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
State(homeserver): State<Arc<dyn HomeserverConnection>>,
|
||||
mut repo: BoxRepository,
|
||||
mut policy: Policy,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
@@ -105,6 +109,9 @@ pub(crate) async fn get(
|
||||
|
||||
let session_counts = count_user_sessions_for_limiting(&mut repo, &session.user).await?;
|
||||
|
||||
// We can close the repository early, we don't need it at this point
|
||||
repo.save().await?;
|
||||
|
||||
// Evaluate the policy
|
||||
let res = policy
|
||||
.evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
|
||||
@@ -133,7 +140,37 @@ pub(crate) async fn get(
|
||||
return Ok((cookie_jar, Html(content)).into_response());
|
||||
}
|
||||
|
||||
let ctx = DeviceConsentContext::new(grant, client)
|
||||
// Fetch informations about the user. This is purely cosmetic, so we let it
|
||||
// fail and put a 1s timeout to it in case we fail to query it
|
||||
// XXX: we're likely to need this in other places
|
||||
let localpart = &session.user.username;
|
||||
let display_name = match tokio::time::timeout(
|
||||
Duration::from_secs(1),
|
||||
homeserver.query_user(localpart),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(user)) => user.displayname,
|
||||
Ok(Err(err)) => {
|
||||
tracing::warn!(
|
||||
error = &*err as &dyn std::error::Error,
|
||||
localpart,
|
||||
"Failed to query user"
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!(localpart, "Timed out while querying user");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let matrix_user = MatrixUser {
|
||||
mxid: homeserver.mxid(localpart),
|
||||
display_name,
|
||||
};
|
||||
|
||||
let ctx = DeviceConsentContext::new(grant, client, matrix_user)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
@@ -153,6 +190,7 @@ pub(crate) async fn post(
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
State(homeserver): State<Arc<dyn HomeserverConnection>>,
|
||||
mut repo: BoxRepository,
|
||||
mut policy: Policy,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
@@ -265,7 +303,37 @@ pub(crate) async fn post(
|
||||
|
||||
repo.save().await?;
|
||||
|
||||
let ctx = DeviceConsentContext::new(grant, client)
|
||||
// Fetch informations about the user. This is purely cosmetic, so we let it
|
||||
// fail and put a 1s timeout to it in case we fail to query it
|
||||
// XXX: we're likely to need this in other places
|
||||
let localpart = &session.user.username;
|
||||
let display_name = match tokio::time::timeout(
|
||||
Duration::from_secs(1),
|
||||
homeserver.query_user(localpart),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(user)) => user.displayname,
|
||||
Ok(Err(err)) => {
|
||||
tracing::warn!(
|
||||
error = &*err as &dyn std::error::Error,
|
||||
localpart,
|
||||
"Failed to query user"
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!(localpart, "Timed out while querying user");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let matrix_user = MatrixUser {
|
||||
mxid: homeserver.mxid(localpart),
|
||||
display_name,
|
||||
};
|
||||
|
||||
let ctx = DeviceConsentContext::new(grant, client, matrix_user)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
@@ -82,6 +82,7 @@ pub(crate) async fn policy_factory(
|
||||
register: "register/violation".to_owned(),
|
||||
client_registration: "client_registration/violation".to_owned(),
|
||||
authorization_grant: "authorization_grant/violation".to_owned(),
|
||||
compat_login: "compat_login/violation".to_owned(),
|
||||
email: "email/violation".to_owned(),
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use mas_policy::model::{
|
||||
AuthorizationGrantInput, ClientRegistrationInput, EmailInput, RegisterInput,
|
||||
AuthorizationGrantInput, ClientRegistrationInput, CompatLoginInput, EmailInput, RegisterInput,
|
||||
};
|
||||
use schemars::{JsonSchema, generate::SchemaSettings};
|
||||
|
||||
@@ -42,5 +42,6 @@ fn main() {
|
||||
write_schema::<RegisterInput>(output_root, "register_input.json");
|
||||
write_schema::<ClientRegistrationInput>(output_root, "client_registration_input.json");
|
||||
write_schema::<AuthorizationGrantInput>(output_root, "authorization_grant_input.json");
|
||||
write_schema::<CompatLoginInput>(output_root, "compat_login_input.json");
|
||||
write_schema::<EmailInput>(output_root, "email_input.json");
|
||||
}
|
||||
|
||||
@@ -19,8 +19,9 @@ use thiserror::Error;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
|
||||
pub use self::model::{
|
||||
AuthorizationGrantInput, ClientRegistrationInput, Code as ViolationCode, EmailInput,
|
||||
EvaluationResult, GrantType, RegisterInput, RegistrationMethod, Requester, Violation,
|
||||
AuthorizationGrantInput, ClientRegistrationInput, Code as ViolationCode, CompatLoginInput,
|
||||
EmailInput, EvaluationResult, GrantType, RegisterInput, RegistrationMethod, Requester,
|
||||
Violation,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -72,15 +73,17 @@ pub struct Entrypoints {
|
||||
pub register: String,
|
||||
pub client_registration: String,
|
||||
pub authorization_grant: String,
|
||||
pub compat_login: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl Entrypoints {
|
||||
fn all(&self) -> [&str; 4] {
|
||||
fn all(&self) -> [&str; 5] {
|
||||
[
|
||||
self.register.as_str(),
|
||||
self.client_registration.as_str(),
|
||||
self.authorization_grant.as_str(),
|
||||
self.compat_login.as_str(),
|
||||
self.email.as_str(),
|
||||
]
|
||||
}
|
||||
@@ -459,6 +462,30 @@ impl Policy {
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Evaluate the `compat_login` entrypoint.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the policy engine fails to evaluate the entrypoint.
|
||||
#[tracing::instrument(
|
||||
name = "policy.evaluate.compat_login",
|
||||
skip_all,
|
||||
fields(
|
||||
%input.user.id,
|
||||
),
|
||||
)]
|
||||
pub async fn evaluate_compat_login(
|
||||
&mut self,
|
||||
input: CompatLoginInput<'_>,
|
||||
) -> Result<EvaluationResult, EvaluationError> {
|
||||
let [res]: [EvaluationResult; 1] = self
|
||||
.instance
|
||||
.evaluate(&mut self.store, &self.entrypoints.compat_login, &input)
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -468,6 +495,16 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
fn make_entrypoints() -> Entrypoints {
|
||||
Entrypoints {
|
||||
register: "register/violation".to_owned(),
|
||||
client_registration: "client_registration/violation".to_owned(),
|
||||
authorization_grant: "authorization_grant/violation".to_owned(),
|
||||
compat_login: "compat_login/violation".to_owned(),
|
||||
email: "email/violation".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_register() {
|
||||
let data = Data::new("example.com".to_owned(), None).with_rest(serde_json::json!({
|
||||
@@ -484,14 +521,9 @@ mod tests {
|
||||
|
||||
let file = tokio::fs::File::open(path).await.unwrap();
|
||||
|
||||
let entrypoints = Entrypoints {
|
||||
register: "register/violation".to_owned(),
|
||||
client_registration: "client_registration/violation".to_owned(),
|
||||
authorization_grant: "authorization_grant/violation".to_owned(),
|
||||
email: "email/violation".to_owned(),
|
||||
};
|
||||
|
||||
let factory = PolicyFactory::load(file, data, entrypoints).await.unwrap();
|
||||
let factory = PolicyFactory::load(file, data, make_entrypoints())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut policy = factory.instantiate().await.unwrap();
|
||||
|
||||
@@ -551,14 +583,9 @@ mod tests {
|
||||
|
||||
let file = tokio::fs::File::open(path).await.unwrap();
|
||||
|
||||
let entrypoints = Entrypoints {
|
||||
register: "register/violation".to_owned(),
|
||||
client_registration: "client_registration/violation".to_owned(),
|
||||
authorization_grant: "authorization_grant/violation".to_owned(),
|
||||
email: "email/violation".to_owned(),
|
||||
};
|
||||
|
||||
let factory = PolicyFactory::load(file, data, entrypoints).await.unwrap();
|
||||
let factory = PolicyFactory::load(file, data, make_entrypoints())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut policy = factory.instantiate().await.unwrap();
|
||||
|
||||
@@ -620,14 +647,9 @@ mod tests {
|
||||
|
||||
let file = tokio::fs::File::open(path).await.unwrap();
|
||||
|
||||
let entrypoints = Entrypoints {
|
||||
register: "register/violation".to_owned(),
|
||||
client_registration: "client_registration/violation".to_owned(),
|
||||
authorization_grant: "authorization_grant/violation".to_owned(),
|
||||
email: "email/violation".to_owned(),
|
||||
};
|
||||
|
||||
let factory = PolicyFactory::load(file, data, entrypoints).await.unwrap();
|
||||
let factory = PolicyFactory::load(file, data, make_entrypoints())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// That is around 1 MB of JSON data. Each element is a 5-digit string, so 8
|
||||
// characters including the quotes and a comma.
|
||||
|
||||
@@ -17,7 +17,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A well-known policy code.
|
||||
#[derive(Deserialize, Debug, Clone, Copy, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum Code {
|
||||
/// The username is too short.
|
||||
@@ -75,7 +75,7 @@ impl Code {
|
||||
}
|
||||
|
||||
/// A single violation of a policy.
|
||||
#[derive(Deserialize, Debug, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
|
||||
pub struct Violation {
|
||||
pub msg: String,
|
||||
pub redirect_uri: Option<String>,
|
||||
@@ -187,6 +187,42 @@ pub struct AuthorizationGrantInput<'a> {
|
||||
pub requester: Requester,
|
||||
}
|
||||
|
||||
/// Input for the compatibility login policy.
|
||||
#[derive(Serialize, Debug, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct CompatLoginInput<'a> {
|
||||
#[schemars(with = "std::collections::HashMap<String, serde_json::Value>")]
|
||||
pub user: &'a User,
|
||||
|
||||
/// How many sessions the user has.
|
||||
pub session_counts: SessionCounts,
|
||||
|
||||
/// Whether a session will be replaced by this login
|
||||
pub session_replaced: bool,
|
||||
|
||||
/// What type of login is being performed.
|
||||
/// This also determines whether the login is interactive.
|
||||
pub login: CompatLogin,
|
||||
|
||||
pub requester: Requester,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum CompatLogin {
|
||||
/// Used as the interactive part of SSO login.
|
||||
#[serde(rename = "m.login.sso")]
|
||||
Sso { redirect_uri: String },
|
||||
|
||||
/// Used as the final (non-interactive) stage of SSO login.
|
||||
#[serde(rename = "m.login.token")]
|
||||
Token,
|
||||
|
||||
/// Non-interactive password-over-the-API login.
|
||||
#[serde(rename = "m.login.password")]
|
||||
Password,
|
||||
}
|
||||
|
||||
/// Information about how many sessions the user has
|
||||
#[derive(Serialize, Debug, JsonSchema)]
|
||||
pub struct SessionCounts {
|
||||
|
||||
20
crates/storage-pg/.sqlx/query-2f66991d7b9ba58f011d9aef0eb6a38f3b244c2f46444c0ab345de7feff54aba.json
generated
Normal file
20
crates/storage-pg/.sqlx/query-2f66991d7b9ba58f011d9aef0eb6a38f3b244c2f46444c0ab345de7feff54aba.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT current_database() as \"current_database!\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "current_database!",
|
||||
"type_info": "Name"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "2f66991d7b9ba58f011d9aef0eb6a38f3b244c2f46444c0ab345de7feff54aba"
|
||||
}
|
||||
20
crates/storage-pg/.sqlx/query-fbf926f630df5d588df4f1c9c0dc0f594332be5829d5d7c6b66183ac25b3d166.json
generated
Normal file
20
crates/storage-pg/.sqlx/query-fbf926f630df5d588df4f1c9c0dc0f594332be5829d5d7c6b66183ac25b3d166.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT EXISTS (\n SELECT 1\n FROM information_schema.tables\n WHERE table_name = '_sqlx_migrations'\n ) AS \"exists!\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "fbf926f630df5d588df4f1c9c0dc0f594332be5829d5d7c6b66183ac25b3d166"
|
||||
}
|
||||
@@ -19,6 +19,7 @@ workspace = true
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
chrono.workspace = true
|
||||
crc.workspace = true
|
||||
futures-util.workspace = true
|
||||
opentelemetry-semantic-conventions.workspace = true
|
||||
opentelemetry.workspace = true
|
||||
@@ -31,6 +32,7 @@ sha2.workspace = true
|
||||
sqlx.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
tokio.workspace = true
|
||||
ulid.workspace = true
|
||||
url.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
@@ -487,14 +487,15 @@ impl AppSessionRepository for PgAppSessionRepository<'_> {
|
||||
clock: &dyn Clock,
|
||||
user: &User,
|
||||
device: &Device,
|
||||
) -> Result<(), Self::Error> {
|
||||
) -> Result<bool, Self::Error> {
|
||||
let mut affected = false;
|
||||
// TODO need to invoke this from all the oauth2 login sites
|
||||
let span = tracing::info_span!(
|
||||
"db.app_session.finish_sessions_to_replace_device.compat_sessions",
|
||||
{ DB_QUERY_TEXT } = tracing::field::Empty,
|
||||
);
|
||||
let finished_at = clock.now();
|
||||
sqlx::query!(
|
||||
let compat_affected = sqlx::query!(
|
||||
"
|
||||
UPDATE compat_sessions SET finished_at = $3 WHERE user_id = $1 AND device_id = $2 AND finished_at IS NULL
|
||||
",
|
||||
@@ -505,7 +506,9 @@ impl AppSessionRepository for PgAppSessionRepository<'_> {
|
||||
.record(&span)
|
||||
.execute(&mut *self.conn)
|
||||
.instrument(span)
|
||||
.await?;
|
||||
.await?
|
||||
.rows_affected();
|
||||
affected |= compat_affected > 0;
|
||||
|
||||
if let Ok([stable_device_as_scope_token, unstable_device_as_scope_token]) =
|
||||
device.to_scope_token()
|
||||
@@ -514,7 +517,7 @@ impl AppSessionRepository for PgAppSessionRepository<'_> {
|
||||
"db.app_session.finish_sessions_to_replace_device.oauth2_sessions",
|
||||
{ DB_QUERY_TEXT } = tracing::field::Empty,
|
||||
);
|
||||
sqlx::query!(
|
||||
let oauth2_affected = sqlx::query!(
|
||||
"
|
||||
UPDATE oauth2_sessions
|
||||
SET finished_at = $4
|
||||
@@ -530,10 +533,12 @@ impl AppSessionRepository for PgAppSessionRepository<'_> {
|
||||
.record(&span)
|
||||
.execute(&mut *self.conn)
|
||||
.instrument(span)
|
||||
.await?;
|
||||
.await?
|
||||
.rows_affected();
|
||||
affected |= oauth2_affected > 0;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(affected)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +160,15 @@
|
||||
#![deny(clippy::future_not_send, missing_docs)]
|
||||
#![allow(clippy::module_name_repetitions, clippy::blocks_in_conditions)]
|
||||
|
||||
use sqlx::migrate::Migrator;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
|
||||
use ::tracing::{Instrument, debug, info, info_span, warn};
|
||||
use opentelemetry_semantic_conventions::trace::DB_QUERY_TEXT;
|
||||
use sqlx::{
|
||||
Either, PgConnection,
|
||||
migrate::{AppliedMigration, Migrate, MigrateError, Migration, Migrator},
|
||||
postgres::{PgAdvisoryLock, PgAdvisoryLockKey},
|
||||
};
|
||||
|
||||
pub mod app_session;
|
||||
pub mod compat;
|
||||
@@ -186,14 +194,290 @@ pub use self::{
|
||||
tracing::ExecuteExt,
|
||||
};
|
||||
|
||||
/// Embedded migrations, allowing them to run on startup
|
||||
pub static MIGRATOR: Migrator = {
|
||||
// XXX: The macro does not let us ignore missing migrations, so we have to do it
|
||||
// like this. See https://github.com/launchbadge/sqlx/issues/1788
|
||||
let mut m = sqlx::migrate!();
|
||||
/// Embedded migrations in the binary
|
||||
pub static MIGRATOR: Migrator = sqlx::migrate!();
|
||||
|
||||
// We manually removed some migrations because they made us depend on the
|
||||
// `pgcrypto` extension. See: https://github.com/matrix-org/matrix-authentication-service/issues/1557
|
||||
m.ignore_missing = true;
|
||||
m
|
||||
};
|
||||
fn available_migrations() -> BTreeMap<i64, &'static Migration> {
|
||||
MIGRATOR.iter().map(|m| (m.version, m)).collect()
|
||||
}
|
||||
|
||||
/// This is the list of migrations we've removed from the migration history but
|
||||
/// might have been applied in the past
|
||||
#[allow(clippy::inconsistent_digit_grouping)]
|
||||
const ALLOWED_MISSING_MIGRATIONS: &[i64] = &[
|
||||
// https://github.com/matrix-org/matrix-authentication-service/pull/1585
|
||||
20220709_210445,
|
||||
20230330_210841,
|
||||
20230408_110421,
|
||||
];
|
||||
|
||||
fn allowed_missing_migrations() -> BTreeSet<i64> {
|
||||
ALLOWED_MISSING_MIGRATIONS.iter().copied().collect()
|
||||
}
|
||||
|
||||
/// This is a list of possible additional checksums from previous versions of
|
||||
/// migrations. The checksum we store in the database is 48 bytes long. We're
|
||||
/// not really concerned with partial hash collisions, and to avoid this file to
|
||||
/// be completely unreadable, we only store the upper 16 bytes of that hash.
|
||||
#[allow(clippy::inconsistent_digit_grouping)]
|
||||
const ALLOWED_ALTERNATE_CHECKSUMS: &[(i64, u128)] = &[
|
||||
// https://github.com/element-hq/matrix-authentication-service/pull/5300
|
||||
(20250410_000000, 0x8811_c3ef_dbee_8c00_5b49_25da_5d55_9c3f),
|
||||
(20250410_000001, 0x7990_37b3_2193_8a5d_c72f_bccd_95fd_82e5),
|
||||
(20250410_000002, 0xf2b8_f120_deae_27e7_60d0_79a3_0b77_eea3),
|
||||
(20250410_000003, 0x06be_fc2b_cedc_acf4_b981_02c7_b40c_c469),
|
||||
(20250410_000004, 0x0a90_9c6a_dba7_545c_10d9_60eb_6d30_2f50),
|
||||
(20250410_000006, 0xcc7f_5152_6497_5729_d94b_be0d_9c95_8316),
|
||||
(20250410_000007, 0x12e7_cfab_a017_a5a5_4f2c_18fa_541c_ce62),
|
||||
(20250410_000008, 0x171d_62e5_ee1a_f0d9_3639_6c5a_277c_54cd),
|
||||
(20250410_000009, 0xb1a0_93c7_6645_92ad_df45_b395_57bb_a281),
|
||||
(20250410_000010, 0x8089_86ac_7cff_8d86_2850_d287_cdb1_2b57),
|
||||
(20250410_000011, 0x8d9d_3fae_02c9_3d3f_81e4_6242_2b39_b5b8),
|
||||
(20250410_000012, 0x9805_1372_41aa_d5b0_ebe1_ba9d_28c7_faf6),
|
||||
(20250410_000013, 0x7291_9a97_e4d1_0d45_1791_6e8c_3f2d_e34d),
|
||||
(20250410_000014, 0x811d_f965_8127_e168_4aa2_f177_a4e6_f077),
|
||||
(20250410_000015, 0xa639_0780_aab7_d60d_5fcb_771d_13ed_73ee),
|
||||
(20250410_000016, 0x22b6_e909_6de4_39e3_b2b9_c684_7417_fe07),
|
||||
(20250410_000017, 0x9dfe_b6d3_89e4_e509_651b_2793_8d8d_cd32),
|
||||
(20250410_000018, 0x638f_bdbc_2276_5094_020b_cec1_ab95_c07f),
|
||||
(20250410_000019, 0xa283_84bc_5fd5_7cbd_b5fb_b5fe_0255_6845),
|
||||
(20250410_000020, 0x17d1_54b1_7c6e_fc48_61dd_da3d_f8a5_9546),
|
||||
(20250410_000022, 0xbc36_af82_994a_6f93_8aca_a46b_fc3c_ffde),
|
||||
(20250410_000023, 0x54ec_3b07_ac79_443b_9e18_a2b3_2d17_5ab9),
|
||||
(20250410_000024, 0x8ab4_4f80_00b6_58b2_d757_c40f_bc72_3d87),
|
||||
(20250410_000025, 0x5dc4_2ff3_3042_2f45_046d_10af_ab3a_b583),
|
||||
(20250410_000026, 0x5263_c547_0b64_6425_5729_48b2_ce84_7cad),
|
||||
(20250410_000027, 0x0aad_cb50_1d6a_7794_9017_d24d_55e7_1b9d),
|
||||
(20250410_000028, 0x8fc1_92f8_68df_ca4e_3e2b_cddf_bc12_cffe),
|
||||
(20250410_000029, 0x416c_9446_b6a3_1b49_2940_a8ac_c1c2_665a),
|
||||
(20250410_000030, 0x83a5_e51e_25a6_77fb_2b79_6ea5_db1e_364f),
|
||||
(20250410_000031, 0xfa18_a707_9438_dbc7_2cde_b5f1_ee21_5c7e),
|
||||
(20250410_000032, 0xd669_662e_8930_838a_b142_c3fa_7b39_d2a0),
|
||||
(20250410_000033, 0x4019_1053_cabc_191c_c02e_9aa9_407c_0de5),
|
||||
(20250410_000034, 0xdd59_e595_24e6_4dad_c5f7_fef2_90b8_df57),
|
||||
(20250410_000035, 0x09b4_ea53_2da4_9c39_eb10_db33_6a6d_608b),
|
||||
(20250410_000036, 0x3ca5_9c78_8480_e342_d729_907c_d293_2049),
|
||||
(20250410_000037, 0xc857_2a10_450b_0612_822c_2b86_535a_ea7d),
|
||||
(20250410_000038, 0x1642_39da_9c3b_d9fd_b1e1_72b1_db78_b978),
|
||||
(20250410_000039, 0xdd70_b211_6016_bb84_0d84_f04e_eb8a_59d9),
|
||||
(20250410_000040, 0xe435_ead6_c363_a0b6_e048_dd85_0ecb_9499),
|
||||
(20250410_000041, 0xe9f3_122f_70d4_9839_c818_4b18_0192_ae26),
|
||||
(20250410_000043, 0xec5e_1400_483d_c4bf_6014_aba4_ffc3_6236),
|
||||
(20250410_000044, 0x4750_5eba_4095_6664_78d0_27f9_64bf_64f4),
|
||||
(20250410_000045, 0x9a53_bd70_4cad_2bf1_61d4_f143_0c82_681d),
|
||||
(20250410_121612, 0x25f0_9d20_a897_df18_162d_1c47_b68e_81bd),
|
||||
(20250602_212101, 0xd1a8_782c_b3f0_5045_3f46_49a0_bab0_822b),
|
||||
(20250708_155857, 0xb78e_6957_a588_c16a_d292_a0c7_cae9_f290),
|
||||
(20250915_092635, 0x6854_d58b_99d7_3ac5_82f8_25e5_b1c3_cc0b),
|
||||
(20251127_145951, 0x3bcd_d92e_8391_2a2c_8a18_1d76_354f_96c6),
|
||||
];
|
||||
|
||||
fn alternate_checksums_map() -> BTreeMap<i64, HashSet<u128>> {
|
||||
let mut map = BTreeMap::new();
|
||||
for (version, checksum) in ALLOWED_ALTERNATE_CHECKSUMS {
|
||||
map.entry(*version)
|
||||
.or_insert_with(HashSet::new)
|
||||
.insert(*checksum);
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/// Load the list of applied migrations into a map.
|
||||
///
|
||||
/// It's important to use a [`BTreeMap`] so that the migrations are naturally
|
||||
/// ordered by version.
|
||||
async fn applied_migrations_map(
|
||||
conn: &mut PgConnection,
|
||||
) -> Result<BTreeMap<i64, AppliedMigration>, MigrateError> {
|
||||
let applied_migrations = conn
|
||||
.list_applied_migrations()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|m| (m.version, m))
|
||||
.collect();
|
||||
|
||||
Ok(applied_migrations)
|
||||
}
|
||||
|
||||
/// Checks if the migration table exists
|
||||
async fn migration_table_exists(conn: &mut PgConnection) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_name = '_sqlx_migrations'
|
||||
) AS "exists!"
|
||||
"#,
|
||||
)
|
||||
.fetch_one(conn)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Run the migrations on the given connection
|
||||
///
|
||||
/// This function acquires an advisory lock on the database to ensure that only
|
||||
/// one migrator is running at a time.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function returns an error if the migration fails.
|
||||
#[::tracing::instrument(name = "db.migrate", skip_all, err)]
|
||||
pub async fn migrate(conn: &mut PgConnection) -> Result<(), MigrateError> {
|
||||
// Get the database name and use it to derive an advisory lock key. This
|
||||
// is the same lock key used by SQLx default migrator, so that it works even
|
||||
// with older versions of MAS, and when running through `cargo sqlx migrate run`
|
||||
let database_name = sqlx::query_scalar!(r#"SELECT current_database() as "current_database!""#)
|
||||
.fetch_one(&mut *conn)
|
||||
.await
|
||||
.map_err(MigrateError::from)?;
|
||||
|
||||
let lock =
|
||||
PgAdvisoryLock::with_key(PgAdvisoryLockKey::BigInt(generate_lock_id(&database_name)));
|
||||
|
||||
// Try to acquire the migration lock in a loop.
|
||||
//
|
||||
// The reason we do that with a `try_acquire` is because in Postgres, `CREATE
|
||||
// INDEX CONCURRENTLY` will *not* complete whilst an advisory lock is being
|
||||
// acquired on another connection. This then means that if we run two
|
||||
// migration process at the same time, one of them will go through and block
|
||||
// on concurrent index creations, because the other will get stuck trying to
|
||||
// acquire this lock.
|
||||
//
|
||||
// To avoid this, we use `try_acquire`/`pg_advisory_lock_try` in a loop, which
|
||||
// will fail immediately if the lock is held by another connection, allowing
|
||||
// potential 'CREATE INDEX CONCURRENTLY' statements to complete.
|
||||
let mut backoff = std::time::Duration::from_millis(250);
|
||||
let mut conn = conn;
|
||||
let mut locked_connection = loop {
|
||||
match lock.try_acquire(conn).await? {
|
||||
Either::Left(guard) => break guard,
|
||||
Either::Right(conn_) => {
|
||||
warn!(
|
||||
"Another process is already running migrations on the database, waiting {duration}s and trying again…",
|
||||
duration = backoff.as_secs_f32()
|
||||
);
|
||||
tokio::time::sleep(backoff).await;
|
||||
backoff = std::cmp::min(backoff * 2, std::time::Duration::from_secs(5));
|
||||
conn = conn_;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Creates the migration table if missing
|
||||
// We check if the table exists before calling `ensure_migrations_table` to
|
||||
// avoid the pesky 'relation "_sqlx_migrations" already exists, skipping' notice
|
||||
if !migration_table_exists(locked_connection.as_mut()).await? {
|
||||
locked_connection.as_mut().ensure_migrations_table().await?;
|
||||
}
|
||||
|
||||
for migration in pending_migrations(locked_connection.as_mut()).await? {
|
||||
info!(
|
||||
"Applying migration {version}: {description}",
|
||||
version = migration.version,
|
||||
description = migration.description
|
||||
);
|
||||
locked_connection
|
||||
.as_mut()
|
||||
.apply(migration)
|
||||
.instrument(info_span!(
|
||||
"db.migrate.run_migration",
|
||||
db.migration.version = migration.version,
|
||||
db.migration.description = &*migration.description,
|
||||
{ DB_QUERY_TEXT } = &*migration.sql,
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
|
||||
locked_connection.release_now().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the list of pending migrations
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function returns an error if there is a problem checking the applied
|
||||
/// migrations
|
||||
pub async fn pending_migrations(
|
||||
conn: &mut PgConnection,
|
||||
) -> Result<Vec<&'static Migration>, MigrateError> {
|
||||
// Load the maps of available migrations, applied migrations, migrations that
|
||||
// are allowed to be missing, alternate checksums for migrations that changed
|
||||
let available_migrations = available_migrations();
|
||||
let allowed_missing = allowed_missing_migrations();
|
||||
let alternate_checksums = alternate_checksums_map();
|
||||
let applied_migrations = if migration_table_exists(&mut *conn).await? {
|
||||
applied_migrations_map(&mut *conn).await?
|
||||
} else {
|
||||
BTreeMap::new()
|
||||
};
|
||||
|
||||
// Check that all applied migrations are still valid
|
||||
for applied_migration in applied_migrations.values() {
|
||||
// Check that we know about the applied migration
|
||||
if let Some(migration) = available_migrations.get(&applied_migration.version) {
|
||||
// Check the migration checksum
|
||||
if applied_migration.checksum != migration.checksum {
|
||||
// The checksum we have in the database doesn't match the one we
|
||||
// have embedded. This might be because a migration was
|
||||
// intentionally changed, so we check the alternate checksums
|
||||
if let Some(alternates) = alternate_checksums.get(&applied_migration.version) {
|
||||
// This converts the first 16 bytes of the checksum into a u128
|
||||
let Some(applied_checksum_prefix) = applied_migration
|
||||
.checksum
|
||||
.get(..16)
|
||||
.and_then(|bytes| bytes.try_into().ok())
|
||||
.map(u128::from_be_bytes)
|
||||
else {
|
||||
return Err(MigrateError::ExecuteMigration(
|
||||
sqlx::Error::InvalidArgument(
|
||||
"checksum stored in database is invalid".to_owned(),
|
||||
),
|
||||
applied_migration.version,
|
||||
));
|
||||
};
|
||||
|
||||
if !alternates.contains(&applied_checksum_prefix) {
|
||||
warn!(
|
||||
"The database has a migration applied ({version}) which has known alternative checksums {alternates:x?}, but none of them matched {applied_checksum_prefix:x}",
|
||||
version = applied_migration.version,
|
||||
);
|
||||
return Err(MigrateError::VersionMismatch(applied_migration.version));
|
||||
}
|
||||
} else {
|
||||
return Err(MigrateError::VersionMismatch(applied_migration.version));
|
||||
}
|
||||
}
|
||||
} else if allowed_missing.contains(&applied_migration.version) {
|
||||
// The migration is missing, but allowed to be missing
|
||||
debug!(
|
||||
"The database has a migration applied ({version}) that doesn't exist anymore, but it was intentionally removed",
|
||||
version = applied_migration.version
|
||||
);
|
||||
} else {
|
||||
// The migration is missing, warn about it
|
||||
warn!(
|
||||
"The database has a migration applied ({version}) that doesn't exist anymore! This should not happen, unless rolling back to an older version of MAS.",
|
||||
version = applied_migration.version
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(available_migrations
|
||||
.values()
|
||||
.copied()
|
||||
.filter(|migration| {
|
||||
!migration.migration_type.is_down_migration()
|
||||
&& !applied_migrations.contains_key(&migration.version)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
// Copied from the sqlx source code, so that we generate the same lock ID
|
||||
fn generate_lock_id(database_name: &str) -> i64 {
|
||||
const CRC_IEEE: crc::Crc<u32> = crc::Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
|
||||
// 0x3d32ad9e chosen by fair dice roll
|
||||
0x3d32_ad9e * i64::from(CRC_IEEE.checksum(database_name.as_bytes()))
|
||||
}
|
||||
|
||||
@@ -196,12 +196,14 @@ pub trait AppSessionRepository: Send + Sync {
|
||||
/// replacing a device).
|
||||
///
|
||||
/// Should be called *before* creating a new session for the device.
|
||||
///
|
||||
/// Returns true if a session was finished.
|
||||
async fn finish_sessions_to_replace_device(
|
||||
&mut self,
|
||||
clock: &dyn Clock,
|
||||
user: &User,
|
||||
device: &Device,
|
||||
) -> Result<(), Self::Error>;
|
||||
) -> Result<bool, Self::Error>;
|
||||
}
|
||||
|
||||
repository_impl!(AppSessionRepository:
|
||||
@@ -218,5 +220,5 @@ repository_impl!(AppSessionRepository:
|
||||
clock: &dyn Clock,
|
||||
user: &User,
|
||||
device: &Device,
|
||||
) -> Result<(), Self::Error>;
|
||||
) -> Result<bool, Self::Error>;
|
||||
);
|
||||
|
||||
@@ -41,6 +41,7 @@ oauth2-types.workspace = true
|
||||
mas-data-model.workspace = true
|
||||
mas-i18n.workspace = true
|
||||
mas-iana.workspace = true
|
||||
mas-policy.workspace = true
|
||||
mas-router.workspace = true
|
||||
mas-spa.workspace = true
|
||||
|
||||
|
||||
@@ -21,13 +21,15 @@ use chrono::{DateTime, Duration, Utc};
|
||||
use http::{Method, Uri, Version};
|
||||
use mas_data_model::{
|
||||
AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
|
||||
DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports,
|
||||
UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderOnBackchannelLogout,
|
||||
UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderTokenAuthMethod, User,
|
||||
UserEmailAuthentication, UserEmailAuthenticationCode, UserRecoverySession, UserRegistration,
|
||||
DeviceCodeGrant, MatrixUser, UpstreamOAuthLink, UpstreamOAuthProvider,
|
||||
UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderDiscoveryMode,
|
||||
UpstreamOAuthProviderOnBackchannelLogout, UpstreamOAuthProviderPkceMode,
|
||||
UpstreamOAuthProviderTokenAuthMethod, User, UserEmailAuthentication,
|
||||
UserEmailAuthenticationCode, UserRecoverySession, UserRegistration,
|
||||
};
|
||||
use mas_i18n::DataLocale;
|
||||
use mas_iana::jose::JsonWebSignatureAlg;
|
||||
use mas_policy::{Violation, ViolationCode};
|
||||
use mas_router::{Account, GraphQL, PostAuthAction, UrlBuilder};
|
||||
use oauth2_types::scope::{OPENID, Scope};
|
||||
use rand::{
|
||||
@@ -732,6 +734,7 @@ pub struct ConsentContext {
|
||||
grant: AuthorizationGrant,
|
||||
client: Client,
|
||||
action: PostAuthAction,
|
||||
matrix_user: MatrixUser,
|
||||
}
|
||||
|
||||
impl TemplateContext for ConsentContext {
|
||||
@@ -755,6 +758,10 @@ impl TemplateContext for ConsentContext {
|
||||
grant,
|
||||
client,
|
||||
action,
|
||||
matrix_user: MatrixUser {
|
||||
mxid: "@alice:example.com".to_owned(),
|
||||
display_name: Some("Alice".to_owned()),
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
@@ -765,12 +772,13 @@ impl TemplateContext for ConsentContext {
|
||||
impl ConsentContext {
|
||||
/// Constructs a context for the client consent page
|
||||
#[must_use]
|
||||
pub fn new(grant: AuthorizationGrant, client: Client) -> Self {
|
||||
pub fn new(grant: AuthorizationGrant, client: Client, matrix_user: MatrixUser) -> Self {
|
||||
let action = PostAuthAction::continue_grant(grant.id);
|
||||
Self {
|
||||
grant,
|
||||
client,
|
||||
action,
|
||||
matrix_user,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -860,11 +868,50 @@ impl PolicyViolationContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Context used by the `compat_login_policy_violation.html` template
|
||||
#[derive(Serialize)]
|
||||
pub struct CompatLoginPolicyViolationContext {
|
||||
violations: Vec<Violation>,
|
||||
}
|
||||
|
||||
impl TemplateContext for CompatLoginPolicyViolationContext {
|
||||
fn sample<R: Rng>(
|
||||
_now: chrono::DateTime<Utc>,
|
||||
_rng: &mut R,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
sample_list(vec![
|
||||
CompatLoginPolicyViolationContext { violations: vec![] },
|
||||
CompatLoginPolicyViolationContext {
|
||||
violations: vec![Violation {
|
||||
msg: "user has too many active sessions".to_owned(),
|
||||
redirect_uri: None,
|
||||
field: None,
|
||||
code: Some(ViolationCode::TooManySessions),
|
||||
}],
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl CompatLoginPolicyViolationContext {
|
||||
/// Constructs a context for the compatibility login policy violation page
|
||||
/// given the list of violations
|
||||
#[must_use]
|
||||
pub const fn for_violations(violations: Vec<Violation>) -> Self {
|
||||
Self { violations }
|
||||
}
|
||||
}
|
||||
|
||||
/// Context used by the `sso.html` template
|
||||
#[derive(Serialize)]
|
||||
pub struct CompatSsoContext {
|
||||
login: CompatSsoLogin,
|
||||
action: PostAuthAction,
|
||||
matrix_user: MatrixUser,
|
||||
}
|
||||
|
||||
impl TemplateContext for CompatSsoContext {
|
||||
@@ -877,23 +924,33 @@ impl TemplateContext for CompatSsoContext {
|
||||
Self: Sized,
|
||||
{
|
||||
let id = Ulid::from_datetime_with_source(now.into(), rng);
|
||||
sample_list(vec![CompatSsoContext::new(CompatSsoLogin {
|
||||
id,
|
||||
redirect_uri: Url::parse("https://app.element.io/").unwrap(),
|
||||
login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
|
||||
created_at: now,
|
||||
state: CompatSsoLoginState::Pending,
|
||||
})])
|
||||
sample_list(vec![CompatSsoContext::new(
|
||||
CompatSsoLogin {
|
||||
id,
|
||||
redirect_uri: Url::parse("https://app.element.io/").unwrap(),
|
||||
login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
|
||||
created_at: now,
|
||||
state: CompatSsoLoginState::Pending,
|
||||
},
|
||||
MatrixUser {
|
||||
mxid: "@alice:example.com".to_owned(),
|
||||
display_name: Some("Alice".to_owned()),
|
||||
},
|
||||
)])
|
||||
}
|
||||
}
|
||||
|
||||
impl CompatSsoContext {
|
||||
/// Constructs a context for the legacy SSO login page
|
||||
#[must_use]
|
||||
pub fn new(login: CompatSsoLogin) -> Self
|
||||
pub fn new(login: CompatSsoLogin, matrix_user: MatrixUser) -> Self
|
||||
where {
|
||||
let action = PostAuthAction::continue_compat_sso_login(login.id);
|
||||
Self { login, action }
|
||||
Self {
|
||||
login,
|
||||
action,
|
||||
matrix_user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1748,13 +1805,18 @@ impl TemplateContext for DeviceLinkContext {
|
||||
pub struct DeviceConsentContext {
|
||||
grant: DeviceCodeGrant,
|
||||
client: Client,
|
||||
matrix_user: MatrixUser,
|
||||
}
|
||||
|
||||
impl DeviceConsentContext {
|
||||
/// Constructs a new context with an existing linked user
|
||||
#[must_use]
|
||||
pub fn new(grant: DeviceCodeGrant, client: Client) -> Self {
|
||||
Self { grant, client }
|
||||
pub fn new(grant: DeviceCodeGrant, client: Client, matrix_user: MatrixUser) -> Self {
|
||||
Self {
|
||||
grant,
|
||||
client,
|
||||
matrix_user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1782,7 +1844,14 @@ impl TemplateContext for DeviceConsentContext {
|
||||
ip_address: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||
user_agent: Some("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned()),
|
||||
};
|
||||
Self { grant, client }
|
||||
Self {
|
||||
grant,
|
||||
client,
|
||||
matrix_user: MatrixUser {
|
||||
mxid: "@alice:example.com".to_owned(),
|
||||
display_name: Some("Alice".to_owned()),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ pub fn register(
|
||||
env.add_filter("simplify_url", filter_simplify_url);
|
||||
env.add_filter("add_slashes", filter_add_slashes);
|
||||
env.add_filter("parse_user_agent", filter_parse_user_agent);
|
||||
env.add_filter("id_color_hash", filter_id_color_hash);
|
||||
env.add_function("add_params_to_url", function_add_params_to_url);
|
||||
env.add_function("counter", || Ok(Value::from_object(Counter::default())));
|
||||
if let Some(vite_manifest) = vite_manifest {
|
||||
@@ -138,6 +139,12 @@ fn filter_simplify_url(url: &str, kwargs: Kwargs) -> Result<String, minijinja::E
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter which computes a hash between 1 and 6 of an input string, identitical
|
||||
/// to compound-web's `useIdColorHash`
|
||||
fn filter_id_color_hash(input: &str) -> u32 {
|
||||
input.chars().fold(0, |hash, c| hash + c as u32) % 6 + 1
|
||||
}
|
||||
|
||||
/// Filter which parses a user-agent string
|
||||
fn filter_parse_user_agent(user_agent: String) -> Value {
|
||||
let user_agent = mas_data_model::UserAgent::parse(user_agent);
|
||||
|
||||
@@ -37,14 +37,15 @@ mod macros;
|
||||
|
||||
pub use self::{
|
||||
context::{
|
||||
AccountInactiveContext, ApiDocContext, AppContext, CompatSsoContext, ConsentContext,
|
||||
DeviceConsentContext, DeviceLinkContext, DeviceLinkFormField, DeviceNameContext,
|
||||
EmailRecoveryContext, EmailVerificationContext, EmptyContext, ErrorContext,
|
||||
FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext,
|
||||
PasswordRegisterContext, PolicyViolationContext, PostAuthContext, PostAuthContextInner,
|
||||
RecoveryExpiredContext, RecoveryFinishContext, RecoveryFinishFormField,
|
||||
RecoveryProgressContext, RecoveryStartContext, RecoveryStartFormField, RegisterContext,
|
||||
RegisterFormField, RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
|
||||
AccountInactiveContext, ApiDocContext, AppContext, CompatLoginPolicyViolationContext,
|
||||
CompatSsoContext, ConsentContext, DeviceConsentContext, DeviceLinkContext,
|
||||
DeviceLinkFormField, DeviceNameContext, EmailRecoveryContext, EmailVerificationContext,
|
||||
EmptyContext, ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField,
|
||||
NotFoundContext, PasswordRegisterContext, PolicyViolationContext, PostAuthContext,
|
||||
PostAuthContextInner, RecoveryExpiredContext, RecoveryFinishContext,
|
||||
RecoveryFinishFormField, RecoveryProgressContext, RecoveryStartContext,
|
||||
RecoveryStartFormField, RegisterContext, RegisterFormField,
|
||||
RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
|
||||
RegisterStepsEmailInUseContext, RegisterStepsRegistrationTokenContext,
|
||||
RegisterStepsRegistrationTokenFormField, RegisterStepsVerifyEmailContext,
|
||||
RegisterStepsVerifyEmailFormField, SiteBranding, SiteConfigExt, SiteFeatures,
|
||||
@@ -391,6 +392,9 @@ register_templates! {
|
||||
/// Render the policy violation page
|
||||
pub fn render_policy_violation(WithLanguage<WithCsrf<WithSession<PolicyViolationContext>>>) { "pages/policy_violation.html" }
|
||||
|
||||
/// Render the compatibility login policy violation page
|
||||
pub fn render_compat_login_policy_violation(WithLanguage<WithCsrf<WithSession<CompatLoginPolicyViolationContext>>>) { "pages/compat_login_policy_violation.html" }
|
||||
|
||||
/// Render the legacy SSO login consent page
|
||||
pub fn render_sso_login(WithLanguage<WithCsrf<WithSession<CompatSsoContext>>>) { "pages/sso.html" }
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ ignore = [
|
||||
# RSA key extraction "Marvin Attack". This is only relevant when using
|
||||
# PKCS#1 v1.5 encryption, which we don't
|
||||
"RUSTSEC-2023-0071",
|
||||
# This is a newly unmaintained package that we can allow temporarily.
|
||||
# Remove ASAP once https://github.com/element-hq/matrix-authentication-service/issues/5337 is fixed.
|
||||
"RUSTSEC-2025-0134",
|
||||
]
|
||||
|
||||
[licenses]
|
||||
|
||||
@@ -1883,6 +1883,10 @@
|
||||
"description": "Entrypoint to use when evaluating authorization grants",
|
||||
"type": "string"
|
||||
},
|
||||
"compat_login_entrypoint": {
|
||||
"description": "Entrypoint to use when evaluating compatibility logins",
|
||||
"type": "string"
|
||||
},
|
||||
"password_entrypoint": {
|
||||
"description": "Entrypoint to use when changing password",
|
||||
"type": "string"
|
||||
@@ -2467,6 +2471,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"skip_confirmation": {
|
||||
"description": "Whether to skip the interactive screen prompting the user to confirm the\n attributes that are being imported. This requires `localpart.action` to\n be `require` and other attribute actions to be either `ignore`, `force`\n or `require`",
|
||||
"type": "boolean"
|
||||
},
|
||||
"localpart": {
|
||||
"description": "Import the localpart of the MXID",
|
||||
"allOf": [
|
||||
@@ -2484,7 +2492,7 @@
|
||||
]
|
||||
},
|
||||
"email": {
|
||||
"description": "Import the email address of the user based on the `email` and\n `email_verified` claims",
|
||||
"description": "Import the email address of the user",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/EmailImportPreference"
|
||||
@@ -2572,14 +2580,24 @@
|
||||
"description": "How to handle an existing localpart claim",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Fails the sso login on conflict",
|
||||
"description": "Fails the upstream OAuth 2.0 login on conflict",
|
||||
"type": "string",
|
||||
"const": "fail"
|
||||
},
|
||||
{
|
||||
"description": "Adds the oauth identity link, regardless of whether there is an existing\n link or not",
|
||||
"description": "Adds the upstream OAuth 2.0 identity link, regardless of whether there\n is an existing link or not",
|
||||
"type": "string",
|
||||
"const": "add"
|
||||
},
|
||||
{
|
||||
"description": "Replace any existing upstream OAuth 2.0 identity link",
|
||||
"type": "string",
|
||||
"const": "replace"
|
||||
},
|
||||
{
|
||||
"description": "Adds the upstream OAuth 2.0 identity link *only* if there is no existing\n link for this provider on the matching user",
|
||||
"type": "string",
|
||||
"const": "set"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -40,7 +40,7 @@ cargo sqlx prepare
|
||||
|
||||
## Migrations
|
||||
|
||||
Migration files live in the `migrations` folder in the `mas-core` crate.
|
||||
Migration files live in the `migrations` folder in the `mas-storage-pg` crate.
|
||||
|
||||
```sh
|
||||
cd crates/storage-pg/ # Again, in the mas-storage-pg crate folder
|
||||
@@ -50,3 +50,29 @@ cargo sqlx migrate add [description] # Add new migration files
|
||||
```
|
||||
|
||||
Note that migrations are embedded in the final binary and can be run from the service CLI tool.
|
||||
|
||||
### Removing migrations
|
||||
|
||||
For various reasons, we may want to delete migrations.
|
||||
In case we do, we *must* declare that migration version as allowed to be missing.
|
||||
This is because on startup, MAS will validate that all the applied migrations are known, and warn if some are missing.
|
||||
|
||||
To do so, get the migration version and add it to the `ALLOWED_MISSING_MIGRATIONS` array in the `mas-storage-pg` crate.
|
||||
|
||||
### Modifying existing migrations
|
||||
|
||||
We may want to modify existing migrations to fix mistakes.
|
||||
In case we do, we *must* save the hash of the original migration file so that MAS can validate it on startup.
|
||||
|
||||
To do so, extract the first 16 bytes of the existing applied migration and append it to the `ALLOWED_ALTERNATE_CHECKSUMS` array in the `mas-storage-pg` crate.
|
||||
|
||||
```sql
|
||||
SELECT version, ENCODE(SUBSTRING(checksum FOR 16), 'hex') AS short_checksum
|
||||
FROM _sqlx_migrations
|
||||
WHERE version = 20250410000002;
|
||||
```
|
||||
```
|
||||
version | short_checksum
|
||||
----------------+----------------------------------
|
||||
20250410000002 | f2b8f120deae27e760d079a30b77eea3
|
||||
```
|
||||
|
||||
@@ -196,7 +196,7 @@ secrets:
|
||||
|
||||
# Signing keys
|
||||
keys:
|
||||
# It needs at least an RSA key to work properly
|
||||
# At least one RSA key must be configured
|
||||
- key_file: keys/rsa_key
|
||||
- kid: "iv1aShae"
|
||||
key: |
|
||||
@@ -222,7 +222,7 @@ The secret is not updated when the content of the file changes.
|
||||
> Changing the encryption secret afterwards will lead to a loss of all encrypted
|
||||
> information in the database.
|
||||
|
||||
### Singing Keys
|
||||
### Signing Keys
|
||||
|
||||
The service can use a number of key types for signing.
|
||||
The following key types are supported:
|
||||
@@ -238,9 +238,24 @@ The following key formats are supported:
|
||||
- PKCS#8 PEM or DER-encoded RSA or ECDSA private key, encrypted or not
|
||||
- SEC1 PEM or DER-encoded ECDSA private key
|
||||
|
||||
The signing keys are used for:
|
||||
- signing ID Tokens (as returned in the [Token Endpoint] at `/oauth2/token`);
|
||||
- signing the response of the [UserInfo Endpoint] at `/oauth2/userinfo` if the
|
||||
client requests a signed response;
|
||||
- (niche) signing a JWT for authenticating to an upstream OAuth provider when
|
||||
the `private_key_jwt` client auth method is configured.
|
||||
|
||||
At a minimum, an RSA key must be configured in order to be compliant with the
|
||||
[OpenID Connect Core specification][oidc-core-rs256] which specifies the RS256 algorithm
|
||||
as mandatory to implement by servers for interoperability reasons.
|
||||
|
||||
The keys can be given as a directory path via `secrets.keys_dir`
|
||||
or, alternatively, as an inline configuration list via `secrets.keys`.
|
||||
|
||||
[Token Endpoint]: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
||||
[UserInfo Endpoint]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||
[oidc-core-rs256]: https://openid.net/specs/openid-connect-core-1_0.html#ServerMTI
|
||||
|
||||
#### `secrets.keys_dir`
|
||||
|
||||
Path to the directory containing MAS signing key files.
|
||||
@@ -771,6 +786,14 @@ upstream_oauth2:
|
||||
subject:
|
||||
#template: "{{ user.sub }}"
|
||||
|
||||
# By default, new users will see a screen confirming the attributes they
|
||||
# are about to have on their account.
|
||||
#
|
||||
# Setting this to `true` allows skipping this screen, but requires the
|
||||
# `localpart.action` to be set to `require` and the other attributes
|
||||
# actions to be set to `ignore`, `force` or `require`.
|
||||
#skip_confirmation: false
|
||||
|
||||
# The localpart is the local part of the user's Matrix ID.
|
||||
# For example, on the `example.com` server, if the localpart is `alice`,
|
||||
# the user's Matrix ID will be `@alice:example.com`.
|
||||
@@ -780,8 +803,10 @@ upstream_oauth2:
|
||||
|
||||
# How to handle when localpart already exists.
|
||||
# Possible values are (default: fail):
|
||||
# - `add` : Adds the upstream account link to the existing user, regardless of whether there is an existing link or not.
|
||||
# - `fail` : Fails the upstream OAuth 2.0 login.
|
||||
# - `add` : Adds the upstream account link to the existing user, regardless of whether there is an existing link or not.
|
||||
# - `replace` : Replace any existing upstream OAuth 2.0 identity link for this provider on the matching user.
|
||||
# - `set` : Adds the upstream account link *only* if there is no existing link for this provider on the matching user.
|
||||
#on_conflict: fail
|
||||
|
||||
# The display name is the user's display name.
|
||||
|
||||
@@ -69,20 +69,25 @@ The template has the following variables available:
|
||||
|
||||
## Allow linking existing user accounts
|
||||
|
||||
The authentication service supports linking external provider identities to existing local user accounts.
|
||||
The authentication service supports linking external provider identities to existing local user accounts if the `localpart` matches.
|
||||
|
||||
To enable this behavior, the following option must be explicitly set in the provider configuration:
|
||||
If the `localpart` given by the upstream provider matches an existing user and the `claims_imports.localpart.action` is set to `force` or `require`, by default the service will refuse to link to that existing account.
|
||||
This behaviour is controlled by the `claims_imports.localpart.on_conflict` option, which can be set to:
|
||||
|
||||
* `fail` *(default)*: fails the upstream OAuth 2.0 login
|
||||
* `add`: automatically adds the upstream account to the existing user, regardless of whether the existing user already has another upstream account or not
|
||||
* `set`: automatically adds the upstream account to the existing user only if there are no other upstream accounts for that provider linked to the user
|
||||
* `replace`: automatically replaces any upstream account for that provider linked to the user
|
||||
|
||||
```yaml
|
||||
claims_imports:
|
||||
localpart:
|
||||
on_conflict: add
|
||||
upstream_oauth2:
|
||||
providers:
|
||||
- id: …
|
||||
claims_imports:
|
||||
localpart:
|
||||
action: force
|
||||
on_conflict: set
|
||||
```
|
||||
`on_conflict` configuration is specific to `localpart` claim_imports, it can be either:
|
||||
* `add` : when a user authenticates with the provider for the first time, the system checks whether a local user already exists with a `localpart` matching the attribute mapping `localpart` , _by default `{{ user.preferred_username }}`_. If a match is found, the external identity is linked to the existing local account.
|
||||
* `fail` *(default)* : fails the sso login.
|
||||
|
||||
To enable this option, the `localpart` mapping must be set to either `force` or `require`.
|
||||
|
||||
> ⚠️ **Security Notice**
|
||||
> Enabling this option can introduce a risk of account takeover.
|
||||
|
||||
@@ -27,7 +27,7 @@ export type LocalazyMetadata = {
|
||||
};
|
||||
|
||||
const localazyMetadata: LocalazyMetadata = {
|
||||
projectUrl: "https://localazy.com/p/matrix-authentication-service!v1.7",
|
||||
projectUrl: "https://localazy.com/p/matrix-authentication-service!v1.8",
|
||||
baseLocale: "en",
|
||||
languages: [
|
||||
{
|
||||
@@ -181,22 +181,22 @@ const localazyMetadata: LocalazyMetadata = {
|
||||
file: "frontend.json",
|
||||
path: "",
|
||||
cdnFiles: {
|
||||
"cs": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/cs/frontend.json",
|
||||
"da": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/da/frontend.json",
|
||||
"de": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/de/frontend.json",
|
||||
"en": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/en/frontend.json",
|
||||
"et": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/et/frontend.json",
|
||||
"fi": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fi/frontend.json",
|
||||
"fr": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fr/frontend.json",
|
||||
"hu": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/hu/frontend.json",
|
||||
"nb_NO": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nb-NO/frontend.json",
|
||||
"nl": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nl/frontend.json",
|
||||
"pl": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pl/frontend.json",
|
||||
"pt": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt/frontend.json",
|
||||
"ru": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/ru/frontend.json",
|
||||
"sv": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sv/frontend.json",
|
||||
"uk": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uk/frontend.json",
|
||||
"zh#Hans": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/zh-Hans/frontend.json"
|
||||
"cs": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/cs/frontend.json",
|
||||
"da": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/da/frontend.json",
|
||||
"de": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/de/frontend.json",
|
||||
"en": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/en/frontend.json",
|
||||
"et": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/et/frontend.json",
|
||||
"fi": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fi/frontend.json",
|
||||
"fr": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fr/frontend.json",
|
||||
"hu": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/hu/frontend.json",
|
||||
"nb_NO": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nb-NO/frontend.json",
|
||||
"nl": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nl/frontend.json",
|
||||
"pl": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pl/frontend.json",
|
||||
"pt": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt/frontend.json",
|
||||
"ru": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/ru/frontend.json",
|
||||
"sv": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sv/frontend.json",
|
||||
"uk": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uk/frontend.json",
|
||||
"zh#Hans": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/zh-Hans/frontend.json"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -204,22 +204,22 @@ const localazyMetadata: LocalazyMetadata = {
|
||||
file: "file.json",
|
||||
path: "",
|
||||
cdnFiles: {
|
||||
"cs": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/cs/file.json",
|
||||
"da": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/da/file.json",
|
||||
"de": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/de/file.json",
|
||||
"en": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/en/file.json",
|
||||
"et": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/et/file.json",
|
||||
"fi": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fi/file.json",
|
||||
"fr": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fr/file.json",
|
||||
"hu": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/hu/file.json",
|
||||
"nb_NO": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nb-NO/file.json",
|
||||
"nl": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nl/file.json",
|
||||
"pl": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pl/file.json",
|
||||
"pt": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt/file.json",
|
||||
"ru": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/ru/file.json",
|
||||
"sv": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sv/file.json",
|
||||
"uk": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uk/file.json",
|
||||
"zh#Hans": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/zh-Hans/file.json"
|
||||
"cs": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/cs/file.json",
|
||||
"da": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/da/file.json",
|
||||
"de": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/de/file.json",
|
||||
"en": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/en/file.json",
|
||||
"et": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/et/file.json",
|
||||
"fi": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fi/file.json",
|
||||
"fr": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fr/file.json",
|
||||
"hu": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/hu/file.json",
|
||||
"nb_NO": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nb-NO/file.json",
|
||||
"nl": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nl/file.json",
|
||||
"pl": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pl/file.json",
|
||||
"pt": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt/file.json",
|
||||
"ru": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/ru/file.json",
|
||||
"sv": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sv/file.json",
|
||||
"uk": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uk/file.json",
|
||||
"zh#Hans": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/zh-Hans/file.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"alert_description": "Dieses Konto wird dauerhaft entfernt und du hast keinen Zugriff mehr auf deine Nachrichten.",
|
||||
"alert_title": "Du bist kurz davor, alle deine Daten zu verlieren.",
|
||||
"button": "Account löschen",
|
||||
"dialog_description": "<text>Bestätige, dass du dein Konto löschen möchtest:</text><profile />\n<list>\n<item>Du kannst dein Konto nicht reaktivieren</item>\n<item>Du kannst dich nicht mehr anmelden</item>\n<item>Niemand kann deinen Benutzernamen (MXID) wieder verwenden, auch du nicht.</item>\n<item>Du verlässt alle Gruppen und Chats</item>\n<item>Du wirst vom Identitätsserver entfernt und niemand kann dich mit deiner E-Mail-Adresse oder Telefonnummer finden</item>\n</list>\n<text>Deine alten Nachrichten sind für Empfänger weiterhin sichtbar. Möchtest du deine gesendeten Nachrichten vor zukünftigen Gruppen-Besuchern verbergen?</text>",
|
||||
"dialog_description": "<text>Bestätige, dass du dein Konto löschen möchtest:</text>\n<profile />\n<list>\n<item>Du kannst dein Konto nicht reaktivieren</item>\n<item>Du kannst dich nicht mehr anmelden</item>\n<item>Niemand kann deinen Benutzernamen (MXID) wieder verwenden, auch du nicht.</item>\n<item>Du verlässt alle Gruppen und Chats</item>\n<item>Du wirst vom Identitätsserver entfernt und niemand kann dich mit deiner E-Mail-Adresse oder Telefonnummer finden</item>\n</list>\n<text>Deine alten Nachrichten sind für die jeweiligen Empfänger weiterhin sichtbar. Möchtest du deine gesendeten Nachrichten vor zukünftigen Gruppen-Besuchern verbergen?</text>",
|
||||
"dialog_title": "Dieses Konto löschen?",
|
||||
"erase_checkbox_label": "Ja, alle meine Nachrichten vor neuen Mitgliedern verbergen",
|
||||
"incorrect_password": "Falsches Passwort, versuch's nochmal",
|
||||
|
||||
@@ -319,9 +319,9 @@
|
||||
"scope": {
|
||||
"edit_profile": "Edit your profile and contact details",
|
||||
"manage_sessions": "Manage your devices and sessions",
|
||||
"mas_admin": "Administer any user on the matrix-authentication-service",
|
||||
"mas_admin": "Manage users (urn:mas:admin)",
|
||||
"send_messages": "Send new messages on your behalf",
|
||||
"synapse_admin": "Administer the Synapse homeserver",
|
||||
"synapse_admin": "Administer the server (urn:synapse:admin:*)",
|
||||
"view_messages": "View your existing messages and data",
|
||||
"view_profile": "See your profile info and contact details"
|
||||
}
|
||||
|
||||
@@ -391,9 +391,9 @@
|
||||
"scope": {
|
||||
"edit_profile": "Muuta sinu kasutajaprofiili ning kontaktandmeid",
|
||||
"manage_sessions": "Hallata sinu seadmeid ja sessioone",
|
||||
"mas_admin": "Hallata iga kasutajat teenuses matrix-authentication-service",
|
||||
"mas_admin": "Hallata kasutajaid (urn:mas:admin)",
|
||||
"send_messages": "Saata sõnumeid sinu nimel",
|
||||
"synapse_admin": "Hallata seda Synapse koduserverit",
|
||||
"synapse_admin": "Hallata seda Synapse koduserverit (urn:synapse:admin:*)",
|
||||
"view_messages": "Vaadata sinu sõnumeid ja andmeid",
|
||||
"view_profile": "Vaadata sinu profiili teavet ja kontaktadmeid"
|
||||
}
|
||||
|
||||
@@ -391,9 +391,9 @@
|
||||
"scope": {
|
||||
"edit_profile": "Modifier votre profil et vos coordonnées",
|
||||
"manage_sessions": "Gérer vos appareils et vos sessions",
|
||||
"mas_admin": "Administrer n'importe quel utilisateur dans matrix-authentication-service",
|
||||
"mas_admin": "Administrer les utilisateurs (urn:mas:admin)",
|
||||
"send_messages": "Envoyez de nouveaux messages en votre nom",
|
||||
"synapse_admin": "Administrer le serveur d’accueil Synapse",
|
||||
"synapse_admin": "Administrer le serveur (urn:synapse:admin:*)",
|
||||
"view_messages": "Afficher vos messages et données existants",
|
||||
"view_profile": "Voir les informations de votre profil et vos coordonnées"
|
||||
}
|
||||
|
||||
@@ -391,9 +391,9 @@
|
||||
"scope": {
|
||||
"edit_profile": "Edit your profile and contact details",
|
||||
"manage_sessions": "Manage your devices and sessions",
|
||||
"mas_admin": "Administer any user on the matrix-authentication-service",
|
||||
"mas_admin": "Manage users (urn:mas:admin)",
|
||||
"send_messages": "Send new messages on your behalf",
|
||||
"synapse_admin": "Administer the Synapse homeserver",
|
||||
"synapse_admin": "Administer the server (urn:synapse:admin:*)",
|
||||
"view_messages": "View your existing messages and data",
|
||||
"view_profile": "See your profile info and contact details"
|
||||
}
|
||||
|
||||
@@ -394,9 +394,9 @@
|
||||
"scope": {
|
||||
"edit_profile": "Редагування профілю та контактних даних",
|
||||
"manage_sessions": "Керування пристроями та сеансами",
|
||||
"mas_admin": "Адміністрування будь-якого користувача на matrix-authentication-service",
|
||||
"mas_admin": "Керування користувачами (urn:mas:admin)",
|
||||
"send_messages": "Надсилати нові повідомлення від вашого імені",
|
||||
"synapse_admin": "Адміністрування домашнього сервера Synapse",
|
||||
"synapse_admin": "Адмініструвати сервер (urn:synapse:admin:*)",
|
||||
"view_messages": "Перегляд наявних повідомлень і даних",
|
||||
"view_profile": "Перегляд інформації профілю та контактних даних"
|
||||
}
|
||||
|
||||
1189
frontend/package-lock.json
generated
1189
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@tanstack/react-query": "^5.90.10",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@tanstack/react-router": "^1.131.44",
|
||||
"@vector-im/compound-design-tokens": "6.4.0",
|
||||
"@vector-im/compound-web": "^8.2.5",
|
||||
@@ -30,44 +30,44 @@
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
"classnames": "^2.5.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"i18next": "^25.6.3",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
"i18next": "^25.7.2",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-i18next": "^16.4.1",
|
||||
"swagger-ui-dist": "^5.29.5",
|
||||
"valibot": "^1.1.0",
|
||||
"valibot": "^1.2.0",
|
||||
"vaul": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.2",
|
||||
"@biomejs/biome": "^2.3.8",
|
||||
"@browser-logos/chrome": "^2.0.0",
|
||||
"@browser-logos/firefox": "^3.0.10",
|
||||
"@browser-logos/safari": "^2.1.0",
|
||||
"@graphql-codegen/cli": "^6.0.2",
|
||||
"@graphql-codegen/cli": "^6.1.0",
|
||||
"@graphql-codegen/client-preset": "^5.1.1",
|
||||
"@graphql-codegen/typescript-msw": "^3.0.1",
|
||||
"@storybook/addon-docs": "^10.0.8",
|
||||
"@storybook/react-vite": "^10.0.8",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@storybook/addon-docs": "^10.1.4",
|
||||
"@storybook/react-vite": "^10.1.4",
|
||||
"@tanstack/react-query-devtools": "^5.91.1",
|
||||
"@tanstack/react-router-devtools": "^1.131.44",
|
||||
"@tanstack/router-plugin": "^1.131.44",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "19.2.6",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/swagger-ui-dist": "^3.30.6",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^4.0.15",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"graphql": "^16.11.0",
|
||||
"happy-dom": "^20.0.4",
|
||||
"i18next-cli": "^1.24.20",
|
||||
"i18next-cli": "^1.30.5",
|
||||
"knip": "^5.66.4",
|
||||
"msw": "^2.11.6",
|
||||
"msw-storybook-addon": "^2.0.5",
|
||||
"msw-storybook-addon": "^2.0.6",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-import": "^16.1.1",
|
||||
"postcss-nesting": "^13.0.2",
|
||||
@@ -76,11 +76,11 @@
|
||||
"tailwindcss": "^3.4.18",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "7.2.4",
|
||||
"vite": "7.2.7",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-graphql-codegen": "^3.7.0",
|
||||
"vite-plugin-manifest-sri": "^0.2.0",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.14"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": [
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
letter-spacing: var(--cpd-font-letter-spacing-body-lg);
|
||||
}
|
||||
|
||||
.cpd-text-heading-xl-semibold {
|
||||
font: var(--cpd-font-heading-xl-semibold);
|
||||
letter-spacing: var(--cpd-font-letter-spacing-heading-xl);
|
||||
.cpd-text-body-lg-semibold {
|
||||
font: var(--cpd-font-body-lg-semibold);
|
||||
letter-spacing: var(--cpd-font-letter-spacing-body-lg);
|
||||
}
|
||||
|
||||
.cpd-text-body-md-regular {
|
||||
@@ -33,6 +33,36 @@
|
||||
letter-spacing: var(--cpd-font-letter-spacing-body-md);
|
||||
}
|
||||
|
||||
.cpd-text-body-md-semibold {
|
||||
font: var(--cpd-font-body-md-semibold);
|
||||
letter-spacing: var(--cpd-font-letter-spacing-body-md);
|
||||
}
|
||||
|
||||
.cpd-text-body-sm-regular {
|
||||
font: var(--cpd-font-body-sm-regular);
|
||||
letter-spacing: var(--cpd-font-letter-spacing-body-sm);
|
||||
}
|
||||
|
||||
.cpd-text-body-sm-semibold {
|
||||
font: var(--cpd-font-body-sm-semibold);
|
||||
letter-spacing: var(--cpd-font-letter-spacing-body-sm);
|
||||
}
|
||||
|
||||
.cpd-text-body-xs-regular {
|
||||
font: var(--cpd-font-body-xs-regular);
|
||||
letter-spacing: var(--cpd-font-letter-spacing-body-xs);
|
||||
}
|
||||
|
||||
.cpd-text-body-xs-semibold {
|
||||
font: var(--cpd-font-body-xs-semibold);
|
||||
letter-spacing: var(--cpd-font-letter-spacing-body-xs);
|
||||
}
|
||||
|
||||
.cpd-text-heading-xl-semibold {
|
||||
font: var(--cpd-font-heading-xl-semibold);
|
||||
letter-spacing: var(--cpd-font-letter-spacing-heading-xl);
|
||||
}
|
||||
|
||||
.cpd-text-primary {
|
||||
color: var(--cpd-color-text-primary);
|
||||
}
|
||||
@@ -186,3 +216,48 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
height: var(--cpd-space-14x);
|
||||
width: var(--cpd-space-14x);
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
line-height: var(--cpd-space-14x);
|
||||
font-size: 32px;
|
||||
font-family: var(--cpd-font-family-sans);
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
background-color: var(--cpd-avatar-bg);
|
||||
color: var(--cpd-avatar-color);
|
||||
|
||||
&[data-color] {
|
||||
--cpd-avatar-bg: var(--cpd-color-bg-decorative-1);
|
||||
--cpd-avatar-color: var(--cpd-color-text-decorative-1);
|
||||
}
|
||||
|
||||
&[data-color="2"] {
|
||||
--cpd-avatar-bg: var(--cpd-color-bg-decorative-2);
|
||||
--cpd-avatar-color: var(--cpd-color-text-decorative-2);
|
||||
}
|
||||
|
||||
&[data-color="3"] {
|
||||
--cpd-avatar-bg: var(--cpd-color-bg-decorative-3);
|
||||
--cpd-avatar-color: var(--cpd-color-text-decorative-3);
|
||||
}
|
||||
|
||||
&[data-color="4"] {
|
||||
--cpd-avatar-bg: var(--cpd-color-bg-decorative-4);
|
||||
--cpd-avatar-color: var(--cpd-color-text-decorative-4);
|
||||
}
|
||||
|
||||
&[data-color="5"] {
|
||||
--cpd-avatar-bg: var(--cpd-color-bg-decorative-5);
|
||||
--cpd-avatar-color: var(--cpd-color-text-decorative-5);
|
||||
}
|
||||
|
||||
&[data-color="6"] {
|
||||
--cpd-avatar-bg: var(--cpd-color-bg-decorative-6);
|
||||
--cpd-avatar-color: var(--cpd-color-text-decorative-6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,31 @@ import { vi } from "vitest";
|
||||
* Defaults to `en-GB`
|
||||
*/
|
||||
export const mockLocale = (defaultLocale = "en-GB"): void => {
|
||||
const { DateTimeFormat } = Intl;
|
||||
const OriginalDateTimeFormat = Intl.DateTimeFormat;
|
||||
|
||||
// Vitest 4.x requires function/class implementations for spyOn mocks when
|
||||
// mocking constructors. For built-in constructors like Intl.DateTimeFormat
|
||||
// that have internal slots, we use a function that returns a new instance.
|
||||
// This is valid JavaScript - when a constructor returns an object, that
|
||||
// object becomes the instance (instead of `this`).
|
||||
function MockDateTimeFormat(
|
||||
this: unknown,
|
||||
locales?: Intl.LocalesArgument,
|
||||
options?: Intl.DateTimeFormatOptions,
|
||||
): Intl.DateTimeFormat {
|
||||
// Apply default locale when no locale is specified
|
||||
return new OriginalDateTimeFormat(locales || defaultLocale, options);
|
||||
}
|
||||
|
||||
// Inherit static methods from the original DateTimeFormat
|
||||
Object.setPrototypeOf(MockDateTimeFormat, OriginalDateTimeFormat);
|
||||
// Set up prototype chain so instanceof checks work correctly
|
||||
Object.setPrototypeOf(
|
||||
MockDateTimeFormat.prototype,
|
||||
OriginalDateTimeFormat.prototype,
|
||||
);
|
||||
|
||||
vi.spyOn(Intl, "DateTimeFormat").mockImplementation(
|
||||
(
|
||||
locales?: Intl.LocalesArgument,
|
||||
options?: Intl.DateTimeFormatOptions | undefined,
|
||||
) => new DateTimeFormat(locales || defaultLocale, options),
|
||||
MockDateTimeFormat as typeof Intl.DateTimeFormat,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ INPUTS := \
|
||||
client_registration/client_registration.rego \
|
||||
register/register.rego \
|
||||
authorization_grant/authorization_grant.rego \
|
||||
compat_login/compat_login.rego \
|
||||
email/email.rego
|
||||
|
||||
ifeq ($(DOCKER), 1)
|
||||
@@ -38,6 +39,7 @@ policy.wasm: $(INPUTS)
|
||||
-e "client_registration/violation" \
|
||||
-e "register/violation" \
|
||||
-e "authorization_grant/violation" \
|
||||
-e "compat_login/violation" \
|
||||
-e "email/violation" \
|
||||
$^
|
||||
tar xzf bundle.tar.gz /policy.wasm
|
||||
|
||||
74
policies/compat_login/compat_login.rego
Normal file
74
policies/compat_login/compat_login.rego
Normal file
@@ -0,0 +1,74 @@
|
||||
# Copyright 2025 Element Creations Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# METADATA
|
||||
# schemas:
|
||||
# - input: schema["compat_login_input"]
|
||||
package compat_login
|
||||
|
||||
import rego.v1
|
||||
|
||||
import data.common
|
||||
|
||||
default allow := false
|
||||
|
||||
allow if {
|
||||
count(violation) == 0
|
||||
}
|
||||
|
||||
violation contains {"msg": sprintf(
|
||||
"Requester [%s] isn't allowed to do this action",
|
||||
[common.format_requester(input.requester)],
|
||||
)} if {
|
||||
common.requester_banned(input.requester, data.requester)
|
||||
}
|
||||
|
||||
violation contains {
|
||||
"code": "too-many-sessions",
|
||||
"msg": "user has too many active sessions (soft limit)",
|
||||
} if {
|
||||
# Only apply if session limits are enabled in the config
|
||||
data.session_limit != null
|
||||
|
||||
# This is a web-based interactive login
|
||||
is_interactive
|
||||
|
||||
# Only apply if this login doesn't replace a session
|
||||
# (As then this login is not actually increasing the number of devices)
|
||||
not input.session_replaced
|
||||
|
||||
# For web-based 'compat SSO' login, a violation occurs when the soft limit has already been
|
||||
# reached or exceeded.
|
||||
# We use the soft limit because the user will be able to interactively remove
|
||||
# sessions to return under the limit.
|
||||
data.session_limit.soft_limit <= input.session_counts.total
|
||||
}
|
||||
|
||||
violation contains {
|
||||
"code": "too-many-sessions",
|
||||
"msg": "user has too many active sessions (hard limit)",
|
||||
} if {
|
||||
# Only apply if session limits are enabled in the config
|
||||
data.session_limit != null
|
||||
|
||||
# This is not a web-based interactive login
|
||||
not is_interactive
|
||||
|
||||
# Only apply if this login doesn't replace a session
|
||||
# (As then this login is not actually increasing the number of devices)
|
||||
not input.session_replaced
|
||||
|
||||
# For `m.login.password` login, a violation occurs when the hard limit has already been
|
||||
# reached or exceeded.
|
||||
# We don't use the soft limit because the user won't be able to interactively remove
|
||||
# sessions to return under the limit.
|
||||
data.session_limit.hard_limit <= input.session_counts.total
|
||||
}
|
||||
|
||||
is_interactive if {
|
||||
# Only `m.login.sso` (the interactive web form) is interactive;
|
||||
# `m.login.password` and `m.login.token` (including the finalisation of an SSO login) are not
|
||||
input.login.type == "m.login.sso"
|
||||
}
|
||||
99
policies/compat_login/compat_login_test.rego
Normal file
99
policies/compat_login/compat_login_test.rego
Normal file
@@ -0,0 +1,99 @@
|
||||
# Copyright 2025 Element Creations Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
package compat_login_test
|
||||
|
||||
import data.compat_login
|
||||
import rego.v1
|
||||
|
||||
user := {"username": "john"}
|
||||
|
||||
# Tests session limiting when using (the interactive part of) `m.login.sso`
|
||||
test_session_limiting_sso if {
|
||||
compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 1}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 31}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 32}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 42}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 65}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
# No limit configured
|
||||
compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 1}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as null
|
||||
}
|
||||
|
||||
# Test session limiting when using `m.login.password`
|
||||
test_session_limiting_password if {
|
||||
compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 1}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 63}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 64}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 65}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
# No limit configured
|
||||
compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 1}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as null
|
||||
}
|
||||
|
||||
test_no_session_limiting_upon_replacement if {
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 65}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 65}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
}
|
||||
144
policies/schema/compat_login_input.json
Normal file
144
policies/schema/compat_login_input.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "CompatLoginInput",
|
||||
"description": "Input for the compatibility login policy.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"session_counts": {
|
||||
"description": "How many sessions the user has.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SessionCounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"session_replaced": {
|
||||
"description": "Whether a session will be replaced by this login",
|
||||
"type": "boolean"
|
||||
},
|
||||
"login": {
|
||||
"description": "What type of login is being performed.\n This also determines whether the login is interactive.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CompatLogin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"requester": {
|
||||
"$ref": "#/definitions/Requester"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"user",
|
||||
"session_counts",
|
||||
"session_replaced",
|
||||
"login",
|
||||
"requester"
|
||||
],
|
||||
"definitions": {
|
||||
"SessionCounts": {
|
||||
"description": "Information about how many sessions the user has",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"oauth2": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"compat": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"personal": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"total",
|
||||
"oauth2",
|
||||
"compat",
|
||||
"personal"
|
||||
]
|
||||
},
|
||||
"CompatLogin": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Used as the interactive part of SSO login.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "m.login.sso"
|
||||
},
|
||||
"redirect_uri": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"redirect_uri"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Used as the final (non-interactive) stage of SSO login.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "m.login.token"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Non-interactive password-over-the-API login.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "m.login.password"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"Requester": {
|
||||
"description": "Identity of the requester",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ip_address": {
|
||||
"description": "IP address of the entity making the request",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "ip"
|
||||
},
|
||||
"user_agent": {
|
||||
"description": "User agent of the entity making the request",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
-#}
|
||||
|
||||
{% macro button(text, csrf_token, as_link=false, post_logout_action={}) %}
|
||||
<form method="POST" action="{{ "/logout" | prefix_url }}" class="inline-flex">
|
||||
{% macro button(csrf_token, text="", as_link=false, post_logout_action={}) %}
|
||||
<form method="POST" action="{{ "/logout" | prefix_url }}" class="inline-flex [&>button]:flex-1">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||
{% for key, value in post_logout_action|items %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
||||
{% endfor %}
|
||||
{% if as_link %}
|
||||
<button class="cpd-link flex-1" data-kind="critical" type="submit">{{ text }}</button>
|
||||
{% if caller is defined %}
|
||||
{{ caller() }}
|
||||
{% elif as_link %}
|
||||
<button class="cpd-link" data-kind="critical" type="submit">{{ text }}</button>
|
||||
{% else %}
|
||||
<button class="cpd-button destructive flex-1" data-kind="secondary" data-size="lg" type="submit">{{ text }}</button>
|
||||
<button class="cpd-button destructive" data-kind="secondary" data-size="lg" type="submit">{{ text }}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -6,9 +6,38 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
-#}
|
||||
|
||||
{# Macro to remove 'safe' scope from a scope list. Usage:
|
||||
|
||||
{% call(scopes) scope.unsafe_scopes(scopes=["openid", "urn:matrix:client:api:*", "urn:synapse:admin:*", "urn:mas:admin"]) %}
|
||||
`scopes` only has unsafe scopes: ["urn:synapse:admin:*", "urn:mas:admin"]
|
||||
|
||||
<ul>
|
||||
{% for scope in scopes %}
|
||||
<li>{{ scope }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endcall %}
|
||||
#}
|
||||
{% macro unsafe_scopes(scopes) -%}
|
||||
{% set ns = namespace(unsafe_scopes=[]) %}
|
||||
{% set safe_scope_prefixes = ["openid", "urn:matrix:client:api:", "urn:matrix:org.matrix.msc2967.client:api:", "urn:matrix:client:device:", "urn:matrix:org.matrix.msc2967.client:device:"] %}
|
||||
{% for scope in scopes %}
|
||||
{% set ns.is_safe = False %}
|
||||
{% for safe_scope_prefix in safe_scope_prefixes %}
|
||||
{% if scope.startswith(safe_scope_prefix) %}
|
||||
{% set ns.is_safe = True %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not ns.is_safe %}
|
||||
{% set ns.unsafe_scopes = ns.unsafe_scopes + [scope] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ caller(ns.unsafe_scopes) }}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro list(scopes) %}
|
||||
<ul>
|
||||
{% for scope in (scopes | split(" ")) %}
|
||||
{% for scope in scopes %}
|
||||
{% if scope == "openid" %}
|
||||
<li>{{ icon.user_profile() }}<p>{{ _("mas.scope.view_profile") }}</p></li>
|
||||
{% elif scope == "urn:mas:graphql:*" %}
|
||||
@@ -18,9 +47,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
<li>{{ icon.chat() }}<p>{{ _("mas.scope.view_messages") }}</p></li>
|
||||
<li>{{ icon.send() }}<p>{{ _("mas.scope.send_messages") }}</p></li>
|
||||
{% elif scope == "urn:synapse:admin:*" %}
|
||||
<li class="dangerous">{{ icon.room() }}<p>{{ _("mas.scope.synapse_admin") }}</p></li>
|
||||
<li class="dangerous">{{ icon.room() }}<p>{{ _("mas.scope.synapse_admin", scope=scope) }}</p></li>
|
||||
{% elif scope == "urn:mas:admin" %}
|
||||
<li class="dangerous">{{ icon.admin() }}<p>{{ _("mas.scope.mas_admin") }}</p></li>
|
||||
<li class="dangerous">{{ icon.admin() }}<p>{{ _("mas.scope.mas_admin", scope=scope) }}</p></li>
|
||||
{% elif scope is startingwith("urn:matrix:client:device:") or scope is startingwith("urn:matrix:org.matrix.msc2967.client:device:") %}
|
||||
{# We hide this scope #}
|
||||
{% else %}
|
||||
|
||||
31
templates/pages/compat_login_policy_violation.html
Normal file
31
templates/pages/compat_login_policy_violation.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{#
|
||||
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.
|
||||
-#}
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<header class="page-heading">
|
||||
<div class="icon invalid">
|
||||
{{ icon.error_solid() }}
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1 class="title">{{ _("mas.policy_violation.heading") }}</h1>
|
||||
<p class="text">{{ _("mas.policy_violation.description") }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex flex-col gap-10">
|
||||
<div class="flex gap-1 justify-center items-center">
|
||||
<p class="cpd-text-secondary cpd-text-body-md-regular">
|
||||
{{ _("mas.policy_violation.logged_as", username=current_session.user.username) }}
|
||||
</p>
|
||||
|
||||
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=True) }}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock content %}
|
||||
@@ -12,6 +12,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
{% block content %}
|
||||
{% set client_name = client.client_name or client.client_id %}
|
||||
|
||||
<header class="page-heading">
|
||||
{% if client.logo_uri %}
|
||||
<img class="consent-client-icon image" referrerpolicy="no-referrer" src="{{ client.logo_uri }}" />
|
||||
@@ -22,33 +23,42 @@ Please see LICENSE files in the repository root for full details.
|
||||
{% endif %}
|
||||
|
||||
<div class="header">
|
||||
<h1 class="title">{{ _("mas.consent.heading") }}</h1>
|
||||
<p class="text [&>span]:whitespace-nowrap">
|
||||
{{ _("mas.consent.client_wants_access", client_name=client_name, redirect_uri=(grant.redirect_uri | simplify_url)) }}
|
||||
{{ _("mas.consent.this_will_allow", client_name=client_name) }}
|
||||
<h1 class="title">
|
||||
{{ _('mas.consent.continue_to', client_name=client_name) }}
|
||||
</h1>
|
||||
<p class="text [&>span]:whitespace-nowrap [&>span]:text-[var(--cpd-color-text-link-external)]">
|
||||
{{ _("mas.consent.this_will_setup", client_name=client_name, client_uri=((client.client_uri or grant.redirect_uri) | simplify_url), server_name=branding.server_name) }}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="consent-scope-list">
|
||||
{{ scope.list(scopes=grant.scope) }}
|
||||
</section>
|
||||
|
||||
<section class="text-center cpd-text-secondary cpd-text-body-md-regular [&>span]:whitespace-nowrap">
|
||||
<strong class="font-semibold cpd-text-primary [&>span]:whitespace-nowrap">{{ _("mas.consent.make_sure_you_trust", client_name=client_name) }}</strong>
|
||||
{{ _("mas.consent.you_may_be_sharing") }}
|
||||
{% if client.policy_uri or client.tos_uri %}
|
||||
Find out how <span>{{ client_name }}</span> will handle your data by reviewing its
|
||||
{% if client.policy_uri %}
|
||||
<a target="_blank" href="{{ client.policy_uri }}" class="cpd-link" data-kind="primary">privacy policy</a>{% if not client.tos_uri %}.{% endif %}
|
||||
{% endif %}
|
||||
{% if client.policy_uri and client.tos_uri%}
|
||||
and
|
||||
{% endif %}
|
||||
{% if client.tos_uri %}
|
||||
<a target="_blank" href="{{ client.tos_uri }}" class="cpd-link" data-kind="primary">terms of service</a>.
|
||||
{% endif %}
|
||||
{% call(scopes) scope.unsafe_scopes(scopes=grant.scope.split(" ")) %}
|
||||
{% if scopes is not empty %}
|
||||
<section class="flex flex-col gap-3">
|
||||
<p class="text-center cpd-text-body-md-regular">
|
||||
{{ _('mas.consent.scope_list_preface', client_name=client_name) }}
|
||||
</p>
|
||||
<div class="consent-scope-list">
|
||||
{{ scope.list(scopes=scopes) }}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
|
||||
{% set initial -%}
|
||||
{%- if matrix_user.display_name -%}
|
||||
{{- matrix_user.display_name[0] | upper -}}
|
||||
{%- else -%}
|
||||
{{- matrix_user.mxid[1] | upper -}}
|
||||
{%- endif -%}
|
||||
{%- endset %}
|
||||
|
||||
<section class="flex items-center p-4 gap-4 border border-[var(--cpd-color-gray-400)] rounded-xl">
|
||||
<div class="avatar-placeholder" data-color="{{ matrix_user.mxid | id_color_hash }}">{{ initial }}</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-primary cpd-text-body-lg-semibold">{{ matrix_user.display_name or current_session.user.username }}</div>
|
||||
<div class="text-secondary cpd-text-body-md-regular">{{ matrix_user.mxid }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-6">
|
||||
@@ -57,13 +67,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
{{ button.button(text=_("action.continue")) }}
|
||||
</form>
|
||||
|
||||
<div class="flex gap-1 justify-center items-center">
|
||||
<p class="cpd-text-secondary cpd-text-body-md-regular">
|
||||
{{ _("mas.not_you", username=current_session.user.username) }}
|
||||
</p>
|
||||
|
||||
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=true) }}
|
||||
</div>
|
||||
{% call logout.button(csrf_token=csrf_token, post_logout_action=action) %}
|
||||
<button type="submit" class="cpd-button primary" data-kind="secondary" data-size="lg" type="submit">
|
||||
{{ _("mas.consent.use_another_account") }}
|
||||
</button>
|
||||
{% endcall %}
|
||||
|
||||
{{ back_to_client.link(
|
||||
text=_("action.cancel"),
|
||||
|
||||
@@ -25,9 +25,15 @@ Please see LICENSE files in the repository root for full details.
|
||||
{% endif %}
|
||||
|
||||
<div class="header">
|
||||
<h1 class="title">{{ _("mas.consent.heading") }}</h1>
|
||||
<h1 class="title">
|
||||
{{ _('mas.consent.continue_to', client_name=client_name) }}
|
||||
</h1>
|
||||
|
||||
<div class="session-card my-4">
|
||||
<p class="text [&>span]:whitespace-nowrap [&>span]:text-[var(--cpd-color-text-link-external)]">
|
||||
{{ _("mas.device_consent.this_will_setup", client_name=client_name, client_uri=((client.client_uri or "") | simplify_url), server_name=branding.server_name) }}
|
||||
</p>
|
||||
|
||||
<div class="session-card mt-4">
|
||||
<div class="card-header" {%- if user_agent %} title="{{ user_agent.raw }}"{% endif %}>
|
||||
<div class="device-type-icon">
|
||||
{% if user_agent.device_type == "mobile" %}
|
||||
@@ -88,33 +94,36 @@ Please see LICENSE files in the repository root for full details.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text [&>span]:whitespace-nowrap">
|
||||
{{ _("mas.device_consent.another_device_access") }}
|
||||
{{ _("mas.consent.this_will_allow", client_name=client_name) }}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="consent-scope-list">
|
||||
{{ scope.list(scopes=grant.scope) }}
|
||||
</section>
|
||||
|
||||
<section class="text-center text-balance cpd-text-secondary cpd-text-body-md-regular [&>span]:whitespace-nowrap">
|
||||
<strong class="font-semibold cpd-text-primary [&>span]:whitespace-nowrap">{{ _("mas.consent.make_sure_you_trust", client_name=client_name) }}</strong>
|
||||
{{ _("mas.consent.you_may_be_sharing") }}
|
||||
{% if client.policy_uri or client.tos_uri %}
|
||||
Find out how <span>{{ client_name }}</span> will handle your data by reviewing its
|
||||
{% if client.policy_uri %}
|
||||
<a target="_blank" href="{{ client.policy_uri }}" class="cpd-link" data-kind="primary">privacy policy</a>{% if not client.tos_uri %}.{% endif %}
|
||||
{% endif %}
|
||||
{% if client.policy_uri and client.tos_uri%}
|
||||
and
|
||||
{% endif %}
|
||||
{% if client.tos_uri %}
|
||||
<a target="_blank" href="{{ client.tos_uri }}" class="cpd-link" data-kind="primary">terms of service</a>.
|
||||
{% endif %}
|
||||
{% call(scopes) scope.unsafe_scopes(scopes=grant.scope.split(" ")) %}
|
||||
{% if scopes is not empty %}
|
||||
<section class="flex flex-col gap-3">
|
||||
<p class="text-center cpd-text-body-md-regular">
|
||||
{{ _('mas.consent.scope_list_preface', client_name=client_name) }}
|
||||
</p>
|
||||
<div class="consent-scope-list">
|
||||
{{ scope.list(scopes=scopes) }}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
|
||||
{% set initial -%}
|
||||
{%- if matrix_user.display_name -%}
|
||||
{{- matrix_user.display_name[0] | upper -}}
|
||||
{%- else -%}
|
||||
{{- matrix_user.mxid[1] | upper -}}
|
||||
{%- endif -%}
|
||||
{%- endset %}
|
||||
|
||||
<section class="flex items-center p-4 gap-4 border border-[var(--cpd-color-gray-400)] rounded-xl">
|
||||
<div class="avatar-placeholder" data-color="{{ matrix_user.mxid | id_color_hash }}">{{ initial }}</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-primary cpd-text-body-lg-semibold">{{ matrix_user.display_name or current_session.user.username }}</div>
|
||||
<div class="text-secondary cpd-text-body-md-regular">{{ matrix_user.mxid }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-6">
|
||||
@@ -123,18 +132,20 @@ Please see LICENSE files in the repository root for full details.
|
||||
<button type="submit" name="action" value="consent" class="cpd-button" data-kind="primary" data-size="lg">
|
||||
{{ _("action.continue") }}
|
||||
</button>
|
||||
<button type="submit" name="action" value="reject" class="cpd-button destructive" data-kind="secondary" data-size="lg">
|
||||
</form>
|
||||
|
||||
{% call logout.button(csrf_token=csrf_token, post_logout_action=action) %}
|
||||
<button type="submit" class="cpd-button primary flex-1" data-kind="secondary" data-size="lg" type="submit">
|
||||
{{ _("mas.consent.use_another_account") }}
|
||||
</button>
|
||||
{% endcall %}
|
||||
|
||||
<form method="POST" class="cpd-form-root">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||
<button type="submit" name="action" value="reject" class="cpd-button" data-kind="tertiary" data-size="lg">
|
||||
{{ _("action.cancel") }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="flex gap-1 justify-center items-center">
|
||||
<p class="cpd-text-secondary cpd-text-body-md-regular">
|
||||
{{ _("mas.not_you", username=current_session.user.username) }}
|
||||
</p>
|
||||
|
||||
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=true) }}
|
||||
</div>
|
||||
</section>
|
||||
{% elif grant.state == "rejected" %}
|
||||
<header class="page-heading">
|
||||
|
||||
@@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
-#}
|
||||
|
||||
{% set consent_page = true %}
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
@@ -17,18 +19,29 @@ Please see LICENSE files in the repository root for full details.
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1 class="title">Allow access to your account?</h1>
|
||||
<p class="text"><span class="whitespace-nowrap">{{ client_name }}</span> wants to access your account. This will allow <span class="whitespace-nowrap">{{ client_name }}</span> to:</p>
|
||||
<h1 class="title">
|
||||
{{ _('mas.consent.continue_to', client_name=client_name) }}
|
||||
</h1>
|
||||
<p class="text [&>span]:whitespace-nowrap [&>span]:text-[var(--cpd-color-text-link-external)]">
|
||||
{{ _("mas.legacy_consent.this_will_setup", client_name=client_name, server_name=branding.server_name) }}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="consent-scope-list">
|
||||
{{ scope.list(scopes="openid urn:matrix:client:api:*") }}
|
||||
</section>
|
||||
{% set initial -%}
|
||||
{%- if matrix_user.display_name -%}
|
||||
{{- matrix_user.display_name[0] | upper -}}
|
||||
{%- else -%}
|
||||
{{- matrix_user.mxid[1] | upper -}}
|
||||
{%- endif -%}
|
||||
{%- endset %}
|
||||
|
||||
<section class="text-center cpd-text-secondary cpd-text-body-md-regular">
|
||||
<span class="font-semibold cpd-text-primary">Make sure that you trust <span class="whitespace-nowrap">{{ client_name }}</span>.</span>
|
||||
You may be sharing sensitive information with this site or app.
|
||||
<section class="flex items-center p-4 gap-4 border border-[var(--cpd-color-gray-400)] rounded-xl">
|
||||
<div class="avatar-placeholder" data-color="{{ matrix_user.mxid | id_color_hash }}">{{ initial }}</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-primary cpd-text-body-lg-semibold">{{ matrix_user.display_name or current_session.user.username }}</div>
|
||||
<div class="text-secondary cpd-text-body-md-regular">{{ matrix_user.mxid }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-6">
|
||||
@@ -37,12 +50,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
{{ button.button(text=_("action.continue")) }}
|
||||
</form>
|
||||
|
||||
<div class="flex gap-1 justify-center items-center">
|
||||
<p class="cpd-text-secondary cpd-text-body-md-regular">
|
||||
{{ _("mas.not_you", username=current_session.user.username) }}
|
||||
</p>
|
||||
|
||||
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=true) }}
|
||||
</div>
|
||||
{% call logout.button(csrf_token=csrf_token, post_logout_action=action) %}
|
||||
<button type="submit" class="cpd-button primary" data-kind="secondary" data-size="lg" type="submit">
|
||||
{{ _("mas.consent.use_another_account") }}
|
||||
</button>
|
||||
{% endcall %}
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
},
|
||||
"cancel": "Cancel",
|
||||
"@cancel": {
|
||||
"context": "pages/consent.html:69:11-29, pages/device_consent.html:127:13-31, pages/policy_violation.html:44:13-31"
|
||||
"context": "pages/consent.html:77:11-29, pages/device_consent.html:146:13-31, pages/policy_violation.html:44:13-31"
|
||||
},
|
||||
"continue": "Continue",
|
||||
"@continue": {
|
||||
"context": "form_post.html:25:28-48, pages/consent.html:57:28-48, pages/device_consent.html:124:13-33, pages/device_link.html:40:26-46, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:77:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/registration_token.html:41:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:37:28-48"
|
||||
"context": "form_post.html:25:28-48, pages/consent.html:67:28-48, pages/device_consent.html:133:13-33, pages/device_link.html:40:26-46, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:77:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/registration_token.html:41:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:50:28-48"
|
||||
},
|
||||
"create_account": "Create Account",
|
||||
"@create_account": {
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"sign_out": "Sign out",
|
||||
"@sign_out": {
|
||||
"context": "pages/account/logged_out.html:22:28-48, pages/consent.html:65:28-48, pages/device_consent.html:136:30-50, pages/index.html:28:28-48, pages/policy_violation.html:38:28-48, pages/sso.html:45:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46"
|
||||
"context": "pages/account/logged_out.html:22:28-48, pages/compat_login_policy_violation.html:28:28-48, pages/index.html:28:28-48, pages/policy_violation.html:38:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46"
|
||||
},
|
||||
"skip": "Skip",
|
||||
"@skip": {
|
||||
@@ -165,8 +165,6 @@
|
||||
"@current": {
|
||||
"description": "Field for the user's current password"
|
||||
},
|
||||
"description": "This will change the password on your account.",
|
||||
"@description": {},
|
||||
"heading": "Change my password",
|
||||
"@heading": {
|
||||
"description": "Heading on the change password page"
|
||||
@@ -189,43 +187,39 @@
|
||||
}
|
||||
},
|
||||
"consent": {
|
||||
"client_wants_access": "<span>%(client_name)s</span> at <span>%(redirect_uri)s</span> wants to access your account.",
|
||||
"@client_wants_access": {
|
||||
"context": "pages/consent.html:27:11-122"
|
||||
"continue_to": "Continue to <span>%(client_name)s</span>?",
|
||||
"@continue_to": {
|
||||
"context": "pages/consent.html:27:11-64, pages/device_consent.html:29:13-66, pages/sso.html:23:11-64"
|
||||
},
|
||||
"heading": "Allow access to your account?",
|
||||
"@heading": {
|
||||
"context": "pages/consent.html:25:27-51, pages/device_consent.html:28:29-53"
|
||||
"scope_list_preface": "By continuing, you allow <span>%(client_name)s</span> to:",
|
||||
"@scope_list_preface": {
|
||||
"context": "pages/consent.html:39:13-73, pages/device_consent.html:104:15-75"
|
||||
},
|
||||
"make_sure_you_trust": "Make sure that you trust <span>%(client_name)s</span>.",
|
||||
"@make_sure_you_trust": {
|
||||
"context": "pages/consent.html:38:81-142, pages/device_consent.html:104:83-144"
|
||||
"this_will_setup": "This will set up %(client_name)s (<span>%(client_uri)s</span>) with your <span>%(server_name)s</span> account.",
|
||||
"@this_will_setup": {
|
||||
"context": "pages/consent.html:30:11-173"
|
||||
},
|
||||
"this_will_allow": "This will allow <span>%(client_name)s</span> to:",
|
||||
"@this_will_allow": {
|
||||
"context": "pages/consent.html:28:11-68, pages/device_consent.html:94:13-70"
|
||||
},
|
||||
"you_may_be_sharing": "You may be sharing sensitive information with this site or app.",
|
||||
"@you_may_be_sharing": {
|
||||
"context": "pages/consent.html:39:7-42, pages/device_consent.html:105:9-44"
|
||||
"use_another_account": "Use another account",
|
||||
"@use_another_account": {
|
||||
"context": "pages/consent.html:72:11-47, pages/device_consent.html:139:13-49, pages/sso.html:55:11-47"
|
||||
}
|
||||
},
|
||||
"device_card": {
|
||||
"access_requested": "Access requested",
|
||||
"@access_requested": {
|
||||
"context": "pages/device_consent.html:82:34-71"
|
||||
"context": "pages/device_consent.html:88:34-71"
|
||||
},
|
||||
"device_code": "Code",
|
||||
"@device_code": {
|
||||
"context": "pages/device_consent.html:86:34-66"
|
||||
"context": "pages/device_consent.html:92:34-66"
|
||||
},
|
||||
"generic_device": "Device",
|
||||
"@generic_device": {
|
||||
"context": "pages/device_consent.html:70:22-57"
|
||||
"context": "pages/device_consent.html:76:22-57"
|
||||
},
|
||||
"ip_address": "IP address",
|
||||
"@ip_address": {
|
||||
"context": "pages/device_consent.html:77:36-67"
|
||||
"context": "pages/device_consent.html:83:36-67"
|
||||
}
|
||||
},
|
||||
"device_code_link": {
|
||||
@@ -239,29 +233,29 @@
|
||||
}
|
||||
},
|
||||
"device_consent": {
|
||||
"another_device_access": "Another device wants to access your account.",
|
||||
"@another_device_access": {
|
||||
"context": "pages/device_consent.html:93:13-58"
|
||||
},
|
||||
"denied": {
|
||||
"description": "You denied access to %(client_name)s. You can close this window.",
|
||||
"@description": {
|
||||
"context": "pages/device_consent.html:147:27-94"
|
||||
"context": "pages/device_consent.html:158:27-94"
|
||||
},
|
||||
"heading": "Access denied",
|
||||
"@heading": {
|
||||
"context": "pages/device_consent.html:146:29-67"
|
||||
"context": "pages/device_consent.html:157:29-67"
|
||||
}
|
||||
},
|
||||
"granted": {
|
||||
"description": "You granted access to %(client_name)s. You can close this window.",
|
||||
"@description": {
|
||||
"context": "pages/device_consent.html:158:27-95"
|
||||
"context": "pages/device_consent.html:169:27-95"
|
||||
},
|
||||
"heading": "Access granted",
|
||||
"@heading": {
|
||||
"context": "pages/device_consent.html:157:29-68"
|
||||
"context": "pages/device_consent.html:168:29-68"
|
||||
}
|
||||
},
|
||||
"this_will_setup": "Another device wants to set up %(client_name)s (<span>%(client_uri)s</span>) with your <span>%(server_name)s</span> account. Make sure you recognise that device.",
|
||||
"@this_will_setup": {
|
||||
"context": "pages/device_consent.html:33:13-166"
|
||||
}
|
||||
},
|
||||
"device_display_name": {
|
||||
@@ -416,6 +410,12 @@
|
||||
"context": "components/field.html:32:11-45"
|
||||
}
|
||||
},
|
||||
"legacy_consent": {
|
||||
"this_will_setup": "This will set up <span>%(client_name)s</span> with your <span>%(server_name)s</span> account.",
|
||||
"@this_will_setup": {
|
||||
"context": "pages/sso.html:26:11-109"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"call_to_register": "Don't have an account yet?",
|
||||
"@call_to_register": {
|
||||
@@ -485,7 +485,6 @@
|
||||
},
|
||||
"not_you": "Not %(username)s?",
|
||||
"@not_you": {
|
||||
"context": "pages/consent.html:62:11-67, pages/device_consent.html:133:13-69, pages/sso.html:42:11-67",
|
||||
"description": "Suggestions for the user to log in as a different user"
|
||||
},
|
||||
"or_separator": "Or",
|
||||
@@ -496,17 +495,17 @@
|
||||
"policy_violation": {
|
||||
"description": "This might be because of the client which authored the request, the currently logged in user, or the request itself.",
|
||||
"@description": {
|
||||
"context": "pages/policy_violation.html:19:25-62",
|
||||
"context": "pages/compat_login_policy_violation.html:18:25-62, pages/policy_violation.html:19:25-62",
|
||||
"description": "Displayed when an authorization request is denied by the policy"
|
||||
},
|
||||
"heading": "The authorization request was denied by the policy enforced by this service",
|
||||
"@heading": {
|
||||
"context": "pages/policy_violation.html:18:27-60",
|
||||
"context": "pages/compat_login_policy_violation.html:17:27-60, pages/policy_violation.html:18:27-60",
|
||||
"description": "Displayed when an authorization request is denied by the policy"
|
||||
},
|
||||
"logged_as": "Logged as <span class=\"font-semibold\">%(username)s</span>",
|
||||
"@logged_as": {
|
||||
"context": "pages/policy_violation.html:35:11-86"
|
||||
"context": "pages/compat_login_policy_violation.html:25:11-86, pages/policy_violation.html:35:11-86"
|
||||
}
|
||||
},
|
||||
"recovery": {
|
||||
@@ -656,36 +655,36 @@
|
||||
"scope": {
|
||||
"edit_profile": "Edit your profile and contact details",
|
||||
"@edit_profile": {
|
||||
"context": "components/scope.html:15:35-62",
|
||||
"context": "components/scope.html:44:35-62",
|
||||
"description": "Displayed when the 'urn:mas:graphql:*' scope is requested"
|
||||
},
|
||||
"manage_sessions": "Manage your devices and sessions",
|
||||
"@manage_sessions": {
|
||||
"context": "components/scope.html:16:39-69",
|
||||
"context": "components/scope.html:45:39-69",
|
||||
"description": "Displayed when the 'urn:mas:graphql:*' scope is requested"
|
||||
},
|
||||
"mas_admin": "Administer any user on the matrix-authentication-service",
|
||||
"mas_admin": "Manage users (urn:mas:admin)",
|
||||
"@mas_admin": {
|
||||
"context": "components/scope.html:23:54-78",
|
||||
"context": "components/scope.html:52:54-91",
|
||||
"description": "Displayed when the 'urn:mas:admin' scope is requested"
|
||||
},
|
||||
"send_messages": "Send new messages on your behalf",
|
||||
"@send_messages": {
|
||||
"context": "components/scope.html:19:35-63"
|
||||
"context": "components/scope.html:48:35-63"
|
||||
},
|
||||
"synapse_admin": "Administer the Synapse homeserver",
|
||||
"synapse_admin": "Administer the server (urn:synapse:admin:*)",
|
||||
"@synapse_admin": {
|
||||
"context": "components/scope.html:21:53-81",
|
||||
"context": "components/scope.html:50:53-94",
|
||||
"description": "Displayed when the 'urn:synapse:admin:*' scope is requested"
|
||||
},
|
||||
"view_messages": "View your existing messages and data",
|
||||
"@view_messages": {
|
||||
"context": "components/scope.html:18:35-63",
|
||||
"context": "components/scope.html:47:35-63",
|
||||
"description": "Displayed when the 'urn:matrix:client:api:*' scope is requested"
|
||||
},
|
||||
"view_profile": "See your profile info and contact details",
|
||||
"@view_profile": {
|
||||
"context": "components/scope.html:13:43-70",
|
||||
"context": "components/scope.html:42:43-70",
|
||||
"description": "Displayed when the 'openid' scope is requested"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -74,9 +74,13 @@
|
||||
},
|
||||
"consent": {
|
||||
"client_wants_access": "<span>%(client_name)s</span> aadressil <span>%(redirect_uri)s</span> soovib ligipääsu sinu kasutajakontole.",
|
||||
"continue_to": "Kas jätkad kliendis <span>%(client_name)s</span>?",
|
||||
"heading": "Kas lubad ligipääsu sinu kasutajakontole?",
|
||||
"make_sure_you_trust": "Palun kontrolli, et <span>%(client_name)s</span> on sinu jaoks usaldusväärne teenus.",
|
||||
"scope_list_preface": "Jätkates lubad sa <span>%(client_name)s</span> kliendil:",
|
||||
"this_will_allow": "Sellega <span>%(client_name)s</span> saab õigused:",
|
||||
"this_will_setup": "Sellega seadistad %(client_name)s (<span>%(client_uri)s</span>) kliendi kasutama sinu <span>%(server_name)s</span> kontot.",
|
||||
"use_another_account": "Kasuta teist kontot",
|
||||
"you_may_be_sharing": "Sa tõenäoliselt jagad privaatset teavet selle veebisaidi või rakendusega."
|
||||
},
|
||||
"device_card": {
|
||||
@@ -98,7 +102,8 @@
|
||||
"granted": {
|
||||
"description": "Sa lubasid seadmele %(client_name)s ligipääsu. Sa võid nüüd selle akna sulgeda.",
|
||||
"heading": "Ligipääs on lubatud"
|
||||
}
|
||||
},
|
||||
"this_will_setup": "Üks teine seade tahab seadistada %(client_name)s (<span>%(client_uri)s</span>) klienti kasutama sinu <span>%(server_name)s</span> kontot. Palun kontrolli, et see on õige ja sinule vajalik seade."
|
||||
},
|
||||
"device_display_name": {
|
||||
"client_on_device": "%(client_name)s seadmes %(device_name)s",
|
||||
@@ -145,6 +150,9 @@
|
||||
"username_too_long": "Kasutajanimi on liiga pikk",
|
||||
"username_too_short": "Kasutajanimi on liiga lühike"
|
||||
},
|
||||
"legacy_consent": {
|
||||
"this_will_setup": "Sellega seadistad <span>%(client_name)s</span> kliendi kasutama oma <span>%(server_name)s</span> kontot."
|
||||
},
|
||||
"login": {
|
||||
"call_to_register": "Sul veel pole kasutajakontot?",
|
||||
"continue_with_provider": "Jätka teenusepakkujaga %(provider)s",
|
||||
@@ -226,9 +234,9 @@
|
||||
"scope": {
|
||||
"edit_profile": "Muuta sinu kasutajaprofiili ning kontaktandmeid",
|
||||
"manage_sessions": "Hallata sinu seadmeid ja sessioone",
|
||||
"mas_admin": "Hallata iga kasutajat teenuses matrix-authentication-service",
|
||||
"mas_admin": "Hallata kasutajaid (urn:mas:admin)",
|
||||
"send_messages": "Saata sõnumeid sinu nimel",
|
||||
"synapse_admin": "Hallata seda Synapse koduserverit",
|
||||
"synapse_admin": "Hallata seda Synapse koduserverit (urn:synapse:admin:*)",
|
||||
"view_messages": "Vaadata sinu sõnumeid ja andmeid",
|
||||
"view_profile": "Vaadata sinu profiili teavet ja kontaktadmeid"
|
||||
},
|
||||
|
||||
@@ -74,9 +74,13 @@
|
||||
},
|
||||
"consent": {
|
||||
"client_wants_access": "<span>%(client_name)s</span> à l'adresse <span>%(redirect_uri)s</span> souhaite accéder à votre compte.",
|
||||
"continue_to": "Continuer vers <span>%(client_name)s</span>?",
|
||||
"heading": "Autoriser l'accès à votre compte ?",
|
||||
"make_sure_you_trust": "Assurez-vous de faire confiance <span>%(client_name)s</span>.",
|
||||
"scope_list_preface": "En continuant, vous autorisez <span>%(client_name)s</span> à :",
|
||||
"this_will_allow": "Cela va permettre à <span>%(client_name)s</span> de :",
|
||||
"this_will_setup": "Continuer connectera %(client_name)s (<span>%(client_uri)s</span>) avec votre compte %(server_name)s<span></span>.",
|
||||
"use_another_account": "Utiliser un autre compte",
|
||||
"you_may_be_sharing": "Vous partagez peut-être des informations sensibles avec ce site ou cette application."
|
||||
},
|
||||
"device_card": {
|
||||
@@ -98,7 +102,8 @@
|
||||
"granted": {
|
||||
"description": "Vous avez accordé l'accès à %(client_name)s. Vous pouvez fermer cette fenêtre.",
|
||||
"heading": "Accès accordé"
|
||||
}
|
||||
},
|
||||
"this_will_setup": "Un autre appareil souhaite connecter %(client_name)s (<span>%(client_uri)s</span>) avec votre compte <span>%(server_name)s</span>. Assurez-vous de reconnaître cet appareil."
|
||||
},
|
||||
"device_display_name": {
|
||||
"client_on_device": "%(client_name)s sur %(device_name)s",
|
||||
@@ -145,6 +150,9 @@
|
||||
"username_too_long": "Le nom d'utilisateur est trop long",
|
||||
"username_too_short": "Le nom d'utilisateur est trop court"
|
||||
},
|
||||
"legacy_consent": {
|
||||
"this_will_setup": "Continuer connectera <span>%(client_name)s</span> avec votre compte <span>%(server_name)s</span>."
|
||||
},
|
||||
"login": {
|
||||
"call_to_register": "Vous n’avez pas encore de compte ?",
|
||||
"continue_with_provider": "Poursuivre avec %(provider)s",
|
||||
@@ -226,9 +234,9 @@
|
||||
"scope": {
|
||||
"edit_profile": "Modifier votre profil et vos coordonnées",
|
||||
"manage_sessions": "Gérer vos appareils et vos sessions",
|
||||
"mas_admin": "Administrer n'importe quel utilisateur dans matrix-authentication-service",
|
||||
"mas_admin": "Administrer les utilisateurs (urn:mas:admin)",
|
||||
"send_messages": "Envoyez de nouveaux messages en votre nom",
|
||||
"synapse_admin": "Administrer le serveur d’accueil Synapse",
|
||||
"synapse_admin": "Administrer le serveur (urn:synapse:admin:*)",
|
||||
"view_messages": "Afficher vos messages et données existants",
|
||||
"view_profile": "Voir les informations de votre profil et vos coordonnées"
|
||||
},
|
||||
|
||||
@@ -74,9 +74,13 @@
|
||||
},
|
||||
"consent": {
|
||||
"client_wants_access": "<span>%(client_name)s</span> за <span>%(redirect_uri)s</span> хоче отримати доступ до вашого облікового запису.",
|
||||
"continue_to": "Продовжити в <span>%(client_name)s</span>?",
|
||||
"heading": "Дозволити доступ до свого облікового запису?",
|
||||
"make_sure_you_trust": "Переконайтеся, що ви довіряєте <span> %(client_name)s</span>.",
|
||||
"scope_list_preface": "Продовжуючи, ви дозволяєте <span>%(client_name)s</span>:",
|
||||
"this_will_allow": "Це дозволить <span>%(client_name)s</span>:",
|
||||
"this_will_setup": "Це налаштує %(client_name)s (<span>%(client_uri)s</span>) з вашим обліковим записом <span>%(server_name)s</span>.",
|
||||
"use_another_account": "Використати інший обліковий запис",
|
||||
"you_may_be_sharing": "Можливо, ви ділитеся конфіденційною інформацією з цим сайтом або застосунком."
|
||||
},
|
||||
"device_card": {
|
||||
@@ -98,7 +102,8 @@
|
||||
"granted": {
|
||||
"description": "Ви надали доступ до %(client_name)s. Ви можете закрити це вікно.",
|
||||
"heading": "Доступ надано"
|
||||
}
|
||||
},
|
||||
"this_will_setup": "Інший пристрій хоче налаштувати %(client_name)s (<span>%(client_uri)s</span>) з вашим обліковим записом <span>%(server_name)s</span>. Переконайтеся, що ви розпізнаєте цей пристрій."
|
||||
},
|
||||
"device_display_name": {
|
||||
"client_on_device": "%(client_name)s на %(device_name)s",
|
||||
@@ -145,6 +150,9 @@
|
||||
"username_too_long": "Ім'я користувача задовге",
|
||||
"username_too_short": "Ім'я користувача закоротке"
|
||||
},
|
||||
"legacy_consent": {
|
||||
"this_will_setup": "Це налаштує <span>%(client_name)s</span> з вашим обліковим записом <span>%(server_name)s</span>."
|
||||
},
|
||||
"login": {
|
||||
"call_to_register": "У вас ще немає облікового запису?",
|
||||
"continue_with_provider": "Продовжити з %(provider)s",
|
||||
@@ -210,6 +218,7 @@
|
||||
"register": {
|
||||
"call_to_login": "Вже маєте обліковий запис?",
|
||||
"continue_with_email": "Продовжити за допомогою е-пошти",
|
||||
"continue_with_password": "Продовжити з паролем",
|
||||
"create_account": {
|
||||
"description": "Виберіть ім'я користувача, щоб продовжити.",
|
||||
"heading": "Створити обліковий запис"
|
||||
@@ -225,9 +234,9 @@
|
||||
"scope": {
|
||||
"edit_profile": "Редагування профілю та контактних даних",
|
||||
"manage_sessions": "Керування пристроями та сеансами",
|
||||
"mas_admin": "Адміністрування будь-якого користувача на matrix-authentication-service",
|
||||
"mas_admin": "Керування користувачами (urn:mas:admin)",
|
||||
"send_messages": "Надсилати нові повідомлення від вашого імені",
|
||||
"synapse_admin": "Адміністрування домашнього сервера Synapse",
|
||||
"synapse_admin": "Адмініструвати сервер (urn:synapse:admin:*)",
|
||||
"view_messages": "Перегляд наявних повідомлень і даних",
|
||||
"view_profile": "Перегляд інформації профілю та контактних даних"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user