From eb2212d6a1ea0400be5287beb5331871f54b233b Mon Sep 17 00:00:00 2001 From: Ayush Jain <76424614+ayushjain17@users.noreply.github.com> Date: Fri, 20 Dec 2024 17:41:55 +0530 Subject: [PATCH] feat: Add auth via OAUTH2 (#321) * feat: Add auth via OAUTH2 * feat: Make auth endpoints part of AuthHandler --- .env.example | 8 +- .../workflows/example_docker_image_gen.yaml | 4 +- Cargo.lock | 572 +++++++++++++++++- Cargo.toml | 5 +- Dockerfile | 2 + crates/context_aware_config/Cargo.toml | 4 +- crates/frontend/Cargo.toml | 2 +- crates/frontend/src/api.rs | 17 + crates/frontend/src/app.rs | 10 + crates/frontend/src/pages.rs | 1 + crates/frontend/src/pages/organisations.rs | 58 ++ crates/service_utils/Cargo.toml | 4 +- crates/service_utils/src/db/utils.rs | 17 + .../service_utils/src/middlewares/tenant.rs | 4 +- crates/superposition/Cargo.toml | 6 + crates/superposition/src/app_state.rs | 9 +- crates/superposition/src/auth.rs | 165 +++++ .../superposition/src/auth/authenticator.rs | 22 + crates/superposition/src/auth/no_auth.rs | 45 ++ crates/superposition/src/auth/oidc.rs | 376 ++++++++++++ crates/superposition/src/auth/oidc/types.rs | 51 ++ crates/superposition/src/auth/oidc/utils.rs | 30 + crates/superposition/src/main.rs | 49 +- crates/superposition_types/src/database.rs | 4 +- .../src/database/models.rs | 1 - crates/superposition_types/src/lib.rs | 12 - example.Dockerfile | 2 + 27 files changed, 1407 insertions(+), 73 deletions(-) create mode 100644 crates/frontend/src/pages/organisations.rs create mode 100644 crates/superposition/src/auth.rs create mode 100644 crates/superposition/src/auth/authenticator.rs create mode 100644 crates/superposition/src/auth/no_auth.rs create mode 100644 crates/superposition/src/auth/oidc.rs create mode 100644 crates/superposition/src/auth/oidc/types.rs create mode 100644 crates/superposition/src/auth/oidc/utils.rs diff --git a/.env.example b/.env.example index 8e81a063..cb4412d0 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,12 @@ ACTIX_KEEP_ALIVE=120 MAX_DB_CONNECTION_POOL_SIZE=3 ENABLE_TENANT_AND_SCOPE=true TENANTS=dev,test -TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/assets/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin,/" +TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/assets/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin,/oidc/login,/admin/organisations,/organisations,/organisations/switch/{organisation_id},/" SERVICE_PREFIX="" SERVICE_NAME="CAC" +AUTH_PROVIDER=DISABLED +## AUTH_PROVIDER=OIDC+http://localhost:8081/realms/users +OIDC_CLIENT_ID=superposition +OIDC_CLIENT_SECRET=superposition_secret +OIDC_TOKEN_ENDPOINT_FORMAT="http://localhost:8081/realms//protocol/openid-connect/token" +OIDC_ISSUER_ENDPOINT_FORMAT="http://http://localhost:8081/realms/" \ No newline at end of file diff --git a/.github/workflows/example_docker_image_gen.yaml b/.github/workflows/example_docker_image_gen.yaml index db46b28c..c6da9a79 100644 --- a/.github/workflows/example_docker_image_gen.yaml +++ b/.github/workflows/example_docker_image_gen.yaml @@ -26,8 +26,8 @@ jobs: id: git_tag shell: bash run: | - docker_tag=`git tag -l --sort=-creatordate | grep "^v" | head -n 1 | sed 's/^v//'` - echo "docker_tag=$docker_tag" >> $GITHUB_OUTPUT + docker_tag=`git tag -l --sort=-creatordate | grep "^v" | head -n 1 | sed 's/^v//'` + echo "docker_tag=$docker_tag" >> $GITHUB_OUTPUT - name: Login to Docker Hub uses: docker/login-action@v3 diff --git a/Cargo.lock b/Cargo.lock index 519d2161..2a6a4d60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -814,6 +814,18 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.2" @@ -836,6 +848,12 @@ dependencies = [ "vsimd", ] +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bigdecimal" version = "0.3.1" @@ -1022,7 +1040,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c878c71c2821aa2058722038a59a67583a4240524687c6028571c9b395ded61f" dependencies = [ - "darling", + "darling 0.14.4", "proc-macro2", "quote", "syn 1.0.109", @@ -1152,7 +1170,7 @@ dependencies = [ "bitflags 1.3.2", "clap_lex 0.2.4", "indexmap 1.9.3", - "strsim", + "strsim 0.10.0", "termcolor", "textwrap", ] @@ -1178,7 +1196,7 @@ dependencies = [ "anstyle", "bitflags 1.3.2", "clap_lex 0.5.0", - "strsim", + "strsim 0.10.0", ] [[package]] @@ -1254,6 +1272,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const_format" version = "0.2.31" @@ -1407,6 +1431,18 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1417,6 +1453,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "cxx" version = "1.0.94" @@ -1467,8 +1530,18 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core 0.20.10", + "darling_macro 0.20.10", ] [[package]] @@ -1481,21 +1554,46 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 1.0.109", ] +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.48", +] + [[package]] name = "darling_macro" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ - "darling_core", + "darling_core 0.14.4", "quote", "syn 1.0.109", ] +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core 0.20.10", + "quote", + "syn 2.0.48", +] + [[package]] name = "dashmap" version = "5.4.0" @@ -1509,6 +1607,27 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "derive-where" version = "1.2.5" @@ -1589,11 +1708,12 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -1610,12 +1730,77 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.32" @@ -1752,6 +1937,22 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "flate2" version = "1.0.26" @@ -1973,6 +2174,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -2034,6 +2236,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "h2" version = "0.3.24" @@ -2117,6 +2330,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" @@ -2322,6 +2544,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -2332,6 +2555,7 @@ checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", "hashbrown 0.14.2", + "serde", ] [[package]] @@ -2486,6 +2710,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "leptos" @@ -2722,6 +2949,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "linear-map" version = "1.2.0" @@ -2966,6 +3199,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-cmp" version = "0.1.0" @@ -2981,6 +3231,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.45" @@ -3021,6 +3277,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3033,6 +3290,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", + "http 0.2.9", + "rand", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror", + "url", +] + [[package]] name = "object" version = "0.30.4" @@ -3048,6 +3325,38 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "openidconnect" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47e80a9cfae4462dd29c41e987edd228971d6565553fbc14b8a11e666d91590" +dependencies = [ + "base64 0.13.1", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", + "http 0.2.9", + "itertools 0.10.5", + "log", + "oauth2", + "p256", + "p384", + "rand", + "rsa", + "serde", + "serde-value", + "serde_derive", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2", + "subtle", + "thiserror", + "url", +] + [[package]] name = "openssl" version = "0.10.54" @@ -3092,6 +3401,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "os_str_bytes" version = "6.6.1" @@ -3104,6 +3422,30 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "pad-adapter" version = "0.1.1" @@ -3145,6 +3487,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3228,12 +3579,39 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -3259,6 +3637,15 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -3481,6 +3868,7 @@ dependencies = [ "http 0.2.9", "http-body 0.4.5", "hyper", + "hyper-rustls", "hyper-tls", "ipnet", "js-sys", @@ -3490,19 +3878,33 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "winreg", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.16.20" @@ -3539,6 +3941,26 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e60ef3b82994702bbe4e134d98aadca4b49ed04440148985678d415c68127666" +[[package]] +name = "rsa" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rstml" version = "0.11.2" @@ -3692,6 +4114,20 @@ dependencies = [ "untrusted 0.7.1", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.9.1" @@ -3748,6 +4184,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde-wasm-bindgen" version = "0.6.5" @@ -3790,6 +4236,25 @@ 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_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_qs" version = "0.12.0" @@ -3831,6 +4296,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.0.2", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling 0.20.10", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "server_fn" version = "0.6.11" @@ -3944,6 +4439,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "skeptic" version = "0.13.7" @@ -4016,12 +4521,28 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[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 = "strum" version = "0.25.0" @@ -4053,6 +4574,7 @@ version = "0.1.0" dependencies = [ "actix-files", "actix-web", + "aws-sdk-kms", "cac_toml", "cfg-if", "context_aware_config", @@ -4061,14 +4583,19 @@ dependencies = [ "experimentation_platform", "fred", "frontend", + "futures-util", "leptos", "leptos_actix", + "log", + "openidconnect", "reqwest", "rs-snowflake", + "serde", "serde_json", "service_utils", "superposition_types", "toml 0.8.8", + "url", ] [[package]] @@ -4276,11 +4803,14 @@ dependencies = [ [[package]] name = "time" -version = "0.3.21" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ + "deranged", "itoa", + "num-conv", + "powerfmt", "serde", "time-core", "time-macros", @@ -4288,16 +4818,17 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ + "num-conv", "time-core", ] @@ -4586,6 +5117,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -4785,6 +5317,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "winapi" version = "0.3.9" @@ -5077,9 +5615,9 @@ checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" [[package]] name = "zeroize" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zstd" diff --git a/Cargo.toml b/Cargo.toml index 3e216cf5..82bcd23c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,8 +32,9 @@ assets-dir = "crates/frontend/assets" [workspace.dependencies] actix-web = "4.5.0" anyhow = "1.0.75" +aws-sdk-kms = { version = "1.38.0" } base64 = "0.21.2" -bigdecimal = { version = "0.3.1" , features= ["serde"]} +bigdecimal = { version = "0.3.1", features = ["serde"] } cfg-if = "1.0.0" chrono = { version = "0.4.26", features = ["serde"] } derive_more = "^0.99" @@ -46,6 +47,7 @@ diesel = { version = "2.1.0", features = [ "postgres_backend", ] } fred = { version = "9.2.1" } +futures-util = "0.3.28" itertools = { version = "0.10.5" } jsonlogic = { git = "https://github.com/juspay/jsonlogic_rs.git", version = "0.5.3" } jsonschema = "~0.17" @@ -60,6 +62,7 @@ serde_json = { version = "1.0" } strum = "0.25" strum_macros = "0.25" toml = { version = "0.8.8", features = ["preserve_order"] } +url = "2.5.0" uuid = { version = "1.3.4", features = ["v4", "serde"] } [workspace.lints.clippy] diff --git a/Dockerfile b/Dockerfile index dbbe35c5..3f623724 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,6 +42,8 @@ RUN ls -l target FROM debian:bookworm-slim as runtime +RUN mkdir -p /app/crates/superposition +COPY --from=builder /build/crates/superposition/Superposition.cac.toml /app/crates/superposition/Superposition.cac.toml ENV NODE_VERSION=18.19.0 WORKDIR /app diff --git a/crates/context_aware_config/Cargo.toml b/crates/context_aware_config/Cargo.toml index 6edcaff0..30cf0e3a 100644 --- a/crates/context_aware_config/Cargo.toml +++ b/crates/context_aware_config/Cargo.toml @@ -17,9 +17,9 @@ cac_client = { path = "../cac_client" } cfg-if = { workspace = true } chrono = { workspace = true } derive_more = { workspace = true } -diesel = { workspace = true , features = ["numeric"]} +diesel = { workspace = true, features = ["numeric"] } fred = { workspace = true, optional = true, features = ["metrics"] } -futures-util = "0.3.28" +futures-util = { workspace = true } itertools = { workspace = true } jsonlogic = { workspace = true } jsonschema = { workspace = true } diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 9be7ff93..aac4840e 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -27,7 +27,7 @@ strum_macros = { workspace = true } superposition_types = { path = "../superposition_types", features = [ "experimentation", ], default-features = false } -url = "2.5.0" +url = { workspace = true } wasm-bindgen = "=0.2.89" web-sys = { version = "0.3.64", features = [ "Event", diff --git a/crates/frontend/src/api.rs b/crates/frontend/src/api.rs index 30cfdab7..8cbfc8f1 100644 --- a/crates/frontend/src/api.rs +++ b/crates/frontend/src/api.rs @@ -257,6 +257,23 @@ pub async fn delete_dimension(name: String, tenant: String) -> Result<(), String Ok(()) } +pub async fn fetch_organisations() -> Result, ServerFnError> { + let client = reqwest::Client::new(); + let host = use_host_server(); + let url = format!("{host}/organisations"); + + match client.get(url).send().await { + Ok(organisations) => { + let organisations = organisations + .json() + .await + .map_err(|err| ServerFnError::new(err.to_string()))?; + Ok(organisations) + } + Err(e) => Err(ServerFnError::new(e.to_string())), + } +} + pub async fn fetch_types( filters: &PaginationParams, tenant: String, diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index aeb26a1c..fb88ff44 100755 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -14,6 +14,7 @@ use crate::pages::function::{ use crate::pages::{ context_override::ContextOverride, custom_types::TypesPage, default_config::DefaultConfig, experiment::ExperimentPage, home::Home, + organisations::Organisations, }; use crate::providers::alert_provider::AlertProvider; use crate::types::Envs; @@ -93,6 +94,15 @@ pub fn app(app_envs: Envs) -> impl IntoView { + + } + } + /> impl IntoView { + let host = StoredValue::new(use_host_server()); + let (organisation_rs, organisation_ws) = create_signal::>(None); + + let organisation_resource = create_local_resource( + || (), + |_| async { fetch_organisations().await.unwrap_or_default() }, + ); + + view! { +
+ } + }> + {move || { + view! { +
+
Select Organisation
+ + +
+ } + }} + +
+
+ } +} diff --git a/crates/service_utils/Cargo.toml b/crates/service_utils/Cargo.toml index bf6cbbe0..d035b65e 100644 --- a/crates/service_utils/Cargo.toml +++ b/crates/service_utils/Cargo.toml @@ -9,13 +9,13 @@ edition = "2021" actix-web = { workspace = true } anyhow = { workspace = true } aws-config = { version = "1.1.7", features = ["behavior-version-latest"] } -aws-sdk-kms = { version = "1.38.0" } +aws-sdk-kms = { workspace = true } base64 = { workspace = true } chrono = { workspace = true } derive_more = { workspace = true } diesel = { workspace = true } fred = { workspace = true, optional = true } -futures-util = "0.3.28" +futures-util = { workspace = true } jsonschema = { workspace = true } log = { workspace = true } once_cell = { workspace = true } diff --git a/crates/service_utils/src/db/utils.rs b/crates/service_utils/src/db/utils.rs index c2de20bd..4010abb0 100644 --- a/crates/service_utils/src/db/utils.rs +++ b/crates/service_utils/src/db/utils.rs @@ -25,6 +25,23 @@ pub async fn get_superposition_token( } } +pub async fn get_oidc_client_secret( + kms_client: &Option, + app_env: &AppEnv, +) -> String { + match app_env { + AppEnv::DEV | AppEnv::TEST | AppEnv::SANDBOX => { + get_from_env_or_default("OIDC_CLIENT_SECRET", "123456".into()) + } + _ => { + let kms_client = kms_client.clone().unwrap(); + let superposition_token_raw = + kms::decrypt(kms_client, "OIDC_CLIENT_SECRET").await; + encode(superposition_token_raw.as_str()).to_string() + } + } +} + pub async fn get_database_url(kms_client: &Option, app_env: &AppEnv) -> String { let db_user: String = get_from_env_unsafe("DB_USER").unwrap(); let db_password: String = match app_env { diff --git a/crates/service_utils/src/middlewares/tenant.rs b/crates/service_utils/src/middlewares/tenant.rs index 7cf76487..bc096586 100644 --- a/crates/service_utils/src/middlewares/tenant.rs +++ b/crates/service_utils/src/middlewares/tenant.rs @@ -94,13 +94,15 @@ where }; let request_path = req.uri().path().replace(&base, ""); + let request_pattern = + req.match_pattern().unwrap_or_else(|| request_path.clone()); let pkg_regex = Regex::new(".*/pkg/.+") .map_err(|err| error::ErrorInternalServerError(err.to_string()))?; let assets_regex = Regex::new(".*/assets/.+") .map_err(|err| error::ErrorInternalServerError(err.to_string()))?; let is_excluded: bool = app_state .tenant_middleware_exclusion_list - .contains(&request_path) + .contains(&request_pattern) || pkg_regex.is_match(&request_path) || assets_regex.is_match(&request_path); diff --git a/crates/superposition/Cargo.toml b/crates/superposition/Cargo.toml index 2481c6d2..b1fdfa41 100644 --- a/crates/superposition/Cargo.toml +++ b/crates/superposition/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] actix-files = { version = "0.6" } actix-web = { workspace = true } +aws-sdk-kms = { workspace = true } cac_toml = { path = "../cac_toml" } cfg-if = { workspace = true } context_aware_config = { path = "../context_aware_config" } @@ -16,14 +17,19 @@ env_logger = "0.8" experimentation_platform = { path = "../experimentation_platform" } fred = { workspace = true, optional = true } frontend = { path = "../frontend" } +futures-util = { workspace = true } leptos = { workspace = true } leptos_actix = { version = "0.6.11" } +log = { workspace = true } +openidconnect = "3.5.0" reqwest = { workspace = true } rs-snowflake = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } service_utils = { path = "../service_utils" } superposition_types = { path = "../superposition_types" } toml = { workspace = true } +url = { workspace = true } [features] high-performance-mode = [ diff --git a/crates/superposition/src/app_state.rs b/crates/superposition/src/app_state.rs index f25b7158..0457dab7 100644 --- a/crates/superposition/src/app_state.rs +++ b/crates/superposition/src/app_state.rs @@ -16,7 +16,6 @@ use fred::{ types::{ConnectionConfig, PerformanceConfig, ReconnectPolicy, RedisConfig}, }; use service_utils::{ - aws::kms, db::utils::{get_superposition_token, init_pool_manager}, helpers::{get_from_env_or_default, get_from_env_unsafe}, service::types::{AppEnv, AppState, ExperimentationFlags}, @@ -27,6 +26,8 @@ use superposition_types::TenantConfig; const TENANT_CONFIG_FILE: &str = "crates/superposition/Superposition.cac.toml"; pub async fn get( + app_env: AppEnv, + kms_client: &Option, service_prefix: String, base: &String, tenants: &HashSet, @@ -34,7 +35,6 @@ pub async fn get( let cac_host = get_from_env_unsafe::("CAC_HOST").expect("CAC host is not set") + base; let max_pool_size = get_from_env_or_default("MAX_DB_CONNECTION_POOL_SIZE", 2); - let app_env = get_from_env_unsafe("APP_ENV").expect("APP_ENV is not set"); let enable_tenant_and_scope = get_from_env_unsafe("ENABLE_TENANT_AND_SCOPE") .expect("ENABLE_TENANT_AND_SCOPE is not set"); @@ -57,11 +57,6 @@ pub async fn get( let snowflake_generator = Arc::new(Mutex::new(SnowflakeIdGenerator::new(1, 1))); - let kms_client = match app_env { - AppEnv::DEV | AppEnv::TEST => None, - _ => Some(kms::new_client().await), - }; - cfg_if::cfg_if! { if #[cfg(feature = "high-performance-mode")] { let redis_url = diff --git a/crates/superposition/src/auth.rs b/crates/superposition/src/auth.rs new file mode 100644 index 00000000..aa4db290 --- /dev/null +++ b/crates/superposition/src/auth.rs @@ -0,0 +1,165 @@ +mod authenticator; +mod no_auth; +mod oidc; + +use std::{ + future::{ready, Ready}, + sync::Arc, +}; + +use actix_web::{ + body::{BoxBody, EitherBody}, + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + get, + http::header, + web::{self, Data, Path}, + Error, HttpMessage, HttpRequest, HttpResponse, Scope, +}; +use authenticator::{Authenticator, SwitchOrgParams}; +use aws_sdk_kms::Client; +use futures_util::future::LocalBoxFuture; +use no_auth::DisabledAuthenticator; +use service_utils::{ + db::utils::get_oidc_client_secret, + helpers::get_from_env_unsafe, + service::types::{AppEnv, AppState}, +}; +use superposition_types::User; +use url::Url; + +pub struct AuthMiddleware { + service: S, + auth_handler: AuthHandler, +} + +impl Service for AuthMiddleware +where + S: Service, Error = Error>, + S::Future: 'static, +{ + type Response = ServiceResponse>; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + // Generate polling fn. + forward_ready!(service); + + fn call(&self, request: ServiceRequest) -> Self::Future { + let state = request.app_data::>().unwrap(); + + let result = request + .headers() + .get(header::AUTHORIZATION) + .and_then(|auth| auth.to_str().ok()) + .and_then(|auth| { + let mut token = auth.split(' ').into_iter(); + match (token.next(), token.next()) { + (Some("Internal"), Some(token)) + if token == state.superposition_token => + { + request + .headers() + .get("x-user") + .and_then(|auth| auth.to_str().ok()) + .and_then(|user_str| { + serde_json::from_str::(user_str).ok() + }) + .map(Ok) + } + (_, _) => None, + } + }) + .unwrap_or_else(|| self.auth_handler.0.authenticate(&request)); + + match result { + Ok(user) => { + request.extensions_mut().insert::(user); + let fut = self.service.call(request); + Box::pin(async move { fut.await.map(|sr| sr.map_into_left_body()) }) + } + Err(resp) => Box::pin(async move { + Ok(ServiceResponse::new( + request.request().clone(), + resp.map_into_right_body(), + )) + }), + } + } +} + +#[derive(Clone)] +pub struct AuthHandler(Arc); + +impl AuthHandler { + pub fn routes(&self) -> Scope { + self.0.routes() + } + + pub fn org_routes(&self) -> Scope { + routes(self.clone()) + } + + pub async fn init(kms_client: &Option, app_env: &AppEnv) -> Self { + let auth_provider: String = get_from_env_unsafe("AUTH_PROVIDER").unwrap(); + let mut auth = auth_provider.split('+'); + + let ap: Arc = match auth.next() { + Some("DISABLED") => Arc::new(DisabledAuthenticator), + Some("OIDC") => { + let url = Url::parse(auth.next().unwrap()) + .map_err(|e| e.to_string()) + .unwrap(); + let base_url = get_from_env_unsafe("CAC_HOST").unwrap(); + let cid = get_from_env_unsafe("OIDC_CLIENT_ID").unwrap(); + let csecret = get_oidc_client_secret(kms_client, app_env).await; + Arc::new( + oidc::OIDCAuthenticator::new(url, base_url, cid, csecret) + .await + .unwrap(), + ) + } + _ => panic!("Missing/Unknown authenticator."), + }; + Self(ap) + } +} + +pub fn routes(auth: AuthHandler) -> Scope { + web::scope("organisations") + .app_data(Data::new(auth)) + .service(get_organisations) + .service(switch_organisation) +} + +#[get("")] +async fn get_organisations(data: Data, req: HttpRequest) -> HttpResponse { + data.0.get_organisations(&req) +} + +#[get("/switch/{organisation_id}")] +async fn switch_organisation( + data: Data, + req: HttpRequest, + path: Path, +) -> actix_web::Result { + data.0.switch_organisation(&req, &path).await +} + +impl Transform for AuthHandler +where + S: Service, Error = Error>, + S::Future: 'static, +{ + type Response = ServiceResponse>; + type Error = Error; + type Transform = AuthMiddleware; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(AuthMiddleware { + service, + auth_handler: self.clone(), + })) + } +} diff --git a/crates/superposition/src/auth/authenticator.rs b/crates/superposition/src/auth/authenticator.rs new file mode 100644 index 00000000..0e8391c7 --- /dev/null +++ b/crates/superposition/src/auth/authenticator.rs @@ -0,0 +1,22 @@ +use actix_web::{dev::ServiceRequest, web::Path, HttpRequest, HttpResponse, Scope}; +use futures_util::future::LocalBoxFuture; +use serde::Deserialize; +use superposition_types::User; + +#[derive(Deserialize)] +pub(super) struct SwitchOrgParams { + pub(super) organisation_id: String, +} + +pub trait Authenticator: Sync + Send { + fn authenticate(&self, request: &ServiceRequest) -> Result; + fn routes(&self) -> Scope; + + fn get_organisations(&self, req: &HttpRequest) -> HttpResponse; + + fn switch_organisation( + &self, + req: &HttpRequest, + path: &Path, + ) -> LocalBoxFuture<'static, actix_web::Result>; +} diff --git a/crates/superposition/src/auth/no_auth.rs b/crates/superposition/src/auth/no_auth.rs new file mode 100644 index 00000000..648f7c7a --- /dev/null +++ b/crates/superposition/src/auth/no_auth.rs @@ -0,0 +1,45 @@ +use actix_web::{ + cookie::{time::Duration, Cookie}, + dev::ServiceRequest, + web::Path, + HttpRequest, HttpResponse, Scope, +}; +use futures_util::future::LocalBoxFuture; +use superposition_types::User; + +use super::authenticator::{Authenticator, SwitchOrgParams}; + +pub struct DisabledAuthenticator; + +impl Authenticator for DisabledAuthenticator { + fn authenticate(&self, _: &ServiceRequest) -> Result { + Ok(User::default()) + } + + fn routes(&self) -> actix_web::Scope { + Scope::new("no_auth") + } + + fn get_organisations(&self, _: &actix_web::HttpRequest) -> HttpResponse { + HttpResponse::Ok().json(serde_json::json!(vec!["superposition"])) + } + + fn switch_organisation( + &self, + _: &HttpRequest, + _: &Path, + ) -> LocalBoxFuture<'static, actix_web::Result> { + let cookie = Cookie::build("org_user", "org_token") + .path("/") + .http_only(true) + .max_age(Duration::days(1)) + .finish(); + + Box::pin(async move { + Ok(HttpResponse::Found() + .cookie(cookie) + .insert_header(("Location", "/")) + .finish()) + }) + } +} diff --git a/crates/superposition/src/auth/oidc.rs b/crates/superposition/src/auth/oidc.rs new file mode 100644 index 00000000..425a9eca --- /dev/null +++ b/crates/superposition/src/auth/oidc.rs @@ -0,0 +1,376 @@ +mod types; +mod utils; + +use actix_web::{ + cookie::{time::Duration, Cookie}, + dev::ServiceRequest, + error::{ErrorBadRequest, ErrorInternalServerError}, + get, + http::header, + web::{self, Data, Path, Query}, + HttpRequest, HttpResponse, +}; +use futures_util::future::LocalBoxFuture; +use openidconnect::{ + self as oidcrs, + core::{CoreClient, CoreProviderMetadata, CoreResponseType}, + AuthenticationFlow, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, RedirectUrl, + ResourceOwnerPassword, ResourceOwnerUsername, Scope, TokenResponse, TokenUrl, +}; +use service_utils::helpers::get_from_env_unsafe; +use superposition_types::User; +use types::{LoginParams, ProtectionCookie, UserClaims, UserTokenResponse}; +use utils::{presence_no_check, try_user_from, verify_presence}; + +use crate::auth::authenticator::SwitchOrgParams; + +use super::authenticator::Authenticator; + +#[derive(Clone)] +pub struct OIDCAuthenticator { + client: CoreClient, + provider_metadata: CoreProviderMetadata, + client_id: String, + client_secret: String, + base_url: String, + issuer_endpoint_format: String, + token_endpoint_format: String, +} + +impl OIDCAuthenticator { + pub async fn new( + idp_url: url::Url, + base_url: String, + client_id: String, + client_secret: String, + ) -> Result> { + let issuer_endpoint_format = + get_from_env_unsafe::("OIDC_ISSUER_ENDPOINT_FORMAT").unwrap(); + let token_endpoint_format = + get_from_env_unsafe::("OIDC_TOKEN_ENDPOINT_FORMAT").unwrap(); + + let issuer_url = IssuerUrl::from_url(idp_url); + + // Discover OpenID Provider metadata + let provider_metadata = CoreProviderMetadata::discover_async( + issuer_url, + oidcrs::reqwest::async_http_client, + ) + .await?; + + // Create client + let client = CoreClient::from_provider_metadata( + provider_metadata.clone(), + ClientId::new(client_id.clone()), + Some(ClientSecret::new(client_secret.clone())), + ) + .set_redirect_uri(RedirectUrl::new(format!("{}/oidc/login", base_url))?); + + Ok(Self { + client, + provider_metadata, + client_id, + client_secret, + base_url, + issuer_endpoint_format, + token_endpoint_format, + }) + } + + fn get_issuer_url( + &self, + organisation_id: &String, + ) -> Result { + let issuer_endpoint = self + .issuer_endpoint_format + .replace("", organisation_id); + IssuerUrl::new(issuer_endpoint) + } + + fn get_token_url( + &self, + organisation_id: &String, + ) -> Result { + let token_endpoint = self + .token_endpoint_format + .replace("", organisation_id); + TokenUrl::new(token_endpoint) + } + + fn new_redirect(&self) -> HttpResponse { + let (auth_url, csrf_token, nonce) = self + .client + .authorize_url( + AuthenticationFlow::::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .add_scope(oidcrs::Scope::new("email".to_string())) + .add_scope(oidcrs::Scope::new("profile".to_string())) + .url(); + + let protection = ProtectionCookie { + csrf: csrf_token, + nonce, + }; + + let cookie_result = serde_json::to_string(&protection) + .map_err(|err| { + log::error!("Unable to stringify data: {err}"); + ErrorInternalServerError(format!("Unable to stringify data")) + }) + .map(|cookie| { + Cookie::build("protection", cookie) + .max_age(Duration::days(7)) + // .http_only(true) + // .same_site(SameSite::Strict) + .path("/") + .finish() + }); + + match cookie_result { + Ok(p_cookie) => HttpResponse::Found() + .insert_header((header::LOCATION, auth_url.to_string())) + .cookie(p_cookie) + // Deletes the cookie. + .cookie( + Cookie::build("user", "") + .max_age(Duration::seconds(0)) + .finish(), + ) + .finish(), + Err(_) => HttpResponse::InternalServerError().finish(), + } + } + + fn decode_token(&self, cookie: &str) -> Result { + let ctr = serde_json::from_str::(cookie) + .map_err(|e| format!("Error while decoding token {e}"))?; + ctr.id_token() + .ok_or(String::from("Id Token not found"))? + .claims(&self.client.id_token_verifier(), verify_presence) + .map_err(|err| format!("Error in claims verification {err}")) + .cloned() + } +} + +impl Authenticator for OIDCAuthenticator { + fn authenticate(&self, request: &ServiceRequest) -> Result { + let token = request.cookie("user").and_then(|c| { + self.decode_token(c.value()) + .map_err(|e| log::error!("Error in decoding user : {e}")) + .ok() + }); + let path = &request.path(); + let excep = path.matches("login").count() > 0 + // Implies it's a local/un-forwarded request. + || !request.headers().contains_key(header::USER_AGENT) + || path.matches("health").count() > 0 + || path.matches("ready").count() > 0; + + if excep { + Ok(User::default()) + } else if let Some(token_response) = token { + Ok(try_user_from(&token_response).map_err(|err| { + log::error!("Unable to get user {err}"); + ErrorBadRequest(String::from("Unable to get user")) + })?) + } else { + Err(self.new_redirect()) + } + } + + fn routes(&self) -> actix_web::Scope { + web::scope("oidc") + .app_data(Data::new(self.to_owned())) + .service(login) + } + + fn get_organisations(&self, req: &HttpRequest) -> HttpResponse { + let organisations = req + .cookie("user") + .and_then(|user_cookie| { + self.decode_token(user_cookie.value()) + .map_err(|e| log::error!("Error in decoding user : {e}")) + .ok() + }) + .map(|claims| claims.additional_claims().organisations.clone()); + + match organisations { + Some(organisations) => { + HttpResponse::Ok().json(serde_json::json!(organisations)) + } + None => self.new_redirect(), + } + } + + fn switch_organisation( + &self, + req: &HttpRequest, + path: &Path, + ) -> LocalBoxFuture<'static, actix_web::Result> { + let issuer_url = match self.get_issuer_url(&path.organisation_id) { + Ok(issuer_url) => issuer_url, + Err(e) => { + log::error!("Unable to create issuer url {e}"); + return Box::pin(async move { + Err(ErrorBadRequest(String::from("Unable to create issuer url"))) + }); + } + }; + + let token_url = match self.get_token_url(&path.organisation_id) { + Ok(token_url) => token_url, + Err(e) => { + log::error!("Unable to create token url {e}"); + return Box::pin(async move { + Err(ErrorInternalServerError("Unable to create token url")) + }); + } + }; + + let redirect_url = match RedirectUrl::new(format!("{}/", self.base_url.clone())) { + Ok(redirect_url) => redirect_url, + Err(e) => { + log::error!("Unable to create redirect url {e}"); + return Box::pin(async move { + Err(ErrorInternalServerError("Unable to create redirect url")) + }); + } + }; + + let provider = self + .provider_metadata + .clone() + .set_issuer(issuer_url) + .set_token_endpoint(Some(token_url)); + + let client = CoreClient::from_provider_metadata( + provider, + ClientId::new(self.client_id.clone()), + Some(ClientSecret::new(self.client_secret.clone())), + ) + .set_redirect_uri(redirect_url); + + let user = req + .cookie("user") + .and_then(|user_cookie| { + self.decode_token(user_cookie.value()) + .map_err(|e| log::error!("Error in decoding user : {e}")) + .ok() + }) + .map(|claims| { + ( + claims.preferred_username().cloned(), + claims.additional_claims().switch_pass.clone(), + ) + }); + let (username, switch_pass) = if let Some(user) = user { + user + } else { + return Box::pin(async move { Err(ErrorBadRequest("Cookie incorrect")) }); + }; + + let username = if let Some(u) = username { + u + } else { + return Box::pin(async move { Err(ErrorBadRequest("Username not found")) }); + }; + + let user = ResourceOwnerUsername::new(username.to_string()); + let pass = ResourceOwnerPassword::new(switch_pass); + let redirect = self.new_redirect(); + + Box::pin(async move { + let response = client + .exchange_password(&user, &pass) + .add_scope(Scope::new(String::from("openid"))) + .request_async(oidcrs::reqwest::async_http_client) + .await + .map_err(|e| log::error!("Failed to switch organisation for token: {e}")) + .and_then(|tr| { + tr.id_token() + .ok_or_else(|| eprintln!("No identity-token!")) + .and_then(|t| { + t.claims(&client.id_token_verifier(), presence_no_check) + .map_err(|e| log::error!("Couldn't verify claims: {e}")) + })?; + Ok(tr) + }); + + match response { + Ok(r) => { + let token = serde_json::to_string(&r).map_err(|err| { + log::error!("Unable to stringify data: {err}"); + ErrorInternalServerError(format!("Unable to stringify data")) + })?; + let cookie = Cookie::build("org_user", token) + .path("/") + .http_only(true) + .max_age(Duration::days(1)) + .finish(); + Ok(HttpResponse::Found() + .cookie(cookie) + .insert_header(("Location", "/")) + .finish()) + } + Err(()) => Ok(redirect), + } + }) + } +} + +#[get("/login")] +async fn login( + data: Data, + req: HttpRequest, + params: Query, +) -> actix_web::Result { + let p_cookie = if let Some(p_cookie) = ProtectionCookie::from_req(&req) { + p_cookie + } else { + log::error!("OIDC: Missing/Bad protection-cookie, redirecting..."); + return Ok(data.new_redirect()); + }; + + if params.state.secret() != p_cookie.csrf.secret() { + log::error!("OIDC: Bad csrf",); + return Ok(data.new_redirect()); + } + + // Exchange the code with a token. + let response = data + .client + .exchange_code(params.code.clone()) + .request_async(oidcrs::reqwest::async_http_client) + .await + .map_err(|e| log::error!("Failed to exchange auth-code for token: {e}")) + .and_then(|tr| { + tr.id_token() + .ok_or_else(|| log::error!("No identity-token!")) + .and_then(|t| { + t.claims(&data.client.id_token_verifier(), &p_cookie.nonce) + .map_err(|e| log::error!("Couldn't verify claims: {e}")) + })?; + Ok(tr) + }); + + match response { + Ok(r) => { + let token = serde_json::to_string(&r).map_err(|err| { + log::error!("Unable to stringify data: {err}"); + ErrorInternalServerError(format!("Unable to stringify data")) + })?; + let cookie = Cookie::build("user", token) + .path("/") + .http_only(true) + .max_age(Duration::days(1)) + .finish(); + Ok(HttpResponse::Found() + .cookie(cookie) + .insert_header(("Location", "/admin/organisations")) + .finish()) + } + Err(()) => Ok(data.new_redirect()), + } +} diff --git a/crates/superposition/src/auth/oidc/types.rs b/crates/superposition/src/auth/oidc/types.rs new file mode 100644 index 00000000..002d77b8 --- /dev/null +++ b/crates/superposition/src/auth/oidc/types.rs @@ -0,0 +1,51 @@ +use actix_web::HttpRequest; +use openidconnect::{ + core::{ + CoreGenderClaim, CoreJsonWebKeyType, CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, CoreTokenType, + }, + AdditionalClaims, AuthorizationCode, CsrfToken, EmptyExtraTokenFields, IdTokenClaims, + IdTokenFields, Nonce, StandardTokenResponse, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Debug, Deserialize, Clone)] +pub(super) struct ExtraClaims { + pub(super) organisations: Vec, + pub(super) switch_pass: String, +} + +impl AdditionalClaims for ExtraClaims {} + +pub(super) type CoreIdTokenFields = IdTokenFields< + ExtraClaims, + EmptyExtraTokenFields, + CoreGenderClaim, + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, +>; + +pub(super) type UserTokenResponse = + StandardTokenResponse; + +pub(super) type UserClaims = IdTokenClaims; + +#[derive(Deserialize, Serialize)] +pub(super) struct ProtectionCookie { + pub(super) csrf: CsrfToken, + pub(super) nonce: Nonce, +} + +impl ProtectionCookie { + pub(super) fn from_req(req: &HttpRequest) -> Option { + req.cookie("protection") + .and_then(|c| serde_json::from_str(c.value()).ok()) + } +} + +#[derive(Deserialize)] +pub(super) struct LoginParams { + pub(super) code: AuthorizationCode, + pub(super) state: CsrfToken, +} diff --git a/crates/superposition/src/auth/oidc/utils.rs b/crates/superposition/src/auth/oidc/utils.rs new file mode 100644 index 00000000..a08bebe6 --- /dev/null +++ b/crates/superposition/src/auth/oidc/utils.rs @@ -0,0 +1,30 @@ +use openidconnect::Nonce; +use superposition_types::User; + +use super::types::UserClaims; + +pub(super) fn verify_presence(n: Option<&Nonce>) -> Result<(), String> { + if n.is_some() { + Ok(()) + } else { + Err("missing nonce claim".to_string()) + } +} + +pub(super) fn presence_no_check(_: Option<&Nonce>) -> Result<(), String> { + Ok(()) +} + +pub(super) fn try_user_from(claims: &UserClaims) -> Result { + let user = User { + email: claims + .email() + .ok_or(String::from("Username not found"))? + .to_string(), + username: claims + .preferred_username() + .ok_or(String::from("Username not found"))? + .to_string(), + }; + Ok(user) +} diff --git a/crates/superposition/src/main.rs b/crates/superposition/src/main.rs index 649fe4e3..146a7d61 100644 --- a/crates/superposition/src/main.rs +++ b/crates/superposition/src/main.rs @@ -1,16 +1,16 @@ #![deny(unused_crate_dependencies)] mod app_state; +mod auth; use std::{collections::HashSet, io::Result, time::Duration}; use actix_files::Files; use actix_web::{ - dev::Service, - http::header, middleware::Compress, web::{self, get, scope, Data, PathConfig}, - App, HttpMessage, HttpResponse, HttpServer, + App, HttpResponse, HttpServer, }; +use auth::AuthHandler; use context_aware_config::api::*; use experimentation_platform::api::*; use frontend::app::*; @@ -18,13 +18,13 @@ use frontend::types::Envs as UIEnvs; use leptos::*; use leptos_actix::{generate_route_list, LeptosRoutes}; use service_utils::{ + aws::kms, helpers::get_from_env_unsafe, middlewares::{ app_scope::AppExecutionScopeMiddlewareFactory, tenant::TenantMiddlewareFactory, }, - service::types::{AppScope, AppState}, + service::types::{AppEnv, AppScope}, }; -use superposition_types::User; #[actix_web::get("favicon.ico")] async fn favicon( @@ -89,8 +89,24 @@ async fn main() -> Result<()> { view! { } }); - let app_state = - Data::new(app_state::get(service_prefix_str.to_owned(), &base, &tenants).await); + let app_env = get_from_env_unsafe("APP_ENV").expect("APP_ENV is not set"); + let kms_client = match app_env { + AppEnv::DEV | AppEnv::TEST => None, + _ => Some(kms::new_client().await), + }; + + let app_state = Data::new( + app_state::get( + app_env, + &kms_client, + service_prefix_str.to_owned(), + &base, + &tenants, + ) + .await, + ); + + let auth = AuthHandler::init(&kms_client, &app_env).await; HttpServer::new(move || { let leptos_options = &conf.leptos_options; @@ -99,22 +115,6 @@ async fn main() -> Result<()> { App::new() .wrap(Compress::default()) .app_data(app_state.clone()) - .wrap_fn(|req, srv| { - let state = req.app_data::>().unwrap(); - let user = req.headers().get(header::AUTHORIZATION).and_then(|auth| auth.to_str().ok()).and_then(|auth| { - let mut token = auth.split(' ').into_iter(); - match (token.next(), token.next()) { - (Some("Internal"), Some(token)) if token == state.superposition_token => - req.headers().get("x-user").and_then(|auth| auth.to_str().ok()).and_then(|user_str| { - serde_json::from_str::(user_str).ok() - }), - (_, _) => None - } - }).unwrap_or_default(); - - req.extensions_mut().insert::(user); - srv.call(req) - }) .wrap(TenantMiddlewareFactory) .app_data(PathConfig::default().error_handler(|err, _| { actix_web::error::ErrorBadRequest(err) @@ -127,6 +127,8 @@ async fn main() -> Result<()> { .service(web::redirect("/", ui_redirect_path.to_string())) .service(web::redirect("/admin", ui_redirect_path.to_string())) .service(web::redirect("/admin/{tenant}/", "default-config")) + .service(auth.routes()) + .service(auth.org_routes()) .leptos_routes( leptos_options.to_owned(), routes.to_owned(), @@ -192,6 +194,7 @@ async fn main() -> Result<()> { get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), ) .app_data(Data::new(leptos_options.to_owned())) + .wrap(auth.clone()) }) .bind(("0.0.0.0", cac_port))? .workers(5) diff --git a/crates/superposition_types/src/database.rs b/crates/superposition_types/src/database.rs index c95cf2db..d418c532 100644 --- a/crates/superposition_types/src/database.rs +++ b/crates/superposition_types/src/database.rs @@ -1,6 +1,4 @@ +pub mod models; #[cfg(feature = "diesel_derives")] pub mod schema; - pub mod types; - -pub mod models; diff --git a/crates/superposition_types/src/database/models.rs b/crates/superposition_types/src/database/models.rs index 448b838b..f3bca3b3 100644 --- a/crates/superposition_types/src/database/models.rs +++ b/crates/superposition_types/src/database/models.rs @@ -1,4 +1,3 @@ pub mod cac; - #[cfg(feature = "experimentation")] pub mod experimentation; diff --git a/crates/superposition_types/src/lib.rs b/crates/superposition_types/src/lib.rs index 986bc9f9..53924d98 100644 --- a/crates/superposition_types/src/lib.rs +++ b/crates/superposition_types/src/lib.rs @@ -32,8 +32,6 @@ pub use crate::overridden::Overridden; pub struct User { pub email: String, pub username: String, - pub auth_token: String, - pub auth_type: String, } impl User { @@ -44,14 +42,6 @@ impl User { pub fn get_username(&self) -> String { self.username.clone() } - - pub fn get_auth_token(&self) -> String { - self.auth_token.clone() - } - - pub fn get_auth_type(&self) -> String { - self.auth_type.clone() - } } impl Default for User { @@ -59,8 +49,6 @@ impl Default for User { Self { email: "user@superposition.io".into(), username: "superposition".into(), - auth_token: "1234abcd".into(), - auth_type: "Bearer".into(), } } } diff --git a/example.Dockerfile b/example.Dockerfile index 86dd4190..4e644e50 100644 --- a/example.Dockerfile +++ b/example.Dockerfile @@ -41,6 +41,8 @@ RUN cargo build --release FROM debian:bookworm-slim as runtime +RUN mkdir -p /app/crates/superposition +COPY --from=builder /build/crates/superposition/Superposition.cac.toml /app/crates/superposition/Superposition.cac.toml ENV NODE_VERSION=20.17.0 WORKDIR /app