Skip to content

Commit 37f772c

Browse files
committed
allow importing existing users when the localpart matches in upstream OAuth 2.0 logins
1 parent 8658e8d commit 37f772c

File tree

15 files changed

+663
-77
lines changed

15 files changed

+663
-77
lines changed

crates/cli/src/sync.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,19 @@ fn map_import_action(
3737
}
3838
}
3939

40+
fn map_import_on_conflict(
41+
config: mas_config::UpstreamOAuth2OnConflict,
42+
) -> mas_data_model::UpstreamOAuthProviderOnConflict {
43+
match config {
44+
mas_config::UpstreamOAuth2OnConflict::Add => {
45+
mas_data_model::UpstreamOAuthProviderOnConflict::Add
46+
}
47+
mas_config::UpstreamOAuth2OnConflict::Fail => {
48+
mas_data_model::UpstreamOAuthProviderOnConflict::Fail
49+
}
50+
}
51+
}
52+
4053
fn map_claims_imports(
4154
config: &mas_config::UpstreamOAuth2ClaimsImports,
4255
) -> mas_data_model::UpstreamOAuthProviderClaimsImports {
@@ -47,14 +60,17 @@ fn map_claims_imports(
4760
localpart: mas_data_model::UpstreamOAuthProviderImportPreference {
4861
action: map_import_action(config.localpart.action),
4962
template: config.localpart.template.clone(),
63+
on_conflict: map_import_on_conflict(config.localpart.on_conflict),
5064
},
5165
displayname: mas_data_model::UpstreamOAuthProviderImportPreference {
5266
action: map_import_action(config.displayname.action),
5367
template: config.displayname.template.clone(),
68+
on_conflict: mas_data_model::UpstreamOAuthProviderOnConflict::default(),
5469
},
5570
email: mas_data_model::UpstreamOAuthProviderImportPreference {
5671
action: map_import_action(config.email.action),
5772
template: config.email.template.clone(),
73+
on_conflict: mas_data_model::UpstreamOAuthProviderOnConflict::default(),
5874
},
5975
account_name: mas_data_model::UpstreamOAuthProviderSubjectPreference {
6076
template: config.account_name.template.clone(),

crates/config/src/sections/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ pub use self::{
5252
upstream_oauth2::{
5353
ClaimsImports as UpstreamOAuth2ClaimsImports, DiscoveryMode as UpstreamOAuth2DiscoveryMode,
5454
EmailImportPreference as UpstreamOAuth2EmailImportPreference,
55-
ImportAction as UpstreamOAuth2ImportAction, PkceMethod as UpstreamOAuth2PkceMethod,
56-
Provider as UpstreamOAuth2Provider, ResponseMode as UpstreamOAuth2ResponseMode,
55+
ImportAction as UpstreamOAuth2ImportAction, OnConflict as UpstreamOAuth2OnConflict,
56+
PkceMethod as UpstreamOAuth2PkceMethod, Provider as UpstreamOAuth2Provider,
57+
ResponseMode as UpstreamOAuth2ResponseMode,
5758
TokenAuthMethod as UpstreamOAuth2TokenAuthMethod, UpstreamOAuth2Config,
5859
},
5960
};

crates/config/src/sections/upstream_oauth2.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,20 @@ impl ConfigurationSection for UpstreamOAuth2Config {
110110
}
111111
}
112112
}
113+
114+
if !provider.claims_imports.localpart.on_conflict.is_default()
115+
&& !matches!(
116+
provider.claims_imports.localpart.action,
117+
ImportAction::Force | ImportAction::Require
118+
)
119+
{
120+
return annotate(figment::Error::custom(
121+
"When `on_conflict` is used to resolved conflicts, `localpart` claim import must be either `force` or `require`",
122+
));
123+
}
124+
125+
//TODO : check that claims imports use on_conflict where it is not
126+
// supported?
113127
}
114128

