diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ed6f5a..1e99701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [11.0.0] - 2021-04-05 + +### Added +- Added email field on user +- Added data field on user +- Added user email address validation +- Added update user endpoint + ## [10.0.0] - 2021-04-05 -## Fixed +### Fixed - Fixed infinite SSE event sending ### Changed diff --git a/Cargo.lock b/Cargo.lock index 78c7d83..d5e1791 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "ascii_utils" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" + [[package]] name = "async-attributes" version = "1.1.2" @@ -459,7 +465,7 @@ dependencies = [ [[package]] name = "broker" -version = "10.0.0" +version = "11.0.0" dependencies = [ "anyhow", "async-std", @@ -471,6 +477,7 @@ dependencies = [ "json", "jsonwebtoken", "lazy_static", + "mailchecker", "nippy", "rmp-serde", "rocksdb", @@ -814,6 +821,15 @@ version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" +[[package]] +name = "fast_chemail" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +dependencies = [ + "ascii_utils", +] + [[package]] name = "fastrand" version = "1.4.0" @@ -1263,6 +1279,15 @@ dependencies = [ "value-bag", ] +[[package]] +name = "mailchecker" +version = "4.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be9884d8adeab79160bc07b42bce71324406c4792fcf9e785476dbc796d3d55" +dependencies = [ + "fast_chemail", +] + [[package]] name = "matches" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index 46ccbd8..0c7d6be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "broker" -version = "10.0.0" +version = "11.0.0" authors = ["Bevan Hunt "] edition = "2018" license = "MIT" @@ -32,3 +32,4 @@ tide-rustls = "0.3" futures = "0.3" tide-acme = "0.1.0" base64 = "0.13" +mailchecker = "4" diff --git a/README.md b/README.md index 9e1280c..8386189 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ Broker follows an insert-only/publish/subscribe paradigm rather than a REST CRUD * Sync latest events on SSE client connection * Auto-provision and renews SSL cert via LetsEncrypt * Verify endpoint for external services using Broker user system like [portal](https://crates.io/crates/portal) -* User Management API endpoints (revoke, unrevoke, list, get) +* User Management API endpoints (revoke, unrevoke, list, get, update) +* User Email Address Validation (regex and blacklist check against throwaway emails) ### How it works @@ -68,10 +69,16 @@ POST /create_user "username": "bob", "password": "password1", "admin_token": "letmein", - "tenant_name": "tenant_1" + "tenant_name": "tenant_1", + "email": "bob@hotmail.com", + "data": { + "name": "Robert Wieland", + "image": "https://img.com/bucket/123/123.jpg" + } } ``` -- admin_token is required and can be set in the command args - it is for not allowing everyone to add a user - the default is `letmein` +- `admin_token` is required and can be set in the command args - it is for not allowing everyone to add a user - the default is `letmein` +- `email` and `data` is an optional field will return `200` or `500` or `400` @@ -94,7 +101,6 @@ will return "jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTc2NzQ5MTUsImlhdCI6MTYxNzU4ODUxNSwiaXNzIjoiRGlzcGF0Y2hlciIsInN1YiI6ImZvbyJ9.OwiaZJcFUC_B0CA0ffRZVTWKRf5_vQ7vt5USNJEeKRE" } ``` -- where {...} is a JWT (string) #### Step 3 - connect to SSE @@ -175,7 +181,6 @@ POST /list_users "admin_token": "letmein", } ``` -- where {...} is for the event a string and data is any JSON you want will return: `200` or `500` or `400` or `401` @@ -187,10 +192,16 @@ will return: `200` or `500` or `400` or `401` "password": "***", "revoked": false, "tenant_name": "tenant_1", - "username": "bob" + "username": "bob", + "email": "bob@hotmail.com", + "data": { + "name": "Robert Wieland", + "image": "https://img.com/bucket/123/123.jpg" + } } ] ``` +- note: `email` and `data` can be `null` #### Optional - get user @@ -204,7 +215,6 @@ POST /get_user "username": "bob" } ``` -- where {...} is for the event a string and data is any JSON you want will return: `200` or `500` or `400` or `401` @@ -215,9 +225,38 @@ will return: `200` or `500` or `400` or `401` "password": "***", "revoked": false, "tenant_name": "tenant_1", - "username": "bob" + "username": "bob", + "email": "bob@hotmail.com", + "data": { + "name": "Robert Wieland", + "image": "https://img.com/bucket/123/123.jpg" + } } ``` +- note: `email` and `data` can be `null` + +#### Optional - update user + +```html +POST /update_user +``` +- public endpoint +```json +{ + "admin_token": "letmein", + "username": "bob", + "tenant_name": "tenant_2", + "password": "new_password", + "email": "bober@hotmail.com", + "data": { + "name": "Robert Falcon", + "image": "https://img.com/bucket/123/1234.jpg" + } +} +``` +- note: `tenant_name`, `password`, `email`, `data` are optional fields + +will return: `200` or `500` or `400` or `401` ### Install diff --git a/src/main.rs b/src/main.rs index 4c3dfcf..124187e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ use async_std::stream; use std::time::Duration; use futures::StreamExt; use tide_acme::{AcmeConfig, TideRustlsExt}; +use mailchecker::is_valid; lazy_static! { static ref DB : Arc = { @@ -47,9 +48,11 @@ pub struct EnvVarConfig { pub struct User { pub id: uuid::Uuid, pub revoked: bool, + pub email: Option, pub username: String, pub password: String, pub tenant_name: String, + pub data: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -58,6 +61,18 @@ pub struct UserForm { pub password: String, pub tenant_name: String, pub admin_token: String, + pub email: Option, + pub data: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UpdateUserForm { + pub username: String, + pub tenant_name: Option, + pub admin_token: String, + pub email: Option, + pub password: Option, + pub data: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -128,6 +143,46 @@ fn activate_user(username: String) -> Result<()> { } } +fn modify_user(update_user_form: UpdateUserForm) -> Result<()> { + match get_user_by_username(update_user_form.clone().username)? { + Some(mut user) => { + + match update_user_form.tenant_name { + Some(tn) => { + user.tenant_name = tn; + }, + None => {} + } + + match update_user_form.email { + Some(email) => { + if is_valid(&email) { + user.email = Some(email); + } + }, + None => {} + } + + user.data = update_user_form.data; + + match update_user_form.password { + Some(password) => { + let config = Argon2Config::default(); + let uuid_string = Uuid::new_v4().to_string(); + let salt = uuid_string.as_bytes(); + let password = password.as_bytes(); + let hashed = argon2::hash_encoded(password, salt, &config).unwrap(); + user.password = hashed; + }, + None => {} + } + puts_user(user)?; + Ok(()) + }, + None => { Ok(()) } + } +} + fn get_user_by_username(user_username: String) -> Result> { let users = get_users()?; Ok(users.into_iter().filter(|user| user.username == user_username).last()) @@ -144,7 +199,7 @@ fn get_users() -> Result> { } fn puts_user(user: User) -> Result<()> { - let key = format!("users_{}_{}", user.tenant_name, user.id); + let key = format!("users_{}", user.username); let value = rmp_serde::to_vec_named(&user)?; replace(key, value)?; Ok(()) @@ -193,6 +248,16 @@ fn user_create(user_form: UserForm) -> Result> { let configure = env_var_config(); if configure.admin_token == user_form.clone().admin_token { + + match user_form.clone().email { + Some(email) => { + if !is_valid(&email) { + let j = json!({"error": "email is invalid"}).to_string(); + return Ok(Some(j)); + } + }, + None => {} + } if !is_user_unique(user_form.clone().username)? { let j = json!({"error": "username already taken"}).to_string(); return Ok(Some(j)); @@ -209,8 +274,10 @@ fn user_create(user_form: UserForm) -> Result> { password: hashed, tenant_name: user_form.clone().tenant_name, revoked: false, + email: user_form.clone().email, + data: user_form.clone().data, }; - + puts_user(new_user).unwrap(); return Ok(None); } @@ -474,6 +541,18 @@ async fn unrevoke_user(mut req: Request<()>) -> tide::Result { } } +async fn update_user(mut req: Request<()>) -> tide::Result { + let r = req.body_string().await?; + let update_user_form : UpdateUserForm = serde_json::from_str(&r)?; + let configure = env_var_config(); + if configure.admin_token == update_user_form.admin_token { + modify_user(update_user_form)?; + Ok(tide::Response::builder(200).header("content-type", "application/json").build()) + } else { + Ok(tide::Response::builder(401).header("content-type", "application/json").build()) + } +} + #[async_std::main] async fn main() -> tide::Result<()> { @@ -496,6 +575,7 @@ async fn main() -> tide::Result<()> { app.at("/revoke_user").post(revoke_user); app.at("/get_user").post(get_user); app.at("/unrevoke_user").post(unrevoke_user); + app.at("/update_user").post(update_user); app.at("/sse").get(tide::sse::endpoint(|req: Request<()>, sender| async move { @@ -572,4 +652,3 @@ async fn main() -> tide::Result<()> { Ok(()) } -