From bb2f765e5de028c58c9ba02e8bfd944461b2f8f7 Mon Sep 17 00:00:00 2001 From: bestgopher <84328409@qq.com> Date: Mon, 14 Oct 2024 23:11:49 +0800 Subject: [PATCH] add iam system add iam store feat: add crypto crate introduce decrypt_data and encrypt_data functions Signed-off-by: bestgopher <84328409@qq.com> --- Cargo.lock | 469 ++++++++++++++++--- Cargo.toml | 5 +- crypto/Cargo.toml | 32 ++ crypto/src/encdec.rs | 11 + crypto/src/encdec/aes.rs | 18 + crypto/src/encdec/decrypt.rs | 41 ++ crypto/src/encdec/encrypt.rs | 53 +++ crypto/src/encdec/id.rs | 39 ++ crypto/src/encdec/tests.rs | 14 + crypto/src/error.rs | 28 ++ crypto/src/jwt.rs | 6 + crypto/src/jwt/decode.rs | 12 + crypto/src/jwt/encode.rs | 12 + crypto/src/jwt/tests.rs | 16 + crypto/src/lib.rs | 9 + ecstore/src/config/error.rs | 5 +- ecstore/src/lib.rs | 2 + ecstore/src/store.rs | 2 +- ecstore/src/store_api.rs | 10 +- iam/Cargo.toml | 28 ++ iam/src/arn.rs | 18 + iam/src/auth.rs | 23 + iam/src/auth/credentials.rs | 372 +++++++++++++++ iam/src/cache.rs | 320 +++++++++++++ iam/src/error.rs | 35 ++ iam/src/format.rs | 17 + iam/src/lib.rs | 58 +++ iam/src/manager.rs | 223 +++++++++ iam/src/policy.rs | 120 +++++ iam/src/policy/action.rs | 143 ++++++ iam/src/policy/doc.rs | 12 + iam/src/policy/effect.rs | 32 ++ iam/src/policy/function.rs | 175 +++++++ iam/src/policy/function/addr.rs | 146 ++++++ iam/src/policy/function/binary.rs | 10 + iam/src/policy/function/bool_null.rs | 119 +++++ iam/src/policy/function/condition.rs | 78 +++ iam/src/policy/function/date.rs | 107 +++++ iam/src/policy/function/func.rs | 91 ++++ iam/src/policy/function/key.rs | 110 +++++ iam/src/policy/function/key_name.rs | 333 +++++++++++++ iam/src/policy/function/number.rs | 113 +++++ iam/src/policy/function/string.rs | 220 +++++++++ iam/src/policy/id.rs | 29 ++ iam/src/policy/policy.rs | 235 ++++++++++ iam/src/policy/resource.rs | 118 +++++ iam/src/policy/statement.rs | 102 ++++ iam/src/policy/utils.rs | 87 ++++ iam/src/policy/utils/path.rs | 141 ++++++ iam/src/policy/utils/wildcard.rs | 189 ++++++++ iam/src/service_type.rs | 20 + iam/src/store.rs | 43 ++ iam/src/store/object.rs | 345 ++++++++++++++ iam/src/utils.rs | 62 +++ rustfs/Cargo.toml | 4 +- rustfs/src/admin/handlers.rs | 2 + rustfs/src/admin/handlers/service_account.rs | 229 +++++++++ rustfs/src/admin/mod.rs | 34 ++ rustfs/src/admin/models.rs | 1 + rustfs/src/admin/models/service_account.rs | 51 ++ rustfs/src/main.rs | 4 +- 61 files changed, 5319 insertions(+), 64 deletions(-) create mode 100644 crypto/Cargo.toml create mode 100644 crypto/src/encdec.rs create mode 100644 crypto/src/encdec/aes.rs create mode 100644 crypto/src/encdec/decrypt.rs create mode 100644 crypto/src/encdec/encrypt.rs create mode 100644 crypto/src/encdec/id.rs create mode 100644 crypto/src/encdec/tests.rs create mode 100644 crypto/src/error.rs create mode 100644 crypto/src/jwt.rs create mode 100644 crypto/src/jwt/decode.rs create mode 100644 crypto/src/jwt/encode.rs create mode 100644 crypto/src/jwt/tests.rs create mode 100644 crypto/src/lib.rs create mode 100644 iam/Cargo.toml create mode 100644 iam/src/arn.rs create mode 100644 iam/src/auth.rs create mode 100644 iam/src/auth/credentials.rs create mode 100644 iam/src/cache.rs create mode 100644 iam/src/error.rs create mode 100644 iam/src/format.rs create mode 100644 iam/src/lib.rs create mode 100644 iam/src/manager.rs create mode 100644 iam/src/policy.rs create mode 100644 iam/src/policy/action.rs create mode 100644 iam/src/policy/doc.rs create mode 100644 iam/src/policy/effect.rs create mode 100644 iam/src/policy/function.rs create mode 100644 iam/src/policy/function/addr.rs create mode 100644 iam/src/policy/function/binary.rs create mode 100644 iam/src/policy/function/bool_null.rs create mode 100644 iam/src/policy/function/condition.rs create mode 100644 iam/src/policy/function/date.rs create mode 100644 iam/src/policy/function/func.rs create mode 100644 iam/src/policy/function/key.rs create mode 100644 iam/src/policy/function/key_name.rs create mode 100644 iam/src/policy/function/number.rs create mode 100644 iam/src/policy/function/string.rs create mode 100644 iam/src/policy/id.rs create mode 100644 iam/src/policy/policy.rs create mode 100644 iam/src/policy/resource.rs create mode 100644 iam/src/policy/statement.rs create mode 100644 iam/src/policy/utils.rs create mode 100644 iam/src/policy/utils/path.rs create mode 100644 iam/src/policy/utils/wildcard.rs create mode 100644 iam/src/service_type.rs create mode 100644 iam/src/store.rs create mode 100644 iam/src/store/object.rs create mode 100644 iam/src/utils.rs create mode 100644 rustfs/src/admin/handlers/service_account.rs create mode 100644 rustfs/src/admin/models.rs create mode 100644 rustfs/src/admin/models/service_account.rs diff --git a/Cargo.lock b/Cargo.lock index 6d3c3d53..03538738 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,41 @@ dependencies = [ "tower 0.5.1", ] +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.6", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.8" @@ -122,6 +157,24 @@ version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -208,7 +261,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "tokio", "tower 0.5.1", "tower-layer", @@ -231,7 +284,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "tower-layer", "tower-service", "tracing", @@ -263,6 +316,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -279,6 +338,12 @@ dependencies = [ "vsimd", ] +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" @@ -344,18 +409,18 @@ checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" [[package]] name = "bytestring" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" dependencies = [ "bytes", ] [[package]] name = "cc" -version = "1.1.37" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" +checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" dependencies = [ "jobserver", "libc", @@ -374,6 +439,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.38" @@ -389,6 +478,17 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.6", + "inout", + "zeroize", +] + [[package]] name = "clap" version = "4.5.21" @@ -425,9 +525,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] name = "colorchoice" @@ -507,9 +607,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -532,6 +632,24 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto" +version = "0.0.1" +dependencies = [ + "aes-gcm", + "argon2", + "cfg-if", + "chacha20poly1305", + "jsonwebtoken", + "pbkdf2", + "rand", + "serde_json", + "sha2 0.10.8", + "test-case", + "thiserror 2.0.3", + "time", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -539,6 +657,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -553,6 +672,15 @@ dependencies = [ "rand_core", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darwin-libproc" version = "0.1.2" @@ -688,7 +816,7 @@ dependencies = [ "s3s-policy", "serde", "serde_json", - "sha2", + "sha2 0.11.0-pre.4", "siphasher", "tempfile", "thiserror 2.0.3", @@ -722,12 +850,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -754,9 +882,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -898,8 +1026,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", ] [[package]] @@ -969,9 +1109,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -1001,6 +1141,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c706f1711006204c2ba8fb1a7bd55f689bbf7feca9ff40325206b5e140cff6df" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "hmac" version = "0.13.0-pre.4" @@ -1118,6 +1267,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iam" +version = "0.0.1" +dependencies = [ + "arc-swap", + "async-trait", + "base64-simd", + "crypto", + "ecstore", + "futures", + "ipnetwork", + "itertools", + "log", + "rand", + "serde", + "serde_json", + "strum", + "test-case", + "thiserror 2.0.3", + "time", + "tokio", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -1297,10 +1469,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.1", + "hashbrown 0.15.2", "serde", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -1310,6 +1491,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + [[package]] name = "is_debug" version = "1.0.1" @@ -1333,9 +1523,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jobserver" @@ -1355,6 +1545,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1363,9 +1568,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.162" +version = "0.2.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" [[package]] name = "libgit2-sys" @@ -1405,9 +1610,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock" @@ -1729,6 +1934,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.68" @@ -1804,6 +2015,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -1834,6 +2056,26 @@ dependencies = [ "once_cell", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1894,6 +2136,29 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d0eef3571242013a0d5dc84861c3ae4a652e56e12adf8bdc26ff5f8cb34c94" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1921,9 +2186,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -2037,9 +2302,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.37.0" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbfb3ddf5364c9cfcd65549a1e7b801d0e8d1b14c1a1590a6408aa93cfbfa84" +checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03" dependencies = [ "memchr", "serde", @@ -2094,7 +2359,7 @@ dependencies = [ "md-5", "pin-project-lite", "s3s", - "sha2", + "sha2 0.11.0-pre.4", "thiserror 2.0.3", "tokio", "tracing", @@ -2132,7 +2397,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.8", + "regex-automata 0.4.9", "regex-syntax 0.8.5", ] @@ -2147,9 +2412,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -2232,6 +2497,7 @@ dependencies = [ "clap", "common", "const-str", + "crypto", "ecstore", "flatbuffers", "futures", @@ -2241,6 +2507,7 @@ dependencies = [ "http-body", "hyper", "hyper-util", + "iam", "lazy_static", "lock", "log", @@ -2278,9 +2545,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.39" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags 2.6.0", "errno", @@ -2291,9 +2558,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.16" +version = "0.23.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" dependencies = [ "log", "once_cell", @@ -2359,7 +2626,7 @@ dependencies = [ "digest 0.11.0-pre.9", "futures", "hex-simd", - "hmac", + "hmac 0.13.0-pre.4", "http-body", "http-body-util", "httparse", @@ -2375,10 +2642,10 @@ dependencies = [ "serde", "serde_urlencoded", "sha1", - "sha2", + "sha2 0.11.0-pre.4", "smallvec", "std-next", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "thiserror 2.0.3", "time", "tokio", @@ -2477,6 +2744,17 @@ dependencies = [ "digest 0.11.0-pre.9", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.11.0-pre.4" @@ -2531,6 +2809,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[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 1.0.69", + "time", +] + [[package]] name = "siphasher" version = "1.0.1" @@ -2554,9 +2844,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2590,6 +2880,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2598,9 +2910,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.87" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", @@ -2615,9 +2927,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "synstructure" @@ -2643,6 +2955,39 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "test-case-core", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2809,7 +3154,7 @@ dependencies = [ "async-stream", "async-trait", "axum", - "base64", + "base64 0.22.1", "bytes", "flate2", "h2", @@ -3039,9 +3384,9 @@ checksum = "ccb97dac3243214f8d8507998906ca3e2e0b900bf9bf4870477f125b82e68f6e" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-xid" @@ -3049,6 +3394,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.6", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3348,9 +3703,9 @@ checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -3360,9 +3715,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", @@ -3393,18 +3748,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 43a8abb6..e8f2eea1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,4 @@ [workspace] -resolver = "2" members = [ "madmin", "rustfs", @@ -11,7 +10,10 @@ members = [ "api/admin", "reader", "common/workers", + "iam", + "crypto", ] +resolver = "2" [workspace.package] edition = "2021" @@ -90,3 +92,4 @@ log = "0.4.22" axum = "0.7.9" md-5 = "0.10.6" workers = { path = "./common/workers" } +test-case = "3.3.1" diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml new file mode 100644 index 00000000..22204622 --- /dev/null +++ b/crypto/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "crypto" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[dependencies] +aes-gcm = { version = "0.10.3", features = ["std"], optional = true } +argon2 = { version = "0.5.3", features = ["std"], optional = true } +cfg-if = "1.0.0" +chacha20poly1305 = { version = "0.10.1", optional = true } +jsonwebtoken = "9.3.0" +pbkdf2 = { version = "0.12.2", optional = true } +rand.workspace = true +sha2 = "0.10.8" +thiserror.workspace = true +serde_json.workspace = true + + +[dev-dependencies] +test-case.workspace = true +time.workspace = true + +[features] +fips = [] +crypto = ["dep:aes-gcm", "dep:argon2", "dep:chacha20poly1305", "dep:pbkdf2"] +# default = ["crypto", "fips"] + +[lints.clippy] +unwrap_used = "deny" diff --git a/crypto/src/encdec.rs b/crypto/src/encdec.rs new file mode 100644 index 00000000..0aec217c --- /dev/null +++ b/crypto/src/encdec.rs @@ -0,0 +1,11 @@ +#[cfg(not(feature = "fips"))] +mod aes; + +#[cfg(any(test, feature = "crypto"))] +pub(crate) mod id; + +pub(crate) mod decrypt; +pub(crate) mod encrypt; + +#[cfg(test)] +mod tests; diff --git a/crypto/src/encdec/aes.rs b/crypto/src/encdec/aes.rs new file mode 100644 index 00000000..e1a3649a --- /dev/null +++ b/crypto/src/encdec/aes.rs @@ -0,0 +1,18 @@ +pub fn native_aes() -> bool { + cfg_if::cfg_if! { + if #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] { + std::is_x86_feature_detected!("aes") && std::is_x86_feature_detected!("pclmulqdq") + } else if #[cfg(target_arch = "aarch64")] { + std::arch::is_aarch64_feature_detected!("aes") + } else if #[cfg(target_arch = "powerpc64")] { + false + } else if #[cfg(target_arch = "s390x")] { + std::is_s390x_feature_detected!("aes") + && std::is_s390x_feature_detected!("aescbc") + && std::is_s390x_feature_detected!("aesctr") + && (std::is_s390x_feature_detected!("aesgcm") || std::is_s390x_feature_detected!("ghash")) + } else { + false + } + } +} diff --git a/crypto/src/encdec/decrypt.rs b/crypto/src/encdec/decrypt.rs new file mode 100644 index 00000000..13a81dd1 --- /dev/null +++ b/crypto/src/encdec/decrypt.rs @@ -0,0 +1,41 @@ +#[cfg(any(test, feature = "crypto"))] +pub fn decrypt_data(password: &[u8], data: &[u8]) -> Result, crate::Error> { + use crate::encdec::id::ID; + use aes_gcm::{Aes256Gcm, KeyInit as _}; + use chacha20poly1305::ChaCha20Poly1305; + + // 32: salt + // 1: id + // 12: nonce + const HEADER_LENGTH: usize = 45; + if data.len() < HEADER_LENGTH { + return Err(Error::ErrUnexpectedHeader); + } + + let (salt, id, nonce) = (&data[..32], ID::try_from(data[32])?, &data[33..45]); + let data = &data[HEADER_LENGTH..]; + + match id { + ID::Argon2idChaCHa20Poly1305 => { + let key = id.get_key(password, salt)?; + decryp(ChaCha20Poly1305::new_from_slice(&key)?, nonce, data) + } + _ => { + let key = id.get_key(password, salt)?; + decryp(Aes256Gcm::new_from_slice(&key)?, nonce, data) + } + } +} + +#[cfg(any(test, feature = "crypto"))] +#[inline] +fn decryp(stream: T, nonce: &[u8], data: &[u8]) -> Result, crate::Error> { + stream + .decrypt(aes_gcm::Nonce::from_slice(nonce), data) + .map_err(Error::ErrDecryptFailed) +} + +#[cfg(all(not(test), not(feature = "crypto")))] +pub fn decrypt_data(_password: &[u8], data: &[u8]) -> Result, crate::Error> { + Ok(data.to_vec()) +} diff --git a/crypto/src/encdec/encrypt.rs b/crypto/src/encdec/encrypt.rs new file mode 100644 index 00000000..a6b5c28d --- /dev/null +++ b/crypto/src/encdec/encrypt.rs @@ -0,0 +1,53 @@ +#[cfg(any(test, feature = "crypto"))] +pub fn encrypt_data(password: &[u8], data: &[u8]) -> Result, crate::Error> { + use crate::encdec::id::ID; + use aes_gcm::Aes256Gcm; + use aes_gcm::KeyInit as _; + + let salt: [u8; 32] = random(); + + #[cfg(feature = "fips")] + let id = ID::Pbkdf2AESGCM; + + #[cfg(not(feature = "fips"))] + let id = if native_aes() { + ID::Argon2idAESGCM + } else { + ID::Argon2idChaCHa20Poly1305 + }; + + let key = id.get_key(password, &salt)?; + + #[cfg(feature = "fips")] + { + encrypt(Aes256Gcm::new_from_slice(&key)?, &salt, id, data) + } + + #[cfg(not(feature = "fips"))] + { + if native_aes() { + encrypt(Aes256Gcm::new_from_slice(&key)?, &salt, id, data) + } else { + encrypt(ChaCha20Poly1305::new_from_slice(&key)?, &salt, id, data) + } + } +} + +#[cfg(any(test, feature = "crypto"))] +fn encrypt(stream: T, salt: &[u8], id: ID, data: &[u8]) -> Result, crate::Error> { + let nonce = T::generate_nonce(rand::thread_rng()); + let encryptor = stream.encrypt(&nonce, data).map_err(Error::ErrEncryptFailed)?; + + let mut ciphertext = Vec::with_capacity(salt.len() + 1 + nonce.len() + encryptor.len()); + ciphertext.extend_from_slice(salt); + ciphertext.push(id as u8); + ciphertext.extend_from_slice(nonce.as_slice()); + ciphertext.extend_from_slice(&encryptor); + + Ok(ciphertext) +} + +#[cfg(all(not(test), not(feature = "crypto")))] +pub fn encrypt_data(_password: &[u8], data: &[u8]) -> Result, crate::Error> { + Ok(data.to_vec()) +} diff --git a/crypto/src/encdec/id.rs b/crypto/src/encdec/id.rs new file mode 100644 index 00000000..5214c478 --- /dev/null +++ b/crypto/src/encdec/id.rs @@ -0,0 +1,39 @@ +use argon2::{Algorithm, Argon2, Params, Version}; +use pbkdf2::pbkdf2_hmac; +use sha2::Sha256; + +#[repr(u8)] +pub(crate) enum ID { + Argon2idAESGCM = 0x00, + Argon2idChaCHa20Poly1305 = 0x01, + Pbkdf2AESGCM = 0x02, +} + +impl TryFrom for ID { + type Error = crate::Error; + fn try_from(value: u8) -> Result { + match value { + 0x00 => Ok(Self::Argon2idAESGCM), + 0x01 => Ok(Self::Argon2idChaCHa20Poly1305), + 0x02 => Ok(Self::Pbkdf2AESGCM), + _ => Err(crate::Error::ErrInvalidAlgID(value)), + } + } +} + +impl ID { + pub(crate) fn get_key(&self, password: &[u8], salt: &[u8]) -> Result<[u8; 32], crate::Error> { + let mut key = [0u8; 32]; + match self { + ID::Pbkdf2AESGCM => pbkdf2_hmac::(password, salt, 8192, &mut key), + _ => { + let params = Params::new(64 * 1024, 1, 4, Some(32))?; + let argon_2id = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); + let mut key = vec![0u8; 32]; + argon_2id.hash_password_into(password, salt, &mut key)?; + } + } + + Ok(key) + } +} diff --git a/crypto/src/encdec/tests.rs b/crypto/src/encdec/tests.rs new file mode 100644 index 00000000..1258ace1 --- /dev/null +++ b/crypto/src/encdec/tests.rs @@ -0,0 +1,14 @@ +use crate::{decrypt_data, encrypt_data}; + +const PASSWORD: &[u8] = "test_password".as_bytes(); + +#[test_case::test_case("hello world".as_bytes())] +#[test_case::test_case(&[])] +#[test_case::test_case(&[1, 2, 3])] +#[test_case::test_case(&[3, 2, 1])] +fn test(input: &[u8]) -> Result<(), crate::Error> { + let encrypted = encrypt_data(PASSWORD, input)?; + let decrypted = decrypt_data(PASSWORD, &encrypted)?; + assert_eq!(input, decrypted, "input is not equal output"); + Ok(()) +} diff --git a/crypto/src/error.rs b/crypto/src/error.rs new file mode 100644 index 00000000..6ab05e67 --- /dev/null +++ b/crypto/src/error.rs @@ -0,0 +1,28 @@ +use sha2::digest::InvalidLength; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("unexpected header")] + ErrUnexpectedHeader, + + #[error("invalid encryption algorithm ID: {0}")] + ErrInvalidAlgID(u8), + + #[error("{0}")] + ErrInvalidLength(#[from] InvalidLength), + + #[cfg(any(test, feature = "crypto"))] + #[error("encrypt failed")] + ErrEncryptFailed(aes_gcm::aead::Error), + + #[cfg(any(test, feature = "crypto"))] + #[error("decrypt failed")] + ErrDecryptFailed(aes_gcm::aead::Error), + + #[cfg(any(test, feature = "crypto"))] + #[error("argon2 err: {0}")] + ErrArgon2(#[from] argon2::Error), + + #[error("jwt err: {0}")] + ErrJwt(#[from] jsonwebtoken::errors::Error), +} diff --git a/crypto/src/jwt.rs b/crypto/src/jwt.rs new file mode 100644 index 00000000..e8d4e0f9 --- /dev/null +++ b/crypto/src/jwt.rs @@ -0,0 +1,6 @@ +pub mod decode; +pub mod encode; +pub use serde_json::Value as Claims; + +#[cfg(test)] +mod tests; diff --git a/crypto/src/jwt/decode.rs b/crypto/src/jwt/decode.rs new file mode 100644 index 00000000..ad76fa43 --- /dev/null +++ b/crypto/src/jwt/decode.rs @@ -0,0 +1,12 @@ +use jsonwebtoken::{Algorithm, DecodingKey, TokenData, Validation}; + +use crate::jwt::Claims; +use crate::Error; + +pub fn decode(token: &str, token_secret: &[u8]) -> Result, Error> { + Ok(jsonwebtoken::decode( + token, + &DecodingKey::from_secret(token_secret), + &Validation::new(Algorithm::HS512), + )?) +} diff --git a/crypto/src/jwt/encode.rs b/crypto/src/jwt/encode.rs new file mode 100644 index 00000000..04e3a1c7 --- /dev/null +++ b/crypto/src/jwt/encode.rs @@ -0,0 +1,12 @@ +use jsonwebtoken::{Algorithm, EncodingKey, Header}; + +use crate::jwt::Claims; +use crate::Error; + +pub fn encode(token_secret: &[u8], claims: &Claims) -> Result { + Ok(jsonwebtoken::encode( + &Header::new(Algorithm::HS512), + claims, + &EncodingKey::from_secret(token_secret), + )?) +} diff --git a/crypto/src/jwt/tests.rs b/crypto/src/jwt/tests.rs new file mode 100644 index 00000000..abfe0020 --- /dev/null +++ b/crypto/src/jwt/tests.rs @@ -0,0 +1,16 @@ +use time::OffsetDateTime; + +use super::{decode::decode, encode::encode}; + +#[test] +fn test() { + let claims = serde_json::json!({ + "exp": OffsetDateTime::now_utc().unix_timestamp() + 1000, + "aaa": 1, + "bbb": "bbb" + }); + + let jwt_token = encode(b"aaaa", &claims).unwrap(); + let new_claims = decode(&jwt_token, b"aaaa").unwrap(); + assert_eq!(new_claims.claims, claims); +} diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs new file mode 100644 index 00000000..4deec394 --- /dev/null +++ b/crypto/src/lib.rs @@ -0,0 +1,9 @@ +mod encdec; +mod error; +mod jwt; + +pub use encdec::decrypt::decrypt_data; +pub use encdec::encrypt::encrypt_data; +pub use error::Error; +pub use jwt::decode::decode as jwt_decode; +pub use jwt::encode::encode as jwt_encode; diff --git a/ecstore/src/config/error.rs b/ecstore/src/config/error.rs index 0ec83565..27518aef 100644 --- a/ecstore/src/config/error.rs +++ b/ecstore/src/config/error.rs @@ -1,4 +1,4 @@ -use crate::error::Error; +use crate::{disk, error::Error}; #[derive(Debug, thiserror::Error)] pub enum ConfigError { @@ -15,10 +15,11 @@ impl ConfigError { matches!(self, Self::NotFound) } } - pub fn is_not_found(err: &Error) -> bool { if let Some(e) = err.downcast_ref::() { ConfigError::is_not_found(e) + } else if let Some(e) = err.downcast_ref::() { + matches!(e, disk::error::DiskError::FileNotFound) } else { false } diff --git a/ecstore/src/lib.rs b/ecstore/src/lib.rs index 9c5bbaa3..20e09a14 100644 --- a/ecstore/src/lib.rs +++ b/ecstore/src/lib.rs @@ -30,4 +30,6 @@ pub mod xhttp; pub use global::new_object_layer_fn; pub use global::set_global_endpoints; pub use global::update_erasure_type; + pub use global::GLOBAL_Endpoints; +pub use store_api::StorageAPI; diff --git a/ecstore/src/store.rs b/ecstore/src/store.rs index 39fc4c68..2dcc5dc6 100644 --- a/ecstore/src/store.rs +++ b/ecstore/src/store.rs @@ -264,7 +264,7 @@ impl ECStore { self.pools.len() == 1 } - async fn list_path(&self, opts: &ListPathOptions, delimiter: &str) -> Result { + pub async fn list_path(&self, opts: &ListPathOptions, delimiter: &str) -> Result { // if opts.prefix.ends_with(SLASH_SEPARATOR) { // return Err(Error::msg("eof")); // } diff --git a/ecstore/src/store_api.rs b/ecstore/src/store_api.rs index fc414684..2f9d5793 100644 --- a/ecstore/src/store_api.rs +++ b/ecstore/src/store_api.rs @@ -9,7 +9,7 @@ use crate::{ use futures::StreamExt; use http::HeaderMap; use rmp_serde::Serializer; -use s3s::dto::StreamingBlob; +use s3s::{dto::StreamingBlob, Body}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; @@ -412,6 +412,14 @@ impl PutObjReader { pub fn new(stream: StreamingBlob, content_length: usize) -> Self { PutObjReader { stream, content_length } } + + pub fn from_vec(data: Vec) -> Self { + let content_length = data.len(); + PutObjReader { + stream: Body::from(data).into(), + content_length, + } + } } pub struct GetObjectReader { diff --git a/iam/Cargo.toml b/iam/Cargo.toml new file mode 100644 index 00000000..debdcd25 --- /dev/null +++ b/iam/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "iam" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[dependencies] +tokio.workspace = true +log.workspace = true +time = { workspace = true, features = ["serde-human-readable"] } +serde = { workspace = true, features = ["derive", "rc"] } +ecstore = { path = "../ecstore" } +serde_json.workspace = true +async-trait.workspace = true +thiserror.workspace = true +strum = { version = "0.26.3", features = ["derive"] } +arc-swap = "1.7.1" +crypto = { path = "../crypto" } +ipnetwork = "0.20.0" +itertools = "0.13.0" +futures.workspace = true +rand.workspace = true +base64-simd = "0.8.0" + +[dev-dependencies] +test-case.workspace = true diff --git a/iam/src/arn.rs b/iam/src/arn.rs new file mode 100644 index 00000000..f4d1b3ac --- /dev/null +++ b/iam/src/arn.rs @@ -0,0 +1,18 @@ +use std::str::FromStr; + +#[derive(PartialEq, Eq, Hash)] +pub struct ARN { + partition: String, + service: String, + region: String, + resource_type: String, + resource_id: String, +} + +impl FromStr for ARN { + type Err = String; + + fn from_str(s: &str) -> Result { + todo!() + } +} diff --git a/iam/src/auth.rs b/iam/src/auth.rs new file mode 100644 index 00000000..679dd2bb --- /dev/null +++ b/iam/src/auth.rs @@ -0,0 +1,23 @@ +mod credentials; + +pub use credentials::Credentials; +pub use credentials::CredentialsBuilder; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +#[derive(Serialize, Deserialize, Clone)] +pub struct UserIdentity { + pub version: i64, + pub credentials: Credentials, + pub update_at: OffsetDateTime, +} + +impl From for UserIdentity { + fn from(value: Credentials) -> Self { + UserIdentity { + version: 1, + credentials: value, + update_at: OffsetDateTime::now_utc(), + } + } +} diff --git a/iam/src/auth/credentials.rs b/iam/src/auth/credentials.rs new file mode 100644 index 00000000..3b72cb63 --- /dev/null +++ b/iam/src/auth/credentials.rs @@ -0,0 +1,372 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::cell::LazyCell; +use std::collections::HashMap; +use std::env::var; +use time::format_description::BorrowedFormatItem; +use time::{Date, OffsetDateTime}; + +use crate::policy::{Policy, Validator}; +use crate::service_type::ServiceType; +use crate::{utils, Error}; + +#[cfg_attr(test, derive(PartialEq, Eq, Debug))] +struct CredentialHeader { + access_key: String, + scop: CredentialHeaderScope, +} + +#[cfg_attr(test, derive(PartialEq, Eq, Debug))] +struct CredentialHeaderScope { + date: Date, + region: String, + service: ServiceType, + request: String, +} + +impl TryFrom<&str> for CredentialHeader { + type Error = Error; + fn try_from(value: &str) -> Result { + let mut elem = value.trim().splitn(2, '='); + let (Some(h), Some(cred_elems)) = (elem.next(), elem.next()) else { + return Err(Error::ErrCredMalformed); + }; + + if h != "Credential" { + return Err(Error::ErrCredMalformed); + } + + let mut cred_elems = cred_elems.trim().rsplitn(5, '/'); + + let Some(request) = cred_elems.next() else { + return Err(Error::ErrCredMalformed); + }; + + let Some(service) = cred_elems.next() else { + return Err(Error::ErrCredMalformed); + }; + + let Some(region) = cred_elems.next() else { + return Err(Error::ErrCredMalformed); + }; + + let Some(date) = cred_elems.next() else { + return Err(Error::ErrCredMalformed); + }; + + let Some(ak) = cred_elems.next() else { + return Err(Error::ErrCredMalformed); + }; + + if ak.len() < 3 { + return Err(Error::ErrCredMalformed); + } + + if request != "aws4_request" { + return Err(Error::ErrCredMalformed); + } + + Ok(CredentialHeader { + access_key: ak.to_owned(), + scop: CredentialHeaderScope { + date: { + const FORMATTER: LazyCell>> = + LazyCell::new(|| time::format_description::parse("[year][month][day]").unwrap()); + + Date::parse(date, &FORMATTER).map_err(|_| Error::ErrCredMalformed)? + }, + region: region.to_owned(), + service: service.try_into()?, + request: request.to_owned(), + }, + }) + } +} + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct Credentials { + pub access_key: String, + pub secret_key: String, + pub session_token: String, + pub expiration: Option, + pub status: String, + pub parent_user: String, + pub groups: Option>, + pub claims: Option>>, + pub name: Option, + pub description: Option, +} + +impl Credentials { + pub fn new(elem: &str) -> crate::Result { + let header: CredentialHeader = elem.try_into()?; + Self::check_key_value(header) + } + + pub fn check_key_value(header: CredentialHeader) -> crate::Result { + todo!() + } + + pub fn is_expired(&self) -> bool { + self.expiration + .as_ref() + .map(|e| time::OffsetDateTime::now_utc() > *e) + .unwrap_or(false) + } + + pub fn is_temp(&self) -> bool { + !self.session_token.is_empty() && !self.is_expired() + } + + pub fn is_service_account(&self) -> bool { + const IAM_POLICY_CLAIM_NAME_SA: &str = "sa-policy"; + self.claims + .as_ref() + .map(|x| { + x.get(IAM_POLICY_CLAIM_NAME_SA) + .map_or(false, |_| !self.parent_user.is_empty()) + }) + .unwrap_or_default() + } + + pub fn is_valid(&self) -> bool { + if self.status == "off" { + return false; + } + + self.access_key.len() >= 3 && self.secret_key.len() >= 8 && !self.is_expired() + } + + pub fn is_owner(&self) -> bool { + false + } +} + +#[derive(Default)] +pub struct CredentialsBuilder { + session_policy: Option, + access_key: String, + secret_key: String, + name: Option, + description: Option, + expiration: Option, + allow_site_replicator_account: bool, + claims: Option, + parent_user: String, + groups: Option>, +} + +impl CredentialsBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn session_policy(mut self, policy: Option) -> Self { + self.session_policy = policy; + self + } + + pub fn access_key(mut self, access_key: String) -> Self { + self.access_key = access_key; + self + } + + pub fn secret_key(mut self, secret_key: String) -> Self { + self.secret_key = secret_key; + self + } + + pub fn name(mut self, name: String) -> Self { + self.name = Some(name); + self + } + + pub fn description(mut self, description: String) -> Self { + self.description = Some(description); + self + } + + pub fn expiration(mut self, expiration: Option) -> Self { + self.expiration = expiration; + self + } + + pub fn allow_site_replicator_account(mut self, allow_site_replicator_account: bool) -> Self { + self.allow_site_replicator_account = allow_site_replicator_account; + self + } + + pub fn claims(mut self, claims: serde_json::Value) -> Self { + self.claims = Some(claims); + self + } + + pub fn parent_user(mut self, parent_user: String) -> Self { + self.parent_user = parent_user; + self + } + + pub fn groups(mut self, groups: Vec) -> Self { + self.groups = Some(groups); + self + } + + pub fn try_build(self) -> crate::Result { + self.try_into() + } +} + +impl TryFrom for Credentials { + type Error = crate::Error; + fn try_from(mut value: CredentialsBuilder) -> Result { + if value.parent_user.is_empty() { + return Err(Error::InvalidArgument); + } + + if (value.access_key.is_empty() && !value.secret_key.is_empty()) + || (!value.access_key.is_empty() && value.secret_key.is_empty()) + { + return Err(Error::StringError("Either ak or sk is empty".into())); + } + + if value.parent_user == value.access_key.as_str() { + return Err(Error::InvalidArgument); + } + + if value.access_key == "site-replicator-0" && !value.allow_site_replicator_account { + return Err(Error::InvalidArgument); + } + + let mut claim = serde_json::json!({ + "parent": value.parent_user + }); + + if let Some(p) = value.session_policy { + p.is_valid()?; + let policy_buf = serde_json::to_vec(&p).map_err(|_| Error::InvalidArgument)?; + if policy_buf.len() > 4096 { + return Err(crate::Error::StringError("session policy is too large".into())); + } + claim["sessionPolicy"] = serde_json::json!(base64_simd::STANDARD.encode_to_string(&policy_buf)); + claim["sa-policy"] = serde_json::json!("embedded-policy"); + } else { + claim["sa-policy"] = serde_json::json!("inherited-policy"); + } + + if let Some(Value::Object(obj)) = value.claims { + for (key, value) in obj { + if claim.get(&key).is_some() { + continue; + } + claim[key] = value; + } + } + + if value.access_key.is_empty() { + value.access_key = utils::gen_access_key(20)?; + } + + if value.secret_key.is_empty() { + value.access_key = utils::gen_secret_key(40)?; + } + + claim["accessKey"] = json!(&value.access_key); + + let mut cred = Credentials { + status: "on".into(), + parent_user: value.parent_user, + groups: value.groups, + name: value.name, + description: value.description, + ..Default::default() + }; + + if !value.secret_key.is_empty() { + let session_token = crypto::jwt_encode(value.access_key.as_bytes(), &claim) + .map_err(|_| crate::Error::StringError("session policy is too large".into()))?; + cred.session_token = session_token; + // cred.expiration = Some( + // OffsetDateTime::from_unix_timestamp( + // claim + // .get("exp") + // .and_then(|x| x.as_i64()) + // .ok_or(crate::Error::StringError("invalid exp".into()))?, + // ) + // .map_err(|_| crate::Error::StringError("invalie timestamp".into()))?, + // ); + } else { + // cred.expiration = + // Some(OffsetDateTime::from_unix_timestamp(0).map_err(|_| crate::Error::StringError("invalie timestamp".into()))?); + } + + cred.expiration = value.expiration; + cred.access_key = value.access_key; + cred.secret_key = value.secret_key; + + Ok(cred) + } +} + +#[cfg(test)] +#[allow(non_snake_case)] +mod tests { + use test_case::test_case; + use time::Date; + + use super::CredentialHeader; + use super::CredentialHeaderScope; + use crate::service_type::ServiceType; + + #[test_case( + "Credential=aaaaaaaaaaaaaaaaaaaa/20241127/us-east-1/s3/aws4_request" => + CredentialHeader{ + access_key: "aaaaaaaaaaaaaaaaaaaa".into(), + scop: CredentialHeaderScope { + date: Date::from_calendar_date(2024, time::Month::November, 27).unwrap(), + region: "us-east-1".to_owned(), + service: ServiceType::S3, + request: "aws4_request".into(), + } + }; + "1")] + #[test_case( + "Credential=aaaaaaaaaaa/aaaaaaaaa/20241127/us-east-1/s3/aws4_request" => + CredentialHeader{ + access_key: "aaaaaaaaaaa/aaaaaaaaa".into(), + scop: CredentialHeaderScope { + date: Date::from_calendar_date(2024, time::Month::November, 27).unwrap(), + region: "us-east-1".to_owned(), + service: ServiceType::S3, + request: "aws4_request".into(), + } + }; + "2")] + #[test_case( + "Credential=aaaaaaaaaaa/aaaaaaaaa/20241127/us-east-1/sts/aws4_request" => + CredentialHeader{ + access_key: "aaaaaaaaaaa/aaaaaaaaa".into(), + scop: CredentialHeaderScope { + date: Date::from_calendar_date(2024, time::Month::November, 27).unwrap(), + region: "us-east-1".to_owned(), + service: ServiceType::STS, + request: "aws4_request".into(), + } + }; + "3")] + fn test_CredentialHeader_from_str_successful(input: &str) -> CredentialHeader { + CredentialHeader::try_from(input).unwrap() + } + + #[test_case("Credential")] + #[test_case("Cred=")] + #[test_case("Credential=abc")] + #[test_case("Credential=a/20241127/us-east-1/s3/aws4_request")] + #[test_case("Credential=aa/20241127/us-east-1/s3/aws4_request")] + #[test_case("Credential=aaaa/20241127/us-east-1/asa/aws4_request")] + #[test_case("Credential=aaaa/20241127/us-east-1/sts/aws4a_request")] + fn test_CredentialHeader_from_str_failed(input: &str) { + if CredentialHeader::try_from(input).is_ok() { + unreachable!() + } + } +} diff --git a/iam/src/cache.rs b/iam/src/cache.rs new file mode 100644 index 00000000..e615632f --- /dev/null +++ b/iam/src/cache.rs @@ -0,0 +1,320 @@ +use std::{ + collections::{HashMap, HashSet}, + ops::{Deref, DerefMut}, + ptr, + sync::Arc, +}; + +use arc_swap::{ArcSwap, AsRaw, Guard}; +use log::warn; +use time::OffsetDateTime; + +use crate::{ + auth::UserIdentity, + policy::{Args, MappedPolicy, Policy, PolicyDoc}, + Error, +}; + +pub struct Cache { + pub policy_docs: ArcSwap>, + pub users: ArcSwap>, + pub user_policies: ArcSwap>, + pub sts_accounts: ArcSwap>, + pub sts_policies: ArcSwap>, + pub groups: ArcSwap>, + pub user_group_memeberships: ArcSwap>>, + pub group_policies: ArcSwap>, +} + +impl Default for Cache { + fn default() -> Self { + Self { + policy_docs: ArcSwap::new(Arc::new(CacheEntity::default())), + users: ArcSwap::new(Arc::new(CacheEntity::default())), + user_policies: ArcSwap::new(Arc::new(CacheEntity::default())), + sts_accounts: ArcSwap::new(Arc::new(CacheEntity::default())), + sts_policies: ArcSwap::new(Arc::new(CacheEntity::default())), + groups: ArcSwap::new(Arc::new(CacheEntity::default())), + user_group_memeberships: ArcSwap::new(Arc::new(CacheEntity::default())), + group_policies: ArcSwap::new(Arc::new(CacheEntity::default())), + } + } +} + +impl Cache { + pub fn ptr_eq(a: A, b: B) -> bool + where + A: AsRaw, + B: AsRaw, + { + let a = a.as_raw(); + let b = b.as_raw(); + ptr::eq(a, b) + } + + fn exec(target: &ArcSwap>, t: OffsetDateTime, mut op: impl FnMut(&mut CacheEntity)) { + let mut cur = target.load(); + loop { + // 当前的更新时间晚于执行时间,说明后台任务加载完毕,不需要执行当前操作。 + if cur.load_time >= t { + return; + } + + let mut new = CacheEntity::clone(&cur); + op(&mut new); + + // 使用cas原子替换内容 + let prev = target.compare_and_swap(&*cur, Arc::new(new)); + let swapped = Self::ptr_eq(&*cur, &*prev); + if swapped { + return; + } else { + cur = prev; + } + } + } + + pub fn add_or_update(target: &ArcSwap>, key: &str, value: &T, t: OffsetDateTime) { + Self::exec(target, t, |map: &mut CacheEntity| { + map.insert(key.to_string(), value.clone()); + }) + } + + pub fn delete(target: &ArcSwap>, key: &str, t: OffsetDateTime) { + Self::exec(target, t, |map: &mut CacheEntity| { + map.remove(key); + }) + } +} + +impl CacheInner { + #[inline] + fn get_user<'a>(&self, user_name: &'a str) -> Option<&UserIdentity> { + self.users.get(user_name).or_else(|| self.sts_accounts.get(user_name)) + } + + fn get_policy(&self, name: &str, groups: &[String]) -> crate::Result> { + todo!() + } + + /// 如果是临时用户,返回Ok(Some(partent_name))) + /// 如果不是临时用户,返回Ok(None) + fn is_temp_user<'a>(&self, user_name: &'a str) -> crate::Result> { + let user = self + .get_user(user_name) + .ok_or_else(|| Error::NoSuchUser(user_name.to_owned()))?; + + if user.credentials.is_temp() { + Ok(Some(&user.credentials.parent_user)) + } else { + Ok(None) + } + } + + /// 如果是临时用户,返回Ok(Some(partent_name))) + /// 如果不是临时用户,返回Ok(None) + fn is_service_account<'a>(&self, user_name: &'a str) -> crate::Result> { + let user = self + .get_user(user_name) + .ok_or_else(|| Error::NoSuchUser(user_name.to_owned()))?; + + if user.credentials.is_service_account() { + Ok(Some(&user.credentials.parent_user)) + } else { + Ok(None) + } + } + + // todo + pub fn is_allowed_sts(&self, args: &Args, parent: &str) -> bool { + warn!("unimplement is_allowed_sts"); + false + } + + // todo + pub fn is_allowed_service_account(&self, args: &Args, parent: &str) -> bool { + warn!("unimplement is_allowed_sts"); + false + } + + pub fn is_allowed(&self, args: Args) -> bool { + todo!() + } + + pub fn policy_db_get(&self, name: &str, groups: &[String]) -> Vec { + todo!() + } +} + +#[derive(Clone)] +pub struct CacheEntity { + map: HashMap, + /// 重新加载的时间 + load_time: OffsetDateTime, +} + +impl Deref for CacheEntity { + type Target = HashMap; + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl DerefMut for CacheEntity { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} + +impl CacheEntity { + pub fn new(map: HashMap) -> Self { + Self { + map, + load_time: OffsetDateTime::UNIX_EPOCH, + } + } +} + +impl Default for CacheEntity { + fn default() -> Self { + Self { + map: HashMap::new(), + load_time: OffsetDateTime::UNIX_EPOCH, + } + } +} + +impl CacheEntity { + pub fn update_load_time(mut self) -> Self { + self.load_time = OffsetDateTime::now_utc(); + self + } +} + +pub type G = Guard>>; + +pub struct CacheInner { + pub policy_docs: G, + pub users: G, + pub user_policies: G, + pub sts_accounts: G, + pub sts_policies: G, + pub groups: G, + pub user_group_memeberships: G>, + pub group_policies: G, +} + +impl From<&Cache> for CacheInner { + fn from(value: &Cache) -> Self { + Self { + policy_docs: value.policy_docs.load(), + users: value.users.load(), + user_policies: value.user_policies.load(), + sts_accounts: value.sts_accounts.load(), + sts_policies: value.sts_policies.load(), + groups: value.groups.load(), + user_group_memeberships: value.user_group_memeberships.load(), + group_policies: value.group_policies.load(), + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use arc_swap::ArcSwap; + use futures::future::join_all; + use time::OffsetDateTime; + + use super::CacheEntity; + use crate::cache::Cache; + + #[tokio::test] + async fn test_cache_entity_add() { + let cache = ArcSwap::new(Arc::new(CacheEntity::::default())); + + let mut f = vec![]; + + for (index, key) in (0..100).map(|x| x.to_string()).enumerate() { + let c = &cache; + f.push(async move { + Cache::add_or_update(&c, &key, &index, OffsetDateTime::now_utc()); + }); + } + join_all(f).await; + + let cache = cache.load(); + for (index, key) in (0..100).map(|x| x.to_string()).enumerate() { + assert_eq!(cache.get(&key), Some(&index)); + } + } + + #[tokio::test] + async fn test_cache_entity_update() { + let cache = ArcSwap::new(Arc::new(CacheEntity::::default())); + + let mut f = vec![]; + + for (index, key) in (0..100).map(|x| x.to_string()).enumerate() { + let c = &cache; + f.push(async move { + Cache::add_or_update(&c, &key, &index, OffsetDateTime::now_utc()); + }); + } + join_all(f).await; + + let cache_load = cache.load(); + for (index, key) in (0..100).map(|x| x.to_string()).enumerate() { + assert_eq!(cache_load.get(&key), Some(&index)); + } + + let mut f = vec![]; + + for (index, key) in (0..100).map(|x| x.to_string()).enumerate() { + let c = &cache; + f.push(async move { + Cache::add_or_update(&c, &key, &(index * 1000), OffsetDateTime::now_utc()); + }); + } + join_all(f).await; + + let cache_load = cache.load(); + for (index, key) in (0..100).map(|x| x.to_string()).enumerate() { + assert_eq!(cache_load.get(&key), Some(&(index * 1000))); + } + } + + #[tokio::test] + async fn test_cache_entity_delete() { + let cache = ArcSwap::new(Arc::new(CacheEntity::::default())); + + let mut f = vec![]; + + for (index, key) in (0..100).map(|x| x.to_string()).enumerate() { + let c = &cache; + f.push(async move { + Cache::add_or_update(&c, &key, &index, OffsetDateTime::now_utc()); + }); + } + join_all(f).await; + + let cache_load = cache.load(); + for (index, key) in (0..100).map(|x| x.to_string()).enumerate() { + assert_eq!(cache_load.get(&key), Some(&index)); + } + + let mut f = vec![]; + + for key in (0..100).map(|x| x.to_string()) { + let c = &cache; + f.push(async move { + Cache::delete(&c, &key, OffsetDateTime::now_utc()); + }); + } + join_all(f).await; + + let cache_load = cache.load(); + assert!(cache_load.is_empty()); + } +} diff --git a/iam/src/error.rs b/iam/src/error.rs new file mode 100644 index 00000000..c6b8a791 --- /dev/null +++ b/iam/src/error.rs @@ -0,0 +1,35 @@ +use core::error; + +use crate::policy; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + PolicyError(#[from] policy::Error), + + #[error("ecsotre error: {0}")] + EcstoreError(ecstore::error::Error), + + #[error("{0}")] + StringError(String), + + #[error("crypto: {0}")] + CryptoError(#[from] crypto::Error), + + #[error("user '{0}' does not exist")] + NoSuchUser(String), + + #[error("invalid arguments specified")] + InvalidArgument, + + #[error("not initialized")] + IamSysNotInitialized, + + #[error("invalid service type: {0}")] + InvalidServiceType(String), + + #[error("malformed credential")] + ErrCredMalformed, +} + +pub type Result = std::result::Result; diff --git a/iam/src/format.rs b/iam/src/format.rs new file mode 100644 index 00000000..619db78e --- /dev/null +++ b/iam/src/format.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct Format { + pub version: i32, +} + +impl Format { + pub const PATH: &str = "config/iam/config/format.json"; + pub const DEFAULT_VERSION: i32 = 1; + + pub fn new() -> Self { + Self { + version: Self::DEFAULT_VERSION, + } + } +} diff --git a/iam/src/lib.rs b/iam/src/lib.rs new file mode 100644 index 00000000..5d41c790 --- /dev/null +++ b/iam/src/lib.rs @@ -0,0 +1,58 @@ +use auth::{Credentials, UserIdentity}; +use ecstore::store::ECStore; +use log::debug; +use manager::IamCache; +use policy::{Args, Policy}; +use std::sync::{Arc, OnceLock}; +use store::object::ObjectStore; +use time::OffsetDateTime; + +mod cache; +mod format; +mod handler; + +pub mod arn; +pub mod auth; +pub mod error; +pub mod manager; +pub mod policy; +pub mod service_type; +pub mod store; +pub mod utils; + +pub use error::{Error, Result}; + +static IAM_SYS: OnceLock>> = OnceLock::new(); + +pub async fn init_iam_sys(ecstore: Arc) -> crate::Result<()> { + debug!("init iam system"); + let s = IamCache::new(ObjectStore::new(ecstore)).await; + IAM_SYS.get_or_init(move || s); + Ok(()) +} + +#[inline] +pub fn get() -> crate::Result>> { + IAM_SYS.get().map(|x| Arc::clone(x)).ok_or(Error::IamSysNotInitialized) +} + +pub async fn is_allowed<'a>(args: Args<'a>) -> crate::Result { + Ok(get()?.is_allowed(args).await) +} + +pub async fn get_service_account(ak: &str) -> crate::Result<(Credentials, Option)> { + let (mut sa, policy) = get()?.get_service_account(ak).await?; + + sa.credentials.secret_key.clear(); + sa.credentials.access_key.clear(); + + Ok((sa.credentials, policy)) +} + +pub async fn add_service_account(cred: Credentials) -> crate::Result { + get()?.add_service_account(cred).await +} + +pub async fn check_key(ak: &str) -> crate::Result> { + get()?.check_key(ak).await +} diff --git a/iam/src/manager.rs b/iam/src/manager.rs new file mode 100644 index 00000000..84daf875 --- /dev/null +++ b/iam/src/manager.rs @@ -0,0 +1,223 @@ +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicBool, AtomicI64, Ordering}, + Arc, + }, + time::Duration, +}; + +use log::debug; +use time::OffsetDateTime; +use tokio::{ + select, + sync::{ + mpsc, + mpsc::{Receiver, Sender}, + }, +}; + +use crate::{ + arn::ARN, + auth::{Credentials, UserIdentity}, + cache::Cache, + format::Format, + handler::Handler, + policy::{Args, Policy, UserType}, + store::Store, + Error, +}; + +pub struct IamCache { + pub cache: Cache, + pub api: T, + pub loading: Arc, + pub roles: HashMap>, + pub send_chan: Sender, + pub last_timestamp: AtomicI64, +} + +impl IamCache +where + T: Store, +{ + pub(crate) async fn new(api: T) -> Arc { + let (sender, reciver) = mpsc::channel::(100); + + let sys = Arc::new(Self { + api, + cache: Cache::default(), + loading: Arc::new(AtomicBool::new(false)), + send_chan: sender, + roles: HashMap::new(), + last_timestamp: AtomicI64::new(0), + }); + + sys.clone().init(reciver).await.unwrap(); + sys + } + + async fn init(self: Arc, reciver: Receiver) -> crate::Result<()> { + self.clone().save_iam_formatter().await?; + self.clone().load().await?; + + // 后台线程开启定时更新或者接收到信号更新 + tokio::spawn({ + let s = Arc::clone(&self); + async move { + let ticker = tokio::time::interval(Duration::from_secs(120)); + tokio::pin!(ticker, reciver); + loop { + select! { + _ = ticker.tick() => { + s.clone().load().await.unwrap(); + }, + i = reciver.recv() => { + match i { + Some(t) => { + let last = s.last_timestamp.load(Ordering::Relaxed); + if last <= t { + s.clone().load().await.unwrap(); + ticker.reset(); + } + }, + None => return, + } + } + } + } + } + }); + + Ok(()) + } + + async fn notify(&self) { + self.send_chan.send(OffsetDateTime::now_utc().unix_timestamp()).await.unwrap(); + } + + async fn load(self: Arc) -> crate::Result<()> { + debug!("load iam to cache"); + self.api.load_all(&self.cache).await?; + self.last_timestamp + .store(OffsetDateTime::now_utc().unix_timestamp(), Ordering::Relaxed); + Ok(()) + } + + pub async fn list_all_iam_config_items(&self) -> crate::Result>> { + todo!() + } + + // todo, 判断是否存在,是否可以重试 + async fn save_iam_formatter(self: Arc) -> crate::Result<()> { + match self.api.load_iam_config::(Format::PATH).await { + Ok((format, _)) if format.version >= 1 => return Ok(()), + Err(Error::EcstoreError(e)) if !ecstore::disk::error::is_err_file_not_found(&e) => { + return Err(Error::EcstoreError(e)) + } + _ => {} + } + + self.api.save_iam_config(Format::new(), Format::PATH).await?; + Ok(()) + } + + pub async fn list_service_accounts(&self, access_key: &str) -> crate::Result> { + let users = self.cache.users.load(); + Ok(users + .values() + .filter_map(|x| { + if !access_key.is_empty() && x.credentials.parent_user.as_str() == access_key { + if x.credentials.is_service_account() { + let mut c = x.credentials.clone(); + c.secret_key = String::new(); + c.session_token = String::new(); + return Some(c); + } + } + + None + }) + .collect()) + } + + /// create a service account and update cache + pub async fn add_service_account(&self, cred: Credentials) -> crate::Result { + if cred.parent_user.is_empty() { + return Err(Error::InvalidArgument); + } + + if (cred.access_key.is_empty() && !cred.secret_key.is_empty()) + || (!cred.access_key.is_empty() && cred.secret_key.is_empty()) + { + return Err(Error::StringError("Either ak or sk is empty".into())); + } + + let users = self.cache.users.load(); + if let Some(x) = users.get(&cred.access_key) { + if x.credentials.parent_user.as_str() != cred.parent_user.as_str() { + return Err(crate::Error::StringError("access key is taken by another user".into())); + } + return Err(crate::Error::StringError("access key already taken".into())); + } + + if let Some(x) = users.get(&cred.parent_user) { + if x.credentials.is_service_account() { + return Err(crate::Error::StringError( + "unable to create a service account for another service account".into(), + )); + } + } + + let user_entiry = UserIdentity::from(cred); + let path = format!( + "config/iam/{}{}/identity.json", + UserType::Svc.prefix(), + user_entiry.credentials.access_key + ); + debug!("save object: {path:?}"); + self.api.save_iam_config(&user_entiry, path).await?; + + Cache::add_or_update( + &self.cache.users, + &user_entiry.credentials.access_key, + &user_entiry, + OffsetDateTime::now_utc(), + ); + + Ok(user_entiry.update_at) + } + + pub async fn is_allowed<'a>(&self, args: Args<'a>) -> bool { + let handler = Handler::new((&self.cache).into(), &self.api, &self.roles); + handler.is_allowed(args).await + } + + pub async fn get_service_account(&self, ak: &str) -> crate::Result<(UserIdentity, Option)> { + let user = self.cache.users.load(); + let Some(u) = user.get(ak) else { + return Err(Error::StringError("no service account".into())); + }; + + // if !u.credentials.is_service_account() { + // return Err(Error::StringError("it is not service account".into())); + // } + + Ok((u.clone(), None)) + } + + pub async fn check_key(&self, ak: &str) -> crate::Result> { + let user = self + .cache + .users + .load() + .get(ak) + .cloned() + .or_else(|| self.cache.sts_accounts.load().get(ak).cloned()); + + match user { + Some(u) if u.credentials.is_valid() => Ok(Some(u)), + _ => Ok(None), + } + } +} diff --git a/iam/src/policy.rs b/iam/src/policy.rs new file mode 100644 index 00000000..114c5662 --- /dev/null +++ b/iam/src/policy.rs @@ -0,0 +1,120 @@ +pub mod action; +mod doc; +mod effect; +mod function; +mod id; +mod policy; +mod resource; +mod statement; +pub(crate) mod utils; + +use action::Action; +pub use action::ActionSet; +pub use doc::PolicyDoc; +pub use effect::Effect; +pub use function::Functions; +pub use id::ID; +pub use policy::{default::DEFAULT_POLICIES, Policy}; +pub use resource::ResourceSet; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +pub use statement::Statement; +use std::collections::HashMap; +use time::OffsetDateTime; + +#[derive(Serialize, Deserialize, Clone)] +pub struct MappedPolicy { + pub version: i64, + pub policies: String, + pub update_at: OffsetDateTime, +} + +impl MappedPolicy { + pub fn new(policy: &str) -> Self { + Self { + version: 1, + policies: policy.to_owned(), + update_at: OffsetDateTime::now_utc(), + } + } +} + +pub struct GroupInfo { + version: i64, + status: String, + members: Vec, + update_at: OffsetDateTime, +} + +#[derive(thiserror::Error, Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub enum Error { + #[error("invalid Version '{0}'")] + InvalidVersion(String), + + #[error("invalid Effect '{0}'")] + InvalidEffect(String), + + #[error("both 'Action' and 'NotAction' are empty")] + NonAction, + + #[error("'Resource' is empty")] + NonResource, + + #[error("invalid key name: '{0}'")] + InvalidKeyName(String), + + #[error("invalid key: '{0}'")] + InvalidKey(String), + + #[error("invalid action: '{0}'")] + InvalidAction(String), + + #[error("invalid resource, type: '{0}', pattern: '{1}'")] + InvalidResource(String, String), +} + +/// DEFAULT_VERSION is the default version. +/// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html +pub const DEFAULT_VERSION: &str = "2012-10-17"; + +/// check the data is Validator +pub trait Validator { + fn is_valid(&self) -> Result<(), Error> { + Ok(()) + } +} + +pub enum UserType { + Svc, + Sts, + Reg, +} + +impl UserType { + pub fn prefix(&self) -> &'static str { + match self { + UserType::Svc => "service-accounts/", + UserType::Sts => "sts/", + UserType::Reg => "users/", + } + } +} + +pub struct Args<'a> { + pub account: &'a str, + pub groups: &'a [String], + pub action: Action, + pub bucket: &'a str, + pub conditions: &'a HashMap>, + pub is_owner: bool, + pub object: &'a str, + pub claims: &'a HashMap, + pub deny_only: bool, +} + +impl<'a> Args<'a> { + pub fn get_role_arn(&self) -> Option<&str> { + self.claims.get("roleArn").and_then(|x| x.as_str()) + } +} diff --git a/iam/src/policy/action.rs b/iam/src/policy/action.rs new file mode 100644 index 00000000..0ae53c3b --- /dev/null +++ b/iam/src/policy/action.rs @@ -0,0 +1,143 @@ +use std::{collections::HashSet, ops::Deref}; + +use serde::{Deserialize, Serialize}; +use strum::{EnumString, IntoStaticStr}; + +use super::{utils::wildcard, Error, Validator}; + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct ActionSet(pub HashSet); + +impl ActionSet { + pub fn is_match(&self, action: &Action) -> bool { + for act in self.0.iter() { + if act.is_match(action) { + return true; + } + + if matches!(act, Action::S3Action(S3Action::GetObjectVersionAction)) + && matches!(action, Action::S3Action(S3Action::GetObjectAction)) + { + return true; + } + } + + false + } +} + +impl Deref for ActionSet { + type Target = HashSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Validator for ActionSet { + fn is_valid(&self) -> Result<(), super::Error> { + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, IntoStaticStr)] +#[serde(try_from = "&str", untagged)] +pub enum Action { + S3Action(S3Action), + AdminAction(AdminAction), + StsAction(StsAction), + KmsAction(KmsAction), +} + +impl Action { + pub fn is_match(&self, action: &Action) -> bool { + wildcard::is_match::<&str, &str>(self.into(), action.into()) + } +} + +impl Action { + const S3_PREFIX: &str = "s3:"; + const ADMIN_PREFIX: &str = "admin:"; + const STS_PREFIX: &str = "sts:"; + const KMS_PREFIX: &str = "kms:"; +} + +impl TryFrom<&str> for Action { + type Error = Error; + fn try_from(value: &str) -> Result { + if value.starts_with(Self::S3_PREFIX) { + Ok(Self::S3Action(S3Action::try_from(value).map_err(|_| Error::InvalidAction(value.into()))?)) + } else if value.starts_with(Self::ADMIN_PREFIX) { + Ok(Self::AdminAction( + AdminAction::try_from(value).map_err(|_| Error::InvalidAction(value.into()))?, + )) + } else if value.starts_with(Self::STS_PREFIX) { + Ok(Self::StsAction( + StsAction::try_from(value).map_err(|_| Error::InvalidAction(value.into()))?, + )) + } else if value.starts_with(Self::KMS_PREFIX) { + Ok(Self::KmsAction( + KmsAction::try_from(value).map_err(|_| Error::InvalidAction(value.into()))?, + )) + } else { + Err(Error::InvalidAction(value.into())) + } + } +} + +#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, EnumString, IntoStaticStr)] +#[serde(try_from = "&str", into = "&str")] +pub enum S3Action { + #[strum(serialize = "s3:*")] + AllActions, + #[strum(serialize = "s3:GetBucketLocation")] + GetBucketLocationAction, + #[strum(serialize = "s3:GetObject")] + GetObjectAction, + #[strum(serialize = "s3:PutObject")] + PutObjectAction, + #[strum(serialize = "s3:GetObjectVersion")] + GetObjectVersionAction, +} + +#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, EnumString, IntoStaticStr)] +#[serde(try_from = "&str", into = "&str")] +pub enum AdminAction { + #[strum(serialize = "admin:*")] + AllActions, + #[strum(serialize = "admin:Profiling")] + ProfilingAdminAction, + #[strum(serialize = "admin:ServerTrace")] + TraceAdminAction, + #[strum(serialize = "admin:ConsoleLog")] + ConsoleLogAdminAction, + #[strum(serialize = "admin:ServerInfo")] + ServerInfoAdminAction, + #[strum(serialize = "admin:OBDInfo")] + HealthInfoAdminAction, + #[strum(serialize = "admin:TopLocksInfo")] + TopLocksAdminAction, + #[strum(serialize = "admin:LicenseInfo")] + LicenseInfoAdminAction, + #[strum(serialize = "admin:BandwidthMonitor")] + BandwidthMonitorAction, + #[strum(serialize = "admin:InspectData")] + InspectDataAction, + #[strum(serialize = "admin:Prometheus")] + PrometheusAdminAction, + #[strum(serialize = "admin:ListServiceAccounts")] + ListServiceAccountsAdminAction, + #[strum(serialize = "admin:CreateServiceAccount")] + CreateServiceAccountAdminAction, +} + +#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, EnumString, IntoStaticStr)] +#[serde(try_from = "&str", into = "&str")] +pub enum StsAction {} + +#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, EnumString, IntoStaticStr)] +#[serde(try_from = "&str", into = "&str")] +pub enum KmsAction { + #[strum(serialize = "kms:*")] + AllActions, +} diff --git a/iam/src/policy/doc.rs b/iam/src/policy/doc.rs new file mode 100644 index 00000000..9b1b70a9 --- /dev/null +++ b/iam/src/policy/doc.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +use super::Policy; + +#[derive(Serialize, Deserialize, Default, Clone)] +pub struct PolicyDoc { + pub version: i64, + pub policy: Policy, + pub create_date: Option, + pub update_date: Option, +} diff --git a/iam/src/policy/effect.rs b/iam/src/policy/effect.rs new file mode 100644 index 00000000..f932fe24 --- /dev/null +++ b/iam/src/policy/effect.rs @@ -0,0 +1,32 @@ +use std::default; + +use serde::{Deserialize, Serialize}; +use strum::{EnumString, IntoStaticStr}; + +use super::{Error, Validator}; + +#[derive(Serialize, Clone, Deserialize, EnumString, IntoStaticStr, Default)] +#[serde(try_from = "&str", into = "&str")] +pub enum Effect { + #[default] + #[strum(serialize = "Allow")] + Allow, + #[strum(serialize = "Deny")] + Deny, +} + +impl Effect { + pub fn is_allowed(&self, allowed: bool) -> bool { + if matches!(self, Self::Allow) { + return allowed; + } + + !allowed + } +} + +impl Validator for Effect { + fn is_valid(&self) -> Result<(), Error> { + Ok(()) + } +} diff --git a/iam/src/policy/function.rs b/iam/src/policy/function.rs new file mode 100644 index 00000000..f4b5b647 --- /dev/null +++ b/iam/src/policy/function.rs @@ -0,0 +1,175 @@ +use std::{collections::HashMap, ops::Deref}; + +use func::Func; +use key::Key; +use serde::{de, Deserialize, Serialize}; + +pub mod addr; +pub mod binary; +pub mod bool_null; +pub mod condition; +pub mod date; +pub mod func; +pub mod key; +pub mod key_name; +pub mod number; +pub mod string; + +#[derive(Clone, Default, Serialize)] +pub struct Functions(pub Vec); + +impl Functions { + pub fn evaluate(&self, values: &HashMap>) -> bool { + self.0.iter().all(|x| x.evaluate(values)) + } +} + +impl<'de> Deserialize<'de> for Functions { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct FuncVisitor; + use serde::de::Visitor; + + impl<'de> Visitor<'de> for FuncVisitor { + type Value = Functions; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("Functions") + } + + fn visit_map(self, mut map: A) -> Result + where + A: de::MapAccess<'de>, + { + use serde::de::Error; + + let inner_data = Vec::with_capacity(map.size_hint().unwrap_or(0)); + while let Some(key) = map.next_key::<&str>()? { + let mut tokens = key.split(":"); + let name = tokens.next(); + let qualifier = tokens.next(); + + // 多个: + if tokens.next().is_some() { + return Err(A::Error::custom("invalid codition")); + } + + let Some(name) = name else { return Err(A::Error::custom("invalid codition")) }; + + let f = match qualifier { + Some("ForAnyValues") => Func::ForAnyValues, + Some("ForAllValues") => Func::ForAllValues, + Some(q) => return Err(A::Error::custom(format!("invalid qualifier `{q}`"))), + None => Func::ForNormal, + }; + + // inner_data.push(f(name.try_into()?)) + } + + Ok(Functions(inner_data)) + } + } + + deserializer.deserialize_map(FuncVisitor) + } +} + +impl Deref for Functions { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct Value; + +#[cfg(test)] +mod tests { + + #[test_case::test_case( + r#"{ + "Null": { + "s3:x-amz-server-side-encryption-customer-algorithm": true + }, + "Null": { + "s3:x-amz-server-side-encryption-customer-algorithm": "true" + } + }"# => true; "1")] + #[test_case::test_case(r#"{}"# => true; "2")] + #[test_case::test_case( + r#"{ + "StringLike": { + "s3:x-amz-metadata-directive": "REPL*" + }, + "StringEquals": { + "s3:x-amz-copy-source": "mybucket/myobject" + }, + "StringNotEquals": { + "s3:x-amz-server-side-encryption": "AES256" + }, + "NotIpAddress": { + "aws:SourceIp": [ + "10.1.10.0/24", + "10.10.1.0/24" + ] + }, + "StringNotLike": { + "s3:x-amz-storage-class": "STANDARD" + }, + "Null": { + "s3:x-amz-server-side-encryption-customer-algorithm": true + }, + "IpAddress": { + "aws:SourceIp": [ + "192.168.1.0/24", + "192.168.2.0/24" + ] + } + }"# => true; "3" + )] + #[test_case::test_case( + r#"{ + "StringLike": { + "s3:x-amz-metadata-directive": "REPL*" + }, + "StringEquals": { + "s3:x-amz-copy-source": "mybucket/myobject", + "s3:prefix": [ + "", + "home/" + ], + "s3:delimiter": [ + "/" + ] + }, + "StringNotEquals": { + "s3:x-amz-server-side-encryption": "AES256" + }, + "NotIpAddress": { + "aws:SourceIp": [ + "10.1.10.0/24", + "10.10.1.0/24" + ] + }, + "StringNotLike": { + "s3:x-amz-storage-class": "STANDARD" + }, + "Null": { + "s3:x-amz-server-side-encryption-customer-algorithm": true + }, + "IpAddress": { + "aws:SourceIp": [ + "192.168.1.0/24", + "192.168.2.0/24" + ] + } + }"# => true; "4" + )] + fn test_serde(input: &str) -> bool { + true + } +} diff --git a/iam/src/policy/function/addr.rs b/iam/src/policy/function/addr.rs new file mode 100644 index 00000000..36e08318 --- /dev/null +++ b/iam/src/policy/function/addr.rs @@ -0,0 +1,146 @@ +use super::func::InnerFunc; +use ipnetwork::IpNetwork; +use serde::{de::Visitor, Deserialize, Serialize}; +use std::{borrow::Cow, collections::HashMap, net::IpAddr}; + +pub type AddrFunc = InnerFunc; + +impl AddrFunc { + pub(crate) fn evaluate(&self, values: &HashMap>) -> bool { + let rvalues = values.get(self.key.name().as_str()).map(|t| t.iter()).unwrap_or_default(); + + for r in rvalues { + let Ok(ip) = r.parse::() else { + return false; + }; + + for ip_net in self.values.0.iter() { + if ip_net.contains(ip) { + return true; + } + } + } + + false + } +} + +#[derive(Serialize, Clone)] +#[serde(transparent)] +#[cfg_attr(test, derive(PartialEq, Eq, Debug))] +pub struct AddrFuncValue(Vec); + +impl<'de> Deserialize<'de> for AddrFuncValue { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct AddrFuncValueVisitor; + impl<'d> Visitor<'d> for AddrFuncValueVisitor { + type Value = AddrFuncValue; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("cidr string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(AddrFuncValue(vec![Self::to_cidr::(v)?])) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'d>, + { + Ok(AddrFuncValue({ + let mut data = Vec::with_capacity(seq.size_hint().unwrap_or_default()); + while let Some(v) = seq.next_element::<&str>()? { + data.push(Self::to_cidr::(v)?) + } + data + })) + } + } + + impl AddrFuncValueVisitor { + fn to_cidr(v: &str) -> Result { + let mut cidr_str = Cow::from(v); + if v.find('/').is_none() { + cidr_str.to_mut().push_str("/32"); + } + + Ok(cidr_str + .parse::() + .map_err(|_| E::custom(format!("{v} can not be parsed to CIDR")))?) + } + } + + deserializer.deserialize_any(AddrFuncValueVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::{AddrFunc, AddrFuncValue}; + use crate::policy::function::{ + key::Key, + key_name::AwsKeyName::*, + key_name::KeyName::{self, *}, + }; + use test_case::test_case; + + fn new_func(name: KeyName, variable: Option, value: Vec<&str>) -> AddrFunc { + AddrFunc { + key: Key { name, variable }, + values: AddrFuncValue(value.into_iter().map(|x| x.parse().unwrap()).collect()), + } + } + + #[test_case(r#"{"aws:SourceIp": "203.0.113.0/24"}"#, new_func(Aws(AWSSourceIP), None, vec!["203.0.113.0/24"]); "1")] + #[test_case(r#"{"aws:SourceIp": "203.0.113.0"}"#, new_func(Aws(AWSSourceIP), None, vec!["203.0.113.0/32"]); "2")] + #[test_case(r#"{"aws:SourceIp": "2001:DB8:1234:5678::/64"}"#, new_func(Aws(AWSSourceIP),None, vec!["2001:DB8:1234:5678::/64"]); "3")] + #[test_case(r#"{"aws:SourceIp": "2001:DB8:1234:5678::"}"#, new_func(Aws(AWSSourceIP), None, vec!["2001:DB8:1234:5678::/32"]); "4")] + #[test_case(r#"{"aws:SourceIp": ["203.0.113.0/24","203.0.113.0"]}"#, new_func(Aws(AWSSourceIP), None, vec!["203.0.113.0/24", "203.0.113.0/32"]); "5")] + #[test_case(r#"{"aws:SourceIp": ["2001:DB8:1234:5678::/64","203.0.113.0/24"]}"#, new_func(Aws(AWSSourceIP), None, vec!["2001:DB8:1234:5678::/64", "203.0.113.0/24"]); "6")] + #[test_case(r#"{"aws:SourceIp": ["2001:DB8:1234:5678::/64", "2001:DB8:1234:5678::"]}"#, new_func(Aws(AWSSourceIP),None, vec!["2001:DB8:1234:5678::/64", "2001:DB8:1234:5678::/32"]); "7")] + #[test_case(r#"{"aws:SourceIp": ["2001:DB8:1234:5678::", "203.0.113.0"]}"#, new_func(Aws(AWSSourceIP), None, vec!["2001:DB8:1234:5678::/32", "203.0.113.0/32"]); "8")] + #[test_case(r#"{"aws:SourceIp/a": "203.0.113.0/24"}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/24"]); "9")] + #[test_case(r#"{"aws:SourceIp/a": "203.0.113.0/24"}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/24"]); "10")] + #[test_case(r#"{"aws:SourceIp/a": "203.0.113.0"}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/32"]); "11")] + #[test_case(r#"{"aws:SourceIp/a": "2001:DB8:1234:5678::/64"}"#, new_func(Aws(AWSSourceIP),Some("a".into()), vec!["2001:DB8:1234:5678::/64"]); "12")] + #[test_case(r#"{"aws:SourceIp/a": "2001:DB8:1234:5678::"}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["2001:DB8:1234:5678::/32"]); "13")] + #[test_case(r#"{"aws:SourceIp/a": ["203.0.113.0/24", "203.0.113.0"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/24", "203.0.113.0/32"]); "14")] + #[test_case(r#"{"aws:SourceIp/a": ["2001:DB8:1234:5678::/64", "203.0.113.0/24"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["2001:DB8:1234:5678::/64", "203.0.113.0/24"]); "15")] + #[test_case(r#"{"aws:SourceIp/a": ["2001:DB8:1234:5678::/64", "2001:DB8:1234:5678::"]}"#, new_func(Aws(AWSSourceIP),Some("a".into()), vec!["2001:DB8:1234:5678::/64", "2001:DB8:1234:5678::/32"]); "16")] + #[test_case(r#"{"aws:SourceIp/a": ["2001:DB8:1234:5678::", "203.0.113.0"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["2001:DB8:1234:5678::/32", "203.0.113.0/32"]); "17")] + fn test_deser(input: &str, expect: AddrFunc) -> Result<(), serde_json::Error> { + let v: AddrFunc = serde_json::from_str(input)?; + assert_eq!(v, expect); + Ok(()) + } + + #[test_case(r#"{"aws:SourceIp":["203.0.113.0/24"]}"#, new_func(Aws(AWSSourceIP), None, vec!["203.0.113.0/24"]); "1")] + #[test_case(r#"{"aws:SourceIp":["203.0.113.0/32"]}"#, new_func(Aws(AWSSourceIP), None, vec!["203.0.113.0/32"]); "2")] + #[test_case(r#"{"aws:SourceIp":["2001:db8:1234:5678::/64"]}"#, new_func(Aws(AWSSourceIP),None, vec!["2001:DB8:1234:5678::/64"]); "3")] + #[test_case(r#"{"aws:SourceIp":["2001:db8:1234:5678::/32"]}"#, new_func(Aws(AWSSourceIP), None, vec!["2001:DB8:1234:5678::/32"]); "4")] + #[test_case(r#"{"aws:SourceIp":["203.0.113.0/24","203.0.113.0/32"]}"#, new_func(Aws(AWSSourceIP), None, vec!["203.0.113.0/24", "203.0.113.0/32"]); "5")] + #[test_case(r#"{"aws:SourceIp":["2001:db8:1234:5678::/64","203.0.113.0/24"]}"#, new_func(Aws(AWSSourceIP), None, vec!["2001:DB8:1234:5678::/64", "203.0.113.0/24"]); "6")] + #[test_case(r#"{"aws:SourceIp":["2001:db8:1234:5678::/64","2001:db8:1234:5678::/32"]}"#, new_func(Aws(AWSSourceIP),None, vec!["2001:DB8:1234:5678::/64", "2001:DB8:1234:5678::/32"]); "7")] + #[test_case(r#"{"aws:SourceIp":["2001:db8:1234:5678::/32","203.0.113.0/32"]}"#, new_func(Aws(AWSSourceIP), None, vec!["2001:DB8:1234:5678::/32", "203.0.113.0/32"]); "8")] + #[test_case(r#"{"aws:SourceIp/a":["203.0.113.0/24"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/24"]); "9")] + #[test_case(r#"{"aws:SourceIp/a":["203.0.113.0/24"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/24"]); "10")] + #[test_case(r#"{"aws:SourceIp/a":["203.0.113.0/32"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/32"]); "11")] + #[test_case(r#"{"aws:SourceIp/a":["2001:db8:1234:5678::/64"]}"#, new_func(Aws(AWSSourceIP),Some("a".into()), vec!["2001:DB8:1234:5678::/64"]); "12")] + #[test_case(r#"{"aws:SourceIp/a":["2001:db8:1234:5678::/32"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["2001:DB8:1234:5678::/32"]); "13")] + #[test_case(r#"{"aws:SourceIp/a":["203.0.113.0/24","203.0.113.0/32"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/24", "203.0.113.0/32"]); "14")] + #[test_case(r#"{"aws:SourceIp/a":["2001:db8:1234:5678::/64","203.0.113.0/24"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["2001:DB8:1234:5678::/64", "203.0.113.0/24"]); "15")] + #[test_case(r#"{"aws:SourceIp/a":["2001:db8:1234:5678::/64","2001:db8:1234:5678::/32"]}"#, new_func(Aws(AWSSourceIP),Some("a".into()), vec!["2001:DB8:1234:5678::/64", "2001:DB8:1234:5678::/32"]); "16")] + #[test_case(r#"{"aws:SourceIp/a":["2001:db8:1234:5678::/32","203.0.113.0/32"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["2001:DB8:1234:5678::/32", "203.0.113.0/32"]); "17")] + fn test_ser(expect: &str, input: AddrFunc) -> Result<(), serde_json::Error> { + let v = serde_json::to_string(&input)?; + assert_eq!(v, expect); + Ok(()) + } +} diff --git a/iam/src/policy/function/binary.rs b/iam/src/policy/function/binary.rs new file mode 100644 index 00000000..299dac64 --- /dev/null +++ b/iam/src/policy/function/binary.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +use super::func::InnerFunc; + +pub type BinaryFunc = InnerFunc; + +// todo implement it +#[derive(Serialize, Deserialize, Clone)] +#[serde(transparent)] +pub struct BinaryFuncValue(String); diff --git a/iam/src/policy/function/bool_null.rs b/iam/src/policy/function/bool_null.rs new file mode 100644 index 00000000..b2ee1dbe --- /dev/null +++ b/iam/src/policy/function/bool_null.rs @@ -0,0 +1,119 @@ +use super::func::InnerFunc; +use serde::{de, Deserialize, Deserializer, Serialize}; +use std::{collections::HashMap, fmt}; + +pub type BoolFunc = InnerFunc; +impl BoolFunc { + pub fn evaluate_bool(&self, values: &HashMap>) -> bool { + match values.get(self.key.name().as_str()).and_then(|x| x.get(0)) { + Some(x) => self.values.0.to_string().as_str() == x, + None => false, + } + } + + pub fn evaluate_null(&self, values: &HashMap>) -> bool { + let len = values.get(self.key.name().as_str()).map(Vec::len).unwrap_or(0); + if self.values.0 { + return len == 0; + } + + len != 0 + } +} + +#[derive(Clone)] +#[cfg_attr(test, derive(PartialEq, Eq, Debug))] +pub struct BoolFuncValue(bool); + +impl Serialize for BoolFuncValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.0.to_string()) + } +} + +impl<'de> Deserialize<'de> for BoolFuncValue { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct BoolOrStringVisitor; + + impl<'de> de::Visitor<'de> for BoolOrStringVisitor { + type Value = BoolFuncValue; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a boolean or a string representing 'true' or 'false'") + } + + fn visit_bool(self, value: bool) -> Result + where + E: de::Error, + { + Ok(BoolFuncValue(value)) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(BoolFuncValue(value.parse::().map_err(|e| E::custom(format!("{e:?}")))?)) + } + } + + deserializer.deserialize_any(BoolOrStringVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::{BoolFunc, BoolFuncValue}; + use crate::policy::function::{ + key::Key, + key_name::AwsKeyName::*, + key_name::KeyName::{self, *}, + }; + use test_case::test_case; + + fn new_func(name: KeyName, variable: Option, value: bool) -> BoolFunc { + BoolFunc { + key: Key { name, variable }, + values: BoolFuncValue(value), + } + } + + #[test_case(r#"{"aws:SecureTransport": "true"}"#, new_func(Aws(AWSSecureTransport), None, true); "1")] + #[test_case(r#"{"aws:SecureTransport": "false"}"#, new_func(Aws(AWSSecureTransport), None, false); "2")] + #[test_case(r#"{"aws:SecureTransport": true}"#, new_func(Aws(AWSSecureTransport), None, true); "3")] + #[test_case(r#"{"aws:SecureTransport": false}"#, new_func(Aws(AWSSecureTransport), None, false); "4")] + #[test_case(r#"{"aws:SecureTransport/a": "true"}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), true); "9")] + #[test_case(r#"{"aws:SecureTransport/a": "false"}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), false); "10")] + #[test_case(r#"{"aws:SecureTransport/a": true}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), true); "11")] + #[test_case(r#"{"aws:SecureTransport/a": false}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), false); "12")] + fn test_deser(input: &str, expect: BoolFunc) -> Result<(), serde_json::Error> { + let v: BoolFunc = serde_json::from_str(input)?; + assert_eq!(v, expect); + Ok(()) + } + + #[test_case(r#"{"aws:usernamea":"johndoe"}"#)] + #[test_case(r#"{"aws:username":[]}"#)] // 空 + #[test_case(r#"{"aws:usernamea/value":"johndoe"}"#)] + #[test_case(r#"{"aws:usernamea/value":["johndoe", "aaa"]}"#)] + #[test_case(r#""aaa""#)] + fn test_deser_failed(input: &str) { + assert!(serde_json::from_str::(input).is_err()); + } + + #[test_case(r#"{"aws:SecureTransport":"true"}"#, new_func(Aws(AWSSecureTransport), None, true); "1")] + #[test_case(r#"{"aws:SecureTransport":"false"}"#, new_func(Aws(AWSSecureTransport), None, false);"2")] + #[test_case(r#"{"aws:SecureTransport/aa":"true"}"#, new_func(Aws(AWSSecureTransport),Some("aa".into()), true);"3")] + #[test_case(r#"{"aws:SecureTransport/aa":"false"}"#, new_func(Aws(AWSSecureTransport), Some("aa".into()), false);"4")] + fn test_ser(expect: &str, input: BoolFunc) -> Result<(), serde_json::Error> { + let v = serde_json::to_string(&input)?; + assert_eq!(v.as_str(), expect); + Ok(()) + } +} diff --git a/iam/src/policy/function/condition.rs b/iam/src/policy/function/condition.rs new file mode 100644 index 00000000..4f743af3 --- /dev/null +++ b/iam/src/policy/function/condition.rs @@ -0,0 +1,78 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +use super::{addr::AddrFunc, binary::BinaryFunc, bool_null::BoolFunc, date::DateFunc, number::NumberFunc, string::StringFunc}; + +#[derive(Clone, Serialize, Deserialize)] +pub enum Condition { + StringEquals(StringFunc), + StringNotEquals(StringFunc), + StringEqualsIgnoreCase(StringFunc), + StringNotEqualsIgnoreCase(StringFunc), + StringLike(StringFunc), + StringNotLike(StringFunc), + BinaryEquals(BinaryFunc), + IpAddress(AddrFunc), + NotIpAddress(AddrFunc), + Null(BoolFunc), + Bool(BoolFunc), + NumericEquals(NumberFunc), + NumericNotEquals(NumberFunc), + NumericLessThan(NumberFunc), + NumericLessThanEquals(NumberFunc), + NumericGreaterThan(NumberFunc), + NumericGreaterThanIfExists(NumberFunc), + NumericGreaterThanEquals(NumberFunc), + DateEquals(DateFunc), + DateNotEquals(DateFunc), + DateLessThan(DateFunc), + DateLessThanEquals(DateFunc), + DateGreaterThan(DateFunc), + DateGreaterThanEquals(DateFunc), +} + +impl Condition { + pub fn evaluate(&self, for_all: bool, values: &HashMap>) -> bool { + use Condition::*; + + let r = match self { + StringEquals(s) => s.evaluate(for_all, false, false, values), + StringNotEquals(s) => s.evaluate(for_all, false, false, values), + StringEqualsIgnoreCase(s) => s.evaluate(for_all, true, false, values), + StringNotEqualsIgnoreCase(s) => s.evaluate(for_all, true, false, values), + StringLike(s) => s.evaluate(for_all, false, true, values), + StringNotLike(s) => s.evaluate(for_all, false, true, values), + BinaryEquals(s) => todo!(), + IpAddress(s) => s.evaluate(values), + NotIpAddress(s) => s.evaluate(values), + Null(s) => s.evaluate_null(values), + Bool(s) => s.evaluate_bool(values), + NumericEquals(s) => s.evaluate(i64::eq, false, values), + NumericNotEquals(s) => s.evaluate(i64::ne, false, values), + NumericLessThan(s) => s.evaluate(i64::lt, false, values), + NumericLessThanEquals(s) => s.evaluate(i64::le, false, values), + NumericGreaterThan(s) => s.evaluate(i64::gt, false, values), + NumericGreaterThanIfExists(s) => s.evaluate(i64::ge, true, values), + NumericGreaterThanEquals(s) => s.evaluate(i64::ge, false, values), + DateEquals(s) => s.evaluate(OffsetDateTime::eq, values), + DateNotEquals(s) => s.evaluate(OffsetDateTime::ne, values), + DateLessThan(s) => s.evaluate(OffsetDateTime::lt, values), + DateLessThanEquals(s) => s.evaluate(OffsetDateTime::le, values), + DateGreaterThan(s) => s.evaluate(OffsetDateTime::gt, values), + DateGreaterThanEquals(s) => s.evaluate(OffsetDateTime::ge, values), + }; + + if self.is_negate() { + !r + } else { + r + } + } + + pub fn is_negate(&self) -> bool { + use Condition::*; + matches!(self, StringNotEquals(_) | StringNotEqualsIgnoreCase(_) | NotIpAddress(_)) + } +} diff --git a/iam/src/policy/function/date.rs b/iam/src/policy/function/date.rs new file mode 100644 index 00000000..beb52e99 --- /dev/null +++ b/iam/src/policy/function/date.rs @@ -0,0 +1,107 @@ +use super::func::InnerFunc; +use serde::{de, Deserialize, Deserializer, Serialize}; +use std::{collections::HashMap, fmt}; +use time::{format_description::well_known::Rfc3339, OffsetDateTime}; + +pub type DateFunc = InnerFunc; + +impl DateFunc { + pub fn evaluate( + &self, + op: impl FnOnce(&OffsetDateTime, &OffsetDateTime) -> bool, + values: &HashMap>, + ) -> bool { + let v = match values.get(self.key.name().as_str()).and_then(|x| x.get(0)) { + Some(x) => x, + None => return false, + }; + + let Ok(rv) = OffsetDateTime::parse(v, &Rfc3339) else { + return false; + }; + + op(&self.values.0, &rv) + } +} + +#[derive(Clone)] +#[cfg_attr(test, derive(PartialEq, Eq, Debug))] +pub struct DateFuncValue(OffsetDateTime); + +impl Serialize for DateFuncValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::Error; + serializer.serialize_str( + &self + .0 + .format(&Rfc3339) + .map_err(|e| S::Error::custom(format!("format datetime failed: {e:?}")))?, + ) + } +} + +impl<'de> Deserialize<'de> for DateFuncValue { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct DateVisitor; + + impl<'de> de::Visitor<'de> for DateVisitor { + type Value = DateFuncValue; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a data string that is representable in RFC 3339 format.") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(DateFuncValue( + OffsetDateTime::parse(value, &Rfc3339).map_err(|e| E::custom(format!("{e:?}")))?, + )) + } + } + + deserializer.deserialize_str(DateVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::{DateFunc, DateFuncValue}; + use crate::policy::function::{ + key::Key, + key_name::KeyName::{self, *}, + key_name::S3KeyName::*, + }; + use test_case::test_case; + use time::{format_description::well_known::Rfc3339, OffsetDateTime}; + + fn new_func(name: KeyName, variable: Option, value: &str) -> DateFunc { + DateFunc { + key: Key { name, variable }, + values: DateFuncValue(OffsetDateTime::parse(value, &Rfc3339).unwrap()), + } + } + + #[test_case(r#"{"s3:object-lock-retain-until-date": "2009-11-10T15:00:00Z"}"#, new_func(S3(S3ObjectLockRetainUntilDate), None, "2009-11-10T15:00:00Z"); "1")] + #[test_case(r#"{"s3:object-lock-retain-until-date/a": "2009-11-10T15:00:00Z"}"#, new_func(S3(S3ObjectLockRetainUntilDate), Some("a".into()), "2009-11-10T15:00:00Z"); "2")] + fn test_deser(input: &str, expect: DateFunc) -> Result<(), serde_json::Error> { + let v: DateFunc = serde_json::from_str(input)?; + assert_eq!(v, expect); + Ok(()) + } + + #[test_case(r#"{"s3:object-lock-retain-until-date":"2009-11-10T15:00:00Z"}"#, new_func(S3(S3ObjectLockRetainUntilDate), None, "2009-11-10T15:00:00Z"); "1")] + #[test_case(r#"{"s3:object-lock-retain-until-date/a":"2009-11-10T15:00:00Z"}"#, new_func(S3(S3ObjectLockRetainUntilDate), Some("a".into()), "2009-11-10T15:00:00Z"); "2")] + fn test_ser(expect: &str, input: DateFunc) -> Result<(), serde_json::Error> { + let v = serde_json::to_string(&input)?; + assert_eq!(v, expect); + Ok(()) + } +} diff --git a/iam/src/policy/function/func.rs b/iam/src/policy/function/func.rs new file mode 100644 index 00000000..9c4e7bdd --- /dev/null +++ b/iam/src/policy/function/func.rs @@ -0,0 +1,91 @@ +use std::{collections::HashMap, marker::PhantomData}; + +use serde::{ + de::{self, Visitor}, + Deserialize, Deserializer, Serialize, +}; + +use super::{condition::Condition, key::Key}; + +#[derive(Clone, Serialize, Deserialize)] +pub enum Func { + ForAnyValues(Vec), + ForAllValues(Vec), + ForNormal(Vec), +} + +impl Func { + pub fn evaluate(&self, values: &HashMap>) -> bool { + match self { + Self::ForAnyValues(conditions) => conditions.iter().all(|x| x.evaluate(true, values)), + Self::ForAllValues(conditions) => conditions.iter().all(|x| x.evaluate(false, values)), + Self::ForNormal(conditions) => conditions.iter().all(|x| x.evaluate(false, values)), + } + } +} + +#[cfg_attr(test, derive(PartialEq, Eq, Debug))] +pub struct InnerFunc { + pub key: Key, + pub values: T, +} + +impl Clone for InnerFunc { + fn clone(&self) -> Self { + Self { + key: self.key.clone(), + values: self.values.clone(), + } + } +} + +impl Serialize for InnerFunc { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_key(&self.key)?; + map.serialize_value(&self.values)?; + map.end() + } +} + +impl<'de, T> Deserialize<'de> for InnerFunc +where + T: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct FuncVisitor(PhantomData); + impl<'v, T> Visitor<'v> for FuncVisitor + where + T: Deserialize<'v>, + { + type Value = InnerFunc; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("struct StringFunc") + } + + fn visit_map(self, mut map: A) -> Result + where + A: de::MapAccess<'v>, + { + use serde::de::Error; + + let Some((key, values)) = map.next_entry::()? else { + return Err(A::Error::custom("no k-v pair")); + }; + + Ok(InnerFunc { key, values }) + } + } + + deserializer.deserialize_map(FuncVisitor::(PhantomData)) + } +} diff --git a/iam/src/policy/function/key.rs b/iam/src/policy/function/key.rs new file mode 100644 index 00000000..54ba53a1 --- /dev/null +++ b/iam/src/policy/function/key.rs @@ -0,0 +1,110 @@ +use serde::{Deserialize, Serialize}; + +use super::key_name::KeyName; +use crate::policy::{Error, Validator}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(into = "String")] +#[serde(try_from = "&str")] +pub struct Key { + pub name: KeyName, + pub variable: Option, +} + +impl Validator for Key {} + +impl Key { + pub fn is(&self, other: &KeyName) -> bool { + self.name.eq(other) + } + + pub fn val_name(&self) -> String { + self.name.val_name() + } + + pub fn name(&self) -> String { + if let Some(ref x) = self.variable { + format!("{}/{}", self.name.name(), x) + } else { + self.name.name().to_owned() + } + } +} + +impl From for String { + fn from(value: Key) -> Self { + value.name() + } +} + +impl TryFrom<&str> for Key { + type Error = Error; + + fn try_from(value: &str) -> Result { + let mut iter = value.splitn(2, '/'); + let name = iter.next().ok_or_else(|| Error::InvalidKey(value.to_string()))?; + let variable = iter.next().map(Into::into); + + Ok(Self { + name: KeyName::try_from(name)?, + variable, + }) + } +} + +#[cfg(test)] +mod tests { + use super::Key; + use test_case::test_case; + + fn new_key(name: &str, value: Option<&str>) -> Key { + Key { + name: name.try_into().unwrap(), + variable: value.map(ToString::to_string), + } + } + + #[test_case(new_key("s3:x-amz-copy-source", Some("aaa")), r#""s3:x-amz-copy-source/aaa""#)] + #[test_case(new_key("s3:x-amz-copy-source", None), r#""s3:x-amz-copy-source""#)] + #[test_case(new_key("aws:Referer", Some("bbb")), r#""aws:Referer/bbb""#)] + #[test_case(new_key("aws:Referer", None), r#""aws:Referer""#)] + #[test_case(new_key("jwt:website", None), r#""jwt:website""#)] + #[test_case(new_key("jwt:website", Some("aaa")), r#""jwt:website/aaa""#)] + #[test_case(new_key("svc:DurationSeconds", None), r#""svc:DurationSeconds""#)] + #[test_case(new_key("svc:DurationSeconds", Some("aaa")), r#""svc:DurationSeconds/aaa""#)] + fn test_serialize_successful(key: Key, except: &str) -> Result<(), serde_json::Error> { + let val = serde_json::to_string(&key)?; + assert_eq!(val.as_str(), except); + Ok(()) + } + + #[test_case("s3:x-amz-copy-source1/aaa")] + #[test_case("s33:x-amz-copy-source")] + #[test_case("aw2s:Referer/bbb")] + #[test_case("aws:Referera")] + #[test_case("jwdt:website")] + #[test_case("jwt:dwebsite/aaa")] + #[test_case("sfvc:DuratdionSeconds")] + #[test_case("svc:DursationSeconds/aaa")] + fn test_deserialize_falied(key: &str) { + let val = serde_json::from_str::(key); + assert!(val.is_err()); + } + + #[test_case(new_key("s3:x-amz-copy-source", Some("aaa")), r#""s3:x-amz-copy-source/aaa""#)] + #[test_case(new_key("s3:x-amz-copy-source", None), r#""s3:x-amz-copy-source""#)] + #[test_case(new_key("aws:Referer", Some("bbb")), r#""aws:Referer/bbb""#)] + #[test_case(new_key("aws:Referer", None), r#""aws:Referer""#)] + #[test_case(new_key("jwt:website", None), r#""jwt:website""#)] + #[test_case(new_key("jwt:website", Some("aaa")), r#""jwt:website/aaa""#)] + #[test_case(new_key("svc:DurationSeconds", None), r#""svc:DurationSeconds""#)] + #[test_case(new_key("svc:DurationSeconds", Some("aaa")), r#""svc:DurationSeconds/aaa""#)] + fn test_deserialize(except: Key, input: &str) -> Result<(), serde_json::Error> { + let v = serde_json::from_str::(input)?; + assert_eq!(v.name, except.name); + assert_eq!(v.variable, except.variable); + + Ok(()) + } +} diff --git a/iam/src/policy/function/key_name.rs b/iam/src/policy/function/key_name.rs new file mode 100644 index 00000000..f2fb6fdb --- /dev/null +++ b/iam/src/policy/function/key_name.rs @@ -0,0 +1,333 @@ +use crate::policy::Error::{self, InvalidKeyName}; +use serde::{Deserialize, Serialize}; +use strum::{EnumString, IntoStaticStr}; + +#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)] +#[serde(try_from = "&str", untagged)] +pub enum KeyName { + Aws(AwsKeyName), + Jwt(JwtKeyName), + Ldap(LdapKeyName), + Sts(StsKeyName), + Svc(SvcKeyName), + S3(S3KeyName), +} + +impl TryFrom<&str> for KeyName { + type Error = Error; + fn try_from(value: &str) -> Result { + Ok(if value.starts_with("s3:") { + Self::S3(S3KeyName::try_from(value).map_err(|_| InvalidKeyName(value.into()))?) + } else if value.starts_with("aws:") { + Self::Aws(AwsKeyName::try_from(value).map_err(|_| InvalidKeyName(value.into()))?) + } else if value.starts_with("ldap:") { + Self::Ldap(LdapKeyName::try_from(value).map_err(|_| InvalidKeyName(value.into()))?) + } else if value.starts_with("sts:") { + Self::Sts(StsKeyName::try_from(value).map_err(|_| InvalidKeyName(value.into()))?) + } else if value.starts_with("jwt:") { + Self::Jwt(JwtKeyName::try_from(value).map_err(|_| InvalidKeyName(value.into()))?) + } else if value.starts_with("svc:") { + Self::Svc(SvcKeyName::try_from(value).map_err(|_| InvalidKeyName(value.into()))?) + } else { + Err(InvalidKeyName(value.into()))? + }) + } +} + +impl KeyName { + pub const COMMON_KEYS: &[KeyName] = &[ + // s3 + KeyName::S3(S3KeyName::S3SignatureVersion), + KeyName::S3(S3KeyName::S3AuthType), + KeyName::S3(S3KeyName::S3SignatureAge), + KeyName::S3(S3KeyName::S3XAmzContentSha256), + KeyName::S3(S3KeyName::S3LocationConstraint), + //aws + KeyName::Aws(AwsKeyName::AWSReferer), + KeyName::Aws(AwsKeyName::AWSSourceIP), + KeyName::Aws(AwsKeyName::AWSUserAgent), + KeyName::Aws(AwsKeyName::AWSSecureTransport), + KeyName::Aws(AwsKeyName::AWSCurrentTime), + KeyName::Aws(AwsKeyName::AWSEpochTime), + KeyName::Aws(AwsKeyName::AWSPrincipalType), + KeyName::Aws(AwsKeyName::AWSUserID), + KeyName::Aws(AwsKeyName::AWSUsername), + KeyName::Aws(AwsKeyName::AWSGroups), + // ldap + KeyName::Ldap(LdapKeyName::LDAPUser), + KeyName::Ldap(LdapKeyName::LDAPUsername), + KeyName::Ldap(LdapKeyName::LDAPGroups), + // jwt + KeyName::Jwt(JwtKeyName::JWTSub), + KeyName::Jwt(JwtKeyName::JWTIss), + KeyName::Jwt(JwtKeyName::JWTAud), + KeyName::Jwt(JwtKeyName::JWTJti), + KeyName::Jwt(JwtKeyName::JWTName), + KeyName::Jwt(JwtKeyName::JWTUpn), + KeyName::Jwt(JwtKeyName::JWTGroups), + KeyName::Jwt(JwtKeyName::JWTGivenName), + KeyName::Jwt(JwtKeyName::JWTFamilyName), + KeyName::Jwt(JwtKeyName::JWTMiddleName), + KeyName::Jwt(JwtKeyName::JWTNickName), + KeyName::Jwt(JwtKeyName::JWTPrefUsername), + KeyName::Jwt(JwtKeyName::JWTProfile), + KeyName::Jwt(JwtKeyName::JWTPicture), + KeyName::Jwt(JwtKeyName::JWTWebsite), + KeyName::Jwt(JwtKeyName::JWTEmail), + KeyName::Jwt(JwtKeyName::JWTGender), + KeyName::Jwt(JwtKeyName::JWTBirthdate), + KeyName::Jwt(JwtKeyName::JWTPhoneNumber), + KeyName::Jwt(JwtKeyName::JWTAddress), + KeyName::Jwt(JwtKeyName::JWTScope), + KeyName::Jwt(JwtKeyName::JWTClientID), + ]; + + pub fn name(&self) -> &str { + match self { + KeyName::Aws(aws) => aws.into(), + KeyName::Jwt(jwt) => jwt.into(), + KeyName::Ldap(ldap) => ldap.into(), + KeyName::Sts(sts) => sts.into(), + KeyName::Svc(svc) => svc.into(), + KeyName::S3(s3) => s3.into(), + } + } + + pub fn val_name(&self) -> String { + match self { + KeyName::Aws(aws) => Into::<&str>::into(aws).to_owned(), + KeyName::Jwt(jwt) => Into::<&str>::into(jwt).to_owned(), + KeyName::Ldap(ldap) => Into::<&str>::into(ldap).to_owned(), + KeyName::Sts(sts) => Into::<&str>::into(sts).to_owned(), + KeyName::Svc(svc) => Into::<&str>::into(svc).to_owned(), + KeyName::S3(s3) => Into::<&str>::into(s3).to_owned(), + } + } +} + +#[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "&str", into = "&str")] +pub enum S3KeyName { + #[strum(serialize = "s3:x-amz-copy-source")] + S3XAmzCopySource, + + #[strum(serialize = "s3:x-amz-server-side-encryption")] + S3XAmzServerSideEncryption, + + #[strum(serialize = "s3:x-amz-server-side-encryption-customer-algorithm")] + S3XAmzServerSideEncryptionCustomerAlgorithm, + + #[strum(serialize = "s3:signatureversion")] + S3SignatureVersion, + + #[strum(serialize = "s3:authType")] + S3AuthType, + + #[strum(serialize = "s3:signatureAge")] + S3SignatureAge, + + #[strum(serialize = "s3:x-amz-content-sha256")] + S3XAmzContentSha256, + + #[strum(serialize = "s3:LocationConstraint")] + S3LocationConstraint, + + #[strum(serialize = "s3:object-lock-retain-until-date")] + S3ObjectLockRetainUntilDate, + + #[strum(serialize = "s3:max-keys")] + S3MaxKeys, +} + +#[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "&str", into = "&str")] +pub enum JwtKeyName { + #[strum(serialize = "jwt:sub")] + JWTSub, + + #[strum(serialize = "jwt:iss")] + JWTIss, + + #[strum(serialize = "jwt:aud")] + JWTAud, + + #[strum(serialize = "jwt:jti")] + JWTJti, + + #[strum(serialize = "jwt:name")] + JWTName, + + #[strum(serialize = "jwt:upn")] + JWTUpn, + + #[strum(serialize = "jwt:groups")] + JWTGroups, + + #[strum(serialize = "jwt:given_name")] + JWTGivenName, + + #[strum(serialize = "jwt:family_name")] + JWTFamilyName, + + #[strum(serialize = "jwt:middle_name")] + JWTMiddleName, + + #[strum(serialize = "jwt:nickname")] + JWTNickName, + + #[strum(serialize = "jwt:preferred_username")] + JWTPrefUsername, + + #[strum(serialize = "jwt:profile")] + JWTProfile, + + #[strum(serialize = "jwt:picture")] + JWTPicture, + + #[strum(serialize = "jwt:website")] + JWTWebsite, + + #[strum(serialize = "jwt:email")] + JWTEmail, + + #[strum(serialize = "jwt:gender")] + JWTGender, + + #[strum(serialize = "jwt:birthdate")] + JWTBirthdate, + + #[strum(serialize = "jwt:phone_number")] + JWTPhoneNumber, + + #[strum(serialize = "jwt:address")] + JWTAddress, + + #[strum(serialize = "jwt:scope")] + JWTScope, + + #[strum(serialize = "jwt:client_id")] + JWTClientID, +} + +#[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "&str", into = "&str")] +pub enum SvcKeyName { + #[strum(serialize = "svc:DurationSeconds")] + SVCDurationSeconds, +} + +#[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "&str", into = "&str")] +pub enum LdapKeyName { + #[strum(serialize = "ldap:user")] + LDAPUser, + + #[strum(serialize = "ldap:username")] + LDAPUsername, + + #[strum(serialize = "ldap:groups")] + LDAPGroups, +} + +#[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "&str", into = "&str")] +pub enum StsKeyName { + #[strum(serialize = "sts:DurationSeconds")] + STSDurationSeconds, +} + +#[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "&str", into = "&str")] +pub enum AwsKeyName { + #[strum(serialize = "aws:Referer")] + AWSReferer, + + #[strum(serialize = "aws:SourceIp")] + AWSSourceIP, + + #[strum(serialize = "aws:UserAgent")] + AWSUserAgent, + + #[strum(serialize = "aws:SecureTransport")] + AWSSecureTransport, + + #[strum(serialize = "aws:CurrentTime")] + AWSCurrentTime, + + #[strum(serialize = "aws:EpochTime")] + AWSEpochTime, + + #[strum(serialize = "aws:principaltype")] + AWSPrincipalType, + + #[strum(serialize = "aws:userid")] + AWSUserID, + + #[strum(serialize = "aws:username")] + AWSUsername, + + #[strum(serialize = "aws:groups")] + AWSGroups, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::policy::Error; + use serde::Deserialize; + use test_case::test_case; + + #[test_case("s3:x-amz-copy-source", KeyName::S3(S3KeyName::S3XAmzCopySource))] + #[test_case("aws:SecureTransport", KeyName::Aws(AwsKeyName::AWSSecureTransport))] + #[test_case("jwt:sub", KeyName::Jwt(JwtKeyName::JWTSub))] + #[test_case("ldap:user", KeyName::Ldap(LdapKeyName::LDAPUser))] + #[test_case("sts:DurationSeconds", KeyName::Sts(StsKeyName::STSDurationSeconds))] + #[test_case("svc:DurationSeconds", KeyName::Svc(SvcKeyName::SVCDurationSeconds))] + fn key_name_from_str_successful(val: &str, except: KeyName) { + let key_name = KeyName::try_from(val); + assert_eq!(key_name, Ok(except)); + } + + #[test_case("S3:x-amz-copy-source")] + #[test_case("aWs:SecureTransport")] + #[test_case("jwt:suB")] + #[test_case("ldap:us")] + #[test_case("DurationSeconds")] + fn key_name_from_str_failed(val: &str) { + assert_eq!(KeyName::try_from(val), Err(Error::InvalidKeyName(val.to_string()))); + } + + #[test_case("s3:x-amz-copy-source", KeyName::S3(S3KeyName::S3XAmzCopySource))] + #[test_case("aws:SecureTransport", KeyName::Aws(AwsKeyName::AWSSecureTransport))] + #[test_case("jwt:sub", KeyName::Jwt(JwtKeyName::JWTSub))] + #[test_case("ldap:user", KeyName::Ldap(LdapKeyName::LDAPUser))] + #[test_case("sts:DurationSeconds", KeyName::Sts(StsKeyName::STSDurationSeconds))] + #[test_case("svc:DurationSeconds", KeyName::Svc(SvcKeyName::SVCDurationSeconds))] + fn key_name_deserialize(val: &str, except: KeyName) { + #[derive(Deserialize)] + struct TestCase { + data: KeyName, + } + + let data = format!("{{\"data\":\"{val}\"}}"); + let data: TestCase = serde_json::from_str(data.as_str()).expect("unmarshal failed"); + assert_eq!(data.data, except); + } + + #[test_case("s3:x-amz-copy-source", KeyName::S3(S3KeyName::S3XAmzCopySource))] + #[test_case("aws:SecureTransport", KeyName::Aws(AwsKeyName::AWSSecureTransport))] + #[test_case("jwt:sub", KeyName::Jwt(JwtKeyName::JWTSub))] + #[test_case("ldap:user", KeyName::Ldap(LdapKeyName::LDAPUser))] + #[test_case("sts:DurationSeconds", KeyName::Sts(StsKeyName::STSDurationSeconds))] + #[test_case("svc:DurationSeconds", KeyName::Svc(SvcKeyName::SVCDurationSeconds))] + fn key_name_serialize(except: &str, value: KeyName) { + #[derive(Serialize)] + struct TestCase { + data: KeyName, + } + + let except = format!("{{\"data\":\"{except}\"}}"); + let data = serde_json::to_string(&TestCase { data: value }).expect("marshal failed"); + assert_eq!(data, except); + } +} diff --git a/iam/src/policy/function/number.rs b/iam/src/policy/function/number.rs new file mode 100644 index 00000000..fe91e213 --- /dev/null +++ b/iam/src/policy/function/number.rs @@ -0,0 +1,113 @@ +use std::collections::HashMap; + +use super::func::InnerFunc; +use serde::{ + de::{Error, Visitor}, + Deserialize, Deserializer, Serialize, +}; + +pub type NumberFunc = InnerFunc; + +#[derive(Clone)] +#[cfg_attr(test, derive(PartialEq, Eq, Debug))] +pub struct NumberFuncValue(i64); + +impl NumberFunc { + pub fn evaluate(&self, op: impl FnOnce(&i64, &i64) -> bool, if_exists: bool, values: &HashMap>) -> bool { + let v = match values.get(self.key.name().as_str()).and_then(|x| x.get(0)) { + Some(x) => x, + None => return if_exists, + }; + + let Ok(rv) = v.parse::() else { + return false; + }; + + op(&rv, &self.values.0) + } +} + +impl Serialize for NumberFuncValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.0.to_string().as_str()) + } +} + +impl<'de> Deserialize<'de> for NumberFuncValue { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct NumberVisitor; + + impl<'de> Visitor<'de> for NumberVisitor { + type Value = NumberFuncValue; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a number or a string that can be represented as a number.") + } + + fn visit_str(self, value: &str) -> Result + where + E: Error, + { + Ok(NumberFuncValue(value.parse().map_err(|e| E::custom(format!("{e:?}")))?)) + } + + fn visit_i64(self, value: i64) -> Result + where + E: Error, + { + Ok(NumberFuncValue(value)) + } + + fn visit_u64(self, value: u64) -> Result + where + E: Error, + { + Ok(NumberFuncValue(value as i64)) + } + } + + deserializer.deserialize_any(NumberVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::{NumberFunc, NumberFuncValue}; + use crate::policy::function::{ + key::Key, + key_name::KeyName::{self, *}, + key_name::S3KeyName::*, + }; + use test_case::test_case; + + fn new_func(name: KeyName, variable: Option, value: i64) -> NumberFunc { + NumberFunc { + key: Key { name, variable }, + values: NumberFuncValue(value), + } + } + + #[test_case(r#"{"s3:max-keys": 1}"#, new_func(S3(S3MaxKeys), None, 1); "1")] + #[test_case(r#"{"s3:max-keys/a": 1}"#, new_func(S3(S3MaxKeys), Some("a".into()), 1); "2")] + #[test_case(r#"{"s3:max-keys": "1"}"#, new_func(S3(S3MaxKeys), None, 1); "3")] + #[test_case(r#"{"s3:max-keys/a": "1"}"#, new_func(S3(S3MaxKeys), Some("a".into()), 1); "4")] + fn test_deser(input: &str, expect: NumberFunc) -> Result<(), serde_json::Error> { + let v: NumberFunc = serde_json::from_str(input)?; + assert_eq!(v, expect); + Ok(()) + } + + #[test_case(r#"{"s3:max-keys":"1"}"#, new_func(S3(S3MaxKeys), None, 1); "1")] + #[test_case(r#"{"s3:max-keys/a":"1"}"#, new_func(S3(S3MaxKeys), Some("a".into()), 1); "2")] + fn test_ser(expect: &str, input: NumberFunc) -> Result<(), serde_json::Error> { + let v = serde_json::to_string(&input)?; + assert_eq!(v, expect); + Ok(()) + } +} diff --git a/iam/src/policy/function/string.rs b/iam/src/policy/function/string.rs new file mode 100644 index 00000000..12602d83 --- /dev/null +++ b/iam/src/policy/function/string.rs @@ -0,0 +1,220 @@ +#[cfg(test)] +use std::collections::BTreeSet as Set; +#[cfg(not(test))] +use std::collections::HashSet as Set; +use std::fmt; +use std::{borrow::Cow, collections::HashMap}; + +use serde::{de, ser::SerializeSeq, Deserialize, Deserializer, Serialize}; + +use crate::policy::utils::wildcard; + +use super::{func::InnerFunc, key_name::KeyName}; + +pub type StringFunc = InnerFunc; + +impl StringFunc { + fn eval(&self, for_all: bool, ignore_case: bool, values: &HashMap>) -> bool { + let rvalues = values + .get(self.key.name().as_str()) + .map(|t| { + t.iter() + .map(|x| { + if ignore_case { + Cow::Owned(x.to_lowercase()) + } else { + Cow::from(x) + } + }) + .collect::>() + }) + .unwrap_or_default(); + + let fvalues = self + .values + .0 + .iter() + .map(|c| { + let mut c = Cow::from(c); + for key in KeyName::COMMON_KEYS { + match values.get(key.name()).and_then(|x| x.get(0)) { + Some(v) if !v.is_empty() => return Cow::Owned(c.to_mut().replace(key.name(), v)), + _ => continue, + }; + } + + c + }) + .map(|x| if ignore_case { Cow::Owned(x.to_lowercase()) } else { x }) + .collect::>(); + + let ivalues = rvalues.intersection(&fvalues); + if for_all { + rvalues.is_empty() || rvalues.len() == ivalues.count() + } else { + ivalues.count() > 0 + } + } + + fn eval_like(&self, for_all: bool, values: &HashMap>) -> bool { + if let Some(rvalues) = values.get(self.key.name().as_str()) { + for v in rvalues.iter() { + let matched = self + .values + .0 + .iter() + .map(|c| { + let mut c = Cow::from(c); + for key in KeyName::COMMON_KEYS { + match values.get(key.name()).and_then(|x| x.get(0)) { + Some(v) if !v.is_empty() => return Cow::Owned(c.to_mut().replace(key.name(), v)), + _ => continue, + }; + } + + c + }) + .any(|x| wildcard::is_match(x, v)); + + if for_all { + if !matched { + return false; + } + } else if matched { + return true; + } + } + } + + for_all + } + + pub(crate) fn evaluate(&self, for_all: bool, ignore_case: bool, like: bool, values: &HashMap>) -> bool { + if like { + self.eval_like(for_all, values) + } else { + self.eval(for_all, ignore_case, values) + } + } +} + +/// 解析values字段 +#[derive(Clone)] +#[cfg_attr(test, derive(PartialEq, Eq, Debug))] +pub struct StringFuncValue(Set); + +impl Serialize for StringFuncValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.0.len() == 1 { + serializer.serialize_some(&self.0.iter().next()) + } else { + let mut seq = serializer.serialize_seq(Some(self.0.len()))?; + for element in &self.0 { + seq.serialize_element(element)?; + } + seq.end() + } + } +} + +impl<'d> Deserialize<'d> for StringFuncValue { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'d>, + { + struct StringOrVecVisitor; + + impl<'de> de::Visitor<'de> for StringOrVecVisitor { + type Value = StringFuncValue; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string or an array of strings") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok({ + let mut hash = Set::new(); + hash.insert(value.to_string()); + StringFuncValue(hash) + }) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + #[cfg(test)] + let mut values = Set::new(); + #[cfg(not(test))] + let mut values = Set::with_capacity(seq.size_hint().unwrap_or(0)); + + while let Some(value) = seq.next_element::()? { + values.insert(value); + } + Ok(StringFuncValue(values)) + } + } + + let result = deserializer.deserialize_any(StringOrVecVisitor)?; + if result.0.is_empty() { + use serde::de::Error; + + return Err(D::Error::custom("empty")); + } + + Ok(result) + } +} + +#[cfg(test)] +mod tests { + use super::{StringFunc, StringFuncValue}; + use crate::policy::function::{ + key::Key, + key_name::AwsKeyName::*, + key_name::KeyName::{self, *}, + }; + use test_case::test_case; + + fn new_func(name: KeyName, variable: Option, values: Vec<&str>) -> StringFunc { + StringFunc { + key: Key { name, variable }, + values: StringFuncValue(values.into_iter().map(|x| x.to_owned()).collect()), + } + } + + #[test_case(r#"{"aws:username": "johndoe"}"#, new_func(Aws(AWSUsername), None, vec!["johndoe"]))] + #[test_case(r#"{"aws:username": ["johndoe", "aaa"]}"#, new_func(Aws(AWSUsername), None, vec!["johndoe", "aaa"]))] + #[test_case(r#"{"aws:username/value": "johndoe"}"#, new_func(Aws(AWSUsername), Some("value".into()), vec!["johndoe"]))] + #[test_case(r#"{"aws:username/value": ["johndoe", "aaa"]}"#, new_func(Aws(AWSUsername), Some("value".into()), vec!["johndoe", "aaa"]))] + fn test_deser(input: &str, expect: StringFunc) -> Result<(), serde_json::Error> { + let v: StringFunc = serde_json::from_str(input)?; + assert_eq!(v, expect); + Ok(()) + } + + #[test_case(r#"{"aws:usernamea":"johndoe"}"#)] + #[test_case(r#"{"aws:username":[]}"#)] // 空 + #[test_case(r#"{"aws:usernamea/value":"johndoe"}"#)] + #[test_case(r#"{"aws:usernamea/value":["johndoe", "aaa"]}"#)] + #[test_case(r#""aaa""#)] + fn test_deser_failed(input: &str) { + assert!(serde_json::from_str::(input).is_err()); + } + + #[test_case(r#"{"aws:username":"johndoe"}"#, new_func(Aws(AWSUsername), None, vec!["johndoe"]))] + #[test_case(r#"{"aws:username":["aaa","johndoe"]}"#, new_func(Aws(AWSUsername), None, vec!["johndoe", "aaa"]))] + #[test_case(r#"{"aws:username/value":"johndoe"}"#, new_func(Aws(AWSUsername), Some("value".into()), vec!["johndoe"]))] + #[test_case(r#"{"aws:username/value":["aaa","johndoe"]}"#, new_func(Aws(AWSUsername), Some("value".into()), vec!["johndoe", "aaa"]))] + fn test_ser(expect: &str, input: StringFunc) -> Result<(), serde_json::Error> { + let v = serde_json::to_string(&input)?; + assert_eq!(v.as_str(), expect); + Ok(()) + } +} diff --git a/iam/src/policy/id.rs b/iam/src/policy/id.rs new file mode 100644 index 00000000..957b3dd9 --- /dev/null +++ b/iam/src/policy/id.rs @@ -0,0 +1,29 @@ +use std::ops::Deref; + +use serde::{Deserialize, Serialize}; + +use super::{Error, Validator}; + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct ID(pub String); + +impl Validator for ID { + /// if id is a valid utf string, then it is valid. + fn is_valid(&self) -> Result<(), Error> { + Ok(()) + } +} + +impl From for ID { + fn from(value: T) -> Self { + Self(value.to_string()) + } +} + +impl Deref for ID { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/iam/src/policy/policy.rs b/iam/src/policy/policy.rs new file mode 100644 index 00000000..28340c88 --- /dev/null +++ b/iam/src/policy/policy.rs @@ -0,0 +1,235 @@ +use serde::{Deserialize, Serialize}; + +use super::{Args, Effect, Error, Statement, Validator, DEFAULT_VERSION, ID}; + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct Policy { + pub id: ID, + pub version: String, + pub statements: Vec, +} + +impl Policy { + pub fn is_allowed(&self, args: &Args) -> bool { + for statement in self.statements.iter().filter(|s| matches!(s.effect, Effect::Deny)) { + if !statement.is_allowed(args) { + return false; + } + } + + if args.deny_only || args.is_owner { + return true; + } + + for statement in self.statements.iter().filter(|s| matches!(s.effect, Effect::Allow)) { + if statement.is_allowed(args) { + return false; + } + } + + false + } +} + +impl Validator for Policy { + fn is_valid(&self) -> Result<(), Error> { + if !self.id.is_empty() && !self.id.eq(DEFAULT_VERSION) { + return Err(Error::InvalidVersion(self.id.0.clone())); + } + + for statement in self.statements.iter() { + statement.is_valid()?; + } + + Ok(()) + } +} + +pub mod default { + use std::{collections::HashSet, sync::LazyLock}; + + use crate::policy::{ + action::{Action, AdminAction, KmsAction, S3Action}, + resource::Resource, + ActionSet, Effect, Functions, ResourceSet, Statement, DEFAULT_VERSION, + }; + + use super::Policy; + + pub const DEFAULT_POLICIES: LazyLock<[(&'static str, Policy); 6]> = LazyLock::new(|| { + [ + ( + "readwrite", + Policy { + id: "".into(), + version: DEFAULT_VERSION.into(), + statements: vec![Statement { + sid: "".into(), + effect: Effect::Allow, + actions: ActionSet({ + let mut hash_set = HashSet::new(); + hash_set.insert(Action::S3Action(S3Action::AllActions)); + hash_set + }), + not_actions: ActionSet(Default::default()), + resoures: ResourceSet({ + let mut hash_set = HashSet::new(); + hash_set.insert(Resource::S3("*".into())); + hash_set + }), + conditions: Functions(vec![]), + }], + }, + ), + ( + "readonly", + Policy { + id: "".into(), + version: DEFAULT_VERSION.into(), + statements: vec![Statement { + sid: "".into(), + effect: Effect::Allow, + actions: ActionSet({ + let mut hash_set = HashSet::new(); + hash_set.insert(Action::S3Action(S3Action::GetBucketLocationAction)); + hash_set.insert(Action::S3Action(S3Action::GetObjectAction)); + hash_set + }), + not_actions: ActionSet(Default::default()), + resoures: ResourceSet({ + let mut hash_set = HashSet::new(); + hash_set.insert(Resource::S3("*".into())); + hash_set + }), + conditions: Functions(vec![]), + }], + }, + ), + ( + "writeonly", + Policy { + id: "".into(), + version: DEFAULT_VERSION.into(), + statements: vec![Statement { + sid: "".into(), + effect: Effect::Allow, + actions: ActionSet({ + let mut hash_set = HashSet::new(); + hash_set.insert(Action::S3Action(S3Action::PutObjectAction)); + hash_set + }), + not_actions: ActionSet(Default::default()), + resoures: ResourceSet({ + let mut hash_set = HashSet::new(); + hash_set.insert(Resource::S3("*".into())); + hash_set + }), + conditions: Functions(vec![]), + }], + }, + ), + ( + "writeonly", + Policy { + id: "".into(), + version: DEFAULT_VERSION.into(), + statements: vec![Statement { + sid: "".into(), + effect: Effect::Allow, + actions: ActionSet({ + let mut hash_set = HashSet::new(); + hash_set.insert(Action::S3Action(S3Action::PutObjectAction)); + hash_set + }), + not_actions: ActionSet(Default::default()), + resoures: ResourceSet({ + let mut hash_set = HashSet::new(); + hash_set.insert(Resource::S3("*".into())); + hash_set + }), + conditions: Functions(vec![]), + }], + }, + ), + ( + "diagnostics", + Policy { + id: "".into(), + version: DEFAULT_VERSION.into(), + statements: vec![Statement { + sid: "".into(), + effect: Effect::Allow, + actions: ActionSet({ + let mut hash_set = HashSet::new(); + hash_set.insert(Action::AdminAction(AdminAction::ProfilingAdminAction)); + hash_set.insert(Action::AdminAction(AdminAction::TraceAdminAction)); + hash_set.insert(Action::AdminAction(AdminAction::ConsoleLogAdminAction)); + hash_set.insert(Action::AdminAction(AdminAction::ServerInfoAdminAction)); + hash_set.insert(Action::AdminAction(AdminAction::TopLocksAdminAction)); + hash_set.insert(Action::AdminAction(AdminAction::HealthInfoAdminAction)); + hash_set.insert(Action::AdminAction(AdminAction::PrometheusAdminAction)); + hash_set.insert(Action::AdminAction(AdminAction::BandwidthMonitorAction)); + hash_set + }), + not_actions: ActionSet(Default::default()), + resoures: ResourceSet({ + let mut hash_set = HashSet::new(); + hash_set.insert(Resource::S3("*".into())); + hash_set + }), + conditions: Functions(vec![]), + }], + }, + ), + ( + "consoleAdmin", + Policy { + id: "".into(), + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement { + sid: "".into(), + effect: Effect::Allow, + actions: ActionSet({ + let mut hash_set = HashSet::new(); + hash_set.insert(Action::AdminAction(AdminAction::AllActions)); + hash_set + }), + not_actions: ActionSet(Default::default()), + resoures: ResourceSet(HashSet::new()), + conditions: Functions(vec![]), + }, + Statement { + sid: "".into(), + effect: Effect::Allow, + actions: ActionSet({ + let mut hash_set = HashSet::new(); + hash_set.insert(Action::KmsAction(KmsAction::AllActions)); + hash_set + }), + not_actions: ActionSet(Default::default()), + resoures: ResourceSet(HashSet::new()), + conditions: Functions(vec![]), + }, + Statement { + sid: "".into(), + effect: Effect::Allow, + actions: ActionSet({ + let mut hash_set = HashSet::new(); + hash_set.insert(Action::S3Action(S3Action::AllActions)); + hash_set + }), + not_actions: ActionSet(Default::default()), + resoures: ResourceSet({ + let mut hash_set = HashSet::new(); + hash_set.insert(Resource::S3("*".into())); + hash_set + }), + conditions: Functions(vec![]), + }, + ], + }, + ), + ] + }); +} diff --git a/iam/src/policy/resource.rs b/iam/src/policy/resource.rs new file mode 100644 index 00000000..91c77497 --- /dev/null +++ b/iam/src/policy/resource.rs @@ -0,0 +1,118 @@ +use std::{ + collections::{HashMap, HashSet}, + hash::Hash, + ops::Deref, +}; + +use serde::{Deserialize, Serialize}; + +use super::{ + function::key_name::KeyName, + utils::{path, wildcard}, + Error, Validator, +}; + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct ResourceSet(pub HashSet); + +impl ResourceSet { + pub fn is_match(&self, resource: &str, conditons: &HashMap>) -> bool { + for re in self.0.iter() { + if re.is_match(resource, conditons) { + return true; + } + } + + false + } +} + +impl Deref for ResourceSet { + type Target = HashSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Validator for ResourceSet { + fn is_valid(&self) -> Result<(), Error> { + for resource in self.0.iter() { + resource.is_valid()?; + } + + Ok(()) + } +} + +#[derive(Hash, Eq, PartialEq, Serialize, Deserialize, Clone)] +pub enum Resource { + S3(String), + Kms(String), +} + +impl Resource { + pub const S3_PREFIX: &str = "arn:aws:s3:::"; + + pub fn is_match(&self, resource: &str, conditons: &HashMap>) -> bool { + let mut pattern = match self { + Resource::S3(s) => s.to_owned(), + Resource::Kms(s) => s.to_owned(), + }; + + if !conditons.is_empty() { + for key in KeyName::COMMON_KEYS { + if let Some(rvalue) = conditons.get(key.name()) { + if matches!(rvalue.first().map(|c| !c.is_empty()), Some(true)) { + pattern = pattern.replace(key.name(), &rvalue[0]); + } + } + } + } + + let cp = path::clean(resource); + if cp != "." && cp == pattern.as_str() { + return true; + } + + wildcard::is_match(pattern, resource) + } +} + +impl TryFrom<&str> for Resource { + type Error = Error; + fn try_from(value: &str) -> Result { + let resource = if value.starts_with(Self::S3_PREFIX) { + Resource::S3(value[Self::S3_PREFIX.len() + 1..].into()) + } else { + return Err(Error::InvalidResource("unknown".into(), value.into())); + }; + + resource.is_valid()?; + Ok(resource) + } +} + +impl Validator for Resource { + fn is_valid(&self) -> Result<(), Error> { + match self { + Self::S3(pattern) => { + if pattern.is_empty() || pattern.starts_with('/') { + return Err(Error::InvalidResource("s3".into(), pattern.into())); + } + } + Self::Kms(pattern) => { + if pattern.is_empty() + || pattern + .char_indices() + .find(|&(_, c)| c == '/' || c == '\\' || c == '.') + .map(|(i, _)| i) + .is_some() + { + return Err(Error::InvalidResource("kms".into(), pattern.into())); + } + } + } + Ok(()) + } +} diff --git a/iam/src/policy/statement.rs b/iam/src/policy/statement.rs new file mode 100644 index 00000000..04b1e7d4 --- /dev/null +++ b/iam/src/policy/statement.rs @@ -0,0 +1,102 @@ +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +use super::{action::Action, ActionSet, Args, Effect, Error, Functions, ResourceSet, Validator, ID}; + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct Statement { + pub sid: ID, + pub effect: Effect, + pub actions: ActionSet, + pub not_actions: ActionSet, + pub resoures: ResourceSet, + pub conditions: Functions, +} + +impl Statement { + fn is_kms(&self) -> bool { + for act in self.actions.iter() { + if matches!(act, Action::KmsAction(_)) { + return true; + } + } + + false + } + + fn is_admin(&self) -> bool { + for act in self.actions.iter() { + if matches!(act, Action::AdminAction(_)) { + return true; + } + } + + false + } + + fn is_sts(&self) -> bool { + for act in self.actions.iter() { + if matches!(act, Action::StsAction(_)) { + return true; + } + } + + false + } + + pub fn is_allowed(&self, args: &Args) -> bool { + let check = 'c: { + if (!self.actions.is_match(&args.action) && !self.actions.is_empty()) || self.not_actions.is_match(&args.action) { + break 'c false; + } + + let mut resource = String::from(args.bucket); + if !args.object.is_empty() { + if !args.object.starts_with('/') { + resource.push('/'); + } + + resource.push_str(args.object); + } else { + resource.push('/'); + } + + if self.is_kms() { + if resource == "/" || self.resoures.is_empty() { + break 'c self.conditions.evaluate(&args.conditions); + } + } + + if !self.resoures.is_match(&resource, &args.conditions) && !self.is_admin() && !self.is_sts() { + break 'c false; + } + + self.conditions.evaluate(&args.conditions) + }; + + self.effect.is_allowed(check) + } +} + +impl Validator for Statement { + fn is_valid(&self) -> Result<(), Error> { + self.effect.is_valid()?; + // check sid + self.sid.is_valid()?; + + if self.actions.is_empty() || self.not_actions.is_empty() { + return Err(Error::NonAction); + } + + if self.resoures.is_empty() { + return Err(Error::NonResource); + } + + self.actions.is_valid()?; + self.not_actions.is_valid()?; + self.resoures.is_valid()?; + + Ok(()) + } +} diff --git a/iam/src/policy/utils.rs b/iam/src/policy/utils.rs new file mode 100644 index 00000000..298d2586 --- /dev/null +++ b/iam/src/policy/utils.rs @@ -0,0 +1,87 @@ +use std::collections::HashMap; + +use serde_json::Value; + +pub mod path; +pub mod wildcard; + +pub fn get_values_from_claims(claim: &HashMap, chaim_name: &str) -> (Vec, bool) { + let mut result = vec![]; + let Some(pname) = claim.get(chaim_name) else { + return (result, false); + }; + + let mut func = |pname_str: &str| { + for s in pname_str.split(',').map(str::trim) { + if s.is_empty() { + continue; + } + result.push(s.to_owned()); + } + }; + + if let Some(arrays) = pname.as_array() { + for array in arrays { + let Some(pname_str) = array.as_str() else { + continue; + }; + + func(pname_str); + } + } else { + let Some(pname_str) = pname.as_str() else { + return (result, false); + }; + + func(pname_str); + } + + (result, true) +} + +pub fn split_path(path: &str, second_index: bool) -> (&str, &str) { + let index = if second_index { + let Some(first) = path.find('/') else { + return (path, ""); + }; + + let Some(second) = &(path[first + 1..]).find('/') else { + return (path, ""); + }; + + Some(first + second + 1) + } else { + path.find('/') + }; + + let Some(index) = index else { + return (path, ""); + }; + + (&path[..index + 1], &path[index + 1..]) +} + +#[cfg(test)] +mod tests { + use super::split_path; + + #[test_case::test_case("format.json", false => ("format.json", ""))] + #[test_case::test_case("users/tester.json", false => ("users/", "tester.json"))] + #[test_case::test_case("groups/test/group.json", false => ("groups/", "test/group.json"))] + #[test_case::test_case("policydb/groups/testgroup.json", true => ("policydb/groups/", "testgroup.json"))] + #[test_case::test_case( + "policydb/sts-users/uid=slash/user,ou=people,ou=swengg,dc=min,dc=io.json", true => + ("policydb/sts-users/", "uid=slash/user,ou=people,ou=swengg,dc=min,dc=io.json")) + ] + #[test_case::test_case( + "policydb/sts-users/uid=slash/user/twice,ou=people,ou=swengg,dc=min,dc=io.json", true => + ("policydb/sts-users/", "uid=slash/user/twice,ou=people,ou=swengg,dc=min,dc=io.json")) + ] + #[test_case::test_case( + "policydb/groups/cn=project/d,ou=groups,ou=swengg,dc=min,dc=io.json", true => + ("policydb/groups/", "cn=project/d,ou=groups,ou=swengg,dc=min,dc=io.json")) + ] + fn test_split_path(path: &str, second_index: bool) -> (&str, &str) { + split_path(path, second_index) + } +} diff --git a/iam/src/policy/utils/path.rs b/iam/src/policy/utils/path.rs new file mode 100644 index 00000000..a6807ef3 --- /dev/null +++ b/iam/src/policy/utils/path.rs @@ -0,0 +1,141 @@ +use std::{fmt::Write, usize}; + +struct LazyBuf<'a> { + s: &'a str, + buf: Option>, + w: usize, +} + +impl<'a> LazyBuf<'a> { + pub fn new(s: &'a str) -> Self { + Self { s, buf: None, w: 0 } + } + + fn index(&self, i: usize) -> u8 { + self.buf.as_ref().map(|x| x[i]).unwrap_or_else(|| self.s.as_bytes()[i]) + } + + fn append(&mut self, c: u8) { + if self.buf.is_none() { + if self.w < self.s.len() && self.s.as_bytes()[self.w] == c { + self.w += 1; + return; + } + self.buf = Some({ + let mut buf = vec![0u8; self.s.len()]; + buf[..self.w].copy_from_slice(&self.s.as_bytes()[..self.w]); + buf + }); + } + + self.buf.as_mut().unwrap()[self.w] = c; + self.w += 1; + } + + fn string(&self) -> String { + match self.buf { + Some(ref s) => String::from_utf8_lossy(&s[..self.w]).to_string(), + None => String::from_utf8_lossy(&self.s.as_bytes()[..self.w]).to_string(), + } + } +} + +/// copy from golang(path.Clean) +pub fn clean(path: &str) -> String { + if path.is_empty() { + return ".".into(); + } + + let p = path.as_bytes(); + let (rooted, n, mut out, mut r, mut dotdot) = (p[0] == b'/', path.len(), LazyBuf::new(path), 0, 0); + + if rooted { + out.append(b'/'); + r = 1; + dotdot = 1; + } + + while r < n { + if p[r] == b'/' || (p[r] == b'.' && (r + 1 == n || p[r + 1] == b'/')) { + r += 1; + } else if p[r] == b'.' && p[r + 1] == b'.' && (r + 2 == n || p[r + 2] == b'/') { + r += 2; + if out.w > dotdot { + out.w -= 1; + + while out.w > dotdot && out.index(out.w) != b'/' { + out.w -= 1; + } + } else if !rooted { + if out.w > 0 { + out.append(b'/'); + } + + out.append(b'.'); + out.append(b'.'); + dotdot = out.w; + } + } else { + if rooted && out.w != 1 || !rooted && out.w != 0 { + out.append(b'/'); + } + + while r < n && p[r] != b'/' { + out.append(p[r]); + r += 1; + } + } + } + + if out.w == 0 { + ".".into() + } else { + out.string() + } +} + +#[cfg(test)] +mod tests { + use super::clean; + + #[test_case::test_case("", "."; "1")] + #[test_case::test_case("abc", "abc"; "2")] + #[test_case::test_case("abc/def", "abc/def"; "3")] + #[test_case::test_case("a/b/c", "a/b/c"; "4")] + #[test_case::test_case(".", "."; "5")] + #[test_case::test_case("..", ".."; "6")] + #[test_case::test_case("../..", "../.."; "7")] + #[test_case::test_case("../../abc", "../../abc"; "8")] + #[test_case::test_case("/abc", "/abc"; "9")] + #[test_case::test_case("/", "/"; "10")] + #[test_case::test_case("abc/", "abc"; "11")] + #[test_case::test_case("abc/def/", "abc/def"; "12")] + #[test_case::test_case("a/b/c/", "a/b/c"; "13")] + #[test_case::test_case("./", "."; "14")] + #[test_case::test_case("../", ".."; "15")] + #[test_case::test_case("../../", "../.."; "16")] + #[test_case::test_case("/abc/", "/abc"; "17")] + #[test_case::test_case("abc//def//ghi", "abc/def/ghi"; "18")] + #[test_case::test_case("//abc", "/abc"; "19")] + #[test_case::test_case("///abc", "/abc"; "20")] + #[test_case::test_case("//abc//", "/abc"; "21")] + #[test_case::test_case("abc//", "abc"; "22")] + #[test_case::test_case("abc/./def", "abc/def"; "23")] + #[test_case::test_case("/./abc/def", "/abc/def"; "24")] + #[test_case::test_case("abc/.", "abc"; "25")] + #[test_case::test_case("abc/def/ghi/../jkl", "abc/def/jkl"; "26")] + #[test_case::test_case("abc/def/../ghi/../jkl", "abc/jkl"; "27")] + #[test_case::test_case("abc/def/..", "abc"; "28")] + #[test_case::test_case("abc/def/../..", "."; "29")] + #[test_case::test_case("/abc/def/../..", "/"; "30")] + #[test_case::test_case("abc/def/../../..", ".."; "31")] + #[test_case::test_case("/abc/def/../../..", "/"; "32")] + #[test_case::test_case("abc/def/../../../ghi/jkl/../../../mno", "../../mno"; "33")] + #[test_case::test_case("abc/./../def", "def"; "34")] + #[test_case::test_case("abc//./../def", "def"; "35")] + #[test_case::test_case("abc/../../././../def", "../../def"; "36")] + fn test_clean(path: &str, result: &str) { + assert_eq!(clean(path), result.to_owned()); + assert_eq!(clean(result), result.to_owned()); + } +} diff --git a/iam/src/policy/utils/wildcard.rs b/iam/src/policy/utils/wildcard.rs new file mode 100644 index 00000000..5e0070aa --- /dev/null +++ b/iam/src/policy/utils/wildcard.rs @@ -0,0 +1,189 @@ +pub fn is_simple_match(pattern: P, name: N) -> bool +where + P: AsRef, + N: AsRef, +{ + inner_match(pattern, name, true) +} + +pub fn is_match(pattern: P, name: N) -> bool +where + P: AsRef, + N: AsRef, +{ + inner_match(pattern, name, false) +} + +pub fn is_match_as_pattern_prefix(pattern: P, text: N) -> bool +where + P: AsRef, + N: AsRef, +{ + let (mut p, mut t) = (pattern.as_ref().as_bytes().into_iter(), text.as_ref().as_bytes().into_iter()); + + while let (Some(&x), Some(&y)) = (p.next(), t.next()) { + if x == b'*' { + return true; + } + + if x == b'?' { + continue; + } + + if x != y { + return false; + } + } + + text.as_ref().len() <= pattern.as_ref().len() +} + +fn inner_match(pattern: impl AsRef, name: impl AsRef, simple: bool) -> bool { + let (pattern, name) = (pattern.as_ref(), name.as_ref()); + + if pattern.is_empty() { + return pattern == name; + } + + if pattern == "*" { + return true; + } + + deep_match(name.as_bytes(), pattern.as_bytes(), simple) +} + +fn deep_match(mut name: &[u8], mut pattern: &[u8], simple: bool) -> bool { + while !pattern.is_empty() { + match pattern[0] { + b'?' => { + if name.is_empty() { + return simple; + } + } + + b'*' => { + return pattern.len() == 1 + || deep_match(name, &pattern[1..], simple) + || (!name.is_empty() && deep_match(&name[1..], pattern, simple)); + } + + _ => { + if name.is_empty() || name[0] != pattern[0] { + return false; + } + } + } + + name = &name[1..]; + pattern = &pattern[1..]; + } + + name.is_empty() && pattern.is_empty() +} + +#[cfg(test)] +mod tests { + use super::{is_match, is_match_as_pattern_prefix, is_simple_match}; + + #[test_case::test_case("*", "s3:GetObject" => true ; "1")] + #[test_case::test_case("", "s3:GetObject" => false ; "2")] + #[test_case::test_case("", "" => true; "3")] + #[test_case::test_case("s3:*", "s3:ListMultipartUploadParts" => true; "4")] + #[test_case::test_case("s3:ListBucketMultipartUploads", "s3:ListBucket" => false; "5")] + #[test_case::test_case("s3:ListBucket", "s3:ListBucket" => true; "6")] + #[test_case::test_case("s3:ListBucketMultipartUploads", "s3:ListBucketMultipartUploads" => true; "7")] + #[test_case::test_case("my-bucket/oo*", "my-bucket/oo" => true; "8")] + #[test_case::test_case("my-bucket/In*", "my-bucket/India/Karnataka/" => true; "9")] + #[test_case::test_case("my-bucket/In*", "my-bucket/Karnataka/India/" => false; "10")] + #[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Ban" => true; "11")] + #[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Ban/Ban/Ban/Ban/Ban" => true; "12")] + #[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Area1/Area2/Area3/Ban" => true; "13")] + #[test_case::test_case( "my-bucket/In*/Ka*/Ba", "my-bucket/India/State1/State2/Karnataka/Area1/Area2/Area3/Ban" => ignore["will fail"] true; "14")] + #[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Bangalore" => false; "15")] + #[test_case::test_case("my-bucket/In*/Ka*/Ban*", "my-bucket/India/Karnataka/Bangalore" => true; "16")] + #[test_case::test_case("my-bucket/*", "my-bucket/India" => true; "17")] + #[test_case::test_case("my-bucket/oo*", "my-bucket/odo" => false; "18")] + #[test_case::test_case("my-bucket?/abc*", "mybucket/abc" => false; "19")] + #[test_case::test_case("my-bucket?/abc*", "my-bucket1/abc" => true; "20")] + #[test_case::test_case("my-?-bucket/abc*", "my--bucket/abc" => false; "21")] + #[test_case::test_case("my-?-bucket/abc*", "my-1-bucket/abc" => true; "22")] + #[test_case::test_case("my-?-bucket/abc*", "my-k-bucket/abc" => true; "23")] + #[test_case::test_case("my??bucket/abc*", "mybucket/abc" => false; "24")] + #[test_case::test_case("my??bucket/abc*", "my4abucket/abc" => true; "25")] + #[test_case::test_case("my-bucket?abc*", "my-bucket/abc" => true; "26")] + #[test_case::test_case("my-bucket/abc?efg", "my-bucket/abcdefg" => true; "27")] + #[test_case::test_case("my-bucket/abc?efg", "my-bucket/abc/efg" => true; "28")] + #[test_case::test_case("my-bucket/abc????", "my-bucket/abcde" => false; "29")] + #[test_case::test_case("my-bucket/abc????", "my-bucket/abcdefg" => true; "30")] + #[test_case::test_case("my-bucket/abc?", "my-bucket/abc" => false; "31")] + #[test_case::test_case("my-bucket/abc?", "my-bucket/abcd" => true; "32")] + #[test_case::test_case("my-bucket/abc?", "my-bucket/abcde" => false; "33")] + #[test_case::test_case("my-bucket/mnop*?", "my-bucket/mnop" => false; "34")] + #[test_case::test_case("my-bucket/mnop*?", "my-bucket/mnopqrst/mnopqr" => true; "35")] + #[test_case::test_case("my-bucket/mnop*?", "my-bucket/mnopqrst/mnopqrs" => true; "36")] + #[test_case::test_case("my-bucket/mnop*?", "my-bucket/mnop" => false; "37")] + #[test_case::test_case("my-bucket/mnop*?", "my-bucket/mnopq" => true; "38")] + #[test_case::test_case("my-bucket/mnop*?", "my-bucket/mnopqr" => true; "39")] + #[test_case::test_case("my-bucket/mnop*?and", "my-bucket/mnopqand" => true; "40")] + #[test_case::test_case("my-bucket/mnop*?and", "my-bucket/mnopand" => false; "41")] + #[test_case::test_case("my-bucket/mnop*?and", "my-bucket/mnopqand" => true; "42")] + #[test_case::test_case("my-bucket/mnop*?", "my-bucket/mn" => false; "43")] + #[test_case::test_case("my-bucket/mnop*?", "my-bucket/mnopqrst/mnopqrs" => true; "44")] + #[test_case::test_case("my-bucket/mnop*??", "my-bucket/mnopqrst" => true; "45")] + #[test_case::test_case("my-bucket/mnop*qrst", "my-bucket/mnopabcdegqrst" => true; "46")] + #[test_case::test_case("my-bucket/mnop*?and", "my-bucket/mnopqand" => true; "47")] + #[test_case::test_case("my-bucket/mnop*?and", "my-bucket/mnopand" => false; "48")] + #[test_case::test_case("my-bucket/mnop*?and?", "my-bucket/mnopqanda" => true; "49")] + #[test_case::test_case("my-bucket/mnop*?and", "my-bucket/mnopqanda" => false; "50")] + #[test_case::test_case("my-?-bucket/abc*", "my-bucket/mnopqanda" => false; "51")] + #[test_case::test_case("a?", "a" => false; "52")] + fn test_is_match(pattern: &str, text: &str) -> bool { + is_match(pattern, text) + } + + #[test_case::test_case("*", "s3:GetObject" => true ; "1")] + #[test_case::test_case("", "s3:GetObject" => false ; "2")] + #[test_case::test_case("", "" => true ; "3")] + #[test_case::test_case("s3:*", "s3:ListMultipartUploadParts" => true ; "4")] + #[test_case::test_case("s3:ListBucketMultipartUploads", "s3:ListBucket" => false ; "5")] + #[test_case::test_case("s3:ListBucket", "s3:ListBucket" => true ; "6")] + #[test_case::test_case("s3:ListBucketMultipartUploads", "s3:ListBucketMultipartUploads" => true ; "7")] + #[test_case::test_case("my-bucket/oo*", "my-bucket/oo" => true ; "8")] + #[test_case::test_case("my-bucket/In*", "my-bucket/India/Karnataka/" => true ; "9")] + #[test_case::test_case("my-bucket/In*", "my-bucket/Karnataka/India/" => false ; "10")] + #[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Ban" => true ; "11")] + #[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Ban/Ban/Ban/Ban/Ban" => true ; "12")] + #[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Area1/Area2/Area3/Ban" => true ; "13")] + #[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/State1/State2/Karnataka/Area1/Area2/Area3/Ban" => true ; "14")] + #[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Bangalore" => false ; "15")] + #[test_case::test_case("my-bucket/In*/Ka*/Ban*", "my-bucket/India/Karnataka/Bangalore" => true ; "16")] + #[test_case::test_case("my-bucket/*", "my-bucket/India" => true ; "17")] + #[test_case::test_case("my-bucket/oo*", "my-bucket/odo" => false ; "18")] + #[test_case::test_case("my-bucket/oo?*", "my-bucket/oo???" => true ; "19")] + #[test_case::test_case("my-bucket/oo??*", "my-bucket/odo" => false ; "20")] + #[test_case::test_case("?h?*", "?h?hello" => true ; "21")] + #[test_case::test_case("a?", "a" => true ; "22")] + fn test_is_simple_match(pattern: &str, text: &str) -> bool { + is_simple_match(pattern, text) + } + + #[test_case::test_case("", "" => true ; "1")] + #[test_case::test_case("a", "" => true ; "2")] + #[test_case::test_case("a", "b" => false ; "3")] + #[test_case::test_case("abc", "ab" => true ; "4")] + #[test_case::test_case("ab*", "ab" => true ; "5")] + #[test_case::test_case("abc*", "ab" => true ; "6")] + #[test_case::test_case("abc?", "ab" => true ; "7")] + #[test_case::test_case("abc*", "abd" => false ; "8")] + #[test_case::test_case("abc*c", "abcd" => true ; "9")] + #[test_case::test_case("ab*??d", "abxxc" => true ; "10")] + #[test_case::test_case("ab*??", "abxc" => true ; "11")] + #[test_case::test_case("ab??", "abxc" => true ; "12")] + #[test_case::test_case("ab??", "abx" => true ; "13")] + #[test_case::test_case("ab??d", "abcxd" => true ; "14")] + #[test_case::test_case("ab??d", "abcxdd" => false ; "15")] + #[test_case::test_case("", "b" => false ; "16")] + fn test_is_match_as_pattern_prefix(pattern: &str, text: &str) -> bool { + is_match_as_pattern_prefix(pattern, text) + } +} diff --git a/iam/src/service_type.rs b/iam/src/service_type.rs new file mode 100644 index 00000000..459cb08c --- /dev/null +++ b/iam/src/service_type.rs @@ -0,0 +1,20 @@ +use crate::Error; + +#[derive(PartialEq, Eq, Debug)] +pub enum ServiceType { + S3, + STS, +} + +impl TryFrom<&str> for ServiceType { + type Error = Error; + fn try_from(value: &str) -> Result { + let service_type = match value { + "s3" => Self::S3, + "sts" => Self::STS, + _ => return Err(Error::InvalidServiceType(value.to_owned())), + }; + + Ok(service_type) + } +} diff --git a/iam/src/store.rs b/iam/src/store.rs new file mode 100644 index 00000000..7944eeaf --- /dev/null +++ b/iam/src/store.rs @@ -0,0 +1,43 @@ +pub mod object; + +use std::collections::HashMap; + +use ecstore::store_api::ObjectInfo; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::{ + auth::UserIdentity, + cache::Cache, + policy::{PolicyDoc, UserType, DEFAULT_POLICIES}, +}; + +#[async_trait::async_trait] +pub trait Store: Clone + Send + Sync + 'static { + async fn load_iam_config(&self, path: impl AsRef + Send) -> crate::Result<(Item, ObjectInfo)> + where + Item: DeserializeOwned; + + async fn save_iam_config(&self, item: Item, path: impl AsRef + Send) -> crate::Result<()>; + + async fn load_all(&self, cache: &Cache) -> crate::Result<()>; + + fn get_default_policyes() -> HashMap { + DEFAULT_POLICIES + .iter() + .map(|(n, p)| { + ( + n.to_string(), + PolicyDoc { + version: 1, + policy: p.clone(), + ..Default::default() + }, + ) + }) + .collect() + } + + async fn load_users(&self, user_type: UserType) -> crate::Result>; + + async fn load_policy_docs(&self) -> crate::Result>; +} diff --git a/iam/src/store/object.rs b/iam/src/store/object.rs new file mode 100644 index 00000000..02f49324 --- /dev/null +++ b/iam/src/store/object.rs @@ -0,0 +1,345 @@ +use std::{collections::HashMap, path::Path, sync::Arc}; + +use ecstore::{ + config::error::is_not_found, + store::{ECStore, ListPathOptions}, + store_api::{HTTPRangeSpec, ObjectIO, ObjectInfo, ObjectOptions, PutObjReader}, + utils::path::dir, +}; +use futures::{future::try_join_all, SinkExt}; +use log::debug; +use serde::{de::DeserializeOwned, Serialize}; + +use super::Store; +use crate::{ + auth::UserIdentity, + cache::{Cache, CacheEntity, CacheInner}, + policy::{utils::split_path, MappedPolicy, PolicyDoc, UserType}, + Error, +}; + +#[derive(Clone)] +pub struct ObjectStore { + object_api: Arc, +} + +impl ObjectStore { + const BUCKET_NAME: &str = ".rustfs.sys"; + + pub fn new(object_api: Arc) -> Self { + Self { object_api } + } + + async fn list_iam_config_items(&self, prefix: &str, items: &[&str]) -> crate::Result> { + debug!("list iam config items, prefix: {prefix}"); + + // todo, 实现walk,使用walk + let mut futures = Vec::with_capacity(items.len()); + + for item in items { + let prefix = format!("{}{}", prefix, item); + futures.push(async move { + let items = self + .object_api + .list_path( + &ListPathOptions { + bucket: Self::BUCKET_NAME.into(), + prefix: prefix.clone(), + ..Default::default() + }, + "", + ) + .await; + + match items { + Ok(items) => Result::<_, crate::Error>::Ok(items.objects), + Err(e) if is_not_found(&e) => Result::<_, crate::Error>::Ok(vec![]), + Err(e) => Err(Error::StringError(format!("list {prefix} failed, err: {e:?}"))), + } + }); + } + + Ok(try_join_all(futures) + .await? + .into_iter() + .flat_map(|x| x.into_iter()) + .map(|x| x.name) + .collect()) + } + + async fn load_policy(&self, name: &str) -> crate::Result { + let (mut policy, object) = self + .load_iam_config::(&format!("config/iam/policies/{name}/policy.json")) + .await?; + + if policy.version == 0 { + policy.create_date = object.mod_time; + policy.update_date = object.mod_time; + } + + Ok(policy) + } + + async fn load_user_identity(&self, user_type: UserType, name: &str) -> crate::Result> { + let (mut user, _) = self + .load_iam_config::(&format!( + "config/iam/{base}{name}/identity.json", + base = user_type.prefix(), + name = name + )) + .await?; + + if user.credentials.is_expired() { + return Ok(None); + } + + if user.credentials.access_key.is_empty() { + user.credentials.access_key = name.to_owned(); + } + + // todo, 校验session token + + Ok(Some(user)) + } + + async fn load_mapped_policy(&self, user_type: UserType, name: &str, is_group: bool) -> crate::Result { + let (p, _) = self + .load_iam_config::(&format!("{base}{name}.json", base = user_type.prefix(), name = name)) + .await?; + + Ok(p) + } +} + +#[async_trait::async_trait] +impl Store for ObjectStore { + async fn load_iam_config(&self, path: impl AsRef + Send) -> crate::Result<(Item, ObjectInfo)> + where + Item: DeserializeOwned, + { + debug!("load iam config, path: {}", path.as_ref()); + let mut reader = self + .object_api + .get_object_reader( + Self::BUCKET_NAME, + path.as_ref(), + HTTPRangeSpec::nil(), + Default::default(), + &Default::default(), + ) + .await + .map_err(crate::Error::EcstoreError)?; + + let data = reader.read_all().await.map_err(crate::Error::EcstoreError)?; + // let data = crypto::decrypt_data(&[], &data)?; + + Ok(( + serde_json::from_slice(&data).map_err(|e| crate::Error::StringError(e.to_string()))?, + reader.object_info, + )) + } + + async fn save_iam_config(&self, item: Item, path: impl AsRef + Send) -> crate::Result<()> { + let data = serde_json::to_vec(&item).map_err(|e| crate::Error::StringError(e.to_string()))?; + // let data = crypto::encrypt_data(&[], &data)?; + + self.object_api + .put_object( + Self::BUCKET_NAME, + path.as_ref(), + &mut PutObjReader::from_vec(data), + &ObjectOptions { + max_parity: true, + ..Default::default() + }, + ) + .await + .map_err(crate::Error::EcstoreError)?; + + Ok(()) + } + + async fn load_policy_docs(&self) -> crate::Result> { + let paths = self.list_iam_config_items("config/iam/", &["policies/"]).await?; + + let mut result = Self::get_default_policyes(); + for path in paths { + let name = Path::new(&path).iter().rev().nth(0).unwrap(); + + let (mut policy_doc, object_info) = self + .load_iam_config::(format!("config/iam/policies/{}/policy.json", name.to_str().unwrap())) + .await?; + + if policy_doc.version == 0 { + policy_doc.create_date = object_info.mod_time.clone(); + policy_doc.update_date = object_info.mod_time.clone(); + } + + result.insert(name.to_str().unwrap().to_owned(), policy_doc); + } + + Ok(result) + } + + async fn load_users(&self, user_type: UserType) -> crate::Result> { + let paths = self.list_iam_config_items("config/iam/", &[user_type.prefix()]).await?; + + let mut result = HashMap::new(); + for path in paths { + let name = Path::new(&path).iter().rev().nth(0).unwrap(); + + let (mut user_identity, _) = self + .load_iam_config::(format!("config/iam/users/{}/identity.json", name.to_str().unwrap())) + .await?; + + if user_identity.credentials.is_expired() { + return Err(Error::NoSuchUser(name.to_str().unwrap().to_owned())); + } + + if user_identity.credentials.access_key.is_empty() { + user_identity.credentials.access_key = name.to_str().unwrap().to_owned(); + } + + // todo 解析 sts + + result.insert(name.to_str().unwrap().to_owned(), user_identity); + } + + Ok(result) + } + + /// load all and make a new cache. + async fn load_all(&self, cache: &Cache) -> crate::Result<()> { + let items = self + .list_iam_config_items( + "config/iam/", + &[ + "policydb/", + "policies/", + "groups/", + "policydb/users/", + "policydb/groups/", + "service-accounts/", + "policydb/sts-users/", + "sts", + ], + ) + .await?; + debug!("all iam items: {items:?}"); + + let (policy_docs, users, user_policies, sts_policies, sts_accounts) = ( + Arc::new(tokio::sync::Mutex::new(CacheEntity::new(Self::get_default_policyes()))), + Arc::new(tokio::sync::Mutex::new(CacheEntity::default())), + Arc::new(tokio::sync::Mutex::new(CacheEntity::default())), + Arc::new(tokio::sync::Mutex::new(CacheEntity::default())), + Arc::new(tokio::sync::Mutex::new(CacheEntity::default())), + ); + + // 一次读取32个元素 + let mut iter = items + .iter() + .map(|item| item.trim_start_matches("config/iam/")) + .map(|item| split_path(item, item.starts_with("policydb/"))) + .filter_map(|(list_key, trimmed_item)| { + debug!("list_key: {list_key}, trimmed_item: {trimmed_item}"); + if list_key == "format.json" { + return None; + } + + let (policy_docs, users, user_policies, sts_policies, sts_accounts) = ( + policy_docs.clone(), + users.clone(), + user_policies.clone(), + sts_policies.clone(), + sts_accounts.clone(), + ); + + Some(async move { + match list_key { + "policies/" => { + let name = dir(trimmed_item).trim_end_matches('/'); + let policy_doc = self.load_policy(name).await?; + policy_docs.lock().await.insert(name.to_owned(), policy_doc); + } + "users/" => { + let name = dir(trimmed_item); + if let Some(user) = self.load_user_identity(UserType::Reg, name).await? { + users.lock().await.insert(name.to_owned(), user); + }; + } + "groups/" => {} + "policydb/users/" | "policydb/groups/" => { + let name = trimmed_item.strip_suffix(".json").unwrap_or(trimmed_item); + let mapped_policy = self + .load_mapped_policy(UserType::Reg, name, list_key == "policydb/groups/") + .await?; + if !mapped_policy.policies.is_empty() { + user_policies.lock().await.insert(name.to_owned(), mapped_policy); + } + } + "service-accounts/" => { + let name = dir(trimmed_item).trim_end_matches('/'); + let Some(user) = self.load_user_identity(UserType::Svc, name).await? else { + return Ok(()); + }; + + let parent = user.credentials.parent_user.clone(); + + { + users.lock().await.insert(name.to_owned(), user); + } + + if users.lock().await.get(&parent).is_some() { + return Ok(()); + } + + match self.load_mapped_policy(UserType::Sts, parent.as_str(), false).await { + Ok(m) => sts_policies.lock().await.insert(name.to_owned(), m), + Err(Error::EcstoreError(e)) if is_not_found(&e) => return Ok(()), + Err(e) => return Err(e), + }; + } + "sts/" => { + let name = dir(trimmed_item); + if let Some(user) = self.load_user_identity(UserType::Sts, name).await? { + sts_accounts.lock().await.insert(name.to_owned(), user); + }; + } + "policydb/sts-users/" => { + let name = trimmed_item.strip_suffix(".json").unwrap_or(trimmed_item); + let mapped_policy = self.load_mapped_policy(UserType::Sts, name, false).await?; + if !mapped_policy.policies.is_empty() { + sts_policies.lock().await.insert(name.to_owned(), mapped_policy); + } + } + _ => {} + } + + crate::Result::Ok(()) + }) + }); + + let mut all_futures = Vec::with_capacity(32); + + while let Some(f) = iter.next() { + all_futures.push(f); + + if all_futures.len() == 32 { + try_join_all(all_futures).await?; + all_futures = Vec::with_capacity(32); + } + } + + if !all_futures.is_empty() { + try_join_all(all_futures).await?; + } + + Arc::into_inner(users).map(|x| cache.users.store(Arc::new(x.into_inner().update_load_time()))); + Arc::into_inner(policy_docs).map(|x| cache.policy_docs.store(Arc::new(x.into_inner().update_load_time()))); + Arc::into_inner(user_policies).map(|x| cache.user_policies.store(Arc::new(x.into_inner().update_load_time()))); + Arc::into_inner(sts_policies).map(|x| cache.sts_policies.store(Arc::new(x.into_inner().update_load_time()))); + Arc::into_inner(sts_accounts).map(|x| cache.sts_accounts.store(Arc::new(x.into_inner().update_load_time()))); + + Ok(()) + } +} diff --git a/iam/src/utils.rs b/iam/src/utils.rs new file mode 100644 index 00000000..30285d52 --- /dev/null +++ b/iam/src/utils.rs @@ -0,0 +1,62 @@ +use rand::{Rng, RngCore}; + +use crate::Error; + +pub fn gen_access_key(length: usize) -> crate::Result { + const ALPHA_NUMERIC_TABLE: [char; 36] = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + ]; + + if length < 3 { + return Err(Error::StringError("access key length is too short".into())); + } + + let mut result = String::with_capacity(length); + let mut rng = rand::thread_rng(); + + for _ in 0..length { + result.push(ALPHA_NUMERIC_TABLE[rng.gen_range(0..ALPHA_NUMERIC_TABLE.len())]); + } + + Ok(result) +} + +pub fn gen_secret_key(length: usize) -> crate::Result { + use base64_simd::URL_SAFE_NO_PAD; + + if length < 8 { + return Err(Error::StringError("secret key length is too short".into())); + } + let mut rng = rand::thread_rng(); + + let mut key = vec![0u8; URL_SAFE_NO_PAD.estimated_decoded_length(length)]; + rng.fill_bytes(&mut key); + + let encoded = URL_SAFE_NO_PAD.encode_to_string(&key); + let key_str = encoded.replace("/", "+"); + + Ok(key_str) +} + +#[cfg(test)] +mod tests { + use super::{gen_access_key, gen_secret_key}; + + #[test] + fn test_gen_access_key() { + let a = gen_access_key(10).unwrap(); + let b = gen_access_key(10).unwrap(); + + assert_eq!(a.len(), 10); + assert_eq!(b.len(), 10); + assert_ne!(a, b); + } + + #[test] + fn test_gen_secret_key() { + let a = gen_secret_key(10).unwrap(); + let b = gen_secret_key(10).unwrap(); + assert_ne!(a, b); + } +} diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index ea9aa4c0..9b51bad3 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -37,7 +37,7 @@ s3s.workspace = true serde.workspace = true serde_json.workspace = true tracing.workspace = true -time = { workspace = true, features = ["parsing", "formatting"] } +time = { workspace = true, features = ["parsing", "formatting", "serde"] } tokio-util = { version = "0.7.12", features = ["io", "compat"] } tokio = { workspace = true, features = [ "rt-multi-thread", @@ -62,6 +62,8 @@ shadow-rs = "0.36.0" const-str = { version = "0.5.7", features = ["std", "proc"] } atoi = "2.0.0" serde_urlencoded = "0.7.1" +crypto = { path = "../crypto" } +iam = { path = "../iam" } [build-dependencies] prost-build.workspace = true diff --git a/rustfs/src/admin/handlers.rs b/rustfs/src/admin/handlers.rs index bd9f48f2..b355ac8e 100644 --- a/rustfs/src/admin/handlers.rs +++ b/rustfs/src/admin/handlers.rs @@ -34,6 +34,8 @@ use tokio::spawn; use tokio::sync::mpsc; use tracing::{info, warn}; +pub mod service_account; + #[derive(Deserialize, Debug, Default)] #[serde(rename_all = "PascalCase", default)] pub struct AssumeRoleRequest { diff --git a/rustfs/src/admin/handlers/service_account.rs b/rustfs/src/admin/handlers/service_account.rs new file mode 100644 index 00000000..47d6e8b1 --- /dev/null +++ b/rustfs/src/admin/handlers/service_account.rs @@ -0,0 +1,229 @@ +use std::collections::HashMap; + +use hyper::StatusCode; +use iam::{ + auth::CredentialsBuilder, + policy::{ + action::{Action, AdminAction::ListServiceAccountsAdminAction}, + Args, + }, +}; +use matchit::Params; +use s3s::{s3_error, Body, S3Request, S3Response, S3Result}; +use tracing::{debug, warn}; + +use crate::admin::models::service_account::{AddServiceAccountReq, AddServiceAccountResp, Credentials, InfoServiceAccountResp}; +use crate::admin::router::Operation; + +pub struct AddServiceAccount {} +#[async_trait::async_trait] +impl Operation for AddServiceAccount { + async fn call(&self, mut req: S3Request, _params: Params<'_, '_>) -> S3Result> { + warn!("handle AddServiceAccount, req: {req:?}"); + + let Some(cred) = req.credentials else { return Err(s3_error!(InvalidRequest, "get cred failed")) }; + let is_owner = true; // 先按true处理,后期根据请求决定。 + let body = req.input.store_all_unlimited().await.unwrap(); + let body = crypto::decrypt_data(cred.secret_key.expose().as_bytes(), &body[..]) + .map_err(|_| s3_error!(InternalError, "encrypt data failed"))?; + + debug!("body: {:?}", String::from_utf8_lossy(&body)); + + let mut create_req: AddServiceAccountReq = + serde_json::from_slice(&body[..]).map_err(|e| s3_error!(InvalidRequest, "unmarshal body failed, e: {:?}", e))?; + + create_req.expiration = create_req.expiration.and_then(|expire| expire.replace_millisecond(0).ok()); + + if create_req.access_key.trim().len() != create_req.access_key.len() { + return Err(s3_error!(InvalidRequest, "access key has spaces")); + } + + // 校验合法性, Name, Expiration, Description + let target_user = create_req.target_user.as_ref().unwrap_or(&cred.access_key); + let deny_only = true; + + // todo 校验权限 + + // if !iam::is_allowed(Args { + // account: &cred.access_key, + // groups: &[], + // action: Action::AdminAction(AdminAction::CreateServiceAccountAdminAction), + // bucket: "", + // conditions: &HashMap::new(), + // is_owner, + // object: "", + // claims: &HashMap::new(), + // deny_only, + // }) + // .await + // .unwrap_or(false) + // { + // return Err(s3_error!(AccessDenied)); + // } + // + + let cred = CredentialsBuilder::new() + .parent_user(match create_req.target_user { + Some(target_user) => target_user, + _ => cred.access_key, + }) + .access_key(create_req.access_key) + .secret_key(create_req.secret_key) + .description(create_req.description) + .expiration(create_req.expiration) + .session_policy({ + match create_req.policy { + Some(p) if !p.is_empty() => { + Some(serde_json::from_slice(&p).map_err(|_| s3_error!(InvalidRequest, "invalid policy"))?) + } + _ => None, + } + }) + .name(create_req.name) + .try_build() + .map_err(|e| s3_error!(InvalidRequest, "build cred failed, err: {:?}", e))?; + + let resp = serde_json::to_vec(&AddServiceAccountResp { + credentials: Credentials { + access_key: &cred.access_key, + secret_key: &cred.secret_key, + session_token: None, + expiration: cred.expiration, + }, + }) + .unwrap() + .into(); + + iam::add_service_account(cred).await.map_err(|e| { + debug!("add cred failed: {e:?}"); + s3_error!(InternalError, "add cred failed") + })?; + + Ok(S3Response::new((StatusCode::OK, resp))) + } +} + +pub struct UpdateServiceAccount {} +#[async_trait::async_trait] +impl Operation for UpdateServiceAccount { + async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + warn!("handle UpdateServiceAccount"); + + let Some(cred) = req.credentials else { return Err(s3_error!(InvalidRequest, "get cred failed")) }; + + // return Err(s3_error!(NotImplemented)); + // + + todo!() + } +} + +pub struct InfoServiceAccount {} +#[async_trait::async_trait] +impl Operation for InfoServiceAccount { + async fn call(&self, req: S3Request, params: Params<'_, '_>) -> S3Result> { + warn!("handle InfoServiceAccount"); + + let Some(cred) = req.credentials else { return Err(s3_error!(InvalidRequest, "get cred failed")) }; + + //accessKey + let Some(ak) = req.uri.query().and_then(|x| { + for mut x in x.split('&').map(|x| x.split('=')) { + let Some(key) = x.next() else { + continue; + }; + + if key != "accessKey" { + continue; + } + + let Some(value) = x.next() else { + continue; + }; + + return Some(value); + } + + None + }) else { + return Err(s3_error!(InvalidRequest, "access key is not exist")); + }; + + let (sa, sp) = iam::get_service_account(ak).await.map_err(|e| { + debug!("get service account failed, err: {e:?}"); + s3_error!(InternalError) + })?; + + if !iam::is_allowed(Args { + account: &sa.access_key, + groups: &sa.groups.unwrap_or_default()[..], + action: Action::AdminAction(ListServiceAccountsAdminAction), + bucket: "", + conditions: &HashMap::new(), + is_owner: true, + object: "", + claims: &HashMap::new(), + deny_only: false, + }) + .await + .map_err(|_| s3_error!(InternalError))? + { + let req_user = &cred.access_key; + if req_user != &sa.parent_user { + return Err(s3_error!(AccessDenied)); + } + } + + // let implied_policy = sp.version.is_empty() && sp.statements.is_empty(); + // let sva = if implied_policy { + // sp + // } else { + // // 这里使用 + // todo!(); + // }; + + let body = serde_json::to_vec(&InfoServiceAccountResp { + parent_user: sa.parent_user, + account_status: sa.status, + implied_policy: true, + // policy: serde_json::to_string_pretty(&sva).map_err(|_| s3_error!(InternalError, "json marshal failed"))?, + policy: "".into(), + name: sa.name.unwrap_or_default(), + description: sa.description.unwrap_or_default(), + expiration: sa.expiration, + }) + .map_err(|_| s3_error!(InternalError, "json marshal failed"))?; + + Ok(S3Response::new(( + StatusCode::OK, + crypto::encrypt_data(cred.access_key.as_bytes(), &body[..]) + .map_err(|_| s3_error!(InternalError, "encrypt data failed"))? + .into(), + ))) + } +} + +pub struct ListServiceAccount {} +#[async_trait::async_trait] +impl Operation for ListServiceAccount { + async fn call(&self, req: S3Request, params: Params<'_, '_>) -> S3Result> { + warn!("handle ListServiceAccount"); + todo!() + } +} + +pub struct DeleteServiceAccount {} +#[async_trait::async_trait] +impl Operation for DeleteServiceAccount { + async fn call(&self, req: S3Request, params: Params<'_, '_>) -> S3Result> { + warn!("handle DeleteServiceAccount"); + + let Some(cred) = req.credentials else { return Err(s3_error!(InvalidRequest, "get cred failed")) }; + + let Some(service_account) = params.get("accessKey") else { + return Err(s3_error!(InvalidRequest, "Invalid arguments specified.")); + }; + + todo!() + } +} diff --git a/rustfs/src/admin/mod.rs b/rustfs/src/admin/mod.rs index c7223f84..eb82136c 100644 --- a/rustfs/src/admin/mod.rs +++ b/rustfs/src/admin/mod.rs @@ -1,8 +1,12 @@ pub mod handlers; +pub mod models; pub mod router; use common::error::Result; // use ecstore::global::{is_dist_erasure, is_erasure}; +use handlers::service_account::{ + AddServiceAccount, DeleteServiceAccount, InfoServiceAccount, ListServiceAccount, UpdateServiceAccount, +}; use hyper::Method; use router::{AdminOperation, S3Router}; use s3s::route::S3Route; @@ -105,5 +109,35 @@ pub fn make_admin_route() -> Result { )?; // } + r.insert( + Method::POST, + format!("{}{}", ADMIN_PREFIX, "/v3/update-service-account").as_str(), + AdminOperation(&UpdateServiceAccount {}), + )?; + + r.insert( + Method::GET, + format!("{}{}", ADMIN_PREFIX, "/v3/info-service-account").as_str(), + AdminOperation(&InfoServiceAccount {}), + )?; + + r.insert( + Method::GET, + format!("{}{}", ADMIN_PREFIX, "/v3/list-service-accounts").as_str(), + AdminOperation(&ListServiceAccount {}), + )?; + + r.insert( + Method::DELETE, + format!("{}{}", ADMIN_PREFIX, "/v3/delete-service-accounts").as_str(), + AdminOperation(&DeleteServiceAccount {}), + )?; + + r.insert( + Method::PUT, + format!("{}{}", ADMIN_PREFIX, "/v3/add-service-accounts").as_str(), + AdminOperation(&AddServiceAccount {}), + )?; + Ok(r) } diff --git a/rustfs/src/admin/models.rs b/rustfs/src/admin/models.rs new file mode 100644 index 00000000..f84f4b7a --- /dev/null +++ b/rustfs/src/admin/models.rs @@ -0,0 +1 @@ +pub mod service_account; diff --git a/rustfs/src/admin/models/service_account.rs b/rustfs/src/admin/models/service_account.rs new file mode 100644 index 00000000..596868b3 --- /dev/null +++ b/rustfs/src/admin/models/service_account.rs @@ -0,0 +1,51 @@ +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +#[derive(Deserialize, Default)] +#[serde(rename_all = "camelCase", default)] +pub struct AddServiceAccountReq { + pub access_key: String, + pub secret_key: String, + + pub policy: Option>, + pub target_user: Option, + pub name: String, + pub description: String, + #[serde(with = "time::serde::rfc3339::option")] + #[serde(skip_serializing_if = "Option::is_none")] + pub expiration: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Credentials<'a> { + pub access_key: &'a str, + pub secret_key: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + pub session_token: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "time::serde::rfc3339::option")] + pub expiration: Option, +} + +#[derive(Serialize)] +pub struct AddServiceAccountResp<'a> { + pub credentials: Credentials<'a>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InfoServiceAccountResp { + pub parent_user: String, + pub account_status: String, + pub implied_policy: bool, + pub policy: String, + #[serde(skip_serializing_if = "String::is_empty")] + pub name: String, + #[serde(skip_serializing_if = "String::is_empty")] + pub description: String, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "time::serde::rfc3339::option")] + pub expiration: Option, +} diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 92460bd2..550eb044 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -21,10 +21,11 @@ use hyper_util::{ server::conn::auto::Builder as ConnBuilder, service::TowerToHyperService, }; +use iam::init_iam_sys; use protos::proto_gen::node_service::node_service_server::NodeServiceServer; use s3s::{auth::SimpleAuth, service::S3ServiceBuilder}; use service::hybrid; -use std::{io::IsTerminal, net::SocketAddr, str::FromStr}; +use std::{io::IsTerminal, net::SocketAddr, str::FromStr, sync::Arc}; use tokio::net::TcpListener; use tonic::{metadata::MetadataValue, Request, Status}; use tracing::{debug, error, info, warn}; @@ -195,6 +196,7 @@ async fn run(opt: config::Opt) -> Result<()> { init_data_scanner().await; // init auto heal init_auto_heal().await; + init_iam_sys(store.clone()).await.unwrap(); info!("server was started");