From bcfeb8a72e0837fdf05db9ef05a76261c224583e Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Mon, 04 Oct 2021 02:38:21 +0100 Subject: [PATCH] OAuth support in the backend --- Cargo.lock | 657 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-fs/Cargo.toml | 2 ++ chartered-web/Cargo.toml | 5 +++++ chartered-db/src/lib.rs | 6 ++++-- chartered-db/src/users.rs | 26 ++++++++++++++++++++++++++ chartered-frontend/src/index.sass | 18 +++++++++++++++++- chartered-frontend/src/useAuth.tsx | 6 +++++- chartered-frontend/src/util.tsx | 25 +++++++++++++++++++++++++ chartered-fs/src/lib.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- chartered-web/src/config.rs | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-web/src/main.rs | 135 ++++++++++++++++++++++++++++++++++++++------------------------------------------ book/src/getting-started/installation.md | 3 ++- chartered-frontend/src/pages/Login.tsx | 31 +++++++++++++++++++++---------- chartered-web/src/endpoints/cargo_api/mod.rs | 33 ++++++++++++++++++++++++++++----- chartered-web/src/endpoints/web_api/login.rs | 75 --------------------------------------------------------------------------- chartered-web/src/endpoints/web_api/mod.rs | 47 +++++++++++++++++++++++++++++++++++++++++------ chartered-web/src/endpoints/web_api/auth/mod.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-web/src/endpoints/web_api/auth/openid.rs | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-web/src/endpoints/web_api/auth/password.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ chartered-web/src/endpoints/web_api/crates/mod.rs | 30 +++++++++++++++++++++++++----- chartered-web/src/endpoints/web_api/organisations/mod.rs | 30 +++++++++++++++++++++++++----- 21 files changed, 1336 insertions(+), 214 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 59835fe..733132f 100644 --- a/Cargo.lock +++ a/Cargo.lock @@ -9,6 +9,16 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", + "rand_core", +] + +[[package]] name = "aes" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -115,6 +125,21 @@ "pbkdf2", "sha2", "zeroize", +] + +[[package]] +name = "biscuit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dee631cea28b00e115fd355a1adedc860b155096941dc01259969eabd434a37" +dependencies = [ + "chrono", + "data-encoding", + "num", + "once_cell", + "ring", + "serde", + "serde_json", ] [[package]] @@ -164,6 +189,12 @@ "cipher", "opaque-debug", ] + +[[package]] +name = "bumpalo" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" [[package]] name = "byteorder" @@ -188,6 +219,31 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha20" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b72a433d0cf2aef113ba70f62634c56fddb0f244e6377185c56a7cadbd8f91" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "chacha20poly1305" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b84ed6d1d5f7aa9bdde921a5090e0ca4d934d250ea3b402a5fab3a994e28a2a" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] [[package]] name = "chartered-db" @@ -222,8 +278,10 @@ version = "0.1.0" dependencies = [ "async-trait", + "itertools", "serde", "tokio", + "url", "uuid", ] @@ -269,7 +327,9 @@ version = "0.1.0" dependencies = [ "axum", + "base64", "bytes", + "chacha20poly1305", "chartered-db", "chartered-fs", "chartered-types", @@ -281,7 +341,10 @@ "log", "nom", "once_cell", + "openid", + "rand", "regex", + "reqwest", "serde", "serde_json", "sha2", @@ -319,6 +382,22 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb58b6451e8c2a812ad979ed1d83378caa5e927eef2622017a45f251457c2c9d" + +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" [[package]] name = "cpufeatures" @@ -487,6 +566,15 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +dependencies = [ + "cfg-if", +] + +[[package]] name = "env_logger" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -516,6 +604,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" @@ -662,7 +765,32 @@ "cfg-if", "libc", "wasi", +] + +[[package]] +name = "h2" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c06815895acec637cd6ed6e9662c935b866d20a106f8361892893a7d9234964" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "headers" @@ -773,6 +901,7 @@ "futures-channel", "futures-core", "futures-util", + "h2", "http", "http-body", "httparse", @@ -784,6 +913,46 @@ "tower-service", "tracing", "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", ] [[package]] @@ -803,6 +972,12 @@ dependencies = [ "cfg-if", ] + +[[package]] +name = "ipnet" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" [[package]] name = "itertools" @@ -818,6 +993,15 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] [[package]] name = "lazy_static" @@ -953,6 +1137,24 @@ checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ "winapi", +] + +[[package]] +name = "native-tls" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", ] [[package]] @@ -973,6 +1175,31 @@ checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" dependencies = [ "winapi", +] + +[[package]] +name = "num" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7a8e9be5e039e2ff869df49155f1c06bd01ade2117ec783e56ab0932b67a8f" +dependencies = [ + "num-bigint 0.3.3", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[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]] @@ -983,6 +1210,15 @@ dependencies = [ "autocfg", "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5" +dependencies = [ "num-traits", ] @@ -991,8 +1227,31 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" dependencies = [ "autocfg", + "num-bigint 0.3.3", + "num-integer", "num-traits", ] @@ -1026,6 +1285,57 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openid" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab30a9456b3484c408d9708b6f65b2bd834fdf22b73567775e1ca6de5524dd19" +dependencies = [ + "base64", + "biscuit", + "chrono", + "lazy_static", + "reqwest", + "serde", + "serde_json", + "thiserror", + "url", + "validator", +] + +[[package]] +name = "openssl" +version = "0.10.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "openssl-sys" +version = "0.9.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] [[package]] name = "option_set" @@ -1129,6 +1439,17 @@ version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb" + +[[package]] +name = "poly1305" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] [[package]] name = "ppv-lite86" @@ -1143,6 +1464,30 @@ checksum = "6ac25eee5a0582f45a67e837e350d784e7003bd29a5f460796772061ca49ffda" dependencies = [ "vcpkg", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] @@ -1261,6 +1606,65 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] [[package]] name = "ryu" @@ -1275,6 +1679,16 @@ checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", ] [[package]] @@ -1291,6 +1705,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] name = "serde" @@ -1318,6 +1755,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" dependencies = [ + "indexmap", "itoa", "ryu", "serde", @@ -1397,6 +1835,12 @@ "libc", "winapi", ] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "subtle" @@ -1422,6 +1866,20 @@ checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" [[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] name = "termcolor" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1490,7 +1948,7 @@ "hmac", "log", "md5", - "num-bigint", + "num-bigint 0.4.2", "num-integer", "pbkdf2", "rand", @@ -1525,7 +1983,22 @@ dependencies = [ "libc", "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +dependencies = [ + "tinyvec_macros", ] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" @@ -1556,6 +2029,16 @@ "proc-macro2", "quote", "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", ] [[package]] @@ -1689,6 +2172,21 @@ checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" [[package]] +name = "unicode-bidi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] name = "unicode-segmentation" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1705,6 +2203,35 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", + "serde", +] [[package]] name = "uuid" @@ -1713,8 +2240,47 @@ checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ "getrandom", + "serde", +] + +[[package]] +name = "validator" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d6937c33ec6039d8071bcf72933146b5bbe378d645d8fa59bdadabfc2a249" +dependencies = [ + "idna", + "lazy_static", + "regex", "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", + "validator_types", ] + +[[package]] +name = "validator_derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286b4497f270f59276a89ae0ad109d5f8f18c69b613e3fb22b61201aadb0c4d" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9680608df133af2c1ddd5eaf1ddce91d60d61b6bc51494ef326458365a470a" [[package]] name = "vcpkg" @@ -1756,6 +2322,84 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "web-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1785,6 +2429,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi", +] [[package]] name = "yasna" @@ -1793,7 +2446,7 @@ checksum = "e262a29d0e61ccf2b6190d7050d4b237535fc76ce4c1210d9caa316f71dffa75" dependencies = [ "bit-vec", - "num-bigint", + "num-bigint 0.4.2", ] [[package]] diff --git a/chartered-fs/Cargo.toml b/chartered-fs/Cargo.toml index 5467201..3709d6e 100644 --- a/chartered-fs/Cargo.toml +++ a/chartered-fs/Cargo.toml @@ -7,8 +7,10 @@ [dependencies] async-trait = "0.1" +itertools = "*" serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["fs", "io-util"] } +url = "2" uuid = { version = "0.8", features = ["v4", "serde"] } [dev-dependencies] diff --git a/chartered-web/Cargo.toml b/chartered-web/Cargo.toml index 25a8848..7ef5394 100644 --- a/chartered-web/Cargo.toml +++ a/chartered-web/Cargo.toml @@ -11,7 +11,9 @@ chartered-types = { path = "../chartered-types" } axum = { version = "0.2", features = ["headers"] } +base64 = "0.13" bytes = "1" +chacha20poly1305 = { version = "0.9", features = ["std"] } chrono = { version = "0.4", features = ["serde"] } env_logger = "0.9" futures = "0.3" @@ -20,7 +22,10 @@ log = "0.4" nom = "7" once_cell = "1.8" +openid = "0.9" +rand = "*" regex = "1.5" +reqwest = "0.11" serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.9" diff --git a/chartered-db/src/lib.rs b/chartered-db/src/lib.rs index c35c0fc..22cf0a1 100644 --- a/chartered-db/src/lib.rs +++ a/chartered-db/src/lib.rs @@ -38,8 +38,10 @@ pub mod users; pub mod uuid; -#[macro_use] extern crate diesel; -#[macro_use] extern crate diesel_migrations; +#[macro_use] +extern crate diesel; +#[macro_use] +extern crate diesel_migrations; use diesel::{ expression::{grouped::Grouped, AsExpression, Expression}, diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs index 69e16d3..2470f2f 100644 --- a/chartered-db/src/users.rs +++ a/chartered-db/src/users.rs @@ -113,6 +113,32 @@ .await? } + pub async fn find_or_create(conn: ConnectionPool, given_username: String) -> Result { + use crate::schema::users::dsl::username; + + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + let user: Option = crate::schema::users::table + .filter(username.eq(&given_username)) + .get_result(&conn) + .optional()?; + + if let Some(user) = user { + return Ok(user); + } + + diesel::insert_into(users::table) + .values(username.eq(&given_username)) + .execute(&conn)?; + + Ok(crate::schema::users::table + .filter(username.eq(given_username)) + .get_result(&conn)?) + }) + .await? + } + /// Parses an ssh key from its `ssh-add -L` format (`ssh-ed25519 AAAAC3N...`) and /// inserts it to the database for the user. pub async fn insert_ssh_key( diff --git a/chartered-frontend/src/index.sass b/chartered-frontend/src/index.sass index c48704b..879e263 100644 --- a/chartered-frontend/src/index.sass +++ a/chartered-frontend/src/index.sass @@ -25,4 +25,20 @@ white-space: nowrap .text-decoration-underline-dotted - text-decoration: underline dotted+ text-decoration: underline dotted + +.side-lines + display: grid + grid-template-columns: minmax(20px, 1fr) auto minmax(20px, 1fr) + align-items: center + text-align: center + grid-gap: 20px + width: 100% + + &::before + content: "" + border-top: 1px solid + + &::after + content: "" + border-top: 1px solid diff --git a/chartered-frontend/src/useAuth.tsx b/chartered-frontend/src/useAuth.tsx index eec1f0b..acc0ccc 100644 --- a/chartered-frontend/src/useAuth.tsx +++ a/chartered-frontend/src/useAuth.tsx @@ -1,7 +1,11 @@ import React = require("react"); import { useState, useEffect, useContext, createContext } from "react"; import { unauthenticatedEndpoint } from "./util"; +export interface OAuthProviders { + providers: string[]; +} + export interface AuthContext { login: (username: string, password: string) => Promise; logout: () => Promise; @@ -33,7 +37,7 @@ }, [auth]); const login = async (username: string, password: string) => { - let res = await fetch(unauthenticatedEndpoint("login"), { + let res = await fetch(unauthenticatedEndpoint("login/password"), { method: "POST", headers: { "Content-Type": "application/json", diff --git a/chartered-frontend/src/util.tsx b/chartered-frontend/src/util.tsx index b4c0e44..28e26a7 100644 --- a/chartered-frontend/src/util.tsx +++ a/chartered-frontend/src/util.tsx @@ -46,6 +46,31 @@ return { response, error }; } +export function useUnauthenticatedRequest( + { endpoint }: { endpoint: string }, + reloadOn = [] +): { response: S | null; error: string | null } { + const [error, setError] = React.useState(null); + const [response, setResponse] = React.useState(null); + + React.useEffect(async () => { + try { + let res = await fetch(unauthenticatedEndpoint(endpoint)); + let jsonRes = await res.json(); + + if (jsonRes.error) { + setError(jsonRes.error); + } else { + setResponse(jsonRes); + } + } catch (e) { + setError(e.message); + } + }, reloadOn); + + return { response, error }; +} + export function RoundedPicture({ src, height, diff --git a/chartered-fs/src/lib.rs b/chartered-fs/src/lib.rs index a6e395d..309e238 100644 --- a/chartered-fs/src/lib.rs +++ a/chartered-fs/src/lib.rs @@ -1,22 +1,62 @@ #![deny(clippy::pedantic)] -#![deny(clippy::pedantic)] +use std::path::PathBuf; + use async_trait::async_trait; +use itertools::Itertools; use serde::{Deserialize, Serialize}; use tokio::{ fs::File, io::{AsyncReadExt, AsyncWriteExt}, }; + +#[derive(Debug)] +pub enum FS { + S3 { + host: String, + bucket: String, + path: String, + }, + Local { + path: PathBuf, + }, +} + +impl std::str::FromStr for FS { + type Err = url::ParseError; + + fn from_str(s: &str) -> Result { + let uri = url::Url::parse(s)?; + + Ok(match uri.scheme() { + "s3" => { + let mut path = uri.path_segments().unwrap(); + + Self::S3 { + host: uri.host().unwrap().to_string(), + bucket: path.next().unwrap().to_string(), + path: path.intersperse("/").collect(), + } + } + "file" => { + panic!("{:#?}", uri); + } + _ => panic!("na"), + }) + } +} #[derive(Debug, Serialize, Deserialize)] pub enum FileSystemKind { Local, + S3, } impl std::fmt::Display for FileSystemKind { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::Local => f.write_str("local"), + Self::S3 => f.write_str("s3"), } } } @@ -105,7 +145,15 @@ #[cfg(test)] mod tests { - use super::FileSystem; + use super::{FileSystem, FS}; + use std::str::FromStr; + + #[tokio::test] + #[allow(clippy::pedantic)] + async fn parse_filesystem() { + // panic!("{:#?}", FS::from_str("s3://10.0.64.101:9000/my-bucket/my-location")); + FS::from_str("file:///tmp/chartered"); + } #[tokio::test] #[allow(clippy::pedantic)] diff --git a/chartered-web/src/config.rs b/chartered-web/src/config.rs new file mode 100644 index 0000000..0a222c0 100644 --- /dev/null +++ a/chartered-web/src/config.rs @@ -1,0 +1,88 @@ +use chacha20poly1305::Key as ChaCha20Poly1305Key; +use openid::DiscoveredClient; +use serde::{de::Error as SerdeDeError, Deserialize}; +use std::collections::HashMap; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Error discovering OpenID provider: {0}")] + OpenId(#[from] openid::error::Error), +} + +pub type OidcClients = HashMap; + +#[derive(Deserialize, Default, Debug)] +#[serde(deny_unknown_fields)] +pub struct Config { + pub auth: AuthConfig, + #[serde(deserialize_with = "deserialize_encryption_key")] + pub encryption_key: ChaCha20Poly1305Key, +} + +impl Config { + pub async fn create_oidc_clients(&self) -> Result { + Ok(futures::future::try_join_all( + self.auth + .oauth + .iter() + .filter(|(_, config)| config.enabled) + .map(|(name, config)| async move { + Ok::<_, Error>(( + name.to_string(), + DiscoveredClient::discover( + config.client_id.to_string(), + config.client_secret.to_string(), + Some("http://127.0.0.1:1234/login/oauth".to_string()), + config.discovery_uri.clone(), + ) + .await?, + )) + }), + ) + .await? + .into_iter() + .collect()) + } +} + +#[derive(Deserialize, Default, Debug)] +pub struct AuthConfig { + pub password: PasswordAuthConfig, + #[serde(flatten)] + pub oauth: HashMap, +} + +#[derive(Deserialize, Default, Debug)] +#[serde(deny_unknown_fields)] +pub struct PasswordAuthConfig { + pub enabled: bool, +} + +#[derive(Deserialize, Debug)] +pub struct OAuthConfig { + pub enabled: bool, + #[serde(deserialize_with = "deserialize_url")] + pub discovery_uri: reqwest::Url, + pub client_id: String, + pub client_secret: String, +} + +fn deserialize_encryption_key<'de, D: serde::Deserializer<'de>>( + deserializer: D, +) -> Result { + let key = String::deserialize(deserializer)?; + + if key.as_bytes().len() != 32 { + return Err(D::Error::custom("encryption_key must be 32 bytes")); + } + + Ok(ChaCha20Poly1305Key::clone_from_slice(key.as_bytes())) +} + +fn deserialize_url<'de, D: serde::Deserializer<'de>>( + deserializer: D, +) -> Result { + let uri = String::deserialize(deserializer)?; + reqwest::Url::parse(&uri).map_err(D::Error::custom) +} diff --git a/chartered-web/src/main.rs b/chartered-web/src/main.rs index 57f478e..37fc215 100644 --- a/chartered-web/src/main.rs +++ a/chartered-web/src/main.rs @@ -1,14 +1,16 @@ #![deny(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] +mod config; mod endpoints; mod middleware; use axum::{ - handler::{delete, get, patch, post, put}, - http::{Method, header}, + handler::get, + http::{header, Method}, AddExtensionLayer, Router, }; +use std::sync::Arc; use tower::ServiceBuilder; use tower_http::cors::{Any, CorsLayer}; @@ -22,111 +24,30 @@ // new route, the workaround is to box the router down to a // dynamically dispatched version with every new route. macro_rules! axum_box_after_every_route { - (Router::new()$(.route($path:expr, $svc:expr$(,)?))*) => { + (Router::new() + $(.nest($nest_path:expr, $nest_svc:expr$(,)?))* + $(.route($route_path:expr, $route_svc:expr$(,)?))* + ) => { Router::new() $( - .route($path, $svc) + .nest($nest_path, $nest_svc) .boxed() )* + $( + .route($route_path, $route_svc) + .boxed() + )* }; } +pub(crate) use axum_box_after_every_route; + #[tokio::main] #[allow(clippy::semicolon_if_nothing_returned)] // lint breaks with tokio::main -#[allow(clippy::too_many_lines)] // todo: refactor async fn main() { env_logger::init(); let pool = chartered_db::init().unwrap(); - - let api_authenticated = axum_box_after_every_route!(Router::new() - .route("/crates/new", put(endpoints::cargo_api::publish)) - .route("/crates/search", get(hello_world)) - .route( - "/crates/:crate/owners", - get(endpoints::cargo_api::get_owners) - ) - .route("/crates/:crate/owners", put(hello_world)) - .route("/crates/:crate/owners", delete(hello_world)) - .route( - "/crates/:crate/:version/yank", - delete(endpoints::cargo_api::yank) - ) - .route( - "/crates/:crate/:version/unyank", - put(endpoints::cargo_api::unyank) - ) - .route( - "/crates/:crate/:version/download", - get(endpoints::cargo_api::download) - )) - .layer( - ServiceBuilder::new() - .layer_fn(middleware::auth::AuthMiddleware) - .into_inner(), - ); - - let web_unauthenticated = - axum_box_after_every_route!(Router::new().route("/login", post(endpoints::web_api::login))); - - let web_authenticated = axum_box_after_every_route!(Router::new() - // organisations endpoints - .route( - "/organisations", - get(endpoints::web_api::organisations::list) - ) - .route( - "/organisations", - put(endpoints::web_api::organisations::create) - ) - .route( - "/organisations/:org", - get(endpoints::web_api::organisations::info) - ) - .route( - "/organisations/:org/members", - patch(endpoints::web_api::organisations::update_member) - ) - .route( - "/organisations/:org/members", - put(endpoints::web_api::organisations::insert_member) - ) - .route( - "/organisations/:org/members", - delete(endpoints::web_api::organisations::delete_member) - ) - // crate endpoints - .route("/crates/:org/:crate", get(endpoints::web_api::crates::info)) - .route( - "/crates/:org/:crate/members", - get(endpoints::web_api::crates::get_members) - ) - .route( - "/crates/:org/:crate/members", - patch(endpoints::web_api::crates::update_member) - ) - .route( - "/crates/:org/:crate/members", - put(endpoints::web_api::crates::insert_member) - ) - .route( - "/crates/:org/:crate/members", - delete(endpoints::web_api::crates::delete_member) - ) - .route( - "/crates/recently-updated", - get(endpoints::web_api::crates::list_recently_updated) - ) - // users endpoints - .route("/users/search", get(endpoints::web_api::search_users)) - .route("/ssh-key", get(endpoints::web_api::get_ssh_keys)) - .route("/ssh-key", put(endpoints::web_api::add_ssh_key)) - .route("/ssh-key/:id", delete(endpoints::web_api::delete_ssh_key))) - .layer( - ServiceBuilder::new() - .layer_fn(middleware::auth::AuthMiddleware) - .into_inner(), - ); let middleware_stack = ServiceBuilder::new() .layer_fn(middleware::logging::LoggingMiddleware) @@ -134,9 +55,23 @@ let app = Router::new() .route("/", get(hello_world)) - .nest("/a/:key/web/v1", web_authenticated) - .nest("/a/-/web/v1", web_unauthenticated) - .nest("/a/:key/o/:organisation/api/v1", api_authenticated) + .nest( + "/a/:key/web/v1", + endpoints::web_api::authenticated_routes().layer( + ServiceBuilder::new() + .layer_fn(crate::middleware::auth::AuthMiddleware) + .into_inner(), + ), + ) + .nest("/a/-/web/v1", endpoints::web_api::unauthenticated_routes()) + .nest( + "/a/:key/o/:organisation/api/v1", + endpoints::cargo_api::routes().layer( + ServiceBuilder::new() + .layer_fn(crate::middleware::auth::AuthMiddleware) + .into_inner(), + ), + ) .layer(middleware_stack) // TODO!!! .layer( @@ -153,7 +88,11 @@ .allow_origin(Any) .allow_credentials(false), ) - .layer(AddExtensionLayer::new(pool)); + .layer(AddExtensionLayer::new(pool)) + .layer(AddExtensionLayer::new(Arc::new( + config.create_oidc_clients().await.unwrap(), + ))) + .layer(AddExtensionLayer::new(Arc::new(config))); axum::Server::bind(&"0.0.0.0:8888".parse().unwrap()) .serve(app.into_make_service_with_connect_info::()) diff --git a/book/src/getting-started/installation.md b/book/src/getting-started/installation.md index c1c3efc..8d7d77c 100644 --- a/book/src/getting-started/installation.md +++ a/book/src/getting-started/installation.md @@ -12,7 +12,8 @@ Each of these services are hosted separately from one another, and could technically be swapped out for other implementations - the only shared layer between the three of them is database -storage for crate lookups and authentication credential vending. +storage for crate lookups and authentication credential vending. All of the services have the +ability to be clustered with no extra configuration. ### Backend Services diff --git a/chartered-frontend/src/pages/Login.tsx b/chartered-frontend/src/pages/Login.tsx index b1c4fdb..0a4eb38 100644 --- a/chartered-frontend/src/pages/Login.tsx +++ a/chartered-frontend/src/pages/Login.tsx @@ -1,7 +1,8 @@ import React = require("react"); import { useState, useEffect, useRef } from "react"; import { useAuth } from "../useAuth"; +import { useUnauthenticatedRequest } from "../util"; export default function Login() { const auth = useAuth(); @@ -12,6 +13,8 @@ const [loading, setLoading] = useState(false); const isMountedRef = useRef(null); + const { response: oauthProviders } = useUnauthenticatedRequest({ endpoint: "login/oauth/providers" }); + useEffect(() => { isMountedRef.current = true; return () => (isMountedRef.current = false); @@ -61,36 +64,38 @@
-
- +
setUsername(e.target.value)} /> + +
-
- + +
setPassword(e.target.value)} /> + +
-
+ +
+ + {oauthProviders?.providers.length > 0 ? (<> +
or
+ + {oauthProviders.providers.map((v, i) => Login with {v})} + ): <>}
diff --git a/chartered-web/src/endpoints/cargo_api/mod.rs b/chartered-web/src/endpoints/cargo_api/mod.rs index 9ea8e43..1740db0 100644 --- a/chartered-web/src/endpoints/cargo_api/mod.rs +++ a/chartered-web/src/endpoints/cargo_api/mod.rs @@ -1,10 +1,33 @@ mod download; mod owners; mod publish; mod yank; -pub use download::handle as download; -pub use owners::handle_get as get_owners; -pub use publish::handle as publish; -pub use yank::handle_unyank as unyank; -pub use yank::handle_yank as yank; +use axum::{ + body::{Body, BoxBody}, + handler::{delete, get, put}, + http::{Request, Response}, + Router, +}; +use futures::future::Future; +use std::convert::Infallible; + +pub fn routes() -> Router< + impl tower::Service< + Request, + Response = Response, + Error = Infallible, + Future = impl Future, Infallible>> + Send, + > + Clone + + Send, +> { + crate::axum_box_after_every_route!(Router::new() + .route("/crates/new", put(publish::handle)) + // .route("/crates/search", get(hello_world)) + .route("/crates/:crate/owners", get(owners::handle_get)) + // .route("/crates/:crate/owners", put(hello_world)) + // .route("/crates/:crate/owners", delete(hello_world)) + .route("/crates/:crate/:version/yank", delete(yank::handle_yank)) + .route("/crates/:crate/:version/unyank", put(yank::handle_unyank)) + .route("/crates/:crate/:version/download", get(download::handle))) +} diff --git a/chartered-web/src/endpoints/web_api/login.rs b/chartered-web/src/endpoints/web_api/login.rs deleted file mode 100644 index a617bb8..0000000 100644 --- a/chartered-web/src/endpoints/web_api/login.rs +++ /dev/null @@ -1,75 +1,0 @@ -use axum::{extract, Json}; -use chartered_db::{ - users::{User, UserSession}, - ConnectionPool, -}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("Failed to query database")] - Database(#[from] chartered_db::Error), - #[error("Invalid username/password")] - UnknownUser, -} - -impl Error { - pub fn status_code(&self) -> axum::http::StatusCode { - use axum::http::StatusCode; - - match self { - Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::UnknownUser => StatusCode::FORBIDDEN, - } - } -} - -define_error_response!(Error); - -pub async fn handle( - extract::Extension(db): extract::Extension, - extract::Json(req): extract::Json, - user_agent: Option>, - extract::ConnectInfo(addr): extract::ConnectInfo, -) -> Result, Error> { - // TODO: passwords - let user = User::find_by_username(db.clone(), req.username) - .await? - .ok_or(Error::UnknownUser)?; - - let user_agent = if let Some(extract::TypedHeader(user_agent)) = user_agent { - Some(user_agent.as_str().to_string()) - } else { - None - }; - - // todo: session? ip storage? etc... - let expires = chrono::Utc::now() + chrono::Duration::hours(1); - let key = UserSession::generate( - db, - user.id, - None, - Some(expires.naive_utc()), - user_agent, - Some(addr.to_string()), - ) - .await?; - - Ok(Json(Response { - key: key.session_key, - expires, - })) -} - -#[derive(Deserialize)] -pub struct Request { - username: String, - password: String, -} - -#[derive(Serialize)] -pub struct Response { - key: String, - expires: chrono::DateTime, -} diff --git a/chartered-web/src/endpoints/web_api/mod.rs b/chartered-web/src/endpoints/web_api/mod.rs index d44e5e5..5e80e62 100644 --- a/chartered-web/src/endpoints/web_api/mod.rs +++ a/chartered-web/src/endpoints/web_api/mod.rs @@ -1,11 +1,44 @@ -pub mod crates; -mod login; -pub mod organisations; +mod auth; +mod crates; +mod organisations; mod search_users; mod ssh_key; -pub use login::handle as login; -pub use search_users::handle as search_users; -pub use ssh_key::{ - handle_delete as delete_ssh_key, handle_get as get_ssh_keys, handle_put as add_ssh_key, +use axum::{ + body::{Body, BoxBody}, + handler::{delete, get, put}, + http::{Request, Response}, + Router, }; +use futures::future::Future; +use std::convert::Infallible; + +pub fn authenticated_routes() -> Router< + impl tower::Service< + Request, + Response = Response, + Error = Infallible, + Future = impl Future, Infallible>> + Send, + > + Clone + + Send, +> { + crate::axum_box_after_every_route!(Router::new() + .nest("/organisations", organisations::routes()) + .nest("/crates", crates::routes()) + .route("/users/search", get(search_users::handle)) + .route("/ssh-key", get(ssh_key::handle_get)) + .route("/ssh-key", put(ssh_key::handle_put)) + .route("/ssh-key/:id", delete(ssh_key::handle_delete))) +} + +pub fn unauthenticated_routes() -> Router< + impl tower::Service< + Request, + Response = Response, + Error = Infallible, + Future = impl Future, Infallible>> + Send, + > + Clone + + Send, +> { + crate::axum_box_after_every_route!(Router::new().nest("/login", auth::routes())) +} diff --git a/chartered-web/src/endpoints/web_api/auth/mod.rs b/chartered-web/src/endpoints/web_api/auth/mod.rs new file mode 100644 index 0000000..e0b309d 100644 --- /dev/null +++ a/chartered-web/src/endpoints/web_api/auth/mod.rs @@ -1,0 +1,68 @@ +use axum::{ + body::{Body, BoxBody}, + extract, + handler::{get, post}, + http::{Request, Response}, + Router, +}; +use chartered_db::{ + users::{User, UserSession}, + ConnectionPool, +}; +use futures::future::Future; +use serde::Serialize; +use std::convert::Infallible; + +pub mod openid; +pub mod password; + +pub fn routes() -> Router< + impl tower::Service< + Request, + Response = Response, + Error = Infallible, + Future = impl Future, Infallible>> + Send, + > + Clone + + Send, +> { + crate::axum_box_after_every_route!(Router::new() + .route("/password", post(password::handle)) + .route("/oauth/:provider/begin", get(openid::begin_oidc)) + .route("/oauth/complete", get(openid::complete_oidc)) + .route("/oauth/providers", get(openid::list_providers))) +} + +#[derive(Serialize)] +pub struct LoginResponse { + key: String, + expires: chrono::DateTime, +} + +pub async fn login( + db: ConnectionPool, + user: User, + user_agent: Option>, + extract::ConnectInfo(addr): extract::ConnectInfo, +) -> Result { + let user_agent = if let Some(extract::TypedHeader(user_agent)) = user_agent { + Some(user_agent.as_str().to_string()) + } else { + None + }; + + let expires = chrono::Utc::now() + chrono::Duration::hours(1); + let key = UserSession::generate( + db, + user.id, + None, + Some(expires.naive_utc()), + user_agent, + Some(addr.to_string()), + ) + .await?; + + Ok(LoginResponse { + key: key.session_key, + expires, + }) +} diff --git a/chartered-web/src/endpoints/web_api/auth/openid.rs b/chartered-web/src/endpoints/web_api/auth/openid.rs new file mode 100644 index 0000000..63dee0f 100644 --- /dev/null +++ a/chartered-web/src/endpoints/web_api/auth/openid.rs @@ -1,0 +1,168 @@ +use crate::config::{Config, OidcClients}; +use axum::{extract, Json}; +use chacha20poly1305::{ + aead::{Aead, NewAead}, + ChaCha20Poly1305, Nonce as ChaCha20Poly1305Nonce, +}; +use chartered_db::{users::User, ConnectionPool}; +use openid::{Options, Token}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use thiserror::Error; + +pub type Nonce = [u8; 16]; + +#[derive(Serialize)] +pub struct ListProvidersResponse { + providers: Vec, +} + +pub async fn list_providers( + extract::Extension(oidc_clients): extract::Extension>, +) -> Json { + Json(ListProvidersResponse { + providers: oidc_clients + .keys() + .into_iter() + .map(|v| v.to_string()) + .collect(), + }) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct State { + provider: String, + nonce: Nonce, +} + +#[derive(Serialize)] +pub struct BeginResponse { + redirect_url: String, +} + +pub async fn begin_oidc( + extract::Path(provider): extract::Path, + extract::Extension(config): extract::Extension>, + extract::Extension(oidc_clients): extract::Extension>, +) -> Result, Error> { + let client = oidc_clients + .get(&provider) + .ok_or(Error::UnknownOauthProvider)?; + + let nonce = rand::random::(); + let state = serde_json::to_vec(&State { provider, nonce })?; + + let auth_url = client.auth_url(&Options { + scope: Some("openid email profile".into()), + nonce: Some(base64::encode_config(&nonce, base64::URL_SAFE_NO_PAD)), + state: Some(encrypt_url_safe(&state, &config)?), + ..Default::default() + }); + + Ok(Json(BeginResponse { + redirect_url: auth_url.to_string(), + })) +} + +#[derive(Deserialize)] +pub struct CompleteOidcParams { + state: String, + code: String, + scope: Option, + prompt: Option, +} + +pub async fn complete_oidc( + extract::Query(params): extract::Query, + extract::Extension(config): extract::Extension>, + extract::Extension(oidc_clients): extract::Extension>, + extract::Extension(db): extract::Extension, + user_agent: Option>, + addr: extract::ConnectInfo, +) -> Result, Error> { + let state: State = serde_json::from_slice(&decrypt_url_safe(¶ms.state, &config)?)?; + + let client = oidc_clients + .get(&state.provider) + .ok_or(Error::UnknownOauthProvider)?; + + let mut token: Token = client.request_token(¶ms.code).await?.into(); + + if let Some(mut id_token) = token.id_token.as_mut() { + client.decode_token(&mut id_token)?; + + let nonce = base64::encode_config(state.nonce, base64::URL_SAFE_NO_PAD); + client.validate_token(&id_token, Some(nonce.as_str()), None)?; + } else { + return Err(Error::MissingToken); + } + + let userinfo = client.request_userinfo(&token).await?; + + let user = User::find_or_create( + db.clone(), + format!("{}:{}", state.provider, userinfo.sub.unwrap()), + ) + .await?; + + Ok(Json(super::login(db, user, user_agent, addr).await?)) +} + +const NONCE_LEN: usize = 12; + +fn encrypt_url_safe(input: &[u8], config: &Config) -> Result { + let cipher = ChaCha20Poly1305::new(&config.encryption_key); + + let nonce = rand::random::<[u8; NONCE_LEN]>(); + let nonce = ChaCha20Poly1305Nonce::from_slice(&nonce); + + let mut ciphertext = cipher.encrypt(nonce, input)?; + ciphertext.extend_from_slice(&nonce); + + Ok(base64::encode_config(&ciphertext, base64::URL_SAFE_NO_PAD)) +} + +fn decrypt_url_safe(input: &str, config: &Config) -> Result, Error> { + let cipher = ChaCha20Poly1305::new(&config.encryption_key); + + let mut ciphertext = base64::decode_config(input, base64::URL_SAFE_NO_PAD)?; + let ciphertext_nonce = ciphertext.split_off(ciphertext.len() - NONCE_LEN); + let ciphertext_nonce = ChaCha20Poly1305Nonce::from_slice(&ciphertext_nonce); + + cipher + .decrypt(ciphertext_nonce, ciphertext.as_ref()) + .map_err(Error::from) +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), + #[error("Error serialising/deserialsing state: {0}")] + Serde(#[from] serde_json::Error), + #[error("Unknown OAuth provider given")] + UnknownOauthProvider, + #[error("{0}")] + OAuth(#[from] openid::error::Error), + #[error("{0}")] + OAuthClient(#[from] openid::error::ClientError), + #[error("Error during encryption/decryption")] + Cipher(#[from] chacha20poly1305::aead::Error), + #[error("Base64 error")] + Base64(#[from] base64::DecodeError), + #[error("Missing id_token")] + MissingToken, +} + +impl Error { + fn status_code(&self) -> axum::http::StatusCode { + use axum::http::StatusCode; + + match self { + Self::Database(e) => e.status_code(), + _ => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/web_api/auth/password.rs b/chartered-web/src/endpoints/web_api/auth/password.rs new file mode 100644 index 0000000..5e2b720 100644 --- /dev/null +++ a/chartered-web/src/endpoints/web_api/auth/password.rs @@ -1,0 +1,45 @@ +use axum::{extract, Json}; +use chartered_db::{users::User, ConnectionPool}; +use serde::Deserialize; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Failed to query database")] + Database(#[from] chartered_db::Error), + #[error("Invalid username/password")] + UnknownUser, +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + use axum::http::StatusCode; + + match self { + Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::UnknownUser => StatusCode::FORBIDDEN, + } + } +} + +define_error_response!(Error); + +pub async fn handle( + extract::Extension(db): extract::Extension, + extract::Json(req): extract::Json, + user_agent: Option>, + addr: extract::ConnectInfo, +) -> Result, Error> { + // TODO: passwords + let user = User::find_by_username(db.clone(), req.username) + .await? + .ok_or(Error::UnknownUser)?; + + Ok(Json(super::login(db, user, user_agent, addr).await?)) +} + +#[derive(Deserialize)] +pub struct Request { + username: String, + password: String, +} diff --git a/chartered-web/src/endpoints/web_api/crates/mod.rs b/chartered-web/src/endpoints/web_api/crates/mod.rs index 3113603..5f6b636 100644 --- a/chartered-web/src/endpoints/web_api/crates/mod.rs +++ a/chartered-web/src/endpoints/web_api/crates/mod.rs @@ -1,10 +1,30 @@ mod info; mod members; mod recently_updated; -pub use info::handle as info; -pub use members::{ - handle_delete as delete_member, handle_get as get_members, handle_patch as update_member, - handle_put as insert_member, +use axum::{ + body::{Body, BoxBody}, + handler::{delete, get, patch, put}, + http::{Request, Response}, + Router, }; -pub use recently_updated::handle as list_recently_updated; +use futures::future::Future; +use std::convert::Infallible; + +pub fn routes() -> Router< + impl tower::Service< + Request, + Response = Response, + Error = Infallible, + Future = impl Future, Infallible>> + Send, + > + Clone + + Send, +> { + crate::axum_box_after_every_route!(Router::new() + .route("/:org/:crate", get(info::handle)) + .route("/:org/:crate/members", get(members::handle_get)) + .route("/:org/:crate/members", patch(members::handle_patch)) + .route("/:org/:crate/members", put(members::handle_put)) + .route("/:org/:crate/members", delete(members::handle_delete)) + .route("/recently-updated", get(recently_updated::handle))) +} diff --git a/chartered-web/src/endpoints/web_api/organisations/mod.rs b/chartered-web/src/endpoints/web_api/organisations/mod.rs index 7ddfc99..525b1ff 100644 --- a/chartered-web/src/endpoints/web_api/organisations/mod.rs +++ a/chartered-web/src/endpoints/web_api/organisations/mod.rs @@ -1,11 +1,31 @@ mod crud; mod info; mod list; mod members; -pub use crud::handle_put as create; -pub use info::handle_get as info; -pub use list::handle_get as list; -pub use members::{ - handle_delete as delete_member, handle_patch as update_member, handle_put as insert_member, +use axum::{ + body::{Body, BoxBody}, + handler::{delete, get, patch, put}, + http::{Request, Response}, + Router, }; +use futures::future::Future; +use std::convert::Infallible; + +pub fn routes() -> Router< + impl tower::Service< + Request, + Response = Response, + Error = Infallible, + Future = impl Future, Infallible>> + Send, + > + Clone + + Send, +> { + crate::axum_box_after_every_route!(Router::new() + .route("/", get(list::handle_get)) + .route("/", put(crud::handle_put)) + .route("/:org", get(info::handle_get)) + .route("/:org/members", patch(members::handle_patch)) + .route("/:org/members", put(members::handle_put)) + .route("/:org/members", delete(members::handle_delete))) +} -- rgit 0.1.3