Skip to content

Commit 0064541

Browse files
odelcroimcalinghee
authored andcommitted
add error when email is not allowed (#8)
1 parent 3802ae1 commit 0064541

File tree

5 files changed

+225
-2
lines changed

5 files changed

+225
-2
lines changed

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/handlers/src/upstream_oauth2/link.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,12 @@ use mas_templates::{
3737
use minijinja::Environment;
3838
use opentelemetry::{Key, KeyValue, metrics::Counter};
3939
use serde::{Deserialize, Serialize};
40+
//:tchap:
41+
use tchap::{self, EmailAllowedResult};
4042
use thiserror::Error;
4143
use ulid::Ulid;
4244

45+
//:tchap: end
4346
use super::{
4447
UpstreamSessionsCookie,
4548
template::{AttributeMappingContext, environment},
@@ -445,7 +448,46 @@ pub(crate) async fn get(
445448
provider.claims_imports.email.is_required(),
446449
)? {
447450
Some(value) => {
448-
ctx.with_email(value, provider.claims_imports.email.is_forced_or_required())
451+
//:tchap:
452+
let server_name = homeserver.homeserver();
453+
let email_result = check_email_allowed(&value, server_name).await;
454+
455+
match email_result {
456+
EmailAllowedResult::Allowed => {
457+
// Email is allowed, continue
458+
}
459+
EmailAllowedResult::WrongServer => {
460+
// Email is mapped to a different server
461+
let ctx = ErrorContext::new()
462+
.with_code("wrong_server")
463+
.with_description(format!("Votre adresse mail {value} est associée à un autre serveur."))
464+
.with_details("Veuillez-vous contacter le support de Tchap [email protected]".to_owned())
465+
.with_language(&locale);
466+
467+
//return error template
468+
return Ok((
469+
cookie_jar,
470+
Html(templates.render_error(&ctx)?).into_response(),
471+
));
472+
}
473+
EmailAllowedResult::InvitationMissing => {
474+
// Server requires an invitation that is not present
475+
let ctx = ErrorContext::new()
476+
.with_code("invitation_missing")
477+
.with_description("Vous avez besoin d'une invitation pour accéder à Tchap.".to_owned())
478+
.with_details("Les partenaires externes peuvent accéder à Tchap uniquement avec une invitation d'un agent public.".to_owned())
479+
.with_language(&locale);
480+
481+
//return error template
482+
return Ok((
483+
cookie_jar,
484+
Html(templates.render_error(&ctx)?).into_response(),
485+
));
486+
}
487+
}
488+
//:tchap: end
489+
490+
ctx.with_email(value, provider.claims_imports.email.is_forced())
449491
}
450492
None => ctx,
451493
}
@@ -1007,6 +1049,19 @@ pub(crate) async fn post(
10071049
Ok((cookie_jar, post_auth_action.go_next(&url_builder)).into_response())
10081050
}
10091051

1052+
//:tchap:
1053+
///real function used when not testing
1054+
#[cfg(not(test))]
1055+
async fn check_email_allowed(email: &str, server_name: &str) -> EmailAllowedResult {
1056+
tchap::is_email_allowed(email, server_name).await
1057+
}
1058+
///mock function used when testing
1059+
#[cfg(test)]
1060+
async fn check_email_allowed(_email: &str, _server_name: &str) -> EmailAllowedResult {
1061+
EmailAllowedResult::Allowed
1062+
}
1063+
//:tchap:end
1064+
10101065
#[cfg(test)]
10111066
mod tests {
10121067
use hyper::{Request, StatusCode, header::CONTENT_TYPE};

crates/tchap/Cargo.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,13 @@ name = "tchap"
33
version = "0.1.0"
44
description = "Tchap-specific functionality for Matrix Authentication Service"
55
license = "MIT"
6+
edition = "2024"
67

7-
[dependencies]
8+
9+
[dependencies]
10+
reqwest.workspace = true
11+
serde_json.workspace = true
12+
tracing.workspace = true
13+
tokio.workspace = true
14+
url.workspace = true
15+
serde.workspace = true
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//! This module provides utilities for interacting with the Matrix identity
2+
//! server API.
3+
4+
use std::time::Duration;
5+
6+
use tracing::info;
7+
use url::Url;
8+
9+
fn default_identity_server_url() -> Url {
10+
// Try to read the TCHAP_IDENTITY_SERVER_URL environment variable
11+
match std::env::var("TCHAP_IDENTITY_SERVER_URL") {
12+
Ok(url_str) => {
13+
// Attempt to parse the URL from the environment variable
14+
match Url::parse(&url_str) {
15+
Ok(url) => {
16+
// Success: use the URL from the environment variable
17+
return url;
18+
}
19+
Err(err) => {
20+
// Parsing error: log a warning and use the default value
21+
tracing::warn!(
22+
"The TCHAP_IDENTITY_SERVER_URL environment variable contains an invalid URL: {}. Using default value.",
23+
err
24+
);
25+
}
26+
}
27+
}
28+
Err(std::env::VarError::NotPresent) => {
29+
// Variable not defined: use the default value without warning
30+
}
31+
Err(std::env::VarError::NotUnicode(_)) => {
32+
// Variable contains non-Unicode characters: log a warning
33+
tracing::warn!(
34+
"The TCHAP_IDENTITY_SERVER_URL environment variable contains non-Unicode characters. Using default value."
35+
);
36+
}
37+
}
38+
39+
// Default value if the environment variable is not defined or invalid
40+
Url::parse("http://localhost:8090").unwrap()
41+
}
42+
43+
/// Queries the identity server for information about an email address
44+
///
45+
/// # Parameters
46+
///
47+
/// * `email`: The email address to check///
48+
/// # Returns
49+
///
50+
/// A Result containing either the JSON response or an error
51+
pub async fn query_identity_server(email: &str) -> Result<serde_json::Value, reqwest::Error> {
52+
let identity_server_url = default_identity_server_url();
53+
54+
// Construct the URL with the email address
55+
let url = format!(
56+
"{}_matrix/identity/api/v1/internal-info?medium=email&address={}",
57+
identity_server_url, email
58+
);
59+
60+
info!("Making request to identity server: {}", url);
61+
62+
// Create a client with a timeout
63+
let client = reqwest::Client::builder()
64+
.timeout(Duration::from_secs(5))
65+
.build()
66+
.unwrap_or_default();
67+
68+
// Make the HTTP request asynchronously
69+
// should use mas-http instead like SynapseConnection
70+
#[allow(clippy::disallowed_methods)]
71+
let response = client.get(&url).send().await?;
72+
73+
// Parse the JSON response
74+
let json = response.json::<serde_json::Value>().await?;
75+
76+
Ok(json)
77+
}

crates/tchap/src/lib.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55

66
//! Tchap-specific functionality for Matrix Authentication Service
77
8+
extern crate tracing;
9+
use tracing::info;
10+
11+
mod identity_client;
12+
813
/// Capitalise parts of a name containing different words, including those
914
/// separated by hyphens.
1015
///
@@ -135,6 +140,76 @@ pub fn email_to_display_name(address: &str) -> String {
135140
format!("{} [{}]", cap(&username), cap(org))
136141
}
137142

143+
/// Result of checking if an email is allowed on a server
144+
#[derive(Debug, Clone, PartialEq, Eq)]
145+
pub enum EmailAllowedResult {
146+
/// Email is allowed on this server
147+
Allowed,
148+
/// Email is mapped to a different server
149+
WrongServer,
150+
/// Server requires an invitation that is not present
151+
InvitationMissing,
152+
}
153+
154+
/// Checks if an email address is allowed to be associated in the current server
155+
///
156+
/// This function makes an asynchronous GET request to the Matrix identity
157+
/// server API to retrieve information about the home server associated with an
158+
/// email address, then applies logic to determine if the email is allowed.
159+
///
160+
/// # Parameters
161+
///
162+
/// * `email`: The email address to check
163+
/// * `server_name`: The name of the server to check against
164+
///
165+
/// # Returns
166+
///
167+
/// An `EmailAllowedResult` indicating whether the email is allowed and if not,
168+
/// why
169+
#[must_use]
170+
pub async fn is_email_allowed(email: &str, server_name: &str) -> EmailAllowedResult {
171+
// Query the identity server
172+
match identity_client::query_identity_server(email).await {
173+
Ok(json) => {
174+
let hs = json.get("hs");
175+
176+
// Check if "hs" is in the response or if hs different from server_name
177+
if hs.is_none() || hs.unwrap() != server_name {
178+
// Email is mapped to a different server or no server at all
179+
return EmailAllowedResult::WrongServer;
180+
}
181+
182+
info!("hs: {} ", hs.unwrap());
183+
184+
// Check if requires_invite is true and invited is false
185+
let requires_invite = json
186+
.get("requires_invite")
187+
.and_then(|v| v.as_bool())
188+
.unwrap_or(false);
189+
190+
let invited = json
191+
.get("invited")
192+
.and_then(|v| v.as_bool())
193+
.unwrap_or(false);
194+
195+
info!("requires_invite: {} invited: {}", requires_invite, invited);
196+
197+
if requires_invite && !invited {
198+
// Requires an invite but hasn't been invited
199+
return EmailAllowedResult::InvitationMissing;
200+
}
201+
202+
// All checks passed
203+
EmailAllowedResult::Allowed
204+
}
205+
Err(err) => {
206+
// Log the error and return WrongServer as a default error
207+
eprintln!("HTTP request failed: {}", err);
208+
EmailAllowedResult::WrongServer
209+
}
210+
}
211+
}
212+
138213
#[cfg(test)]
139214
mod tests {
140215
use super::*;

0 commit comments

Comments
 (0)