mirror of
https://github.com/rustfs/rustfs.git
synced 2026-01-17 09:40:32 +00:00
add iam system
add iam store feat: add crypto crate introduce decrypt_data and encrypt_data functions Signed-off-by: bestgopher <84328409@qq.com>
This commit is contained in:
469
Cargo.lock
generated
469
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
32
crypto/Cargo.toml
Normal file
32
crypto/Cargo.toml
Normal file
@@ -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"
|
||||
11
crypto/src/encdec.rs
Normal file
11
crypto/src/encdec.rs
Normal file
@@ -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;
|
||||
18
crypto/src/encdec/aes.rs
Normal file
18
crypto/src/encdec/aes.rs
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
41
crypto/src/encdec/decrypt.rs
Normal file
41
crypto/src/encdec/decrypt.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
#[cfg(any(test, feature = "crypto"))]
|
||||
pub fn decrypt_data(password: &[u8], data: &[u8]) -> Result<Vec<u8>, 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<T: aes_gcm::aead::Aead>(stream: T, nonce: &[u8], data: &[u8]) -> Result<Vec<u8>, 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<Vec<u8>, crate::Error> {
|
||||
Ok(data.to_vec())
|
||||
}
|
||||
53
crypto/src/encdec/encrypt.rs
Normal file
53
crypto/src/encdec/encrypt.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
#[cfg(any(test, feature = "crypto"))]
|
||||
pub fn encrypt_data(password: &[u8], data: &[u8]) -> Result<Vec<u8>, 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<T: aes_gcm::aead::Aead>(stream: T, salt: &[u8], id: ID, data: &[u8]) -> Result<Vec<u8>, 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<Vec<u8>, crate::Error> {
|
||||
Ok(data.to_vec())
|
||||
}
|
||||
39
crypto/src/encdec/id.rs
Normal file
39
crypto/src/encdec/id.rs
Normal file
@@ -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<u8> for ID {
|
||||
type Error = crate::Error;
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
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::<Sha256>(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)
|
||||
}
|
||||
}
|
||||
14
crypto/src/encdec/tests.rs
Normal file
14
crypto/src/encdec/tests.rs
Normal file
@@ -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(())
|
||||
}
|
||||
28
crypto/src/error.rs
Normal file
28
crypto/src/error.rs
Normal file
@@ -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),
|
||||
}
|
||||
6
crypto/src/jwt.rs
Normal file
6
crypto/src/jwt.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod decode;
|
||||
pub mod encode;
|
||||
pub use serde_json::Value as Claims;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
12
crypto/src/jwt/decode.rs
Normal file
12
crypto/src/jwt/decode.rs
Normal file
@@ -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<TokenData<Claims>, Error> {
|
||||
Ok(jsonwebtoken::decode(
|
||||
token,
|
||||
&DecodingKey::from_secret(token_secret),
|
||||
&Validation::new(Algorithm::HS512),
|
||||
)?)
|
||||
}
|
||||
12
crypto/src/jwt/encode.rs
Normal file
12
crypto/src/jwt/encode.rs
Normal file
@@ -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<String, Error> {
|
||||
Ok(jsonwebtoken::encode(
|
||||
&Header::new(Algorithm::HS512),
|
||||
claims,
|
||||
&EncodingKey::from_secret(token_secret),
|
||||
)?)
|
||||
}
|
||||
16
crypto/src/jwt/tests.rs
Normal file
16
crypto/src/jwt/tests.rs
Normal file
@@ -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);
|
||||
}
|
||||
9
crypto/src/lib.rs
Normal file
9
crypto/src/lib.rs
Normal file
@@ -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;
|
||||
@@ -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>() {
|
||||
ConfigError::is_not_found(e)
|
||||
} else if let Some(e) = err.downcast_ref::<disk::error::DiskError>() {
|
||||
matches!(e, disk::error::DiskError::FileNotFound)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -264,7 +264,7 @@ impl ECStore {
|
||||
self.pools.len() == 1
|
||||
}
|
||||
|
||||
async fn list_path(&self, opts: &ListPathOptions, delimiter: &str) -> Result<ListObjectsInfo> {
|
||||
pub async fn list_path(&self, opts: &ListPathOptions, delimiter: &str) -> Result<ListObjectsInfo> {
|
||||
// if opts.prefix.ends_with(SLASH_SEPARATOR) {
|
||||
// return Err(Error::msg("eof"));
|
||||
// }
|
||||
|
||||
@@ -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<u8>) -> Self {
|
||||
let content_length = data.len();
|
||||
PutObjReader {
|
||||
stream: Body::from(data).into(),
|
||||
content_length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GetObjectReader {
|
||||
|
||||
28
iam/Cargo.toml
Normal file
28
iam/Cargo.toml
Normal file
@@ -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
|
||||
18
iam/src/arn.rs
Normal file
18
iam/src/arn.rs
Normal file
@@ -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<Self, Self::Err> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
23
iam/src/auth.rs
Normal file
23
iam/src/auth.rs
Normal file
@@ -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<Credentials> for UserIdentity {
|
||||
fn from(value: Credentials) -> Self {
|
||||
UserIdentity {
|
||||
version: 1,
|
||||
credentials: value,
|
||||
update_at: OffsetDateTime::now_utc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
372
iam/src/auth/credentials.rs
Normal file
372
iam/src/auth/credentials.rs
Normal file
@@ -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<Self, Self::Error> {
|
||||
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<Vec<BorrowedFormatItem<'static>>> =
|
||||
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<OffsetDateTime>,
|
||||
pub status: String,
|
||||
pub parent_user: String,
|
||||
pub groups: Option<Vec<String>>,
|
||||
pub claims: Option<HashMap<String, Vec<String>>>,
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl Credentials {
|
||||
pub fn new(elem: &str) -> crate::Result<Self> {
|
||||
let header: CredentialHeader = elem.try_into()?;
|
||||
Self::check_key_value(header)
|
||||
}
|
||||
|
||||
pub fn check_key_value(header: CredentialHeader) -> crate::Result<Self> {
|
||||
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<Policy>,
|
||||
access_key: String,
|
||||
secret_key: String,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
expiration: Option<OffsetDateTime>,
|
||||
allow_site_replicator_account: bool,
|
||||
claims: Option<serde_json::Value>,
|
||||
parent_user: String,
|
||||
groups: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl CredentialsBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn session_policy(mut self, policy: Option<Policy>) -> 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<OffsetDateTime>) -> 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<String>) -> Self {
|
||||
self.groups = Some(groups);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn try_build(self) -> crate::Result<Credentials> {
|
||||
self.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CredentialsBuilder> for Credentials {
|
||||
type Error = crate::Error;
|
||||
fn try_from(mut value: CredentialsBuilder) -> Result<Self, Self::Error> {
|
||||
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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
320
iam/src/cache.rs
Normal file
320
iam/src/cache.rs
Normal file
@@ -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<CacheEntity<PolicyDoc>>,
|
||||
pub users: ArcSwap<CacheEntity<UserIdentity>>,
|
||||
pub user_policies: ArcSwap<CacheEntity<MappedPolicy>>,
|
||||
pub sts_accounts: ArcSwap<CacheEntity<UserIdentity>>,
|
||||
pub sts_policies: ArcSwap<CacheEntity<MappedPolicy>>,
|
||||
pub groups: ArcSwap<CacheEntity<String>>,
|
||||
pub user_group_memeberships: ArcSwap<CacheEntity<HashSet<String>>>,
|
||||
pub group_policies: ArcSwap<CacheEntity<MappedPolicy>>,
|
||||
}
|
||||
|
||||
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<Base, A, B>(a: A, b: B) -> bool
|
||||
where
|
||||
A: AsRaw<Base>,
|
||||
B: AsRaw<Base>,
|
||||
{
|
||||
let a = a.as_raw();
|
||||
let b = b.as_raw();
|
||||
ptr::eq(a, b)
|
||||
}
|
||||
|
||||
fn exec<T: Clone>(target: &ArcSwap<CacheEntity<T>>, t: OffsetDateTime, mut op: impl FnMut(&mut CacheEntity<T>)) {
|
||||
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<T: Clone>(target: &ArcSwap<CacheEntity<T>>, key: &str, value: &T, t: OffsetDateTime) {
|
||||
Self::exec(target, t, |map: &mut CacheEntity<T>| {
|
||||
map.insert(key.to_string(), value.clone());
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete<T: Clone>(target: &ArcSwap<CacheEntity<T>>, key: &str, t: OffsetDateTime) {
|
||||
Self::exec(target, t, |map: &mut CacheEntity<T>| {
|
||||
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<Vec<Policy>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// 如果是临时用户,返回Ok(Some(partent_name)))
|
||||
/// 如果不是临时用户,返回Ok(None)
|
||||
fn is_temp_user<'a>(&self, user_name: &'a str) -> crate::Result<Option<&str>> {
|
||||
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<Option<&str>> {
|
||||
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<String> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CacheEntity<T> {
|
||||
map: HashMap<String, T>,
|
||||
/// 重新加载的时间
|
||||
load_time: OffsetDateTime,
|
||||
}
|
||||
|
||||
impl<T> Deref for CacheEntity<T> {
|
||||
type Target = HashMap<String, T>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.map
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for CacheEntity<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.map
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> CacheEntity<T> {
|
||||
pub fn new(map: HashMap<String, T>) -> Self {
|
||||
Self {
|
||||
map,
|
||||
load_time: OffsetDateTime::UNIX_EPOCH,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for CacheEntity<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
map: HashMap::new(),
|
||||
load_time: OffsetDateTime::UNIX_EPOCH,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> CacheEntity<T> {
|
||||
pub fn update_load_time(mut self) -> Self {
|
||||
self.load_time = OffsetDateTime::now_utc();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub type G<T> = Guard<Arc<CacheEntity<T>>>;
|
||||
|
||||
pub struct CacheInner {
|
||||
pub policy_docs: G<PolicyDoc>,
|
||||
pub users: G<UserIdentity>,
|
||||
pub user_policies: G<MappedPolicy>,
|
||||
pub sts_accounts: G<UserIdentity>,
|
||||
pub sts_policies: G<MappedPolicy>,
|
||||
pub groups: G<String>,
|
||||
pub user_group_memeberships: G<HashSet<String>>,
|
||||
pub group_policies: G<MappedPolicy>,
|
||||
}
|
||||
|
||||
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::<usize>::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::<usize>::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::<usize>::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());
|
||||
}
|
||||
}
|
||||
35
iam/src/error.rs
Normal file
35
iam/src/error.rs
Normal file
@@ -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<T> = std::result::Result<T, Error>;
|
||||
17
iam/src/format.rs
Normal file
17
iam/src/format.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
58
iam/src/lib.rs
Normal file
58
iam/src/lib.rs
Normal file
@@ -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<Arc<IamCache<ObjectStore>>> = OnceLock::new();
|
||||
|
||||
pub async fn init_iam_sys(ecstore: Arc<ECStore>) -> 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<Arc<IamCache<ObjectStore>>> {
|
||||
IAM_SYS.get().map(|x| Arc::clone(x)).ok_or(Error::IamSysNotInitialized)
|
||||
}
|
||||
|
||||
pub async fn is_allowed<'a>(args: Args<'a>) -> crate::Result<bool> {
|
||||
Ok(get()?.is_allowed(args).await)
|
||||
}
|
||||
|
||||
pub async fn get_service_account(ak: &str) -> crate::Result<(Credentials, Option<Policy>)> {
|
||||
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<OffsetDateTime> {
|
||||
get()?.add_service_account(cred).await
|
||||
}
|
||||
|
||||
pub async fn check_key(ak: &str) -> crate::Result<Option<UserIdentity>> {
|
||||
get()?.check_key(ak).await
|
||||
}
|
||||
223
iam/src/manager.rs
Normal file
223
iam/src/manager.rs
Normal file
@@ -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<T> {
|
||||
pub cache: Cache,
|
||||
pub api: T,
|
||||
pub loading: Arc<AtomicBool>,
|
||||
pub roles: HashMap<ARN, Vec<String>>,
|
||||
pub send_chan: Sender<i64>,
|
||||
pub last_timestamp: AtomicI64,
|
||||
}
|
||||
|
||||
impl<T> IamCache<T>
|
||||
where
|
||||
T: Store,
|
||||
{
|
||||
pub(crate) async fn new(api: T) -> Arc<Self> {
|
||||
let (sender, reciver) = mpsc::channel::<i64>(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<Self>, reciver: Receiver<i64>) -> 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<Self>) -> 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<HashMap<String, Vec<String>>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// todo, 判断是否存在,是否可以重试
|
||||
async fn save_iam_formatter(self: Arc<Self>) -> crate::Result<()> {
|
||||
match self.api.load_iam_config::<Format>(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<Vec<Credentials>> {
|
||||
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<OffsetDateTime> {
|
||||
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<Policy>)> {
|
||||
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<Option<UserIdentity>> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
120
iam/src/policy.rs
Normal file
120
iam/src/policy.rs
Normal file
@@ -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<String>,
|
||||
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<String, Vec<String>>,
|
||||
pub is_owner: bool,
|
||||
pub object: &'a str,
|
||||
pub claims: &'a HashMap<String, Value>,
|
||||
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())
|
||||
}
|
||||
}
|
||||
143
iam/src/policy/action.rs
Normal file
143
iam/src/policy/action.rs
Normal file
@@ -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<Action>);
|
||||
|
||||
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<Action>;
|
||||
|
||||
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<Self, Self::Error> {
|
||||
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,
|
||||
}
|
||||
12
iam/src/policy/doc.rs
Normal file
12
iam/src/policy/doc.rs
Normal file
@@ -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<OffsetDateTime>,
|
||||
pub update_date: Option<OffsetDateTime>,
|
||||
}
|
||||
32
iam/src/policy/effect.rs
Normal file
32
iam/src/policy/effect.rs
Normal file
@@ -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(())
|
||||
}
|
||||
}
|
||||
175
iam/src/policy/function.rs
Normal file
175
iam/src/policy/function.rs
Normal file
@@ -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<Func>);
|
||||
|
||||
impl Functions {
|
||||
pub fn evaluate(&self, values: &HashMap<String, Vec<String>>) -> bool {
|
||||
self.0.iter().all(|x| x.evaluate(values))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Functions {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
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<Func>;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
146
iam/src/policy/function/addr.rs
Normal file
146
iam/src/policy/function/addr.rs
Normal file
@@ -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<AddrFuncValue>;
|
||||
|
||||
impl AddrFunc {
|
||||
pub(crate) fn evaluate(&self, values: &HashMap<String, Vec<String>>) -> 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::<IpAddr>() 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<IpNetwork>);
|
||||
|
||||
impl<'de> Deserialize<'de> for AddrFuncValue {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(AddrFuncValue(vec![Self::to_cidr::<E>(v)?]))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
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::<A::Error>(v)?)
|
||||
}
|
||||
data
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl AddrFuncValueVisitor {
|
||||
fn to_cidr<E: serde::de::Error>(v: &str) -> Result<IpNetwork, E> {
|
||||
let mut cidr_str = Cow::from(v);
|
||||
if v.find('/').is_none() {
|
||||
cidr_str.to_mut().push_str("/32");
|
||||
}
|
||||
|
||||
Ok(cidr_str
|
||||
.parse::<IpNetwork>()
|
||||
.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<String>, 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(())
|
||||
}
|
||||
}
|
||||
10
iam/src/policy/function/binary.rs
Normal file
10
iam/src/policy/function/binary.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::func::InnerFunc;
|
||||
|
||||
pub type BinaryFunc = InnerFunc<BinaryFuncValue>;
|
||||
|
||||
// todo implement it
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(transparent)]
|
||||
pub struct BinaryFuncValue(String);
|
||||
119
iam/src/policy/function/bool_null.rs
Normal file
119
iam/src/policy/function/bool_null.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use super::func::InnerFunc;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||
use std::{collections::HashMap, fmt};
|
||||
|
||||
pub type BoolFunc = InnerFunc<BoolFuncValue>;
|
||||
impl BoolFunc {
|
||||
pub fn evaluate_bool(&self, values: &HashMap<String, Vec<String>>) -> 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<String, Vec<String>>) -> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for BoolFuncValue {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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<E>(self, value: bool) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(BoolFuncValue(value))
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(BoolFuncValue(value.parse::<bool>().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<String>, 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::<BoolFunc>(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(())
|
||||
}
|
||||
}
|
||||
78
iam/src/policy/function/condition.rs
Normal file
78
iam/src/policy/function/condition.rs
Normal file
@@ -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<String, Vec<String>>) -> 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(_))
|
||||
}
|
||||
}
|
||||
107
iam/src/policy/function/date.rs
Normal file
107
iam/src/policy/function/date.rs
Normal file
@@ -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<DateFuncValue>;
|
||||
|
||||
impl DateFunc {
|
||||
pub fn evaluate(
|
||||
&self,
|
||||
op: impl FnOnce(&OffsetDateTime, &OffsetDateTime) -> bool,
|
||||
values: &HashMap<String, Vec<String>>,
|
||||
) -> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
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<String>, 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(())
|
||||
}
|
||||
}
|
||||
91
iam/src/policy/function/func.rs
Normal file
91
iam/src/policy/function/func.rs
Normal file
@@ -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<Condition>),
|
||||
ForAllValues(Vec<Condition>),
|
||||
ForNormal(Vec<Condition>),
|
||||
}
|
||||
|
||||
impl Func {
|
||||
pub fn evaluate(&self, values: &HashMap<String, Vec<String>>) -> 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<T> {
|
||||
pub key: Key,
|
||||
pub values: T,
|
||||
}
|
||||
|
||||
impl<T: Clone> Clone for InnerFunc<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
key: self.key.clone(),
|
||||
values: self.values.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> Serialize for InnerFunc<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<T>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct FuncVisitor<T>(PhantomData<T>);
|
||||
impl<'v, T> Visitor<'v> for FuncVisitor<T>
|
||||
where
|
||||
T: Deserialize<'v>,
|
||||
{
|
||||
type Value = InnerFunc<T>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("struct StringFunc")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::MapAccess<'v>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
let Some((key, values)) = map.next_entry::<Key, T>()? else {
|
||||
return Err(A::Error::custom("no k-v pair"));
|
||||
};
|
||||
|
||||
Ok(InnerFunc { key, values })
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_map(FuncVisitor::<T>(PhantomData))
|
||||
}
|
||||
}
|
||||
110
iam/src/policy/function/key.rs
Normal file
110
iam/src/policy/function/key.rs
Normal file
@@ -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<String>,
|
||||
}
|
||||
|
||||
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<Key> for String {
|
||||
fn from(value: Key) -> Self {
|
||||
value.name()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Key {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
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>(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::<Key>(input)?;
|
||||
assert_eq!(v.name, except.name);
|
||||
assert_eq!(v.variable, except.variable);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
333
iam/src/policy/function/key_name.rs
Normal file
333
iam/src/policy/function/key_name.rs
Normal file
@@ -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<Self, Self::Error> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
113
iam/src/policy/function/number.rs
Normal file
113
iam/src/policy/function/number.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::func::InnerFunc;
|
||||
use serde::{
|
||||
de::{Error, Visitor},
|
||||
Deserialize, Deserializer, Serialize,
|
||||
};
|
||||
|
||||
pub type NumberFunc = InnerFunc<NumberFuncValue>;
|
||||
|
||||
#[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<String, Vec<String>>) -> 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::<i64>() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
op(&rv, &self.values.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for NumberFuncValue {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.0.to_string().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for NumberFuncValue {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(NumberFuncValue(value.parse().map_err(|e| E::custom(format!("{e:?}")))?))
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(NumberFuncValue(value))
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
|
||||
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<String>, 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(())
|
||||
}
|
||||
}
|
||||
220
iam/src/policy/function/string.rs
Normal file
220
iam/src/policy/function/string.rs
Normal file
@@ -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<StringFuncValue>;
|
||||
|
||||
impl StringFunc {
|
||||
fn eval(&self, for_all: bool, ignore_case: bool, values: &HashMap<String, Vec<String>>) -> 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::<Set<_>>()
|
||||
})
|
||||
.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::<Set<_>>();
|
||||
|
||||
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<String, Vec<String>>) -> 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<String, Vec<String>>) -> 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<String>);
|
||||
|
||||
impl Serialize for StringFuncValue {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok({
|
||||
let mut hash = Set::new();
|
||||
hash.insert(value.to_string());
|
||||
StringFuncValue(hash)
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
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::<String>()? {
|
||||
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<String>, 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::<StringFunc>(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(())
|
||||
}
|
||||
}
|
||||
29
iam/src/policy/id.rs
Normal file
29
iam/src/policy/id.rs
Normal file
@@ -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<T: ToString> From<T> 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
|
||||
}
|
||||
}
|
||||
235
iam/src/policy/policy.rs
Normal file
235
iam/src/policy/policy.rs
Normal file
@@ -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<Statement>,
|
||||
}
|
||||
|
||||
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![]),
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
});
|
||||
}
|
||||
118
iam/src/policy/resource.rs
Normal file
118
iam/src/policy/resource.rs
Normal file
@@ -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<Resource>);
|
||||
|
||||
impl ResourceSet {
|
||||
pub fn is_match(&self, resource: &str, conditons: &HashMap<String, Vec<String>>) -> bool {
|
||||
for re in self.0.iter() {
|
||||
if re.is_match(resource, conditons) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ResourceSet {
|
||||
type Target = HashSet<Resource>;
|
||||
|
||||
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<String, Vec<String>>) -> 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<Self, Self::Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
102
iam/src/policy/statement.rs
Normal file
102
iam/src/policy/statement.rs
Normal file
@@ -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(())
|
||||
}
|
||||
}
|
||||
87
iam/src/policy/utils.rs
Normal file
87
iam/src/policy/utils.rs
Normal file
@@ -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<String, Value>, chaim_name: &str) -> (Vec<String>, 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)
|
||||
}
|
||||
}
|
||||
141
iam/src/policy/utils/path.rs
Normal file
141
iam/src/policy/utils/path.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use std::{fmt::Write, usize};
|
||||
|
||||
struct LazyBuf<'a> {
|
||||
s: &'a str,
|
||||
buf: Option<Vec<u8>>,
|
||||
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());
|
||||
}
|
||||
}
|
||||
189
iam/src/policy/utils/wildcard.rs
Normal file
189
iam/src/policy/utils/wildcard.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
pub fn is_simple_match<P, N>(pattern: P, name: N) -> bool
|
||||
where
|
||||
P: AsRef<str>,
|
||||
N: AsRef<str>,
|
||||
{
|
||||
inner_match(pattern, name, true)
|
||||
}
|
||||
|
||||
pub fn is_match<P, N>(pattern: P, name: N) -> bool
|
||||
where
|
||||
P: AsRef<str>,
|
||||
N: AsRef<str>,
|
||||
{
|
||||
inner_match(pattern, name, false)
|
||||
}
|
||||
|
||||
pub fn is_match_as_pattern_prefix<P, N>(pattern: P, text: N) -> bool
|
||||
where
|
||||
P: AsRef<str>,
|
||||
N: AsRef<str>,
|
||||
{
|
||||
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<str>, name: impl AsRef<str>, 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)
|
||||
}
|
||||
}
|
||||
20
iam/src/service_type.rs
Normal file
20
iam/src/service_type.rs
Normal file
@@ -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<Self, Self::Error> {
|
||||
let service_type = match value {
|
||||
"s3" => Self::S3,
|
||||
"sts" => Self::STS,
|
||||
_ => return Err(Error::InvalidServiceType(value.to_owned())),
|
||||
};
|
||||
|
||||
Ok(service_type)
|
||||
}
|
||||
}
|
||||
43
iam/src/store.rs
Normal file
43
iam/src/store.rs
Normal file
@@ -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<Item>(&self, path: impl AsRef<str> + Send) -> crate::Result<(Item, ObjectInfo)>
|
||||
where
|
||||
Item: DeserializeOwned;
|
||||
|
||||
async fn save_iam_config<Item: Serialize + Send>(&self, item: Item, path: impl AsRef<str> + Send) -> crate::Result<()>;
|
||||
|
||||
async fn load_all(&self, cache: &Cache) -> crate::Result<()>;
|
||||
|
||||
fn get_default_policyes() -> HashMap<String, PolicyDoc> {
|
||||
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<HashMap<String, UserIdentity>>;
|
||||
|
||||
async fn load_policy_docs(&self) -> crate::Result<HashMap<String, PolicyDoc>>;
|
||||
}
|
||||
345
iam/src/store/object.rs
Normal file
345
iam/src/store/object.rs
Normal file
@@ -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<ECStore>,
|
||||
}
|
||||
|
||||
impl ObjectStore {
|
||||
const BUCKET_NAME: &str = ".rustfs.sys";
|
||||
|
||||
pub fn new(object_api: Arc<ECStore>) -> Self {
|
||||
Self { object_api }
|
||||
}
|
||||
|
||||
async fn list_iam_config_items(&self, prefix: &str, items: &[&str]) -> crate::Result<Vec<String>> {
|
||||
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<PolicyDoc> {
|
||||
let (mut policy, object) = self
|
||||
.load_iam_config::<PolicyDoc>(&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<Option<UserIdentity>> {
|
||||
let (mut user, _) = self
|
||||
.load_iam_config::<UserIdentity>(&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<MappedPolicy> {
|
||||
let (p, _) = self
|
||||
.load_iam_config::<MappedPolicy>(&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<Item>(&self, path: impl AsRef<str> + 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<Item: Serialize + Send>(&self, item: Item, path: impl AsRef<str> + 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<HashMap<String, PolicyDoc>> {
|
||||
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::<PolicyDoc>(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<HashMap<String, UserIdentity>> {
|
||||
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::<UserIdentity>(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(())
|
||||
}
|
||||
}
|
||||
62
iam/src/utils.rs
Normal file
62
iam/src/utils.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use rand::{Rng, RngCore};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
pub fn gen_access_key(length: usize) -> crate::Result<String> {
|
||||
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<String> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
229
rustfs/src/admin/handlers/service_account.rs
Normal file
229
rustfs/src/admin/handlers/service_account.rs
Normal file
@@ -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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
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<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
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<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
warn!("handle ListServiceAccount");
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteServiceAccount {}
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for DeleteServiceAccount {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
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!()
|
||||
}
|
||||
}
|
||||
@@ -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<impl S3Route> {
|
||||
)?;
|
||||
// }
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
1
rustfs/src/admin/models.rs
Normal file
1
rustfs/src/admin/models.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod service_account;
|
||||
51
rustfs/src/admin/models/service_account.rs
Normal file
51
rustfs/src/admin/models/service_account.rs
Normal file
@@ -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<Vec<u8>>,
|
||||
pub target_user: Option<String>,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expiration: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
#[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<OffsetDateTime>,
|
||||
}
|
||||
|
||||
#[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<OffsetDateTime>,
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user