Skip to content

Commit 92483f5

Browse files
committed
Introduce a user_store instead of accessing config.users directly - marks the end of wiring the SQL users
1 parent 523ea52 commit 92483f5

10 files changed

Lines changed: 94 additions & 71 deletions

File tree

src/app_build.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ pub async fn axum_build(
5050
let external_url = config_ref.external_url.clone();
5151
let key = oidc::init(&db).await;
5252
let webauthn = webauthn::init(&title, &external_url).unwrap();
53+
let user_store = config_ref
54+
.get_user_store()
55+
.expect("Could not construct a valid user store");
5356
drop(config_ref);
5457

5558
let state = AppState {
@@ -58,6 +61,7 @@ pub async fn axum_build(
5861
link_senders,
5962
key,
6063
webauthn,
64+
user_store,
6165
};
6266

6367
// TODO: Static files

src/config.rs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use crate::CONFIG;
2121
use crate::database::{ConfigKVRow, Database};
2222
use crate::service::Services;
2323
use crate::user::User;
24+
use crate::user_store::{SQLUserStore, StaticUserStore, UserStoreKind};
25+
// use crate::user_store::{SQLUserStore, StaticUserStore, UserStoreKind};
2426

2527
/// The actual, deserialized config data
2628
///
@@ -79,10 +81,12 @@ pub struct Config {
7981
pub webauthn_enable: bool,
8082

8183
// pub force_https_redirects: bool,
82-
pub users: Vec<User>,
84+
// Private to avoid reading from the field instead of the user store
85+
users: Vec<User>,
8386
/// Path to a file containing the user definitions
8487
pub users_file: Option<String>,
85-
pub users_sql_query: Option<String>,
88+
pub users_sql_query_all: Option<String>,
89+
pub users_sql_query_email: Option<String>,
8690
pub users_sql_url: Option<String>,
8791
pub services: Services,
8892
}
@@ -136,7 +140,8 @@ impl Default for Config {
136140

137141
users: vec![],
138142
users_file: None,
139-
users_sql_query: None,
143+
users_sql_query_all: None,
144+
users_sql_query_email: None,
140145
users_sql_url: None,
141146

142147
services: Services(vec![]),
@@ -247,6 +252,43 @@ impl Config {
247252
.collect::<String>()
248253
.replace("\n", ""))
249254
}
255+
256+
pub fn get_user_store(&self) -> anyhow::Result<UserStoreKind> {
257+
if self.users.len() > 0 {
258+
return Ok(UserStoreKind::Static(StaticUserStore::new(
259+
self.users.clone(),
260+
)));
261+
}
262+
263+
if self.users_file.is_some() {
264+
// TODO: While this is correct since during reload we shove the users to the store, it's not right
265+
return Ok(UserStoreKind::Static(StaticUserStore::new(
266+
self.users.clone(),
267+
)));
268+
}
269+
270+
if let Some(url) = self.users_sql_url.clone() {
271+
let Some(query_all) = self.users_sql_query_all.clone() else {
272+
return Err(anyhow::anyhow!(
273+
"users_sql_query_all is required when users_sql_url is set"
274+
));
275+
};
276+
let Some(query_email) = self.users_sql_query_email.clone() else {
277+
return Err(anyhow::anyhow!(
278+
"users_sql_query_email is required when users_sql_url is set"
279+
));
280+
};
281+
282+
return Ok(UserStoreKind::SQL(SQLUserStore::new(
283+
&url,
284+
query_all,
285+
query_email,
286+
)?));
287+
}
288+
289+
error!("No users configured, using an empty user store");
290+
Ok(UserStoreKind::Static(StaticUserStore::new(vec![])))
291+
}
250292
}
251293

252294
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]

src/handle_login_post.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ use crate::error::AppError;
1818
use crate::pages::{LoginActionPage, Page};
1919
use crate::secret::LoginLinkSecret;
2020
use crate::secret::login_link::LoginLinkRedirect;
21-
use crate::user::User;
21+
22+
use crate::user_store::UserStore as _;
2223

