mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 01:18:34 +00:00
insert audit log on feature flag change
This commit is contained in:
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>(
|
||||
|
||||
+1
@@ -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>,
|
||||
|
||||
+18
@@ -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())
|
||||
}
|
||||
Reference in new issue
Block a user