From 0434463b25daab095544e351828181a04bc074c7 Mon Sep 17 00:00:00 2001 From: Allen Date: Tue, 15 Feb 2022 19:30:38 -0800 Subject: [PATCH] rework codebase to use sea_orm instead move entities rework polar rules buncha other changes rework rules and endpoint logic fix role assn remove unused imports fix oso query, result might be error update deps super thicc diff too many changes to document, but basically reworking to use SeaORM WIP dumb mistake WIP bloop don't actually need the extra ref more WIP don't program when tired seriously, DONT PROGRAM WHEN TIRED fix remove custom-derive, use fieldfilter WIP adjust rules to work with new pclasses impl update_user handler finish user handlers rewrite finish rework whoops --- Cargo.lock | 459 ++++++++++++++---- Cargo.toml | 28 +- Makefile.toml | 33 ++ entity/Cargo.toml | 28 ++ {src => entity/src}/macros.rs | 0 entity/src/mod.rs | 7 + entity/src/prelude.rs | 3 + entity/src/sea_orm_active_enums.rs | 24 + entity/src/user_account.rs | 41 ++ ...0220211233308_create_user_account.down.sql | 4 + ...20220211233308_create_user_account.up.sql} | 8 +- .../20220211233308_create_users.down.sql | 4 - polar/users.polar | 53 +- src/actions.rs | 61 +++ src/auth.rs | 3 +- src/constants.rs | 4 + src/error.rs | 20 +- src/handlers/login.rs | 49 +- src/handlers/mod.rs | 8 - src/handlers/user.rs | 310 ++++-------- src/main.rs | 3 +- src/models/mod.rs | 7 - src/models/user.rs | 79 --- src/server.rs | 28 +- 24 files changed, 758 insertions(+), 506 deletions(-) create mode 100644 Makefile.toml create mode 100644 entity/Cargo.toml rename {src => entity/src}/macros.rs (100%) create mode 100644 entity/src/mod.rs create mode 100644 entity/src/prelude.rs create mode 100644 entity/src/sea_orm_active_enums.rs create mode 100644 entity/src/user_account.rs create mode 100644 migrations/20220211233308_create_user_account.down.sql rename migrations/{20220211233308_create_users.up.sql => 20220211233308_create_user_account.up.sql} (61%) delete mode 100644 migrations/20220211233308_create_users.down.sql create mode 100644 src/actions.rs delete mode 100644 src/models/mod.rs delete mode 100644 src/models/user.rs diff --git a/Cargo.lock b/Cargo.lock index f7b9b27..92cdb28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "ahash" version = "0.7.6" @@ -22,6 +28,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "ansi_term" version = "0.12.1" @@ -55,6 +67,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "ascii-canvas" version = "3.0.0" @@ -64,6 +82,27 @@ dependencies = [ "term", ] +[[package]] +name = "async-stream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171374e7e3b2504e0e5236e3b59260560f9fe94bfe9ac39ba5e4e929c5590625" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.52" @@ -97,9 +136,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" @@ -127,7 +166,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-util", + "tokio-util 0.6.9", "tower", "tower-http", "tower-layer", @@ -148,6 +187,19 @@ dependencies = [ "mime", ] +[[package]] +name = "bae" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b8de67cc41132507eeece2584804efcb15f85ba516e34c944b7667f480397a" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "base32" version = "0.4.0" @@ -197,7 +249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.5.2", "constant_time_eq", ] @@ -211,6 +263,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + [[package]] name = "block-padding" version = "0.2.1" @@ -237,9 +298,9 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.72" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" [[package]] name = "cfg-if" @@ -274,7 +335,7 @@ dependencies = [ "memchr", "pin-project-lite", "tokio", - "tokio-util", + "tokio-util 0.6.9", ] [[package]] @@ -285,9 +346,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "core-foundation" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys", "libc", @@ -335,9 +396,9 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b979d76c9fcb84dffc80a73f7290da0f83e4c95773494674cb44b76d13a7a110" +checksum = "4dd435b205a4842da59efd07628f921c096bc1cc0a156835b4fa0bcb9a19bcce" dependencies = [ "cfg-if", "crossbeam-utils", @@ -345,9 +406,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" +checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" dependencies = [ "cfg-if", "lazy_static", @@ -359,6 +420,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "crypto-mac" version = "0.10.1" @@ -394,6 +465,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", +] + [[package]] name = "dirs" version = "4.0.0" @@ -471,6 +552,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "entity" +version = "0.1.0" +dependencies = [ + "bincode", + "oso", + "redis", + "sea-orm", + "serde", +] + [[package]] name = "fastrand" version = "1.7.0" @@ -480,6 +572,26 @@ dependencies = [ "instant", ] +[[package]] +name = "fieldfilter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbed33889eeeb85517e46db210481cd7404bcdfc1ba4816d011a2f7e4def2ade" +dependencies = [ + "fieldfilter-derive", +] + +[[package]] +name = "fieldfilter-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9912bbdf2e79e87cfd6b5a27e2ce12db39a2898c3fbc51c4f7531a5557e747e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "fixedbitset" version = "0.2.0" @@ -519,9 +631,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", @@ -534,9 +646,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", @@ -544,15 +656,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-executor" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" dependencies = [ "futures-core", "futures-task", @@ -572,15 +684,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-macro" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ "proc-macro2", "quote", @@ -589,21 +701,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-util" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-channel", "futures-core", @@ -642,9 +754,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689" +checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" dependencies = [ "bytes", "fnv", @@ -655,7 +767,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util", + "tokio-util 0.6.9", "tracing", ] @@ -679,9 +791,9 @@ dependencies = [ [[package]] name = "headers" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c84c647447a07ca16f5fbd05b633e535cc41a08d2d74ab1e08648df53be9cb89" +checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" dependencies = [ "base64", "bitflags", @@ -690,7 +802,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha-1", + "sha-1 0.10.0", ] [[package]] @@ -733,7 +845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" dependencies = [ "crypto-mac 0.10.1", - "digest", + "digest 0.9.0", ] [[package]] @@ -743,7 +855,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ "crypto-mac 0.11.1", - "digest", + "digest 0.9.0", ] [[package]] @@ -787,9 +899,9 @@ checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" [[package]] name = "httparse" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" +checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" [[package]] name = "httpdate" @@ -799,9 +911,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.16" +version = "0.14.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" +checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd" dependencies = [ "bytes", "futures-channel", @@ -812,7 +924,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 0.4.8", + "itoa 1.0.1", "pin-project-lite", "socket2", "tokio", @@ -840,9 +952,9 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" [[package]] name = "impl-trait-for-tuples" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5dacb10c5b3bb92d46ba347505a9041e676bb20ad220101326bffb0c93031ee" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" dependencies = [ "proc-macro2", "quote", @@ -975,9 +1087,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.113" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9" +checksum = "06e509672465a0504304aa87f9f176f2b2b716ed8fb105ebe5c02dc6dce96a94" [[package]] name = "libreauth" @@ -994,7 +1106,7 @@ dependencies = [ "nom 6.1.2", "pbkdf2", "rust-argon2", - "sha-1", + "sha-1 0.9.8", "sha2", "sha3", "unicode-normalization", @@ -1002,9 +1114,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" dependencies = [ "scopeguard", ] @@ -1047,9 +1159,9 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "matchit" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b6f41fdfbec185dd3dff58b51e323f5bc61692c0de38419a957b0dcfccca3c" +checksum = "9376a4f0340565ad675d11fc1419227faf5f60cd7ac9cb2e7185a471f30af833" [[package]] name = "md-5" @@ -1057,8 +1169,8 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.9.0", + "digest 0.9.0", "opaque-debug", ] @@ -1082,9 +1194,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "0.7.14" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" dependencies = [ "libc", "log", @@ -1108,9 +1220,10 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", - "bincode", "chrono", "dotenv", + "entity", + "fieldfilter", "lazy_static", "lettre", "libreauth", @@ -1120,9 +1233,9 @@ dependencies = [ "rand", "redis", "regex", + "sea-orm", "serde", "serde_json", - "sqlx", "thiserror", "tokio", "tower", @@ -1199,13 +1312,24 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -1305,6 +1429,30 @@ dependencies = [ "syn", ] +[[package]] +name = "ouroboros" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71643f290d126e18ac2598876d01e1d57aed164afc78fdb6e2a0c6589a1f6662" +dependencies = [ + "aliasable", + "ouroboros_macro", + "stable_deref_trait", +] + +[[package]] +name = "ouroboros_macro" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9a247206016d424fe8497bc611e510887af5c261fbbf977877c4bb55ca4d82" +dependencies = [ + "Inflector", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "oxide-auth" version = "0.5.1" @@ -1388,9 +1536,9 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" dependencies = [ "siphasher", ] @@ -1555,7 +1703,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "tokio", - "tokio-util", + "tokio-util 0.6.9", "url", ] @@ -1646,6 +1794,17 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rust_decimal" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4214023b1223d02a4aad9f0bb9828317634a56530870a2eaf7200a99c0c10f68" +dependencies = [ + "arrayvec 0.7.2", + "num-traits", + "serde", +] + [[package]] name = "rustversion" version = "1.0.6" @@ -1674,11 +1833,97 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sea-orm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd24380b48dacd3ed1c3d467c7b17ffa5818555a2c04066f4a0a9e17d830abc9" +dependencies = [ + "async-stream", + "async-trait", + "chrono", + "futures", + "futures-util", + "once_cell", + "ouroboros", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-strum", + "serde", + "serde_json", + "sqlx", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c199fa8630b1e195d7aef24ce8944af8f4ced67c4eccffd8926453b59f2565a1" +dependencies = [ + "bae", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sea-query" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9088ff96158860a75d98a85a654fdd9d97b10515773af6d87339bfc48258c800" +dependencies = [ + "chrono", + "rust_decimal", + "sea-query-derive", + "serde_json", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34cdc022b4f606353fe5dc85b09713a04e433323b70163e81513b141c6ae6eb5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", + "thiserror", +] + +[[package]] +name = "sea-strum" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391d06a6007842cfe79ac6f7f53911b76dfd69fc9a6769f1cf6569d12ce20e1b" +dependencies = [ + "sea-strum_macros", +] + +[[package]] +name = "sea-strum_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b4397b825df6ccf1e98bcdabef3bbcfc47ff5853983467850eeab878384f21" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "security-framework" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09d3c15d814eda1d6a836f2f2b56a6abc1446c8a34351cb3180d3db92ffe4ce" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" dependencies = [ "bitflags", "core-foundation", @@ -1689,9 +1934,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e90dd10c41c6bfc633da6e0c659bd25d31e0791e5974ac42970267d59eba87f7" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" dependencies = [ "core-foundation-sys", "libc", @@ -1747,23 +1992,34 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.3", +] + [[package]] name = "sha2" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] @@ -1773,8 +2029,8 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.9.0", + "digest 0.9.0", "keccak", "opaque-debug", ] @@ -1817,9 +2073,9 @@ checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "socket2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f82496b90c36d70af5fcd482edaa2e0bd16fade569de1330405fecbbdac736b" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", "winapi", @@ -1884,13 +2140,15 @@ dependencies = [ "log", "md-5", "memchr", + "num-bigint", "once_cell", "parking_lot", "percent-encoding", "rand", + "rust_decimal", "serde", "serde_json", - "sha-1", + "sha-1 0.9.8", "sha2", "smallvec", "sqlformat", @@ -1935,11 +2193,17 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "string_cache" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6" +checksum = "33994d0838dc2d152d17a62adf608a869b5e846b65b389af7f3dbc1de45c5b26" dependencies = [ "lazy_static", "new_debug_unreachable", @@ -2085,9 +2349,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.16.1" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" dependencies = [ "bytes", "libc", @@ -2097,6 +2361,7 @@ dependencies = [ "once_cell", "pin-project-lite", "signal-hook-registry", + "socket2", "tokio-macros", "winapi", ] @@ -2147,18 +2412,32 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5651b5f6860a99bd1adb59dbfe1db8beb433e73709d9032b413a77e2fb7c066a" +checksum = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e" dependencies = [ "futures-core", "futures-util", "pin-project", "pin-project-lite", "tokio", - "tokio-util", + "tokio-util 0.7.0", "tower-layer", "tower-service", "tracing", @@ -2197,9 +2476,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d8d93354fe2a8e50d5953f5ae2e47a3fc2ef03292e7ea46e3cc38f549525fb9" +checksum = "f6c650a8ef0cd2dd93736f033d21cbd1224c5a967aa0c258d00fcf7dafef9b9f" dependencies = [ "cfg-if", "log", @@ -2242,9 +2521,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74786ce43333fcf51efe947aed9718fbe46d5c7328ec3f1029e818083966d9aa" +checksum = "9e0ab7bdc962035a87fba73f3acca9b8a8d0034c2e6f60b84aeaaddddc155dce" dependencies = [ "ansi_term", "lazy_static", @@ -2299,9 +2578,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "unicode-xid" diff --git a/Cargo.toml b/Cargo.toml index 6112a67..8bddff0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,12 +6,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] +members = [".", "entity"] + [dependencies] anyhow = "1.0.53" axum = { version = "0.4.5", features = ["headers", "http2", "multipart"] } -bincode = "1.3.3" chrono = { version = "0.4.19", features = ["serde"] } dotenv = "0.15.0" +entity = { path = "entity" } +fieldfilter = "0.1.0" lazy_static = "1.4.0" lettre = { version = "0.10.0-rc.4", features = [ "tokio1", @@ -32,26 +36,24 @@ redis = { version = "0.21.5", features = [ "connection-manager", ], default-features = false } regex = "1.5.4" +sea-orm = { version = "0.6.0", features = [ + "macros", + "debug-print", + "runtime-tokio-native-tls", + "sqlx-postgres", +], default-features = false } serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" -sqlx = { version = "0.5.10", features = [ - "runtime-tokio-native-tls", - "postgres", - "uuid", - "chrono", - "json", - "macros", -] } thiserror = "1.0.30" -tokio = { version = "1.16.1", features = ["rt-multi-thread", "macros", "sync"] } -tower = "0.4.11" +tokio = { version = "1.17.0", features = ["rt-multi-thread", "macros", "sync"] } +tower = "0.4.12" tower-http = { version = "0.2.2", features = [ "add-extension", "trace", "cors", ] } -tracing = "0.1.30" -tracing-subscriber = { version = "0.3.8", features = ["env-filter"] } +tracing = "0.1.31" +tracing-subscriber = { version = "0.3.9", features = ["env-filter"] } ulid = { version = "0.5.0", features = ["serde", "uuid"] } uuid = { version = "0.8.2", features = ["serde", "v4"] } validator = { version = "0.14.0", features = ["derive"] } diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 0000000..4413b2f --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,33 @@ +[tasks.format] +install_crate = "rustfmt" +command = "cargo" +args = ["fmt", "--", "--emit=files"] + +[tasks.clean] +command = "cargo" +args = ["clean"] + +[tasks.build] +command = "cargo" +args = ["build"] +dependencies = ["clean"] + +[tasks.test] +command = "cargo" +args = ["test"] +dependencies = ["clean"] + +[tasks.create-db] +command = "sqlx" +args = ["database", "create"] +workspace = false + +[tasks.drop-db] +command = "sqlx" +args = ["database", "drop"] +workspace = false + +[tasks.generate-entities] +command = "sea-orm-cli" +args = ["generate", "entity", "-o", "entity/src", "--with-serde", "both"] +workspace = false diff --git a/entity/Cargo.toml b/entity/Cargo.toml new file mode 100644 index 0000000..4f3da89 --- /dev/null +++ b/entity/Cargo.toml @@ -0,0 +1,28 @@ + +[package] +name = "entity" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "entity" +path = "src/mod.rs" + +[dependencies] +bincode = "1.3.3" +oso = { git = "https://github.com/fairingrey/oso", branch = "rust-addl-interface", features = [ + "uuid-07", +] } +redis = { version = "0.21.5", features = [ + "aio", + "tokio-comp", + "connection-manager", +], default-features = false } +sea-orm = { version = "0.6.0", features = [ + "macros", + "debug-print", + "runtime-tokio-native-tls", + "sqlx-postgres", +], default-features = false } +serde = { version = "1.0.136", features = ["derive"] } diff --git a/src/macros.rs b/entity/src/macros.rs similarity index 100% rename from src/macros.rs rename to entity/src/macros.rs diff --git a/entity/src/mod.rs b/entity/src/mod.rs new file mode 100644 index 0000000..bd87764 --- /dev/null +++ b/entity/src/mod.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 + +pub mod macros; +pub mod prelude; + +pub mod sea_orm_active_enums; +pub mod user_account; diff --git a/entity/src/prelude.rs b/entity/src/prelude.rs new file mode 100644 index 0000000..c531f11 --- /dev/null +++ b/entity/src/prelude.rs @@ -0,0 +1,3 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 + +pub use super::user_account::Entity as UserAccount; diff --git a/entity/src/sea_orm_active_enums.rs b/entity/src/sea_orm_active_enums.rs new file mode 100644 index 0000000..bf5527c --- /dev/null +++ b/entity/src/sea_orm_active_enums.rs @@ -0,0 +1,24 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 + +use oso::PolarClass; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, PolarClass, +)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "user_role")] +pub enum UserRole { + #[sea_orm(string_value = "admin")] + Admin, + #[sea_orm(string_value = "contributor")] + Contributor, + #[sea_orm(string_value = "creator")] + Creator, + #[sea_orm(string_value = "maintainer")] + Maintainer, + #[sea_orm(string_value = "member")] + Member, + #[sea_orm(string_value = "moderator")] + Moderator, +} diff --git a/entity/src/user_account.rs b/entity/src/user_account.rs new file mode 100644 index 0000000..938b043 --- /dev/null +++ b/entity/src/user_account.rs @@ -0,0 +1,41 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 + +use super::sea_orm_active_enums::UserRole; +use oso::PolarClass; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, PolarClass)] +#[sea_orm(table_name = "user_account")] +#[polar(class_name = "User")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + #[polar(attribute)] + pub id: Uuid, + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, + #[sea_orm(column_type = "Text")] + #[polar(attribute)] + pub name: String, + #[polar(attribute)] + pub email: String, + #[polar(attribute)] + pub role: UserRole, + #[sea_orm(column_type = "Text")] + pub password: String, + #[polar(attribute)] + pub verified: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation {} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + panic!("No RelationDef") + } +} + +impl ActiveModelBehavior for ActiveModel {} + +crate::impl_redis_rv!(Model); diff --git a/migrations/20220211233308_create_user_account.down.sql b/migrations/20220211233308_create_user_account.down.sql new file mode 100644 index 0000000..dad796a --- /dev/null +++ b/migrations/20220211233308_create_user_account.down.sql @@ -0,0 +1,4 @@ +-- Add down migration script here +DROP TABLE IF EXISTS user_account; + +DROP TYPE IF EXISTS user_role; diff --git a/migrations/20220211233308_create_users.up.sql b/migrations/20220211233308_create_user_account.up.sql similarity index 61% rename from migrations/20220211233308_create_users.up.sql rename to migrations/20220211233308_create_user_account.up.sql index 6a3a989..689712f 100644 --- a/migrations/20220211233308_create_users.up.sql +++ b/migrations/20220211233308_create_user_account.up.sql @@ -1,16 +1,16 @@ -- Add up migration script here -CREATE TYPE role AS ENUM ('admin', 'moderator', 'maintainer', 'creator', 'contributor', 'member'); +CREATE TYPE user_role AS ENUM ('admin', 'moderator', 'maintainer', 'creator', 'contributor', 'member'); -CREATE TABLE users ( +CREATE TABLE user_account ( id UUID PRIMARY KEY NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp, updated_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp, name TEXT NOT NULL, email VARCHAR(254) NOT NULL, UNIQUE (name, email), - role role NOT NULL DEFAULT 'member', + role user_role NOT NULL DEFAULT 'member', password TEXT NOT NULL, verified BOOLEAN NOT NULL DEFAULT FALSE ); -SELECT manage_updated_at('users'); +SELECT manage_updated_at('user_account'); diff --git a/migrations/20220211233308_create_users.down.sql b/migrations/20220211233308_create_users.down.sql deleted file mode 100644 index f384ada..0000000 --- a/migrations/20220211233308_create_users.down.sql +++ /dev/null @@ -1,4 +0,0 @@ --- Add down migration script here -DROP TABLE IF EXISTS users; - -DROP TYPE IF EXISTS role; diff --git a/polar/users.polar b/polar/users.polar index a710560..63bd806 100644 --- a/polar/users.polar +++ b/polar/users.polar @@ -1,55 +1,46 @@ # User rules ## admins and mods can read all of a user's fields except passwords -allow_field(user: User, "READ", _other_user: User, field) if +allow_field(user: User, _: Read, _other_user: User, field) if user.role in [Role::Admin, Role::Moderator] and - field in ["name", "created_at", "updated_at", "role", "email"]; + field in ["created_at", "updated_at", "name", "email", "role"]; ## users can read all of their own fields except passwords -allow_field(user: User, "READ", other_user: User, field) if +allow_field(user: User, _: Read, other_user: User, field) if user.id == other_user.id and - field in ["name", "created_at", "updated_at", "role", "email"]; + field in ["created_at", "updated_at", "name", "email", "role"]; -## anyone can read ids, names, created_at, and role of other users -allow_field(_, "READ", _other_user: User, field: String) if - field in ["name", "created_at", "role"]; +## anyone can read names, created_at, and role of other users +allow_field(_, _: Read, _other_user: User, field: String) if + field in ["created_at", "name", "role"]; -## admins can change user names or emails -allow_field(user: User, "UPDATE", _other_user: User, field: String) if +## admins can change everything for a user except the password +allow(user: User, update: UpdateUser, _other_user: User) if user.role = Role::Admin and - field in ["name", "email"]; + update.password = nil; ## moderators can do the same but only to other users of role below them -allow_field(user: User, "UPDATE", other_user: User, field: String) if +## they cannot assign roles higher than or equal to themselves +allow(user: User, update: UpdateUser, other_user: User) if user.role = Role::Moderator and other_user.role in [Role::Maintainer, Role::Creator, Role::Contributor, Role::Member] and - field in ["name", "email"]; + update.role in [Role::Maintainer, Role::Creator, Role::Contributor, Role::Member, nil] and + update.password = nil; + +## users can update themselves but not their role +allow(user: User, update: UpdateUser, other_user: User) if + user.id = other_user.id and + changes.role = nil; ## admins can delete other users -allow(user: User, "DELETE", _other_user: User) if +allow(user: User, _: Delete, _other_user: User) if user.role = Role::Admin; ## moderators can also, but again only to other users of role below them -allow(user: User, "DELETE", other_user: User) if +allow(user: User, _: Delete, other_user: User) if user.role = Role::Moderator and other_user.role in [Role::Maintainer, Role::Creator, Role::Contributor, Role::Member]; -# role specific stuff - -## admins can assign any role -allow_assign_role(user: User, _role: Role) if - user.role = Role::Admin; - -## Moderators can only assign roles below them -allow_assign_role(user: User, role: Role) if - user.role = Role::Moderator and - role in [Role::Maintainer, Role::Creator, Role::Contributor, Role::Member]; - -## users can update themselves but only on certain fields -allow_field(user: User, "UPDATE", other_user: User, field) if - field in ["name", "email", "password"] and - user.id = other_user.id; - ## users can delete themselves -allow(user: User, "DELETE", other_user: User) if +allow(user: User, _: Delete, other_user: User) if user.id = other_user.id; diff --git a/src/actions.rs b/src/actions.rs new file mode 100644 index 0000000..32c087b --- /dev/null +++ b/src/actions.rs @@ -0,0 +1,61 @@ +//! CRUD action-like resources +use anyhow::Result; +use entity::sea_orm_active_enums::UserRole; +use oso::{Oso, PolarClass}; +use serde::Deserialize; +use validator::Validate; + +use crate::constants::RE_USERNAME; + +/// The "READ" action. Because there is no data pertinent to this action it is a unit struct. +#[derive(Debug, Clone, Copy, PolarClass)] +pub(crate) struct Read; + +/// The "DELETE" action. Because there is no data pertinent to this action it is a unit struct. +#[derive(Debug, Clone, Copy, PolarClass)] +pub(crate) struct Delete; + +/// The action by which a user is updated. Can be understood as a sort of changeset. +/// +/// This struct in particular doubles up for multiple use cases. It's used for PUT `/user/:id` form responses, +/// in authorization rules, and also for updates to the ORM. +#[derive(Debug, Clone, Validate, Deserialize, PolarClass)] +pub(crate) struct UpdateUser { + #[validate( + length( + min = 5, + max = 32, + message = "Minimum length is 5 characters, maximum is 32" + ), + regex( + path = "RE_USERNAME", + message = "Can only contain letters, numbers, dashes (-), periods (.), and underscores (_)" + ) + )] + #[polar(attribute)] + pub(crate) name: Option, + #[validate(email(message = "Must be a valid email address."))] + #[polar(attribute)] + pub(crate) email: Option, + #[polar(attribute)] + pub(crate) role: Option, +} + +/// Attempt to create a new oso instance for managing authorization schemes. +pub(crate) fn try_register_oso() -> Result { + let mut oso = Oso::new(); + + // NOTE: load classes here + oso.register_class(entity::user_account::Model::get_polar_class())?; + oso.register_class(UserRole::get_polar_class())?; + + // action classes in this module should be loaded here too + oso.register_class(Read::get_polar_class())?; + oso.register_class(Delete::get_polar_class())?; + oso.register_class(UpdateUser::get_polar_class())?; + + // NOTE: load oso rule files here + oso.load_files(vec!["polar/users.polar"])?; + + Ok(oso) +} diff --git a/src/auth.rs b/src/auth.rs index c0fce96..1649084 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -8,7 +8,6 @@ use redis::AsyncCommands; use crate::{ constants::{SESSION_COOKIE_NAME, SESSION_DURATION_SECS, SESSION_KEY_PREFIX}, error::MixiniError, - models::User, server::State, }; @@ -18,7 +17,7 @@ use crate::{ /// with the value being the unprefixed key. #[derive(Debug)] pub(crate) enum Auth { - KnownUser(User), + KnownUser(entity::user_account::Model), UnknownUser, } diff --git a/src/constants.rs b/src/constants.rs index a3cc699..e3873ee 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,10 +1,14 @@ //! Constants use lazy_static::lazy_static; +use regex::Regex; lazy_static! { pub(crate) static ref DOMAIN: String = std::env::var("DOMAIN").expect("DOMAIN is not set in env"); + pub(crate) static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9\.\-_]+$").unwrap(); + pub(crate) static ref RE_PASSWORD: Regex = + Regex::new(r"^[a-zA-Z0-9]*[0-9][a-zA-Z0-9]*$").unwrap(); } // for authorized sessions diff --git a/src/error.rs b/src/error.rs index 674e41b..343e041 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,9 +13,6 @@ pub(crate) enum MixiniError { #[error(transparent)] AxumFormRejection(#[from] axum::extract::rejection::FormRejection), - #[error(transparent)] - BincodeError(#[from] bincode::Error), - #[error(transparent)] JsonError(#[from] serde_json::Error), @@ -29,10 +26,10 @@ pub(crate) enum MixiniError { RedisError(#[from] redis::RedisError), #[error(transparent)] - SmtpError(#[from] lettre::transport::smtp::Error), + DatabaseError(#[from] sea_orm::DbErr), #[error(transparent)] - SqlxError(#[from] sqlx::Error), + SmtpError(#[from] lettre::transport::smtp::Error), #[error(transparent)] ValidationError(#[from] validator::ValidationErrors), @@ -45,13 +42,6 @@ impl IntoResponse for MixiniError { fn into_response(self) -> Response { match self { MixiniError::AxumFormRejection(_) => (StatusCode::BAD_REQUEST, self.to_string()), - MixiniError::BincodeError(e) => { - tracing::debug!("Bincode error occurred: {:?}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - INTERNAL_SERVER_ERROR_MESSAGE.into(), - ) - } MixiniError::JsonError(e) => { tracing::debug!("Json error occurred: {:?}", e); ( @@ -87,10 +77,10 @@ impl IntoResponse for MixiniError { INTERNAL_SERVER_ERROR_MESSAGE.into(), ) } - MixiniError::SqlxError(ref e) => match e { - sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, self.to_string()), + MixiniError::DatabaseError(ref e) => match e { + sea_orm::DbErr::RecordNotFound(_) => (StatusCode::NOT_FOUND, self.to_string()), _ => { - tracing::debug!("Sqlx error occurred: {:?}", e); + tracing::debug!("SeaORM error occurred: {:?}", e); ( StatusCode::INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR_MESSAGE.into(), diff --git a/src/handlers/login.rs b/src/handlers/login.rs index d3258e8..4187899 100644 --- a/src/handlers/login.rs +++ b/src/handlers/login.rs @@ -4,17 +4,21 @@ use axum::{ headers::Cookie, http::{header, Response, StatusCode}, }; +use entity::{prelude::*, user_account}; use libreauth::pass::HashBuilder; use redis::AsyncCommands; +use sea_orm::{entity::*, prelude::*}; use serde::Deserialize; use std::sync::Arc; use validator::Validate; use crate::{ - constants::{DOMAIN, SESSION_COOKIE_NAME, SESSION_DURATION_SECS, SESSION_KEY_PREFIX}, + constants::{ + DOMAIN, RE_PASSWORD, RE_USERNAME, SESSION_COOKIE_NAME, SESSION_DURATION_SECS, + SESSION_KEY_PREFIX, + }, error::MixiniError, - handlers::{ValidatedForm, RE_PASSWORD, RE_USERNAME}, - models::User, + handlers::ValidatedForm, server::State, utils::{ pass::{HASHER, PWD_SCHEME_VERSION}, @@ -53,23 +57,25 @@ pub(crate) struct LoginForm { /// Handler for `POST /login` pub(crate) async fn login( - ValidatedForm(input): ValidatedForm, + ValidatedForm(login): ValidatedForm, state: Extension>, ) -> Result, MixiniError> { - let mut db_conn = state.db_pool.acquire().await?; - - let user = sqlx::query_as!( - User, - r#"SELECT id, created_at, updated_at, name, email, role as "role:_", password, verified - FROM users WHERE name = $1"#, - input.name - ) - .fetch_one(&mut db_conn) - .await?; + let user = if let Some(user) = UserAccount::find() + .filter(user_account::Column::Name.eq(login.name)) + .one(&state.db) + .await? + { + user + } else { + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::empty()) + .unwrap()); + }; let checker = HashBuilder::from_phc(&user.password).unwrap(); - if !checker.is_valid(&input.password) { + if !checker.is_valid(&login.password) { return Ok(Response::builder() .status(StatusCode::UNAUTHORIZED) .body(Body::empty()) @@ -77,15 +83,10 @@ pub(crate) async fn login( }; if checker.needs_update(Some(PWD_SCHEME_VERSION)) { // password needs to be updated - let hashed_password = HASHER.hash(&input.password).expect("hasher failed hashing"); - sqlx::query_as!( - User, - r#"UPDATE users SET password = $2 WHERE id = $1"#, - user.id, - hashed_password - ) - .execute(&mut db_conn) - .await?; + let hashed_password = HASHER.hash(&login.password).expect("hasher failed hashing"); + let mut user: user_account::ActiveModel = user.clone().into(); + user.password = Set(hashed_password); + user.update(&state.db).await?; } // create session entry in redis diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index de8abed..25fc62b 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -4,8 +4,6 @@ use axum::{ extract::{Form, FromRequest, RequestParts}, BoxError, }; -use lazy_static::lazy_static; -use regex::Regex; use serde::de::DeserializeOwned; use validator::Validate; @@ -17,12 +15,6 @@ pub(crate) mod user; pub(crate) use login::*; pub(crate) use user::*; -lazy_static! { - pub(crate) static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9\.\-_]+$").unwrap(); - pub(crate) static ref RE_PASSWORD: Regex = - Regex::new(r"^[a-zA-Z0-9]*[0-9][a-zA-Z0-9]*$").unwrap(); -} - /// A validated form with some input. #[derive(Debug, Clone, Copy, Default)] pub(crate) struct ValidatedForm(pub(crate) T); diff --git a/src/handlers/user.rs b/src/handlers/user.rs index 459cfb3..4ecc9c4 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -5,8 +5,10 @@ use axum::{ headers::Cookie, http::{Response, StatusCode}, }; -use chrono::{DateTime, Utc}; +use entity::{prelude::*, sea_orm_active_enums::UserRole, user_account}; +use fieldfilter::FieldFilterable; use redis::AsyncCommands; +use sea_orm::{entity::*, prelude::*, query::*}; use serde::{Deserialize, Serialize}; use std::{collections::HashSet, sync::Arc}; use ulid::Ulid; @@ -14,20 +16,21 @@ use uuid::Uuid; use validator::Validate; use crate::{ + actions::{Delete, Read, UpdateUser}, auth::Auth, constants::{ - SESSION_COOKIE_NAME, SESSION_KEY_PREFIX, VERIFY_EXPIRY_SECONDS, VERIFY_KEY_PREFIX, + RE_PASSWORD, RE_USERNAME, SESSION_COOKIE_NAME, SESSION_KEY_PREFIX, VERIFY_EXPIRY_SECONDS, + VERIFY_KEY_PREFIX, }, error::MixiniError, - handlers::{ValidatedForm, RE_PASSWORD, RE_USERNAME}, - models::{Role, User}, + handlers::ValidatedForm, server::State, utils::{mail::send_email_verification_request, pass::HASHER, RKeys}, }; /// The form input for `POST /user` #[derive(Debug, Validate, Deserialize)] -pub(crate) struct CreateUserForm { +pub(crate) struct CreateUser { /// The provided username. #[validate( length( @@ -59,38 +62,6 @@ pub(crate) struct CreateUserForm { pub(crate) password: String, } -/// The form input for `PUT /user/:id` -#[derive(Debug, Validate, Deserialize)] -pub(crate) struct UpdateUserForm { - #[validate( - length( - min = 5, - max = 32, - message = "Minimum length is 5 characters, maximum is 32" - ), - regex( - path = "RE_USERNAME", - message = "Can only contain letters, numbers, dashes (-), periods (.), and underscores (_)" - ) - )] - pub(crate) name: Option, - #[validate(email(message = "Must be a valid email address."))] - pub(crate) email: Option, - pub(crate) role: Option, - #[validate( - length( - min = 8, - max = 128, - message = "Minimum length is 8 characters, maximum is 128" - ), - regex( - path = "RE_PASSWORD", - message = "Must be alphanumeric and contain at least one number." - ) - )] - pub(crate) password: Option, -} - /// The form input for `PUT /user/verify` #[derive(Debug, Validate, Deserialize)] pub(crate) struct VerifyForm { @@ -101,63 +72,58 @@ pub(crate) struct VerifyForm { pub(crate) key: String, } -/// The response output for `GET /user/:name` -#[derive(Debug, Serialize)] -pub(crate) struct UserResponse { +/// The response for `GET /user/:id` +#[derive(Debug, Serialize, FieldFilterable)] +#[field_filterable_on(user_account::Model)] +pub(crate) struct GetUserResponse { id: Uuid, - created_at: Option>, - updated_at: Option>, - name: String, + created_at: Option, + updated_at: Option, + name: Option, email: Option, - role: Role, + role: Option, } /// Handler for `POST /user` pub(crate) async fn create_user( - ValidatedForm(input): ValidatedForm, + ValidatedForm(create_user): ValidatedForm, state: Extension>, ) -> Result, MixiniError> { // check if either this username or email already exist in our database - let mut db_conn = state.db_pool.acquire().await?; - - // shadow - let (name, email, password) = (input.name, input.email, input.password); - - let conflicts = sqlx::query!( - r#"SELECT id FROM users WHERE name = $1 OR email = $2"#, - name, - email, - ) - .fetch_optional(&mut db_conn) - .await?; + let conflicts = UserAccount::find() + .filter( + Condition::any() + .add(user_account::Column::Name.eq(create_user.name.to_owned())) + .add(user_account::Column::Email.eq(create_user.email.to_owned())), + ) + .all(&state.db) + .await?; - if conflicts.is_some() { - let res = Response::builder() + if !conflicts.is_empty() { + Ok(Response::builder() .status(StatusCode::CONFLICT) .body(Body::from("A user with this name or email already exists.")) - .unwrap(); - return Ok(res); + .unwrap()) + } else { + // create new user account in db + let id = Uuid::from(Ulid::new()); + let password = HASHER + .hash(&create_user.password) + .expect("hasher failed hashing"); + + let new_account = user_account::ActiveModel { + id: Set(id), + name: Set(create_user.name), + email: Set(create_user.email), + password: Set(password), + ..Default::default() + }; + new_account.insert(&state.db).await?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::empty()) + .unwrap()) } - - // create new user in db - let id = Uuid::from(Ulid::new()); - let password = HASHER.hash(&password).expect("hasher failed hashing"); - - sqlx::query_as!( - User, - r#"INSERT INTO users (id, name, email, password) VALUES ($1, $2, $3, $4)"#, - id, - name, - email, - password, - ) - .execute(&mut db_conn) - .await?; - - Ok(Response::builder() - .status(StatusCode::OK) - .body(Body::empty()) - .unwrap()) } /// Handler for `GET /user/:id` @@ -166,59 +132,29 @@ pub(crate) async fn get_user( state: Extension>, auth: Auth, ) -> Result, MixiniError> { - let mut db_conn = state.db_pool.acquire().await?; + let maybe_user = UserAccount::find_by_id(id).one(&state.db).await?; - match sqlx::query_as!( - User, - r#"SELECT id, created_at, updated_at, name, email, role as "role:_", password, verified - FROM users WHERE id = $1"#, - id - ) - .fetch_optional(&mut db_conn) - .await? - { + match maybe_user { Some(user) => { let authorized_fields: HashSet = if let Auth::KnownUser(this_user) = auth { state .oso .lock() .await - .authorized_fields(this_user, "READ", user.to_owned())? + .authorized_fields(this_user, Read, user.to_owned())? } else { state .oso .lock() .await - .authorized_fields("guest", "READ", user.to_owned())? + .authorized_fields("guest", Read, user.to_owned())? }; - let created_at = if authorized_fields.contains("created_at") { - Some(user.created_at) - } else { - None - }; - let updated_at = if authorized_fields.contains("updated_at") { - Some(user.updated_at) - } else { - None - }; - let email = if authorized_fields.contains("email") { - Some(user.email) - } else { - None - }; + let res_body: GetUserResponse = FieldFilterable::field_filter(user, authorized_fields); - let user_response = UserResponse { - id: user.id, - created_at, - updated_at, - name: user.name, - email, - role: user.role, - }; Ok(Response::builder() .status(StatusCode::OK) - .body(Body::from(serde_json::to_vec(&user_response)?)) + .body(Body::from(serde_json::to_vec(&res_body)?)) .unwrap()) } None => Ok(Response::builder() @@ -231,80 +167,49 @@ pub(crate) async fn get_user( /// Handler for `PUT /user/:id` pub(crate) async fn update_user( Path(id): Path, - ValidatedForm(input): ValidatedForm, + ValidatedForm(update_user): ValidatedForm, state: Extension>, auth: Auth, ) -> Result, MixiniError> { match auth { Auth::KnownUser(this_user) => { - let mut db_conn = state.db_pool.acquire().await?; - - let user = if let Some(user) = sqlx::query_as!( - User, - r#"SELECT id, created_at, updated_at, name, email, role as "role:_", password, verified - FROM users WHERE id = $1"#, - id - ) - .fetch_optional(&mut db_conn) - .await? { user } else { - return Ok(Response::builder() - .status(StatusCode::FORBIDDEN) - .body(Body::empty()) - .unwrap()); - }; - - let mut values = Vec::new(); + let user = if let Some(user) = UserAccount::find_by_id(id).one(&state.db).await? { + user + } else { + return Ok(Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::empty()) + .unwrap()); + }; - if let Some(role) = input.role { - if state - .oso - .lock() - .await - .query_rule("allow_assign_role", (this_user.to_owned(), role))? - .next() - .is_some() - { - values.push(format!("role = '{}'", role)); - } else { - return Ok(Response::builder() - .status(StatusCode::FORBIDDEN) - .body(Body::empty()) - .unwrap()); + if state.oso.lock().await.is_allowed( + this_user, + update_user.to_owned(), + user.to_owned(), + )? { + // TODO: When UpdateUser -> ActiveModel works, change this + // https://github.com/SeaQL/sea-orm/issues/547 + let mut user: user_account::ActiveModel = user.into(); + if let Some(name) = update_user.name { + user.name = Set(name); } - } - - let authorized_fields: HashSet = - state - .oso - .lock() - .await - .authorized_fields(this_user, "READ", user.to_owned())?; - - if let Some(name) = input.name { - if authorized_fields.contains("name") { - values.push(format!("name = {}", name)); + if let Some(email) = update_user.email { + user.email = Set(email); } - } - - if let Some(email) = input.email { - if authorized_fields.contains("email") { - values.push(format!("email = {}", email)); + if let Some(role) = update_user.role { + user.role = Set(role); } + user.update(&state.db).await?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::empty()) + .unwrap()) + } else { + Ok(Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::empty()) + .unwrap()) } - - let values = values.join(","); - - // TODO: Requires testing, this is a dynamic query - sqlx::query(r#"UPDATE users SET $2 WHERE id = $1"#) - .bind(id) - .bind(values) - .execute(&mut db_conn) - .await?; - - Ok(Response::builder() - .status(StatusCode::OK) - .body(Body::empty()) - .unwrap()) } Auth::UnknownUser => Ok(Response::builder() .status(StatusCode::UNAUTHORIZED) @@ -322,16 +227,9 @@ pub(crate) async fn delete_user( ) -> Result, MixiniError> { match auth { Auth::KnownUser(this_user) => { - let mut db_conn = state.db_pool.acquire().await?; - - let user = if let Some(user) = sqlx::query_as!( - User, - r#"SELECT id, created_at, updated_at, name, email, role as "role:_", password, verified - FROM users WHERE id = $1"#, - id - ) - .fetch_optional(&mut db_conn) - .await? { user } else { + let user = if let Some(user) = UserAccount::find_by_id(id).one(&state.db).await? { + user + } else { return Ok(Response::builder() .status(StatusCode::FORBIDDEN) .body(Body::empty()) @@ -342,11 +240,9 @@ pub(crate) async fn delete_user( .oso .lock() .await - .is_allowed(this_user, "DELETE", user.to_owned())? + .is_allowed(this_user, Delete, user.to_owned())? { - sqlx::query!(r#"DELETE FROM users WHERE id = $1"#, user.id) - .execute(&mut db_conn) - .await?; + user.delete(&state.db).await?; // also delete cookie in store let base_key = cookie.get(SESSION_COOKIE_NAME).expect("cookie monster!?"); @@ -417,26 +313,30 @@ pub(crate) async fn create_verify_user( /// Handler for `PUT /user/verify` pub(crate) async fn update_verify_user( - ValidatedForm(input): ValidatedForm, + ValidatedForm(verify): ValidatedForm, state: Extension>, ) -> Result, MixiniError> { // value is user id - let prefixed_key = format!("{}{}", VERIFY_KEY_PREFIX, &input.key); + let prefixed_key = format!("{}{}", VERIFY_KEY_PREFIX, &verify.key); let maybe_id: Option = state.redis_manager.to_owned().get(&prefixed_key).await?; match maybe_id { Some(id) => { let id: Uuid = Uuid::parse_str(&id).map_err(|e| format_err!(e))?; - let mut db_conn = state.db_pool.acquire().await?; + // NOTE: Normally this should always be Some(user) but better safe than sorry + let user = if let Some(user) = UserAccount::find_by_id(id).one(&state.db).await? { + user + } else { + return Ok(Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::empty()) + .unwrap()); + }; + let mut user: user_account::ActiveModel = user.into(); - sqlx::query_as!( - User, - r#"UPDATE users SET verified = TRUE WHERE id = $1"#, - id - ) - .execute(&mut db_conn) - .await?; + user.verified = Set(true); + user.update(&state.db).await?; Ok(Response::builder() .status(StatusCode::OK) diff --git a/src/main.rs b/src/main.rs index cf2edb3..5977df9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,12 +6,11 @@ rust_2021_compatibility )] +pub(crate) mod actions; pub(crate) mod auth; pub(crate) mod constants; pub(crate) mod error; pub(crate) mod handlers; -pub(crate) mod macros; -pub(crate) mod models; pub(crate) mod server; pub(crate) mod utils; diff --git a/src/models/mod.rs b/src/models/mod.rs deleted file mode 100644 index b3e3c5c..0000000 --- a/src/models/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Database models -//! -//! Note that these may have to be updated by hand. - -pub(crate) mod user; - -pub(crate) use user::*; diff --git a/src/models/user.rs b/src/models/user.rs deleted file mode 100644 index 7da53e0..0000000 --- a/src/models/user.rs +++ /dev/null @@ -1,79 +0,0 @@ -use serde::{Deserialize, Serialize}; -use sqlx::types::chrono::{DateTime, Utc}; -use std::fmt; -use uuid::Uuid; - -use crate::{handlers::UpdateUserForm, impl_redis_rv}; - -/// User roles -#[derive( - Debug, Clone, Copy, Eq, PartialEq, sqlx::Type, oso::PolarClass, Serialize, Deserialize, -)] -#[sqlx(type_name = "role", rename_all = "lowercase")] -pub(crate) enum Role { - Admin, - Moderator, - Maintainer, - Creator, - Contributor, - Member, -} - -impl fmt::Display for Role { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = format!("{:?}", self).to_ascii_lowercase(); - write!(f, "{}", s) - } -} - -/// User model -#[derive(Debug, Clone, sqlx::FromRow, oso::PolarClass, Serialize, Deserialize)] -pub(crate) struct User { - #[polar(attribute)] - pub(crate) id: Uuid, - pub(crate) created_at: DateTime, - pub(crate) updated_at: DateTime, - #[polar(attribute)] - pub(crate) name: String, - #[polar(attribute)] - pub(crate) email: String, - #[polar(attribute)] - pub(crate) role: Role, - /// The password in hashed PHC form, as represented in the database - #[polar(attribute)] - pub(crate) password: String, - #[polar(attribute)] - pub(crate) verified: bool, -} - -/// User model but all fields are options -#[derive(Debug, Clone, oso::PolarClass, Serialize, Deserialize)] -pub(crate) struct UserOptional { - #[polar(attribute)] - pub(crate) name: Option, - pub(crate) created_at: Option>, - pub(crate) updated_at: Option>, - #[polar(attribute)] - pub(crate) email: Option, - #[polar(attribute)] - pub(crate) role: Option, - #[polar(attribute)] - pub(crate) password: Option, - pub(crate) verified: Option, -} - -impl From for UserOptional { - fn from(form: UpdateUserForm) -> Self { - Self { - name: form.name, - email: form.email, - created_at: None, - updated_at: None, - role: form.role, - password: form.password, - verified: None, - } - } -} - -impl_redis_rv!(User, Role); diff --git a/src/server.rs b/src/server.rs index b0bbac8..79b502d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -7,8 +7,8 @@ use lettre::{ transport::smtp::authentication::{Credentials, Mechanism}, AsyncSmtpTransport, Tokio1Executor, }; -use oso::{Oso, PolarClass}; -use sqlx::PgPool; +use oso::Oso; +use sea_orm::{Database, DatabaseConnection}; use std::{str::FromStr, sync::Arc}; use tokio::sync::Mutex; use tower::ServiceBuilder; @@ -17,12 +17,12 @@ use tower_http::{ trace::TraceLayer, }; -use crate::handlers; +use crate::{actions::try_register_oso, handlers}; #[derive(Clone)] pub(crate) struct State { pub(crate) oso: Arc>, - pub(crate) db_pool: PgPool, + pub(crate) db: DatabaseConnection, pub(crate) redis_manager: redis::aio::ConnectionManager, pub(crate) mailsender: AsyncSmtpTransport, } @@ -31,7 +31,7 @@ impl State { /// Attempt to create a new State instance pub(crate) async fn try_new() -> Result { let oso = Arc::new(Mutex::new(try_register_oso()?)); - let db_pool = PgPool::connect(&std::env::var("DATABASE_URL")?).await?; + let db = Database::connect(&std::env::var("DATABASE_URL")?).await?; let redis_manager = redis::Client::open(std::env::var("REDIS_URL")?)? .get_tokio_connection_manager() .await?; @@ -48,29 +48,13 @@ impl State { Ok(State { oso, - db_pool, + db, redis_manager, mailsender, }) } } -/// Attempt to create a new oso instance for managing authorization schemes. -fn try_register_oso() -> Result { - use crate::models::*; - - let mut oso = Oso::new(); - - // NOTE: load classes here - oso.register_class(User::get_polar_class())?; - oso.register_class(Role::get_polar_class())?; - - // NOTE: load oso rule files here - oso.load_files(vec!["polar/users.polar"])?; - - Ok(oso) -} - /// Attempt to setup the CORS layer. fn try_cors_layer() -> Result { use axum::http::Method;