| ... | ... | @@ -291,6 +291,20 @@ fn signed_claims( |
| 291 | 291 | Ok(claims) |
| 292 | 292 | } |
| 293 | 293 | |
| 294 | pub(crate) fn astheno_profile(value: &str) -> Option<String> { |
| 295 | let url = url::Url::parse(value).ok()?; |
| 296 | (url.origin().ascii_serialization() == ASTHENO |
| 297 | && url.username().is_empty() |
| 298 | && url.password().is_none() |
| 299 | && url.query().is_none() |
| 300 | && url.fragment().is_none() |
| 301 | && url |
| 302 | .path() |
| 303 | .strip_prefix("/user/") |
| 304 | .is_some_and(|id| !id.is_empty() && !id.contains('/'))) |
| 305 | .then(|| url.into()) |
| 306 | } |
| 307 | |
| 294 | 308 | async fn exchange( |
| 295 | 309 | http: &reqwest::Client, |
| 296 | 310 | provider: &str, |
| ... | ... | @@ -299,7 +313,7 @@ async fn exchange( |
| 299 | 313 | code: &str, |
| 300 | 314 | callback: &str, |
| 301 | 315 | flow: &Value, |
| 302 | | ) -> Result<(String, String, Option<String>)> { |
| 316 | ) -> Result<(String, String, Option<String>, Option<String>)> { |
| 303 | 317 | let form = [ |
| 304 | 318 | ("client_id", client), |
| 305 | 319 | ("client_secret", secret), |
| ... | ... | @@ -356,7 +370,7 @@ async fn exchange( |
| 356 | 370 | "GitHub couldn't verify your account. Return to Shale and try again.", |
| 357 | 371 | ) |
| 358 | 372 | })?; |
| 359 | | return Ok((id.to_string(), login.to_owned(), None)); |
| 373 | return Ok((id.to_string(), login.to_owned(), None, None)); |
| 360 | 374 | } |
| 361 | 375 | let mut form = form.to_vec(); |
| 362 | 376 | form.push(("state", "none")); |
| ... | ... | @@ -441,7 +455,13 @@ async fn exchange( |
| 441 | 455 | && url.password().is_none() |
| 442 | 456 | }) |
| 443 | 457 | .map(String::from); |
| 444 | | Ok((string(&profile["sub"]).to_owned(), name, picture)) |
| 458 | let public_profile = profile["profile"].as_str().and_then(astheno_profile); |
| 459 | Ok(( |
| 460 | string(&profile["sub"]).to_owned(), |
| 461 | name, |
| 462 | picture, |
| 463 | public_profile, |
| 464 | )) |
| 445 | 465 | } |
| 446 | 466 | |
| 447 | 467 | fn account( |
| ... | ... | @@ -450,7 +470,12 @@ fn account( |
| 450 | 470 | subject: &str, |
| 451 | 471 | name: &str, |
| 452 | 472 | picture: Option<&str>, |
| 473 | public_profile: Option<&str>, |
| 453 | 474 | ) -> Result<String> { |
| 475 | let mut attributes = json!({"picture":picture.map(|picture| vec![picture])}); |
| 476 | if let Some(profile) = public_profile { |
| 477 | attributes["profile"] = json!([profile]); |
| 478 | } |
| 454 | 479 | let mut db = auth.db.lock().unwrap(); |
| 455 | 480 | let tx = db.transaction()?; |
| 456 | 481 | let existing: Option<String> = tx |
| ... | ... | @@ -470,7 +495,10 @@ fn account( |
| 470 | 495 | } |
| 471 | 496 | tx.execute( |
| 472 | 497 | "UPDATE users SET profile=json_patch(profile,?) WHERE id=?", |
| 473 | | sql![json!({"firstName":name,"attributes":{"picture":picture.map(|picture| vec![picture])}}).to_string(), id], |
| 498 | sql![ |
| 499 | json!({"firstName":name,"attributes":attributes}).to_string(), |
| 500 | id |
| 501 | ], |
| 474 | 502 | )?; |
| 475 | 503 | tx.commit()?; |
| 476 | 504 | return Ok(id); |
| ... | ... | @@ -481,10 +509,11 @@ fn account( |
| 481 | 509 | } else { |
| 482 | 510 | mcp::hash(subject)[..24].to_owned() |
| 483 | 511 | }; |
| 484 | | let mut profile = json!({"kind":"guest","guestProvider":provider,"username":format!("guest-{provider}-{suffix}"),"enabled":true,"email":null,"emailVerified":false,"firstName":name,"lastName":null,"requiredActions":[],"attributes":{},"createdTimestamp":(now()*1000.0) as i64}); |
| 485 | | if let Some(picture) = picture { |
| 486 | | profile["attributes"]["picture"] = json!([picture]); |
| 512 | let mut profile = json!({"kind":"guest","guestProvider":provider,"username":format!("guest-{provider}-{suffix}"),"enabled":true,"email":null,"emailVerified":false,"firstName":name,"lastName":null,"requiredActions":[],"createdTimestamp":(now()*1000.0) as i64}); |
| 513 | if picture.is_none() { |
| 514 | attributes.as_object_mut().unwrap().remove("picture"); |
| 487 | 515 | } |
| 516 | profile["attributes"] = attributes; |
| 488 | 517 | tx.execute( |
| 489 | 518 | "INSERT INTO users(id,profile) VALUES (?,?)", |
| 490 | 519 | sql![id, profile.to_string()], |
| ... | ... | @@ -623,9 +652,16 @@ async fn handle(app: &App, request: Request) -> Result<Response> { |
| 623 | 652 | headers |
| 624 | 653 | }) |
| 625 | 654 | .build()?; |
| 626 | | let (subject, name, picture) = |
| 655 | let (subject, name, picture, public_profile) = |
| 627 | 656 | exchange(&http, provider, &client, &secret, code, &callback, &flow).await?; |
| 628 | | let id = account(auth, provider, &subject, &name, picture.as_deref())?; |
| 657 | let id = account( |
| 658 | auth, |
| 659 | provider, |
| 660 | &subject, |
| 661 | &name, |
| 662 | picture.as_deref(), |
| 663 | public_profile.as_deref(), |
| 664 | )?; |
| 629 | 665 | auth.create_session(&id, "dashboard", headers, None) |
| 630 | 666 | } |
| 631 | 667 | .await; |
| ... | ... | @@ -790,6 +826,22 @@ mod tests { |
| 790 | 826 | } |
| 791 | 827 | } |
| 792 | 828 | |
| 829 | #[test] |
| 830 | fn public_astheno_profiles_stay_on_the_identity_origin() { |
| 831 | let profile = "https://identity.astheno.software/user/00653PKPFWTHWVX7K6NZ06ZW79"; |
| 832 | assert_eq!(astheno_profile(profile).as_deref(), Some(profile)); |
| 833 | for value in [ |
| 834 | "http://identity.astheno.software/user/id", |
| 835 | "https://other.test/user/id", |
| 836 | "https://identity.astheno.software/user/", |
| 837 | "https://identity.astheno.software/user/id/extra", |
| 838 | "https://identity.astheno.software/user/id?query=yes", |
| 839 | "https://user@identity.astheno.software/user/id", |
| 840 | ] { |
| 841 | assert!(astheno_profile(value).is_none()); |
| 842 | } |
| 843 | } |
| 844 | |
| 793 | 845 | #[test] |
| 794 | 846 | fn identities_never_link_by_name_or_email_and_cannot_gain_credentials_or_groups() { |
| 795 | 847 | let path = std::env::temp_dir().join(format!("guest-test-{}", uuid::Uuid::new_v4())); |
| ... | ... | @@ -803,17 +855,18 @@ mod tests { |
| 803 | 855 | { |
| 804 | 856 | let db = auth.db.lock().unwrap(); |
| 805 | 857 | db.execute("INSERT INTO users(id,profile) VALUES ('owner',?)", [json!({"username":"clover","enabled":true,"email":"same@example.invalid","emailVerified":true}).to_string()]).unwrap(); |
| 806 | | db.execute("INSERT INTO roles VALUES ('admin','infra-admin')", []) |
| 858 | db.execute("INSERT INTO roles VALUES ('admin','infra-admin') ON CONFLICT(name) DO UPDATE SET id=excluded.id", []) |
| 807 | 859 | .unwrap(); |
| 808 | 860 | } |
| 809 | | let first = account(&auth, "github", "123", "clover", None).unwrap(); |
| 810 | | let repeat = account(&auth, "github", "123", "renamed", None).unwrap(); |
| 861 | let first = account(&auth, "github", "123", "clover", None, None).unwrap(); |
| 862 | let repeat = account(&auth, "github", "123", "renamed", None, None).unwrap(); |
| 811 | 863 | let other = account( |
| 812 | 864 | &auth, |
| 813 | 865 | "astheno", |
| 814 | 866 | "123", |
| 815 | 867 | "clover", |
| 816 | 868 | Some("https://identity.astheno.software/avatar/123"), |
| 869 | Some("https://identity.astheno.software/user/public-id"), |
| 817 | 870 | ) |
| 818 | 871 | .unwrap(); |
| 819 | 872 | assert_eq!(first, repeat); |
| ... | ... | @@ -823,7 +876,11 @@ mod tests { |
| 823 | 876 | auth::user(&auth.db.lock().unwrap(), &other).unwrap()["attributes"]["picture"][0], |
| 824 | 877 | "https://identity.astheno.software/avatar/123" |
| 825 | 878 | ); |
| 826 | | account(&auth, "astheno", "123", "clover", None).unwrap(); |
| 879 | account(&auth, "astheno", "123", "clover", None, None).unwrap(); |
| 880 | assert_eq!( |
| 881 | auth::user(&auth.db.lock().unwrap(), &other).unwrap()["attributes"]["profile"][0], |
| 882 | "https://identity.astheno.software/user/public-id" |
| 883 | ); |
| 827 | 884 | assert!( |
| 828 | 885 | auth::user(&auth.db.lock().unwrap(), &other).unwrap()["attributes"] |
| 829 | 886 | .get("picture") |
| ... | ... | @@ -854,7 +911,7 @@ mod tests { |
| 854 | 911 | ) |
| 855 | 912 | .unwrap(); |
| 856 | 913 | } |
| 857 | | assert!(account(&auth, "github", "123", "clover", None).is_err()); |
| 914 | assert!(account(&auth, "github", "123", "clover", None, None).is_err()); |
| 858 | 915 | assert!( |
| 859 | 916 | auth.create_session(&other, "file", &HeaderMap::new(), None) |
| 860 | 917 | .is_err() |