diff --git a/.gitignore b/.gitignore index cbfad5a519..9fc2bd6f25 100644 --- a/.gitignore +++ b/.gitignore @@ -17,9 +17,12 @@ report.json # These are backup files generated by rustfmt **/*.rs.bk + + /tmp/ /data/ /.idea/ +/.vscode /.env /.env* /integration-tests diff --git a/Cargo.lock b/Cargo.lock index b7a65230e7..c8ce6e86c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -799,24 +799,31 @@ dependencies = [ [[package]] name = "askama" -version = "0.11.1" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb98f10f371286b177db5eeb9a6e5396609555686a35e1d4f7b9a9c6d8af0139" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" dependencies = [ "askama_derive", "askama_escape", - "askama_shared", + "humansize", + "num-traits", + "percent-encoding", ] [[package]] name = "askama_derive" -version = "0.11.2" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87bf87e6e8b47264efa9bde63d6225c6276a52e05e91bf37eaa8afd0032d6b71" +checksum = "2ccf09143e56923c12e027b83a9553210a3c58322ed8419a53461b14a4dccd85" dependencies = [ - "askama_shared", + "askama_parser", + "basic-toml", + "mime", + "mime_guess", "proc-macro2", - "syn 1.0.109", + "quote", + "serde", + "syn 2.0.41", ] [[package]] @@ -826,23 +833,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" [[package]] -name = "askama_shared" -version = "0.12.2" +name = "askama_parser" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf722b94118a07fcbc6640190f247334027685d4e218b794dbfe17c32bf38ed0" +checksum = "262eb9cf7be51269c5f2951eeda9ccd14d6934e437457f47b4f066bf55a6770d" dependencies = [ - "askama_escape", - "humansize", - "mime", - "mime_guess", "nom", - "num-traits", - "percent-encoding", - "proc-macro2", - "quote", - "serde", - "syn 1.0.109", - "toml", ] [[package]] @@ -1387,6 +1383,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.12.3" @@ -1427,6 +1429,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7b7172542a3d446ca7b2be4e28e4f4c119a89c396712f7ca1ad2822bfc54ca2" +[[package]] +name = "basic-toml" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f2139706359229bfa8f19142ac1155b4b80beafb7a60471ac5dd109d4a19778" +dependencies = [ + "serde", +] + [[package]] name = "bincode" version = "1.3.3" @@ -2167,6 +2178,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[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" @@ -2223,6 +2246,34 @@ dependencies = [ "cipher", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "platforms", + "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.41", +] + [[package]] name = "darling" version = "0.20.3" @@ -2625,9 +2676,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dotenv_config" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef519d2aabc15ca3dba4729976066fa23f80187bf2b19d623d24fe1a0ec3ea" +checksum = "22c42ee79502f8625682f381c05afbd2c0b65115aef83c219f49183f6c4adb1a" dependencies = [ "anyhow", "askama", @@ -2646,6 +2697,44 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.9.0" @@ -2655,6 +2744,27 @@ dependencies = [ "serde", ] +[[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 = "ena" version = "0.14.2" @@ -2823,6 +2933,22 @@ dependencies = [ "serde", ] +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" + [[package]] name = "filetime" version = "0.2.23" @@ -3163,6 +3289,17 @@ dependencies = [ "onig", ] +[[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.22" @@ -3353,9 +3490,12 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humansize" -version = "1.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] [[package]] name = "humantime" @@ -3468,6 +3608,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -3637,6 +3778,21 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" +[[package]] +name = "jsonwebtoken" +version = "9.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ea04a7c5c055c175f189b6dc6ba036fd62306b58c66c9f6389036c503a3f4" +dependencies = [ + "base64 0.21.5", + "js-sys", + "pem", + "ring 0.17.7", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "keccak" version = "0.1.4" @@ -4319,6 +4475,26 @@ dependencies = [ "syn 2.0.41", ] +[[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", + "rand", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror", + "url", +] + [[package]] name = "objc" version = "0.2.7" @@ -4410,6 +4586,38 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openidconnect" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62d6050f6a84b81f23c569f5607ad883293e57491036e318fafe6fc4895fadb1" +dependencies = [ + "base64 0.13.1", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", + "http", + "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 = "openobserve" version = "0.8.0" @@ -4462,6 +4670,7 @@ dependencies = [ "indexmap 2.1.0", "ipnetwork 0.20.0", "itertools 0.12.0", + "jsonwebtoken", "log", "maxminddb", "memchr", @@ -4469,6 +4678,7 @@ dependencies = [ "mimalloc", "object_store", "once_cell", + "openidconnect", "opentelemetry 0.18.0", "opentelemetry-otlp", "opentelemetry-proto 0.2.0", @@ -4753,6 +4963,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[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 = "packed_simd" version = "0.3.9" @@ -4899,6 +5133,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e9ed2178b0575fff8e1b83b58ba6f75e727aafac2e1b6c795169ad3b17eb518" +[[package]] +name = "pem" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" +dependencies = [ + "base64 0.21.5", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -5075,6 +5319,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "platforms" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" + [[package]] name = "poly1305" version = "0.8.0" @@ -5157,6 +5407,15 @@ dependencies = [ "unicode-width", ] +[[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-crate" version = "1.3.1" @@ -5682,6 +5941,16 @@ dependencies = [ "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" @@ -6040,6 +6309,20 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[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.2" @@ -6098,6 +6381,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 2.10.1", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.193" @@ -6133,6 +6426,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_plain" version = "1.0.2" @@ -6154,6 +6457,35 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +dependencies = [ + "base64 0.21.5", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.1.0", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.41", +] + [[package]] name = "serde_yaml" version = "0.8.26" @@ -6281,6 +6613,18 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -7099,15 +7443,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml_datetime" version = "0.6.5" diff --git a/Cargo.toml b/Cargo.toml index bd4621868e..9ffde666f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,7 +82,7 @@ arrow = { version = "49", features = ["simd", "ipc_compression"] } arrow-schema = { version = "49", features = ["serde"] } parquet = { version = "49", features = ["arrow", "async"] } object_store = { version = "0.8", features = ["aws", "azure", "gcp"] } -dotenv_config = "0.1" +dotenv_config = "0.1.7" dotenvy = "0.15" env_logger = "0.10" etcd-client = { version = "0.12", features = ["tls"] } @@ -97,11 +97,13 @@ http-auth-basic = "0.3" indexmap = { version = "2.0", features = ["serde"] } ipnetwork = "0.20" itertools = "0.12" +jsonwebtoken = "9.2.0" log = "0.4" maxminddb = "0.23.0" memchr = "2.5" mimalloc = { version = "0.1", default-features = false, optional = true } once_cell = "1.17" +openidconnect = "3.4.0" opentelemetry = { version = "0.18", features = ["rt-tokio"] } opentelemetry-otlp = { version = "0.11", features = [ "http-proto", @@ -173,6 +175,8 @@ zstd = "0.13" memory-stats = "1.1.0" tracing-appender = "0.2.3" + + [build-dependencies] chrono = { version = "0.4", default-features = false, features = ["clock"] } tonic-build = { version = "0.8", features = ["prost"] } diff --git a/src/cli/basic/cli.rs b/src/cli/basic/cli.rs index 5cf03623ec..9dc229af4c 100644 --- a/src/cli/basic/cli.rs +++ b/src/cli/basic/cli.rs @@ -123,7 +123,7 @@ pub async fn cli() -> Result { role: meta::user::UserRole::Root, first_name: "root".to_owned(), last_name: "".to_owned(), - is_ldap: false, + is_external: false, }, ) .await?; diff --git a/src/common/infra/errors.rs b/src/common/infra/errors.rs index feb30c8254..13ae5a650d 100644 --- a/src/common/infra/errors.rs +++ b/src/common/infra/errors.rs @@ -201,6 +201,16 @@ impl ErrorCodes { } } +#[derive(ThisError, Debug)] +pub enum JwtError { + #[error("No matching JWK found for the given kid")] + KeyNotExists(), + #[error("Token doesn't have a {0} field")] + MissingAttribute(String), + #[error("Token can't be verified")] + ValidationFailed(), +} + #[cfg(test)] mod tests { use expect_test::expect; diff --git a/src/common/meta/user.rs b/src/common/meta/user.rs index f0cf871981..e29e6e6f34 100644 --- a/src/common/meta/user.rs +++ b/src/common/meta/user.rs @@ -30,7 +30,7 @@ pub struct UserRequest { pub role: UserRole, /// Is the user created via ldap flow. #[serde(default)] - pub is_ldap: bool, + pub is_external: bool, } impl UserRequest { @@ -41,7 +41,7 @@ impl UserRequest { org: String, token: String, rum_token: String, - is_ldap: bool, + is_external: bool, ) -> DBUser { DBUser { email: self.email.clone(), @@ -55,7 +55,7 @@ impl UserRequest { rum_token: Some(rum_token), role: self.role.clone(), }], - is_ldap, + is_external, } } } @@ -72,7 +72,7 @@ pub struct DBUser { pub salt: String, pub organizations: Vec, #[serde(default)] - pub is_ldap: bool, + pub is_external: bool, } impl DBUser { @@ -98,7 +98,7 @@ impl DBUser { token: org.token.clone(), rum_token: org.rum_token.clone(), salt: local.salt, - is_ldap: false, + is_external: self.is_external, }) } @@ -118,7 +118,7 @@ impl DBUser { token: org.token, rum_token: org.rum_token, salt: self.salt.clone(), - is_ldap: false, + is_external: self.is_external, }) } ret_val @@ -142,7 +142,7 @@ pub struct User { pub role: UserRole, pub org: String, /// Is the user authenticated and created via LDAP - pub is_ldap: bool, + pub is_external: bool, } #[derive(Clone, Default, Debug, Serialize, Deserialize, ToSchema)] @@ -169,6 +169,8 @@ pub struct UserOrgRole { #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Eq, PartialEq, Default)] pub struct UpdateUser { + #[serde(default)] + pub change_password: bool, #[serde(skip_serializing_if = "Option::is_none")] pub first_name: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -213,7 +215,7 @@ pub struct UserResponse { pub last_name: String, pub role: UserRole, #[serde(default)] - pub is_ldap: bool, + pub is_external: bool, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] @@ -232,3 +234,15 @@ pub struct SignInResponse { pub status: bool, pub message: String, } + +#[derive(Clone, Debug, Default, Serialize, Deserialize, ToSchema)] +pub struct TokenValidationResponse { + pub is_valid: bool, + pub user_email: String, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, ToSchema)] +pub struct RoleOrg { + pub role: UserRole, + pub org: String, +} diff --git a/src/common/utils/auth.rs b/src/common/utils/auth.rs index d414eb2559..2a1615dc77 100644 --- a/src/common/utils/auth.rs +++ b/src/common/utils/auth.rs @@ -13,7 +13,9 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use actix_web::{dev::Payload, Error, FromRequest, HttpRequest}; use argon2::{password_hash::SaltString, Algorithm, Argon2, Params, PasswordHasher, Version}; +use futures::future::{ready, Ready}; use crate::common::{ infra::config::{PASSWORD_HASH, USERS}, @@ -50,6 +52,53 @@ pub(crate) fn is_root_user(user_id: &str) -> bool { } } +pub struct UserEmail { + pub user_id: String, +} + +impl FromRequest for UserEmail { + type Error = Error; + type Future = Ready>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + if let Some(auth_header) = req.headers().get("user_id") { + if let Ok(user_str) = auth_header.to_str() { + return ready(Ok(UserEmail { + user_id: user_str.to_owned(), + })); + } + } + ready(Err(actix_web::error::ErrorUnauthorized("No user found"))) + } +} + +pub struct AuthExtractor { + pub auth: String, +} + +impl FromRequest for AuthExtractor { + type Error = Error; + type Future = Ready>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + if let Some(auth_header) = req.headers().get("Authorization") { + if let Ok(auth_str) = auth_header.to_str() { + return ready(Ok(AuthExtractor { + auth: auth_str.to_owned(), + })); + } + } else if let Some(cookie) = req.cookie("access_token") { + let access_token = cookie.value().to_string(); + return ready(Ok(AuthExtractor { + auth: format!("Bearer {}", access_token), + })); + } + ready(Err(actix_web::error::ErrorUnauthorized( + "Unauthorized Access", + ))) + } +} + #[cfg(test)] mod tests { use super::*; @@ -74,7 +123,7 @@ mod tests { role: crate::common::meta::user::UserRole::Root, first_name: "root".to_owned(), last_name: "".to_owned(), - is_ldap: false, + is_external: false, }, ) .await; diff --git a/src/common/utils/jwt.rs b/src/common/utils/jwt.rs new file mode 100644 index 0000000000..e7774f2d83 --- /dev/null +++ b/src/common/utils/jwt.rs @@ -0,0 +1,176 @@ +#[cfg(feature = "enterprise")] +use { + crate::{ + common::{ + infra::{config::USERS, errors::JwtError}, + meta::user::{DBUser, RoleOrg, TokenValidationResponse, UserOrg}, + }, + service::{db, users}, + }, + o2_enterprise::enterprise::common::infra::config::{DEX_GROUP_REGEX, O2_CONFIG}, +}; +#[cfg(feature = "enterprise")] +use { + jsonwebtoken::{ + decode, decode_header, + jwk::{self, AlgorithmParameters}, + Algorithm, DecodingKey, TokenData, Validation, + }, + serde_json::Value, + std::{collections::HashMap, str::FromStr}, +}; + +#[cfg(feature = "enterprise")] +pub(crate) async fn verify_decode_token( + token: &str, + jwks: &str, + aud: &str, + get_decode_token: bool, +) -> Result< + ( + TokenValidationResponse, + Option>>, + ), + anyhow::Error, +> { + let jwks: jwk::JwkSet = serde_json::from_str(jwks).unwrap(); + let header = decode_header(token)?; + let kid = match header.kid { + Some(k) => k, + None => return Err(JwtError::MissingAttribute("`kid` header".to_owned()).into()), + }; + + if let Some(j) = jwks.find(&kid) { + match &j.algorithm { + AlgorithmParameters::RSA(rsa) => { + let decoding_key = DecodingKey::from_rsa_components(&rsa.n, &rsa.e).unwrap(); + + let mut validation = Validation::new( + Algorithm::from_str(j.common.key_algorithm.unwrap().to_string().as_str()) + .unwrap(), + ); + validation.validate_exp = true; + validation.set_audience(&[aud]); + let decoded_token = decode::>( + token, + &decoding_key, + &validation, + )?; + + let user_email = decoded_token.claims.get("email").unwrap().as_str().unwrap(); + + Ok(( + TokenValidationResponse { + is_valid: true, + user_email: user_email.to_owned(), + }, + if get_decode_token { + Some(decoded_token) + } else { + None + }, + )) + } + _ => Err(JwtError::ValidationFailed().into()), + } + } else { + Err(JwtError::KeyNotExists().into()) + } +} + +#[cfg(feature = "enterprise")] +pub async fn process_token( + res: ( + TokenValidationResponse, + Option>>, + ), +) { + let dec_token = res.1.unwrap(); + let groups = dec_token.claims.get("groups").unwrap().as_array().unwrap(); + let name = dec_token.claims.get("name").unwrap().as_str().unwrap(); + let user_email = res.0.user_email.to_owned(); + let mut source_orgs: Vec = vec![]; + for group in groups { + let role_org = parse_dn(group.as_str().unwrap()).unwrap(); + + source_orgs.push(UserOrg { + role: role_org.role, + name: role_org.org, + ..UserOrg::default() + }); + } + // Check if the user exists in the database + let db_user = db::user::get_user_by_email(&user_email).await; + let updated_db_user = if db_user.is_none() { + log::info!("User does not exist in the database"); + DBUser { + email: user_email.to_owned(), + first_name: name.to_owned(), + last_name: "".to_owned(), + password: "".to_owned(), + salt: "".to_owned(), + organizations: source_orgs, + is_external: true, + } + } else { + log::info!("User exists in the database perform check for role change"); + let existing_db_user = db_user.unwrap(); + let mut existing_orgs = existing_db_user.organizations; + + // Find and remove users from cache + for org in existing_orgs.iter() { + if !source_orgs.iter().any(|src_org| src_org.name == org.name) { + USERS.remove(&format!("{}/{}", org.name, user_email)); + } + } + + // 1. Remove organizations not in source_orgs + existing_orgs.retain(|org| source_orgs.iter().any(|src_org| src_org.name == org.name)); + // 2. Update roles for existing organizations + for org in existing_orgs.iter_mut() { + if let Some(src_org) = source_orgs.iter().find(|src_org| src_org.name == org.name) { + org.role = src_org.role.clone(); + } + } + // 3. Add new organizations from source_orgs + for src_org in source_orgs { + if !existing_orgs.iter().any(|org| org.name == src_org.name) { + existing_orgs.push(src_org.clone()); + } + } + + DBUser { + email: user_email.to_owned(), + first_name: name.to_owned(), + last_name: "".to_owned(), + password: existing_db_user.password, + salt: existing_db_user.salt, + organizations: existing_orgs, + is_external: true, + } + }; + let _ = users::update_db_user(updated_db_user).await; +} + +#[cfg(feature = "enterprise")] +fn parse_dn(dn: &str) -> Option { + DEX_GROUP_REGEX.captures(dn.as_bytes()).map(|caps| { + let role = + String::from_utf8_lossy(caps.name(&O2_CONFIG.dex.role_attribute).unwrap().as_bytes()) + .to_string(); + let org = String::from_utf8_lossy( + caps.name(&O2_CONFIG.dex.group_attribute) + .unwrap() + .as_bytes(), + ) + .to_string(); + + let role = if role.contains("admin") { + crate::common::meta::user::UserRole::Admin + } else { + crate::common::meta::user::UserRole::Member + }; + + RoleOrg { role, org } + }) +} diff --git a/src/common/utils/mod.rs b/src/common/utils/mod.rs index 7aaa09cec8..545f6d4dca 100644 --- a/src/common/utils/mod.rs +++ b/src/common/utils/mod.rs @@ -23,6 +23,7 @@ pub mod functions; pub mod hasher; pub mod http; pub mod json; +pub mod jwt; pub mod rand; pub mod schema; pub mod schema_ext; diff --git a/src/handler/grpc/auth/mod.rs b/src/handler/grpc/auth/mod.rs index 61f4aae164..1d3bd63945 100644 --- a/src/handler/grpc/auth/mod.rs +++ b/src/handler/grpc/auth/mod.rs @@ -107,7 +107,7 @@ mod tests { token: "token".to_string(), rum_token: Some("rum_token".to_string()), org: "dummy".to_owned(), - is_ldap: false, + is_external: false, }, ); @@ -136,7 +136,7 @@ mod tests { token: "token".to_string(), rum_token: Some("rum_token".to_string()), org: "dummy".to_owned(), - is_ldap: false, + is_external: false, }, ); @@ -163,7 +163,7 @@ mod tests { token: "token".to_string(), rum_token: Some("rum_token".to_string()), org: "dummy".to_owned(), - is_ldap: false, + is_external: false, }, ); let mut request = tonic::Request::new(()); diff --git a/src/handler/http/auth/mod.rs b/src/handler/http/auth/mod.rs index 92f1881a2c..8da08f75f4 100644 --- a/src/handler/http/auth/mod.rs +++ b/src/handler/http/auth/mod.rs @@ -13,454 +13,5 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use actix_web::{ - dev::ServiceRequest, - error::{ErrorForbidden, ErrorUnauthorized}, - http::{header, Method}, - web, Error, -}; -use actix_web_httpauth::extractors::basic::BasicAuth; -#[cfg(feature = "enterprise")] -use o2_enterprise::enterprise::{common::infra::config::LDAP_CONFIG, ldap::service::LdapUser}; - -use crate::{ - common::{ - infra::config::CONFIG, - meta::{ - ingestion::INGESTION_EP, - proxy::QueryParamProxyURL, - user::{DBUser, UserRole}, - }, - utils::{ - auth::{get_hash, is_root_user}, - base64, - }, - }, - service::{db, users}, -}; - -pub async fn validator( - req: ServiceRequest, - credentials: BasicAuth, -) -> Result { - let path = match req - .request() - .path() - .strip_prefix(format!("{}/api/", CONFIG.common.base_uri).as_str()) - { - Some(path) => path, - None => req.request().path(), - }; - match validate_credentials( - credentials.user_id(), - credentials.password().unwrap_or_default().trim(), - path, - ) - .await - { - Ok(res) => { - if res { - // / Hack for prometheus, need support POST and check the header - let mut req = req; - if req.method().eq(&Method::POST) && !req.headers().contains_key("content-type") { - req.headers_mut().insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/x-www-form-urlencoded"), - ); - } - Ok(req) - } else { - Err((ErrorUnauthorized("Unauthorized Access"), req)) - } - } - Err(err) => Err((err, req)), - } -} - -/// `validate_token` validates the endpoints which are token only. -/// This includes endpoints like `rum` etc. -/// -/// ### Args: -/// - token: The token to validate -/// -pub async fn validate_token(token: &str, org_id: &str) -> Result { - match users::get_user_by_token(org_id, token).await { - Some(_user) => Ok(true), - None => Err(ErrorForbidden("Not allowed")), - } -} - -pub async fn validate_credentials( - user_id: &str, - user_password: &str, - path: &str, -) -> Result { - #[cfg(feature = "enterprise")] - if LDAP_CONFIG.ldap_enabled { - log::info!("LDAP authentication enabled"); - return validate_user(user_id, user_password).await; - } - - let user; - let mut path_columns = path.split('/').collect::>(); - if let Some(v) = path_columns.last() { - if v.is_empty() { - path_columns.pop(); - } - } - - // this is only applicable for super admin user - if is_root_user(user_id) { - user = users::get_user(None, user_id).await; - if user.is_none() { - return Ok(false); - } - } else if path_columns.last().unwrap_or(&"").eq(&"organizations") { - let db_user = db::user::get_db_user(user_id).await; - user = match db_user { - Ok(user) => { - let all_users = user.get_all_users(); - if all_users.is_empty() { - None - } else { - all_users.first().cloned() - } - } - Err(_) => None, - } - } else { - user = match path.find('/') { - Some(index) => { - let org_id = &path[0..index]; - users::get_user(Some(org_id), user_id).await - } - None => users::get_user(None, user_id).await, - } - }; - - if user.is_none() { - return Ok(false); - } - let user = user.unwrap(); - - if (path_columns.len() == 1 || INGESTION_EP.iter().any(|s| path_columns.contains(s))) - && user.token.eq(&user_password) - { - return Ok(true); - } - - let in_pass = get_hash(user_password, &user.salt); - if !user.password.eq(&in_pass) { - return Ok(false); - } - if !path.contains("/user") - || (path.contains("/user") - && (user.role.eq(&UserRole::Admin) - || user.role.eq(&UserRole::Root) - || user.email.eq(user_id))) - { - Ok(true) - } else { - Err(ErrorForbidden("Not allowed")) - } -} - -async fn validate_user_from_db( - db_user: Result, - user_password: &str, -) -> Result { - // let db_user = db::user::get_db_user(user_id).await; - match db_user { - Ok(user) => { - let in_pass = get_hash(user_password, &user.salt); - if user.password.eq(&in_pass) { - Ok(true) - } else { - Err(ErrorForbidden("Not allowed")) - } - } - Err(_) => Err(ErrorForbidden("Not allowed")), - } -} - -/// Validate the incoming user from the ldap, if ldap is enabled -#[cfg(feature = "enterprise")] -async fn validate_user_from_ldap(user_id: &str, user_password: &str) -> Result { - let ldap_user_res: Result = - o2_enterprise::enterprise::ldap::service::auth::get_user_from_ldap(user_id, user_password) - .await; - - match ldap_user_res { - Ok(ldap_user) => { - for group in ldap_user.groups { - let hierarchy = group.split(',').collect::>(); - let org = hierarchy[1].split('=').last().unwrap(); - let role = if group.contains("admin") { - crate::common::meta::user::UserRole::Admin - } else { - crate::common::meta::user::UserRole::Member - }; - log::info!("Orgs retrieved from the ldap server: {:?}", org); - - // Check if the user exists in the database - let user_exists = db::user::check_user_exists_by_email(user_id).await; - if !user_exists { - log::info!("User does not exist in the database"); - log::warn!("Email is replaced using user_id, beware, in ldap."); - // create the user - let _ = users::post_user( - org, - crate::common::meta::user::UserRequest { - // email: ldap_user.attributes.email.clone(), - email: user_id.to_string(), - password: "ldap+pass".to_owned(), - role, - first_name: ldap_user.attributes.firstname.clone(), - last_name: ldap_user.attributes.lastname.clone(), - is_ldap: true, - }, - ) - .await - .unwrap(); - } else { - log::info!("User exists in the database, should have sync'd the org now"); - let root_user = crate::common::infra::config::ROOT_USER.clone(); - let initiating_user = root_user.get("root").unwrap().clone(); - let _ = users::add_user_to_org(org, user_id, role, &initiating_user.email) - .await - .unwrap(); - } - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - } - Ok(true) - } - Err(_) => Ok(false), - } -} - -#[cfg(feature = "enterprise")] -pub async fn validate_user(user_id: &str, user_password: &str) -> Result { - let db_user = db::user::get_db_user(user_id).await; - let is_ldap_user = match db_user.as_ref() { - Ok(user) => user.is_ldap, - Err(_) => true, - }; - - if LDAP_CONFIG.ldap_enabled && is_ldap_user { - validate_user_from_ldap(user_id, user_password).await - } else { - validate_user_from_db(db_user, user_password).await - } -} - -#[cfg(not(feature = "enterprise"))] -pub async fn validate_user(user_id: &str, user_password: &str) -> Result { - let db_user = db::user::get_db_user(user_id).await; - validate_user_from_db(db_user, user_password).await -} - -pub async fn validator_aws( - req: ServiceRequest, - _credentials: Option, -) -> Result { - let path = req - .request() - .path() - .strip_prefix(format!("{}/aws/", CONFIG.common.base_uri).as_str()) - .unwrap_or(req.request().path()); - - match req.headers().get("X-Amz-Firehose-Access-Key") { - Some(val) => match val.to_str() { - Ok(val) => { - let amz_creds = match base64::decode(val) { - Ok(val) => val, - Err(_) => return Err((ErrorUnauthorized("Unauthorized Access"), req)), - }; - let creds = amz_creds - .split(':') - .map(|s| s.to_string()) - .collect::>(); - - match validate_credentials(&creds[0], &creds[1], path).await { - Ok(res) => { - if res { - Ok(req) - } else { - Err((ErrorUnauthorized("Unauthorized Access"), req)) - } - } - Err(err) => Err((err, req)), - } - } - Err(_) => Err((ErrorUnauthorized("Unauthorized Access"), req)), - }, - None => Err((ErrorUnauthorized("Unauthorized Access"), req)), - } -} - -pub async fn validator_gcp( - req: ServiceRequest, - _credentials: Option, -) -> Result { - let path = req - .request() - .path() - .strip_prefix(format!("{}/gcp/", CONFIG.common.base_uri).as_str()) - .unwrap_or(req.request().path()); - - let query = - web::Query::>::from_query(req.query_string()) - .unwrap(); - match query.get("API-Key") { - Some(val) => { - let gcp_creds = base64::decode(val).unwrap(); - let creds = gcp_creds - .split(':') - .map(|s| s.to_string()) - .collect::>(); - - match validate_credentials(&creds[0], &creds[1], path).await { - Ok(res) => { - if res { - Ok(req) - } else { - Err((ErrorUnauthorized("Unauthorized Access"), req)) - } - } - Err(err) => Err((err, req)), - } - } - None => Err((ErrorUnauthorized("Unauthorized Access"), req)), - } -} - -pub async fn validator_proxy_url( - req: ServiceRequest, - query: web::Query, -) -> Result { - let creds = base64::decode(&query.proxy_token).expect("Invalid base-encoded token"); - let creds = creds - .split(':') - .map(|s| s.to_string()) - .collect::>(); - let path = req - .request() - .path() - .strip_prefix(format!("{}/proxy/", CONFIG.common.base_uri).as_str()) - .unwrap_or(req.request().path()); - - match validate_credentials(&creds[0], &creds[1], path).await { - Ok(res) => { - if res { - Ok(req) - } else { - Err((ErrorUnauthorized("Unauthorized Access"), req)) - } - } - Err(err) => Err((err, req)), - } -} - -pub async fn validator_rum( - req: ServiceRequest, - _credentials: Option, -) -> Result { - let path = req - .request() - .path() - .strip_prefix(format!("{}/rum/v1/", CONFIG.common.base_uri).as_str()) - .unwrap_or(req.request().path()); - - // After this previous path clean we should get only the - // remaining `org_id/rum` or `org_id/replay` or `org_id/logs` - let org_id_end_point: Vec<&str> = path.split('/').collect(); - if org_id_end_point.len() != 2 { - return Err(( - ErrorUnauthorized("Unauthorized Access. Please pass a valid org_id."), - req, - )); - } - - let query = - web::Query::>::from_query(req.query_string()) - .unwrap(); - - let token = query.get("oo-api-key").or_else(|| query.get("o2-api-key")); - match token { - Some(token) => match validate_token(token, org_id_end_point[0]).await { - Ok(res) => { - if res { - Ok(req) - } else { - Err((ErrorUnauthorized("Unauthorized Access"), req)) - } - } - Err(err) => Err((err, req)), - }, - None => Err((ErrorUnauthorized("Unauthorized Access"), req)), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::common::{infra::db as infra_db, meta::user::UserRequest}; - - #[actix_web::test] - async fn test_validate() { - infra_db::create_table().await.unwrap(); - let _ = users::post_user( - "default", - UserRequest { - email: "root@example.com".to_string(), - password: "Complexpass#123".to_string(), - role: crate::common::meta::user::UserRole::Root, - first_name: "root".to_owned(), - last_name: "".to_owned(), - is_ldap: false, - }, - ) - .await; - let _ = users::post_user( - "default", - UserRequest { - email: "user1@example.com".to_string(), - password: "Complexpass#123".to_string(), - role: crate::common::meta::user::UserRole::Member, - first_name: "root".to_owned(), - last_name: "".to_owned(), - is_ldap: false, - }, - ) - .await; - - let pwd = "Complexpass#123"; - assert!( - validate_credentials("root@example.com", pwd, "default/_bulk") - .await - .unwrap() - ); - assert!( - !validate_credentials("", pwd, "default/_bulk") - .await - .unwrap() - ); - assert!(!validate_credentials("", pwd, "/").await.unwrap()); - assert!( - !validate_credentials("user1@example.com", pwd, "/") - .await - .unwrap() - ); - assert!( - validate_credentials("user1@example.com", pwd, "default/user") - .await - .unwrap() - ); - assert!( - !validate_credentials("user1@example.com", "x", "default/user") - .await - .unwrap() - ); - assert!(validate_user("root@example.com", pwd).await.unwrap()); - } -} +pub mod token; +pub mod validator; diff --git a/src/handler/http/auth/token.rs b/src/handler/http/auth/token.rs new file mode 100644 index 0000000000..86dfa34e66 --- /dev/null +++ b/src/handler/http/auth/token.rs @@ -0,0 +1,73 @@ +// Copyright 2023 Zinc Labs Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use actix_web::{dev::ServiceRequest, Error}; +#[cfg(feature = "enterprise")] +use actix_web::{ + error::ErrorUnauthorized, + http::{header, Method}, +}; +#[cfg(feature = "enterprise")] +use o2_enterprise::enterprise::{common::infra::config::O2_CONFIG, dex::service::auth::get_jwks}; + +#[cfg(feature = "enterprise")] +use crate::common::utils::jwt; + +#[cfg(feature = "enterprise")] +pub async fn token_validator( + req: ServiceRequest, + token: &str, +) -> Result { + let keys = get_jwks().await; + match jwt::verify_decode_token( + token.strip_prefix("Bearer").unwrap().trim(), + &keys, + &O2_CONFIG.dex.client_id, + false, + ) + .await + { + Ok(res) => { + if res.0.is_valid { + // / Hack for prometheus, need support POST and check the header + let mut req = req; + if req.method().eq(&Method::POST) && !req.headers().contains_key("content-type") { + req.headers_mut().insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/x-www-form-urlencoded"), + ); + } + req.headers_mut().insert( + header::HeaderName::from_static("user_id"), + header::HeaderValue::from_str(&res.0.user_email).unwrap(), + ); + Ok(req) + } else { + Err((ErrorUnauthorized("Unauthorized Access"), req)) + } + } + Err(err) => Err((ErrorUnauthorized(err), req)), + } +} + +#[cfg(not(feature = "enterprise"))] +pub async fn token_validator( + req: ServiceRequest, + _token: &str, +) -> Result { + use actix_web::error::ErrorForbidden; + + Err((ErrorForbidden("Not Supported"), req)) +} diff --git a/src/handler/http/auth/validator.rs b/src/handler/http/auth/validator.rs new file mode 100644 index 0000000000..52ccce833e --- /dev/null +++ b/src/handler/http/auth/validator.rs @@ -0,0 +1,461 @@ +// Copyright 2023 Zinc Labs Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use actix_web::{ + dev::ServiceRequest, + error::{ErrorForbidden, ErrorUnauthorized}, + http::{header, Method}, + web, Error, +}; +use actix_web_httpauth::extractors::basic::BasicAuth; + +#[cfg(not(feature = "enterprise"))] +use crate::common::meta::user::DBUser; +use crate::{ + common::{ + infra::config::CONFIG, + meta::{ + ingestion::INGESTION_EP, + proxy::QueryParamProxyURL, + user::{TokenValidationResponse, UserRole}, + }, + utils::{ + auth::{get_hash, is_root_user, AuthExtractor}, + base64, + }, + }, + service::{db, users}, +}; + +pub const PKCE_STATE_ORG: &str = "o2_pkce_state"; +pub const ACCESS_TOKEN: &str = "access_token"; +pub const REFRESH_TOKEN: &str = "refresh_token"; + +pub async fn validator( + req: ServiceRequest, + user_id: &str, + password: &str, +) -> Result { + let path = match req + .request() + .path() + .strip_prefix(format!("{}/api/", CONFIG.common.base_uri).as_str()) + { + Some(path) => path, + None => req.request().path(), + }; + match validate_credentials(user_id, password.trim(), path).await { + Ok(res) => { + if res.is_valid { + // / Hack for prometheus, need support POST and check the header + let mut req = req; + if req.method().eq(&Method::POST) && !req.headers().contains_key("content-type") { + req.headers_mut().insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/x-www-form-urlencoded"), + ); + } + + req.headers_mut().insert( + header::HeaderName::from_static("user_id"), + header::HeaderValue::from_str(&res.user_email).unwrap(), + ); + Ok(req) + } else { + Err((ErrorUnauthorized("Unauthorized Access"), req)) + } + } + Err(err) => Err((err, req)), + } +} + +/// `validate_token` validates the endpoints which are token only. +/// This includes endpoints like `rum` etc. +/// +/// ### Args: +/// - token: The token to validate +/// +pub async fn validate_token(token: &str, org_id: &str) -> Result { + match users::get_user_by_token(org_id, token).await { + Some(_user) => Ok(true), + None => Err(ErrorForbidden("Not allowed")), + } +} + +pub async fn validate_credentials( + user_id: &str, + user_password: &str, + path: &str, +) -> Result { + let user; + let mut path_columns = path.split('/').collect::>(); + if let Some(v) = path_columns.last() { + if v.is_empty() { + path_columns.pop(); + } + } + + // this is only applicable for super admin user + if is_root_user(user_id) { + user = users::get_user(None, user_id).await; + if user.is_none() { + return Ok(TokenValidationResponse { + is_valid: false, + user_email: "".to_string(), + }); + } + } else if path_columns.last().unwrap_or(&"").eq(&"organizations") { + let db_user = db::user::get_db_user(user_id).await; + user = match db_user { + Ok(user) => { + let all_users = user.get_all_users(); + if all_users.is_empty() { + None + } else { + all_users.first().cloned() + } + } + Err(_) => None, + } + } else { + user = match path.find('/') { + Some(index) => { + let org_id = &path[0..index]; + users::get_user(Some(org_id), user_id).await + } + None => users::get_user(None, user_id).await, + } + }; + + if user.is_none() { + return Ok(TokenValidationResponse { + is_valid: false, + user_email: "".to_string(), + }); + } + let user = user.unwrap(); + + if (path_columns.len() == 1 || INGESTION_EP.iter().any(|s| path_columns.contains(s))) + && user.token.eq(&user_password) + { + return Ok(TokenValidationResponse { + is_valid: true, + user_email: user.email, + }); + } + + let in_pass = get_hash(user_password, &user.salt); + if !user.password.eq(&in_pass) { + return Ok(TokenValidationResponse { + is_valid: false, + user_email: "".to_string(), + }); + } + if !path.contains("/user") + || (path.contains("/user") + && (user.role.eq(&UserRole::Admin) + || user.role.eq(&UserRole::Root) + || user.email.eq(user_id))) + { + Ok(TokenValidationResponse { + is_valid: true, + user_email: user.email, + }) + } else { + Err(ErrorForbidden("Not allowed")) + } +} + +#[cfg(not(feature = "enterprise"))] +async fn validate_user_from_db( + db_user: Result, + user_password: &str, +) -> Result { + // let db_user = db::user::get_db_user(user_id).await; + match db_user { + Ok(user) => { + let in_pass = get_hash(user_password, &user.salt); + if user.password.eq(&in_pass) { + Ok(TokenValidationResponse { + is_valid: true, + user_email: user.email, + }) + } else { + Err(ErrorForbidden("Not allowed")) + } + } + Err(_) => Err(ErrorForbidden("Not allowed")), + } +} + +#[cfg(feature = "enterprise")] +pub async fn validate_user( + _user_id: &str, + _user_password: &str, +) -> Result { + use actix_web::error::ErrorNotFound; + + Err(ErrorNotFound("Not supported in enterprise version")) +} + +#[cfg(not(feature = "enterprise"))] +pub async fn validate_user( + user_id: &str, + user_password: &str, +) -> Result { + let db_user = db::user::get_db_user(user_id).await; + validate_user_from_db(db_user, user_password).await +} + +pub async fn validator_aws( + req: ServiceRequest, + _credentials: Option, +) -> Result { + let path = req + .request() + .path() + .strip_prefix(format!("{}/aws/", CONFIG.common.base_uri).as_str()) + .unwrap_or(req.request().path()); + + match req.headers().get("X-Amz-Firehose-Access-Key") { + Some(val) => match val.to_str() { + Ok(val) => { + let amz_creds = match base64::decode(val) { + Ok(val) => val, + Err(_) => return Err((ErrorUnauthorized("Unauthorized Access"), req)), + }; + let creds = amz_creds + .split(':') + .map(|s| s.to_string()) + .collect::>(); + + match validate_credentials(&creds[0], &creds[1], path).await { + Ok(res) => { + if res.is_valid { + Ok(req) + } else { + Err((ErrorUnauthorized("Unauthorized Access"), req)) + } + } + Err(err) => Err((err, req)), + } + } + Err(_) => Err((ErrorUnauthorized("Unauthorized Access"), req)), + }, + None => Err((ErrorUnauthorized("Unauthorized Access"), req)), + } +} + +pub async fn validator_gcp( + req: ServiceRequest, + _credentials: Option, +) -> Result { + let path = req + .request() + .path() + .strip_prefix(format!("{}/gcp/", CONFIG.common.base_uri).as_str()) + .unwrap_or(req.request().path()); + + let query = + web::Query::>::from_query(req.query_string()) + .unwrap(); + match query.get("API-Key") { + Some(val) => { + let gcp_creds = base64::decode(val).unwrap(); + let creds = gcp_creds + .split(':') + .map(|s| s.to_string()) + .collect::>(); + + match validate_credentials(&creds[0], &creds[1], path).await { + Ok(res) => { + if res.is_valid { + Ok(req) + } else { + Err((ErrorUnauthorized("Unauthorized Access"), req)) + } + } + Err(err) => Err((err, req)), + } + } + None => Err((ErrorUnauthorized("Unauthorized Access"), req)), + } +} + +pub async fn validator_proxy_url( + req: ServiceRequest, + query: web::Query, +) -> Result { + let creds = base64::decode(&query.proxy_token).expect("Invalid base-encoded token"); + let creds = creds + .split(':') + .map(|s| s.to_string()) + .collect::>(); + let path = req + .request() + .path() + .strip_prefix(format!("{}/proxy/", CONFIG.common.base_uri).as_str()) + .unwrap_or(req.request().path()); + + match validate_credentials(&creds[0], &creds[1], path).await { + Ok(res) => { + if res.is_valid { + Ok(req) + } else { + Err((ErrorUnauthorized("Unauthorized Access"), req)) + } + } + Err(err) => Err((err, req)), + } +} + +pub async fn validator_rum( + req: ServiceRequest, + _credentials: Option, +) -> Result { + let path = req + .request() + .path() + .strip_prefix(format!("{}/rum/v1/", CONFIG.common.base_uri).as_str()) + .unwrap_or(req.request().path()); + + // After this previous path clean we should get only the + // remaining `org_id/rum` or `org_id/replay` or `org_id/logs` + let org_id_end_point: Vec<&str> = path.split('/').collect(); + if org_id_end_point.len() != 2 { + return Err(( + ErrorUnauthorized("Unauthorized Access. Please pass a valid org_id."), + req, + )); + } + + let query = + web::Query::>::from_query(req.query_string()) + .unwrap(); + + let token = query.get("oo-api-key").or_else(|| query.get("o2-api-key")); + match token { + Some(token) => match validate_token(token, org_id_end_point[0]).await { + Ok(res) => { + if res { + Ok(req) + } else { + Err((ErrorUnauthorized("Unauthorized Access"), req)) + } + } + Err(err) => Err((err, req)), + }, + None => Err((ErrorUnauthorized("Unauthorized Access"), req)), + } +} + +pub async fn oo_validator( + req: ServiceRequest, + auth_header: AuthExtractor, +) -> Result { + if auth_header.auth.starts_with("Basic") { + let decoded = base64::decode(auth_header.auth.strip_prefix("Basic").unwrap().trim()) + .expect("Failed to decode base64 string"); + let credentials = String::from_utf8(decoded.into()) + .map_err(|_| ()) + .expect("Failed to decode base64 string"); + let parts: Vec<&str> = credentials.split(':').collect(); + if parts.len() != 2 { + return Err((ErrorUnauthorized("Unauthorized Access"), req)); + } + let (username, password) = (parts[0], parts[1]); + let username = username.to_owned(); + let password = password.to_owned(); + validator(req, &username, &password).await + } else if auth_header.auth.starts_with("Bearer") { + super::token::token_validator(req, &auth_header.auth).await + } else { + Err((ErrorUnauthorized("Unauthorized Access"), req)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::common::{infra::db as infra_db, meta::user::UserRequest}; + + #[actix_web::test] + async fn test_validate() { + infra_db::create_table().await.unwrap(); + let _ = users::post_user( + "default", + UserRequest { + email: "root@example.com".to_string(), + password: "Complexpass#123".to_string(), + role: crate::common::meta::user::UserRole::Root, + first_name: "root".to_owned(), + last_name: "".to_owned(), + is_external: false, + }, + ) + .await; + let _ = users::post_user( + "default", + UserRequest { + email: "user1@example.com".to_string(), + password: "Complexpass#123".to_string(), + role: crate::common::meta::user::UserRole::Member, + first_name: "root".to_owned(), + last_name: "".to_owned(), + is_external: false, + }, + ) + .await; + + let pwd = "Complexpass#123"; + assert!( + validate_credentials("root@example.com", pwd, "default/_bulk") + .await + .unwrap() + .is_valid + ); + assert!( + !validate_credentials("", pwd, "default/_bulk") + .await + .unwrap() + .is_valid + ); + assert!(!validate_credentials("", pwd, "/").await.unwrap().is_valid); + assert!( + !validate_credentials("user1@example.com", pwd, "/") + .await + .unwrap() + .is_valid + ); + assert!( + validate_credentials("user1@example.com", pwd, "default/user") + .await + .unwrap() + .is_valid + ); + assert!( + !validate_credentials("user1@example.com", "x", "default/user") + .await + .unwrap() + .is_valid + ); + assert!( + validate_user("root@example.com", pwd) + .await + .unwrap() + .is_valid + ); + } +} diff --git a/src/handler/http/request/organization/mod.rs b/src/handler/http/request/organization/mod.rs index d2fa2285a0..dbdbfa044c 100644 --- a/src/handler/http/request/organization/mod.rs +++ b/src/handler/http/request/organization/mod.rs @@ -16,7 +16,6 @@ use std::{collections::HashSet, io::Error}; use actix_web::{get, post, put, web, HttpResponse, Result}; -use actix_web_httpauth::extractors::basic::BasicAuth; use crate::{ common::{ @@ -25,7 +24,7 @@ use crate::{ OrgDetails, OrgUser, OrganizationResponse, PasscodeResponse, RumIngestionResponse, CUSTOM, DEFAULT_ORG, THRESHOLD, }, - utils::auth::is_root_user, + utils::auth::{is_root_user, UserEmail}, }, service::organization::{self, get_passcode, get_rum_token, update_passcode, update_rum_token}, }; @@ -49,8 +48,8 @@ pub mod settings; ) )] #[get("/{org_id}/organizations")] -pub async fn organizations(credentials: BasicAuth) -> Result { - let user_id = credentials.user_id(); +pub async fn organizations(user_email: UserEmail) -> Result { + let user_id = user_email.user_id.as_str(); let mut id = 0; let mut orgs: Vec = vec![]; @@ -167,11 +166,11 @@ async fn org_summary(org_id: web::Path) -> Result { )] #[get("/{org_id}/organizations/passcode")] async fn get_user_passcode( - credentials: BasicAuth, + user_email: UserEmail, org_id: web::Path, ) -> Result { let org = org_id.into_inner(); - let user_id = credentials.user_id(); + let user_id = user_email.user_id.as_str(); let mut org_id = Some(org.as_str()); if is_root_user(user_id) { org_id = None; @@ -197,11 +196,11 @@ async fn get_user_passcode( )] #[put("/{org_id}/organizations/passcode")] async fn update_user_passcode( - credentials: BasicAuth, + user_email: UserEmail, org_id: web::Path, ) -> Result { let org = org_id.into_inner(); - let user_id = credentials.user_id(); + let user_id = user_email.user_id.as_str(); let mut org_id = Some(org.as_str()); if is_root_user(user_id) { org_id = None; @@ -227,11 +226,11 @@ async fn update_user_passcode( )] #[get("/{org_id}/organizations/rumtoken")] async fn get_user_rumtoken( - credentials: BasicAuth, + user_email: UserEmail, org_id: web::Path, ) -> Result { let org = org_id.into_inner(); - let user_id = credentials.user_id(); + let user_id = user_email.user_id.as_str(); let mut org_id = Some(org.as_str()); if is_root_user(user_id) { org_id = None; @@ -257,11 +256,11 @@ async fn get_user_rumtoken( )] #[put("/{org_id}/organizations/rumtoken")] async fn update_user_rumtoken( - credentials: BasicAuth, + user_email: UserEmail, org_id: web::Path, ) -> Result { let org = org_id.into_inner(); - let user_id = credentials.user_id(); + let user_id = user_email.user_id.as_str(); let mut org_id = Some(org.as_str()); if is_root_user(user_id) { org_id = None; @@ -287,11 +286,11 @@ async fn update_user_rumtoken( )] #[post("/{org_id}/organizations/rumtoken")] async fn create_user_rumtoken( - credentials: BasicAuth, + user_email: UserEmail, org_id: web::Path, ) -> Result { let org = org_id.into_inner(); - let user_id = credentials.user_id(); + let user_id = user_email.user_id.as_str(); let mut org_id = Some(org.as_str()); if is_root_user(user_id) { org_id = None; diff --git a/src/handler/http/request/status/mod.rs b/src/handler/http/request/status/mod.rs index 3ca9297d91..eb03b06b10 100644 --- a/src/handler/http/request/status/mod.rs +++ b/src/handler/http/request/status/mod.rs @@ -20,6 +20,17 @@ use ahash::AHashMap as HashMap; use datafusion::arrow::datatypes::{Field, Schema}; use serde::Serialize; use utoipa::ToSchema; +#[cfg(feature = "enterprise")] +use { + crate::common::utils::jwt::{process_token, verify_decode_token}, + crate::handler::http::auth::validator::{ACCESS_TOKEN, PKCE_STATE_ORG, REFRESH_TOKEN}, + actix_web::{cookie::Cookie, http::header, web, HttpRequest}, + o2_enterprise::enterprise::{ + common::infra::config::O2_CONFIG, + dex::service::auth::{exchange_code, get_dex_login, get_jwks, refresh_token}, + }, + std::io::ErrorKind, +}; use crate::{ common::{ @@ -51,6 +62,7 @@ struct ConfigResponse<'a> { syslog_enabled: bool, data_retention_days: i64, restricted_routes_on_empty_data: bool, + dex_enabled: bool, } /// Healthz @@ -70,6 +82,11 @@ pub async fn healthz() -> Result { #[get("")] pub async fn zo_config() -> Result { + #[cfg(feature = "enterprise")] + let dex_enabled = O2_CONFIG.dex.dex_enabled; + #[cfg(not(feature = "enterprise"))] + let dex_enabled = false; + Ok(HttpResponse::Ok().json(ConfigResponse { version: VERSION.to_string(), instance: INSTANCE_ID.get("instance_id").unwrap().to_string(), @@ -88,6 +105,7 @@ pub async fn zo_config() -> Result { syslog_enabled: *SYSLOG_ENABLED.read(), data_retention_days: CONFIG.compact.data_retention_days, restricted_routes_on_empty_data: CONFIG.common.restricted_routes_on_empty_data, + dex_enabled, })) } @@ -167,3 +185,108 @@ fn get_stream_schema_status() -> (usize, usize, usize) { } (stream_num, stream_schema_num, mem_size) } + +#[cfg(feature = "enterprise")] +#[get("/callback")] +pub async fn callback(req: HttpRequest) -> Result { + let query = web::Query::>::from_query(req.query_string()).unwrap(); + let code = match query.get("code") { + Some(code) => code, + None => { + return Err(Error::new(ErrorKind::Other, "no code in request")); + } + }; + + match query.get("state") { + Some(code) => match crate::service::kv::get(PKCE_STATE_ORG, code).await { + Ok(_) => { + let _ = crate::service::kv::delete(PKCE_STATE_ORG, code).await; + } + Err(_) => { + return Err(Error::new(ErrorKind::Other, "invalid state in request")); + } + }, + + None => { + return Err(Error::new(ErrorKind::Other, "no state in request")); + } + }; + + match exchange_code(code).await { + Ok(login_data) => { + let token = login_data.access_token; + let keys = get_jwks().await; + let token_ver = + verify_decode_token(&token, &keys, &O2_CONFIG.dex.client_id, true).await; + + match token_ver { + Ok(res) => process_token(res).await, + Err(e) => return Ok(HttpResponse::Unauthorized().json(e.to_string())), + } + + let mut access_token_cookie = Cookie::new(ACCESS_TOKEN, token); + access_token_cookie.set_http_only(true); + access_token_cookie.set_secure(true); + access_token_cookie.set_path("/"); + access_token_cookie.set_same_site(actix_web::cookie::SameSite::Lax); + + let mut refresh_token_cookie = Cookie::new(REFRESH_TOKEN, login_data.refresh_token); + refresh_token_cookie.set_http_only(true); + refresh_token_cookie.set_secure(true); + refresh_token_cookie.set_path("/"); + refresh_token_cookie.set_same_site(actix_web::cookie::SameSite::Lax); + + Ok(HttpResponse::Found() + .append_header((header::LOCATION, login_data.url)) + .cookie(access_token_cookie) + .cookie(refresh_token_cookie) + .finish()) + } + Err(e) => Ok(HttpResponse::Unauthorized().json(e.to_string())), + } +} + +#[cfg(feature = "enterprise")] +#[get("/dex_login")] +pub async fn dex_login() -> Result { + use o2_enterprise::enterprise::dex::meta::auth::PreLoginData; + + let login_data: PreLoginData = get_dex_login(); + let state = login_data.state; + let _ = crate::service::kv::set(PKCE_STATE_ORG, &state, state.to_owned().into()).await; + + Ok(HttpResponse::Ok().json(login_data.url)) +} + +#[cfg(feature = "enterprise")] +#[get("/dex_refresh")] +async fn refresh_token_with_dex(req: actix_web::HttpRequest) -> HttpResponse { + let token = if let Some(cookie) = req.cookie("refresh_token") { + cookie.value().to_string() + } else { + return HttpResponse::Unauthorized().finish(); + }; + + // Exchange the refresh token for a new access token + + match refresh_token(&token).await { + Ok(token_response) => { + // Set the new access token in the cookie + let mut access_token_cookie = Cookie::new(ACCESS_TOKEN, token_response); + access_token_cookie.set_http_only(true); + access_token_cookie.set_secure(true); + access_token_cookie.set_path("/"); + access_token_cookie.set_same_site(actix_web::cookie::SameSite::Lax); + + HttpResponse::Ok().cookie(access_token_cookie).finish() + } + Err(_) => { + let access_cookie = Cookie::new(ACCESS_TOKEN, ""); + let refresh_cookie = Cookie::new(REFRESH_TOKEN, ""); + let mut response = HttpResponse::Unauthorized().finish(); + response.add_removal_cookie(&access_cookie).unwrap(); + response.add_removal_cookie(&refresh_cookie).unwrap(); + response + } + } +} diff --git a/src/handler/http/request/users/mod.rs b/src/handler/http/request/users/mod.rs index 9d0235f347..6df6b6ce12 100644 --- a/src/handler/http/request/users/mod.rs +++ b/src/handler/http/request/users/mod.rs @@ -16,12 +16,12 @@ use std::io::Error; use actix_web::{delete, get, http, post, put, web, HttpResponse}; -use actix_web_httpauth::extractors::basic::BasicAuth; use crate::{ common::{ meta, meta::user::{SignInResponse, SignInUser, UpdateUser, UserOrgRole, UserRequest}, + utils::auth::UserEmail, }, service::users, }; @@ -95,7 +95,7 @@ pub async fn save( pub async fn update( params: web::Path<(String, String)>, user: web::Json, - credentials: BasicAuth, + user_email: UserEmail, ) -> Result { let (org_id, email_id) = params.into_inner(); let email_id = email_id.trim().to_string(); @@ -108,8 +108,8 @@ pub async fn update( )), ); } - let initiator_id = credentials.user_id(); - let self_update = credentials.user_id().eq(&email_id); + let initiator_id = &user_email.user_id; + let self_update = user_email.user_id.eq(&email_id); users::update_user(&org_id, &email_id, self_update, initiator_id, user).await } @@ -134,12 +134,12 @@ pub async fn update( pub async fn add_user_to_org( params: web::Path<(String, String)>, role: web::Json, - credentials: BasicAuth, + user_email: UserEmail, ) -> Result { let (org_id, email_id) = params.into_inner(); let role = role.into_inner().role; - let initiator_id = credentials.user_id(); - users::add_user_to_org(&org_id, &email_id, role, initiator_id).await + let initiator_id = user_email.user_id; + users::add_user_to_org(&org_id, &email_id, role, &initiator_id).await } /// RemoveUserFromOrganization @@ -178,9 +178,9 @@ pub async fn delete(path: web::Path<(String, String)>) -> Result) -> Result { let mut resp = SignInResponse::default(); - match crate::handler::http::auth::validate_user(&auth.name, &auth.password).await { + match crate::handler::http::auth::validator::validate_user(&auth.name, &auth.password).await { Ok(v) => { - if v { + if v.is_valid { resp.status = true; } else { resp.status = false; diff --git a/src/handler/http/router/mod.rs b/src/handler/http/router/mod.rs index 95cbb79154..b4ce9294f2 100644 --- a/src/handler/http/router/mod.rs +++ b/src/handler/http/router/mod.rs @@ -29,7 +29,7 @@ use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; use super::{ - auth::{validator, validator_aws, validator_gcp, validator_proxy_url, validator_rum}, + auth::validator::{validator_aws, validator_gcp, validator_proxy_url, validator_rum}, request::{ dashboards::{folders::*, *}, enrichment_table, functions, kv, logs, metrics, organization, prom, rum, search, status, @@ -44,6 +44,22 @@ use crate::common::{ pub mod openapi; pub mod ui; +#[cfg(feature = "enterprise")] +fn get_cors() -> Rc { + let cors = Cors::default() + .allowed_methods(vec!["HEAD", "GET", "POST", "PUT", "OPTIONS", "DELETE"]) + .allowed_headers(vec![ + header::AUTHORIZATION, + header::ACCEPT, + header::CONTENT_TYPE, + ]) + .allow_any_origin() + .supports_credentials() + .max_age(3600); + Rc::new(cors) +} + +#[cfg(not(feature = "enterprise"))] fn get_cors() -> Rc { let cors = Cors::default() .send_wildcard() @@ -159,16 +175,28 @@ pub fn get_basic_routes(cfg: &mut web::ServiceConfig) { pub fn get_config_routes(cfg: &mut web::ServiceConfig) { let cors = get_cors(); + #[cfg(not(feature = "enterprise"))] cfg.service(web::scope("/config").wrap(cors).service(status::zo_config)); + #[cfg(feature = "enterprise")] + cfg.service( + web::scope("/config") + .wrap(cors) + .service(status::zo_config) + .service(status::callback) + .service(status::dex_login) + .service(status::refresh_token_with_dex), + ); } pub fn get_service_routes(cfg: &mut web::ServiceConfig) { let cors = get_cors(); - let auth = HttpAuthentication::basic(validator); + cfg.service( web::scope("/api") - .wrap(auth) - .wrap(cors) + .wrap(HttpAuthentication::with_fn( + super::auth::validator::oo_validator, + )) + .wrap(cors.clone()) .service(status::cache_status) .service(users::list) .service(users::save) diff --git a/src/job/mod.rs b/src/job/mod.rs index b1233c94bf..801390d2e0 100644 --- a/src/job/mod.rs +++ b/src/job/mod.rs @@ -63,7 +63,7 @@ pub async fn init() -> Result<(), anyhow::Error> { role: crate::common::meta::user::UserRole::Root, first_name: "root".to_owned(), last_name: "".to_owned(), - is_ldap: false, + is_external: false, }, ) .await; diff --git a/src/service/db/user.rs b/src/service/db/user.rs index 5993bce9ff..f98ca89f76 100644 --- a/src/service/db/user.rs +++ b/src/service/db/user.rs @@ -114,7 +114,7 @@ pub async fn set(user: DBUser) -> Result<(), anyhow::Error> { token: org.token, rum_token: org.rum_token.clone(), salt: user.salt.clone(), - is_ldap: user.is_ldap, + is_external: user.is_external, }; USERS.insert( format!("{}/{}", org.name.clone(), user.email.clone()), @@ -235,15 +235,20 @@ pub async fn reset() -> Result<(), anyhow::Error> { Ok(()) } -pub async fn check_user_exists_by_email(email: &str) -> bool { +pub async fn get_user_by_email(email: &str) -> Option { let db = infra_db::get_db().await; let key = "/user/"; let mut ret = db.list_values(key).await.unwrap(); ret.retain(|item| { - let user: DBUser = json::from_slice(item).unwrap(); + let user: DBUser = serde_json::from_slice(item).unwrap(); user.email.eq(email) }); - !ret.is_empty() + if !ret.is_empty() { + let user: DBUser = serde_json::from_slice(&ret[0]).unwrap(); + Some(user) + } else { + None + } } #[cfg(test)] @@ -261,7 +266,7 @@ mod tests { salt: String::from("sdfjshdkfshdfkshdfkshdfkjh"), first_name: "admin".to_owned(), last_name: "".to_owned(), - is_ldap: false, + is_external: false, organizations: vec![UserOrg { role: crate::common::meta::user::UserRole::Admin, name: org_id.clone(), diff --git a/src/service/organization.rs b/src/service/organization.rs index bd85482633..454db7991a 100644 --- a/src/service/organization.rs +++ b/src/service/organization.rs @@ -179,7 +179,7 @@ mod tests { role: crate::common::meta::user::UserRole::Admin, first_name: "admin".to_owned(), last_name: "".to_owned(), - is_ldap: false, + is_external: false, }, ) .await; diff --git a/src/service/users.rs b/src/service/users.rs index ddaabb7e21..712b76ea82 100644 --- a/src/service/users.rs +++ b/src/service/users.rs @@ -27,7 +27,9 @@ use crate::{ meta::{ http::HttpResponse as MetaHttpResponse, organization::DEFAULT_ORG, - user::{UpdateUser, User, UserList, UserOrg, UserRequest, UserResponse, UserRole}, + user::{ + DBUser, UpdateUser, User, UserList, UserOrg, UserRequest, UserResponse, UserRole, + }, }, utils::{ auth::{get_hash, is_root_user}, @@ -54,7 +56,7 @@ pub async fn post_user(org_id: &str, usr_req: UserRequest) -> Result Result Result<(), anyhow::Error> { + if db_user.password.is_empty() { + let salt = Uuid::new_v4().to_string(); + let password = get_hash(&db_user.password, &salt); + db_user.password = password; + db_user.salt = salt; + } + for org in &mut db_user.organizations { + if org.token.is_empty() { + let token = generate_random_string(16); + org.token = token; + }; + if org.rum_token.is_none() { + let rum_token = format!("rum{}", generate_random_string(16)); + org.rum_token = Some(rum_token); + }; + } + db::user::set(db_user).await +} + pub async fn update_user( org_id: &str, email: &str, @@ -91,6 +113,13 @@ pub async fn update_user( let mut message = ""; match existing_user.unwrap() { Some(local_user) => { + if local_user.is_external { + return Ok(HttpResponse::BadRequest().json(MetaHttpResponse::message( + http::StatusCode::BAD_REQUEST.into(), + "Updates not allowed in OpenObserve, please update with source system" + .to_string(), + ))); + } if !self_update { if is_root_user(initiator_id) { allow_password_update = true @@ -124,21 +153,26 @@ pub async fn update_user( } } else if self_update && user.old_password.is_none() { message = "Please provide existing password" - } else if !self_update && allow_password_update && user.new_password.is_some() { + } else if !self_update + && allow_password_update + && user.new_password.is_some() + && !local_user.is_external + { new_user.password = get_hash(&user.new_password.unwrap(), &local_user.salt); is_updated = true; } else { message = "You are not authorised to change the password" } - if user.first_name.is_some() { + if user.first_name.is_some() && !local_user.is_external { new_user.first_name = user.first_name.unwrap(); is_updated = true; } - if user.last_name.is_some() { + if user.last_name.is_some() && !local_user.is_external { new_user.last_name = user.last_name.unwrap(); is_updated = true; } if user.role.is_some() + && !local_user.is_external && (!self_update || (local_user.role.eq(&UserRole::Admin) || local_user.role.eq(&UserRole::Root))) @@ -323,7 +357,7 @@ pub async fn list_users(org_id: &str) -> Result { role: user.value().role.clone(), first_name: user.value().first_name.clone(), last_name: user.value().last_name.clone(), - is_ldap: user.value().is_ldap, + is_external: user.value().is_external, }) } } @@ -421,7 +455,7 @@ mod tests { first_name: "admin".to_owned(), last_name: "".to_owned(), org: "dummy".to_string(), - is_ldap: false, + is_external: false, }, ); } @@ -455,7 +489,7 @@ mod tests { role: crate::common::meta::user::UserRole::Admin, first_name: "admin".to_owned(), last_name: "".to_owned(), - is_ldap: false, + is_external: false, }, ) .await; @@ -473,7 +507,7 @@ mod tests { role: crate::common::meta::user::UserRole::Admin, first_name: "admin".to_owned(), last_name: "".to_owned(), - is_ldap: false, + is_external: false, }, ) .await; @@ -490,7 +524,7 @@ mod tests { first_name: "admin".to_owned(), last_name: "".to_owned(), org: "dummy".to_string(), - is_ldap: false, + is_external: false, }, ); @@ -506,6 +540,7 @@ mod tests { old_password: Some("pass".to_string()), new_password: Some("new_pass".to_string()), role: Some(crate::common::meta::user::UserRole::Member), + change_password: false, }, ) .await; @@ -524,6 +559,7 @@ mod tests { old_password: None, new_password: None, role: Some(crate::common::meta::user::UserRole::Admin), + change_password: false, }, ) .await; diff --git a/web/package-lock.json b/web/package-lock.json index 62eb01758c..0f0c081370 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -22,6 +22,7 @@ "@sentry/tracing": "^7.17.4", "@sentry/vite-plugin": "^2.7.1", "@sentry/vue": "^7.73.0", + "@types/axios": "^0.14.0", "d3-hierarchy": "^3.1.2", "d3-selection": "^3.0.0", "date-fns-tz": "^2.0.0", @@ -36,6 +37,7 @@ "moment-timezone": "^0.5.43", "node-polyfill-webpack-plugin": "^2.0.1", "node-sql-parser": "^4.6.4", + "oidc-client-ts": "^2.4.0", "quasar": "^2.7.5", "rollup-plugin-node-polyfills": "^0.2.1", "rollup-plugin-polyfill-node": "^0.10.2", @@ -5367,6 +5369,15 @@ "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-1.0.13.tgz", "integrity": "sha512-5lYJP45Xllo4yE/RUBccBT32eBlRDbqN8r1/MIvQbKxW3aFqaYPCNgm8D5V20X4ShHcwvYWNlKg3liDh1MlBoA==" }, + "node_modules/@types/axios": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", + "integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==", + "deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!", + "dependencies": { + "axios": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -7135,8 +7146,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -7177,7 +7187,6 @@ "version": "0.27.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "dev": true, "dependencies": { "follow-redirects": "^1.14.9", "form-data": "^4.0.0" @@ -7187,7 +7196,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -8030,7 +8038,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -8320,6 +8327,11 @@ "node": "*" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/css-select": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", @@ -8854,7 +8866,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -10829,7 +10840,6 @@ "version": "1.15.3", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", - "dev": true, "funding": [ { "type": "individual", @@ -14361,6 +14371,18 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "node_modules/oidc-client-ts": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz", + "integrity": "sha512-WijhkTrlXK2VvgGoakWJiBdfIsVGz6CFzgjNNqZU1hPKV2kyeEaJgLs7RwuiSp2WhLfWBQuLvr2SxVlZnk3N1w==", + "dependencies": { + "crypto-js": "^4.2.0", + "jwt-decode": "^3.1.2" + }, + "engines": { + "node": ">=12.13.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", diff --git a/web/package.json b/web/package.json index ef9b908e6d..4fc0381e61 100644 --- a/web/package.json +++ b/web/package.json @@ -36,6 +36,7 @@ "@sentry/tracing": "^7.17.4", "@sentry/vite-plugin": "^2.7.1", "@sentry/vue": "^7.73.0", + "@types/axios": "^0.14.0", "d3-hierarchy": "^3.1.2", "d3-selection": "^3.0.0", "date-fns-tz": "^2.0.0", @@ -50,6 +51,7 @@ "moment-timezone": "^0.5.43", "node-polyfill-webpack-plugin": "^2.0.1", "node-sql-parser": "^4.6.4", + "oidc-client-ts": "^2.4.0", "quasar": "^2.7.5", "rollup-plugin-node-polyfills": "^0.2.1", "rollup-plugin-polyfill-node": "^0.10.2", diff --git a/web/src/aws-exports.ts b/web/src/aws-exports.ts index bb5def22c3..531f20f8d3 100644 --- a/web/src/aws-exports.ts +++ b/web/src/aws-exports.ts @@ -23,9 +23,8 @@ const config = { aws_mobile_analytics_app_region: "us-west-2", oauth: { domain: import.meta.env.VITE_OAUTH_DOMAIN, - scope: `openid%20email%20profile%20offline_access%20urn%3Azitadel%3Aiam%3Aorg%3Aproject%3Aid%3Azitadel%3Aaud%20urn%3Azitadel%3Aiam%3Auser%3Ametadata%20urn%3Azitadel%3Aiam%3Aorg%3Aid%3A${ - import.meta.env.VITE_ZITADEL_ORG_ID - }&prompt=login&code_challenge=9az09PjcfuENS7oDK7jUd2xAWRb-B3N7Sr3kDoWECOY&code_challenge_method=S256`, + scope: `openid%20email%20profile%20offline_access%20urn%3Azitadel%3Aiam%3Aorg%3Aproject%3Aid%3Azitadel%3Aaud%20urn%3Azitadel%3Aiam%3Auser%3Ametadata%20urn%3Azitadel%3Aiam%3Aorg%3Aid%3A${import.meta.env.VITE_ZITADEL_ORG_ID + }&prompt=login&code_challenge=9az09PjcfuENS7oDK7jUd2xAWRb-B3N7Sr3kDoWECOY&code_challenge_method=S256`, redirectSignIn: import.meta.env.VITE_REDIRECT_SIGNIN, redirectSignOut: import.meta.env.VITE_REDIRECT_SIGNOUT, responseType: "code", @@ -40,6 +39,9 @@ const config = { isCloud: import.meta.env.VITE_OPENOBSERVE_CLOUD ? import.meta.env.VITE_OPENOBSERVE_CLOUD : "false", + isEnterprise: import.meta.env.VITE_OPENOBSERVE_ENTERPRISE + ? import.meta.env.VITE_OPENOBSERVE_ENTERPRISE + : "false", freePlan: "Free-Plan-USD-Monthly", paidPlan: "professional-USD-Monthly", ooApplicationID: import.meta.env.VITE_OO_APP_ID, diff --git a/web/src/enterprise/components/login/Login.vue b/web/src/enterprise/components/login/Login.vue index 3613a685d5..3782c42c01 100644 --- a/web/src/enterprise/components/login/Login.vue +++ b/web/src/enterprise/components/login/Login.vue @@ -27,7 +27,7 @@ import { useQuasar } from "quasar"; import { useRouter } from "vue-router"; import { useStore } from "vuex"; import { getUserInfo, getDecodedUserInfo, getPath } from "@/utils/zincutils"; - +import config from "../../../aws-exports"; import usersService from "@/services/users"; import organizationsService from "@/services/organizations"; import { useLocalCurrentUser, useLocalOrganization } from "@/utils/zincutils"; @@ -52,7 +52,7 @@ export default defineComponent({ * redirect user to the page where user was redirected from */ const getDefaultOrganization = () => { - organizationsService.list(0, 1000, "id", false, "").then((res: any) => { + organizationsService.os_list(0, 1000, "id", false, "").then((res: any) => { const localOrg: any = useLocalOrganization(); if ( localOrg.value != null && @@ -89,7 +89,7 @@ export default defineComponent({ if ( (selectedOrg.value == "" && - data.type == "default" && + (data.type == "default" || data.id == '1') && store.state.userInfo.email == data.UserObj.email) || res.data.data.length == 1 ) { @@ -163,11 +163,12 @@ export default defineComponent({ const d = new Date(); this.userInfo = sessionUserInfo !== null ? JSON.parse(sessionUserInfo) : null; - if (this.userInfo !== null && this.userInfo.hasOwnProperty("pgdata")) { + if (this.userInfo !== null && (this.userInfo.hasOwnProperty("pgdata")) || config.isEnterprise === "true") { this.store.dispatch("login", { loginState: true, userInfo: this.userInfo, }); + this.getDefaultOrganization(); } else { this.VerifyAndCreateUser(); } diff --git a/web/src/enterprise/composables/router.ts b/web/src/enterprise/composables/router.ts index 2ed347ae75..92fa506f3a 100644 --- a/web/src/enterprise/composables/router.ts +++ b/web/src/enterprise/composables/router.ts @@ -23,21 +23,38 @@ import { getLogoutURL, } from "@/utils/zincutils"; +import authService from "@/services/auth"; +import config from "@/aws-exports"; import Organizations from "@/enterprise/components/organizations/Organization.vue"; - import Billing from "@/enterprise/components/billings/Billing.vue"; import Plans from "@/enterprise/components/billings/plans.vue"; import InvoiceHistory from "@/enterprise/components/billings/invoiceHistory.vue"; import Usage from "@/enterprise/components/billings/usage.vue"; import { routeGuard } from "@/utils/zincutils"; + const useEnvRoutes = () => { const parentRoutes = [ { path: "/login", component: Login, - beforeEnter(to: any, from: any, next: any) { - window.location.href = getLoginURL(); + beforeEnter: async (to: any, from: any, next: any) => { + if (config.isEnterprise == "true" || config.isEnterprise) { + try { + const url = await authService.get_dex_login(); + if (url) { + window.location.href = url; + } else { + next(); + } + } catch (error) { + console.error("Error during redirection:", error); + next(false); + } + } else { + window.location.href = getLoginURL(); + } + }, }, { @@ -46,7 +63,6 @@ const useEnvRoutes = () => { useLocalToken("", true); useLocalCurrentUser("", true); useLocalUserInfo("", true); - window.location.href = getLogoutURL(); }, }, diff --git a/web/src/layouts/MainLayout.vue b/web/src/layouts/MainLayout.vue index 99a7877c70..e143010997 100644 --- a/web/src/layouts/MainLayout.vue +++ b/web/src/layouts/MainLayout.vue @@ -19,99 +19,45 @@ along with this program. If not, see .
- - Beta + + Beta
-
-
- {{ +
+
+ {{ store.state.organizationData.quotaThresholdMsg }}
- Upgrade to PRO Plan + Upgrade to PRO Plan
- +
- + - + - + {{ lang.label }} @@ -120,37 +66,26 @@ class="warning" />{{
- +
- +