insert audit log on feature flag change

This commit is contained in:
Brandon Thomas committed 2024-04-20 00:50:25 -04:00
1 parent aa20183e51
commit 9c42e76a3a
11 files changed
+191 -68

No files matched your search

@@ -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()),
}
}
}
@@ -0,0 +1 @@
pub mod audit_log_entity;
@@ -1 +1,2 @@
pub mod audit_logs;
pub mod batch_generations;
@@ -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());
}
@@ -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<Self, String> {
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());
}
}
@@ -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<AuditLogToken> {
pub async fn insert_audit_log<'a, 'c, E>(
args: InsertAuditLogArgs<'a, 'c, E>,
) -> AnyhowResult<AuditLogToken>
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 {
@@ -1 +1,2 @@
pub mod insert_audit_log;
pub mod insert_audit_log;
pub mod transactional_insert_audit_log;
@@ -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<AuditLogToken> {
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)
}
@@ -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>(
@@ -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>,
@@ -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())
}