@ -12,7 +12,10 @@ use crate::{
auth ::{ AuthMethod , AuthTokens , BW_EXPIRATION , DEFAULT_REFRESH_VALIDITY , TokenWrapper } ,
db ::{
DbConn ,
models ::{ Device , OIDCAuthenticatedUser , SsoAuth , SsoUser , User } ,
models ::{
Device , Membership , MembershipStatus , MembershipType , OIDCAuthenticatedUser , Organization , OrganizationId ,
SsoAuth , SsoUser , User ,
} ,
} ,
sso_client ::Client ,
} ;
@ -328,6 +331,8 @@ pub async fn redeem(
sso_auth . delete ( conn ) . await ? ;
if sso_user . is_none ( ) {
enroll_user_in_default_organization ( user , conn ) . await ? ;
let user_sso = SsoUser {
user_uuid : user . uuid . clone ( ) ,
identifier : auth_user . identifier . clone ( ) ,
@ -354,6 +359,38 @@ pub async fn redeem(
}
}
async fn enroll_user_in_default_organization ( user : & User , conn : & DbConn ) -> ApiResult < ( ) > {
let Some ( org_uuid ) = CONFIG . sso_default_organization_uuid ( ) else {
return Ok ( ( ) ) ;
} ;
let org_id = normalize_organization_uuid ( & org_uuid ) ? ;
if Membership ::find_by_user_and_org ( & user . uuid , & org_id , conn ) . await . is_some ( ) {
return Ok ( ( ) ) ;
}
if Organization ::find_by_uuid ( & org_id , conn ) . await . is_none ( ) {
err ! ( "The organization configured in `SSO_DEFAULT_ORGANIZATION_UUID` does not exist" )
}
let mut membership = Membership ::new ( user . uuid . clone ( ) , org_id . clone ( ) , None ) ;
membership . status = MembershipStatus ::Accepted as i32 ;
membership . atype = MembershipType ::User as i32 ;
membership . save ( conn ) . await ? ;
info ! ( "Added SSO user {} to default organization {} pending confirmation" , user . uuid , org_id ) ;
Ok ( ( ) )
}
// `Uuid::parse_str` also accepts non-canonical forms (uppercase, braced, without hyphens),
// while stored organization uuids are always lowercase hyphenated and compared as strings.
pub ( crate ) fn normalize_organization_uuid ( org_uuid : & str ) -> ApiResult < OrganizationId > {
let Ok ( parsed ) = uuid ::Uuid ::parse_str ( org_uuid ) else {
err ! ( "`SSO_DEFAULT_ORGANIZATION_UUID` must be a valid UUID" )
} ;
Ok ( OrganizationId ::from ( parsed . to_string ( ) ) )
}
// We always return a refresh_token (with no refresh_token some secrets are not displayed in the web front).
// If there is no SSO refresh_token, we keep the access_token to be able to call user_info to check for validity
pub fn create_auth_tokens (
@ -471,3 +508,25 @@ pub async fn exchange_refresh_token(
None = > err ! ( "No token present while in SSO" ) ,
}
}
#[ cfg(test) ]
mod tests {
use super ::* ;
#[ test ]
fn normalizes_organization_uuid_to_canonical_form ( ) {
for input in [
"1B2C3D4E-5F60-7182-93A4-B5C6D7E8F901" ,
"{1b2c3d4e-5f60-7182-93a4-b5c6d7e8f901}" ,
"1b2c3d4e5f60718293a4b5c6d7e8f901" ,
] {
let org_id = normalize_organization_uuid ( input ) . expect ( "valid UUID form should be accepted" ) ;
assert_eq ! ( org_id . to_string ( ) , "1b2c3d4e-5f60-7182-93a4-b5c6d7e8f901" ) ;
}
}
#[ test ]
fn rejects_invalid_organization_uuid ( ) {
assert ! ( normalize_organization_uuid ( "not-a-uuid" ) . is_err ( ) ) ;
}
}