mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 00:47:53 +00:00
Begin work on "rule reorder", plus tests for unsafe query
This commit is contained in:
1 parent
9b54e5b4be
commit
dfe902de4b
6 files changed
+284
No files matched your search
Generated
+2
@@ -1254,6 +1254,8 @@ dependencies = [
|
||||
"container_common",
|
||||
"dotenv",
|
||||
"log",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
|
||||
@@ -25,6 +25,10 @@ chrono = { version = "0.4.19", features = ["serde"] }
|
||||
dotenv = "0.15.0"
|
||||
log = "0.4.14"
|
||||
|
||||
# Needed to safety check some queries we can't use escaping for
|
||||
once_cell = "1.9.0"
|
||||
regex = "1.5.4"
|
||||
|
||||
# Serialization
|
||||
serde = "1.0.125"
|
||||
serde_derive = "1.0.125"
|
||||
|
||||
@@ -2,4 +2,5 @@ pub mod delete_twitch_event_rule;
|
||||
pub mod get_twitch_event_rule_for_user;
|
||||
pub mod insert_twitch_event_rule_builder;
|
||||
pub mod list_twitch_event_rules_for_user;
|
||||
pub mod reorder_twitch_event_rules;
|
||||
pub mod update_twitch_event_rule_builder;
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
use anyhow::anyhow;
|
||||
use container_common::anyhow_result::AnyhowResult;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use sqlx::MySqlPool;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Check that tokens are alphanumeric or can have colons.
|
||||
static SAFE_TOKENS_REGEX : Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new("^[a-zA-Z0-9:]+$").unwrap()
|
||||
});
|
||||
|
||||
/// Check that tokens are alphanumeric or can have colons.
|
||||
#[must_use]
|
||||
fn check_token_safety(token: &str) -> AnyhowResult<()> {
|
||||
if SAFE_TOKENS_REGEX.is_match(token) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("Token contains invalid characters"))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reorder_twitch_event_rules(
|
||||
rule_token_to_order_map: HashMap<String, u32>,
|
||||
user_token: &str,
|
||||
ip_address_update: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<bool> {
|
||||
|
||||
// THIS IS NOT SANITIZED, so we check for injection here.
|
||||
for (rule_token, _position) in rule_token_to_order_map.iter() {
|
||||
check_token_safety(rule_token.as_str())?;
|
||||
}
|
||||
|
||||
// THIS IS NOT SANITIZED, so we check for injection here.
|
||||
check_token_safety(user_token)?;
|
||||
|
||||
let query_inner = rule_token_to_order_map.iter()
|
||||
.map(|(token, position)| {
|
||||
format!("(\"{}\", {}, \"{}\")", token.as_str(), position, ip_address_update)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
|
||||
// Upsert to accomplish multiple updates at once.
|
||||
// tl;dr https://stackoverflow.com/a/34866431
|
||||
// ALSO THIS IS NOT SANITIZED.
|
||||
let query = format!(r#"
|
||||
INSERT INTO twitch_event_rules (token, user_specified_rule_order, ip_address_update)
|
||||
VALUES
|
||||
{}
|
||||
ON DUPLICATE KEY UPDATE
|
||||
token=VALUES(token),
|
||||
user_specified_rule_order=VALUES(user_specified_rule_order),
|
||||
ip_address_update=VALUES(ip_address_update)
|
||||
WHERE
|
||||
user_token = {}
|
||||
"#, query_inner, user_token);
|
||||
|
||||
// NB: This, unfortunately, cannot be statically checked.
|
||||
let mut query = sqlx::query(&query);
|
||||
|
||||
let result = query.execute(mysql_pool).await;
|
||||
|
||||
match result {
|
||||
Err(err) => Err(anyhow!("error with query: {:?}", err)),
|
||||
Ok(_r) => Ok(true),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::queries::twitch::twitch_event_rules::reorder_twitch_event_rules::check_token_safety;
|
||||
|
||||
#[test]
|
||||
fn safe_tokens() {
|
||||
assert!(check_token_safety("foo").is_ok());
|
||||
assert!(check_token_safety("FOO").is_ok());
|
||||
assert!(check_token_safety("12345").is_ok());
|
||||
assert!(check_token_safety("abcdefABCDEF012345679").is_ok());
|
||||
assert!(check_token_safety("PREFIX:TOKEN1234").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsafe_tokens() {
|
||||
assert!(check_token_safety("").is_err());
|
||||
assert!(check_token_safety(",").is_err());
|
||||
assert!(check_token_safety("ABCD,").is_err());
|
||||
assert!(check_token_safety("foo;").is_err());
|
||||
assert!(check_token_safety("foo;foo").is_err());
|
||||
assert!(check_token_safety("foo;foo").is_err());
|
||||
assert!(check_token_safety("\"foo").is_err());
|
||||
assert!(check_token_safety("foo\"").is_err());
|
||||
assert!(check_token_safety("foo'").is_err());
|
||||
assert!(check_token_safety("foo'bar").is_err());
|
||||
assert!(check_token_safety("foo`foo").is_err());
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,5 @@ pub mod delete_event_rule;
|
||||
pub mod edit_event_rule;
|
||||
pub mod get_event_rule;
|
||||
pub mod list_event_rules_for_user;
|
||||
pub mod reorder_twitch_event_rules;
|
||||
pub mod validations;
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
use actix_http::Error;
|
||||
use actix_http::http::header;
|
||||
use actix_web::HttpResponseBuilder;
|
||||
use actix_web::cookie::Cookie;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Path, Json};
|
||||
use actix_web::{Responder, web, HttpResponse, error, HttpRequest};
|
||||
use crate::http_server::web_utils::ip_address::get_request_ip;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::server_state::ServerState;
|
||||
use database_queries::complex_models::event_match_predicate::EventMatchPredicate;
|
||||
use database_queries::complex_models::event_responses::EventResponse;
|
||||
use database_queries::queries::twitch::twitch_event_rules::get_twitch_event_rule_for_user::get_twitch_event_rule_for_user;
|
||||
use database_queries::queries::twitch::twitch_event_rules::update_twitch_event_rule_builder::UpdateTwitchEventRuleBuilder;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use log::{info, warn, log, error};
|
||||
use sqlx::MySqlPool;
|
||||
use sqlx::error::DatabaseError;
|
||||
use sqlx::error::Error::Database;
|
||||
use sqlx::mysql::MySqlDatabaseError;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use crate::http_server::endpoints::twitch::event_rules::validations::validate_event_response::validate_event_response;
|
||||
use crate::http_server::endpoints::twitch::event_rules::validations::validate_event_match_predicate::validate_event_match_predicate;
|
||||
|
||||
// =============== Request ===============
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditTwitchEventRulePathInfo {
|
||||
token: String,
|
||||
}
|
||||
|
||||
// These are not sparse updates!
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditTwitchEventRuleRequest {
|
||||
pub event_match_predicate: Option<EventMatchPredicate>,
|
||||
pub event_response: Option<EventResponse>,
|
||||
pub rule_is_disabled: bool,
|
||||
}
|
||||
|
||||
// =============== Success Response ===============
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EditTwitchEventRuleResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
// =============== Error Response ===============
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum EditTwitchEventRuleError {
|
||||
BadInput(String),
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for EditTwitchEventRuleError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
EditTwitchEventRuleError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
EditTwitchEventRuleError::NotFound => StatusCode::NOT_FOUND,
|
||||
EditTwitchEventRuleError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
EditTwitchEventRuleError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for EditTwitchEventRuleError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Handler ===============
|
||||
|
||||
pub async fn edit_twitch_event_rule_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<EditTwitchEventRulePathInfo>,
|
||||
request: web::Json<EditTwitchEventRuleRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, EditTwitchEventRuleError>
|
||||
{
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
EditTwitchEventRuleError::ServerError
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) => session,
|
||||
None => {
|
||||
warn!("not logged in");
|
||||
return Err(EditTwitchEventRuleError::NotAuthorized);
|
||||
}
|
||||
};
|
||||
|
||||
if user_session.is_banned {
|
||||
warn!("banned users cannot edit");
|
||||
return Err(EditTwitchEventRuleError::NotAuthorized);
|
||||
}
|
||||
|
||||
let twitch_event_rule = get_twitch_event_rule_for_user(
|
||||
&path.token, &user_session.user_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Error with query: {:?}", e);
|
||||
return EditTwitchEventRuleError::ServerError;
|
||||
})?;
|
||||
|
||||
if twitch_event_rule.is_none() {
|
||||
info!("event rule not found");
|
||||
return Err(EditTwitchEventRuleError::NotFound);
|
||||
}
|
||||
|
||||
let creator_ip_address = get_request_ip(&http_request);
|
||||
|
||||
let event_match_predicate = request.event_match_predicate
|
||||
.clone()
|
||||
.unwrap_or(EventMatchPredicate::NotSet {});
|
||||
|
||||
validate_event_match_predicate(&event_match_predicate)
|
||||
.map_err(|reason| EditTwitchEventRuleError::BadInput(reason))?;
|
||||
|
||||
let mut event_match_predicate = serde_json::to_string(&event_match_predicate)
|
||||
.map_err(|e| {
|
||||
return EditTwitchEventRuleError::BadInput(
|
||||
"improper EventMatchPredicate".to_string());
|
||||
})?;
|
||||
|
||||
let event_response = request.event_response
|
||||
.clone()
|
||||
.unwrap_or(EventResponse::NotSet {});
|
||||
|
||||
validate_event_response(&event_response)
|
||||
.map_err(|reason| EditTwitchEventRuleError::BadInput(reason))?;
|
||||
|
||||
let mut event_response = serde_json::to_string(&event_response)
|
||||
.map_err(|e| {
|
||||
return EditTwitchEventRuleError::BadInput(
|
||||
"improper EventResponse".to_string());
|
||||
})?;
|
||||
|
||||
let update_builder = UpdateTwitchEventRuleBuilder {
|
||||
token: path.token.clone(),
|
||||
event_match_predicate,
|
||||
event_response,
|
||||
rule_is_disabled: request.rule_is_disabled,
|
||||
ip_address_update: creator_ip_address,
|
||||
};
|
||||
|
||||
let _r = update_builder.update(&server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Error with query: {:?}", e);
|
||||
return EditTwitchEventRuleError::ServerError;
|
||||
});
|
||||
|
||||
let response = EditTwitchEventRuleResponse {
|
||||
success: true,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| EditTwitchEventRuleError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
Reference in new issue
Block a user