diff --git a/crates/_types/composite_identifiers/src/by_table/audit_logs/audit_log_entity.rs b/crates/_types/composite_identifiers/src/by_table/audit_logs/audit_log_entity.rs new file mode 100644 index 0000000000..16e2f3f5ab --- /dev/null +++ b/crates/_types/composite_identifiers/src/by_table/audit_logs/audit_log_entity.rs @@ -0,0 +1,43 @@ +use enums::by_table::audit_logs::audit_log_entity_type::AuditLogEntityType; +use tokens::tokens::comments::CommentToken; +use tokens::tokens::media_files::MediaFileToken; +use tokens::tokens::model_weights::ModelWeightToken; +use tokens::tokens::users::UserToken; + +// TODO(bt, 2024-01-16): A ref type with <'a> lifetime of inner data instead of ownership? +pub enum AuditLogEntity { + Comment(CommentToken), + MediaFile(MediaFileToken), + ModelWeight(ModelWeightToken), + User(UserToken), +} + +// TODO(bt, 2024-01-16): Codegen? Make traits for these? Maybe overkill. +impl AuditLogEntity { + pub fn get_entity_type(&self) -> AuditLogEntityType { + match self { + Self::Comment(_) => AuditLogEntityType::Comment, + Self::MediaFile(_) => AuditLogEntityType::MediaFile, + Self::ModelWeight(_) => AuditLogEntityType::ModelWeight, + Self::User(_) => AuditLogEntityType::User, + } + } + + pub fn get_entity_token_str(&self) -> &str { + match self { + Self::Comment(token) => token.as_str(), + Self::MediaFile(token) => token.as_str(), + Self::ModelWeight(token) => token.as_str(), + Self::User(token) => token.as_str(), + } + } + + pub fn get_composite_keys(&self) -> (AuditLogEntityType, &str) { + match self { + Self::Comment(token) => (AuditLogEntityType::Comment, token.as_str()), + Self::MediaFile(token) => (AuditLogEntityType::MediaFile, token.as_str()), + Self::ModelWeight(token) => (AuditLogEntityType::ModelWeight, token.as_str()), + Self::User(token) => (AuditLogEntityType::User, token.as_str()), + } + } +} diff --git a/crates/_types/composite_identifiers/src/by_table/audit_logs/mod.rs b/crates/_types/composite_identifiers/src/by_table/audit_logs/mod.rs new file mode 100644 index 0000000000..01181c661c --- /dev/null +++ b/crates/_types/composite_identifiers/src/by_table/audit_logs/mod.rs @@ -0,0 +1 @@ +pub mod audit_log_entity; \ No newline at end of file diff --git a/crates/_types/composite_identifiers/src/by_table/mod.rs b/crates/_types/composite_identifiers/src/by_table/mod.rs index 4bbf977cdb..390d881f49 100644 --- a/crates/_types/composite_identifiers/src/by_table/mod.rs +++ b/crates/_types/composite_identifiers/src/by_table/mod.rs @@ -1 +1,2 @@ +pub mod audit_logs; pub mod batch_generations; diff --git a/crates/_types/enums/src/by_table/audit_logs/audit_log_entity_action.rs b/crates/_types/enums/src/by_table/audit_logs/audit_log_entity_action.rs index d5b3aa3281..21a4688a16 100644 --- a/crates/_types/enums/src/by_table/audit_logs/audit_log_entity_action.rs +++ b/crates/_types/enums/src/by_table/audit_logs/audit_log_entity_action.rs @@ -17,6 +17,10 @@ pub enum AuditLogEntityAction { #[serde(rename = "edit")] Edit, + /// Edit features (eg. user feature flags) + #[serde(rename = "edit_features")] + EditFeatures, + /// Delete action #[serde(rename = "delete")] Delete, @@ -32,6 +36,7 @@ impl AuditLogEntityAction { match self { Self::Create => "create", Self::Edit => "edit", + Self::EditFeatures => "edit_features", Self::Delete => "delete", } } @@ -40,6 +45,7 @@ impl AuditLogEntityAction { match value { "create" => Ok(Self::Create), "edit" => Ok(Self::Edit), + "edit_features" => Ok(Self::EditFeatures), "delete" => Ok(Self::Delete), _ => Err(format!("invalid value: {:?}", value)), } @@ -58,6 +64,7 @@ mod tests { fn test_serialization() { assert_serialization(AuditLogEntityAction::Create, "create"); assert_serialization(AuditLogEntityAction::Edit, "edit"); + assert_serialization(AuditLogEntityAction::EditFeatures, "edit_features"); assert_serialization(AuditLogEntityAction::Delete, "delete"); } } @@ -69,6 +76,7 @@ mod tests { fn test_to_str() { assert_eq!(AuditLogEntityAction::Create.to_str(), "create"); assert_eq!(AuditLogEntityAction::Edit.to_str(), "edit"); + assert_eq!(AuditLogEntityAction::EditFeatures.to_str(), "edit_features"); assert_eq!(AuditLogEntityAction::Delete.to_str(), "delete"); } @@ -76,6 +84,7 @@ mod tests { fn test_from_str() { assert_eq!(AuditLogEntityAction::from_str("create").unwrap(), AuditLogEntityAction::Create); assert_eq!(AuditLogEntityAction::from_str("edit").unwrap(), AuditLogEntityAction::Edit); + assert_eq!(AuditLogEntityAction::from_str("edit_features").unwrap(), AuditLogEntityAction::EditFeatures); assert_eq!(AuditLogEntityAction::from_str("delete").unwrap(), AuditLogEntityAction::Delete); assert!(AuditLogEntityAction::from_str("foo").is_err()); } diff --git a/crates/_types/enums/src/by_table/audit_logs/audit_log_entity_type.rs b/crates/_types/enums/src/by_table/audit_logs/audit_log_entity_type.rs index 1caa97b10b..2c2c957dde 100644 --- a/crates/_types/enums/src/by_table/audit_logs/audit_log_entity_type.rs +++ b/crates/_types/enums/src/by_table/audit_logs/audit_log_entity_type.rs @@ -13,21 +13,17 @@ pub enum AuditLogEntityType { #[serde(rename = "comment")] Comment, - /// TTS model (architecture does not matter) - #[serde(rename = "tts_model")] - TtsModel, + /// Media file + #[serde(rename = "media_file")] + MediaFile, - /// TTS result (architecture does not matter) - #[serde(rename = "tts_result")] - TtsResult, + /// Model weight + #[serde(rename = "model_weight")] + ModelWeight, - /// W2L template - #[serde(rename = "w2l_template")] - W2lTemplate, - - /// W2L result - #[serde(rename = "w2l_result")] - W2lResult, + /// User + #[serde(rename = "user")] + User, } // TODO(bt, 2023-01-17): This desperately needs MySQL integration tests! @@ -39,20 +35,18 @@ impl AuditLogEntityType { pub fn to_str(&self) -> &'static str { match self { Self::Comment => "comment", - Self::TtsModel => "tts_model", - Self::TtsResult => "tts_result", - Self::W2lTemplate => "w2l_template", - Self::W2lResult => "w2l_result", + Self::MediaFile => "media_file", + Self::ModelWeight => "model_weight", + Self::User => "user", } } pub fn from_str(value: &str) -> Result { match value { "comment" => Ok(Self::Comment), - "tts_model" => Ok(Self::TtsModel), - "tts_result" => Ok(Self::TtsResult), - "w2l_template" => Ok(Self::W2lTemplate), - "w2l_result" => Ok(Self::W2lResult), + "media_file" => Ok(Self::MediaFile), + "model_weight" => Ok(Self::ModelWeight), + "user" => Ok(Self::User), _ => Err(format!("invalid value: {:?}", value)), } } @@ -69,10 +63,9 @@ mod tests { #[test] fn test_serialization() { assert_serialization(AuditLogEntityType::Comment, "comment"); - assert_serialization(AuditLogEntityType::TtsModel, "tts_model"); - assert_serialization(AuditLogEntityType::TtsResult, "tts_result"); - assert_serialization(AuditLogEntityType::W2lTemplate, "w2l_template"); - assert_serialization(AuditLogEntityType::W2lResult, "w2l_result"); + assert_serialization(AuditLogEntityType::MediaFile, "media_file"); + assert_serialization(AuditLogEntityType::ModelWeight, "model_weight"); + assert_serialization(AuditLogEntityType::User, "user"); } } @@ -82,19 +75,17 @@ mod tests { #[test] fn test_to_str() { assert_eq!(AuditLogEntityType::Comment.to_str(), "comment"); - assert_eq!(AuditLogEntityType::TtsModel.to_str(), "tts_model"); - assert_eq!(AuditLogEntityType::TtsResult.to_str(), "tts_result"); - assert_eq!(AuditLogEntityType::W2lTemplate.to_str(), "w2l_template"); - assert_eq!(AuditLogEntityType::W2lResult.to_str(), "w2l_result"); + assert_eq!(AuditLogEntityType::MediaFile.to_str(), "media_file"); + assert_eq!(AuditLogEntityType::ModelWeight.to_str(), "model_weight"); + assert_eq!(AuditLogEntityType::User.to_str(), "user"); } #[test] fn test_from_str() { assert_eq!(AuditLogEntityType::from_str("comment").unwrap(), AuditLogEntityType::Comment); - assert_eq!(AuditLogEntityType::from_str("tts_model").unwrap(), AuditLogEntityType::TtsModel); - assert_eq!(AuditLogEntityType::from_str("tts_result").unwrap(), AuditLogEntityType::TtsResult); - assert_eq!(AuditLogEntityType::from_str("w2l_template").unwrap(), AuditLogEntityType::W2lTemplate); - assert_eq!(AuditLogEntityType::from_str("w2l_result").unwrap(), AuditLogEntityType::W2lResult); + assert_eq!(AuditLogEntityType::from_str("media_file").unwrap(), AuditLogEntityType::MediaFile); + assert_eq!(AuditLogEntityType::from_str("model_weight").unwrap(), AuditLogEntityType::ModelWeight); + assert_eq!(AuditLogEntityType::from_str("user").unwrap(), AuditLogEntityType::User); assert!(AuditLogEntityType::from_str("foo").is_err()); } } diff --git a/crates/lib/mysql_queries/src/queries/audit_logs/insert_audit_log.rs b/crates/lib/mysql_queries/src/queries/audit_logs/insert_audit_log.rs index 5846ecce14..530f8ac245 100644 --- a/crates/lib/mysql_queries/src/queries/audit_logs/insert_audit_log.rs +++ b/crates/lib/mysql_queries/src/queries/audit_logs/insert_audit_log.rs @@ -1,50 +1,42 @@ //! Audit logs are for entities that can be *edited* where we might lose the IP / edit history. +use std::marker::PhantomData; use anyhow::anyhow; -use sqlx::MySqlPool; +use sqlx::{Executor, MySql}; +use composite_identifiers::by_table::audit_logs::audit_log_entity::AuditLogEntity; use enums::by_table::audit_logs::audit_log_entity_action::AuditLogEntityAction; -use enums::by_table::audit_logs::audit_log_entity_type::AuditLogEntityType; use errors::AnyhowResult; +use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken; use tokens::tokens::audit_logs::AuditLogToken; -use tokens::tokens::comments::CommentToken; -use tokens::tokens::tts_models::TtsModelToken; -use tokens::tokens::w2l_templates::W2lTemplateToken; use tokens::tokens::users::UserToken; -pub enum AuditLogEntityToken { - Comment(CommentToken), - TtsModel(TtsModelToken), - TtsResult(String), // TODO: Strong type - W2lTemplate(W2lTemplateToken), - W2lResult(String), // TODO: Strong type -} - -pub struct Args<'a> { - pub entity_token: &'a AuditLogEntityToken, +pub struct InsertAuditLogArgs<'a, 'c : 'a, E> + where E: 'a + Executor<'c, Database = MySql> +{ + pub entity: &'a AuditLogEntity, pub entity_action: AuditLogEntityAction, pub maybe_actor_user_token: Option<&'a UserToken>, + pub maybe_actor_anonymous_visitor_token: Option<&'a AnonymousVisitorTrackingToken>, pub actor_ip_address: &'a str, pub is_actor_moderator: bool, - pub mysql_pool: &'a MySqlPool, + pub mysql_executor: E, + + // TODO: Not sure if this works to tell the compiler we need the lifetime annotation. + // See: https://doc.rust-lang.org/std/marker/struct.PhantomData.html#unused-lifetime-parameters + pub phantom: PhantomData<&'c E>, } - -pub async fn insert_audit_log( - args: Args<'_>, -) -> AnyhowResult { +pub async fn insert_audit_log<'a, 'c, E>( + args: InsertAuditLogArgs<'a, 'c, E>, +) -> AnyhowResult + where E: 'a + Executor<'c, Database = MySql> +{ let audit_log_token = AuditLogToken::generate(); - - let (entity_type, entity_token) = match args.entity_token { - AuditLogEntityToken::Comment(comment_token) => (AuditLogEntityType::Comment, comment_token.as_str()), - AuditLogEntityToken::TtsModel(tts_model_token) => (AuditLogEntityType::TtsModel, tts_model_token.as_str()), - AuditLogEntityToken::TtsResult(tts_result_token) => (AuditLogEntityType::TtsResult, tts_result_token.as_str()), - AuditLogEntityToken::W2lTemplate(w2l_template_token) => (AuditLogEntityType::W2lTemplate, w2l_template_token.as_str()), - AuditLogEntityToken::W2lResult(w2l_result_token) => (AuditLogEntityType::W2lResult, w2l_result_token.as_str()), - }; + let (entity_type, entity_token) = args.entity.get_composite_keys(); let query_result = sqlx::query!( r#" @@ -55,19 +47,20 @@ SET entity_token = ?, entity_action = ?, maybe_actor_user_token = ?, - maybe_actor_anonymous_visitor_token = '', + maybe_actor_anonymous_visitor_token = ?, is_actor_moderator = ?, actor_ip_address = ? "#, &audit_log_token, - entity_type, + entity_type.to_str(), entity_token, - args.entity_action, - args.maybe_actor_user_token, + args.entity_action.to_str(), + args.maybe_actor_user_token.map(|t| t.as_str()), + args.maybe_actor_anonymous_visitor_token.map(|t| t.as_str()), args.is_actor_moderator, args.actor_ip_address, ) - .execute(args.mysql_pool) + .execute(args.mysql_executor) .await; let _record_id = match query_result { diff --git a/crates/lib/mysql_queries/src/queries/audit_logs/mod.rs b/crates/lib/mysql_queries/src/queries/audit_logs/mod.rs index f3815091d7..c5187eb0ae 100644 --- a/crates/lib/mysql_queries/src/queries/audit_logs/mod.rs +++ b/crates/lib/mysql_queries/src/queries/audit_logs/mod.rs @@ -1 +1,2 @@ -pub mod insert_audit_log; \ No newline at end of file +pub mod insert_audit_log; +pub mod transactional_insert_audit_log; \ No newline at end of file diff --git a/crates/lib/mysql_queries/src/queries/audit_logs/transactional_insert_audit_log.rs b/crates/lib/mysql_queries/src/queries/audit_logs/transactional_insert_audit_log.rs new file mode 100644 index 0000000000..e89794712f --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/audit_logs/transactional_insert_audit_log.rs @@ -0,0 +1,64 @@ +//! Audit logs are for entities that can be *edited* where we might lose the IP / edit history. + +use anyhow::anyhow; +use sqlx::{Executor, MySql, Transaction}; + +use composite_identifiers::by_table::audit_logs::audit_log_entity::AuditLogEntity; +use enums::by_table::audit_logs::audit_log_entity_action::AuditLogEntityAction; +use errors::AnyhowResult; +use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken; +use tokens::tokens::audit_logs::AuditLogToken; +use tokens::tokens::users::UserToken; + +pub struct TransactionalInsertAuditLogArgs<'a, 'b> { + pub entity: &'a AuditLogEntity, + pub entity_action: AuditLogEntityAction, + + pub maybe_actor_user_token: Option<&'a UserToken>, + pub maybe_actor_anonymous_visitor_token: Option<&'a AnonymousVisitorTrackingToken>, + pub actor_ip_address: &'a str, + pub is_actor_moderator: bool, + + pub transaction: &'a mut Transaction<'b, MySql>, +} + + +pub async fn transactional_insert_audit_log<'a, 'b>( + args: TransactionalInsertAuditLogArgs<'a, 'b>, +) -> AnyhowResult { + + let audit_log_token = AuditLogToken::generate(); + let (entity_type, entity_token) = args.entity.get_composite_keys(); + + let query_result = sqlx::query!( + r#" +INSERT INTO audit_logs +SET + token = ?, + entity_type = ?, + entity_token = ?, + entity_action = ?, + maybe_actor_user_token = ?, + maybe_actor_anonymous_visitor_token = ?, + is_actor_moderator = ?, + actor_ip_address = ? + "#, + &audit_log_token, + entity_type.to_str(), + entity_token, + args.entity_action.to_str(), + args.maybe_actor_user_token.map(|t| t.as_str()), + args.maybe_actor_anonymous_visitor_token.map(|t| t.as_str()), + args.is_actor_moderator, + args.actor_ip_address, + ) + .execute(&mut **args.transaction) + .await; + + let _record_id = match query_result { + Ok(res) => res.last_insert_id(), + Err(err) => return Err(anyhow!("Mysql error: {:?}", err)), + }; + + Ok(audit_log_token) +} diff --git a/crates/lib/mysql_queries/src/queries/comments/edit_comment.rs b/crates/lib/mysql_queries/src/queries/comments/edit_comment.rs index 33d1956995..fadea8f3c5 100644 --- a/crates/lib/mysql_queries/src/queries/comments/edit_comment.rs +++ b/crates/lib/mysql_queries/src/queries/comments/edit_comment.rs @@ -18,9 +18,10 @@ pub struct Args<'e, 'c, E> pub mysql_executor: E, + // NB: phantom can be passed as Default::default() // TODO: Not sure if this works to tell the compiler we need the lifetime annotation. // See: https://doc.rust-lang.org/std/marker/struct.PhantomData.html#unused-lifetime-parameters - phantom: PhantomData<&'c E>, + pub phantom: PhantomData<&'c E>, } pub async fn edit_comment<'e, 'c : 'e, E>( diff --git a/crates/lib/mysql_queries/src/queries/entity_stats/upsert_entity_stats_on_bookmark_event.rs b/crates/lib/mysql_queries/src/queries/entity_stats/upsert_entity_stats_on_bookmark_event.rs index f2871b44fd..11a42d7269 100644 --- a/crates/lib/mysql_queries/src/queries/entity_stats/upsert_entity_stats_on_bookmark_event.rs +++ b/crates/lib/mysql_queries/src/queries/entity_stats/upsert_entity_stats_on_bookmark_event.rs @@ -20,6 +20,7 @@ pub struct UpsertEntityStatsArgs<'e, 'c, E> pub action: BookmarkAction, pub mysql_executor: E, + // NB: phantom can be passed as Default::default() // TODO: Not sure if this works to tell the compiler we need the lifetime annotation. // See: https://doc.rust-lang.org/std/marker/struct.PhantomData.html#unused-lifetime-parameters pub phantom: PhantomData<&'c E>, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/edit_user_feature_flags_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/edit_user_feature_flags_handler.rs index 6af2c61f50..126a225e37 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/edit_user_feature_flags_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/edit_user_feature_flags_handler.rs @@ -9,8 +9,12 @@ use actix_web::http::StatusCode; use actix_web::web::{Data, Json, Path}; use log::{info, log, warn}; use utoipa::ToSchema; +use composite_identifiers::by_table::audit_logs::audit_log_entity::AuditLogEntity; +use enums::by_table::audit_logs::audit_log_entity_action::AuditLogEntityAction; use enums::by_table::users::user_feature_flag::UserFeatureFlag; +use http_server_common::request::get_request_ip::get_request_ip; +use mysql_queries::queries::audit_logs::insert_audit_log::{insert_audit_log, InsertAuditLogArgs}; use mysql_queries::queries::users::user::get_user_token_by_username::get_user_token_by_username; use mysql_queries::queries::users::user::set_user_feature_flags::{set_user_feature_flags, SetUserFeatureFlagArgs}; use mysql_queries::queries::users::user_profiles::get_user_profile_by_token::get_user_profile_by_token; @@ -184,6 +188,8 @@ pub async fn edit_user_feature_flags_handler( } } + let ip_address = get_request_ip(&http_request); + set_user_feature_flags(SetUserFeatureFlagArgs { subject_user_token: &user_profile.user_token, maybe_feature_flags: user_feature_flags.maybe_serialize_string().as_deref(), @@ -195,5 +201,17 @@ pub async fn edit_user_feature_flags_handler( EditUserFeatureFlagsError::ServerError })?; + // NB: fail open + let _r = insert_audit_log(InsertAuditLogArgs { + entity: &AuditLogEntity::User(user_profile.user_token), + entity_action: AuditLogEntityAction::EditFeatures, + maybe_actor_user_token: Some(&user_session.user_token_typed), + maybe_actor_anonymous_visitor_token: None, + actor_ip_address: &ip_address, + is_actor_moderator: true, + mysql_executor: &server_state.mysql_pool, + phantom: Default::default(), + }).await; + Ok(simple_json_success()) }