diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..514ca385a0 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +CLIENT_ID= +CLIENT_SECRET= +TENANT_ID= +AUTHORITY=https://login.microsoftonline.com/TENANT_ID \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 91ad9dbc48..a748d61762 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.4" @@ -38,6 +48,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.11" @@ -515,23 +539,23 @@ dependencies = [ [[package]] name = "async-graphql" -version = "6.0.11" +version = "7.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298a5d587d6e6fdb271bf56af2dc325a80eb291fd0fc979146584b9a05494a8c" +checksum = "d0808e3dc8be9cee801000b9261081cd7caae9935658be7e52d4df3d5c22bdce" dependencies = [ "async-graphql-derive", "async-graphql-parser", "async-graphql-value", "async-stream", "async-trait", - "base64 0.13.1", + "base64 0.22.1", "bytes", "chrono", "fast_chemail", "fnv", "futures-util", "handlebars", - "http", + "http 1.1.0", "indexmap", "mime", "multer", @@ -542,33 +566,33 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "static_assertions", + "static_assertions_next", "tempfile", "thiserror", ] [[package]] name = "async-graphql-derive" -version = "6.0.11" +version = "7.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f329c7eb9b646a72f70c9c4b516c70867d356ec46cb00dcac8ad343fd006b0" +checksum = "ae80fb7b67deeae84441a9eb156359b99be7902d2d706f43836156eec69a8226" dependencies = [ "Inflector", "async-graphql-parser", "darling 0.20.9", - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "strum 0.25.0", + "strum", "syn 2.0.65", "thiserror", ] [[package]] name = "async-graphql-parser" -version = "6.0.11" +version = "7.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6139181845757fd6a73fbb8839f3d036d7150b798db0e9bb3c6e83cdd65bd53b" +checksum = "8f0fffb19cd96eb084428289f4568b5ad48df32f782f891f709db96384fbdeb2" dependencies = [ "async-graphql-value", "pest", @@ -578,12 +602,13 @@ dependencies = [ [[package]] name = "async-graphql-poem" -version = "6.0.11" +version = "7.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665e1051f0c73fdb2f143a341c531249aedd35473d0f5d8e6e32627502f45cd" +checksum = "ee8483d34d012aea467463c4de9a3bddee965fac6f99ed4a4a859171e4ecadb4" dependencies = [ "async-graphql", "futures-util", + "http 1.1.0", "mime", "poem", "serde_json", @@ -594,9 +619,9 @@ dependencies = [ [[package]] name = "async-graphql-value" -version = "6.0.11" +version = "7.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323a5143f5bdd2030f45e3f2e0c821c9b1d36e79cf382129c64299c50a7f3750" +checksum = "c224c93047a7197fe0f1d6eee98245ba6049706c6c04a372864557fb61495e94" dependencies = [ "bytes", "indexmap", @@ -668,6 +693,12 @@ version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ae037714f313c1353189ead58ef9eec30a8e8dc101b2622d461418fd59e28a9" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" @@ -721,6 +752,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-compat" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7" +dependencies = [ + "byteorder", +] + [[package]] name = "bincode" version = "1.3.3" @@ -918,6 +958,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.38" @@ -1053,8 +1099,8 @@ version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" dependencies = [ - "strum 0.26.2", - "strum_macros 0.26.2", + "strum", + "strum_macros", "unicode-width", ] @@ -1065,7 +1111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" dependencies = [ "async-trait", - "convert_case", + "convert_case 0.6.0", "json5", "lazy_static", "nom", @@ -1114,6 +1160,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "convert_case" version = "0.6.0" @@ -1123,6 +1175,24 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "aes-gcm", + "base64 0.22.1", + "hkdf", + "hmac", + "percent-encoding", + "rand 0.8.5", + "sha2", + "subtle", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1255,6 +1325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1279,6 +1350,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.14.4" @@ -1472,8 +1552,8 @@ dependencies = [ "datafusion-common", "paste", "sqlparser", - "strum 0.26.2", - "strum_macros 0.26.2", + "strum", + "strum_macros", ] [[package]] @@ -1681,6 +1761,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + [[package]] name = "diff" version = "0.1.13" @@ -1750,9 +1843,9 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "dynamic-graphql" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eb2817847d7b2712a7faf8c1c36627a19e87195b23c161d10ae8c07d43a1b7f" +checksum = "fd104a4f23fc94d666917fdf6a8a9c38bec87c7f21741daadbd5282d63072924" dependencies = [ "async-graphql", "dynamic-graphql-derive", @@ -1761,9 +1854,9 @@ dependencies = [ [[package]] name = "dynamic-graphql-derive" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611b88e975cd2d3399abe6164d1a96bf80c3488818aaa8847a4aabcd53f96803" +checksum = "0d8b5d72ddb5add27d0e071039643c894ebdfd14fac88eed61d9111af8395d27" dependencies = [ "Inflector", "darling 0.20.9", @@ -2104,6 +2197,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.28.1" @@ -2133,7 +2236,26 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", "indexmap", "slab", "tokio", @@ -2154,9 +2276,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "4.5.0" +version = "5.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa67bab9ff362228eb3d00bd024a4965d8231bbb7921167f0cfa66c6626b225" +checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" dependencies = [ "log", "pest", @@ -2185,14 +2307,14 @@ dependencies = [ [[package]] name = "headers" -version = "0.3.9" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ "base64 0.21.7", "bytes", "headers-core", - "http", + "http 1.1.0", "httpdate", "mime", "sha1", @@ -2200,11 +2322,11 @@ dependencies = [ [[package]] name = "headers-core" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http", + "http 1.1.0", ] [[package]] @@ -2231,6 +2353,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2257,6 +2388,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -2264,7 +2406,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -2296,9 +2461,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -2310,6 +2475,26 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.5", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + [[package]] name = "hyper-rustls" version = "0.24.2" @@ -2317,13 +2502,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", - "hyper", + "http 0.2.12", + "hyper 0.14.28", "rustls", "tokio", "tokio-rustls", ] +[[package]] +name = "hyper-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.3.1", + "pin-project-lite", + "tokio", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -2504,6 +2704,20 @@ dependencies = [ "serde", ] +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.7", + "pem", + "ring 0.16.20", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kdam" version = "0.5.1" @@ -2824,19 +3038,19 @@ dependencies = [ [[package]] name = "multer" -version = "2.1.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" dependencies = [ "bytes", "encoding_rs", "futures-util", - "http", + "http 1.1.0", "httparse", - "log", "memchr", "mime", "spin 0.9.8", + "tokio", "version_check", ] @@ -2910,12 +3124,13 @@ dependencies = [ [[package]] name = "nix" -version = "0.27.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags 2.5.0", "cfg-if 1.0.0", + "cfg_aliases", "libc", ] @@ -3029,6 +3244,26 @@ dependencies = [ "libc", ] +[[package]] +name = "oauth2" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" +dependencies = [ + "base64 0.13.1", + "chrono", + "getrandom 0.2.15", + "http 0.2.12", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror", + "url", +] + [[package]] name = "object" version = "0.32.2" @@ -3080,6 +3315,12 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.1.5" @@ -3312,6 +3553,15 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3468,31 +3718,41 @@ dependencies = [ [[package]] name = "poem" -version = "1.3.59" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504774c97b0744c1ee108a37e5a65a9745a4725c4c06277521dabc28eb53a904" +checksum = "e88b6912ed1e8833d7c22c9c986c517f4518d7d37e3c04566d917c789aaea591" dependencies = [ - "async-trait", - "base64 0.21.7", + "base64 0.22.1", "bytes", + "chrono", + "cookie", "futures-util", "headers", - "http", - "hyper", + "http 1.1.0", + "http-body-util", + "hyper 1.3.1", + "hyper-util", "mime", + "multer", "nix", "parking_lot", "percent-encoding", "pin-project-lite", "poem-derive", + "quick-xml", "regex", "rfc7239", "serde", "serde_json", "serde_urlencoded", + "serde_yaml", "smallvec", + "sync_wrapper 1.0.1", + "tempfile", "thiserror", + "time", "tokio", + "tokio-stream", "tokio-tungstenite", "tokio-util", "tracing", @@ -3501,14 +3761,57 @@ dependencies = [ [[package]] name = "poem-derive" -version = "1.3.59" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2b961d58a6c53380c20236394381d9292fda03577f902b158f1638932964dcf" +dependencies = [ + "proc-macro-crate 3.1.0", + "proc-macro2", + "quote", + "syn 2.0.65", +] + +[[package]] +name = "poem-openapi" +version = "5.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6445b50be2e26f142d4e554d15773fc1e7510b994083c9625a65eba0d3f4287" +dependencies = [ + "base64 0.22.1", + "bytes", + "derive_more", + "futures-util", + "indexmap", + "mime", + "num-traits", + "poem", + "poem-openapi-derive", + "quick-xml", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "serde_yaml", + "thiserror", + "tokio", +] + +[[package]] +name = "poem-openapi-derive" +version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ddcf4680d8d867e1e375116203846acb088483fa2070244f90589f458bbb31" +checksum = "e890165626ff447a1ff3d6f2293e6ccacbf7fcbdd4c94086aa548de655735b03" dependencies = [ - "proc-macro-crate 2.0.0", + "darling 0.20.9", + "http 1.1.0", + "indexmap", + "mime", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", + "regex", "syn 2.0.65", + "thiserror", ] [[package]] @@ -3610,6 +3913,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "pometry-storage" version = "0.8.1" @@ -3654,11 +3969,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "toml_edit 0.20.7", + "toml_edit 0.21.1", ] [[package]] @@ -3789,6 +4104,16 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quickcheck" version = "0.9.2" @@ -4081,6 +4406,7 @@ dependencies = [ "async-graphql-poem", "async-stream", "base64 0.21.7", + "base64-compat", "bincode", "chrono", "config", @@ -4088,6 +4414,8 @@ dependencies = [ "dynamic-graphql", "futures-util", "itertools 0.12.1", + "jsonwebtoken", + "oauth2", "once_cell", "opentelemetry", "opentelemetry-jaeger", @@ -4095,16 +4423,20 @@ dependencies = [ "ordered-float 4.2.0", "parking_lot", "poem", + "poem-openapi", "raphtory", + "reqwest", "serde", "serde_json", "tempfile", "thiserror", + "time", "tokio", "toml", "tracing", "tracing-opentelemetry", "tracing-subscriber", + "url", "uuid", "walkdir", ] @@ -4238,10 +4570,10 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-rustls", "ipnet", "js-sys", @@ -4257,7 +4589,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-rustls", @@ -4613,6 +4945,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.6" @@ -4634,6 +4976,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -4686,6 +5041,18 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -4832,6 +5199,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "static_assertions_next" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" + [[package]] name = "streaming-decompression" version = "0.1.2" @@ -4874,35 +5247,13 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" -dependencies = [ - "strum_macros 0.25.3", -] - [[package]] name = "strum" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" dependencies = [ - "strum_macros 0.26.2", -] - -[[package]] -name = "strum_macros" -version = "0.25.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.65", + "strum_macros", ] [[package]] @@ -4952,6 +5303,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -5328,9 +5688,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", @@ -5386,9 +5746,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.7" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ "indexmap", "toml_datetime", @@ -5507,14 +5867,14 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.1.0", "httparse", "log", "rand 0.8.5", @@ -5610,6 +5970,22 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.7.1" @@ -5631,6 +6007,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 52857c70c2..8d8916160f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,9 +34,9 @@ debug = true pometry-storage = { version = "0.8.1", path = "pometry-storage" } #[private-storage] # pometry-storage = { path = "pometry-storage-private", package = "pometry-storage-private" } -async-graphql = { version = "6.0.11", features = ["dynamic-schema"] } -async-graphql-poem = "6.0.11" -dynamic-graphql = "0.8.1" +async-graphql = { version = "7.0.5", features = ["dynamic-schema"] } +async-graphql-poem = "7.0.5" +dynamic-graphql = "0.9.0" reqwest = { version = "0.11.22", default-features = false, features = ["rustls-tls"] } serde = { version = "1.0.197", features = ["derive", "rc"] } serde_json = "1.0.114" @@ -51,7 +51,7 @@ tokio = { version = "1.36.0", features = ["full"] } once_cell = "1.19.0" parking_lot = { version = "0.12.1", features = ["serde", "arc_lock", "send_guard"] } ordered-float = "4.2.0" -chrono = { version = "0.4.37", features = ["serde"] } +chrono = { version = "0.4.38", features = ["serde"] } tempfile = "3.10.0" futures-util = "0.3.30" thiserror = "1.0.57" @@ -77,6 +77,8 @@ bzip2 = "0.4.4" tantivy = "0.22" async-trait = "0.1.77" async-openai = "0.17.1" +oauth2 = "4.0" +jsonwebtoken = "8.0" num = "0.4.1" display-error-chain = "0.2.0" polars-arrow = "0.39.2" @@ -92,7 +94,8 @@ proptest = "1.4.0" criterion = "0.5.1" crossbeam-channel = "0.5.11" base64 = "0.21.7" -poem = "1.3.59" +poem = "3.0.1" +poem-openapi = "5.0.2" async-stream = "0.3.5" opentelemetry = "0.21.0" opentelemetry_sdk = { version = "0.21.0", features = ["rt-tokio"] } @@ -101,7 +104,7 @@ tracing = "0.1.37" tracing-opentelemetry = "0.22.0" tracing-subscriber = { version = "0.3.16", features = ["std", "env-filter"] } walkdir = "2" -uuid = "1.7.0" +uuid = { version = "1.0", features = ["v4"] } config = "0.14.0" either = "=1.11.0" toml = "0.8.10" @@ -121,6 +124,9 @@ bytemuck = { version = "1.15.0" } rpds = { version = "1.1.0", features = ["serde"] } thread_local = "1.1.8" ouroboros = "0.18.3" +url = "2.2" +base64-compat = { package = "base64-compat", version = "1.0.0" } +time = "0.3.36" lazy_static = "1.4.0" pest = "2.7.8" diff --git a/python/src/graphql.rs b/python/src/graphql.rs index c3aabfb205..cb5714a122 100644 --- a/python/src/graphql.rs +++ b/python/src/graphql.rs @@ -343,12 +343,13 @@ impl PyRaphtoryServer { /// /// Arguments: /// * `port`: the port to use (defaults to 1736). - #[pyo3(signature = (port = 1736, log_level="INFO".to_string(),enable_tracing=false))] + #[pyo3(signature = (port = 1736, log_level="INFO".to_string(),enable_tracing=false,enable_auth=false))] pub fn start( slf: PyRefMut, port: u16, log_level: String, enable_tracing: bool, + enable_auth: bool, ) -> PyResult { let (sender, receiver) = crossbeam_channel::bounded::(1); let server = take_server_ownership(slf)?; @@ -361,8 +362,10 @@ impl PyRaphtoryServer { .build() .unwrap() .block_on(async move { - let handler = server.start_with_port(port, &log_level, enable_tracing); - let tokio_sender = handler._get_sender().clone(); + let handler = + server.start_with_port(port, &log_level, enable_tracing, enable_auth); + let running_server = handler.await; + let tokio_sender = running_server._get_sender().clone(); tokio::task::spawn_blocking(move || { match receiver.recv().expect("Failed to wait for cancellation") { BridgeCommand::StopServer => tokio_sender @@ -371,7 +374,7 @@ impl PyRaphtoryServer { BridgeCommand::StopListening => (), } }); - let result = handler.wait().await; + let result = running_server.wait().await; _ = cloned_sender.send(BridgeCommand::StopListening); result }) @@ -384,15 +387,17 @@ impl PyRaphtoryServer { /// /// Arguments: /// * `port`: the port to use (defaults to 1736). - #[pyo3(signature = (port = 1736, log_level="INFO".to_string(),enable_tracing=false))] + #[pyo3(signature = (port = 1736, log_level="INFO".to_string(),enable_tracing=false,enable_auth=false))] pub fn run( slf: PyRefMut, py: Python, port: u16, log_level: String, enable_tracing: bool, + enable_auth: bool, ) -> PyResult<()> { - let mut server = Self::start(slf, port, log_level, enable_tracing)?.server_handler; + let mut server = + Self::start(slf, port, log_level, enable_tracing, enable_auth)?.server_handler; py.allow_threads(|| wait_server(&mut server)) } } diff --git a/raphtory-graphql/Cargo.toml b/raphtory-graphql/Cargo.toml index b2a2c3442a..07a7fc6b16 100644 --- a/raphtory-graphql/Cargo.toml +++ b/raphtory-graphql/Cargo.toml @@ -23,6 +23,8 @@ serde = { workspace = true } serde_json = { workspace = true } once_cell = { workspace = true } poem = { workspace = true } +poem-openapi = { workspace = true } +oauth2 = { workspace = true } tokio = { workspace = true } async-graphql = { workspace = true, features=["apollo_tracing"] } dynamic-graphql = { workspace = true } @@ -30,6 +32,7 @@ async-graphql-poem = { workspace = true } parking_lot = { workspace = true } futures-util = { workspace = true } async-stream = { workspace = true } +jsonwebtoken = { workspace = true } opentelemetry = { workspace = true } opentelemetry_sdk = { workspace = true } opentelemetry-jaeger = { workspace = true } @@ -42,6 +45,10 @@ uuid = { workspace = true } chrono = { workspace = true } config = { workspace = true } toml = { workspace = true } +url = { workspace = true } +base64-compat = { workspace = true } +time = { workspace = true } +reqwest = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/raphtory-graphql/readme.md b/raphtory-graphql/readme.md new file mode 100644 index 0000000000..d2b82941f6 --- /dev/null +++ b/raphtory-graphql/readme.md @@ -0,0 +1,99 @@ + +# Raphtory-GraphQL + +## Overview + +Raphtory-GraphQL is part of the Raphtory project, an in-memory vectorized graph database designed for high performance and scalability. This module provides GraphQL support for Raphtory, allowing users to interact with their graph data through GraphQL queries. + +## Features + +- **In-Memory Graph Database:** Offers high-speed data processing and querying capabilities. +- **GraphQL Integration:** Allows seamless integration of graph data with web applications through a GraphQL API. +- **Authentication Support:** Includes options to run the server with authentication, ensuring secure access to the graph data. + +## Installation + +Clone the repository and navigate to the `raphtory-graphql` directory: +```bash +git clone https://github.com/Pometry/Raphtory.git +cd Raphtory/raphtory-graphql +``` + +## Configuration + +Ensure you have the required environment variables set up. For example, set the `GRAPH_DIRECTORY` environment variable: +```bash +export GRAPH_DIRECTORY=/path/to/your/graph_directory +``` + +Create a `config.toml` file with your specific configuration settings. + +## Running the Server + +By default, the server runs without authentication. To run the server, use the following command: +```bash +cargo run +``` + +This command starts the Raphtory server using `from_directory.run`. + +## Running the Server with Authentication (Microsoft) + +### Setting up Authentication + +To enable authentication for the Raphtory-GraphQL server, you need to set up a `.env` file with specific properties from Microsoft. This file should include the following properties: + +- `CLIENT_ID` +- `CLIENT_SECRET` +- `TENANT_ID` +- `AUTHORITY` + +#### Steps + +1. **Azure Portal Registration:** + - Go to the [Azure Portal](https://portal.azure.com/). + - Navigate to "Azure Active Directory" in the left-hand menu. + +2. **Register a New Application:** + - Click on "App registrations" and then "New registration." + - Enter a name for your application. + - Select the supported account types (typically "Accounts in this organizational directory only"). + - Click "Register." + +3. **Get the Client ID and Tenant ID:** + - After registration, you will be taken to the application's overview page. + - Copy the `Application (client) ID` and `Directory (tenant) ID` values. These are your `CLIENT_ID` and `TENANT_ID`, respectively. + +4. **Create a Client Secret:** + - In the left-hand menu, select "Certificates & secrets." + - Click on "New client secret." + - Provide a description and set an expiry period. + - Click "Add." + - Copy the value of the client secret. This is your `CLIENT_SECRET`. + +5. **Set the Authority:** + - The `AUTHORITY` is typically in the format `https://login.microsoftonline.com/{TENANT_ID}`. + +#### Example .env File + +Create a `.env` file in the root directory of your project and add the obtained properties: + +```env +CLIENT_ID=your_client_id +CLIENT_SECRET=your_client_secret +TENANT_ID=your_tenant_id +AUTHORITY=https://login.microsoftonline.com/your_tenant_id +``` + +Ensure that this file is included in your `.gitignore` to prevent sensitive information from being exposed. + +With these settings configured, your Raphtory-GraphQL server will be able to use Microsoft authentication. + +### Running the Auth server + +To run the server with authentication, pass the `--server` argument: +```bash +cargo run -- --server +``` + +This command starts the Raphtory server using `run_with_auth`, which includes authentication mechanisms to secure access. diff --git a/raphtory-graphql/src/azure_auth/common.rs b/raphtory-graphql/src/azure_auth/common.rs new file mode 100644 index 0000000000..28050797f4 --- /dev/null +++ b/raphtory-graphql/src/azure_auth/common.rs @@ -0,0 +1,243 @@ +use base64_compat::{decode_config, URL_SAFE_NO_PAD}; +use chrono::{Duration, Utc}; +use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation}; +use oauth2::{ + basic::BasicClient, reqwest::async_http_client, AuthorizationCode, CsrfToken, + PkceCodeChallenge, PkceCodeVerifier, Scope, TokenResponse, +}; +use poem::{ + handler, + http::StatusCode, + web::{ + cookie::{Cookie, CookieJar}, + Data, Json, Query, Redirect, + }, + IntoResponse, Response, +}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::{ + collections::HashMap, + env, + error::Error, + sync::{Arc, Mutex}, +}; + +#[derive(Deserialize, Serialize)] +struct AuthRequest { + code: String, + state: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Jwks { + pub(crate) keys: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Jwk { + pub(crate) kid: String, + kty: String, + #[serde(rename = "use")] + use_: String, + pub(crate) n: String, + pub(crate) e: String, +} + +#[derive(Clone)] +pub struct AppState { + pub(crate) oauth_client: Arc, + pub(crate) csrf_state: Arc>>, + pub(crate) pkce_verifier: Arc>>, + pub(crate) jwks: Arc, +} + +#[handler] +pub async fn login(data: Data<&AppState>, jar: &CookieJar) -> Redirect { + let session_id = uuid::Uuid::new_v4().to_string(); + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + let (authorize_url, csrf_state) = data + .oauth_client + .authorize_url(CsrfToken::new_random) + .set_pkce_challenge(pkce_challenge) + .add_scope(Scope::new("openid".to_string())) + .add_scope(Scope::new("email".to_string())) + .add_scope(Scope::new("offline_access".to_string())) + .add_scope(Scope::new( + "a10e734e-cb36-46ca-bbfd-c298e15b6327/public-scope".to_string(), + )) + .url(); + + data.csrf_state + .lock() + .unwrap() + .insert(session_id.clone(), csrf_state); + data.pkce_verifier + .lock() + .unwrap() + .insert(session_id.clone(), pkce_verifier); + + let mut session_cookie = Cookie::new("session_id", session_id); + session_cookie.set_path("/"); + session_cookie.set_http_only(true); + jar.add(session_cookie); + + Redirect::temporary(authorize_url.to_string().as_str()) +} + +#[handler] +pub async fn auth_callback( + data: Data<&AppState>, + query: Query, + jar: &CookieJar, +) -> impl IntoResponse { + if let Some(session_cookie) = jar.get("session_id") { + let session_id = session_cookie.value::().unwrap(); + + let code = AuthorizationCode::new(query.0.code.clone()); + let pkce_verifier = data + .pkce_verifier + .lock() + .unwrap() + .remove(&session_id) + .unwrap(); + + let token_result = data + .oauth_client + .exchange_code(code) + .set_pkce_verifier(pkce_verifier) + .request_async(async_http_client) + .await; + + match token_result { + Ok(token) => { + let access_token = token.access_token(); + let expires_in = token + .expires_in() + .unwrap_or(core::time::Duration::from_secs(60 * 60 * 24)); + let expiration = Utc::now() + Duration::from_std(expires_in).unwrap(); + + let token_data = json!({ + "access_token_secret": access_token.secret(), + "expires_at": expiration.to_rfc3339() + }); + + let mut auth_cookie = Cookie::new("auth_token", token_data.to_string()); + auth_cookie.set_expires(expiration); + auth_cookie.set_path("/"); + auth_cookie.set_http_only(true); + jar.add(auth_cookie); + return Redirect::temporary("/").into_response(); + } + Err(_err) => { + return Response::builder() + .status(StatusCode::UNAUTHORIZED) + .content_type("application/json") + .body(json!({"error": "Login failed"}).to_string()); + } + } + } else { + return Response::builder() + .status(StatusCode::UNAUTHORIZED) + .content_type("application/json") + .body(json!({"error": "Session not found. Please login again"}).to_string()); + } +} + +pub fn decode_base64_urlsafe(base64_str: &str) -> Result, Box> { + let decoded = decode_config(base64_str, URL_SAFE_NO_PAD)?; + Ok(decoded) +} + +#[handler] +pub async fn secure_endpoint() -> Json { + Json(serde_json::json!({ + "message": "Secured" + })) +} + +#[handler] +pub async fn logout(jar: &CookieJar) -> String { + if let Some(mut cookie) = jar.get("auth_token") { + cookie.set_expires(Utc::now() - Duration::days(1)); + jar.remove("auth_token"); + } + if let Some(mut cookie) = jar.get("session_id") { + cookie.set_expires(Utc::now() - Duration::days(1)); + jar.remove("session_id"); + } + "You have been logged out.".to_string() +} + +#[handler] +pub async fn verify(data: Data<&AppState>, jar: &CookieJar) -> Json { + if let Some(_session_cookie) = jar.get("session_id") { + if let Some(cookie) = jar.get("auth_token") { + let cookie_value = cookie.value::().expect("Unable to find cookie"); + let token_data: serde_json::Value = + serde_json::from_str(&cookie_value).expect("Invalid cookie format"); + let token = token_data["access_token_secret"] + .as_str() + .expect("No access token found"); + let expires_at_str = token_data["expires_at"] + .as_str() + .expect("No expiration time found"); + + let expires_at = chrono::DateTime::parse_from_rfc3339(expires_at_str) + .expect("Invalid expiration format"); + + if Utc::now() > expires_at { + return Json(serde_json::json!({ + "message": "Access token expired, please login again" + })); + } + + let header = decode_header(token).expect("Unable to decode header"); + let kid = header.kid.expect("Token header does not have a kid field"); + + let jwk = data + .jwks + .keys + .iter() + .find(|&jwk| jwk.kid == kid) + .expect("Key ID not found in JWKS"); + + let n = decode_base64_urlsafe(&jwk.n).unwrap(); + let e = decode_base64_urlsafe(&jwk.e).unwrap(); + + let decoding_key = DecodingKey::from_rsa_raw_components(&n, &e); + + let validation = Validation::new(Algorithm::RS256); + + let token_data = + decode::>(token, &decoding_key, &validation); + + match token_data { + Ok(_dc) => Json(serde_json::json!({ + "message": "Valid access token", + })), + Err(_err) => Json(serde_json::json!({ + "message": "No valid auth token found", + })), + } + } else { + Json(serde_json::json!({ + "message": "No cookie auth_token found, please login" + })) + } + } else { + Json(serde_json::json!({ + "message": "No session_id found, please login" + })) + } +} + +pub async fn get_jwks() -> Result> { + let authority = env::var("AUTHORITY").expect("AUTHORITY not set"); + let jwks_url = format!("{}/discovery/v2.0/keys", authority); + let client = Client::new(); + let response = client.get(&jwks_url).send().await?; + let jwks = response.json::().await?; + Ok(jwks) +} diff --git a/raphtory-graphql/src/azure_auth/mod.rs b/raphtory-graphql/src/azure_auth/mod.rs new file mode 100644 index 0000000000..423dcd51fb --- /dev/null +++ b/raphtory-graphql/src/azure_auth/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod common; + +pub(crate) mod token_middleware; diff --git a/raphtory-graphql/src/azure_auth/token_middleware.rs b/raphtory-graphql/src/azure_auth/token_middleware.rs new file mode 100644 index 0000000000..f5366c3407 --- /dev/null +++ b/raphtory-graphql/src/azure_auth/token_middleware.rs @@ -0,0 +1,103 @@ +use crate::azure_auth::common::{decode_base64_urlsafe, AppState}; +use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation}; +use poem::{ + http::StatusCode, web::Redirect, Endpoint, Error, IntoResponse, Middleware, Request, Response, + Result, +}; +use std::{collections::HashMap, sync::Arc}; + +#[derive(Clone)] +pub struct TokenMiddleware { + app_state: Arc, +} + +impl TokenMiddleware { + pub fn new(app_state: Arc) -> Self { + TokenMiddleware { app_state } + } +} + +impl Middleware for TokenMiddleware { + type Output = TokenMiddlewareImpl; + + fn transform(&self, ep: E) -> Self::Output { + TokenMiddlewareImpl { + ep, + app_state: self.app_state.clone(), + } + } +} + +pub struct TokenMiddlewareImpl { + ep: E, + app_state: Arc, +} + +#[allow(dead_code)] +#[derive(Clone)] +struct Token(String); + +impl Endpoint for TokenMiddlewareImpl { + type Output = Response; + + async fn call(&self, mut req: Request) -> Result { + let jar = req.cookie().clone(); + if let Some(_session_cookie) = jar.get("session_id") { + if let Some(auth_cookie) = jar.get("auth_token") { + let token_data: serde_json::Value = serde_json::from_str( + &auth_cookie + .value::() + .expect("Unable to find cookie"), + ) + .map_err(|_| Error::from_status(StatusCode::UNAUTHORIZED))?; + let access_token = token_data["access_token_secret"] + .as_str() + .ok_or_else(|| Error::from_status(StatusCode::UNAUTHORIZED))?; + let expires_at_str = token_data["expires_at"] + .as_str() + .ok_or_else(|| Error::from_status(StatusCode::UNAUTHORIZED))?; + let expires_at = chrono::DateTime::parse_from_rfc3339(expires_at_str) + .map_err(|_| Error::from_status(StatusCode::UNAUTHORIZED))?; + if chrono::Utc::now() > expires_at { + return Err(Error::from_status(StatusCode::UNAUTHORIZED)); + } + + let header = decode_header(access_token) + .map_err(|_| Error::from_status(StatusCode::UNAUTHORIZED))?; + let kid = header + .kid + .ok_or_else(|| Error::from_status(StatusCode::UNAUTHORIZED))?; + + let jwk = self + .app_state + .jwks + .keys + .iter() + .find(|&jwk| jwk.kid == kid) + .ok_or_else(|| Error::from_status(StatusCode::UNAUTHORIZED))?; + + let n = decode_base64_urlsafe(&jwk.n) + .map_err(|_| Error::from_status(StatusCode::UNAUTHORIZED))?; + let e = decode_base64_urlsafe(&jwk.e) + .map_err(|_| Error::from_status(StatusCode::UNAUTHORIZED))?; + + let decoding_key = DecodingKey::from_rsa_raw_components(&n, &e); + + let validation = Validation::new(Algorithm::RS256); + decode::>( + access_token, + &decoding_key, + &validation, + ) + .map_err(|_| Error::from_status(StatusCode::UNAUTHORIZED))?; + + req.extensions_mut().insert(Token(access_token.to_string())); + + return self.ep.call(req).await.map(IntoResponse::into_response); + } + Ok(Redirect::temporary("/login").into_response()) + } else { + Ok(Redirect::temporary("/login").into_response()) + } + } +} diff --git a/raphtory-graphql/src/lib.rs b/raphtory-graphql/src/lib.rs index b056f1ef06..92e059a2d6 100644 --- a/raphtory-graphql/src/lib.rs +++ b/raphtory-graphql/src/lib.rs @@ -7,6 +7,8 @@ mod observability; mod routes; pub mod server; +pub mod azure_auth; + mod data; #[derive(thiserror::Error, Debug)] diff --git a/raphtory-graphql/src/main.rs b/raphtory-graphql/src/main.rs index 185c36bd7e..be7ff95714 100644 --- a/raphtory-graphql/src/main.rs +++ b/raphtory-graphql/src/main.rs @@ -1,19 +1,32 @@ use crate::server::RaphtoryServer; use std::env; +mod azure_auth; mod data; mod model; mod observability; mod routes; mod server; +extern crate base64_compat as base64_compat; + #[tokio::main] async fn main() { let graph_directory = env::var("GRAPH_DIRECTORY").unwrap_or("/tmp/graphs".to_string()); let config_path = "config.toml"; - RaphtoryServer::from_directory(&graph_directory) - .run(config_path, false) - .await - .unwrap() + let args: Vec = env::args().collect(); + let use_auth = args.contains(&"--server".to_string()); + + if use_auth { + RaphtoryServer::from_directory(&graph_directory) + .run_with_auth(config_path, false) + .await + .unwrap(); + } else { + RaphtoryServer::from_directory(&graph_directory) + .run(config_path, false) + .await + .unwrap(); + } } diff --git a/raphtory-graphql/src/routes.rs b/raphtory-graphql/src/routes.rs index 22865da9c4..cd90a4832f 100644 --- a/raphtory-graphql/src/routes.rs +++ b/raphtory-graphql/src/routes.rs @@ -21,6 +21,8 @@ pub(crate) async fn health() -> impl IntoResponse { #[handler] pub(crate) async fn graphql_playground() -> impl IntoResponse { Html(playground_source( - GraphQLPlaygroundConfig::new("/").subscription_endpoint("/ws"), + GraphQLPlaygroundConfig::new("/") + .subscription_endpoint("/ws") + .with_setting("request.credentials", "include"), )) } diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index b78531ff68..73e9639b5e 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -1,5 +1,9 @@ #![allow(dead_code)] use crate::{ + azure_auth::{ + common::{auth_callback, get_jwks, login, logout, verify, AppState}, + token_middleware::TokenMiddleware, + }, data::Data, model::{ algorithms::{algorithm::Algorithm, algorithm_entry_point::AlgorithmEntryPoint}, @@ -10,8 +14,15 @@ use crate::{ }; use async_graphql::extensions::ApolloTracing; use async_graphql_poem::GraphQL; +use dotenv::dotenv; use itertools::Itertools; -use poem::{get, listener::TcpListener, middleware::Cors, EndpointExt, Route, Server}; +use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; +use poem::{ + get, + listener::TcpListener, + middleware::{CookieJarManager, CookieJarManagerEndpoint, Cors, CorsEndpoint}, + EndpointExt, Route, Server, +}; use raphtory::{ db::api::view::{DynamicGraph, IntoDynamic, MaterializedGraph}, vectors::{ @@ -21,7 +32,12 @@ use raphtory::{ }, }; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fs, path::Path, sync::Arc}; +use std::{ + collections::HashMap, + env, fs, + path::Path, + sync::{Arc, Mutex}, +}; use tokio::{ io::Result as IoResult, signal, @@ -138,16 +154,23 @@ impl RaphtoryServer { } /// Start the server on the default port and return a handle to it. - pub fn start(self, log_config_or_level: &str, enable_tracing: bool) -> RunningRaphtoryServer { - self.start_with_port(1736, log_config_or_level, enable_tracing) + pub async fn start( + self, + log_config_or_level: &str, + enable_tracing: bool, + enable_auth: bool, + ) -> RunningRaphtoryServer { + self.start_with_port(1736, log_config_or_level, enable_tracing, enable_auth) + .await } /// Start the server on the port `port` and return a handle to it. - pub fn start_with_port( + pub async fn start_with_port( self, port: u16, log_config_or_level: &str, enable_tracing: bool, + enable_auth: bool, ) -> RunningRaphtoryServer { fn parse_log_level(input: &str) -> Option { // Parse log level from string @@ -212,6 +235,32 @@ impl RaphtoryServer { .unwrap_or(()); // it is important that this runs after algorithms have been pushed to PLUGIN_ALGOS static variable + + let app: CorsEndpoint> = if enable_auth { + println!("Generating endpoint with auth"); + self.generate_microsoft_endpoint_with_auth(enable_tracing, port) + .await + } else { + self.generate_endpoint(enable_tracing).await + }; + + let (signal_sender, signal_receiver) = mpsc::channel(1); + + println!("Playground: http://localhost:{port}"); + let server_task = Server::new(TcpListener::bind(format!("127.0.0.1:{port}"))) + .run_with_graceful_shutdown(app, server_termination(signal_receiver), None); + let server_result = tokio::spawn(server_task); + + RunningRaphtoryServer { + signal_sender, + server_result, + } + } + + async fn generate_endpoint( + self, + enable_tracing: bool, + ) -> CorsEndpoint> { let schema_builder = App::create_schema(); let schema_builder = schema_builder.data(self.data); let schema = if enable_tracing { @@ -220,27 +269,117 @@ impl RaphtoryServer { } else { schema_builder.finish().unwrap() }; + let app = Route::new() .at("/", get(graphql_playground).post(GraphQL::new(schema))) .at("/health", get(health)) + .with(CookieJarManager::new()) .with(Cors::new()); + app + } - let (signal_sender, signal_receiver) = mpsc::channel(1); + async fn generate_microsoft_endpoint_with_auth( + self, + enable_tracing: bool, + port: u16, + ) -> CorsEndpoint> { + let schema_builder = App::create_schema(); + let schema_builder = schema_builder.data(self.data); + let schema = if enable_tracing { + let schema_builder = schema_builder.extension(ApolloTracing); + schema_builder.finish().unwrap() + } else { + schema_builder.finish().unwrap() + }; - println!("Playground: http://localhost:{port}"); - let server_task = Server::new(TcpListener::bind(format!("0.0.0.0:{port}"))) - .run_with_graceful_shutdown(app, server_termination(signal_receiver), None); - let server_result = tokio::spawn(server_task); + dotenv().ok(); + println!("Loading env"); + let client_id_str = env::var("CLIENT_ID").expect("CLIENT_ID not set"); + let client_secret_str = env::var("CLIENT_SECRET").expect("CLIENT_SECRET not set"); + let tenant_id_str = env::var("TENANT_ID").expect("TENANT_ID not set"); + + let client_id = ClientId::new(client_id_str); + let client_secret = ClientSecret::new(client_secret_str); + + let auth_url = AuthUrl::new(format!( + "https://login.microsoftonline.com/{}/oauth2/v2.0/authorize", + tenant_id_str.clone() + )) + .expect("Invalid authorization endpoint URL"); + let token_url = TokenUrl::new(format!( + "https://login.microsoftonline.com/{}/oauth2/v2.0/token", + tenant_id_str.clone() + )) + .expect("Invalid token endpoint URL"); + + println!("Loading client"); + let client = BasicClient::new( + client_id.clone(), + Some(client_secret.clone()), + auth_url, + Some(token_url), + ) + .set_redirect_uri( + RedirectUrl::new(format!( + "http://localhost:{}/auth/callback", + port.to_string() + )) + .expect("Invalid redirect URL"), + ); + + println!("Fetching JWKS"); + let jwks = get_jwks().await.expect("Failed to fetch JWKS"); + + let app_state = AppState { + oauth_client: Arc::new(client), + csrf_state: Arc::new(Mutex::new(HashMap::new())), + pkce_verifier: Arc::new(Mutex::new(HashMap::new())), + jwks: Arc::new(jwks), + }; - RunningRaphtoryServer { - signal_sender, - server_result, - } + let token_middleware = TokenMiddleware::new(Arc::new(app_state.clone())); + + println!("Making app"); + let app = Route::new() + .at( + "/", + get(graphql_playground) + .post(GraphQL::new(schema)) + .with(token_middleware.clone()), + ) + .at("/health", get(health)) + .at("/login", login.data(app_state.clone())) + .at("/auth/callback", auth_callback.data(app_state.clone())) + .at( + "/verify", + verify + .data(app_state.clone()) + .with(token_middleware.clone()), + ) + .at("/logout", logout.with(token_middleware.clone())) + .with(CookieJarManager::new()) + .with(Cors::new()); + println!("App done"); + app } /// Run the server on the default port until completion. pub async fn run(self, log_config_or_level: &str, enable_tracing: bool) -> IoResult<()> { - self.start(log_config_or_level, enable_tracing).wait().await + self.start(log_config_or_level, enable_tracing, false) + .await + .wait() + .await + } + + pub async fn run_with_auth( + self, + log_config_or_level: &str, + enable_tracing: bool, + ) -> IoResult<()> { + self.start(log_config_or_level, enable_tracing, true) + .await + .wait() + .await } /// Run the server on the port `port` until completion. @@ -250,7 +389,8 @@ impl RaphtoryServer { log_config_or_level: &str, enable_tracing: bool, ) -> IoResult<()> { - self.start_with_port(port, log_config_or_level, enable_tracing) + self.start_with_port(port, log_config_or_level, enable_tracing, false) + .await .wait() .await } @@ -340,10 +480,9 @@ mod server_tests { let graphs = HashMap::from([("test".to_owned(), g)]); let server = RaphtoryServer::from_map(graphs); println!("calling start at time {}", Local::now()); - let handler = server.start_with_port(0, "info", false); + let handler = server.start_with_port(0, "info", false, false); sleep(Duration::from_secs(1)).await; println!("Calling stop at time {}", Local::now()); - handler.stop().await; - handler.wait().await.unwrap() + handler.await.stop().await } }