@@ -116,8 +116,7 @@ pub async fn login(
116116 let nonce = store_oauth_nonce ( & redirect) ;
117117
118118 let scope = PARSEABLE . options . scope . to_string ( ) ;
119- let mut auth_url: String =
120- client. read ( ) . await . auth_url ( & scope, Some ( nonce) ) . into ( ) ;
119+ let mut auth_url: String = client. read ( ) . await . auth_url ( & scope, Some ( nonce) ) . into ( ) ;
121120
122121 auth_url. push_str ( "&access_type=offline&prompt=consent" ) ;
123122 return Ok ( HttpResponse :: TemporaryRedirect ( )
@@ -220,134 +219,154 @@ pub async fn reply_login(
220219 req : HttpRequest ,
221220 login_query : web:: Query < Login > ,
222221) -> Result < HttpResponse , OIDCError > {
223- let oidc_client = if let Some ( oidc_client) = OIDC_CLIENT . get ( ) {
224- oidc_client
225- } else {
226- return Err ( OIDCError :: Unauthorized ) ;
227- } ;
222+ let oidc_client = OIDC_CLIENT . get ( ) . ok_or ( OIDCError :: Unauthorized ) ?;
228223 let tenant_id = get_tenant_id_from_request ( & req) ;
229224
230- let OAuthSession {
231- bearer,
232- claims,
233- userinfo : user_info,
234- } = match oidc_client
225+ let session = oidc_client
235226 . write ( )
236227 . await
237228 . exchange_code ( & login_query. code )
238229 . await
239- {
240- Ok ( session) => session,
241- Err ( e) => {
230+ . map_err ( |e| {
242231 tracing:: error!( "reply_login exchange_code failed: {e}" ) ;
243- return Ok ( HttpResponse :: Unauthorized ( ) . finish ( ) ) ;
232+ OIDCError :: Unauthorized
233+ } ) ?;
234+
235+ let ( username, user_id, user_info) = extract_identity ( & session) ?;
236+ let metadata = get_metadata ( & tenant_id) . await ?;
237+ let existing_user = find_existing_user ( & user_info, tenant_id. clone ( ) ) ;
238+ let final_roles = resolve_roles (
239+ & session. claims . groups ,
240+ & metadata,
241+ & user_info,
242+ & tenant_id,
243+ existing_user. as_ref ( ) ,
244+ ) ;
245+
246+ let expires_in = bearer_expiry ( & session. bearer ) ;
247+ let user = match ( existing_user, final_roles) {
248+ ( Some ( user) , roles) => {
249+ update_user_if_changed ( user, roles, user_info, session. bearer ) . await ?
250+ }
251+ ( None , roles) => {
252+ put_user (
253+ & user_id,
254+ roles,
255+ user_info,
256+ session. bearer ,
257+ tenant_id. clone ( ) ,
258+ )
259+ . await ?
244260 }
245261 } ;
246262
247- let Some ( username) = user_info
263+ let id = Ulid :: new ( ) ;
264+ Users . new_session ( & user, SessionKey :: SessionId ( id) , expires_in) ;
265+
266+ let cookies = [
267+ cookie_session ( id) ,
268+ cookie_username ( & username) ,
269+ cookie_userid ( & user_id) ,
270+ ] ;
271+
272+ Ok ( build_login_response (
273+ & req,
274+ & login_query,
275+ cookies,
276+ id,
277+ & username,
278+ & user_id,
279+ ) )
280+ }
281+
282+ /// Extract username, user_id, and UserInfo from the OAuth session.
283+ fn extract_identity ( session : & OAuthSession ) -> Result < ( String , String , user:: UserInfo ) , OIDCError > {
284+ let user_info = & session. userinfo ;
285+ let username = user_info
248286 . name
249287 . clone ( )
250288 . or_else ( || user_info. email . clone ( ) )
251289 . or_else ( || user_info. sub . clone ( ) )
252- else {
253- tracing:: error!( "OAuth provider did not return a usable identifier (name, email or sub)" ) ;
254- return Err ( OIDCError :: Unauthorized ) ;
255- } ;
256- let user_id = match user_info. sub . clone ( ) {
257- Some ( id) => id,
258- None => {
259- tracing:: error!( "OAuth provider did not return a sub" ) ;
260- return Err ( OIDCError :: Unauthorized ) ;
261- }
262- } ;
263- let user_info: user:: UserInfo = user_info. into ( ) ;
264- let group = claims. groups . clone ( ) ;
265- let metadata = get_metadata ( & tenant_id) . await ?;
290+ . ok_or_else ( || {
291+ tracing:: error!(
292+ "OAuth provider did not return a usable identifier (name, email or sub)"
293+ ) ;
294+ OIDCError :: Unauthorized
295+ } ) ?;
296+ let user_id = user_info. sub . clone ( ) . ok_or_else ( || {
297+ tracing:: error!( "OAuth provider did not return a sub" ) ;
298+ OIDCError :: Unauthorized
299+ } ) ?;
300+ Ok ( ( username, user_id, user_info. clone ( ) . into ( ) ) )
301+ }
266302
267- // Find which OIDC groups match existing roles in Parseable
268- let mut valid_oidc_roles = HashSet :: new ( ) ;
269- for role in metadata. roles . iter ( ) {
270- let role_name = role. 0 ;
271- if group. contains ( role_name) {
272- valid_oidc_roles. insert ( role_name. clone ( ) ) ;
273- }
274- }
303+ /// Determine the final set of roles for the user.
304+ fn resolve_roles (
305+ groups : & HashSet < String > ,
306+ metadata : & StorageMetadata ,
307+ user_info : & user:: UserInfo ,
308+ tenant_id : & Option < String > ,
309+ existing_user : Option < & User > ,
310+ ) -> HashSet < String > {
311+ let valid_oidc_roles: HashSet < String > = metadata
312+ . roles
313+ . keys ( )
314+ . filter ( |role_name| groups. contains ( * role_name) )
315+ . cloned ( )
316+ . collect ( ) ;
275317
276- let default_role = if let Some ( role ) = DEFAULT_ROLE
318+ let default_role = DEFAULT_ROLE
277319 . read ( )
278320 . get ( tenant_id. as_deref ( ) . unwrap_or ( DEFAULT_TENANT ) )
279- && let Some ( role) = role
280- {
281- HashSet :: from ( [ role. to_owned ( ) ] )
282- } else {
283- HashSet :: new ( )
284- } ;
321+ . and_then ( |r| r. clone ( ) )
322+ . map ( |r| HashSet :: from ( [ r] ) )
323+ . unwrap_or_default ( ) ;
285324
286- let existing_user = find_existing_user ( & user_info, tenant_id. clone ( ) ) ;
287- let mut final_roles = match existing_user {
288- Some ( ref user) => {
289- // For existing users: keep existing roles + add new valid OIDC roles
325+ let mut roles = match existing_user {
326+ Some ( user) => {
290327 let mut roles = user. roles . clone ( ) ;
291- roles. extend ( valid_oidc_roles) ; // Add new matching roles
328+ roles. extend ( valid_oidc_roles) ;
292329 roles
293330 }
294- None => {
295- // For new users: use valid OIDC roles, fallback to default if none
296- if valid_oidc_roles. is_empty ( ) {
297- default_role. clone ( )
298- } else {
299- valid_oidc_roles
300- }
301- }
331+ None if !valid_oidc_roles. is_empty ( ) => valid_oidc_roles,
332+ None => default_role. clone ( ) ,
302333 } ;
303- if final_roles . is_empty ( ) {
304- // If no roles were found, use the default role
305- final_roles . clone_from ( & default_role) ;
334+
335+ if roles. is_empty ( ) {
336+ roles . clone_from ( & default_role) ;
306337 }
307- // If still no roles, look for a native user with the same email
308- // and inherit their roles (e.g. tenant owner logging in via OAuth)
309- if final_roles . is_empty ( )
338+
339+ // Inherit roles from a native user with the same email (e.g. tenant owner via OAuth)
340+ if roles . is_empty ( )
310341 && let Some ( email) = & user_info. email
311- {
312- for u in & metadata. users {
313- if matches ! ( u. ty, UserType :: Native ( _) )
314- && u. userid ( ) == email. as_str ( )
315- && !u. roles . is_empty ( )
316- {
317- final_roles. clone_from ( & u. roles ) ;
318- break ;
342+ && let Some ( native) = metadata. users . iter ( ) . find ( |u| {
343+ matches ! ( u. ty, UserType :: Native ( _) )
344+ && u. userid ( ) == email. as_str ( )
345+ && !u. roles . is_empty ( )
346+ } ) {
347+ roles. clone_from ( & native. roles ) ;
319348 }
320- }
321- }
322-
323- let expires_in = if let Some ( expires_in) = bearer. expires_in . as_ref ( ) {
324- // need an i64 somehow
325- if * expires_in > u32:: MAX . into ( ) {
326- EXPIRY_DURATION
327- } else {
328- let v = i64:: from ( * expires_in as u32 ) ;
329- Duration :: seconds ( v)
330- }
331- } else {
332- EXPIRY_DURATION
333- } ;
334349
335- let user = match ( existing_user, final_roles) {
336- ( Some ( user) , roles) => update_user_if_changed ( user, roles, user_info, bearer) . await ?,
337- ( None , roles) => put_user ( & user_id, roles, user_info, bearer, tenant_id. clone ( ) ) . await ?,
338- } ;
339-
340- let id = Ulid :: new ( ) ;
341- Users . new_session ( & user, SessionKey :: SessionId ( id) , expires_in) ;
350+ roles
351+ }
342352
343- let cookies = [
344- cookie_session ( id) ,
345- cookie_username ( & username) ,
346- cookie_userid ( & user_id) ,
347- ] ;
353+ /// Compute session expiry from the bearer token.
354+ fn bearer_expiry ( bearer : & Bearer ) -> TimeDelta {
355+ match bearer. expires_in . as_ref ( ) {
356+ Some ( & exp) if exp <= u32:: MAX . into ( ) => Duration :: seconds ( i64:: from ( exp as u32 ) ) ,
357+ _ => EXPIRY_DURATION ,
358+ }
359+ }
348360
349- // If the request is an XHR/fetch call (e.g. from the SPA frontend),
350- // return 200 with cookies instead of a 301 redirect to avoid CORS issues.
361+ /// Build the HTTP response for the login callback (XHR JSON or redirect).
362+ fn build_login_response (
363+ req : & HttpRequest ,
364+ login_query : & web:: Query < Login > ,
365+ cookies : [ Cookie < ' static > ; 3 ] ,
366+ session_id : Ulid ,
367+ username : & str ,
368+ user_id : & str ,
369+ ) -> HttpResponse {
351370 let is_xhr = req. headers ( ) . contains_key ( "x-p-tenant" )
352371 || req
353372 . headers ( )
@@ -361,20 +380,19 @@ pub async fn reply_login(
361380 for cookie in cookies {
362381 response. cookie ( cookie) ;
363382 }
364- Ok ( response. json ( serde_json:: json!( {
365- "session" : id . to_string( ) ,
383+ response. json ( serde_json:: json!( {
384+ "session" : session_id . to_string( ) ,
366385 "username" : username,
367386 "user_id" : user_id,
368- } ) ) )
387+ } ) )
369388 } else {
370- // The state parameter is a nonce; resolve it to the original redirect URL.
371389 let redirect_url = login_query
372390 . state
373391 . as_deref ( )
374392 . and_then ( consume_oauth_nonce)
375393 . unwrap_or_else ( || PARSEABLE . options . address . to_string ( ) ) ;
376394
377- Ok ( redirect_to_client ( & redirect_url, cookies) )
395+ redirect_to_client ( & redirect_url, cookies)
378396 }
379397}
380398
@@ -630,4 +648,4 @@ fn is_valid_redirect_url(base_url_without_scheme: &str, redirect_url: &str) -> b
630648 let redirect_url_without_scheme = http_scheme_match_regex. replace ( redirect_url, "" ) ;
631649
632650 base_url_without_scheme == redirect_url_without_scheme
633- }
651+ }
0 commit comments