OAuth support in the backend
Diff
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(-)
@@ -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]]
@@ -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]
@@ -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"
@@ -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},
@@ -113,6 +113,32 @@
.await?
}
pub async fn find_or_create(conn: ConnectionPool, given_username: String) -> Result<User> {
use crate::schema::users::dsl::username;
tokio::task::spawn_blocking(move || {
let conn = conn.get()?;
let user: Option<User> = 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?
}
pub async fn insert_ssh_key(
@@ -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
@@ -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<void>;
logout: () => Promise<void>;
@@ -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",
@@ -46,6 +46,31 @@
return { response, error };
}
export function useUnauthenticatedRequest<S>(
{ 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,
@@ -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<Self, Self::Err> {
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() {
FS::from_str("file:///tmp/chartered");
}
#[tokio::test]
#[allow(clippy::pedantic)]
@@ -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<String, DiscoveredClient>;
#[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<OidcClients, Error> {
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<String, OAuthConfig>,
}
#[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<ChaCha20Poly1305Key, D::Error> {
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<reqwest::Url, D::Error> {
let uri = String::deserialize(deserializer)?;
reqwest::Url::parse(&uri).map_err(D::Error::custom)
}
@@ -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 @@
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)]
#[allow(clippy::too_many_lines)]
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()
.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)
)
.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)
)
.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)
.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::<std::net::SocketAddr, _>())
@@ -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
@@ -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 @@
</div>
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label htmlFor="username" className="form-label">
Username
</label>
<div className="form-floating">
<input
type="text"
className="form-control"
placeholder="john.smith"
id="username"
disabled={loading}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<label htmlFor="email" className="form-label">Username</label>
</div>
<div className="mb-3">
<label htmlFor="password" className="form-label">
Password
</label>
<div className="form-floating mt-2">
<input
type="password"
className="form-control"
placeholder="••••••••••••"
id="password"
disabled={loading}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<label htmlFor="password" className="form-label">Password</label>
</div>
<div className="ml-auto">
<div className="mt-2 ml-auto">
<button
type="submit"
className="btn btn-primary"
className="btn btn-lg btn-primary w-100"
style={{ display: !loading ? "block" : "none" }}
>
Login
@@ -105,6 +110,12 @@
</div>
</div>
</form>
{oauthProviders?.providers.length > 0 ? (<>
<div className="side-lines mt-3">or</div>
{oauthProviders.providers.map((v, i) => <a href="#" key={i} className="btn btn-lg btn-dark w-100 mt-3">Login with {v}</a>)}
</>): <></>}
</div>
</div>
</div>
@@ -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<Body>,
Response = Response<BoxBody>,
Error = Infallible,
Future = impl Future<Output = Result<Response<BoxBody>, Infallible>> + Send,
> + Clone
+ Send,
> {
crate::axum_box_after_every_route!(Router::new()
.route("/crates/new", put(publish::handle))
.route("/crates/:crate/owners", get(owners::handle_get))
.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)))
}
@@ -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<ConnectionPool>,
extract::Json(req): extract::Json<Request>,
user_agent: Option<extract::TypedHeader<headers::UserAgent>>,
extract::ConnectInfo(addr): extract::ConnectInfo<std::net::SocketAddr>,
) -> Result<Json<Response>, Error> {
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
};
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<chrono::Utc>,
}
@@ -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<Body>,
Response = Response<BoxBody>,
Error = Infallible,
Future = impl Future<Output = Result<Response<BoxBody>, 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<Body>,
Response = Response<BoxBody>,
Error = Infallible,
Future = impl Future<Output = Result<Response<BoxBody>, Infallible>> + Send,
> + Clone
+ Send,
> {
crate::axum_box_after_every_route!(Router::new().nest("/login", auth::routes()))
}
@@ -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<Body>,
Response = Response<BoxBody>,
Error = Infallible,
Future = impl Future<Output = Result<Response<BoxBody>, 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<chrono::Utc>,
}
pub async fn login(
db: ConnectionPool,
user: User,
user_agent: Option<extract::TypedHeader<headers::UserAgent>>,
extract::ConnectInfo(addr): extract::ConnectInfo<std::net::SocketAddr>,
) -> Result<LoginResponse, chartered_db::Error> {
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,
})
}
@@ -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<String>,
}
pub async fn list_providers(
extract::Extension(oidc_clients): extract::Extension<Arc<OidcClients>>,
) -> Json<ListProvidersResponse> {
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<String>,
extract::Extension(config): extract::Extension<Arc<Config>>,
extract::Extension(oidc_clients): extract::Extension<Arc<OidcClients>>,
) -> Result<Json<BeginResponse>, Error> {
let client = oidc_clients
.get(&provider)
.ok_or(Error::UnknownOauthProvider)?;
let nonce = rand::random::<Nonce>();
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<String>,
prompt: Option<String>,
}
pub async fn complete_oidc(
extract::Query(params): extract::Query<CompleteOidcParams>,
extract::Extension(config): extract::Extension<Arc<Config>>,
extract::Extension(oidc_clients): extract::Extension<Arc<OidcClients>>,
extract::Extension(db): extract::Extension<ConnectionPool>,
user_agent: Option<extract::TypedHeader<headers::UserAgent>>,
addr: extract::ConnectInfo<std::net::SocketAddr>,
) -> Result<Json<super::LoginResponse>, 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<String, Error> {
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<Vec<u8>, 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);
@@ -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<ConnectionPool>,
extract::Json(req): extract::Json<Request>,
user_agent: Option<extract::TypedHeader<headers::UserAgent>>,
addr: extract::ConnectInfo<std::net::SocketAddr>,
) -> Result<Json<super::LoginResponse>, Error> {
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,
}
@@ -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<Body>,
Response = Response<BoxBody>,
Error = Infallible,
Future = impl Future<Output = Result<Response<BoxBody>, 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)))
}
@@ -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<Body>,
Response = Response<BoxBody>,
Error = Infallible,
Future = impl Future<Output = Result<Response<BoxBody>, 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)))
}