115129
Ok(())
@@ -183,6 +197,26 @@ impl ImportAction {
183197
}
184198
}
185199

200+
/// How to handle an existing localpart claim
201+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
202+
#[serde(rename_all = "lowercase")]
203+
pub enum OnConflict {
204+
/// Fails the sso login on conflict
205+
#[default]
206+
Fail,
207+
208+
/// Adds the oauth identity link, regardless of whether there is an existing
209+
/// link or not
210+
Add,
211+
}
212+
213+
impl OnConflict {
214+
#[allow(clippy::trivially_copy_pass_by_ref)]
215+
const fn is_default(&self) -> bool {
216+
matches!(self, OnConflict::Fail)
217+
}
218+
}
219+
186220
/// What should be done for the subject attribute
187221
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
188222
pub struct SubjectImportPreference {
@@ -211,6 +245,10 @@ pub struct LocalpartImportPreference {
211245
/// If not provided, the default template is `{{ user.preferred_username }}`
212246
#[serde(default, skip_serializing_if = "Option::is_none")]
213247
pub template: Option<String>,
248+
249+
/// How to handle conflicts on the claim, default value is `Fail`
250+
#[serde(default, skip_serializing_if = "OnConflict::is_default")]
251+
pub on_conflict: OnConflict,
214252
}
215253

216254
impl LocalpartImportPreference {

crates/data-model/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ pub use self::{
4242
UpstreamOAuthAuthorizationSession, UpstreamOAuthAuthorizationSessionState,
4343
UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports,
4444
UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderImportAction,
45-
UpstreamOAuthProviderImportPreference, UpstreamOAuthProviderPkceMode,
46-
UpstreamOAuthProviderResponseMode, UpstreamOAuthProviderSubjectPreference,
47-
UpstreamOAuthProviderTokenAuthMethod,
45+
UpstreamOAuthProviderImportPreference, UpstreamOAuthProviderOnConflict,
46+
UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderResponseMode,
47+
UpstreamOAuthProviderSubjectPreference, UpstreamOAuthProviderTokenAuthMethod,
4848
},
4949
user_agent::{DeviceType, UserAgent},
5050
users::{

crates/data-model/src/upstream_oauth2/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub use self::{
1515
DiscoveryMode as UpstreamOAuthProviderDiscoveryMode,
1616
ImportAction as UpstreamOAuthProviderImportAction,
1717
ImportPreference as UpstreamOAuthProviderImportPreference,
18-
PkceMode as UpstreamOAuthProviderPkceMode,
18+
OnConflict as UpstreamOAuthProviderOnConflict, PkceMode as UpstreamOAuthProviderPkceMode,
1919
ResponseMode as UpstreamOAuthProviderResponseMode,
2020
SubjectPreference as UpstreamOAuthProviderSubjectPreference,
2121
TokenAuthMethod as UpstreamOAuthProviderTokenAuthMethod, UpstreamOAuthProvider,

crates/data-model/src/upstream_oauth2/provider.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,9 @@ pub struct ImportPreference {
296296

297297
#[serde(default)]
298298
pub template: Option<String>,
299+
300+
#[serde(default)]
301+
pub on_conflict: OnConflict,
299302
}
300303

301304
impl std::ops::Deref for ImportPreference {
@@ -348,3 +351,22 @@ impl ImportAction {
348351
}
349352
}
350353
}
354+
355+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
356+
#[serde(rename_all = "lowercase")]
357+
pub enum OnConflict {
358+
/// Fails the upstream OAuth 2.0 login
359+
#[default]
360+
Fail,
361+
362+
/// Adds the upstream account link, regardless of whether there is an
363+
/// existing link or not
364+
Add,
365+
}
366+
367+
impl OnConflict {
368+
#[must_use]
369+
pub fn is_add(&self) -> bool {
370+
matches!(self, Self::Add)
371+
}
372+
}

0 commit comments

Comments
 (0)