diff --git a/clients/rust/.gitignore b/clients/rust/.gitignore new file mode 100644 index 000000000..2f7896d1d --- /dev/null +++ b/clients/rust/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/clients/rust/Cargo.lock b/clients/rust/Cargo.lock new file mode 100644 index 000000000..241f8baa5 --- /dev/null +++ b/clients/rust/Cargo.lock @@ -0,0 +1,1292 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "cc" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c891175c3fb232128f48de6590095e59198bbeb8620c310be349bfc3afd12c7b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap", + "strsim 0.10.0", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap" +version = "4.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex 0.7.1", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "clap_lex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +dependencies = [ + "clap 3.2.25", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "gamma_rust_client" +version = "0.1.0" +dependencies = [ + "rand", + "reqwest", + "serde", + "thiserror", + "urlencoding", + "uuid", +] + +[[package]] +name = "gamma_rust_client_tests" +version = "1.0.0" +dependencies = [ + "clap 4.5.7", + "dotenvy", + "eyre", + "gamma_rust_client", + "tokio", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[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 = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[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", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "object" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags 2.5.0", +] + +[[package]] +name = "reqwest" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "serde", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] diff --git a/clients/rust/Cargo.toml b/clients/rust/Cargo.toml new file mode 100644 index 000000000..486c662c0 --- /dev/null +++ b/clients/rust/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "2" +members = ["client", "tests"] diff --git a/clients/rust/client/.gitignore b/clients/rust/client/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/clients/rust/client/.gitignore @@ -0,0 +1 @@ +/target diff --git a/clients/rust/client/Cargo.toml b/clients/rust/client/Cargo.toml new file mode 100644 index 000000000..e1199c39a --- /dev/null +++ b/clients/rust/client/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "gamma_rust_client" +version = "0.1.0" +edition = "2021" + +[dependencies] +rand = "0.8.5" +reqwest = { version = "0.12.5", default-features = false, features = ["json"] } +serde = { version = "1.0.203", features = ["derive"] } +thiserror = "1.0.61" +urlencoding = "2.1.3" +uuid = { version = "1.8.0", features = ["serde"] } + +[features] +default = ["api", "oauth"] +oauth = [] +api = [] diff --git a/clients/rust/client/src/api.rs b/clients/rust/client/src/api.rs new file mode 100644 index 000000000..af16536e4 --- /dev/null +++ b/clients/rust/client/src/api.rs @@ -0,0 +1,269 @@ +use reqwest::{header::AUTHORIZATION, Client, RequestBuilder}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{ + config::GammaConfig, + error::{GammaError, GammaResult}, +}; + +const PRE_SHARED: &str = "pre-shared"; + +/// A client to be used in order to perform calls to the gamma API requiring an API key. +#[derive(Debug, Clone)] +pub struct GammaClient { + client: Client, + gamma_url: String, + gamma_api_key: String, +} + +impl GammaClient { + /// Create a new GammaClient from the provided config. + pub fn new(config: &GammaConfig) -> Self { + Self { + client: Client::new(), + gamma_url: format!("{}/api/client", config.gamma_url), + gamma_api_key: config.gamma_api_key.clone(), + } + } + + /// Get all groups. + pub async fn get_groups(&self) -> GammaResult> { + let request = self.client.get(format!("{}/v1/groups", self.gamma_url)); + + let groups: Vec = self + .handle_gamma_request(request, "get groups endpoint") + .await?; + + Ok(groups) + } + + /// Get all super groups. + pub async fn get_super_groups(&self) -> GammaResult> { + let request = self + .client + .get(format!("{}/v1/superGroups", self.gamma_url)); + + let super_groups: Vec = self + .handle_gamma_request(request, "get supergroups endpoint") + .await?; + + Ok(super_groups) + } + + /// Get a single gamma user (that has accepted this client) by its user_id. + pub async fn get_user(&self, user_id: &Uuid) -> GammaResult { + let request = self + .client + .get(format!("{}/v1/users/{user_id}", self.gamma_url)); + + let user: GammaUser = self + .handle_gamma_request(request, "get user endpoint") + .await?; + + Ok(user) + } + + /// Get all users that have accepted this client (this is usually done by authorizing against this client at least once). + pub async fn get_users(&self) -> GammaResult> { + let request = self.client.get(format!("{}/v1/users", self.gamma_url)); + + let users = self + .handle_gamma_request(request, "get users endpoint") + .await?; + + Ok(users) + } + + /// Get all groups that the user with the provided `user_id` are a part of. + pub async fn get_groups_for_user(&self, user_id: &Uuid) -> GammaResult> { + let request = self + .client + .get(format!("{}/v1/groups/for/{user_id}", self.gamma_url)); + + let user_groups = self + .handle_gamma_request(request, "get groups for user endpoint") + .await?; + + Ok(user_groups) + } + + /// Get all authorities for this client. + pub async fn get_authorities(&self) -> GammaResult> { + let request = self + .client + .get(format!("{}/v1/authorities", self.gamma_url)); + + let authorities = self + .handle_gamma_request(request, "get authorities endpoint") + .await?; + + Ok(authorities) + } + + /// Get all authorities for the provided `user_id` and this client. + pub async fn get_authorities_for_user(&self, user_id: &Uuid) -> GammaResult> { + let request = self + .client + .get(format!("{}/v1/authorities/for/{user_id}", self.gamma_url)); + + let authorities = self + .handle_gamma_request(request, "get authorities for user endpoint") + .await?; + + Ok(authorities) + } + + async fn handle_gamma_request( + &self, + request: RequestBuilder, + context: &str, + ) -> GammaResult + where + T: DeserializeOwned, + { + let response = request + .header( + AUTHORIZATION, + format!("{PRE_SHARED} {}", self.gamma_api_key), + ) + .send() + .await + .map_err(|err| GammaError::FailedSendingRequest { + context: context.into(), + err, + })?; + + let status = response.status(); + if !status.is_success() { + if status == 404 { + return Err(GammaError::NotFoundResponse { + context: context.into(), + }); + } + + let body_str = response + .text() + .await + .unwrap_or("Failed to read response body".into()); + + return Err(GammaError::ErrorResponse { + context: context.into(), + status, + body_str, + }); + } + + let body: T = + response + .json() + .await + .map_err(|err| GammaError::FailedToDeserializeResponse { + context: context.into(), + error: err, + })?; + + Ok(body) + } +} + +/// A group in gamma (e.g. digIT'21). +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GammaGroup { + /// A unique identifier for the group. + pub id: Uuid, + /// The name of the group. + pub name: String, + /// The pretty name of the group. + pub pretty_name: String, + /// The supergroup this group belongs to (e.g. digIT). + pub super_group: GammaSuperGroup, +} + +/// A supergroup in gamma (e.g. digIT). +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GammaSuperGroup { + /// A unique identifier for the supergroup. + pub id: Uuid, + /// The name of the supergroup (e.g. "digit"). + pub name: String, + /// The pretty name of the supergroup (e.g. "digIT"). + pub pretty_name: String, + /// The type of supergroup this is. + #[serde(rename = "type")] + pub group_type: GammaSuperGroupType, + /// The swedish description of the supergroup. + pub sv_description: String, + /// The english description of the supergroup. + pub en_description: String, +} + +/// A type of supergroup. +/// Note: In gamma these are generic strings and can be created and deleted through the GUI, +/// for this reason an `Other` option is provided as a cath all and the remaining options +/// are simply the ones currently in use at the time of writing. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum GammaSuperGroupType { + /// An alumni group (not active). + Alumni, + /// A committee within the IT divison. + Committee, + /// A society recognized by the IT division. + Society, + /// Functionary groups within the IT division (e.g. auditors). + Functionaries, + /// Gamma administrators. + Admin, + /// A type that of group that is not specificly supported by this client. + #[serde(untagged)] + Other(String), +} + +/// A gamma user. +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GammaUser { + /// A unique identifier for the user. + pub id: Uuid, + /// The Chalmers ID for the person. + pub cid: String, + /// The IT nickname of the user. + pub nick: String, + /// The first name of the person. + pub first_name: String, + /// The surname of the person. + pub last_name: String, + /// Which year they were accepted to chalmers. + pub acceptance_year: i32, +} + +/// A post within a gamma group. +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GammaPost { + /// A unique identifier for the post. + pub id: Uuid, + /// The swedish version of the name for this post (e.g. "Ordförande"). + pub sv_name: String, + /// The english version of name the for this post (e.g. "Chairman"). + pub en_name: String, +} + +/// Connection between a user and a group containing information about the group, its supergroup as well as which post the user held within that group. +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GammaUserGroup { + /// The group ID. + pub id: Uuid, + /// The name of the group (e.g. "digit22"). + pub name: String, + /// A pretty name of the group (e.g. "digIT 22/23"). + pub pretty_name: String, + /// The supergroup for this group (e.g. didIT). + pub super_group: GammaSuperGroup, + /// The post the user held within this group (e.g. treasurer). + pub post: GammaPost, +} diff --git a/clients/rust/client/src/config.rs b/clients/rust/client/src/config.rs new file mode 100644 index 000000000..593f86099 --- /dev/null +++ b/clients/rust/client/src/config.rs @@ -0,0 +1,18 @@ +/// Configuration parameters required for a gamma client. +#[derive(Debug, Clone)] +pub struct GammaConfig { + /// Client ID for the gamma oauth client. + pub gamma_client_id: String, + /// Client Secret for the gamma oauth client. + pub gamma_client_secret: String, + /// The URI that was registered for the gamma client (i.e. where gamma should redirect to after + /// successful auth). + pub gamma_redirect_uri: String, + /// The URL to gamma. + pub gamma_url: String, + /// The scopes that should be requested, space separated e.g. "openid profile". + pub scopes: String, + #[cfg(feature = "api")] + /// The API key that should be used when requesting information from the API endpoints. + pub gamma_api_key: String, +} diff --git a/clients/rust/client/src/error.rs b/clients/rust/client/src/error.rs new file mode 100644 index 000000000..69c083704 --- /dev/null +++ b/clients/rust/client/src/error.rs @@ -0,0 +1,47 @@ +/// An error that can occur within the library. +#[derive(Debug, thiserror::Error)] +pub enum GammaError { + /// The state received from gamma did not match the state we sent to them. + #[error("State sent to gamma did not match what we got in the callback")] + GammaStateMissmatch, + /// The callback from gamma did not contain the required code query parameter. + #[error("The callback from gamma did not contain the expected code query parameter")] + NoCodeReceived, + /// Failed sending a request to gamma. + #[error("Failed sending request to gamma in context {context:?}")] + FailedSendingRequest { + /// Which endpoint the request failed for. + context: String, + /// The error that occurred. + #[source] + err: reqwest::Error, + }, + /// Got a non 2XX response from the gamma API. + #[error("Got an error from gamma in context {context:?} status {status:?} body: {body_str:?}")] + ErrorResponse { + /// Which endpoint the request failed for. + context: String, + /// The error status returned. + status: reqwest::StatusCode, + /// The stringified response body. + body_str: String, + }, + /// The response body did not match the expected response body. + #[error("Failed to deserialize gamma response in context {context:?}")] + FailedToDeserializeResponse { + /// The endpoint deserialization failed for. + context: String, + /// The error that occurred. + #[source] + error: reqwest::Error, + }, + /// Got a 404 NOT_FOUND from gamma. + #[error("Got a 404 Not Found response from gamma")] + NotFoundResponse { + /// The endpoint that returned 404. + context: String, + }, +} + +/// The result type used within this client. +pub type GammaResult = Result; diff --git a/clients/rust/client/src/lib.rs b/clients/rust/client/src/lib.rs new file mode 100644 index 000000000..a5a6bc0f0 --- /dev/null +++ b/clients/rust/client/src/lib.rs @@ -0,0 +1,59 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +//! # Gamma Rust client library +//! +//! A client for the Gamma Auth service for the Rust programming language. +//! The library consists of two, mainly separate, parts. +//! - The [oauth] part for performing oauth2 login with gamma and retrieving information through its Open ID Connect API. +//! - The [api] part for interacting with the client API using a client API key. +//! +//! ## Usage example for the [oauth] part +//! +//! ```rust +//! # use gamma_rust_client::config::GammaConfig; +//! # use gamma_rust_client::error::GammaResult; +//! # use gamma_rust_client::oauth::{gamma_init_auth, GammaState}; +//! # +//! # fn begin_auth(gamma_config: &GammaConfig) -> GammaResult<()> { +//! let init = gamma_init_auth(gamma_config)?; +//! // Redirect the user to `init.redirect_to` and store `init.state` for later. +//! # Ok(()) +//! # } +//! +//! // User authorizes and we get a code back. +//! # async fn retrieve_auth_token(gamma_config: &GammaConfig, stored_state: GammaState, response_state: &str, response_code: String) -> GammaResult<()> { +//! let access_token = stored_state.gamma_callback_params(gamma_config, response_state, response_code).await?; +//! let user = access_token.get_current_user(gamma_config).await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Usage example for the [api] part. +//! +//! ```rust +//! # use gamma_rust_client::config::GammaConfig; +//! # use gamma_rust_client::error::GammaResult; +//! # use gamma_rust_client::api::GammaClient; +//! # +//! # async fn use_api(gamma_config: &GammaConfig) -> GammaResult<()> { +//! let client = GammaClient::new(gamma_config); +//! +//! client.get_groups().await?; +//! # Ok(()) +//! # } +//! ``` + +/// Configurations for the gamma api. +pub mod config; + +/// Endpoints and types for the gamma client API. +#[cfg(feature = "api")] +pub mod api; + +/// Endpoints and types used for the oauth flow. +#[cfg(feature = "oauth")] +pub mod oauth; + +/// Error types used within the lib. +pub mod error; diff --git a/clients/rust/client/src/oauth.rs b/clients/rust/client/src/oauth.rs new file mode 100644 index 000000000..0f288d513 --- /dev/null +++ b/clients/rust/client/src/oauth.rs @@ -0,0 +1,244 @@ +use std::collections::HashMap; + +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use reqwest::Client; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{ + config::GammaConfig, + error::{GammaError, GammaResult}, +}; + +/// The `state` variable sent to gamma using auth, required for verifying the session after +/// gamma redirects back to us. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GammaState(String); + +/// The reslt of gamma_init_auth. +pub struct GammaInit { + /// The state that should be stored and later used to verify the redirect. + pub state: GammaState, + /// The URL to redirect to. + pub redirect_to: String, +} + +/// Begins the auth flow with gamma, returning a GammaInit containing a redirect url to which the +/// user should be redirected to. +pub fn gamma_init_auth(config: &GammaConfig) -> GammaResult { + let state = GammaState::new(); + + let scopes = urlencoding::encode(&config.scopes); + + let redirect_uri = format!( + "{}/oauth2/authorize?response_type=code&client_id={}&scope={scopes}&redirect_uri={}&state={}", + config.gamma_url, config.gamma_client_id, config.gamma_redirect_uri, state.get_state() + ); + + Ok(GammaInit { + state, + redirect_to: redirect_uri, + }) +} + +impl GammaState { + /// Generate a new state that can be used when querying gamma. + pub fn new() -> Self { + Self( + thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect(), + ) + } + + /// Create a new gamma state from the provided string. + pub fn get_state_str(state: String) -> Self { + Self(state) + } + + /// Returns the contained state + pub fn get_state(&self) -> &str { + &self.0 + } + + /// When receiving a callback from the gamma API. + pub async fn gamma_callback( + &self, + config: &GammaConfig, + query_params: QueryParams, + ) -> GammaResult + where + QueryParams: IntoIterator, + QueryName: AsRef, + QueryValue: AsRef, + { + let params: HashMap = query_params + .into_iter() + .map(|(a, b)| (a.as_ref().to_string(), b.as_ref().to_string())) + .collect(); + + if params.get("state") != Some(&self.0) { + return Err(GammaError::GammaStateMissmatch); + } + + let Some(code) = params.get("code") else { + return Err(GammaError::NoCodeReceived); + }; + + gamma_get_oauth2_token(config, code.into()).await + } + + /// Same as `gamma_callback` but providing the required parameters directly. + /// Note that `callback_state` and `callback_code` should be the values received from gamma in + /// the callback redirect as query parameters. + pub async fn gamma_callback_params( + &self, + config: &GammaConfig, + callback_state: State, + callback_code: String, + ) -> GammaResult + where + State: AsRef, + { + if callback_state.as_ref() != self.get_state() { + return Err(GammaError::GammaStateMissmatch); + } + + gamma_get_oauth2_token(config, callback_code).await + } +} + +impl Default for GammaState { + fn default() -> Self { + Self::new() + } +} + +impl From for GammaState { + fn from(value: String) -> Self { + Self(value) + } +} + +#[derive(Debug, Clone, Serialize)] +struct GammaTokenRequest { + client_id: String, + client_secret: String, + code: String, + redirect_uri: String, + grant_type: String, +} + +#[derive(Debug, Clone, Deserialize)] +struct GammaTokenResponse { + access_token: String, +} + +/// An oauth2 access token that can be used to call gamma APIs on behalf of a user. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GammaAccessToken(String); + +/// Retrieve a gamma oauth2 token from the code received in a callback. +async fn gamma_get_oauth2_token( + config: &GammaConfig, + code: String, +) -> GammaResult { + let client = Client::new(); + let url = format!("{}/oauth2/token", config.gamma_url); + + let request = client + .post(&url) + .form(&GammaTokenRequest { + client_id: config.gamma_client_id.clone(), + client_secret: config.gamma_client_secret.clone(), + code, + redirect_uri: config.gamma_redirect_uri.clone(), + grant_type: "authorization_code".into(), + }) + .header("accept", "application/json") + .send() + .await; + + let body: GammaTokenResponse = + handle_gamma_request(request, "Get token endpoint".into()).await?; + + Ok(GammaAccessToken(body.access_token)) +} + +/// A gamma user retrieved from the OpenID Connect API. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct GammaOpenIDUser { + /// The ID of the gamma user. + #[serde(rename = "sub")] + pub user_id: Uuid, + /// The chalmers ID of this person. + pub cid: String, + /// The IT nick of this person. + #[serde(rename = "nickname")] + pub nick: String, + /// The firstname of the person. + pub given_name: String, + /// The family name of the person. + pub family_name: String, + /// A url pointing to the picture uploaded by this user (if any). + #[serde(rename = "picture")] + pub avatar_url: String, + + /// The email the user has registered in gamma. + /// Requires the `email` scope! + pub email: Option, +} + +impl GammaAccessToken { + /// Get the openid user information for this gamma user. + pub async fn get_current_user(&self, config: &GammaConfig) -> GammaResult { + let client = Client::new(); + let url = format!("{}/oauth2/userinfo", config.gamma_url); + + let request = client.get(&url).bearer_auth(self.0.clone()).send().await; + + let body: GammaOpenIDUser = + handle_gamma_request(request, "Get userinfo endpoint".into()).await?; + Ok(body) + } +} + +async fn handle_gamma_request( + request: reqwest::Result, + context: String, +) -> GammaResult +where + T: DeserializeOwned, +{ + let response = request.map_err(|err| GammaError::FailedSendingRequest { + context: context.clone(), + err, + })?; + + let status = response.status(); + if !status.is_success() { + let body_str = response + .text() + .await + .unwrap_or("Failed to read response body".into()); + + return Err(GammaError::ErrorResponse { + context: context.clone(), + status, + body_str, + }); + } + + let body: T = + response + .json() + .await + .map_err(|error| GammaError::FailedToDeserializeResponse { + context: context.clone(), + error, + })?; + + Ok(body) +} diff --git a/clients/rust/tests/.env.example b/clients/rust/tests/.env.example new file mode 100644 index 000000000..e68762912 --- /dev/null +++ b/clients/rust/tests/.env.example @@ -0,0 +1,7 @@ +GAMMA_CLIENT_ID=id +GAMMA_CLIENT_SECRET=secret +GAMMA_API_KEY=userid:key +GAMMA_URL=https://auth.chalmers.it +GAMMA_REDIRECT_URL=http://localhost:8080 + + diff --git a/clients/rust/tests/.gitignore b/clients/rust/tests/.gitignore new file mode 100644 index 000000000..b7f7b5178 --- /dev/null +++ b/clients/rust/tests/.gitignore @@ -0,0 +1,2 @@ +.env +target/ diff --git a/clients/rust/tests/Cargo.toml b/clients/rust/tests/Cargo.toml new file mode 100644 index 000000000..90c9e5145 --- /dev/null +++ b/clients/rust/tests/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "gamma_rust_client_tests" +version = "1.0.0" +edition = "2021" +license = "MIT or Apache-2.0" +authors = ["Vidar Magnusson "] +description = "Rust bindings for the IT division auth service API" + +[dependencies] +clap = { version = "4.5.7", features = ["env", "derive"] } +dotenvy = { version = "0.15.7", features = ["clap"] } +eyre = "0.6.12" +tokio = { version = "1.38.0", features = ["full"] } + +[dependencies.gamma_rust_client] +path = "../client" +features = ["api"] diff --git a/clients/rust/tests/src/main.rs b/clients/rust/tests/src/main.rs new file mode 100644 index 000000000..a900ffde5 --- /dev/null +++ b/clients/rust/tests/src/main.rs @@ -0,0 +1,188 @@ +use clap::{Parser, Subcommand}; +use gamma_rust_client::{ + api::GammaClient, + config::GammaConfig, + oauth::{gamma_init_auth, GammaState}, +}; + +#[derive(Parser, Debug)] +struct Args { + #[arg(long, env = "GAMMA_CLIENT_ID")] + gamma_client_id: String, + + #[arg(long, env = "GAMMA_CLIENT_SECRET")] + gamma_client_secret: String, + + #[arg(long, env = "GAMMA_API_KEY")] + gamma_api_key: String, + + #[arg(long, env = "GAMMA_URL")] + gamma_url: String, + + #[arg(long, env = "GAMMA_REDIRECT_URL")] + gamma_redirect_url: String, + + #[command(subcommand)] + action: TestAction, + + /// Weather or not to print (almost) complete retrieved content. + #[arg(long, short)] + debug: bool, +} + +#[derive(Subcommand, Debug)] +enum TestAction { + Init, + Finish { code: String }, + Api, +} + +#[tokio::main] +async fn main() { + dotenvy::dotenv().ok(); + + let args = Args::parse(); + run(args).await.expect("failed to run test with argument"); +} + +async fn run(args: Args) -> eyre::Result<()> { + let gamma_config = GammaConfig { + gamma_client_id: args.gamma_client_id, + gamma_client_secret: args.gamma_client_secret, + gamma_redirect_uri: args.gamma_redirect_url, + gamma_url: args.gamma_url, + scopes: "openid profile email".into(), + gamma_api_key: args.gamma_api_key, + }; + + match args.action { + TestAction::Init => init(&gamma_config), + TestAction::Finish { code } => finish(args.debug, &gamma_config, code).await, + TestAction::Api => api(args.debug, &gamma_config).await, + } +} + +fn init(gamma_config: &GammaConfig) -> eyre::Result<()> { + let init = gamma_init_auth(gamma_config)?; + println!("To perform the test, go to the link below and when authorization is complete (redirected back to the configured redirect URL), extract the `code` query param and use for the next step.\n\n Link: {}", init.redirect_to); + Ok(()) +} + +async fn finish(debug: bool, gamma_config: &GammaConfig, code: String) -> eyre::Result<()> { + // Note: We are testing the rust client, not gamma itself so we don't care about the state + // parameter in this case. In real auth flows it is important to verify that the state returned + // by gamma is identical to the state provided when initializing the auth flow. + let state = "IGNORED".to_string(); + + let gamma_state: GammaState = state.clone().into(); + + println!("Retrieving auth token"); + let auth_token = gamma_state + .gamma_callback_params(gamma_config, state, code) + .await?; + + println!("Retrieved auth token complete, using it to retrieve userinfo"); + + let user_info = auth_token.get_current_user(gamma_config).await?; + println!("Retrieving user info was successful"); + + if debug { + println!("\tUserinfo: {user_info:?}"); + } + + Ok(()) +} + +async fn api(debug: bool, gamma_config: &GammaConfig) -> eyre::Result<()> { + let client = GammaClient::new(gamma_config); + + // Get groups + println!("Retrieving groups..."); + let groups = client.get_groups().await?; + println!("Successfully retrieved {} groups", groups.len()); + if debug { + for group in groups.iter() { + println!(" - {{{group:?}}}"); + } + } + + println!("\n\n"); + + // Get super groups + println!("Retrieving super groups..."); + let super_groups = client.get_super_groups().await?; + println!("Successfully retrieved {} super groups", super_groups.len()); + if debug { + for super_group in super_groups.iter() { + println!(" - {{{super_group:?}}}"); + } + } + + println!("\n\n"); + + // Get authorities + println!("Retrieving authorities..."); + let authorities = client.get_authorities().await?; + println!("Successfully retrieved {} authorities", authorities.len()); + if debug { + for authority in authorities.iter() { + println!(" - {{{authority:?}}}"); + } + } + + println!("\n\n"); + + // Get users + println!("Retrieving users..."); + let users = client.get_users().await?; + println!("Successfully retrieved {} users", users.len()); + if debug { + for user in users.iter() { + println!(" - {{{user:?}}}"); + } + } + + println!("\n\n"); + + if let Some(user) = users.first() { + // Get user (again) + println!("Retrieving user by ID {}", user.id); + let user = client.get_user(&user.id).await?; + println!("Successfully retrieved user by its ID"); + if debug { + println!(" - {{{user:?}}}"); + } + + // Get groups for user + println!("Retrieving groups for a specific user... Picking the first user received in the previous call ()."); + let user_groups = client.get_groups_for_user(&user.id).await?; + println!( + "Successfully retrieved {} groups for the user", + user_groups.len() + ); + if debug { + for group in user_groups.iter() { + println!(" - {{{group:?}}}"); + } + } + + println!("\n\n"); + + // Get authorities for user + println!("Retrieving authorities for a specific user... Picking the first user received in the get users call ()."); + let user_authorities = client.get_authorities_for_user(&user.id).await?; + println!( + "Successfully retrieved {} authorities for the user", + user_authorities.len() + ); + if debug { + for authority in user_authorities.iter() { + println!(" - {{{authority:?}}}"); + } + } + } else { + println!("No users received, skipping endpoints requiring a user ID") + } + + Ok(()) +}