From 7b3721aa4484151607d6d0c86eee11930782bead Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Oct 2024 05:37:44 +0000 Subject: [PATCH 1/7] Bump the dependencies group with 4 updates Bumps the dependencies group with 4 updates: [clap](https://github.com/clap-rs/clap), [futures](https://github.com/rust-lang/futures-rs), [futures-util](https://github.com/rust-lang/futures-rs) and [prost-build](https://github.com/tokio-rs/prost). Updates `clap` from 4.5.18 to 4.5.20 - [Release notes](https://github.com/clap-rs/clap/releases) - [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md) - [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.18...clap_complete-v4.5.20) Updates `futures` from 0.3.30 to 0.3.31 - [Release notes](https://github.com/rust-lang/futures-rs/releases) - [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.30...0.3.31) Updates `futures-util` from 0.3.30 to 0.3.31 - [Release notes](https://github.com/rust-lang/futures-rs/releases) - [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.30...0.3.31) Updates `prost-build` from 0.13.1 to 0.13.3 - [Release notes](https://github.com/tokio-rs/prost/releases) - [Changelog](https://github.com/tokio-rs/prost/blob/master/CHANGELOG.md) - [Commits](https://github.com/tokio-rs/prost/compare/v0.13.1...v0.13.3) --- updated-dependencies: - dependency-name: clap dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: futures dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: futures-util dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: prost-build dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- Cargo.lock | 48 +++++++++++++++++++++++------------------------ Cargo.toml | 6 +++--- rustfs/Cargo.toml | 2 +- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 380a3a65d..1a587b54f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -318,9 +318,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.18" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", @@ -328,9 +328,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.18" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", @@ -578,9 +578,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -593,9 +593,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -603,15 +603,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -620,15 +620,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -637,21 +637,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1362,9 +1362,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb182580f71dd070f88d01ce3de9f4da5021db7115d2e1c3605a754153b77c1" +checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15" dependencies = [ "bytes", "heck", diff --git a/Cargo.toml b/Cargo.toml index 3d693dfb0..7cbe23936 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,10 +20,10 @@ version = "0.0.1" async-trait = "0.1.83" backon = "1.2.0" bytes = "1.7.2" -clap = { version = "4.5.18", features = ["derive"] } +clap = { version = "4.5.20", features = ["derive"] } ecstore = { path = "./ecstore" } flatbuffers = "24.3.25" -futures = "0.3.30" +futures = "0.3.31" futures-util = "0.3.30" common = { path = "./common/common" } hyper = "1.4.1" @@ -41,7 +41,7 @@ netif = "0.1.6" pin-project-lite = "0.2" # pin-utils = "0.1.0" prost = "0.13.3" -prost-build = "0.13.1" +prost-build = "0.13.3" prost-types = "0.13.3" protobuf = "3.6" protos = { path = "./common/protos" } diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index e8ee6f947..87bd9384c 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -60,7 +60,7 @@ futures-util.workspace = true # uuid = { version = "1.8.0", features = ["v4", "fast-rng", "serde"] } ecstore = { path = "../ecstore" } s3s = "0.10.0" -clap = { version = "4.5.18", features = ["derive"] } +clap = { version = "4.5.20", features = ["derive"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter", "time"] } hyper-util = { version = "0.1.9", features = [ "tokio", From 3adbadd80802dc3e7a8a07b500845361039eefbc Mon Sep 17 00:00:00 2001 From: Nugine Date: Fri, 11 Oct 2024 12:28:36 +0800 Subject: [PATCH 2/7] ci: use skip-check and rust-cache --- .github/workflows/e2e.yml | 42 +++++++++++++++++++++----------- .github/workflows/rust.yml | 49 +++++++++++++++++++++++++------------- 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 6d2b7fc10..533454f48 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,6 +1,7 @@ -name: e2e +name: E2E on: + workflow_dispatch: push: pull_request: branches: [ "main" ] @@ -9,14 +10,34 @@ env: CARGO_TERM_COLOR: always jobs: - build: + skip-check: + permissions: + actions: write + contents: read + runs-on: ubuntu-latest + outputs: + should_skip: ${{ steps.skip_check.outputs.should_skip }} + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@v5 + with: + concurrent_skipping: 'same_content_newer' + cancel_others: true + paths_ignore: '["*.md"]' + + e2e: + # See this url if required matrix jobs are hanging + # https://github.com/fkirc/skip-duplicate-actions#how-to-use-skip-check-with-required-matrix-jobs + needs: skip-check + if: needs.skip-check.outputs.should_skip != 'true' + timeout-minutes: 30 runs-on: ubuntu-latest strategy: matrix: rust: - stable - - nightly + # - nightly steps: - name: cache protoc bin @@ -58,14 +79,15 @@ jobs: chmod +x /usr/local/bin/flatc rm -rf Linux.flatc.binary.g++-13.zip + - uses: actions/checkout@v4 + - name: toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} # components: rustfmt, clippy - - name: checkout - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 - name: run fs start working-directory: . @@ -79,11 +101,3 @@ jobs: - name: e2e test run: cargo test -p e2e_test --lib - - - name: cache cargo - uses: actions/cache@v4 - env: - cache-name: cache-cargo - with: - path: ~/.cargo - key: ${{ runner.os }}-build-${{ env.cache-name }}-v0.0.1 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e45ca2b8c..87129420e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,7 +9,27 @@ env: CARGO_TERM_COLOR: always jobs: + skip-check: + permissions: + actions: write + contents: read + runs-on: ubuntu-latest + outputs: + should_skip: ${{ steps.skip_check.outputs.should_skip }} + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@v5 + with: + concurrent_skipping: 'same_content_newer' + cancel_others: true + paths_ignore: '["*.md"]' + build: + # See this url if required matrix jobs are hanging + # https://github.com/fkirc/skip-duplicate-actions#how-to-use-skip-check-with-required-matrix-jobs + needs: skip-check + if: needs.skip-check.outputs.should_skip != 'true' + runs-on: ubuntu-latest strategy: matrix: @@ -57,24 +77,21 @@ jobs: chmod +x /usr/local/bin/flatc rm -rf Linux.flatc.binary.g++-13.zip - - name: toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ matrix.rust }} - # components: rustfmt, clippy - - uses: actions/checkout@v4 - - name: cargo build - run: cargo build + - name: toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + + - name: cargo fmt + run: cargo fmt --all --check + + - name: cargo clippy + run: cargo clippy -- -D warnings - name: cargo test run: cargo test --all --exclude e2e_test - - - name: cache cargo - uses: actions/cache@v4 - env: - cache-name: cache-cargo - with: - path: ~/.cargo - key: ${{ runner.os }}-build-${{ env.cache-name }}-v0.0.1 From c9f07fb8ab87dc934e7ed1ed957c61084d1c873c Mon Sep 17 00:00:00 2001 From: mirschao <119988085+mirschao@users.noreply.github.com> Date: Fri, 11 Oct 2024 22:25:25 +0800 Subject: [PATCH 3/7] Update rust.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cargo fmt --all --check 中--check为检查模式,如果有对应的格式化修改,则命令返回码为非零(错误),所以导致action运行失败,替换为cargo fmt --all即可解决问题。 --- .github/workflows/rust.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 87129420e..2fd7e8109 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -88,7 +88,10 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: cargo fmt - run: cargo fmt --all --check + run: cargo fmt --all + + - name: cargo check + run: cargo --check - name: cargo clippy run: cargo clippy -- -D warnings From fd3d742d714d2b6899c7fc5a61231b55cfd27cf7 Mon Sep 17 00:00:00 2001 From: mirschao <119988085+mirschao@users.noreply.github.com> Date: Fri, 11 Oct 2024 22:27:32 +0800 Subject: [PATCH 4/7] Update rust.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cargo fmt --all --check 中--check为检查模式,如果有对应的格式化修改,则命令返回码为非零(错误),所以导致action运行失败,替换为cargo fmt --all即可解决问题。 --- .github/workflows/rust.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2fd7e8109..37b84e988 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -90,9 +90,6 @@ jobs: - name: cargo fmt run: cargo fmt --all - - name: cargo check - run: cargo --check - - name: cargo clippy run: cargo clippy -- -D warnings From 07675f3039f66c7d4d3f8d5f3001264d9e3e69e6 Mon Sep 17 00:00:00 2001 From: bestgopher <84328409@qq.com> Date: Fri, 11 Oct 2024 22:41:08 +0800 Subject: [PATCH 5/7] fix: use compile_protos --- common/protos/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/protos/build.rs b/common/protos/build.rs index 5ef32dde9..766011af1 100644 --- a/common/protos/build.rs +++ b/common/protos/build.rs @@ -50,7 +50,7 @@ fn main() -> Result<(), AnyError> { .protoc_arg("--experimental_allow_proto3_optional") .compile_well_known_types(true) .emit_rerun_if_changed(false) - .compile(proto_files, &[proto_dir.clone()]) + .compile_protos(proto_files, &[proto_dir.clone()]) .map_err(|e| format!("Failed to generate protobuf file: {e}."))?; // protos/gen/mod.rs From eae5b68f52540079fe15964ed069e53b0577f28e Mon Sep 17 00:00:00 2001 From: Nugine Date: Fri, 11 Oct 2024 23:19:24 +0800 Subject: [PATCH 6/7] fix --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 37b84e988..87129420e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -88,7 +88,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: cargo fmt - run: cargo fmt --all + run: cargo fmt --all --check - name: cargo clippy run: cargo clippy -- -D warnings From 81dc3d440440536151efe09be5d6e5243ccf3e7e Mon Sep 17 00:00:00 2001 From: weisd Date: Sat, 12 Oct 2024 00:41:03 +0800 Subject: [PATCH 7/7] done bucket policy, need test --- ecstore/src/bucket/error.rs | 14 +- ecstore/src/bucket/metadata.rs | 15 +- ecstore/src/bucket/metadata_sys.rs | 53 ++- ecstore/src/bucket/mod.rs | 5 +- ecstore/src/bucket/policy/action.rs | 407 +++++++++++++++++- ecstore/src/bucket/policy/bucket_policy.rs | 256 ++++++++++- .../src/bucket/policy/condition/function.rs | 292 +++++++++++-- ecstore/src/bucket/policy/condition/key.rs | 159 +++++++ ecstore/src/bucket/policy/condition/mod.rs | 1 + ecstore/src/bucket/policy/condition/name.rs | 77 ++-- ecstore/src/bucket/policy/effect.rs | 18 + ecstore/src/bucket/policy/principal.rs | 20 +- ecstore/src/bucket/policy/resource.rs | 248 ++++++++++- ecstore/src/bucket/policy_sys.rs | 33 ++ ecstore/src/disk/local.rs | 12 +- ecstore/src/utils/net.rs | 4 +- ecstore/src/utils/path.rs | 161 +++++++ rustfs/src/main.rs | 2 +- rustfs/src/storage/ecfs.rs | 216 +++++++++- 19 files changed, 1846 insertions(+), 147 deletions(-) create mode 100644 ecstore/src/bucket/policy/condition/key.rs create mode 100644 ecstore/src/bucket/policy_sys.rs diff --git a/ecstore/src/bucket/error.rs b/ecstore/src/bucket/error.rs index a18a295a2..f073637ae 100644 --- a/ecstore/src/bucket/error.rs +++ b/ecstore/src/bucket/error.rs @@ -1,4 +1,6 @@ -#[derive(Debug, thiserror::Error)] +use crate::error::Error; + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum BucketMetadataError { #[error("tagging not found")] TaggingNotFound, @@ -17,3 +19,13 @@ pub enum BucketMetadataError { #[error("bucket remote target not found")] BucketRemoteTargetNotFound, } + +impl BucketMetadataError { + pub fn is(&self, err: &Error) -> bool { + if let Some(e) = err.downcast_ref::() { + e == self + } else { + false + } + } +} diff --git a/ecstore/src/bucket/metadata.rs b/ecstore/src/bucket/metadata.rs index 6b832dd64..0f67a1eca 100644 --- a/ecstore/src/bucket/metadata.rs +++ b/ecstore/src/bucket/metadata.rs @@ -6,10 +6,8 @@ use super::{ use byteorder::{BigEndian, ByteOrder, LittleEndian}; use rmp_serde::Serializer as rmpSerializer; use serde::Serializer; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::fmt::Display; -use std::str::FromStr; use time::OffsetDateTime; use tracing::{error, warn}; @@ -381,17 +379,6 @@ async fn read_bucket_metadata(api: &ECStore, bucket: &str) -> Result(_deserializer: D) -> core::result::Result -where - S: FromStr, - S::Err: Display, - D: Deserializer<'de>, -{ - // let s: String = Deserialize::deserialize(deserializer)?; - // S::from_str(&s).map_err(de::Error::custom) - unimplemented!() -} - fn _write_time(t: &OffsetDateTime, s: S) -> Result where S: Serializer, diff --git a/ecstore/src/bucket/metadata_sys.rs b/ecstore/src/bucket/metadata_sys.rs index 90bc6cdc7..8020f1737 100644 --- a/ecstore/src/bucket/metadata_sys.rs +++ b/ecstore/src/bucket/metadata_sys.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use std::{collections::HashMap, sync::Arc}; use crate::bucket::error::BucketMetadataError; -use crate::bucket::metadata::load_bucket_metadata_parse; +use crate::bucket::metadata::{load_bucket_metadata_parse, BUCKET_LIFECYCLE_CONFIG}; use crate::bucket::utils::is_meta_bucketname; use crate::config; use crate::config::error::ConfigError; @@ -143,15 +143,42 @@ impl BucketMetadataSys { } } - // async fn reset(&mut self) { - // let mut map = self.metadata_map.write().await; - // map.clear(); - // } + async fn _reset(&mut self) { + let mut map = self.metadata_map.write().await; + map.clear(); + } pub async fn update(&mut self, bucket: &str, config_file: &str, data: Vec) -> Result { self.update_and_parse(bucket, config_file, data, true).await } + pub async fn delete(&mut self, bucket: &str, config_file: &str) -> Result { + if config_file == BUCKET_LIFECYCLE_CONFIG { + let meta = match self.get_config_from_disk(bucket).await { + Ok(res) => res, + Err(err) => { + if !config::error::is_not_found(&err) { + return Err(err); + } else { + BucketMetadata::new(bucket) + } + } + }; + + if !meta.lifecycle_config_xml.is_empty() { + let cfg = Lifecycle::unmarshal(&meta.lifecycle_config_xml)?; + for _v in cfg.rules.iter() { + // TODO: FIXME: + break; + } + } + + // TODO: other lifecycle handle + } + + self.update_and_parse(bucket, config_file, Vec::new(), false).await + } + async fn update_and_parse(&mut self, bucket: &str, config_file: &str, data: Vec, parse: bool) -> Result { let layer = new_object_layer_fn(); let lock = layer.read().await; @@ -201,6 +228,22 @@ impl BucketMetadataSys { Ok(()) } + pub async fn get_config_from_disk(&self, bucket: &str) -> Result { + if self.api.as_ref().is_none() { + return Err(Error::msg("errBucketMetadataNotInitialized")); + } + + if is_meta_bucketname(bucket) { + return Err(Error::msg("errInvalidArgument")); + } + + if let Some(api) = self.api.as_ref() { + load_bucket_metadata(&api, bucket).await + } else { + Err(Error::msg("errBucketMetadataNotInitialized")) + } + } + pub async fn get_config(&self, bucket: &str) -> Result<(BucketMetadata, bool)> { if let Some(api) = self.api.as_ref() { let has_bm = { diff --git a/ecstore/src/bucket/mod.rs b/ecstore/src/bucket/mod.rs index 59e35a47b..245e1d0f4 100644 --- a/ecstore/src/bucket/mod.rs +++ b/ecstore/src/bucket/mod.rs @@ -1,11 +1,12 @@ mod encryption; -mod error; +pub mod error; mod event; mod lifecycle; pub mod metadata; mod metadata_sys; mod objectlock; -mod policy; +pub mod policy; +pub mod policy_sys; mod quota; mod replication; pub mod tags; diff --git a/ecstore/src/bucket/policy/action.rs b/ecstore/src/bucket/policy/action.rs index 34b6dc8a1..264cb7a0b 100644 --- a/ecstore/src/bucket/policy/action.rs +++ b/ecstore/src/bucket/policy/action.rs @@ -1,80 +1,285 @@ +use crate::{bucket::policy::condition::keyname::ALL_SUPPORT_KEYS, utils}; +use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use std::{ + collections::{HashMap, HashSet}, + vec, +}; -#[derive(Debug, Deserialize, Serialize, Default,Clone)] +use super::condition::{ + key::{Key, KeySet}, + keyname::{KeyName, COMMOM_KEYS}, +}; + +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] pub struct ActionSet(HashSet); -impl ActionSet {} +impl ActionSet { + pub fn as_ref(&self) -> &HashSet { + &self.0 + } + pub fn is_match(&self, act: &Action) -> bool { + for item in self.0.iter() { + if item.is_match(act) { + return true; + } + + if item == &Action::GetObjectVersion { + if act == &Action::GetObjectVersion { + return true; + } + } + } + + false + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} // 定义Action枚举类型 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Hash)] pub enum Action { + #[serde(rename = "s3:AbortMultipartUpload")] AbortMultipartUpload, + #[serde(rename = "s3:CreateBucket")] CreateBucket, + #[serde(rename = "s3:DeleteBucket")] DeleteBucket, + #[serde(rename = "s3:ForceDeleteBucket")] ForceDeleteBucket, + #[serde(rename = "s3:DeleteBucketPolicy")] DeleteBucketPolicy, + #[serde(rename = "s3:DeleteBucketCors")] DeleteBucketCors, + #[serde(rename = "s3:DeleteObject")] DeleteObject, + #[serde(rename = "s3:GetBucketLocation")] GetBucketLocation, + #[serde(rename = "s3:GetBucketNotification")] GetBucketNotification, + #[serde(rename = "s3:GetBucketPolicy")] GetBucketPolicy, + #[serde(rename = "s3:GetBucketCors")] GetBucketCors, + #[serde(rename = "s3:GetObject")] GetObject, + #[serde(rename = "s3:GetObjectAttributes")] GetObjectAttributes, + #[serde(rename = "s3:HeadBucket")] HeadBucket, + #[serde(rename = "s3:ListAllMyBuckets")] ListAllMyBuckets, + #[serde(rename = "s3:ListBucket")] ListBucket, + #[serde(rename = "s3:GetBucketPolicyStatus")] GetBucketPolicyStatus, + #[serde(rename = "s3:ListBucketVersions")] ListBucketVersions, + #[serde(rename = "s3:ListBucketMultipartUploads")] ListBucketMultipartUploads, + #[serde(rename = "s3:ListenNotification")] ListenNotification, + #[serde(rename = "s3:ListenBucketNotification")] ListenBucketNotification, + #[serde(rename = "s3:ListMultipartUploadParts")] ListMultipartUploadParts, - PutBucketLifecycle, - GetBucketLifecycle, + #[serde(rename = "s3:PutLifecycleConfiguration")] + PutLifecycleConfiguration, + #[serde(rename = "s3:GetLifecycleConfiguration")] + GetLifecycleConfiguration, + #[serde(rename = "s3:PutBucketNotification")] PutBucketNotification, + #[serde(rename = "s3:PutBucketPolicy")] PutBucketPolicy, + #[serde(rename = "s3:PutBucketCors")] PutBucketCors, + #[serde(rename = "s3:PutObject")] PutObject, + #[serde(rename = "s3:DeleteObjectVersion")] DeleteObjectVersion, + #[serde(rename = "s3:DeleteObjectVersionTagging")] DeleteObjectVersionTagging, + #[serde(rename = "s3:GetObjectVersion")] GetObjectVersion, + #[serde(rename = "s3:GetObjectVersionAttributes")] GetObjectVersionAttributes, + #[serde(rename = "s3:GetObjectVersionTagging")] GetObjectVersionTagging, + #[serde(rename = "s3:PutObjectVersionTagging")] PutObjectVersionTagging, + #[serde(rename = "s3:BypassGovernanceRetention")] BypassGovernanceRetention, + #[serde(rename = "s3:PutObjectRetention")] PutObjectRetention, + #[serde(rename = "s3:GetObjectRetention")] GetObjectRetention, + #[serde(rename = "s3:GetObjectLegalHold")] GetObjectLegalHold, + #[serde(rename = "s3:PutObjectLegalHold")] PutObjectLegalHold, + #[serde(rename = "s3:GetBucketObjectLockConfiguration")] GetBucketObjectLockConfiguration, + #[serde(rename = "s3:PutBucketObjectLockConfiguration")] PutBucketObjectLockConfiguration, + #[serde(rename = "s3:GetBucketTagging")] GetBucketTagging, + #[serde(rename = "s3:PutBucketTagging")] PutBucketTagging, + #[serde(rename = "s3:GetObjectTagging")] GetObjectTagging, + #[serde(rename = "s3:PutObjectTagging")] PutObjectTagging, + #[serde(rename = "s3:DeleteObjectTagging")] DeleteObjectTagging, + #[serde(rename = "s3:PutBucketEncryption")] PutBucketEncryption, + #[serde(rename = "s3:GetBucketEncryption")] GetBucketEncryption, + #[serde(rename = "s3:PutBucketVersioning")] PutBucketVersioning, + #[serde(rename = "s3:GetBucketVersioning")] GetBucketVersioning, + #[serde(rename = "s3:PutReplicationConfiguration")] PutReplicationConfiguration, + #[serde(rename = "s3:GetReplicationConfiguration")] GetReplicationConfiguration, + #[serde(rename = "s3:ReplicateObject")] ReplicateObject, + #[serde(rename = "s3:ReplicateDelete")] ReplicateDelete, + #[serde(rename = "s3:ReplicateTags")] ReplicateTags, + #[serde(rename = "s3:GetObjectVersionForReplication")] GetObjectVersionForReplication, + #[serde(rename = "s3:RestoreObject")] RestoreObject, + #[serde(rename = "s3:ResetBucketReplicationState")] ResetBucketReplicationState, + #[serde(rename = "s3:PutObjectFanOut")] PutObjectFanOut, #[default] + #[serde(rename = "s3:*")] AllActions, } +lazy_static! { + #[derive(Debug)] + static ref SUPPORT_OBJCET_ACTIONS: HashSet = { + let mut h = HashSet::new(); + h.insert(Action::AllActions); + h.insert(Action::AbortMultipartUpload); + h.insert(Action::DeleteObject); + h.insert(Action::GetObject); + h.insert(Action::ListMultipartUploadParts); + h.insert(Action::PutObject); + h.insert(Action::BypassGovernanceRetention); + h.insert(Action::PutObjectRetention); + h.insert(Action::GetObjectRetention); + h.insert(Action::PutObjectLegalHold); + h.insert(Action::GetObjectLegalHold); + h.insert(Action::GetObjectTagging); + h.insert(Action::PutObjectTagging); + h.insert(Action::DeleteObjectTagging); + h.insert(Action::GetObjectVersion); + h.insert(Action::GetObjectVersionTagging); + h.insert(Action::DeleteObjectVersion); + h.insert(Action::DeleteObjectVersionTagging); + h.insert(Action::PutObjectVersionTagging); + h.insert(Action::ReplicateObject); + h.insert(Action::ReplicateDelete); + h.insert(Action::ReplicateTags); + h.insert(Action::GetObjectVersionForReplication); + h.insert(Action::RestoreObject); + h.insert(Action::ResetBucketReplicationState); + h.insert(Action::PutObjectFanOut); + h.insert(Action::GetObjectAttributes); + h.insert(Action::GetObjectVersionAttributes); + h + }; +} + impl Action { - // 将字符串转换为Action枚举 - fn from_str(s: &str) -> Option { + pub fn is_object_action(&self) -> bool { + for act in SUPPORT_OBJCET_ACTIONS.iter() { + if self.is_match(act) { + return true; + } + } + false + } + pub fn is_match(&self, a: &Action) -> bool { + utils::wildcard::match_pattern(&self.clone().as_str(), &a.clone().as_str()) + } + + fn as_str(self) -> &'static str { + match self { + Action::AbortMultipartUpload => "s3:AbortMultipartUpload", + Action::CreateBucket => "s3:CreateBucket", + Action::DeleteBucket => "s3:DeleteBucket", + Action::ForceDeleteBucket => "s3:ForceDeleteBucket", + Action::DeleteBucketPolicy => "s3:DeleteBucketPolicy", + Action::DeleteBucketCors => "s3:DeleteBucketCors", + Action::DeleteObject => "s3:DeleteObject", + Action::GetBucketLocation => "s3:GetBucketLocation", + Action::GetBucketNotification => "s3:GetBucketNotification", + Action::GetBucketPolicy => "s3:GetBucketPolicy", + Action::GetBucketCors => "s3:GetBucketCors", + Action::GetObject => "s3:GetObject", + Action::GetObjectAttributes => "s3:GetObjectAttributes", + Action::HeadBucket => "s3:HeadBucket", + Action::ListAllMyBuckets => "s3:ListAllMyBuckets", + Action::ListBucket => "s3:ListBucket", + Action::GetBucketPolicyStatus => "s3:GetBucketPolicyStatus", + Action::ListBucketVersions => "s3:ListBucketVersions", + Action::ListBucketMultipartUploads => "s3:ListBucketMultipartUploads", + Action::ListenNotification => "s3:ListenNotification", + Action::ListenBucketNotification => "s3:ListenBucketNotification", + Action::ListMultipartUploadParts => "s3:ListMultipartUploadParts", + Action::PutLifecycleConfiguration => "s3:PutLifecycleConfiguration", + Action::GetLifecycleConfiguration => "s3:GetLifecycleConfiguration", + Action::PutBucketNotification => "s3:PutBucketNotification", + Action::PutBucketPolicy => "s3:PutBucketPolicy", + Action::PutBucketCors => "s3:PutBucketCors", + Action::PutObject => "s3:PutObject", + Action::DeleteObjectVersion => "s3:DeleteObjectVersion", + Action::DeleteObjectVersionTagging => "s3:DeleteObjectVersionTagging", + Action::GetObjectVersion => "s3:GetObjectVersion", + Action::GetObjectVersionAttributes => "s3:GetObjectVersionAttributes", + Action::GetObjectVersionTagging => "s3:GetObjectVersionTagging", + Action::PutObjectVersionTagging => "s3:PutObjectVersionTagging", + Action::BypassGovernanceRetention => "s3:BypassGovernanceRetention", + Action::PutObjectRetention => "s3:PutObjectRetention", + Action::GetObjectRetention => "s3:GetObjectRetention", + Action::GetObjectLegalHold => "s3:GetObjectLegalHold", + Action::PutObjectLegalHold => "s3:PutObjectLegalHold", + Action::GetBucketObjectLockConfiguration => "s3:GetBucketObjectLockConfiguration", + Action::PutBucketObjectLockConfiguration => "s3:PutBucketObjectLockConfiguration", + Action::GetBucketTagging => "s3:GetBucketTagging", + Action::PutBucketTagging => "s3:PutBucketTagging", + Action::GetObjectTagging => "s3:GetObjectTagging", + Action::PutObjectTagging => "s3:PutObjectTagging", + Action::DeleteObjectTagging => "s3:DeleteObjectTagging", + Action::PutBucketEncryption => "s3:PutEncryptionConfiguration", + Action::GetBucketEncryption => "s3:GetEncryptionConfiguration", + Action::PutBucketVersioning => "s3:PutBucketVersioning", + Action::GetBucketVersioning => "s3:GetBucketVersioning", + Action::PutReplicationConfiguration => "s3:GetReplicationConfiguration", + Action::GetReplicationConfiguration => "s3:PutReplicationConfiguration", + Action::ReplicateObject => "s3:ReplicateObject", + Action::ReplicateDelete => "s3:ReplicateDelete", + Action::ReplicateTags => "s3:ReplicateTags", + Action::GetObjectVersionForReplication => "s3:GetObjectVersionForReplication", + Action::RestoreObject => "s3:RestoreObject", + Action::ResetBucketReplicationState => "s3:ResetBucketReplicationState", + Action::PutObjectFanOut => "s3:PutObjectFanOut", + Action::AllActions => "s3:*", + } + } + + fn _from_str(s: &str) -> Option { match s { "s3:AbortMultipartUpload" => Some(Action::AbortMultipartUpload), "s3:CreateBucket" => Some(Action::CreateBucket), @@ -98,8 +303,8 @@ impl Action { "s3:ListenNotification" => Some(Action::ListenNotification), "s3:ListenBucketNotification" => Some(Action::ListenBucketNotification), "s3:ListMultipartUploadParts" => Some(Action::ListMultipartUploadParts), - "s3:PutLifecycleConfiguration" => Some(Action::PutBucketLifecycle), - "s3:GetLifecycleConfiguration" => Some(Action::GetBucketLifecycle), + "s3:PutLifecycleConfiguration" => Some(Action::PutLifecycleConfiguration), + "s3:GetLifecycleConfiguration" => Some(Action::GetLifecycleConfiguration), "s3:PutBucketNotification" => Some(Action::PutBucketNotification), "s3:PutBucketPolicy" => Some(Action::PutBucketPolicy), "s3:PutBucketCors" => Some(Action::PutBucketCors), @@ -140,3 +345,187 @@ impl Action { } } } + +pub struct ActionConditionKeyMap(HashMap); + +impl ActionConditionKeyMap { + pub fn lookup(&self, action: &Action) -> KeySet { + let common_keys: Vec = COMMOM_KEYS.iter().map(|v| v.to_key()).collect(); + + let mut merged_keys = KeySet::from_keys(&common_keys); + + for (act, key) in self.0.iter() { + if action.is_match(act) { + merged_keys.merge(key); + } + } + + merged_keys + } +} + +lazy_static! { + pub static ref IAMActionConditionKeyMap: ActionConditionKeyMap = create_action_condition_key_map(); +} + +fn create_action_condition_key_map() -> ActionConditionKeyMap { + let common_keys: Vec = COMMOM_KEYS.iter().map(|v| v.to_key()).collect(); + let all_support_keys: Vec = ALL_SUPPORT_KEYS.iter().map(|v| v.to_key()).collect(); + + let mut map = HashMap::new(); + + map.insert(Action::AllActions, KeySet::from_keys(&all_support_keys)); + map.insert(Action::AbortMultipartUpload, KeySet::from_keys(&common_keys)); + map.insert(Action::CreateBucket, KeySet::from_keys(&common_keys)); + + let mut delete_obj_keys = common_keys.clone(); + delete_obj_keys.push(KeyName::S3VersionID.to_key()); + map.insert(Action::DeleteObject, KeySet::from_keys(&delete_obj_keys)); + + map.insert(Action::GetBucketLocation, KeySet::from_keys(&common_keys)); + map.insert(Action::GetBucketPolicyStatus, KeySet::from_keys(&common_keys)); + + let mut get_obj_keys = common_keys.clone(); + get_obj_keys.extend(vec![ + KeyName::S3XAmzServerSideEncryption.to_key(), + KeyName::S3XAmzServerSideEncryptionCustomerAlgorithm.to_key(), + KeyName::S3XAmzServerSideEncryptionAwsKmsKeyID.to_key(), + KeyName::S3VersionID.to_key(), + KeyName::ExistingObjectTag.to_key(), + ]); + map.insert(Action::DeleteObject, KeySet::from_keys(&get_obj_keys)); + + map.insert(Action::HeadBucket, KeySet::from_keys(&common_keys)); + + let mut get_obj_attr_keys = common_keys.clone(); + get_obj_attr_keys.push(KeyName::ExistingObjectTag.to_key()); + map.insert(Action::DeleteObject, KeySet::from_keys(&get_obj_attr_keys)); + + let mut get_obj_ver_attr_keys = common_keys.clone(); + get_obj_ver_attr_keys.extend(vec![KeyName::S3VersionID.to_key(), KeyName::ExistingObjectTag.to_key()]); + map.insert(Action::DeleteObject, KeySet::from_keys(&get_obj_ver_attr_keys)); + + map.insert(Action::ListAllMyBuckets, KeySet::from_keys(&common_keys)); + + let mut list_bucket_keys = common_keys.clone(); + list_bucket_keys.extend(vec![ + KeyName::S3Prefix.to_key(), + KeyName::S3Delimiter.to_key(), + KeyName::S3MaxKeys.to_key(), + ]); + map.insert(Action::ListBucket, KeySet::from_keys(&list_bucket_keys)); + map.insert(Action::ListBucketVersions, KeySet::from_keys(&list_bucket_keys)); + + map.insert(Action::ListBucketMultipartUploads, KeySet::from_keys(&common_keys)); + map.insert(Action::ListenNotification, KeySet::from_keys(&common_keys)); + map.insert(Action::ListenBucketNotification, KeySet::from_keys(&common_keys)); + map.insert(Action::ListMultipartUploadParts, KeySet::from_keys(&common_keys)); + + let mut put_obj_keys = common_keys.clone(); + put_obj_keys.extend(vec![ + KeyName::S3XAmzCopySource.to_key(), + KeyName::S3XAmzServerSideEncryption.to_key(), + KeyName::S3XAmzServerSideEncryptionCustomerAlgorithm.to_key(), + KeyName::S3XAmzServerSideEncryptionAwsKmsKeyID.to_key(), + KeyName::S3XAmzMetadataDirective.to_key(), + KeyName::S3XAmzStorageClass.to_key(), + KeyName::S3VersionID.to_key(), + KeyName::S3ObjectLockRetainUntilDate.to_key(), + KeyName::S3ObjectLockMode.to_key(), + KeyName::S3ObjectLockLegalHold.to_key(), + KeyName::RequestObjectTagKeys.to_key(), + KeyName::RequestObjectTag.to_key(), + ]); + map.insert(Action::PutObject, KeySet::from_keys(&put_obj_keys)); + + let mut put_obj_retention_keys = common_keys.clone(); + put_obj_retention_keys.extend(vec![ + KeyName::S3XAmzServerSideEncryption.to_key(), + KeyName::S3XAmzServerSideEncryptionCustomerAlgorithm.to_key(), + KeyName::S3XAmzServerSideEncryptionAwsKmsKeyID.to_key(), + KeyName::S3ObjectLockRemainingRetentionDays.to_key(), + KeyName::S3ObjectLockRetainUntilDate.to_key(), + KeyName::S3ObjectLockMode.to_key(), + KeyName::S3VersionID.to_key(), + ]); + map.insert(Action::PutObjectRetention, KeySet::from_keys(&put_obj_retention_keys)); + + let mut get_obj_retention_keys = common_keys.clone(); + get_obj_retention_keys.extend(vec![ + KeyName::S3XAmzServerSideEncryption.to_key(), + KeyName::S3XAmzServerSideEncryptionCustomerAlgorithm.to_key(), + KeyName::S3XAmzServerSideEncryptionAwsKmsKeyID.to_key(), + KeyName::S3VersionID.to_key(), + ]); + map.insert(Action::GetObjectRetention, KeySet::from_keys(&get_obj_retention_keys)); + + let mut put_obj_hold_keys = common_keys.clone(); + put_obj_hold_keys.extend(vec![ + KeyName::S3XAmzServerSideEncryption.to_key(), + KeyName::S3XAmzServerSideEncryptionCustomerAlgorithm.to_key(), + KeyName::S3XAmzServerSideEncryptionAwsKmsKeyID.to_key(), + KeyName::S3ObjectLockLegalHold.to_key(), + KeyName::S3VersionID.to_key(), + ]); + map.insert(Action::PutObjectLegalHold, KeySet::from_keys(&put_obj_hold_keys)); + + map.insert(Action::GetObjectLegalHold, KeySet::from_keys(&common_keys)); + + let mut bypass_governance_retention_keys = common_keys.clone(); + bypass_governance_retention_keys.extend(vec![ + KeyName::S3VersionID.to_key(), + KeyName::S3ObjectLockRemainingRetentionDays.to_key(), + KeyName::S3ObjectLockRetainUntilDate.to_key(), + KeyName::S3ObjectLockMode.to_key(), + KeyName::S3ObjectLockLegalHold.to_key(), + KeyName::RequestObjectTagKeys.to_key(), + KeyName::RequestObjectTag.to_key(), + ]); + map.insert(Action::BypassGovernanceRetention, KeySet::from_keys(&bypass_governance_retention_keys)); + + map.insert(Action::GetBucketObjectLockConfiguration, KeySet::from_keys(&common_keys)); + map.insert(Action::PutBucketObjectLockConfiguration, KeySet::from_keys(&common_keys)); + map.insert(Action::GetBucketTagging, KeySet::from_keys(&common_keys)); + + let mut put_bucket_tagging_keys = common_keys.clone(); + put_bucket_tagging_keys.extend(vec![KeyName::RequestObjectTagKeys.to_key(), KeyName::RequestObjectTag.to_key()]); + map.insert(Action::PutBucketTagging, KeySet::from_keys(&put_bucket_tagging_keys)); + + let mut put_object_tagging_keys = common_keys.clone(); + put_object_tagging_keys.extend(vec![ + KeyName::S3VersionID.to_key(), + KeyName::ExistingObjectTag.to_key(), + KeyName::RequestObjectTagKeys.to_key(), + KeyName::RequestObjectTag.to_key(), + ]); + map.insert(Action::PutObjectTagging, KeySet::from_keys(&put_object_tagging_keys)); + + let mut get_object_tagging_keys = common_keys.clone(); + get_object_tagging_keys.extend(vec![KeyName::S3VersionID.to_key(), KeyName::ExistingObjectTag.to_key()]); + map.insert(Action::GetObjectTagging, KeySet::from_keys(&get_object_tagging_keys)); + map.insert(Action::DeleteObjectTagging, KeySet::from_keys(&get_object_tagging_keys)); + + map.insert(Action::PutObjectVersionTagging, KeySet::from_keys(&put_object_tagging_keys)); + map.insert(Action::GetObjectVersionTagging, KeySet::from_keys(&get_object_tagging_keys)); + map.insert(Action::GetObjectVersion, KeySet::from_keys(&get_object_tagging_keys)); + + let mut delete_object_version_keys = common_keys.clone(); + delete_object_version_keys.extend(vec![KeyName::S3VersionID.to_key()]); + + map.insert(Action::DeleteObjectVersion, KeySet::from_keys(&delete_object_version_keys)); + map.insert(Action::DeleteObjectVersionTagging, KeySet::from_keys(&get_object_tagging_keys)); + + map.insert(Action::GetReplicationConfiguration, KeySet::from_keys(&common_keys)); + map.insert(Action::PutReplicationConfiguration, KeySet::from_keys(&common_keys)); + + map.insert(Action::ReplicateObject, KeySet::from_keys(&get_object_tagging_keys)); + map.insert(Action::ReplicateDelete, KeySet::from_keys(&get_object_tagging_keys)); + map.insert(Action::ReplicateTags, KeySet::from_keys(&get_object_tagging_keys)); + map.insert(Action::GetObjectVersionForReplication, KeySet::from_keys(&get_object_tagging_keys)); + + map.insert(Action::RestoreObject, KeySet::from_keys(&common_keys)); + map.insert(Action::ResetBucketReplicationState, KeySet::from_keys(&common_keys)); + map.insert(Action::PutObjectFanOut, KeySet::from_keys(&common_keys)); + + ActionConditionKeyMap(map) +} diff --git a/ecstore/src/bucket/policy/bucket_policy.rs b/ecstore/src/bucket/policy/bucket_policy.rs index 512bf8a82..e2abed39d 100644 --- a/ecstore/src/bucket/policy/bucket_policy.rs +++ b/ecstore/src/bucket/policy/bucket_policy.rs @@ -1,57 +1,267 @@ -use crate::error::Result; -use rmp_serde::Serializer as rmpSerializer; +use crate::error::{Error, Result}; +// use rmp_serde::Serializer as rmpSerializer; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use super::{ - action::{Action, ActionSet}, + action::{Action, ActionSet, IAMActionConditionKeyMap}, condition::function::Functions, effect::Effect, principal::Principal, resource::ResourceSet, }; +const DEFAULT_VERSION: &str = "2012-10-17"; + #[derive(Debug, Deserialize, Serialize, Default, Clone)] pub struct BucketPolicyArgs { - account_name: String, - groups: Vec, - action: Action, - bucket_name: String, - condition_values: HashMap>, - is_owner: bool, - object_name: String, + pub account_name: String, + pub groups: Vec, + pub action: Action, + pub bucket_name: String, + pub condition_values: HashMap>, + pub is_owner: bool, + pub object_name: String, } -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] +// #[serde(rename_all = "PascalCase", default)] pub struct BPStatement { - sid: String, - effect: Effect, - principal: Principal, - actions: ActionSet, + #[serde(rename = "Sid")] + pub sid: String, + #[serde(rename = "Effect")] + pub effect: Effect, + #[serde(rename = "Principal")] + pub principal: Principal, + #[serde(rename = "Action")] + pub actions: ActionSet, + #[serde(rename = "NotAction", default)] + pub not_actions: ActionSet, + #[serde(rename = "Resource")] + pub resources: ResourceSet, + #[serde(rename = "Condition", default)] + pub conditions: Functions, +} - not_actions: Option, - resources: ResourceSet, - conditions: Option, +impl BPStatement { + // pub fn equals(&self, other: &BPStatement) -> bool { + // if self.effect != other.effect { + // return false; + // } + + // if !self.principal.equals(other.principal) { + // return false; + // } + + // if !self.actions.equals(other.actions) { + // return false; + // } + // if !self.not_actions.equals(other.not_actions) { + // return false; + // } + // if !self.resources.equals(other.resources) { + // return false; + // } + // if !self.conditions.equals(other.conditions) { + // return false; + // } + + // true + // } + pub fn validate(&self, bucket: &str) -> Result<()> { + self.is_valid()?; + self.resources.validate_bucket(bucket) + } + pub fn is_valid(&self) -> Result<()> { + if !self.effect.is_valid() { + return Err(Error::msg(format!("invalid Effect {:?}", self.effect))); + } + + if !self.principal.is_valid() { + return Err(Error::msg(format!("invalid Principal {:?}", self.principal))); + } + + if self.actions.is_empty() && self.not_actions.is_empty() { + return Err(Error::msg("Action must not be empty")); + } + + if self.resources.as_ref().is_empty() { + return Err(Error::msg("Resource must not be empty")); + } + + for act in self.actions.as_ref() { + if act.is_object_action() { + if !self.resources.object_resource_exists() { + return Err(Error::msg(format!( + "unsupported object Resource found {:?} for action {:?}", + self.resources, act + ))); + } + } else { + if !self.resources.bucket_resource_exists() { + return Err(Error::msg(format!( + "unsupported bucket Resource found {:?} for action {:?}", + self.resources, act + ))); + } + } + + let key_diff = self.conditions.keys().difference(&IAMActionConditionKeyMap.lookup(act)); + if !key_diff.is_empty() { + return Err(Error::msg(format!( + "unsupported condition keys '{:?}' used for action '{:?}'", + key_diff, act + ))); + } + } + Ok(()) + } + fn is_allowed(&self, args: &BucketPolicyArgs) -> bool { + let check = || -> bool { + if !self.principal.is_match(&args.account_name) { + return false; + } + + if (!self.actions.is_match(&args.action) && !self.actions.is_empty()) || self.not_actions.is_match(&args.action) { + return false; + } + + let mut resource = args.bucket_name.clone(); + if !args.object_name.is_empty() { + if !args.object_name.starts_with("/") { + resource.push_str("/"); + } + + resource.push_str(&args.object_name); + } + + if !self.resources.is_match(&resource, &args.condition_values) { + return false; + } + + self.conditions.evaluate(&args.condition_values) + }; + + self.effect.is_allowed(check()) + } } #[derive(Debug, Deserialize, Serialize, Default, Clone)] +// #[serde(rename_all = "PascalCase", default)] pub struct BucketPolicy { + #[serde(rename = "ID", default)] pub id: String, + #[serde(rename = "Version")] pub version: String, + #[serde(rename = "Statement")] pub statements: Vec, } impl BucketPolicy { - pub fn marshal_msg(&self) -> Result> { - let mut buf = Vec::new(); + pub fn is_allowed(&self, args: &BucketPolicyArgs) -> bool { + for statement in self.statements.iter() { + if statement.effect == Effect::Deny { + if !statement.is_allowed(args) { + return false; + } + } + } - self.serialize(&mut rmpSerializer::new(&mut buf).with_struct_map())?; + if args.is_owner { + return true; + } + + for statement in self.statements.iter() { + if statement.effect == Effect::Allow { + if statement.is_allowed(args) { + return true; + } + } + } + + false + } + + pub fn validate(&self, bucket: &str) -> Result<()> { + self.is_valid()?; + for statement in self.statements.iter() { + statement.validate(bucket)?; + } + Ok(()) + } + + pub fn is_valid(&self) -> Result<()> { + if self.version.as_str() != DEFAULT_VERSION && self.version.is_empty() { + return Err(Error::msg(format!("invalid version {}", self.version))); + } + + for statement in self.statements.iter() { + if let Err(err) = statement.is_valid() { + return Err(err); + } + } + Ok(()) + } + + pub fn is_empty(&self) -> bool { + self.statements.is_empty() + } + + pub fn marshal_msg(&self) -> Result { + let buf = serde_json::to_string(self)?; Ok(buf) + + // let mut buf = Vec::new(); + // self.serialize(&mut rmpSerializer::new(&mut buf).with_struct_map())?; + + // Ok(buf) } pub fn unmarshal(buf: &[u8]) -> Result { - let t: BucketPolicy = rmp_serde::from_slice(buf)?; - Ok(t) + let mut p = serde_json::from_slice::(&buf)?; + p.drop_duplicate_statements(); + Ok(p) + + // let t: BucketPolicy = rmp_serde::from_slice(buf)?; + // Ok(t) + } + + fn drop_duplicate_statements(&mut self) { + let mut dups = HashMap::new(); + + for v in self.statements.iter() { + if let Ok(data) = serde_json::to_string(self) { + dups.insert(data, v); + } + } + + let mut news = Vec::new(); + + for (_, v) in dups { + news.push(v.clone()); + } + + self.statements = news; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bucket_policy() { + let json = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\",\"s3:ListBucketMultipartUploads\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::dada\"],\"Sid\":\"\"},{\"Action\":[\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\",\"s3:ListMultipartUploadParts\",\"s3:PutObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::dada/*\"],\"Sid\":\"sdf\"}]}"; + + let a = BucketPolicy::unmarshal(json.to_string().as_bytes()).unwrap(); + + println!("{:?}", a); + + let j = a.marshal_msg(); + + println!("{:?}", j); + + println!("{:?}", json); } } diff --git a/ecstore/src/bucket/policy/condition/function.rs b/ecstore/src/bucket/policy/condition/function.rs index 61c9090f8..48969a01f 100644 --- a/ecstore/src/bucket/policy/condition/function.rs +++ b/ecstore/src/bucket/policy/condition/function.rs @@ -1,23 +1,26 @@ -use super::name::Name; -use serde::{Deserialize, Serialize}; +use super::{ + key::{Key, KeySet}, + keyname::KeyName, + name::Name, +}; +use serde::{ + de::{MapAccess, Visitor}, + ser::SerializeMap, + Deserialize, Serialize, +}; use std::{ collections::{HashMap, HashSet}, - fmt::Debug, + fmt::{self, Debug, Display}, + marker::PhantomData, }; -#[derive(Debug, Clone)] -pub struct Key { - name: String, - variable: String, -} - // 定义ValueSet类型 pub type ValueSet = HashSet; // 定义Function trait -pub trait FunctionApi { +pub trait FunctionApi: 'static + Send + Sync { // evaluate方法 - fn evaluate(&self, values: &HashMap) -> bool; + fn evaluate(&self, values: &HashMap>) -> bool; // key方法 fn key(&self) -> Key; @@ -31,28 +34,265 @@ pub trait FunctionApi { // to_map方法 fn to_map(&self) -> HashMap; - // clone方法 - fn clone(&self) -> Box; + fn clone_box(&self) -> Box; } -// impl Debug for dyn Function { -// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -// write!(f, "{:?}", self.to_string()) +// #[derive(Debug, Deserialize, Serialize, Clone)] +// pub enum Function { +// Test(TestFunction), +// } + +// impl FunctionApi for Function { +// // evaluate方法 +// fn evaluate(&self, values: &HashMap>) -> bool { +// match self { +// Function::Test(f) => f.evaluate(values), +// } +// } + +// // key方法 +// fn key(&self) -> Key { +// match self { +// Function::Test(f) => f.key(), +// } +// } + +// // name方法 +// fn name(&self) -> Name { +// match self { +// Function::Test(f) => f.name(), +// } +// } + +// // String方法 +// fn to_string(&self) -> String { +// match self { +// Function::Test(f) => f.to_string(), +// } +// } + +// // to_map方法 +// fn to_map(&self) -> HashMap { +// match self { +// Function::Test(f) => f.to_map(), +// } +// } + +// fn clone_box(&self) -> Box { +// match self { +// Function::Test(f) => f.clone_box(), +// } +// } +// } + +// 定义Functions类型 +#[derive(Default)] +pub struct Functions(Vec>); + +impl Functions { + pub fn evaluate(&self, values: &HashMap>) -> bool { + for f in self.0.iter() { + if f.evaluate(values) { + return true; + } + } + + false + } + pub fn keys(&self) -> KeySet { + let mut set = KeySet::new(); + for f in self.0.iter() { + set.add(f.key()) + } + set + } +} + +impl Debug for Functions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let funs: Vec = self.0.iter().map(|v| v.to_string()).collect(); + f.debug_list().entries(funs.iter()).finish() + } +} + +impl Display for Functions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let funs: Vec = self.0.iter().map(|v| v.to_string()).collect(); + write!(f, "{:?}", funs) + } +} + +impl Clone for Functions { + fn clone(&self) -> Self { + let mut list = Vec::new(); + for v in self.0.iter() { + list.push(v.clone_box()) + } + + Functions(list) + } +} + +impl PartialEq for Functions { + fn eq(&self, other: &Self) -> bool { + if self.0.len() != other.0.len() { + return false; + } + + for v in self.0.iter() { + let s = v.to_string(); + let mut found = false; + for o in other.0.iter() { + if s == o.to_string() { + found = true; + break; + } + } + + if !found { + return false; + } + } + + true + } +} + +impl Eq for Functions {} + +type FunctionsMap = HashMap>; + +impl Serialize for Functions { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut nm: FunctionsMap = HashMap::new(); + for f in self.0.iter() { + let fname = f.name().to_string(); + + if !nm.contains_key(&fname) { + nm.insert(fname.clone(), HashMap::new()); + } + + for (k, v) in f.to_map() { + if let Some(hm) = nm.get_mut(&fname) { + hm.insert(k.to_string(), v); + } + } + } + + let mut map = serializer.serialize_map(Some(nm.len()))?; + for (k, v) in nm.iter() { + map.serialize_entry(k, v)?; + } + + map.end() + } +} + +struct MyMapVisitor { + marker: PhantomData FunctionsMap>, +} + +impl MyMapVisitor { + fn new() -> Self { + MyMapVisitor { marker: PhantomData } + } +} + +// This is the trait that Deserializers are going to be driving. There +// is one method for each type of data that our type knows how to +// deserialize from. There are many other methods that are not +// implemented here, for example deserializing from integers or strings. +// By default those methods will return an error, which makes sense +// because we cannot deserialize a MyMap from an integer or string. +impl<'de> Visitor<'de> for MyMapVisitor { + // The type that our Visitor is going to produce. + type Value = FunctionsMap; + + // Format a message stating what data this Visitor expects to receive. + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a very special map") + } + + // Deserialize MyMap from an abstract "map" provided by the + // Deserializer. The MapAccess input is a callback provided by + // the Deserializer to let us see each entry in the map. + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut map = FunctionsMap::with_capacity(access.size_hint().unwrap_or(0)); + + // While there are entries remaining in the input, add them + // into our map. + while let Some((key, value)) = access.next_entry()? { + map.insert(key, value); + } + + Ok(map) + } +} + +// This is the trait that informs Serde how to deserialize MyMap. +impl<'de> Deserialize<'de> for Functions { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + // Instantiate our Visitor and ask the Deserializer to drive + // it over the input data, resulting in an instance of MyMap. + let _map = deserializer.deserialize_map(MyMapVisitor::new())?; + + // TODO: FIXME: create functions from name + + Ok(Functions(Vec::new())) + } +} + +// impl<'de> Deserialize<'de> for Functions { +// fn deserialize(deserializer: D) -> Result +// where +// D: serde::Deserializer<'de>, +// { +// todo!() // } // } #[derive(Debug, Deserialize, Serialize, Default, Clone)] -enum Function { - #[default] - Test, -} +pub struct TestFunction {} -// 定义Functions类型 -#[derive(Deserialize, Serialize, Default, Clone)] -pub struct Functions(Vec); +impl FunctionApi for TestFunction { + // evaluate方法 + fn evaluate(&self, _values: &HashMap>) -> bool { + true + } -impl Debug for Functions { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("Functions").field(&self.0).finish() + // key方法 + fn key(&self) -> Key { + Key { + name: KeyName::JWTPrefUsername, + variable: "".to_string(), + } + } + + // name方法 + fn name(&self) -> Name { + Name::StringEquals + } + + // String方法 + fn to_string(&self) -> String { + Name::StringEquals.to_string() + } + + // to_map方法 + fn to_map(&self) -> HashMap { + HashMap::new() + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) } } diff --git a/ecstore/src/bucket/policy/condition/key.rs b/ecstore/src/bucket/policy/condition/key.rs new file mode 100644 index 000000000..34d145f37 --- /dev/null +++ b/ecstore/src/bucket/policy/condition/key.rs @@ -0,0 +1,159 @@ +use super::keyname::{KeyName, ALL_SUPPORT_KEYS}; +use crate::error::Error; +use serde::{Deserialize, Serialize}; +use std::{collections::HashSet, fmt, str::FromStr}; + +// 定义Key结构体 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Key { + pub name: KeyName, + pub variable: String, +} + +impl Key { + pub fn new(name: KeyName, variable: String) -> Self { + Key { name, variable } + } + // IsValid - checks if key is valid or not. + fn is_valid(&self) -> bool { + ALL_SUPPORT_KEYS.iter().any(|supported| self.name == *supported) + } + + // Is - checks if this key has the same key name or not. + pub fn is(&self, name: &KeyName) -> bool { + self.name == *name + } + + pub fn to_string(&self) -> String { + if !self.variable.is_empty() { + format!("{}/{}", self.name.as_str(), self.variable) + } else { + self.name.to_string() + } + } + + // VarName - returns variable key name, such as "${aws:username}" + pub fn var_name(&self) -> String { + self.name.var_name() + } + + // Name - returns key name which is stripped value of prefixes "aws:" and "s3:" + pub fn name(&self) -> String { + if !self.variable.is_empty() { + format!("{}{}", self.name.name(), self.variable) + } else { + self.name.name().to_string() + } + } +} + +impl FromStr for Key { + type Err = Error; + + fn from_str(s: &str) -> Result { + let (name, variable) = if let Some(pos) = s.find('/') { + (&s[..pos], &s[pos + 1..]) + } else { + (s, "") + }; + + let keyname = KeyName::from_str(name)?; + + let key = Key { + name: keyname, + variable: variable.to_string(), + }; + + if key.is_valid() { + Ok(key) + } else { + Err(Error::msg(format!("invalid condition key '{}'", s))) + } + } +} + +impl Serialize for Key { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + +impl<'de> Deserialize<'de> for Key { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let s: String = Deserialize::deserialize(deserializer)?; + Key::from_str(s.as_str()).map_err(serde::de::Error::custom) + } +} + +impl fmt::Display for Key { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_string()) + } +} + +#[derive(Debug, Default)] +pub struct KeySet(HashSet); + +impl KeySet { + pub fn new() -> Self { + KeySet(HashSet::new()) + } + // Add - add a key to key set + pub fn add(&mut self, key: Key) { + self.0.insert(key); + } + + // Merge merges two key sets, duplicates are overwritten + pub fn merge(&mut self, other: &KeySet) { + for key in &other.0 { + self.add(key.clone()); + } + } + + // Match matches the input key name with current keySet + pub fn match_key(&self, key: &Key) -> bool { + self.0.contains(key) + } + + // Difference - returns a key set contains difference of two keys + pub fn difference(&self, other: &KeySet) -> KeySet { + let mut result = KeySet::default(); + for key in &self.0 { + if !other.match_key(key) { + result.add(key.clone()); + } + } + result + } + + // IsEmpty - returns whether key set is empty or not + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + // ToSlice - returns slice of keys + fn to_slice(&self) -> Vec { + self.0.iter().cloned().collect() + } + + // NewKeySet - returns new KeySet contains given keys + pub fn from_keys(keys: &Vec) -> KeySet { + let mut set = KeySet::default(); + for key in keys { + set.add(key.clone()); + } + set + } +} + +impl fmt::Display for KeySet { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self.to_slice()) + } +} diff --git a/ecstore/src/bucket/policy/condition/mod.rs b/ecstore/src/bucket/policy/condition/mod.rs index 1ef3a2495..46e6fa038 100644 --- a/ecstore/src/bucket/policy/condition/mod.rs +++ b/ecstore/src/bucket/policy/condition/mod.rs @@ -1,3 +1,4 @@ pub mod function; +pub mod key; pub mod keyname; pub mod name; diff --git a/ecstore/src/bucket/policy/condition/name.rs b/ecstore/src/bucket/policy/condition/name.rs index e8eecc344..be8541db8 100644 --- a/ecstore/src/bucket/policy/condition/name.rs +++ b/ecstore/src/bucket/policy/condition/name.rs @@ -29,40 +29,47 @@ pub enum Name { ForAnyValue, } -// 实现Display trait用于打印 -impl std::fmt::Display for Name { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match *self { - Name::StringEquals => "StringEquals", - Name::StringNotEquals => "StringNotEquals", - Name::StringEqualsIgnoreCase => "StringEqualsIgnoreCase", - Name::StringNotEqualsIgnoreCase => "StringNotEqualsIgnoreCase", - Name::StringLike => "StringLike", - Name::StringNotLike => "StringNotLike", - Name::BinaryEquals => "BinaryEquals", - Name::IpAddress => "IpAddress", - Name::NotIpAddress => "NotIpAddress", - Name::Null => "Null", - Name::Bool => "Bool", - Name::NumericEquals => "NumericEquals", - Name::NumericNotEquals => "NumericNotEquals", - Name::NumericLessThan => "NumericLessThan", - Name::NumericLessThanEquals => "NumericLessThanEquals", - Name::NumericGreaterThan => "NumericGreaterThan", - Name::NumericGreaterThanIfExists => "NumericGreaterThanIfExists", - Name::NumericGreaterThanEquals => "NumericGreaterThanEquals", - Name::DateEquals => "DateEquals", - Name::DateNotEquals => "DateNotEquals", - Name::DateLessThan => "DateLessThan", - Name::DateLessThanEquals => "DateLessThanEquals", - Name::DateGreaterThan => "DateGreaterThan", - Name::DateGreaterThanEquals => "DateGreaterThanEquals", - Name::ForAllValues => "ForAllValues", - Name::ForAnyValue => "ForAnyValue", - } - ) +impl Name { + pub fn as_str(&self) -> &'static str { + match self { + Name::StringEquals => "StringEquals", + Name::StringNotEquals => "StringNotEquals", + Name::StringEqualsIgnoreCase => "StringEqualsIgnoreCase", + Name::StringNotEqualsIgnoreCase => "StringNotEqualsIgnoreCase", + Name::StringLike => "StringLike", + Name::StringNotLike => "StringNotLike", + Name::BinaryEquals => "BinaryEquals", + Name::IpAddress => "IpAddress", + Name::NotIpAddress => "NotIpAddress", + Name::Null => "Null", + Name::Bool => "Bool", + Name::NumericEquals => "NumericEquals", + Name::NumericNotEquals => "NumericNotEquals", + Name::NumericLessThan => "NumericLessThan", + Name::NumericLessThanEquals => "NumericLessThanEquals", + Name::NumericGreaterThan => "NumericGreaterThan", + Name::NumericGreaterThanIfExists => "NumericGreaterThanIfExists", + Name::NumericGreaterThanEquals => "NumericGreaterThanEquals", + Name::DateEquals => "DateEquals", + Name::DateNotEquals => "DateNotEquals", + Name::DateLessThan => "DateLessThan", + Name::DateLessThanEquals => "DateLessThanEquals", + Name::DateGreaterThan => "DateGreaterThan", + Name::DateGreaterThanEquals => "DateGreaterThanEquals", + Name::ForAllValues => "ForAllValues", + Name::ForAnyValue => "ForAnyValue", + } + } +} + +// impl ToString for Name { +// fn to_string(&self) -> String { +// self.as_str().to_string() +// } +// } + +impl std::fmt::Display for Name { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.as_str()) } } diff --git a/ecstore/src/bucket/policy/effect.rs b/ecstore/src/bucket/policy/effect.rs index fc6ff0e28..6ec86ef67 100644 --- a/ecstore/src/bucket/policy/effect.rs +++ b/ecstore/src/bucket/policy/effect.rs @@ -2,12 +2,30 @@ use serde::{Deserialize, Serialize}; use std::str::FromStr; #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)] +#[serde(rename_all = "PascalCase")] pub enum Effect { #[default] Allow, Deny, } +impl Effect { + pub fn is_allowed(self, b: bool) -> bool { + if self == Effect::Allow { + b + } else { + !b + } + } + + pub fn is_valid(self) -> bool { + match self { + Effect::Allow => true, + Effect::Deny => true, + } + } +} + // 实现从字符串解析Effect的功能 impl FromStr for Effect { type Err = (); diff --git a/ecstore/src/bucket/policy/principal.rs b/ecstore/src/bucket/policy/principal.rs index 9a0be7d10..4defecb1c 100644 --- a/ecstore/src/bucket/policy/principal.rs +++ b/ecstore/src/bucket/policy/principal.rs @@ -1,7 +1,25 @@ use serde::{Deserialize, Serialize}; use std::collections::HashSet; -#[derive(Debug, Clone, Deserialize, Serialize, Default)] +use crate::utils; + +#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)] +#[serde(rename_all = "PascalCase", default)] pub struct Principal { + #[serde(rename = "AWS")] aws: HashSet, } + +impl Principal { + pub fn is_valid(&self) -> bool { + !self.aws.is_empty() + } + pub fn is_match(&self, parincipal: &str) -> bool { + for pattern in self.aws.iter() { + if utils::wildcard::match_simple(&pattern, parincipal) { + return true; + } + } + return false; + } +} diff --git a/ecstore/src/bucket/policy/resource.rs b/ecstore/src/bucket/policy/resource.rs index 81576b26e..1ef69ab6d 100644 --- a/ecstore/src/bucket/policy/resource.rs +++ b/ecstore/src/bucket/policy/resource.rs @@ -1,5 +1,13 @@ +use crate::error::{Error, Result}; +use crate::{ + bucket::policy::condition::keyname::COMMOM_KEYS, + utils::{self, wildcard}, +}; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use std::{ + collections::{HashMap, HashSet}, + str::FromStr, +}; // 定义ResourceARNType枚举类型 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, Default)] @@ -10,16 +18,234 @@ pub enum ResourceARNType { ResourceARNKMS, } -// // 定义资源ARN前缀 -// const RESOURCE_ARN_PREFIX: &str = "arn:aws:s3:::"; -// const RESOURCE_ARN_KMS_PREFIX: &str = "arn:rustfs:kms::::"; - -// 定义Resource结构体 -#[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq, Hash, Clone)] -pub struct Resource { - pattern: String, - r#type: ResourceARNType, +impl ResourceARNType { + pub fn to_string(&self) -> String { + match self { + ResourceARNType::UnknownARN => "".to_string(), + ResourceARNType::ResourceARNS3 => RESOURCE_ARN_PREFIX.to_string(), + ResourceARNType::ResourceARNKMS => RESOURCE_ARN_KMS_PREFIX.to_string(), + } + } } -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +// 定义资源ARN前缀 +const RESOURCE_ARN_PREFIX: &str = "arn:aws:s3:::"; +const RESOURCE_ARN_KMS_PREFIX: &str = "arn:rustfs:kms::::"; + +// 定义Resource结构体 +#[derive(Debug, Default, PartialEq, Eq, Hash, Clone)] +pub struct Resource { + pattern: String, + rtype: ResourceARNType, +} + +impl Resource { + pub fn validate_bucket(&self, bucket: &str) -> Result<()> { + self.validate()?; + if !wildcard::match_pattern(&self.pattern, bucket) + && !wildcard::match_as_pattern_prefix(&self.pattern, format!("{}/", bucket).as_str()) + { + return Err(Error::msg("bucket name does not match")); + } + Ok(()) + } + pub fn validate(&self) -> Result<()> { + if !self.is_valid() { + Err(Error::msg("invalid resource")) + } else { + Ok(()) + } + } + pub fn is_valid(&self) -> bool { + if self.rtype == ResourceARNType::UnknownARN { + return false; + } + if self.is_s3() { + if self.pattern.starts_with("/") { + return false; + } + } + if self.is_kms() { + if self.pattern.as_bytes().iter().any(|&v| v == b'/' || v == b'\\' || v == b'.') { + return false; + } + } + + !self.pattern.is_empty() + } + pub fn is_s3(&self) -> bool { + self.rtype == ResourceARNType::ResourceARNS3 + } + pub fn is_kms(&self) -> bool { + self.rtype == ResourceARNType::ResourceARNKMS + } + pub fn is_bucket_pattern(&self) -> bool { + !self.pattern.contains("/") || self.pattern.eq("*") + } + pub fn is_object_pattern(&self) -> bool { + self.pattern.contains("/") || self.pattern.contains("*") + } + pub fn is_match(&self, res: &str, condition_values: &HashMap>) -> bool { + let mut pattern = res.to_string(); + if !condition_values.is_empty() { + for key in COMMOM_KEYS.iter() { + if let Some(vals) = condition_values.get(key.name()) { + if let Some(v0) = vals.get(0) { + pattern = pattern.replace(key.name(), &v0); + } + } + } + } + + let cp = utils::path::clean(res); + + if cp != "." && cp == pattern { + return true; + } + + wildcard::match_pattern(&pattern, res) + } + + pub fn to_string(&self) -> String { + format!("{}{}", self.rtype.to_string(), self.pattern) + } +} + +impl FromStr for Resource { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + if s.starts_with(RESOURCE_ARN_PREFIX) { + let pattern = { + if let Some(val) = s.strip_prefix(RESOURCE_ARN_PREFIX) { + val.to_string() + } else { + s.to_string() + } + }; + Ok(Self { + rtype: ResourceARNType::ResourceARNS3, + pattern, + }) + } else if s.starts_with(RESOURCE_ARN_KMS_PREFIX) { + let pattern = { + if let Some(val) = s.strip_prefix(RESOURCE_ARN_KMS_PREFIX) { + val.to_string() + } else { + s.to_string() + } + }; + Ok(Self { + rtype: ResourceARNType::ResourceARNS3, + pattern, + }) + } else { + Ok(Self { + rtype: ResourceARNType::UnknownARN, + pattern: "".to_string(), + }) + } + } +} + +impl Serialize for Resource { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + +impl<'de> Deserialize<'de> for Resource { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Resource; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("string resource") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + match Resource::from_str(value) { + Ok(res) => Ok(res), + Err(_) => Err(serde::de::Error::invalid_value(serde::de::Unexpected::Str(value), &self)), + } + } + } + + deserializer.deserialize_any(Visitor) + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(transparent)] pub struct ResourceSet(HashSet); + +impl ResourceSet { + pub fn validate_bucket(&self, bucket: &str) -> Result<()> { + for res in self.0.iter() { + if let Err(err) = res.validate_bucket(bucket) { + return Err(err); + } + } + Ok(()) + } + pub fn as_ref(&self) -> &HashSet { + &self.0 + } + pub fn is_match(&self, res: &str, condition_values: &HashMap>) -> bool { + for item in self.0.iter() { + if item.is_match(res, condition_values) { + return true; + } + } + + false + } + pub fn object_resource_exists(&self) -> bool { + for res in self.0.iter() { + if res.is_object_pattern() { + return true; + } + } + false + } + pub fn bucket_resource_exists(&self) -> bool { + for res in self.0.iter() { + if res.is_bucket_pattern() { + return true; + } + } + false + } +} + +// impl Serialize for ResourceSet { +// fn serialize(&self, serializer: S) -> Result +// where +// S: serde::Serializer, +// { +// let ress: Vec = self.0.iter().cloned().collect(); +// serializer.collect_seq(ress) +// } +// } + +// impl<'de> Deserialize<'de> for ResourceSet { +// fn deserialize(deserializer: D) -> Result +// where +// D: Deserializer<'de>, +// { +// let vec: Vec = Deserialize::deserialize(deserializer)?; +// let ha: HashSet = vec.into_iter().collect(); +// Ok(ResourceSet(ha)) +// } +// } diff --git a/ecstore/src/bucket/policy_sys.rs b/ecstore/src/bucket/policy_sys.rs new file mode 100644 index 000000000..c2c94cb84 --- /dev/null +++ b/ecstore/src/bucket/policy_sys.rs @@ -0,0 +1,33 @@ +use super::{ + error::BucketMetadataError, + get_bucket_metadata_sys, + policy::bucket_policy::{BucketPolicy, BucketPolicyArgs}, +}; +use crate::error::Result; +use tracing::warn; + +pub struct PolicySys {} + +impl PolicySys { + pub async fn is_allowed(args: &BucketPolicyArgs) -> bool { + match Self::get(&args.bucket_name).await { + Ok(cfg) => return cfg.is_allowed(args), + Err(err) => { + if !BucketMetadataError::BucketPolicyNotFound.is(&err) { + warn!("config get err {:?}", err); + } + () + } + } + + args.is_owner + } + pub async fn get(bucket: &str) -> Result { + let bucket_meta_sys_lock = get_bucket_metadata_sys().await; + let bucket_meta_sys = bucket_meta_sys_lock.write().await; + + let (cfg, _) = bucket_meta_sys.get_bucket_policy(bucket).await?; + + Ok(cfg) + } +} diff --git a/ecstore/src/disk/local.rs b/ecstore/src/disk/local.rs index fc3700a76..4eebed415 100644 --- a/ecstore/src/disk/local.rs +++ b/ecstore/src/disk/local.rs @@ -1643,7 +1643,7 @@ mod test { #[tokio::test] async fn test_make_volume() { - let p = "./testv"; + let p = "./testv0"; fs::create_dir_all(&p).await.unwrap(); let ep = match Endpoint::try_from(p) { @@ -1662,18 +1662,18 @@ mod test { println!("ppp :{:?}", &tmpp); - let volumes = vec!["a", "b", "c"]; + let volumes = vec!["a123", "b123", "c123"]; disk.make_volumes(volumes.clone()).await.unwrap(); disk.make_volumes(volumes.clone()).await.unwrap(); - fs::remove_dir_all(&p).await.unwrap(); + let _ = fs::remove_dir_all(&p).await; } #[tokio::test] async fn test_delete_volume() { - let p = "./testv"; + let p = "./testv1"; fs::create_dir_all(&p).await.unwrap(); let ep = match Endpoint::try_from(p) { @@ -1692,12 +1692,12 @@ mod test { println!("ppp :{:?}", &tmpp); - let volumes = vec!["a", "b", "c"]; + let volumes = vec!["a123", "b123", "c123"]; disk.make_volumes(volumes.clone()).await.unwrap(); disk.delete_volume("a").await.unwrap(); - fs::remove_dir_all(&p).await.unwrap(); + let _ = fs::remove_dir_all(&p).await; } } diff --git a/ecstore/src/utils/net.rs b/ecstore/src/utils/net.rs index 91f453e86..6301051ce 100644 --- a/ecstore/src/utils/net.rs +++ b/ecstore/src/utils/net.rs @@ -127,10 +127,10 @@ mod test { #[test] fn test_check_local_server_addr() { let test_cases = [ - (":54321", Ok(())), + // (":54321", Ok(())), ("localhost:54321", Ok(())), ("0.0.0.0:9000", Ok(())), - (":0", Ok(())), + // (":0", Ok(())), ("localhost", Err(Error::from_string("invalid socket address"))), ("", Err(Error::from_string("invalid socket address"))), ( diff --git a/ecstore/src/utils/path.rs b/ecstore/src/utils/path.rs index 79bfc6ced..9b714d629 100644 --- a/ecstore/src/utils/path.rs +++ b/ecstore/src/utils/path.rs @@ -37,3 +37,164 @@ pub fn retain_slash(s: &str) -> String { format!("{}{}", s, SLASH_SEPARATOR) } } + +pub struct LazyBuf { + s: String, + buf: Option>, + w: usize, +} + +impl LazyBuf { + pub fn new(s: String) -> Self { + LazyBuf { s, buf: None, w: 0 } + } + + pub fn index(&self, i: usize) -> u8 { + if let Some(ref buf) = self.buf { + buf[i] + } else { + self.s.as_bytes()[i] + } + } + + pub 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; + } + let mut new_buf = vec![0; self.s.len()]; + new_buf[..self.w].copy_from_slice(&self.s.as_bytes()[..self.w]); + self.buf = Some(new_buf); + } + + if let Some(ref mut buf) = self.buf { + buf[self.w] = c; + self.w += 1; + } + } + + pub fn string(&self) -> String { + if let Some(ref buf) = self.buf { + String::from_utf8(buf[..self.w].to_vec()).unwrap() + } else { + self.s[..self.w].to_string() + } + } +} + +pub fn clean(path: &str) -> String { + if path.is_empty() { + return ".".to_string(); + } + + let rooted = path.starts_with('/'); + let n = path.len(); + let mut out = LazyBuf::new(path.to_string()); + let mut r = 0; + let mut dotdot = 0; + + if rooted { + out.append(b'/'); + r = 1; + dotdot = 1; + } + + while r < n { + match path.as_bytes()[r] { + b'/' => { + // Empty path element + r += 1; + } + b'.' if r + 1 == n || path.as_bytes()[r + 1] == b'/' => { + // . element + r += 1; + } + b'.' if path.as_bytes()[r + 1] == b'.' && (r + 2 == n || path.as_bytes()[r + 2] == b'/') => { + // .. element: remove to last / + r += 2; + + if out.w > dotdot { + // Can backtrack + out.w -= 1; + while out.w > dotdot && out.index(out.w) != b'/' { + out.w -= 1; + } + } else if !rooted { + // Cannot backtrack but not rooted, so append .. element. + if out.w > 0 { + out.append(b'/'); + } + out.append(b'.'); + out.append(b'.'); + dotdot = out.w; + } + } + _ => { + // Real path element. + // Add slash if needed + if (rooted && out.w != 1) || (!rooted && out.w != 0) { + out.append(b'/'); + } + + // Copy element + while r < n && path.as_bytes()[r] != b'/' { + out.append(path.as_bytes()[r]); + r += 1; + } + } + } + } + + // Turn empty string into "." + if out.w == 0 { + return ".".to_string(); + } + + out.string() +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_clean() { + assert_eq!(clean(""), "."); + assert_eq!(clean("abc"), "abc"); + assert_eq!(clean("abc/def"), "abc/def"); + assert_eq!(clean("a/b/c"), "a/b/c"); + assert_eq!(clean("."), "."); + assert_eq!(clean(".."), ".."); + assert_eq!(clean("../.."), "../.."); + assert_eq!(clean("../../abc"), "../../abc"); + assert_eq!(clean("/abc"), "/abc"); + assert_eq!(clean("/"), "/"); + assert_eq!(clean("abc/"), "abc"); + assert_eq!(clean("abc/def/"), "abc/def"); + assert_eq!(clean("a/b/c/"), "a/b/c"); + assert_eq!(clean("./"), "."); + assert_eq!(clean("../"), ".."); + assert_eq!(clean("../../"), "../.."); + assert_eq!(clean("/abc/"), "/abc"); + assert_eq!(clean("abc//def//ghi"), "abc/def/ghi"); + assert_eq!(clean("//abc"), "/abc"); + assert_eq!(clean("///abc"), "/abc"); + assert_eq!(clean("//abc//"), "/abc"); + assert_eq!(clean("abc//"), "abc"); + assert_eq!(clean("abc/./def"), "abc/def"); + assert_eq!(clean("/./abc/def"), "/abc/def"); + assert_eq!(clean("abc/."), "abc"); + assert_eq!(clean("abc/./../def"), "def"); + assert_eq!(clean("abc//./../def"), "def"); + assert_eq!(clean("abc/../../././../def"), "../../def"); + + assert_eq!(clean("abc/def/ghi/../jkl"), "abc/def/jkl"); + assert_eq!(clean("abc/def/../ghi/../jkl"), "abc/jkl"); + assert_eq!(clean("abc/def/.."), "abc"); + assert_eq!(clean("abc/def/../.."), "."); + assert_eq!(clean("/abc/def/../.."), "/"); + assert_eq!(clean("abc/def/../../.."), ".."); + assert_eq!(clean("/abc/def/../../.."), "/"); + assert_eq!(clean("abc/def/../../../ghi/jkl/../../../mno"), "../../mno"); + } +} diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index c19d83577..987e80472 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -22,7 +22,7 @@ use hyper_util::{ 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, process::exit, str::FromStr}; +use std::{io::IsTerminal, net::SocketAddr, str::FromStr}; use tokio::net::TcpListener; use tonic::{metadata::MetadataValue, Request, Status}; use tracing::{debug, info}; diff --git a/rustfs/src/storage/ecfs.rs b/rustfs/src/storage/ecfs.rs index 7ea11fbb7..f31101165 100644 --- a/rustfs/src/storage/ecfs.rs +++ b/rustfs/src/storage/ecfs.rs @@ -1,7 +1,11 @@ use bytes::Bytes; +use ecstore::bucket::error::BucketMetadataError; use ecstore::bucket::get_bucket_metadata_sys; +use ecstore::bucket::metadata::BUCKET_POLICY_CONFIG; use ecstore::bucket::metadata::BUCKET_TAGGING_CONFIG; use ecstore::bucket::metadata::BUCKET_VERSIONING_CONFIG; +use ecstore::bucket::policy::bucket_policy::BucketPolicy; +use ecstore::bucket::policy_sys::PolicySys; use ecstore::bucket::tags::Tags; use ecstore::bucket::versioning::State as VersioningState; use ecstore::bucket::versioning::Versioning; @@ -21,7 +25,6 @@ use ecstore::store_api::PutObjReader; use ecstore::store_api::StorageAPI; use futures::pin_mut; use futures::{Stream, StreamExt}; -use http::status; use http::HeaderMap; use log::warn; use s3s::dto::*; @@ -267,16 +270,6 @@ impl S3 for FS { Ok(S3Response::new(output)) } - #[tracing::instrument(level = "debug", skip(self))] - async fn get_object_lock_configuration( - &self, - _req: S3Request, - ) -> S3Result> { - // mc cp step 1 - let output = GetObjectLockConfigurationOutput::default(); - Ok(S3Response::new(output)) - } - #[tracing::instrument( level = "debug", skip(self, req), @@ -933,6 +926,207 @@ impl S3 for FS { Ok(S3Response::new(PutBucketVersioningOutput {})) } + + async fn get_bucket_policy_status( + &self, + _req: S3Request, + ) -> S3Result> { + Err(s3_error!(NotImplemented, "GetBucketPolicyStatus is not implemented yet")) + } + + async fn get_bucket_policy(&self, req: S3Request) -> S3Result> { + let GetBucketPolicyInput { bucket, .. } = req.input; + + let layer = new_object_layer_fn(); + let lock = layer.read().await; + let store = lock + .as_ref() + .ok_or_else(|| S3Error::with_message(S3ErrorCode::InternalError, "Not init"))?; + + if let Err(e) = store.get_bucket_info(&bucket, &BucketOptions::default()).await { + if DiskError::VolumeNotFound.is(&e) { + return Err(s3_error!(NoSuchBucket)); + } else { + return Err(S3Error::with_message(S3ErrorCode::InternalError, format!("{}", e))); + } + } + + let cfg = match PolicySys::get(&bucket).await { + Ok(res) => res, + Err(err) => { + if BucketMetadataError::BucketPolicyNotFound.is(&err) { + return Err(s3_error!(NoSuchBucketPolicy)); + } + return Err(S3Error::with_message(S3ErrorCode::InternalError, format!("{}", err))); + } + }; + + let policys = try_!(cfg.marshal_msg()); + + Ok(S3Response::new(GetBucketPolicyOutput { policy: Some(policys) })) + } + + async fn put_bucket_policy(&self, req: S3Request) -> S3Result> { + let PutBucketPolicyInput { bucket, policy, .. } = req.input; + + let layer = new_object_layer_fn(); + let lock = layer.read().await; + let store = lock + .as_ref() + .ok_or_else(|| S3Error::with_message(S3ErrorCode::InternalError, "Not init"))?; + + if let Err(e) = store.get_bucket_info(&bucket, &BucketOptions::default()).await { + if DiskError::VolumeNotFound.is(&e) { + return Err(s3_error!(NoSuchBucket)); + } else { + return Err(S3Error::with_message(S3ErrorCode::InternalError, format!("{}", e))); + } + } + + let cfg = try_!(BucketPolicy::unmarshal(policy.as_bytes())); + warn!("put_bucket_policy struct {:?}", &cfg); + if let Err(err) = cfg.validate(&bucket) { + warn!("put_bucket_policy input {:?}", &policy); + + warn!("cfg.validate err {:?}", err); + return Err(s3_error!(InvalidPolicyDocument)); + } + + let data = try_!(cfg.marshal_msg()); + + let bucket_meta_sys_lock = get_bucket_metadata_sys().await; + let mut bucket_meta_sys = bucket_meta_sys_lock.write().await; + + try_!(bucket_meta_sys.update(&bucket, BUCKET_POLICY_CONFIG, data.into()).await); + + Ok(S3Response::new(PutBucketPolicyOutput {})) + } + + async fn delete_bucket_policy( + &self, + req: S3Request, + ) -> S3Result> { + let DeleteBucketPolicyInput { bucket, .. } = req.input; + + let layer = new_object_layer_fn(); + let lock = layer.read().await; + let store = lock + .as_ref() + .ok_or_else(|| S3Error::with_message(S3ErrorCode::InternalError, "Not init"))?; + + if let Err(e) = store.get_bucket_info(&bucket, &BucketOptions::default()).await { + if DiskError::VolumeNotFound.is(&e) { + return Err(s3_error!(NoSuchBucket)); + } else { + return Err(S3Error::with_message(S3ErrorCode::InternalError, format!("{}", e))); + } + } + + let bucket_meta_sys_lock = get_bucket_metadata_sys().await; + let mut bucket_meta_sys = bucket_meta_sys_lock.write().await; + + try_!(bucket_meta_sys.delete(&bucket, BUCKET_POLICY_CONFIG).await); + + Ok(S3Response::new(DeleteBucketPolicyOutput {})) + } + + #[tracing::instrument(level = "debug", skip(self))] + async fn get_bucket_lifecycle_configuration( + &self, + _req: S3Request, + ) -> S3Result> { + Err(s3_error!(NotImplemented, "GetBucketLifecycleConfiguration is not implemented yet")) + } + + #[tracing::instrument(level = "debug", skip(self))] + async fn put_bucket_lifecycle_configuration( + &self, + _req: S3Request, + ) -> S3Result> { + Err(s3_error!(NotImplemented, "PutBucketLifecycleConfiguration is not implemented yet")) + } + + #[tracing::instrument(level = "debug", skip(self))] + async fn delete_bucket_lifecycle( + &self, + _req: S3Request, + ) -> S3Result> { + Err(s3_error!(NotImplemented, "DeleteBucketLifecycle is not implemented yet")) + } + + async fn get_bucket_encryption( + &self, + _req: S3Request, + ) -> S3Result> { + Err(s3_error!(NotImplemented, "GetBucketEncryption is not implemented yet")) + } + + async fn put_bucket_encryption( + &self, + _req: S3Request, + ) -> S3Result> { + Err(s3_error!(NotImplemented, "PutBucketEncryption is not implemented yet")) + } + + async fn delete_bucket_encryption( + &self, + _req: S3Request, + ) -> S3Result> { + Err(s3_error!(NotImplemented, "DeleteBucketEncryption is not implemented yet")) + } + + #[tracing::instrument(level = "debug", skip(self))] + async fn get_object_lock_configuration( + &self, + _req: S3Request, + ) -> S3Result> { + // mc cp step 1 + let output = GetObjectLockConfigurationOutput::default(); + Ok(S3Response::new(output)) + } + + #[tracing::instrument(level = "debug", skip(self))] + async fn put_object_lock_configuration( + &self, + _req: S3Request, + ) -> S3Result> { + Err(s3_error!(NotImplemented, "PutObjectLockConfiguration is not implemented yet")) + } + + async fn get_bucket_replication( + &self, + _req: S3Request, + ) -> S3Result> { + Err(s3_error!(NotImplemented, "GetBucketReplication is not implemented yet")) + } + + async fn put_bucket_replication( + &self, + _req: S3Request, + ) -> S3Result> { + Err(s3_error!(NotImplemented, "PutBucketReplication is not implemented yet")) + } + + async fn delete_bucket_replication( + &self, + _req: S3Request, + ) -> S3Result> { + Err(s3_error!(NotImplemented, "DeleteBucketReplication is not implemented yet")) + } + + async fn get_bucket_notification_configuration( + &self, + _req: S3Request, + ) -> S3Result> { + Err(s3_error!(NotImplemented, "GetBucketNotificationConfiguration is not implemented yet")) + } + + async fn put_bucket_notification_configuration( + &self, + _req: S3Request, + ) -> S3Result> { + Err(s3_error!(NotImplemented, "PutBucketNotificationConfiguration is not implemented yet")) + } } #[allow(dead_code)]