mirror of
https://github.com/rustfs/rustfs.git
synced 2026-01-16 17:20:33 +00:00
feat:policy Resources support string and array modes. (#1346)
Co-authored-by: loverustfs <hello@rustfs.com>
This commit is contained in:
@@ -13,13 +13,16 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashSet, ops::Deref};
|
||||
use serde::{
|
||||
Deserialize, Deserializer, Serialize,
|
||||
de::{self, Error as DeError, Visitor},
|
||||
};
|
||||
use std::{collections::HashSet, fmt, ops::Deref};
|
||||
use strum::{EnumString, IntoStaticStr};
|
||||
|
||||
use super::{Error as IamError, Validator, utils::wildcard};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
|
||||
#[derive(Serialize, Clone, Default, Debug)]
|
||||
pub struct ActionSet(pub HashSet<Action>);
|
||||
|
||||
impl ActionSet {
|
||||
@@ -61,6 +64,54 @@ impl PartialEq for ActionSet {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ActionSet {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ActionOrVecVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ActionOrVecVisitor {
|
||||
type Value = ActionSet;
|
||||
|
||||
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) -> std::result::Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
let action = Action::try_from(value).map_err(|e| E::custom(format!("invalid action: {}", e)))?;
|
||||
let mut set = HashSet::new();
|
||||
set.insert(action);
|
||||
Ok(ActionSet(set))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::SeqAccess<'de>,
|
||||
A::Error: DeError,
|
||||
{
|
||||
let mut set = HashSet::with_capacity(seq.size_hint().unwrap_or(0));
|
||||
while let Some(value) = seq.next_element::<String>()? {
|
||||
match Action::try_from(value.as_str()) {
|
||||
Ok(action) => {
|
||||
set.insert(action);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(A::Error::custom(format!("invalid action: {}", e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ActionSet(set))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(ActionOrVecVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Debug, Copy)]
|
||||
#[serde(try_from = "&str", untagged)]
|
||||
pub enum Action {
|
||||
|
||||
@@ -526,6 +526,72 @@ mod test {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_parse_policy_with_single_string_action_and_resource() -> Result<()> {
|
||||
// Test policy with single string Action and Resource (AWS IAM allows both formats)
|
||||
let data = r#"
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "s3:GetObject",
|
||||
"Resource": "arn:aws:s3:::test/analytics/customers/*"
|
||||
}
|
||||
]
|
||||
}
|
||||
"#;
|
||||
|
||||
let p = Policy::parse_config(data.as_bytes())?;
|
||||
assert!(!p.statements.is_empty());
|
||||
assert!(!p.statements[0].actions.is_empty());
|
||||
assert!(!p.statements[0].resources.is_empty());
|
||||
|
||||
// Test with array format (should still work)
|
||||
let data_array = r#"
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:GetObject"],
|
||||
"Resource": ["arn:aws:s3:::test/analytics/customers/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
"#;
|
||||
|
||||
let p2 = Policy::parse_config(data_array.as_bytes())?;
|
||||
assert!(!p2.statements.is_empty());
|
||||
assert!(!p2.statements[0].actions.is_empty());
|
||||
assert!(!p2.statements[0].resources.is_empty());
|
||||
|
||||
// Verify that both formats produce equivalent results
|
||||
assert_eq!(
|
||||
p.statements.len(),
|
||||
p2.statements.len(),
|
||||
"Both policies should have the same number of statements"
|
||||
);
|
||||
assert_eq!(
|
||||
p.statements[0].actions, p2.statements[0].actions,
|
||||
"ActionSet from string format should equal ActionSet from array format"
|
||||
);
|
||||
assert_eq!(
|
||||
p.statements[0].resources, p2.statements[0].resources,
|
||||
"ResourceSet from string format should equal ResourceSet from array format"
|
||||
);
|
||||
assert_eq!(
|
||||
p.statements[0].effect, p2.statements[0].effect,
|
||||
"Effect should be the same in both formats"
|
||||
);
|
||||
|
||||
// Verify specific content
|
||||
assert_eq!(p.statements[0].actions.len(), 1, "ActionSet should contain exactly one action");
|
||||
assert_eq!(p.statements[0].resources.len(), 1, "ResourceSet should contain exactly one resource");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_aws_username_policy_variable() -> Result<()> {
|
||||
let data = r#"
|
||||
|
||||
@@ -13,9 +13,13 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{
|
||||
Deserialize, Deserializer, Serialize,
|
||||
de::{self, Error as DeError, Visitor},
|
||||
};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
hash::Hash,
|
||||
ops::Deref,
|
||||
};
|
||||
@@ -27,7 +31,7 @@ use super::{
|
||||
variables::PolicyVariableResolver,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
|
||||
#[derive(Serialize, Clone, Default, Debug)]
|
||||
pub struct ResourceSet(pub HashSet<Resource>);
|
||||
|
||||
impl ResourceSet {
|
||||
@@ -86,6 +90,54 @@ impl PartialEq for ResourceSet {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ResourceSet {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ResourceOrVecVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ResourceOrVecVisitor {
|
||||
type Value = ResourceSet;
|
||||
|
||||
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) -> std::result::Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
let resource = Resource::try_from(value).map_err(|e| E::custom(format!("invalid resource: {}", e)))?;
|
||||
let mut set = HashSet::new();
|
||||
set.insert(resource);
|
||||
Ok(ResourceSet(set))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::SeqAccess<'de>,
|
||||
A::Error: DeError,
|
||||
{
|
||||
let mut set = HashSet::with_capacity(seq.size_hint().unwrap_or(0));
|
||||
while let Some(value) = seq.next_element::<String>()? {
|
||||
match Resource::try_from(value.as_str()) {
|
||||
Ok(resource) => {
|
||||
set.insert(resource);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(A::Error::custom(format!("invalid resource: {}", e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ResourceSet(set))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(ResourceOrVecVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
|
||||
pub enum Resource {
|
||||
S3(String),
|
||||
|
||||
@@ -167,7 +167,10 @@ impl Operation for AssumeRoleHandle {
|
||||
pub fn populate_session_policy(claims: &mut HashMap<String, Value>, policy: &str) -> S3Result<()> {
|
||||
if !policy.is_empty() {
|
||||
let session_policy = Policy::parse_config(policy.as_bytes())
|
||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("parse policy err {e}")))?;
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("Failed to parse session policy: {}. Please check that the policy is valid JSON format with standard brackets [] for arrays.", e);
|
||||
S3Error::with_message(S3ErrorCode::InvalidRequest, error_msg)
|
||||
})?;
|
||||
if session_policy.version.is_empty() {
|
||||
return Err(s3_error!(InvalidRequest, "invalid policy"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user