Skip to content

Commit c30b69a

Browse files
committed
fix: deepsourcex
1 parent fad61a0 commit c30b69a

1 file changed

Lines changed: 125 additions & 107 deletions

File tree

src/handlers/http/oidc.rs

Lines changed: 125 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)