Skip to content

Commit a8109a3

Browse files
authored
add support to match identity based on email with fallback rules (#19)
* add email mapping fallback rule * add :tchap: comment
1 parent 66c298e commit a8109a3

File tree

8 files changed

+154
-2
lines changed

8 files changed

+154
-2
lines changed

Cargo.lock

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

crates/cli/src/commands/server.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ use mas_config::{
2222
};
2323
use mas_context::LogContext;
2424
use mas_data_model::{
25+
//:tchap:
26+
EmailLookupFallbackRule,
27+
//:tchap:end
2528
SystemClock,
2629
//:tchap:
2730
TchapConfig, // :tchap: end
@@ -361,6 +364,14 @@ impl Options {
361364
fn tchap_config_from_tchap_app_config(tchap_app_config: &TchapAppConfig) -> TchapConfig {
362365
TchapConfig {
363366
identity_server_url: tchap_app_config.identity_server_url.clone(),
367+
email_lookup_fallback_rules: tchap_app_config
368+
.email_lookup_fallback_rules
369+
.iter()
370+
.map(|rule| EmailLookupFallbackRule {
371+
match_with: rule.match_with.clone(),
372+
search: rule.search.clone(),
373+
})
374+
.collect(),
364375
}
365376
}
366377
//:tchap: end

crates/config/src/sections/tchap.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,24 @@ pub struct TchapAppConfig {
4141
/// Identity Server Url
4242
#[serde(default = "default_identity_server_url")]
4343
pub identity_server_url: Url,
44+
45+
/// Fallback Rules to use when linking an upstream account
46+
#[serde(default)]
47+
pub email_lookup_fallback_rules: Vec<EmailLookupFallbackRule>,
48+
}
49+
50+
/// When linking the localpart, the email can be used to find the correct
51+
/// localpart. By using the fallback rule, we can search for a Matrix account
52+
/// with the `search` email pattern for an upstream account matching with the
53+
/// `match_with` pattern
54+
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, Default, JsonSchema)]
55+
pub struct EmailLookupFallbackRule {
56+
/// The upstream email pattern to match with when linking the localpart by
57+
/// email
58+
pub match_with: String,
59+
/// The email pattern to use for the search when linking the localpart by
60+
/// email
61+
pub search: String,
4462
}
4563

4664
impl ConfigurationSection for TchapAppConfig {
@@ -72,6 +90,9 @@ mod tests {
7290
r"
7391
tchap:
7492
identity_server_url: http://localhost:8091
93+
email_lookup_fallback_rules:
94+
- match_with : '@upstream.domain.tld'
95+
search: '@matrix.domain.tld'
7596
",
7697
)?;
7798

@@ -84,6 +105,14 @@ mod tests {
84105
"http://localhost:8091/"
85106
);
86107

108+
assert_eq!(
109+
config.email_lookup_fallback_rules,
110+
vec![EmailLookupFallbackRule {
111+
match_with: "@upstream.domain.tld".to_string(),
112+
search: "@matrix.domain.tld".to_string(),
113+
}]
114+
);
115+
87116
Ok(())
88117
});
89118
}

crates/data-model/src/tchap_config.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,13 @@ use url::Url;
3131
pub struct TchapConfig {
3232
/// Identity Server Url
3333
pub identity_server_url: Url,
34+
35+
/// Fallback Rules to use when linking an upstream account
36+
pub email_lookup_fallback_rules: Vec<EmailLookupFallbackRule>,
37+
}
38+
39+
#[derive(PartialEq, Eq, Clone, Debug)]
40+
pub struct EmailLookupFallbackRule {
41+
pub match_with: String,
42+
pub search: String,
3443
}

