db: Change the DB storage type to NaiveDateTime

The entire internals of the server now work using only NaiveDateTime,
since we know they are all UTC. At the fringes (LDAP, GraphQL, JWT
tokens) we convert back into UTC to make sure we have a clear API.

This allows us to be compatible with Postgres (which doesn't support
DateTime<UTC>, only NaiveDateTime).

This change is backwards compatible since in SQlite with
Sea-query/Sea-ORM, the UTC datetimes are stored without a timezone, as
simple strings. It's the same format as NaiveDateTime.

Fixes #87.
This commit is contained in:
Valentin Tolmer 2023-01-13 15:09:25 +01:00 committed by nitnelave
parent 692bbb00f1
commit e458aca3e3
15 changed files with 60 additions and 41 deletions

View File

@ -140,8 +140,14 @@ mod tests {
fn test_uuid_time() { fn test_uuid_time() {
use chrono::prelude::*; use chrono::prelude::*;
let user_id = "bob"; let user_id = "bob";
let date1 = Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap(); let date1 = Utc
let date2 = Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 12).unwrap(); .with_ymd_and_hms(2014, 7, 8, 9, 10, 11)
.unwrap()
.naive_utc();
let date2 = Utc
.with_ymd_and_hms(2014, 7, 8, 9, 10, 12)
.unwrap()
.naive_utc();
assert_ne!( assert_ne!(
Uuid::from_name_and_date(user_id, &date1), Uuid::from_name_and_date(user_id, &date1),
Uuid::from_name_and_date(user_id, &date2) Uuid::from_name_and_date(user_id, &date2)

View File

@ -1,3 +1,4 @@
use chrono::TimeZone;
use ldap3_proto::{ use ldap3_proto::{
proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry,
}; };
@ -49,7 +50,10 @@ fn get_user_attribute(
}) })
.collect(), .collect(),
"cn" | "displayname" => vec![user.display_name.clone()?.into_bytes()], "cn" | "displayname" => vec![user.display_name.clone()?.into_bytes()],
"createtimestamp" | "modifytimestamp" => vec![user.creation_date.to_rfc3339().into_bytes()], "createtimestamp" | "modifytimestamp" => vec![chrono::Utc
.from_utc_datetime(&user.creation_date)
.to_rfc3339()
.into_bytes()],
"1.1" => return None, "1.1" => return None,
// We ignore the operational attribute wildcard. // We ignore the operational attribute wildcard.
"+" => return None, "+" => return None,

View File

@ -11,7 +11,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)] #[sea_orm(primary_key, auto_increment = false)]
pub group_id: GroupId, pub group_id: GroupId,
pub display_name: String, pub display_name: String,
pub creation_date: chrono::DateTime<chrono::Utc>, pub creation_date: chrono::NaiveDateTime,
pub uuid: Uuid, pub uuid: Uuid,
} }

View File

@ -11,7 +11,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)] #[sea_orm(primary_key, auto_increment = false)]
pub refresh_token_hash: i64, pub refresh_token_hash: i64,
pub user_id: UserId, pub user_id: UserId,
pub expiry_date: chrono::DateTime<chrono::Utc>, pub expiry_date: chrono::NaiveDateTime,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@ -11,7 +11,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)] #[sea_orm(primary_key, auto_increment = false)]
pub jwt_hash: i64, pub jwt_hash: i64,
pub user_id: UserId, pub user_id: UserId,
pub expiry_date: chrono::DateTime<chrono::Utc>, pub expiry_date: chrono::NaiveDateTime,
pub blacklisted: bool, pub blacklisted: bool,
} }

View File

