mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-20 19:42:17 +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.
|
// limitations under the License.
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{
|
||||||
use std::{collections::HashSet, ops::Deref};
|
Deserialize, Deserializer, Serialize,
|
||||||
|
de::{self, Error as DeError, Visitor},
|
||||||
|
};
|
||||||
|
use std::{collections::HashSet, fmt, ops::Deref};
|
||||||
use strum::{EnumString, IntoStaticStr};
|
use strum::{EnumString, IntoStaticStr};
|
||||||
|
|
||||||
use super::{Error as IamError, Validator, utils::wildcard};
|
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>);
|
pub struct ActionSet(pub HashSet<Action>);
|
||||||
|
|
||||||
impl ActionSet {
|
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)]
|
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Debug, Copy)]
|
||||||
#[serde(try_from = "&str", untagged)]
|
#[serde(try_from = "&str", untagged)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
|
|||||||
@@ -526,6 +526,72 @@ mod test {
|
|||||||
Ok(())
|
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]
|
#[tokio::test]
|
||||||
async fn test_aws_username_policy_variable() -> Result<()> {
|
async fn test_aws_username_policy_variable() -> Result<()> {
|
||||||
let data = r#"
|
let data = r#"
|
||||||
|
|||||||
@@ -13,9 +13,13 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{
|
||||||
|
Deserialize, Deserializer, Serialize,
|
||||||
|
de::{self, Error as DeError, Visitor},
|
||||||
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
|
fmt,
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
};
|
};
|
||||||
@@ -27,7 +31,7 @@ use super::{
|
|||||||
variables::PolicyVariableResolver,
|
variables::PolicyVariableResolver,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
|
#[derive(Serialize, Clone, Default, Debug)]
|
||||||
pub struct ResourceSet(pub HashSet<Resource>);
|
pub struct ResourceSet(pub HashSet<Resource>);
|
||||||
|
|
||||||
impl ResourceSet {
|
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)]
|
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
|
||||||
pub enum Resource {
|
pub enum Resource {
|
||||||
S3(String),
|
S3(String),
|
||||||
|
|||||||
@@ -167,7 +167,10 @@ impl Operation for AssumeRoleHandle {
|
|||||||
pub fn populate_session_policy(claims: &mut HashMap<String, Value>, policy: &str) -> S3Result<()> {
|
pub fn populate_session_policy(claims: &mut HashMap<String, Value>, policy: &str) -> S3Result<()> {
|
||||||
if !policy.is_empty() {
|
if !policy.is_empty() {
|
||||||
let session_policy = Policy::parse_config(policy.as_bytes())
|
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() {
|
if session_policy.version.is_empty() {
|
||||||
return Err(s3_error!(InvalidRequest, "invalid policy"));
|
return Err(s3_error!(InvalidRequest, "invalid policy"));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user