crates/handlers/src/upstream_oauth2/link.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,32 @@ pub(crate) async fn get(
524524
// We could run policy & existing user checks when the user submits the
525525
// form, but this lead to poor UX. This is why we do
526526
// it ahead of time here.
527-
let maybe_existing_user = repo.user().find_by_username(&localpart).await?;
527+
//:tchap:
528+
let mut maybe_existing_user =
529+
repo.user().find_by_username(&localpart).await?;
530+
//if not found by username, check by email
531+
if maybe_existing_user.is_none() {
532+
let template = provider
533+
.claims_imports
534+
.email
535+
.template
536+
.as_deref()
537+
.unwrap_or(DEFAULT_EMAIL_TEMPLATE);
538+
539+
let maybe_email = render_attribute_template(
540+
&env,
541+
template,
542+
&context,
543+
provider.claims_imports.email.is_required(),
544+
);
545+
546+
if let Ok(Some(email)) = maybe_email {
547+
maybe_existing_user =
548+
tchap::search_user_by_email(&mut repo, &email, &tchap_config)
549+
.await?;
550+
}
551+
}
552+
//:tchap: end
528553
let is_available = homeserver
529554
.is_localpart_available(&localpart)
530555
.await

crates/tchap/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ tokio.workspace = true
1414
url.workspace = true
1515
serde.workspace = true
1616

17-
mas-data-model.workspace = true
17+
mas-data-model.workspace = true
18+
mas-storage.workspace = true

crates/tchap/src/lib.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
2828
extern crate tracing;
2929
use mas_data_model::TchapConfig;
30+
use mas_storage::BoxRepository;
3031
use tracing::info;
3132

3233
mod identity_client;
@@ -233,6 +234,80 @@ pub async fn is_email_allowed(
233234
}
234235
}
235236

237+
/// Search for a user by email with fallback rules
238+
///
239+
/// # Parameters
240+
/// * `repo` - Repository access
241+
/// * `email` - The email to search for
242+
/// * `fallback_rules` - Fallback rules for email transformation
243+
///
244+
/// # Returns
245+
/// Option<`mas_data_model::User`> - The found user if any
246+
pub async fn search_user_by_email(
247+
repo: &mut BoxRepository,
248+
email: &str,
249+
tchap_config: &TchapConfig,
250+
) -> Result<Option<mas_data_model::User>, mas_storage::RepositoryError> {
251+
tracing::info!("Matching oidc identity by email:{}", email);
252+
let maybe_user_email = repo.user_email().find_by_email(email).await?;
253+
254+
if let Some(user_email) = maybe_user_email {
255+
let maybe_user_found: Option<mas_data_model::User> =
256+
repo.user().lookup(user_email.user_id).await?;
257+
return Ok(maybe_user_found);
258+
}
259+
260+
tracing::info!(
261+
"Email not found, Matching oidc identity by email using fallback rules:{}",
262+
email
263+
);
264+
let fallback_rules = &tchap_config.email_lookup_fallback_rules;
265+
// let fallback_rules: Value =
266+
// serde_json::from_str(r#"[{"match":"@numerique.gouv.fr",
267+
// "search":"@beta.gouv.fr"}]"#) .unwrap();
268+
269+
// Iterate on fallback_rules, if a rule 'match' matches the email,
270+
// replace by value of 'search' and lookup again the email
271+
for rule in fallback_rules {
272+
let match_pattern = &rule.match_with;
273+
let search_value = &rule.search;
274+
tracing::info!(
275+
"Checking fallback rules {} : {}",
276+
match_pattern,
277+
search_value
278+
);
279+
280+
// Check if email contains the match pattern
281+
if email.contains(match_pattern) {
282+
// Replace match pattern with search value
283+
let transformed_email = email.replace(match_pattern, search_value);
284+
tracing::debug!(
285+
"Search by transformed email fallback rules {}",
286+
transformed_email
287+
);
288+
289+
// Look up the transformed email
290+
let maybe_transformed_user_email =
291+
repo.user_email().find_by_email(&transformed_email).await?;
292+
293+
if let Some(transformed_user_email) = maybe_transformed_user_email {
294+
let user_found: Option<mas_data_model::User> =
295+
repo.user().lookup(transformed_user_email.user_id).await?;
296+
tracing::info!(
297+
"User found with fallback rules {} : {}",
298+
match_pattern,
299+
search_value
300+
);
301+
302+
return Ok(user_found);
303+
}
304+
}
305+
}
306+
307+
Ok(None)
308+
}
309+
//:tchap: end
310+
236311
pub use self::test_utils::*;
237312

238313
#[cfg(test)]

crates/tchap/src/test_utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ use url::Url;
2929
pub fn test_tchap_config() -> TchapConfig {
3030
TchapConfig {
3131
identity_server_url: Url::parse("http://localhost:8091").unwrap(),
32+
email_lookup_fallback_rules: vec![],
3233
}
3334
}

0 commit comments

Comments
 (0)