2324
/// Used to get the login form data for from the login page
2425
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
@@ -29,14 +30,14 @@ pub struct LoginInfo {
2930
#[axum::debug_handler]
3031
pub async fn handle_login_post(
3132
config: LiveConfig,
32-
State(state): State<AppState>,
33+
State(mut state): State<AppState>,
3334
Query(login_redirect): Query<LoginLinkRedirect>,
3435
Form(form): Form<LoginInfo>,
3536
) -> Result<Response, AppError> {
3637
let login_action_page = LoginActionPage.render().await;
3738

3839
// Return 200 to avoid leaking valid emails
39-
let Some(user) = User::from_email(&config, &form.email) else {
40+
let Some(user) = state.user_store.from_email(&form.email).await else {
4041
return Ok(login_action_page.into_response());
4142
};
4243

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ use tracing_subscriber::util::SubscriberInitExt;
5858
use tracing_subscriber::{EnvFilter, fmt};
5959

6060
use crate::error::AppError;
61+
use crate::user_store::UserStoreKind;
6162
use crate::{config::Config, user::User};
6263

6364
pub mod app_build;
@@ -142,6 +143,7 @@ pub struct InFlightConfig(Arc<Config>);
142143
pub struct AppState {
143144
pub db: crate::Database,
144145
config: Arc<ArcSwap<Config>>,
146+
pub user_store: UserStoreKind,
145147
pub link_senders: Vec<Arc<dyn LinkSender>>,
146148

147149
pub key: jsonwebtoken::EncodingKey,

src/tests/config.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ fn get_saml_key_strips_pem_headers() {
99
let path = std::env::temp_dir().join(format!("testkey-{}.pem", Uuid::new_v4()));
1010
std::fs::write(&path, pem).expect("write pem");
1111

12-
let config = Config {
13-
saml_key_pem_path: path.to_string_lossy().into_owned(),
14-
..Default::default()
15-
};
12+
let mut config = Config::default();
13+
config.saml_key_pem_path = path.to_string_lossy().into_owned();
1614

1715
let key = config.get_saml_key().expect("read key");
1816
assert_eq!(key, "ABCDEF");

src/tests/render_pages.rs

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,53 +11,13 @@ use crate::config::Config;
1111
use crate::error::AppError;
1212
use crate::pages::*;
1313

14-
/// Mock configuration for testing
15-
fn create_mock_config() -> Config {
16-
Config {
17-
database_url: "sqlite::memory:".to_string(),
18-
listen_host: "127.0.0.1".to_string(),
19-
listen_port: 8080,
20-
path_prefix: "/demo".to_string(),
21-
external_url: "http://localhost:8080".to_string(),
22-
link_duration: chrono::Duration::try_hours(12).unwrap(),
23-
session_duration: chrono::Duration::try_days(30).unwrap(),
24-
secrets_cleanup_interval: chrono::Duration::try_hours(24).unwrap(),
25-
title: "MagicEntry Demo".to_string(),
26-
static_path: "static".to_string(),
27-
auth_url_enable: true,
28-
auth_url_user_header: "X-Remote-User".to_string(),
29-
auth_url_email_header: "X-Remote-Email".to_string(),
30-
auth_url_name_header: "X-Remote-Name".to_string(),
31-
auth_url_realms_header: "X-Remote-Realms".to_string(),
32-
oidc_code_duration: chrono::Duration::try_minutes(1).unwrap(),
33-
saml_cert_pem_path: "saml_cert.pem".to_string(),
34-
saml_key_pem_path: "saml_key.pem".to_string(),
35-
smtp_enable: false,
36-
smtp_url: "smtp://localhost:25".to_string(),
37-
smtp_from: "{title} <magicentry@example.com>".to_string(),
38-
smtp_subject: "{title} Login".to_string(),
39-
smtp_body: "Click the link to login: {magic_link}".to_string(),
40-
request_enable: false,
41-
request_url: "https://www.cinotify.cc/api/notify".to_string(),
42-
request_method: "POST".to_string(),
43-
request_data: Some("to={email}&subject={title} Login&body=Click the link to login: <a href=\"{magic_link}\">Login</a>&type=text/html".to_string()),
44-
request_content_type: "application/x-www-form-urlencoded".to_string(),
45-
webauthn_enable: true,
46-
users_file: None,
47-
users_sql_query: None,
48-
users_sql_url: None,
49-
users: vec![],
50-
services: crate::service::Services(vec![]),
51-
}
52-
}
53-
5414
/// Helper function to render pages with mock config
5515
fn render_with_mock_config<P: Page>(page: &P, filename: &str) -> Result<(), AppError> {
5616
// For this example, we'll simulate the global CONFIG with a local Arc<RwLock>
5717
// In a real application, the global CONFIG would be properly initialized
5818

5919
// Create a mock config and use it directly with render_partial
60-
let mock_config = create_mock_config();
20+
let mock_config = Config::default();
6121

6222
// Manually implement the render logic using the mock config
6323
let content = page.render_partial();

src/user.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use serde::{Deserialize, Serialize};
22
use uuid::Uuid;
33

4-
use crate::config::LiveConfig;
5-
64
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
75
pub struct User {
86
pub username: String,
@@ -12,11 +10,6 @@ pub struct User {
1210
}
1311

1412
impl User {
15-
pub fn from_email(config: &LiveConfig, email: &str) -> Option<Self> {
16-
config.users.iter().find(|u| u.email == email).cloned()
17-
}
18-
19-
#[must_use]
2013
pub fn has_any_realm(&self, realms: &[String]) -> bool {
2114
self.realms.contains(&"all".to_string()) || self.realms.iter().any(|r| realms.contains(r))
2215
}

src/user_store.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,37 @@ use tracing::*;
55
use crate::user::User;
66

77
#[async_trait::async_trait]
8-
pub trait UserStore {
8+
pub trait UserStore: Send + Sync {
99
async fn from_email(&mut self, email: &str) -> Option<User>;
1010
}
1111

12+
#[derive(Debug, Clone)]
13+
pub enum UserStoreKind {
14+
Static(StaticUserStore),
15+
SQL(SQLUserStore),
16+
}
17+
18+
#[async_trait::async_trait]
19+
impl UserStore for UserStoreKind {
20+
async fn from_email(&mut self, email: &str) -> Option<User> {
21+
match self {
22+
UserStoreKind::Static(store) => store.from_email(email).await,
23+
UserStoreKind::SQL(store) => store.from_email(email).await,
24+
}
25+
}
26+
}
27+
1228
#[derive(Debug, Clone, Serialize, Deserialize)]
13-
pub struct ConfigUserStore(Vec<User>);
29+
pub struct StaticUserStore(Vec<User>);
1430

15-
impl ConfigUserStore {
31+
impl StaticUserStore {
1632
pub fn new(users: Vec<User>) -> Self {
17-
ConfigUserStore(users)
33+
StaticUserStore(users)
1834
}
1935
}
2036

2137
#[async_trait::async_trait]
22-
impl UserStore for ConfigUserStore {
38+
impl UserStore for StaticUserStore {
2339
async fn from_email(&mut self, email: &str) -> Option<User> {
2440
self.0.iter().find(|user| user.email == email).cloned()
2541
}
@@ -28,6 +44,8 @@ impl UserStore for ConfigUserStore {
2844
#[derive(Debug, Clone)]
2945
pub struct SQLUserStore {
3046
conn: sqlx::AnyPool,
47+
#[allow(dead_code)]
48+
// This is not used but there's no way we won't need it at some point, right?
3149
query_all: String,
3250
query_email: String,
3351
}

src/utils.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub mod tests {
1818
use crate::config::Config;
1919
use crate::database::init_database;
2020
use crate::user::User;
21+
use crate::user_store::UserStore;
2122
use crate::{CONFIG_FILE, Database};
2223

2324
use super::*;
@@ -35,11 +36,11 @@ pub mod tests {
3536

3637
let config = Config::reload_from_path(&CONFIG_FILE).await.unwrap();
3738
let user = config
38-
.users
39-
.iter()
40-
.find(|u| u.email == user_email)
39+
.get_user_store()
4140
.unwrap()
42-
.clone();
41+
.from_email(user_email)
42+
.await
43+
.unwrap();
4344

4445
assert_eq!(user.email, user_email);
4546
assert_eq!(user.realms, user_realms);

src/webauthn/handle_auth_start.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,30 @@ use axum::extract::State;
44
use axum::response::IntoResponse;
55
use axum_extra::extract::CookieJar;
66

7+
use super::store::PasskeyStore;
78
use crate::AppState;
89
use crate::config::LiveConfig;
910
use crate::error::{AppError, WebAuthnError};
1011
use crate::handle_login_post::LoginInfo;
1112
use crate::secret::WebAuthnAuthSecret;
12-
use crate::user::User;
1313

14-
use super::store::PasskeyStore;
14+
use crate::user_store::UserStore as _;
1515

1616
#[axum::debug_handler]
1717
pub async fn handle_auth_start(
1818
config: LiveConfig,
19-
State(state): State<AppState>,
19+
State(mut state): State<AppState>,
2020
jar: CookieJar,
2121
form: Json<LoginInfo>,
2222
) -> Result<(CookieJar, impl IntoResponse), AppError> {
2323
let webauthn = state.webauthn.clone();
2424

2525
// TODO: Handle the errors to avoid leaking (in)valid emails
26-
let user = User::from_email(&config, &form.email).ok_or(WebAuthnError::SecretNotFound)?;
26+
let user = state
27+
.user_store
28+
.from_email(&form.email)
29+
.await
30+
.ok_or(WebAuthnError::SecretNotFound)?;
2731

2832
let passkey_stores = PasskeyStore::get_by_user(&user, &state.db).await?;
2933
let passkeys = passkey_stores

0 commit comments

Comments
 (0)