mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-04-26 13:07:56 +00:00
891 lines
28 KiB
Rust
891 lines
28 KiB
Rust
// Copyright 2025, 2026 Element Creations Ltd.
|
|
// Copyright 2024, 2025 New Vector Ltd.
|
|
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
// Please see LICENSE files in the repository root for full details.
|
|
|
|
use chrono::Duration;
|
|
use mas_data_model::{Clock, clock::MockClock};
|
|
use mas_iana::jose::JsonWebSignatureAlg;
|
|
use mas_storage::{
|
|
Pagination, RepositoryAccess,
|
|
upstream_oauth2::{UpstreamOAuthProviderParams, UpstreamOAuthSessionFilter},
|
|
user::{
|
|
BrowserSessionFilter, BrowserSessionRepository, UserEmailFilter, UserEmailRepository,
|
|
UserFilter, UserPasswordRepository, UserRepository,
|
|
},
|
|
};
|
|
use oauth2_types::scope::{OPENID, Scope};
|
|
use rand::SeedableRng;
|
|
use rand_chacha::ChaChaRng;
|
|
use sqlx::PgPool;
|
|
|
|
use crate::PgRepository;
|
|
|
|
/// Test the user repository, by adding and looking up a user
|
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
|
async fn test_user_repo(pool: PgPool) {
|
|
const USERNAME: &str = "john";
|
|
|
|
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
|
|
let mut rng = ChaChaRng::seed_from_u64(42);
|
|
let clock = MockClock::default();
|
|
|
|
let all = UserFilter::new();
|
|
let admin = all.can_request_admin_only();
|
|
let non_admin = all.cannot_request_admin_only();
|
|
let active = all.active_only();
|
|
let locked = all.locked_only();
|
|
let deactivated = all.deactivated_only();
|
|
|
|
// Initially, the user shouldn't exist
|
|
assert!(!repo.user().exists(USERNAME).await.unwrap());
|
|
assert!(
|
|
repo.user()
|
|
.find_by_username(USERNAME)
|
|
.await
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
|
|
assert_eq!(repo.user().count(all).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(admin).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(non_admin).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(active).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(locked).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(deactivated).await.unwrap(), 0);
|
|
|
|
// Adding the user should work
|
|
let user = repo
|
|
.user()
|
|
.add(&mut rng, &clock, USERNAME.to_owned())
|
|
.await
|
|
.unwrap();
|
|
|
|
// And now it should exist
|
|
assert!(repo.user().exists(USERNAME).await.unwrap());
|
|
assert!(
|
|
repo.user()
|
|
.find_by_username(USERNAME)
|
|
.await
|
|
.unwrap()
|
|
.is_some()
|
|
);
|
|
assert!(repo.user().lookup(user.id).await.unwrap().is_some());
|
|
|
|
assert_eq!(repo.user().count(all).await.unwrap(), 1);
|
|
assert_eq!(repo.user().count(admin).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(non_admin).await.unwrap(), 1);
|
|
assert_eq!(repo.user().count(active).await.unwrap(), 1);
|
|
assert_eq!(repo.user().count(locked).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(deactivated).await.unwrap(), 0);
|
|
|
|
// Adding a second time should give a conflict
|
|
// It should not poison the transaction though
|
|
assert!(
|
|
repo.user()
|
|
.add(&mut rng, &clock, USERNAME.to_owned())
|
|
.await
|
|
.is_err()
|
|
);
|
|
|
|
// Try locking a user
|
|
assert!(user.is_valid());
|
|
let user = repo.user().lock(&clock, user).await.unwrap();
|
|
assert!(!user.is_valid());
|
|
|
|
assert_eq!(repo.user().count(all).await.unwrap(), 1);
|
|
assert_eq!(repo.user().count(admin).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(non_admin).await.unwrap(), 1);
|
|
assert_eq!(repo.user().count(active).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(locked).await.unwrap(), 1);
|
|
assert_eq!(repo.user().count(deactivated).await.unwrap(), 0);
|
|
|
|
// Check that the property is retrieved on lookup
|
|
let user = repo.user().lookup(user.id).await.unwrap().unwrap();
|
|
assert!(!user.is_valid());
|
|
|
|
// Locking a second time should not fail
|
|
let user = repo.user().lock(&clock, user).await.unwrap();
|
|
assert!(!user.is_valid());
|
|
|
|
// Try unlocking a user
|
|
let user = repo.user().unlock(user).await.unwrap();
|
|
assert!(user.is_valid());
|
|
|
|
// Check that the property is retrieved on lookup
|
|
let user = repo.user().lookup(user.id).await.unwrap().unwrap();
|
|
assert!(user.is_valid());
|
|
|
|
// Unlocking a second time should not fail
|
|
let user = repo.user().unlock(user).await.unwrap();
|
|
assert!(user.is_valid());
|
|
|
|
// Set the can_request_admin flag
|
|
let user = repo.user().set_can_request_admin(user, true).await.unwrap();
|
|
assert!(user.can_request_admin);
|
|
|
|
assert_eq!(repo.user().count(all).await.unwrap(), 1);
|
|
assert_eq!(repo.user().count(admin).await.unwrap(), 1);
|
|
assert_eq!(repo.user().count(non_admin).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(active).await.unwrap(), 1);
|
|
assert_eq!(repo.user().count(locked).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(deactivated).await.unwrap(), 0);
|
|
|
|
// Check that the property is retrieved on lookup
|
|
let user = repo.user().lookup(user.id).await.unwrap().unwrap();
|
|
assert!(user.can_request_admin);
|
|
|
|
// Unset the can_request_admin flag
|
|
let user = repo
|
|
.user()
|
|
.set_can_request_admin(user, false)
|
|
.await
|
|
.unwrap();
|
|
assert!(!user.can_request_admin);
|
|
|
|
// Check that the property is retrieved on lookup
|
|
let user = repo.user().lookup(user.id).await.unwrap().unwrap();
|
|
assert!(!user.can_request_admin);
|
|
|
|
assert_eq!(repo.user().count(all).await.unwrap(), 1);
|
|
assert_eq!(repo.user().count(admin).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(non_admin).await.unwrap(), 1);
|
|
assert_eq!(repo.user().count(active).await.unwrap(), 1);
|
|
assert_eq!(repo.user().count(locked).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(deactivated).await.unwrap(), 0);
|
|
|
|
// Deactivating the user should work
|
|
let user = repo.user().deactivate(&clock, user).await.unwrap();
|
|
assert!(user.deactivated_at.is_some());
|
|
|
|
// Check that the property is retrieved on lookup
|
|
let user = repo.user().lookup(user.id).await.unwrap().unwrap();
|
|
assert!(user.deactivated_at.is_some());
|
|
|
|
// Deactivating a second time should not fail
|
|
let user = repo.user().deactivate(&clock, user).await.unwrap();
|
|
assert!(user.deactivated_at.is_some());
|
|
|
|
assert_eq!(repo.user().count(all).await.unwrap(), 1);
|
|
assert_eq!(repo.user().count(admin).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(non_admin).await.unwrap(), 1);
|
|
assert_eq!(repo.user().count(active).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(locked).await.unwrap(), 0);
|
|
assert_eq!(repo.user().count(deactivated).await.unwrap(), 1);
|
|
|
|
// Test the search filter
|
|
assert_eq!(
|
|
repo.user()
|
|
.count(all.matching_search("alice"))
|
|
.await
|
|
.unwrap(),
|
|
0
|
|
);
|
|
assert_eq!(
|
|
repo.user().count(all.matching_search("JO")).await.unwrap(),
|
|
1
|
|
);
|
|
|
|
// Check the list method
|
|
let list = repo.user().list(all, Pagination::first(10)).await.unwrap();
|
|
assert_eq!(list.edges.len(), 1);
|
|
assert_eq!(list.edges[0].node.id, user.id);
|
|
|
|
let list = repo
|
|
.user()
|
|
.list(admin, Pagination::first(10))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(list.edges.len(), 0);
|
|
|
|
let list = repo
|
|
.user()
|
|
.list(non_admin, Pagination::first(10))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(list.edges.len(), 1);
|
|
assert_eq!(list.edges[0].node.id, user.id);
|
|
|
|
let list = repo
|
|
.user()
|
|
.list(active, Pagination::first(10))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(list.edges.len(), 0);
|
|
|
|
let list = repo
|
|
.user()
|
|
.list(locked, Pagination::first(10))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(list.edges.len(), 0);
|
|
|
|
let list = repo
|
|
.user()
|
|
.list(deactivated, Pagination::first(10))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(list.edges.len(), 1);
|
|
assert_eq!(list.edges[0].node.id, user.id);
|
|
|
|
repo.save().await.unwrap();
|
|
}
|
|
|
|
/// Test [`UserRepository::find_by_username`] with different casings.
|
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
|
async fn test_user_repo_find_by_username(pool: PgPool) {
|
|
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
|
|
let mut rng = ChaChaRng::seed_from_u64(42);
|
|
let clock = MockClock::default();
|
|
|
|
let alice = repo
|
|
.user()
|
|
.add(&mut rng, &clock, "Alice".to_owned())
|
|
.await
|
|
.unwrap();
|
|
let bob1 = repo
|
|
.user()
|
|
.add(&mut rng, &clock, "Bob".to_owned())
|
|
.await
|
|
.unwrap();
|
|
let bob2 = repo
|
|
.user()
|
|
.add(&mut rng, &clock, "BOB".to_owned())
|
|
.await
|
|
.unwrap();
|
|
|
|
// This is fine, we can do a case-insensitive search
|
|
assert_eq!(
|
|
repo.user().find_by_username("alice").await.unwrap(),
|
|
Some(alice)
|
|
);
|
|
|
|
// In case there are multiple users with the same username, we should return the
|
|
// one that matches the exact casing
|
|
assert_eq!(
|
|
repo.user().find_by_username("Bob").await.unwrap(),
|
|
Some(bob1)
|
|
);
|
|
assert_eq!(
|
|
repo.user().find_by_username("BOB").await.unwrap(),
|
|
Some(bob2)
|
|
);
|
|
|
|
// If none match, we should return None
|
|
assert!(repo.user().find_by_username("bob").await.unwrap().is_none());
|
|
}
|
|
|
|
/// Test the user email repository, by trying out most of its methods
|
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
|
async fn test_user_email_repo(pool: PgPool) {
|
|
const USERNAME: &str = "john";
|
|
const EMAIL: &str = "john@example.com";
|
|
// This is what is stored in the database, making sure that:
|
|
// 1. we don't normalize the email address when storing it
|
|
// 2. looking it up is case-incensitive
|
|
const UPPERCASE_EMAIL: &str = "JOHN@EXAMPLE.COM";
|
|
|
|
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
|
|
let mut rng = ChaChaRng::seed_from_u64(42);
|
|
let clock = MockClock::default();
|
|
|
|
let user = repo
|
|
.user()
|
|
.add(&mut rng, &clock, USERNAME.to_owned())
|
|
.await
|
|
.unwrap();
|
|
|
|
// The user email should not exist yet
|
|
assert!(
|
|
repo.user_email()
|
|
.find(&user, EMAIL)
|
|
.await
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
|
|
let all = UserEmailFilter::new().for_user(&user);
|
|
|
|
// Check the counts
|
|
assert_eq!(repo.user_email().count(all).await.unwrap(), 0);
|
|
|
|
let user_email = repo
|
|
.user_email()
|
|
.add(&mut rng, &clock, &user, UPPERCASE_EMAIL.to_owned())
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(user_email.user_id, user.id);
|
|
assert_eq!(user_email.email, UPPERCASE_EMAIL);
|
|
|
|
// Check the counts
|
|
assert_eq!(repo.user_email().count(all).await.unwrap(), 1);
|
|
|
|
assert!(
|
|
repo.user_email()
|
|
.find(&user, EMAIL)
|
|
.await
|
|
.unwrap()
|
|
.is_some()
|
|
);
|
|
|
|
let user_email = repo
|
|
.user_email()
|
|
.lookup(user_email.id)
|
|
.await
|
|
.unwrap()
|
|
.expect("user email was not found");
|
|
|
|
assert_eq!(user_email.user_id, user.id);
|
|
assert_eq!(user_email.email, UPPERCASE_EMAIL);
|
|
|
|
// Listing the user emails should work
|
|
let emails = repo
|
|
.user_email()
|
|
.list(all, Pagination::first(10))
|
|
.await
|
|
.unwrap();
|
|
assert!(!emails.has_next_page);
|
|
assert_eq!(emails.edges.len(), 1);
|
|
assert_eq!(emails.edges[0].node, user_email);
|
|
|
|
// Listing emails from the email address should work
|
|
let emails = repo
|
|
.user_email()
|
|
.list(all.for_email(EMAIL), Pagination::first(10))
|
|
.await
|
|
.unwrap();
|
|
assert!(!emails.has_next_page);
|
|
assert_eq!(emails.edges.len(), 1);
|
|
assert_eq!(emails.edges[0].node, user_email);
|
|
|
|
// Filtering on another email should not return anything
|
|
let emails = repo
|
|
.user_email()
|
|
.list(all.for_email("hello@example.com"), Pagination::first(10))
|
|
.await
|
|
.unwrap();
|
|
assert!(!emails.has_next_page);
|
|
assert!(emails.edges.is_empty());
|
|
|
|
// Counting also works with the email filter
|
|
assert_eq!(
|
|
repo.user_email().count(all.for_email(EMAIL)).await.unwrap(),
|
|
1
|
|
);
|
|
assert_eq!(
|
|
repo.user_email()
|
|
.count(all.for_email("hello@example.com"))
|
|
.await
|
|
.unwrap(),
|
|
0
|
|
);
|
|
|
|
// Deleting the user email should work
|
|
repo.user_email().remove(user_email).await.unwrap();
|
|
assert_eq!(repo.user_email().count(all).await.unwrap(), 0);
|
|
|
|
// Add a few emails
|
|
for i in 0..5 {
|
|
let email = format!("email{i}@example.com");
|
|
repo.user_email()
|
|
.add(&mut rng, &clock, &user, email)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
assert_eq!(repo.user_email().count(all).await.unwrap(), 5);
|
|
|
|
// Try removing all the emails
|
|
let affected = repo.user_email().remove_bulk(all).await.unwrap();
|
|
assert_eq!(affected, 5);
|
|
assert_eq!(repo.user_email().count(all).await.unwrap(), 0);
|
|
|
|
repo.save().await.unwrap();
|
|
}
|
|
|
|
/// Test the authentication codes methods in the user email repository
|
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
|
async fn test_user_email_repo_authentications(pool: PgPool) {
|
|
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
|
|
let mut rng = ChaChaRng::seed_from_u64(42);
|
|
let clock = MockClock::default();
|
|
|
|
// Create a user and a user session so that we can create an authentication
|
|
let user = repo
|
|
.user()
|
|
.add(&mut rng, &clock, "alice".to_owned())
|
|
.await
|
|
.unwrap();
|
|
|
|
let browser_session = repo
|
|
.browser_session()
|
|
.add(&mut rng, &clock, &user, None)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Create an authentication session
|
|
let authentication = repo
|
|
.user_email()
|
|
.add_authentication_for_session(
|
|
&mut rng,
|
|
&clock,
|
|
"alice@example.com".to_owned(),
|
|
&browser_session,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(authentication.email, "alice@example.com");
|
|
assert_eq!(authentication.user_session_id, Some(browser_session.id));
|
|
assert_eq!(authentication.created_at, clock.now());
|
|
assert_eq!(authentication.completed_at, None);
|
|
|
|
// Check that we can find the authentication by its ID
|
|
let lookup = repo
|
|
.user_email()
|
|
.lookup_authentication(authentication.id)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(lookup.id, authentication.id);
|
|
assert_eq!(lookup.email, "alice@example.com");
|
|
assert_eq!(lookup.user_session_id, Some(browser_session.id));
|
|
assert_eq!(lookup.created_at, clock.now());
|
|
assert_eq!(lookup.completed_at, None);
|
|
|
|
// Add a code to the session
|
|
let code = repo
|
|
.user_email()
|
|
.add_authentication_code(
|
|
&mut rng,
|
|
&clock,
|
|
Duration::minutes(5),
|
|
&authentication,
|
|
"123456".to_owned(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(code.code, "123456");
|
|
assert_eq!(code.created_at, clock.now());
|
|
assert_eq!(code.expires_at, clock.now() + Duration::minutes(5));
|
|
|
|
// Check that we can find the code by its ID
|
|
let id = code.id;
|
|
let lookup = repo
|
|
.user_email()
|
|
.find_authentication_code(&authentication, "123456")
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
assert_eq!(lookup.id, id);
|
|
assert_eq!(lookup.code, "123456");
|
|
assert_eq!(lookup.created_at, clock.now());
|
|
assert_eq!(lookup.expires_at, clock.now() + Duration::minutes(5));
|
|
|
|
// Complete the authentication
|
|
let authentication = repo
|
|
.user_email()
|
|
.complete_authentication_with_code(&clock, authentication, &code)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(authentication.id, authentication.id);
|
|
assert_eq!(authentication.email, "alice@example.com");
|
|
assert_eq!(authentication.user_session_id, Some(browser_session.id));
|
|
assert_eq!(authentication.created_at, clock.now());
|
|
assert_eq!(authentication.completed_at, Some(clock.now()));
|
|
|
|
// Check that we can find the completed authentication by its ID
|
|
let lookup = repo
|
|
.user_email()
|
|
.lookup_authentication(authentication.id)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(lookup.id, authentication.id);
|
|
assert_eq!(lookup.email, "alice@example.com");
|
|
assert_eq!(lookup.user_session_id, Some(browser_session.id));
|
|
assert_eq!(lookup.created_at, clock.now());
|
|
assert_eq!(lookup.completed_at, Some(clock.now()));
|
|
|
|
// Completing a second time should fail
|
|
let res = repo
|
|
.user_email()
|
|
.complete_authentication_with_code(&clock, authentication, &code)
|
|
.await;
|
|
assert!(res.is_err());
|
|
}
|
|
|
|
/// Test the user password repository implementation.
|
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
|
async fn test_user_password_repo(pool: PgPool) {
|
|
const USERNAME: &str = "john";
|
|
const FIRST_PASSWORD_HASH: &str = "doesntmatter";
|
|
const SECOND_PASSWORD_HASH: &str = "alsodoesntmatter";
|
|
|
|
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
|
|
let mut rng = ChaChaRng::seed_from_u64(42);
|
|
let clock = MockClock::default();
|
|
|
|
let user = repo
|
|
.user()
|
|
.add(&mut rng, &clock, USERNAME.to_owned())
|
|
.await
|
|
.unwrap();
|
|
|
|
// User should have no active password
|
|
assert!(repo.user_password().active(&user).await.unwrap().is_none());
|
|
|
|
// Insert a first password
|
|
let first_password = repo
|
|
.user_password()
|
|
.add(
|
|
&mut rng,
|
|
&clock,
|
|
&user,
|
|
1,
|
|
FIRST_PASSWORD_HASH.to_owned(),
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// User should now have an active password
|
|
let first_password_lookup = repo
|
|
.user_password()
|
|
.active(&user)
|
|
.await
|
|
.unwrap()
|
|
.expect("user should have an active password");
|
|
|
|
assert_eq!(first_password.id, first_password_lookup.id);
|
|
assert_eq!(first_password_lookup.hashed_password, FIRST_PASSWORD_HASH);
|
|
assert_eq!(first_password_lookup.version, 1);
|
|
assert_eq!(first_password_lookup.upgraded_from_id, None);
|
|
|
|
// Getting the last inserted password is based on the clock, so we need to
|
|
// advance it
|
|
clock.advance(Duration::microseconds(10 * 1000 * 1000));
|
|
|
|
let second_password = repo
|
|
.user_password()
|
|
.add(
|
|
&mut rng,
|
|
&clock,
|
|
&user,
|
|
2,
|
|
SECOND_PASSWORD_HASH.to_owned(),
|
|
Some(&first_password),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// User should now have an active password
|
|
let second_password_lookup = repo
|
|
.user_password()
|
|
.active(&user)
|
|
.await
|
|
.unwrap()
|
|
.expect("user should have an active password");
|
|
|
|
assert_eq!(second_password.id, second_password_lookup.id);
|
|
assert_eq!(second_password_lookup.hashed_password, SECOND_PASSWORD_HASH);
|
|
assert_eq!(second_password_lookup.version, 2);
|
|
assert_eq!(
|
|
second_password_lookup.upgraded_from_id,
|
|
Some(first_password.id)
|
|
);
|
|
|
|
repo.save().await.unwrap();
|
|
}
|
|
|
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
|
async fn test_user_session(pool: PgPool) {
|
|
let mut repo = PgRepository::from_pool(&pool).await.unwrap();
|
|
let mut rng = ChaChaRng::seed_from_u64(42);
|
|
let clock = MockClock::default();
|
|
|
|
let alice = repo
|
|
.user()
|
|
.add(&mut rng, &clock, "alice".to_owned())
|
|
.await
|
|
.unwrap();
|
|
|
|
let bob = repo
|
|
.user()
|
|
.add(&mut rng, &clock, "bob".to_owned())
|
|
.await
|
|
.unwrap();
|
|
|
|
let all = BrowserSessionFilter::default();
|
|
let active = all.active_only();
|
|
let finished = all.finished_only();
|
|
|
|
assert_eq!(repo.browser_session().count(all).await.unwrap(), 0);
|
|
assert_eq!(repo.browser_session().count(active).await.unwrap(), 0);
|
|
assert_eq!(repo.browser_session().count(finished).await.unwrap(), 0);
|
|
|
|
let session = repo
|
|
.browser_session()
|
|
.add(&mut rng, &clock, &alice, None)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(session.user.id, alice.id);
|
|
assert!(session.finished_at.is_none());
|
|
|
|
assert_eq!(repo.browser_session().count(all).await.unwrap(), 1);
|
|
assert_eq!(repo.browser_session().count(active).await.unwrap(), 1);
|
|
assert_eq!(repo.browser_session().count(finished).await.unwrap(), 0);
|
|
|
|
// The session should be in the list of active sessions
|
|
let session_list = repo
|
|
.browser_session()
|
|
.list(active, Pagination::first(10))
|
|
.await
|
|
.unwrap();
|
|
assert!(!session_list.has_next_page);
|
|
assert_eq!(session_list.edges.len(), 1);
|
|
assert_eq!(session_list.edges[0].node, session);
|
|
|
|
let session_lookup = repo
|
|
.browser_session()
|
|
.lookup(session.id)
|
|
.await
|
|
.unwrap()
|
|
.expect("user session not found");
|
|
|
|
assert_eq!(session_lookup.id, session.id);
|
|
assert_eq!(session_lookup.user.id, alice.id);
|
|
assert!(session_lookup.finished_at.is_none());
|
|
|
|
// Finish the session
|
|
repo.browser_session()
|
|
.finish(&clock, session_lookup)
|
|
.await
|
|
.unwrap();
|
|
|
|
// The active session counter should be 0, and the finished one should be 1
|
|
assert_eq!(repo.browser_session().count(all).await.unwrap(), 1);
|
|
assert_eq!(repo.browser_session().count(active).await.unwrap(), 0);
|
|
assert_eq!(repo.browser_session().count(finished).await.unwrap(), 1);
|
|
|
|
// The session should not be in the list of active sessions anymore
|
|
let session_list = repo
|
|
.browser_session()
|
|
.list(active, Pagination::first(10))
|
|
.await
|
|
.unwrap();
|
|
assert!(!session_list.has_next_page);
|
|
assert!(session_list.edges.is_empty());
|
|
|
|
// Reload the session
|
|
let session_lookup = repo
|
|
.browser_session()
|
|
.lookup(session.id)
|
|
.await
|
|
.unwrap()
|
|
.expect("user session not found");
|
|
|
|
assert_eq!(session_lookup.id, session.id);
|
|
assert_eq!(session_lookup.user.id, alice.id);
|
|
// This time the session is finished
|
|
assert!(session_lookup.finished_at.is_some());
|
|
|
|
// Create a bunch of other sessions
|
|
for _ in 0..5 {
|
|
for user in &[&alice, &bob] {
|
|
repo.browser_session()
|
|
.add(&mut rng, &clock, user, None)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
let all_alice = BrowserSessionFilter::new().for_user(&alice);
|
|
let active_alice = BrowserSessionFilter::new().for_user(&alice).active_only();
|
|
let all_bob = BrowserSessionFilter::new().for_user(&bob);
|
|
let active_bob = BrowserSessionFilter::new().for_user(&bob).active_only();
|
|
assert_eq!(repo.browser_session().count(all).await.unwrap(), 11);
|
|
assert_eq!(repo.browser_session().count(active).await.unwrap(), 10);
|
|
assert_eq!(repo.browser_session().count(finished).await.unwrap(), 1);
|
|
assert_eq!(repo.browser_session().count(all_alice).await.unwrap(), 6);
|
|
assert_eq!(repo.browser_session().count(active_alice).await.unwrap(), 5);
|
|
assert_eq!(repo.browser_session().count(all_bob).await.unwrap(), 5);
|
|
assert_eq!(repo.browser_session().count(active_bob).await.unwrap(), 5);
|
|
|
|
// Finish all the sessions for alice
|
|
let affected = repo
|
|
.browser_session()
|
|
.finish_bulk(&clock, active_alice)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(affected, 5);
|
|
assert_eq!(repo.browser_session().count(all_alice).await.unwrap(), 6);
|
|
assert_eq!(repo.browser_session().count(active_alice).await.unwrap(), 0);
|
|
assert_eq!(repo.browser_session().count(finished).await.unwrap(), 6);
|
|
|
|
// Finish all the sessions for bob
|
|
let affected = repo
|
|
.browser_session()
|
|
.finish_bulk(&clock, active_bob)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(affected, 5);
|
|
assert_eq!(repo.browser_session().count(all_bob).await.unwrap(), 5);
|
|
assert_eq!(repo.browser_session().count(active_bob).await.unwrap(), 0);
|
|
assert_eq!(repo.browser_session().count(finished).await.unwrap(), 11);
|
|
|
|
// Checking the 'authenticaated by upstream sessions' filter
|
|
// We need a provider
|
|
let provider = repo
|
|
.upstream_oauth_provider()
|
|
.add(
|
|
&mut rng,
|
|
&clock,
|
|
UpstreamOAuthProviderParams {
|
|
issuer: None,
|
|
human_name: None,
|
|
brand_name: None,
|
|
scope: Scope::from_iter([OPENID]),
|
|
token_endpoint_auth_method:
|
|
mas_data_model::UpstreamOAuthProviderTokenAuthMethod::None,
|
|
token_endpoint_signing_alg: None,
|
|
id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
|
|
fetch_userinfo: false,
|
|
userinfo_signed_response_alg: None,
|
|
client_id: "client".to_owned(),
|
|
encrypted_client_secret: None,
|
|
claims_imports: mas_data_model::UpstreamOAuthProviderClaimsImports::default(),
|
|
authorization_endpoint_override: None,
|
|
token_endpoint_override: None,
|
|
userinfo_endpoint_override: None,
|
|
jwks_uri_override: None,
|
|
discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Disabled,
|
|
pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Disabled,
|
|
response_mode: None,
|
|
additional_authorization_parameters: Vec::new(),
|
|
forward_login_hint: false,
|
|
ui_order: 0,
|
|
on_backchannel_logout:
|
|
mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Start a authorization session
|
|
let upstream_oauth_session = repo
|
|
.upstream_oauth_session()
|
|
.add(&mut rng, &clock, &provider, "state".to_owned(), None, None)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Start a browser session
|
|
let session = repo
|
|
.browser_session()
|
|
.add(&mut rng, &clock, &alice, None)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Make the session from alice authenticated by this session
|
|
repo.browser_session()
|
|
.authenticate_with_upstream(&mut rng, &clock, &session, &upstream_oauth_session)
|
|
.await
|
|
.unwrap();
|
|
|
|
// This will match all authorization sessions, which matches exactly that one
|
|
// authorization session
|
|
let upstream_oauth_session_filter = UpstreamOAuthSessionFilter::new();
|
|
let filter =
|
|
BrowserSessionFilter::new().linked_to_upstream_sessions_only(upstream_oauth_session_filter);
|
|
|
|
// Now try to look it up
|
|
let page = repo
|
|
.browser_session()
|
|
.list(filter, Pagination::first(10))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(page.edges.len(), 1);
|
|
assert_eq!(page.edges[0].node.id, session.id);
|
|
|
|
// Try counting
|
|
assert_eq!(repo.browser_session().count(filter).await.unwrap(), 1);
|
|
|
|
// Try finishing the session
|
|
let affected = repo
|
|
.browser_session()
|
|
.finish_bulk(&clock, filter)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(affected, 1);
|
|
|
|
// Lookup the session by its ID
|
|
let lookup = repo
|
|
.browser_session()
|
|
.lookup(session.id)
|
|
.await
|
|
.unwrap()
|
|
.expect("session to be found in the database");
|
|
// It should be finished
|
|
assert!(lookup.finished_at.is_some());
|
|
}
|
|
|
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
|
async fn test_user_terms(pool: PgPool) {
|
|
let mut repo = PgRepository::from_pool(&pool).await.unwrap();
|
|
let mut rng = ChaChaRng::seed_from_u64(42);
|
|
let clock = MockClock::default();
|
|
|
|
let user = repo
|
|
.user()
|
|
.add(&mut rng, &clock, "john".to_owned())
|
|
.await
|
|
.unwrap();
|
|
|
|
// Accepting the terms should work
|
|
repo.user_terms()
|
|
.accept_terms(
|
|
&mut rng,
|
|
&clock,
|
|
&user,
|
|
"https://example.com/terms".parse().unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Accepting a second time should also work
|
|
repo.user_terms()
|
|
.accept_terms(
|
|
&mut rng,
|
|
&clock,
|
|
&user,
|
|
"https://example.com/terms".parse().unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Accepting a different terms should also work
|
|
repo.user_terms()
|
|
.accept_terms(
|
|
&mut rng,
|
|
&clock,
|
|
&user,
|
|
"https://example.com/terms?v=2".parse().unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let mut conn = repo.into_inner();
|
|
|
|
// We should have two rows, as the first terms was deduped
|
|
let res: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM user_terms")
|
|
.fetch_one(&mut *conn)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res, 2);
|
|
}
|