From 8fa4ca6fba77395ae2d5941a2d8dd7e8a22d9782 Mon Sep 17 00:00:00 2001 From: JesusPerez Date: Wed, 1 Sep 2021 19:31:13 +0100 Subject: [PATCH] chore: add reqtasks --- reqtasks/.gitignore | 10 ++ reqtasks/Cargo.toml | 55 +++++++ reqtasks/README.md | 14 ++ reqtasks/TODO.md | 9 ++ reqtasks/src/lib.rs | 385 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 473 insertions(+) create mode 100644 reqtasks/.gitignore create mode 100644 reqtasks/Cargo.toml create mode 100644 reqtasks/README.md create mode 100644 reqtasks/TODO.md create mode 100644 reqtasks/src/lib.rs diff --git a/reqtasks/.gitignore b/reqtasks/.gitignore new file mode 100644 index 0000000..c3a29c2 --- /dev/null +++ b/reqtasks/.gitignore @@ -0,0 +1,10 @@ +/target +target +Cargo.lock +.cache +.temp +.env +*.log +.DS_Store +logs +tmp diff --git a/reqtasks/Cargo.toml b/reqtasks/Cargo.toml new file mode 100644 index 0000000..21a3f6d --- /dev/null +++ b/reqtasks/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "reqtasks" +version = "0.1.0" +authors = ["JesusPerez "] +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.40" +base64 = "0.13.0" +casbin = "2.0.7" +chrono = "0.4" +dotenv = "0.15.0" +envmnt = "0.9.0" +error-chain = "0.12.4" +glob = "0.3.0" +json = "0.12.4" +once_cell = "1.7.2" +parking_lot = "0.11.1" +rand = "0.8.3" +regex = "1.4.3" +serde = { version = "1.0", features = ["derive"] } +serde_derive = "1.0.125" +serde_json = "1.0.64" +serde_yaml = "0.8.17" +slab = "0.4.3" +tempfile = "3.2.0" +tera = "1.8.0" +thiserror = "1.0.24" +toml = "0.5.8" +yaml-rust = "0.4" +tokio = { version = "1.5.0", features = ["full"] } +uuid = { version = "0.8", features = ["serde", "v4"] } +url = "2.2.1" +warp = { version = "0.3", features = ["default","websocket","tls","compression"] } +app_tools = { version = "0.1.0", path = "../../utils/app_tools" } +app_env = { version = "0.1.0", path = "../../defs/app_env" } +app_auth = { version = "0.1.0", path = "../../defs/app_auth" } +app_errors = { version = "0.1.0", path = "../../defs/app_errors" } + +[dev-dependencies] +pretty_env_logger = "0.4" +tracing-subscriber = "0.2.15" +tracing-log = "0.1" +serde_derive = "1.0.125" +handlebars = "3.0.0" +tokio = { version = "1.5.0", features = ["macros", "rt-multi-thread"] } +tokio-stream = { version = "0.1.5", features = ["net"] } +listenfd = "0.3" +envmnt = "0.9.0" + +[build-dependencies] +envmnt = "0.9.0" diff --git a/reqtasks/README.md b/reqtasks/README.md new file mode 100644 index 0000000..0ca6bfa --- /dev/null +++ b/reqtasks/README.md @@ -0,0 +1,14 @@ +# ReqTasks handlers library + +Handlers to manage request using [warp](https://github.com/seanmonstar/warp) + +It use one struct **ReqTasks** + +It can be extended or included in other types like reqenv + +## Handlers + +- Authentication +- Access: admin, session token. +- Render tamplates and pages +- Lang definitions diff --git a/reqtasks/TODO.md b/reqtasks/TODO.md new file mode 100644 index 0000000..95c07f8 --- /dev/null +++ b/reqtasks/TODO.md @@ -0,0 +1,9 @@ +# ReqTasks handlers library + +- [ ] Tests + +- [ ] Manage tokens and certification + +- [ ] Dynamic load profiles, modules, etc + +- [ ] Track sessions, devices, etc. [server timing](https://w3c.github.io/server-timing/) diff --git a/reqtasks/src/lib.rs b/reqtasks/src/lib.rs new file mode 100644 index 0000000..1759450 --- /dev/null +++ b/reqtasks/src/lib.rs @@ -0,0 +1,385 @@ +// +/*! Zterton +*/ +// Copyright 2021, Jesús Pérez Lorenzo +// +#![allow(clippy::needless_lifetimes)] +use std::collections::HashMap; +use std::fmt; +use std::str::from_utf8; + +use casbin::prelude::*; +use tera::Tera; + +use app_env::{ + AppStore, + appenv::AppEnv, + config::Config, + module::Module, + appdata::AppData +}; +use app_tools::{hash_from_data,read_path_file}; +use app_auth::{ + AuthStore, + UserCtx, + BEARER_PREFIX, + AuthError, + LoginRequest, +}; + +use warp::{ + http::{header::AUTHORIZATION,method::Method, HeaderMap, HeaderValue}, +// Filter, +}; + +// use salvo::{Request}; + +use app_errors::AppError; + +use std::result::Result; +// use std::io::Result; + +/// `ReqTasks` is a facilitator container for http request parsing and response composition +/// Manage info from `AppData` from env, vault, tera, ctx, etc. +/// Evaluate token, session state +/// Control some authorizations and permissions +#[derive(Clone)] +pub struct ReqTasks { + /// AppData object + pub app_data: AppData, +// pub app_data: &'a AppData, + /// Auth object + pub auth_store: AuthStore, +// pub auth_store: &'a AuthStore, + /// Request objet + pub header: HeaderMap, + pub method: Method, + pub path: String, + /// It will allow to take decisions based in origin or source context. + pub origin: String, + pub key_module: String, +} + +impl fmt::Display for ReqTasks { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {} {}", &self.path, &self.origin, &self.key_module) + } +} + +impl ReqTasks { + #[must_use] + pub fn new( + app_db: AppStore, + auth_store: AuthStore, + header: HeaderMap, + method: Method, + path: &str, + origin: &str, + key_module: &str + ) -> ReqTasks { + let app_data = app_db.app_data.read(); + // let auth_store: &'a AuthStore = &AuthStore { + // users: auth_db.users.clone(), + // sessions: auth_db.sessions.clone(), + // enforcer: auth_db.enforcer.clone(), + // }; + ReqTasks { + app_data: app_data.to_owned(), + auth_store: auth_store.to_owned(), + header, + method, + path: path.to_string(), + origin: origin.to_string(), + key_module: key_module.to_string(), + } + } + /// Get `AppEnv` + #[must_use] + pub fn env(&self) -> AppEnv { + self.app_data.env.to_owned() + // self.app_data.env.lock().await.to_owned() + } + /// Get Tera + #[must_use] + pub fn tera(&self) -> tera::Tera { + self.app_data.tera.to_owned() + } + // /// Get Vault + // pub fn vault(&self) -> ServerVault { + // self.app_data.vault.to_owned() + // } + /// Get Context (ctx) + #[must_use] + pub fn ctx(&self) -> tera::Context { + self.app_data.ctx.to_owned() + } + /// Get `AppEnv` Config + #[must_use] + pub fn config(&self) -> Config { + self.app_data.env.config.to_owned() + } + #[must_use] + pub fn module(&self) -> Module { + self.app_data.env.get_module(&self.key_module) + } + /// Get `AppEnv` Config + // #[must_use] + // pub fn params(&self) -> HashMap { + // self.req + // .uri() + // .query() + // .map_or_else(HashMap::new, |v| { + // url::form_urlencoded::parse(v.as_bytes()) + // .into_owned() + // .collect() + // }) + // } + /// Get Lang list from header - accept-language + /// get first one but only root part as first two characters (es in case of es-ES) + /// if lang is not in `Config.langs`it will fallback to `Config.default_lang` + #[must_use] + pub fn lang(&self) -> String { + #[allow(unused_assignments)] + let mut lang = self.config().default_lang; + let default_lang = self.config().default_lang; + let langs = self.config().langs; + // As fallback set default_lang + lang = default_lang; + // Get langs list from header + if let Some(languages) = self.header.get("accept-language") { + match languages.to_str() { + Ok(list_lngs) => { + // Get only first one + if let Some(full_lng) = list_lngs.split(',').collect::>().first() { + // Get root_lng part + let mut end: usize = 0; + full_lng + .chars() + .into_iter() + .take(2) + .for_each(|x| end += x.len_utf8()); + let root_lng = &full_lng[..end]; + // Filter to check if is in `Config.langs` + let list: Vec = langs + .iter() + .filter(|lng| lng.as_str() == root_lng) + .cloned() + .collect(); + if let Some(lng) = list.get(0) { + // Set if it is found and available + lang = lng.to_owned(); + } + } + } + Err(e) => println!("Error: {}", e), + } + } + // println!("Lang: {}",&lang); + lang + } + /// Get `Schema` Config + pub async fn is_admin(&self, ky: String) -> bool { + self.env().appkey == ky + } + + /// Render a template with context and load `data_path` from `root_path` in case is not empty + /// it use a `data_hash` for context with data if it is not empty + /// if errors it will render a `fallback_template` + #[allow(clippy::too_many_arguments,clippy::missing_errors_doc)] + pub async fn render_template( + &self, + ctx: &mut tera::Context, + root_path: &str, + template_name: &str, + fallback_template: &str, + data_path: &str, + data_hash: &mut HashMap, + app_module: &str, + ) -> Result { + let tera: &mut Tera = &mut self.tera(); //.to_owned(); + if ! app_module.is_empty() { + AppData::load_modules(&self.env(), &app_module,ctx); + } + Ok(if data_path.is_empty() { + tera + .render(&template_name, &ctx) + .map_err(|e| AppError::ErrorInternalServerError(format!("Error: {}", e)))? + } else { + let render_fallback = |e| -> Result { + println!("Error parsing data {}:\n{}", &data_path, e); + Ok(tera + .render(fallback_template, &tera::Context::new()) + .map_err(|e| AppError::ErrorInternalServerError(format!("Template error {}",e)))?) + }; + match hash_from_data(format!("{}/{}", &root_path, &data_path).as_str(), ctx, data_hash, true) { + Ok(_) => { + let mut need_reload = false; + for (key, value) in data_hash.iter() { + if key.as_str() == "need_reload" { + need_reload = true; + } else { + ctx.insert(key, value); + } + } + if need_reload { + match read_path_file(&root_path, &template_name, "content") { + Ok(tpl) => { + println!("reload {}",&template_name); + tera.add_raw_templates(vec![(&template_name, tpl)]) + .map_err(|e| AppError::ErrorInternalServerError(format!("Error reload: {}", e)))? + }, + Err(e) => return render_fallback(e), + }; + } + tera + .render(&template_name, &ctx) + .map_err(|e| AppError::ErrorInternalServerError(format!("Error render: {}", e)))? + } + Err(e) => { + println!("Error parsing data {}:\n{}", &data_path, e); + return render_fallback(e); + } + } + }) + } + #[allow(clippy::too_many_arguments,clippy::missing_errors_doc)] + pub async fn render_page( + &self, + ctx: &mut tera::Context, + root_path: &str, + template_name: &str, + fallback_template: &str, + data_path: &str, + data_hash: &mut HashMap, + app_module: &str, + ) -> Result { + let tera = &self.tera(); + let res = match self + .render_template( + ctx, + root_path, + template_name, + fallback_template, + data_path, + data_hash, + app_module, + ) + .await + { + Ok(res) => res, + Err(e) => { + println!( + "Error render template + data {} + {}:\n{}", + &template_name, &data_path, e + ); + tera + .render(&fallback_template, &tera::Context::new()) + .map_err(|e| AppError::ErrorInternalServerError(format!("Template error {}",e)))? + } + }; +// Ok(HttpResponse::Ok().content_type("text/html").body(res)) + Ok(res) + } + #[allow(clippy::missing_errors_doc)] + pub async fn render_to( + &self, + template_name: &str, + data_path: &str, + data_hash: &mut HashMap, + app_module: &str, + ) -> Result { + let app_env = &self.env(); + // From Default Context + let mut ctx = self.ctx().to_owned(); + let fallback_template = "index.html"; + self + .render_page( + &mut ctx, + app_env.config.templates_path.as_str(), + template_name, + fallback_template, + data_path, + data_hash, + app_module, + ) + .await + } + #[allow(clippy::missing_errors_doc)] + pub fn token_from_header(&self) -> anyhow::Result { + if let Some(header) = self.header.get(AUTHORIZATION) { + let auth_header = match from_utf8(header.as_bytes()) { + Ok(v) => v, + Err(_) => return Err(AuthError::NoAuthHeaderFoundError.into()), + }; + if !auth_header.starts_with(BEARER_PREFIX) { + return Err(AuthError::InvalidAuthHeaderFormatError.into()); + } + let without_prefix = auth_header.trim_start_matches(BEARER_PREFIX); + Ok(without_prefix.to_owned()) + } else { + Err(AuthError::NoAuthHeaderFoundError.into()) + } + } + #[allow(clippy::missing_errors_doc)] + pub async fn token_session(&self, login: &LoginRequest) -> anyhow::Result { + // dbg!("{}",login); + if let Some(user) = &self.auth_store.users + .read() + .await + .iter() + .find(|(_, v)| v.name == *login.name) + // .filter(|(_, v)| *v.name == name) + // .nth(0) + { + let shdw = &self.auth_store.shadows.read().await; + if let Some(usr_shdw) = shdw.get(&String::from(user.0)) { + // println!("{}",&usr_shdw.passwd); + // println!("{}",&login.passwd); + // println!("{}",&user.0); + if usr_shdw.passwd == login.passwd { + // println!("OK "); + let token = uuid::Uuid::new_v4().to_string(); + self.auth_store.sessions.write().await.insert(token.clone(), String::from(user.0)); + return Ok(token); + } + } + Err(AuthError::UserNotFoundError.into()) + //shdw.get(usr_key); + } else { + Err(AuthError::UserNotFoundError.into()) + } + } + #[allow(clippy::missing_errors_doc)] + pub async fn user_authentication(&self) -> anyhow::Result { + let token = self.token_from_header().unwrap_or_else(|e| { println!("{}",e); String::from("")}); + self.check_authentication(token).await + } + #[allow(clippy::missing_errors_doc)] + pub async fn check_authentication(&self, token: String) -> anyhow::Result { + if let Some(user_id) = self.auth_store.sessions.read().await.get(&token) { + if let Some(user) = self.auth_store.users.read().await.get(user_id) { + // println!("{} - {} - {}",&user.role.as_str(), &self.path.as_str(), &self.method.as_str()); + match self.auth_store.enforcer.enforce( + (&user.role.as_str(), &self.path.as_str(), &self.method.as_str()) + ) + { + Ok(authorized) => { + if authorized { + Ok(UserCtx { user_id: user.user_id.to_owned(), token }) + } else { + Err(AuthError::UnauthorizedError.into()) + } + } + Err(e) => { + eprintln!("error during authorization: {}", e); + Err(AuthError::AuthorizationError.into()) + } + } + } else { + Err(AuthError::InvalidTokenError.into()) + } + } else { + Err(AuthError::InvalidTokenError.into()) + } + } +}