@ -11,7 +11,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)] #[sea_orm(primary_key, auto_increment = false)]
pub token: String, pub token: String,
pub user_id: UserId, pub user_id: UserId,
pub expiry_date: chrono::DateTime<chrono::Utc>, pub expiry_date: chrono::NaiveDateTime,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@ -18,7 +18,7 @@ pub struct Model {
pub first_name: Option<String>, pub first_name: Option<String>,
pub last_name: Option<String>, pub last_name: Option<String>,
pub avatar: Option<JpegPhoto>, pub avatar: Option<JpegPhoto>,
pub creation_date: chrono::DateTime<chrono::Utc>, pub creation_date: chrono::NaiveDateTime,
pub password_hash: Option<Vec<u8>>, pub password_hash: Option<Vec<u8>>,
pub totp_secret: Option<String>, pub totp_secret: Option<String>,
pub mfa_type: Option<String>, pub mfa_type: Option<String>,

View File

@ -116,7 +116,7 @@ impl GroupBackendHandler for SqlBackendHandler {
#[instrument(skip_all, level = "debug", ret, err)] #[instrument(skip_all, level = "debug", ret, err)]
async fn create_group(&self, group_name: &str) -> Result<GroupId> { async fn create_group(&self, group_name: &str) -> Result<GroupId> {
debug!(?group_name); debug!(?group_name);
let now = chrono::Utc::now(); let now = chrono::Utc::now().naive_utc();
let uuid = Uuid::from_name_and_date(group_name, &now); let uuid = Uuid::from_name_and_date(group_name, &now);
let new_group = model::groups::ActiveModel { let new_group = model::groups::ActiveModel {
display_name: ActiveValue::Set(group_name.to_owned()), display_name: ActiveValue::Set(group_name.to_owned()),

View File

@ -170,7 +170,7 @@ pub async fn upgrade_to_v1(pool: &DbConnection) -> std::result::Result<(), sea_o
struct ShortGroupDetails { struct ShortGroupDetails {
group_id: GroupId, group_id: GroupId,
display_name: String, display_name: String,
creation_date: chrono::DateTime<chrono::Utc>, creation_date: chrono::NaiveDateTime,
} }
for result in ShortGroupDetails::find_by_statement( for result in ShortGroupDetails::find_by_statement(
builder.build( builder.build(
@ -220,7 +220,7 @@ pub async fn upgrade_to_v1(pool: &DbConnection) -> std::result::Result<(), sea_o
#[derive(FromQueryResult)] #[derive(FromQueryResult)]
struct ShortUserDetails { struct ShortUserDetails {
user_id: UserId, user_id: UserId,
creation_date: chrono::DateTime<chrono::Utc>, creation_date: chrono::NaiveDateTime,
} }
for result in ShortUserDetails::find_by_statement( for result in ShortUserDetails::find_by_statement(
builder.build( builder.build(

View File

@ -67,7 +67,7 @@ mod tests {
#[derive(FromQueryResult, PartialEq, Eq, Debug)] #[derive(FromQueryResult, PartialEq, Eq, Debug)]
struct ShortUserDetails { struct ShortUserDetails {
display_name: String, display_name: String,
creation_date: chrono::DateTime<chrono::Utc>, creation_date: chrono::NaiveDateTime,
} }
let result = ShortUserDetails::find_by_statement(raw_statement( let result = ShortUserDetails::find_by_statement(raw_statement(
r#"SELECT display_name, creation_date FROM users WHERE user_id = "bôb""#, r#"SELECT display_name, creation_date FROM users WHERE user_id = "bôb""#,
@ -80,7 +80,7 @@ mod tests {
result, result,
ShortUserDetails { ShortUserDetails {
display_name: "Bob Bobbersön".to_owned(), display_name: "Bob Bobbersön".to_owned(),
creation_date: Utc.timestamp_opt(0, 0).unwrap() creation_date: Utc.timestamp_opt(0, 0).unwrap().naive_utc(),
} }
); );
} }

View File

@ -158,7 +158,7 @@ impl UserBackendHandler for SqlBackendHandler {
#[instrument(skip_all, level = "debug", err)] #[instrument(skip_all, level = "debug", err)]
async fn create_user(&self, request: CreateUserRequest) -> Result<()> { async fn create_user(&self, request: CreateUserRequest) -> Result<()> {
debug!(user_id = ?request.user_id); debug!(user_id = ?request.user_id);
let now = chrono::Utc::now(); let now = chrono::Utc::now().naive_utc();
let uuid = Uuid::from_name_and_date(request.user_id.as_str(), &now); let uuid = Uuid::from_name_and_date(request.user_id.as_str(), &now);
let new_user = model::users::ActiveModel { let new_user = model::users::ActiveModel {
user_id: Set(request.user_id), user_id: Set(request.user_id),

View File

@ -1,3 +1,4 @@
use chrono::{NaiveDateTime, TimeZone};
use sea_orm::{ use sea_orm::{
entity::IntoActiveValue, entity::IntoActiveValue,
sea_query::{value::ValueType, ArrayType, ColumnType, Nullable, ValueTypeErr}, sea_query::{value::ValueType, ArrayType, ColumnType, Nullable, ValueTypeErr},
@ -7,18 +8,23 @@ use serde::{Deserialize, Serialize};
pub use super::model::{GroupColumn, UserColumn}; pub use super::model::{GroupColumn, UserColumn};
pub type DateTime = chrono::DateTime<chrono::Utc>;
#[derive(PartialEq, Hash, Eq, Clone, Debug, Default, Serialize, Deserialize)] #[derive(PartialEq, Hash, Eq, Clone, Debug, Default, Serialize, Deserialize)]
#[serde(try_from = "&str")] #[serde(try_from = "&str")]
pub struct Uuid(String); pub struct Uuid(String);
impl Uuid { impl Uuid {
pub fn from_name_and_date(name: &str, creation_date: &DateTime) -> Self { pub fn from_name_and_date(name: &str, creation_date: &NaiveDateTime) -> Self {
Uuid( Uuid(
uuid::Uuid::new_v3( uuid::Uuid::new_v3(
&uuid::Uuid::NAMESPACE_X500, &uuid::Uuid::NAMESPACE_X500,
&[name.as_bytes(), creation_date.to_rfc3339().as_bytes()].concat(), &[
name.as_bytes(),
chrono::Utc
.from_utc_datetime(creation_date)
.to_rfc3339()
.as_bytes(),
]
.concat(),
) )
.to_string(), .to_string(),
) )
@ -308,15 +314,14 @@ pub struct User {
pub first_name: Option<String>, pub first_name: Option<String>,
pub last_name: Option<String>, pub last_name: Option<String>,
pub avatar: Option<JpegPhoto>, pub avatar: Option<JpegPhoto>,
pub creation_date: DateTime, pub creation_date: NaiveDateTime,
pub uuid: Uuid, pub uuid: Uuid,
} }
#[cfg(test)] #[cfg(test)]
impl Default for User { impl Default for User {
fn default() -> Self { fn default() -> Self {
use chrono::TimeZone; let epoch = chrono::Utc.timestamp_opt(0, 0).unwrap().naive_utc();
let epoch = chrono::Utc.timestamp_opt(0, 0).unwrap();
User { User {
user_id: UserId::default(), user_id: UserId::default(),
email: String::new(), email: String::new(),
@ -373,7 +378,7 @@ impl TryFromU64 for GroupId {
pub struct Group { pub struct Group {
pub id: GroupId, pub id: GroupId,
pub display_name: String, pub display_name: String,
pub creation_date: DateTime, pub creation_date: NaiveDateTime,
pub uuid: Uuid, pub uuid: Uuid,
pub users: Vec<UserId>, pub users: Vec<UserId>,
} }
@ -382,7 +387,7 @@ pub struct Group {
pub struct GroupDetails { pub struct GroupDetails {
pub group_id: GroupId, pub group_id: GroupId,
pub display_name: String, pub display_name: String,
pub creation_date: DateTime, pub creation_date: NaiveDateTime,
pub uuid: Uuid, pub uuid: Uuid,
} }

View File

@ -3,6 +3,7 @@ use crate::domain::{
ldap::utils::map_user_field, ldap::utils::map_user_field,
types::{GroupDetails, GroupId, UserColumn, UserId}, types::{GroupDetails, GroupId, UserColumn, UserId},
}; };
use chrono::TimeZone;
use juniper::{graphql_object, FieldResult, GraphQLInputObject}; use juniper::{graphql_object, FieldResult, GraphQLInputObject};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{debug, debug_span, Instrument}; use tracing::{debug, debug_span, Instrument};
@ -230,7 +231,7 @@ impl<Handler: BackendHandler + Sync> User<Handler> {
} }
fn creation_date(&self) -> chrono::DateTime<chrono::Utc> { fn creation_date(&self) -> chrono::DateTime<chrono::Utc> {
self.user.creation_date chrono::Utc.from_utc_datetime(&self.user.creation_date)
} }
fn uuid(&self) -> &str { fn uuid(&self) -> &str {
@ -275,7 +276,7 @@ impl<Handler: BackendHandler> From<DomainUserAndGroups> for User<Handler> {
pub struct Group<Handler: BackendHandler> { pub struct Group<Handler: BackendHandler> {
group_id: i32, group_id: i32,
display_name: String, display_name: String,
creation_date: chrono::DateTime<chrono::Utc>, creation_date: chrono::NaiveDateTime,
uuid: String, uuid: String,
members: Option<Vec<String>>, members: Option<Vec<String>>,
_phantom: std::marker::PhantomData<Box<Handler>>, _phantom: std::marker::PhantomData<Box<Handler>>,
@ -290,7 +291,7 @@ impl<Handler: BackendHandler + Sync> Group<Handler> {
self.display_name.clone() self.display_name.clone()
} }
fn creation_date(&self) -> chrono::DateTime<chrono::Utc> { fn creation_date(&self) -> chrono::DateTime<chrono::Utc> {
self.creation_date chrono::Utc.from_utc_datetime(&self.creation_date)
} }
fn uuid(&self) -> String { fn uuid(&self) -> String {
self.uuid.clone() self.uuid.clone()
@ -389,7 +390,7 @@ mod tests {
Ok(DomainUser { Ok(DomainUser {
user_id: UserId::new("bob"), user_id: UserId::new("bob"),
email: "bob@bobbers.on".to_string(), email: "bob@bobbers.on".to_string(),
creation_date: chrono::Utc.timestamp_millis_opt(42).unwrap(), creation_date: chrono::Utc.timestamp_millis_opt(42).unwrap().naive_utc(),
uuid: crate::uuid!("b1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), uuid: crate::uuid!("b1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
..Default::default() ..Default::default()
}) })
@ -398,7 +399,7 @@ mod tests {
groups.insert(GroupDetails { groups.insert(GroupDetails {
group_id: GroupId(3), group_id: GroupId(3),
display_name: "Bobbersons".to_string(), display_name: "Bobbersons".to_string(),
creation_date: chrono::Utc.timestamp_nanos(42), creation_date: chrono::Utc.timestamp_nanos(42).naive_utc(),
uuid: crate::uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), uuid: crate::uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
}); });
mock.expect_get_user_groups() mock.expect_get_user_groups()

View File

@ -667,7 +667,7 @@ mod tests {
set.insert(GroupDetails { set.insert(GroupDetails {
group_id: GroupId(42), group_id: GroupId(42),
display_name: group, display_name: group,
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
}); });
Ok(set) Ok(set)
@ -754,7 +754,7 @@ mod tests {
set.insert(GroupDetails { set.insert(GroupDetails {
group_id: GroupId(42), group_id: GroupId(42),
display_name: "lldap_admin".to_string(), display_name: "lldap_admin".to_string(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
}); });
Ok(set) Ok(set)
@ -841,7 +841,7 @@ mod tests {
groups: Some(vec![GroupDetails { groups: Some(vec![GroupDetails {
group_id: GroupId(42), group_id: GroupId(42),
display_name: "rockstars".to_string(), display_name: "rockstars".to_string(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
}]), }]),
}]) }])
@ -1006,7 +1006,10 @@ mod tests {
last_name: Some("Cricket".to_string()), last_name: Some("Cricket".to_string()),
avatar: Some(JpegPhoto::for_tests()), avatar: Some(JpegPhoto::for_tests()),
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
creation_date: Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap(), creation_date: Utc
.with_ymd_and_hms(2014, 7, 8, 9, 10, 11)
.unwrap()
.naive_utc(),
}, },
groups: None, groups: None,
}, },
@ -1135,14 +1138,14 @@ mod tests {
Group { Group {
id: GroupId(1), id: GroupId(1),
display_name: "group_1".to_string(), display_name: "group_1".to_string(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
users: vec![UserId::new("bob"), UserId::new("john")], users: vec![UserId::new("bob"), UserId::new("john")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
}, },
Group { Group {
id: GroupId(3), id: GroupId(3),
display_name: "BestGroup".to_string(), display_name: "BestGroup".to_string(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
users: vec![UserId::new("john")], users: vec![UserId::new("john")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
}, },
@ -1228,7 +1231,7 @@ mod tests {
Ok(vec![Group { Ok(vec![Group {
display_name: "group_1".to_string(), display_name: "group_1".to_string(),
id: GroupId(1), id: GroupId(1),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
users: vec![], users: vec![],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
}]) }])
@ -1279,7 +1282,7 @@ mod tests {
Ok(vec![Group { Ok(vec![Group {
display_name: "group_1".to_string(), display_name: "group_1".to_string(),
id: GroupId(1), id: GroupId(1),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
users: vec![], users: vec![],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
}]) }])
@ -1555,7 +1558,7 @@ mod tests {
Ok(vec![Group { Ok(vec![Group {
id: GroupId(1), id: GroupId(1),
display_name: "group_1".to_string(), display_name: "group_1".to_string(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
users: vec![UserId::new("bob"), UserId::new("john")], users: vec![UserId::new("bob"), UserId::new("john")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
}]) }])
@ -1629,7 +1632,7 @@ mod tests {
Ok(vec![Group { Ok(vec![Group {
id: GroupId(1), id: GroupId(1),
display_name: "group_1".to_string(), display_name: "group_1".to_string(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
users: vec![UserId::new("bob"), UserId::new("john")], users: vec![UserId::new("bob"), UserId::new("john")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
}]) }])
@ -1962,7 +1965,7 @@ mod tests {
groups.insert(GroupDetails { groups.insert(GroupDetails {
group_id: GroupId(0), group_id: GroupId(0),
display_name: "lldap_admin".to_string(), display_name: "lldap_admin".to_string(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
}); });
mock.expect_get_user_groups() mock.expect_get_user_groups()

View File

@ -61,7 +61,7 @@ impl TcpBackendHandler for SqlBackendHandler {
let new_token = model::jwt_refresh_storage::Model { let new_token = model::jwt_refresh_storage::Model {
refresh_token_hash: refresh_token_hash as i64, refresh_token_hash: refresh_token_hash as i64,
user_id: user.clone(), user_id: user.clone(),
expiry_date: chrono::Utc::now() + duration, expiry_date: chrono::Utc::now().naive_utc() + duration,
} }
.into_active_model(); .into_active_model();
new_token.insert(&self.sql_pool).await?; new_token.insert(&self.sql_pool).await?;
@ -131,7 +131,7 @@ impl TcpBackendHandler for SqlBackendHandler {
let new_token = model::password_reset_tokens::Model { let new_token = model::password_reset_tokens::Model {
token: token.clone(), token: token.clone(),
user_id: user.clone(), user_id: user.clone(),
expiry_date: chrono::Utc::now() + duration, expiry_date: chrono::Utc::now().naive_utc() + duration,
} }
.into_active_model(); .into_active_model();
new_token.insert(&self.sql_pool).await?; new_token.insert(&self.sql_pool).await?;