Skip to content

Commit ec31000

Browse files
mcalingheeodelcroi
authored andcommitted
allow importing existing users when the localpart matches in upstream OAuth 2.0 logins
1 parent a0cd546 commit ec31000

File tree

15 files changed

+656
-25
lines changed

15 files changed

+656
-25
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ pub use self::{
5252
upstream_oauth2::{
5353
ClaimsImports as UpstreamOAuth2ClaimsImports, DiscoveryMode as UpstreamOAuth2DiscoveryMode,
5454
EmailImportPreference as UpstreamOAuth2EmailImportPreference,
55-
ImportAction as UpstreamOAuth2ImportAction,
55+
ImportAction as UpstreamOAuth2ImportAction, OnConflict as UpstreamOAuth2OnConflict,
5656
OnBackchannelLogout as UpstreamOAuth2OnBackchannelLogout,
5757
PkceMethod as UpstreamOAuth2PkceMethod, Provider as UpstreamOAuth2Provider,
5858
ResponseMode as UpstreamOAuth2ResponseMode,

crates/config/src/sections/upstream_oauth2.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,20 @@ impl ConfigurationSection for UpstreamOAuth2Config {
117117
}
118118
}
119119
}
120+
121+
if !provider.claims_imports.localpart.on_conflict.is_default()
122+
&& !matches!(
123+
provider.claims_imports.localpart.action,
124+
ImportAction::Force | ImportAction::Require
125+
)
126+
{
127+
return annotate(figment::Error::custom(
128+
"The field `action` must be either `force` or `require` when `on_conflict` is set to `add`",
129+
));
130+
}
131+
132+
//TODO : check that claims imports use on_conflict where it is not
133+
// supported?
120134
}
121135

122136
Ok(())
@@ -190,6 +204,26 @@ impl ImportAction {
190204
}
191205
}
192206

207+
/// How to handle an existing localpart claim
208+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
209+
#[serde(rename_all = "lowercase")]
210+
pub enum OnConflict {
211+
/// Fails the sso login on conflict
212+
#[default]
213+
Fail,
214+
215+
/// Adds the oauth identity link, regardless of whether there is an existing
216+
/// link or not
217+
Add,
218+
}
219+
220+
impl OnConflict {
221+
#[allow(clippy::trivially_copy_pass_by_ref)]
222+
const fn is_default(&self) -> bool {
223+
matches!(self, OnConflict::Fail)
224+
}
225+
}
226+
193227
/// What should be done for the subject attribute
194228
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
195229
pub struct SubjectImportPreference {
@@ -218,6 +252,10 @@ pub struct LocalpartImportPreference {
218252
/// If not provided, the default template is `{{ user.preferred_username }}`
219253
#[serde(default, skip_serializing_if = "Option::is_none")]
220254
pub template: Option<String>,
255+
256+
/// How to handle conflicts on the claim, default value is `Fail`
257+
#[serde(default, skip_serializing_if = "OnConflict::is_default")]
258+
pub on_conflict: OnConflict,
221259
}
222260

223261
impl LocalpartImportPreference {

crates/data-model/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub use self::{
4242
UpstreamOAuthAuthorizationSession, UpstreamOAuthAuthorizationSessionState,
4343
UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports,
4444
UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderImportAction,
45-
UpstreamOAuthProviderImportPreference, UpstreamOAuthProviderOnBackchannelLogout,
45+
UpstreamOAuthProviderImportPreference, UpstreamOAuthProviderOnConflict, UpstreamOAuthProviderOnBackchannelLogout,
4646
UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderResponseMode,
4747
UpstreamOAuthProviderSubjectPreference, UpstreamOAuthProviderTokenAuthMethod,
4848
},

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub use self::{
1616
ImportAction as UpstreamOAuthProviderImportAction,
1717
ImportPreference as UpstreamOAuthProviderImportPreference,
1818
OnBackchannelLogout as UpstreamOAuthProviderOnBackchannelLogout,
19-
PkceMode as UpstreamOAuthProviderPkceMode,
19+
OnConflict as UpstreamOAuthProviderOnConflict, PkceMode as UpstreamOAuthProviderPkceMode,
2020
ResponseMode as UpstreamOAuthProviderResponseMode,
2121
SubjectPreference as UpstreamOAuthProviderSubjectPreference,
2222
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
@@ -339,6 +339,9 @@ pub struct ImportPreference {
339339

340340
#[serde(default)]
341341
pub template: Option<String>,
342+
343+
#[serde(default)]
344+
pub on_conflict: OnConflict,
342345
}
343346

344347
impl std::ops::Deref for ImportPreference {
@@ -391,3 +394,22 @@ impl ImportAction {
391394
}
392395
}
393396
}
397+
398+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
399+
#[serde(rename_all = "lowercase")]
400+
pub enum OnConflict {
401+
/// Fails the upstream OAuth 2.0 login
402+
#[default]
403+
Fail,
404+
405+
/// Adds the upstream account link, regardless of whether there is an
406+
/// existing link or not
407+
Add,
408+
}
409+
410+
impl OnConflict {
411+
#[must_use]
412+
pub fn is_add(&self) -> bool {
413+
matches!(self, Self::Add)
414+
}
415+
}

0 commit comments

Comments
 (0)