From 4f4eb500654b589a6cb4cbc8a4ba90b99f964e16 Mon Sep 17 00:00:00 2001 From: div-seungha Date: Tue, 17 Dec 2024 14:42:18 +0900 Subject: [PATCH] Add `theme` field in `Account` to represent user's screen color theme --- CHANGELOG.md | 5 + Cargo.toml | 2 +- src/account.rs | 7 +- src/migration.rs | 280 ++++++++++++++++++++++++++++++++++++++--- src/tables/accounts.rs | 11 ++ 5 files changed, 283 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8383618..1198c4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ Versioning](https://semver.org/spec/v2.0.0.html). ## [0.33.0] - 2024-12-12 +### Added + +- Added `Account::theme` field to represent user's selected screen color theme + on the user interface. + ### Changed - Modified `Table::get_patterns` to take a reference to a slice of tuples diff --git a/Cargo.toml b/Cargo.toml index 17afa9f..708fe90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "review-database" -version = "0.33.0" +version = "0.33.1-alpha" edition = "2021" [dependencies] diff --git a/src/account.rs b/src/account.rs index f356fac..f43e3dc 100644 --- a/src/account.rs +++ b/src/account.rs @@ -36,6 +36,7 @@ pub struct Account { pub name: String, pub department: String, pub language: Option, + pub theme: Option, pub(crate) creation_time: DateTime, pub(crate) last_signin_time: Option>, pub allow_access_from: Option>, @@ -60,6 +61,7 @@ impl Account { name: String, department: String, language: Option, + theme: Option, allow_access_from: Option>, max_parallel_sessions: Option, ) -> Result { @@ -72,6 +74,7 @@ impl Account { role, name, department, + theme, language, creation_time: now, last_signin_time: None, @@ -170,7 +173,7 @@ impl SaltedPassword { /// # Errors /// /// Returns an error if the salt cannot be generated. - fn new_with_hash_algorithm( + pub(crate) fn new_with_hash_algorithm( password: &str, hash_algorithm: &PasswordHashAlgorithm, ) -> Result { @@ -292,6 +295,7 @@ mod tests { None, None, None, + None, ); assert!(account.is_ok()); @@ -319,6 +323,7 @@ mod tests { department: String::new(), name: String::new(), language: None, + theme: None, creation_time: Utc::now(), last_signin_time: None, allow_access_from: None, diff --git a/src/migration.rs b/src/migration.rs index d0ad939..e38e76c 100644 --- a/src/migration.rs +++ b/src/migration.rs @@ -136,6 +136,11 @@ pub fn migrate_data_dir>(data_dir: P, backup_dir: P) -> Result<() Version::parse("0.30.0")?, migrate_0_29_to_0_30_0, ), + ( + VersionReq::parse(">=0.33.0,<0.34.0")?, + Version::parse("0.34.0")?, + migrate_0_33_to_0_34_0, + ), ]; let mut store = super::Store::new(data_dir, backup_dir)?; @@ -211,6 +216,67 @@ fn read_version_file(path: &Path) -> Result { Version::parse(&ver).context("cannot parse VERSION") } +fn migrate_0_33_to_0_34_0(store: &super::Store) -> Result<()> { + migrate_0_34_account(store) +} + +fn migrate_0_34_account(store: &super::Store) -> Result<()> { + use bincode::Options; + use chrono::{DateTime, Utc}; + + use crate::account::{PasswordHashAlgorithm, Role, SaltedPassword}; + use crate::types::Account; + + #[derive(Deserialize, Serialize)] + pub struct OldAccount { + pub username: String, + password: SaltedPassword, + pub role: Role, + pub name: String, + pub department: String, + pub language: Option, + creation_time: DateTime, + last_signin_time: Option>, + pub allow_access_from: Option>, + pub max_parallel_sessions: Option, + password_hash_algorithm: PasswordHashAlgorithm, + password_last_modified_at: DateTime, + } + + impl From for Account { + fn from(input: OldAccount) -> Self { + Self { + username: input.username, + password: input.password, + role: input.role, + name: input.name, + department: input.department, + language: input.language, + theme: None, + creation_time: input.creation_time, + last_signin_time: input.last_signin_time, + allow_access_from: input.allow_access_from, + max_parallel_sessions: input.max_parallel_sessions, + password_hash_algorithm: input.password_hash_algorithm, + password_last_modified_at: input.password_last_modified_at, + } + } + } + + let map = store.account_map(); + let raw = map.raw(); + let mut accounts = vec![]; + for (key, old_value) in raw.iter_forward()? { + let old = bincode::DefaultOptions::new().deserialize::(&old_value)?; + raw.delete(&key)?; + accounts.push(old.into()); + } + for account in accounts { + map.insert(&account)?; + } + Ok(()) +} + fn migrate_0_29_to_0_30_0(store: &super::Store) -> Result<()> { migrate_0_30_tidb(store)?; migrate_0_30_event_struct(store) @@ -1016,7 +1082,6 @@ fn migrate_0_29_account(store: &super::Store) -> Result<()> { use chrono::{DateTime, Utc}; use crate::account::{PasswordHashAlgorithm, Role, SaltedPassword}; - use crate::types::Account; #[derive(Deserialize, Serialize)] pub struct OldAccount { @@ -1032,7 +1097,23 @@ fn migrate_0_29_account(store: &super::Store) -> Result<()> { password_hash_algorithm: PasswordHashAlgorithm, } - impl From for Account { + #[derive(Deserialize, Serialize)] + pub struct AccountV29 { + pub username: String, + password: SaltedPassword, + pub role: Role, + pub name: String, + pub department: String, + language: Option, + creation_time: DateTime, + last_signin_time: Option>, + pub allow_access_from: Option>, + pub max_parallel_sessions: Option, + password_hash_algorithm: PasswordHashAlgorithm, + password_last_modified_at: DateTime, + } + + impl From for AccountV29 { fn from(input: OldAccount) -> Self { Self { username: input.username, @@ -1053,14 +1134,11 @@ fn migrate_0_29_account(store: &super::Store) -> Result<()> { let map = store.account_map(); let raw = map.raw(); - let mut accounts = vec![]; for (key, old_value) in raw.iter_forward()? { let old = bincode::DefaultOptions::new().deserialize::(&old_value)?; - raw.delete(&key)?; - accounts.push(old.into()); - } - for account in accounts { - map.insert(&account)?; + let new: AccountV29 = old.into(); + let new_value = bincode::DefaultOptions::new().serialize::(&new)?; + raw.update((&key, &old_value), (&key, &new_value))?; } Ok(()) } @@ -3441,12 +3519,12 @@ mod tests { fn migrate_0_26_to_0_29_account() { use std::net::IpAddr; + use anyhow::Result; use bincode::Options; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::account::{PasswordHashAlgorithm, Role, SaltedPassword}; - use crate::types::Account; #[derive(Deserialize, Serialize)] pub struct OldAccount { @@ -3462,7 +3540,58 @@ mod tests { password_hash_algorithm: PasswordHashAlgorithm, } - impl From for Account { + #[derive(Clone, Deserialize, Serialize, PartialEq, Debug)] + pub struct AccountV29 { + pub username: String, + password: SaltedPassword, + pub role: Role, + pub name: String, + pub department: String, + language: Option, + creation_time: DateTime, + last_signin_time: Option>, + pub allow_access_from: Option>, + pub max_parallel_sessions: Option, + password_hash_algorithm: PasswordHashAlgorithm, + password_last_modified_at: DateTime, + } + + impl AccountV29 { + const DEFAULT_HASH_ALGORITHM: PasswordHashAlgorithm = PasswordHashAlgorithm::Argon2id; + + fn new( + username: &str, + password: &str, + role: Role, + name: String, + department: String, + language: Option, + allow_access_from: Option>, + max_parallel_sessions: Option, + ) -> Result { + let password = SaltedPassword::new_with_hash_algorithm( + password, + &Self::DEFAULT_HASH_ALGORITHM, + )?; + let now = Utc::now(); + Ok(Self { + username: username.to_string(), + password, + role, + name, + department, + language, + creation_time: now, + last_signin_time: None, + allow_access_from, + max_parallel_sessions, + password_hash_algorithm: Self::DEFAULT_HASH_ALGORITHM, + password_last_modified_at: now, + }) + } + } + + impl From for AccountV29 { fn from(input: OldAccount) -> Self { Self { username: input.username, @@ -3481,8 +3610,8 @@ mod tests { } } - impl From for OldAccount { - fn from(input: Account) -> Self { + impl From for OldAccount { + fn from(input: AccountV29) -> Self { Self { username: input.username, password: input.password, @@ -3502,7 +3631,7 @@ mod tests { let map = settings.store.account_map(); let raw = map.raw(); - let mut test = Account::new( + let mut test = AccountV29::new( "test", "password", Role::SecurityAdministrator, @@ -3526,13 +3655,21 @@ mod tests { assert!(super::migrate_0_29_account(&settings.store).is_ok()); let map = settings.store.account_map(); - let res = map.get(&test.username); - assert!(res.is_ok()); - let account = res.unwrap(); - if let Some(a) = &account { - test.password_last_modified_at = a.password_last_modified_at; - } - assert_eq!(account, Some(test)); + let raw = map.raw(); + let raw_value = raw + .get(test.username.as_bytes()) + .expect("Failed to get raw value from database"); + assert!(raw_value.is_some()); + + let raw_owned = raw_value.unwrap(); + let raw_bytes = raw_owned.as_ref(); + + let account: AccountV29 = bincode::DefaultOptions::new() + .deserialize(raw_bytes) + .expect("Failed to deserialize into AccountV29"); + + test.password_last_modified_at = account.password_last_modified_at; + assert_eq!(account, test); } #[test] @@ -3614,4 +3751,107 @@ mod tests { assert_eq!(rule.category, EventCategory::Reconnaissance); }); } + + #[test] + fn migrate_0_34_account() { + use std::net::IpAddr; + + use bincode::Options; + use chrono::{DateTime, Utc}; + use serde::{Deserialize, Serialize}; + + use crate::account::{PasswordHashAlgorithm, Role, SaltedPassword}; + use crate::types::Account; + + #[derive(Deserialize, Serialize)] + pub struct OldAccount { + pub username: String, + password: SaltedPassword, + pub role: Role, + pub name: String, + pub department: String, + language: Option, + creation_time: DateTime, + last_signin_time: Option>, + pub allow_access_from: Option>, + pub max_parallel_sessions: Option, + password_hash_algorithm: PasswordHashAlgorithm, + password_last_modified_at: DateTime, + } + + impl From for Account { + fn from(input: OldAccount) -> Self { + Self { + username: input.username, + password: input.password, + role: input.role, + name: input.name, + department: input.department, + language: input.language, + theme: None, + creation_time: input.creation_time, + last_signin_time: input.last_signin_time, + allow_access_from: input.allow_access_from, + max_parallel_sessions: input.max_parallel_sessions, + password_hash_algorithm: input.password_hash_algorithm, + password_last_modified_at: input.password_last_modified_at, + } + } + } + + impl From for OldAccount { + fn from(input: Account) -> Self { + Self { + username: input.username, + password: input.password, + role: input.role, + name: input.name, + department: input.department, + language: input.language, + creation_time: input.creation_time, + last_signin_time: input.last_signin_time, + allow_access_from: input.allow_access_from, + max_parallel_sessions: input.max_parallel_sessions, + password_hash_algorithm: input.password_hash_algorithm, + password_last_modified_at: input.password_last_modified_at, + } + } + } + + let settings = TestSchema::new(); + let map = settings.store.account_map(); + let raw = map.raw(); + + let test = Account::new( + "test", + "password", + Role::SecurityAdministrator, + "name".to_string(), + "department".to_string(), + None, + None, + None, + None, + ) + .unwrap(); + + let old: OldAccount = test.clone().into(); + let value = bincode::DefaultOptions::new() + .serialize(&old) + .expect("serializable"); + + assert!(raw.put(old.username.as_bytes(), &value).is_ok()); + + let (db_dir, backup_dir) = settings.close(); + let settings = TestSchema::new_with_dir(db_dir, backup_dir); + + assert!(super::migrate_0_34_account(&settings.store).is_ok()); + + let map = settings.store.account_map(); + let res = map.get(&test.username); + assert!(res.is_ok()); + let account = res.unwrap(); + + assert_eq!(account, Some(test)); + } } diff --git a/src/tables/accounts.rs b/src/tables/accounts.rs index da4f893..eec4dc6 100644 --- a/src/tables/accounts.rs +++ b/src/tables/accounts.rs @@ -77,6 +77,7 @@ impl<'d> Table<'d, Account> { name: &Option<(String, String)>, department: &Option<(String, String)>, language: &Option<(Option, Option)>, + theme: &Option<(Option, Option)>, allow_access_from: &Option<(Option>, Option>)>, max_parallel_sessions: &Option<(Option, Option)>, ) -> Result<(), anyhow::Error> { @@ -117,6 +118,12 @@ impl<'d> Table<'d, Account> { } account.language.clone_from(new); } + if let Some((old, new)) = theme { + if account.theme != *old { + bail!("old value mismatch"); + } + account.theme.clone_from(new); + } if let Some((old, new)) = &allow_access_from { if account.allow_access_from != *old { bail!("old value mismatch"); @@ -177,6 +184,7 @@ mod tests { None, None, None, + None, ) .unwrap(); table.put(&acc1).unwrap(); @@ -191,6 +199,7 @@ mod tests { None, None, None, + None, ) .unwrap(); table.put(&acc2).unwrap(); @@ -221,6 +230,7 @@ mod tests { None, None, None, + None, ) .unwrap(); table.put(&acc1).unwrap(); @@ -239,6 +249,7 @@ mod tests { None, None, None, + None, ) .unwrap(); table.put(&acc2).unwrap();