mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 15:37:02 +00:00
fix(policy): preserve IAM policy readback shape (#3431)
This commit is contained in:
@@ -17,7 +17,7 @@ use serde::{
|
|||||||
Deserialize, Deserializer, Serialize,
|
Deserialize, Deserializer, Serialize,
|
||||||
de::{self, Error as DeError, Visitor},
|
de::{self, Error as DeError, Visitor},
|
||||||
};
|
};
|
||||||
use std::{collections::HashSet, fmt, ops::Deref};
|
use std::{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};
|
||||||
@@ -25,7 +25,7 @@ use super::{Error as IamError, Validator, utils::wildcard};
|
|||||||
/// A set of policy actions that always serializes as an array of strings,
|
/// A set of policy actions that always serializes as an array of strings,
|
||||||
/// conforming to the S3 policy specification for consistency and compatibility.
|
/// conforming to the S3 policy specification for consistency and compatibility.
|
||||||
#[derive(Clone, Default, Debug)]
|
#[derive(Clone, Default, Debug)]
|
||||||
pub struct ActionSet(pub HashSet<Action>);
|
pub struct ActionSet(pub Vec<Action>);
|
||||||
|
|
||||||
impl Serialize for ActionSet {
|
impl Serialize for ActionSet {
|
||||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
@@ -36,12 +36,10 @@ impl Serialize for ActionSet {
|
|||||||
|
|
||||||
// Always serialize as array, even for single action, to match S3 specification
|
// Always serialize as array, even for single action, to match S3 specification
|
||||||
// and ensure compatibility with AWS SDK clients that expect array format
|
// and ensure compatibility with AWS SDK clients that expect array format
|
||||||
let mut actions: Vec<&str> = self.0.iter().map(Into::into).collect();
|
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
||||||
actions.sort_unstable();
|
for action in &self.0 {
|
||||||
|
let action_str: &str = action.into();
|
||||||
let mut seq = serializer.serialize_seq(Some(actions.len()))?;
|
seq.serialize_element(action_str)?;
|
||||||
for action in actions {
|
|
||||||
seq.serialize_element(action)?;
|
|
||||||
}
|
}
|
||||||
seq.end()
|
seq.end()
|
||||||
}
|
}
|
||||||
@@ -53,6 +51,12 @@ impl ActionSet {
|
|||||||
self.0.is_empty()
|
self.0.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_unique(&mut self, action: Action) {
|
||||||
|
if !self.0.contains(&action) {
|
||||||
|
self.0.push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_match(&self, action: &Action) -> bool {
|
pub fn is_match(&self, action: &Action) -> bool {
|
||||||
for act in self.0.iter() {
|
for act in self.0.iter() {
|
||||||
if act.is_match(action) {
|
if act.is_match(action) {
|
||||||
@@ -71,7 +75,7 @@ impl ActionSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for ActionSet {
|
impl Deref for ActionSet {
|
||||||
type Target = HashSet<Action>;
|
type Target = [Action];
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.0
|
&self.0
|
||||||
@@ -87,7 +91,7 @@ impl Validator for ActionSet {
|
|||||||
|
|
||||||
impl PartialEq for ActionSet {
|
impl PartialEq for ActionSet {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.len() == other.len() && self.0.iter().all(|x| other.0.contains(x))
|
self.0.iter().all(|x| other.0.contains(x)) && other.0.iter().all(|x| self.0.contains(x))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,9 +114,9 @@ impl<'de> Deserialize<'de> for ActionSet {
|
|||||||
E: de::Error,
|
E: de::Error,
|
||||||
{
|
{
|
||||||
let action = Action::try_from(value).map_err(|e| E::custom(format!("invalid action: {}", e)))?;
|
let action = Action::try_from(value).map_err(|e| E::custom(format!("invalid action: {}", e)))?;
|
||||||
let mut set = HashSet::new();
|
let mut actions = ActionSet::default();
|
||||||
set.insert(action);
|
actions.push_unique(action);
|
||||||
Ok(ActionSet(set))
|
Ok(actions)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
|
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
|
||||||
@@ -120,18 +124,18 @@ impl<'de> Deserialize<'de> for ActionSet {
|
|||||||
A: de::SeqAccess<'de>,
|
A: de::SeqAccess<'de>,
|
||||||
A::Error: DeError,
|
A::Error: DeError,
|
||||||
{
|
{
|
||||||
let mut set = HashSet::with_capacity(seq.size_hint().unwrap_or(0));
|
let mut actions = ActionSet(Vec::with_capacity(seq.size_hint().unwrap_or(0)));
|
||||||
while let Some(value) = seq.next_element::<String>()? {
|
while let Some(value) = seq.next_element::<String>()? {
|
||||||
match Action::try_from(value.as_str()) {
|
match Action::try_from(value.as_str()) {
|
||||||
Ok(action) => {
|
Ok(action) => {
|
||||||
set.insert(action);
|
actions.push_unique(action);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(A::Error::custom(format!("invalid action: {}", e)));
|
return Err(A::Error::custom(format!("invalid action: {}", e)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(ActionSet(set))
|
Ok(actions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,7 +729,6 @@ pub enum KmsAction {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_action_wildcard_parsing() {
|
fn test_action_wildcard_parsing() {
|
||||||
@@ -774,9 +777,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_actionset_serialize_single_element() {
|
fn test_actionset_serialize_single_element() {
|
||||||
// Single element should serialize as array for S3 specification compliance
|
// Single element should serialize as array for S3 specification compliance
|
||||||
let mut set = HashSet::new();
|
let actionset = ActionSet(vec![Action::S3Action(S3Action::GetObjectAction)]);
|
||||||
set.insert(Action::S3Action(S3Action::GetObjectAction));
|
|
||||||
let actionset = ActionSet(set);
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&actionset).expect("Should serialize");
|
let json = serde_json::to_string(&actionset).expect("Should serialize");
|
||||||
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse");
|
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse");
|
||||||
@@ -789,10 +790,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_actionset_serialize_multiple_elements() {
|
fn test_actionset_serialize_multiple_elements() {
|
||||||
// Multiple elements should serialize as array
|
// Multiple elements should serialize as array
|
||||||
let mut set = HashSet::new();
|
let actionset = ActionSet(vec![
|
||||||
set.insert(Action::S3Action(S3Action::GetObjectAction));
|
Action::S3Action(S3Action::GetObjectAction),
|
||||||
set.insert(Action::S3Action(S3Action::PutObjectAction));
|
Action::S3Action(S3Action::PutObjectAction),
|
||||||
let actionset = ActionSet(set);
|
]);
|
||||||
|
|
||||||
let json = serde_json::to_string(&actionset).expect("Should serialize");
|
let json = serde_json::to_string(&actionset).expect("Should serialize");
|
||||||
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse");
|
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse");
|
||||||
@@ -804,9 +805,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_actionset_wildcard_serialization() {
|
fn test_actionset_wildcard_serialization() {
|
||||||
// Wildcard action should serialize as array for S3 specification compliance
|
// Wildcard action should serialize as array for S3 specification compliance
|
||||||
let mut set = HashSet::new();
|
let actionset = ActionSet(vec![Action::try_from("*").expect("Should parse wildcard")]);
|
||||||
set.insert(Action::try_from("*").expect("Should parse wildcard"));
|
|
||||||
let actionset = ActionSet(set);
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&actionset).expect("Should serialize");
|
let json = serde_json::to_string(&actionset).expect("Should serialize");
|
||||||
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse");
|
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse");
|
||||||
|
|||||||
+116
-107
@@ -355,7 +355,7 @@ pub async fn bucket_policy_needs_existing_object_tag_for_args(policy: &BucketPol
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod default {
|
pub mod default {
|
||||||
use std::{collections::HashSet, sync::LazyLock};
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use crate::policy::{
|
use crate::policy::{
|
||||||
ActionSet, DEFAULT_VERSION, Effect, Functions, ResourceSet, Statement,
|
ActionSet, DEFAULT_VERSION, Effect, Functions, ResourceSet, Statement,
|
||||||
@@ -377,28 +377,16 @@ pub mod default {
|
|||||||
Statement {
|
Statement {
|
||||||
sid: "".into(),
|
sid: "".into(),
|
||||||
effect: Effect::Allow,
|
effect: Effect::Allow,
|
||||||
actions: ActionSet({
|
actions: ActionSet(vec![Action::S3Action(S3Action::AllActions)]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Action::S3Action(S3Action::AllActions));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
not_actions: ActionSet(Default::default()),
|
not_actions: ActionSet(Default::default()),
|
||||||
resources: ResourceSet({
|
resources: ResourceSet(vec![Resource::S3("*".into())]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Resource::S3("*".into()));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
conditions: Functions::default(),
|
conditions: Functions::default(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Statement {
|
Statement {
|
||||||
sid: "".into(),
|
sid: "".into(),
|
||||||
effect: Effect::Allow,
|
effect: Effect::Allow,
|
||||||
actions: ActionSet({
|
actions: ActionSet(vec![Action::StsAction(StsAction::AssumeRoleAction)]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Action::StsAction(StsAction::AssumeRoleAction));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
not_actions: ActionSet(Default::default()),
|
not_actions: ActionSet(Default::default()),
|
||||||
resources: ResourceSet(Default::default()),
|
resources: ResourceSet(Default::default()),
|
||||||
conditions: Functions::default(),
|
conditions: Functions::default(),
|
||||||
@@ -416,30 +404,20 @@ pub mod default {
|
|||||||
Statement {
|
Statement {
|
||||||
sid: "".into(),
|
sid: "".into(),
|
||||||
effect: Effect::Allow,
|
effect: Effect::Allow,
|
||||||
actions: ActionSet({
|
actions: ActionSet(vec![
|
||||||
let mut hash_set = HashSet::new();
|
Action::S3Action(S3Action::GetBucketLocationAction),
|
||||||
hash_set.insert(Action::S3Action(S3Action::GetBucketLocationAction));
|
Action::S3Action(S3Action::GetObjectAction),
|
||||||
hash_set.insert(Action::S3Action(S3Action::GetObjectAction));
|
Action::S3Action(S3Action::GetBucketQuotaAction),
|
||||||
hash_set.insert(Action::S3Action(S3Action::GetBucketQuotaAction));
|
]),
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
not_actions: ActionSet(Default::default()),
|
not_actions: ActionSet(Default::default()),
|
||||||
resources: ResourceSet({
|
resources: ResourceSet(vec![Resource::S3("*".into())]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Resource::S3("*".into()));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
conditions: Functions::default(),
|
conditions: Functions::default(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Statement {
|
Statement {
|
||||||
sid: "".into(),
|
sid: "".into(),
|
||||||
effect: Effect::Allow,
|
effect: Effect::Allow,
|
||||||
actions: ActionSet({
|
actions: ActionSet(vec![Action::StsAction(StsAction::AssumeRoleAction)]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Action::StsAction(StsAction::AssumeRoleAction));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
not_actions: ActionSet(Default::default()),
|
not_actions: ActionSet(Default::default()),
|
||||||
resources: ResourceSet(Default::default()),
|
resources: ResourceSet(Default::default()),
|
||||||
conditions: Functions::default(),
|
conditions: Functions::default(),
|
||||||
@@ -457,28 +435,16 @@ pub mod default {
|
|||||||
Statement {
|
Statement {
|
||||||
sid: "".into(),
|
sid: "".into(),
|
||||||
effect: Effect::Allow,
|
effect: Effect::Allow,
|
||||||
actions: ActionSet({
|
actions: ActionSet(vec![Action::S3Action(S3Action::PutObjectAction)]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Action::S3Action(S3Action::PutObjectAction));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
not_actions: ActionSet(Default::default()),
|
not_actions: ActionSet(Default::default()),
|
||||||
resources: ResourceSet({
|
resources: ResourceSet(vec![Resource::S3("*".into())]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Resource::S3("*".into()));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
conditions: Functions::default(),
|
conditions: Functions::default(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Statement {
|
Statement {
|
||||||
sid: "".into(),
|
sid: "".into(),
|
||||||
effect: Effect::Allow,
|
effect: Effect::Allow,
|
||||||
actions: ActionSet({
|
actions: ActionSet(vec![Action::StsAction(StsAction::AssumeRoleAction)]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Action::StsAction(StsAction::AssumeRoleAction));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
not_actions: ActionSet(Default::default()),
|
not_actions: ActionSet(Default::default()),
|
||||||
resources: ResourceSet(Default::default()),
|
resources: ResourceSet(Default::default()),
|
||||||
conditions: Functions::default(),
|
conditions: Functions::default(),
|
||||||
@@ -496,35 +462,25 @@ pub mod default {
|
|||||||
Statement {
|
Statement {
|
||||||
sid: "".into(),
|
sid: "".into(),
|
||||||
effect: Effect::Allow,
|
effect: Effect::Allow,
|
||||||
actions: ActionSet({
|
actions: ActionSet(vec![
|
||||||
let mut hash_set = HashSet::new();
|
Action::AdminAction(AdminAction::ProfilingAdminAction),
|
||||||
hash_set.insert(Action::AdminAction(AdminAction::ProfilingAdminAction));
|
Action::AdminAction(AdminAction::TraceAdminAction),
|
||||||
hash_set.insert(Action::AdminAction(AdminAction::TraceAdminAction));
|
Action::AdminAction(AdminAction::ConsoleLogAdminAction),
|
||||||
hash_set.insert(Action::AdminAction(AdminAction::ConsoleLogAdminAction));
|
Action::AdminAction(AdminAction::ServerInfoAdminAction),
|
||||||
hash_set.insert(Action::AdminAction(AdminAction::ServerInfoAdminAction));
|
Action::AdminAction(AdminAction::TopLocksAdminAction),
|
||||||
hash_set.insert(Action::AdminAction(AdminAction::TopLocksAdminAction));
|
Action::AdminAction(AdminAction::HealthInfoAdminAction),
|
||||||
hash_set.insert(Action::AdminAction(AdminAction::HealthInfoAdminAction));
|
Action::AdminAction(AdminAction::PrometheusAdminAction),
|
||||||
hash_set.insert(Action::AdminAction(AdminAction::PrometheusAdminAction));
|
Action::AdminAction(AdminAction::BandwidthMonitorAction),
|
||||||
hash_set.insert(Action::AdminAction(AdminAction::BandwidthMonitorAction));
|
]),
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
not_actions: ActionSet(Default::default()),
|
not_actions: ActionSet(Default::default()),
|
||||||
resources: ResourceSet({
|
resources: ResourceSet(vec![Resource::S3("*".into())]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Resource::S3("*".into()));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
conditions: Functions::default(),
|
conditions: Functions::default(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Statement {
|
Statement {
|
||||||
sid: "".into(),
|
sid: "".into(),
|
||||||
effect: Effect::Allow,
|
effect: Effect::Allow,
|
||||||
actions: ActionSet({
|
actions: ActionSet(vec![Action::StsAction(StsAction::AssumeRoleAction)]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Action::StsAction(StsAction::AssumeRoleAction));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
not_actions: ActionSet(Default::default()),
|
not_actions: ActionSet(Default::default()),
|
||||||
resources: ResourceSet(Default::default()),
|
resources: ResourceSet(Default::default()),
|
||||||
conditions: Functions::default(),
|
conditions: Functions::default(),
|
||||||
@@ -542,56 +498,36 @@ pub mod default {
|
|||||||
Statement {
|
Statement {
|
||||||
sid: "".into(),
|
sid: "".into(),
|
||||||
effect: Effect::Allow,
|
effect: Effect::Allow,
|
||||||
actions: ActionSet({
|
actions: ActionSet(vec![Action::AdminAction(AdminAction::AllAdminActions)]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Action::AdminAction(AdminAction::AllAdminActions));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
not_actions: ActionSet(Default::default()),
|
not_actions: ActionSet(Default::default()),
|
||||||
resources: ResourceSet(HashSet::new()),
|
resources: ResourceSet(Vec::new()),
|
||||||
conditions: Functions::default(),
|
conditions: Functions::default(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Statement {
|
Statement {
|
||||||
sid: "".into(),
|
sid: "".into(),
|
||||||
effect: Effect::Allow,
|
effect: Effect::Allow,
|
||||||
actions: ActionSet({
|
actions: ActionSet(vec![Action::KmsAction(KmsAction::AllActions)]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Action::KmsAction(KmsAction::AllActions));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
not_actions: ActionSet(Default::default()),
|
not_actions: ActionSet(Default::default()),
|
||||||
resources: ResourceSet(HashSet::new()),
|
resources: ResourceSet(Vec::new()),
|
||||||
conditions: Functions::default(),
|
conditions: Functions::default(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Statement {
|
Statement {
|
||||||
sid: "".into(),
|
sid: "".into(),
|
||||||
effect: Effect::Allow,
|
effect: Effect::Allow,
|
||||||
actions: ActionSet({
|
actions: ActionSet(vec![Action::S3Action(S3Action::AllActions)]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Action::S3Action(S3Action::AllActions));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
not_actions: ActionSet(Default::default()),
|
not_actions: ActionSet(Default::default()),
|
||||||
resources: ResourceSet({
|
resources: ResourceSet(vec![Resource::S3("*".into())]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Resource::S3("*".into()));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
conditions: Functions::default(),
|
conditions: Functions::default(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Statement {
|
Statement {
|
||||||
sid: "".into(),
|
sid: "".into(),
|
||||||
effect: Effect::Allow,
|
effect: Effect::Allow,
|
||||||
actions: ActionSet({
|
actions: ActionSet(vec![Action::StsAction(StsAction::AssumeRoleAction)]),
|
||||||
let mut hash_set = HashSet::new();
|
|
||||||
hash_set.insert(Action::StsAction(StsAction::AssumeRoleAction));
|
|
||||||
hash_set
|
|
||||||
}),
|
|
||||||
not_actions: ActionSet(Default::default()),
|
not_actions: ActionSet(Default::default()),
|
||||||
resources: ResourceSet(HashSet::new()),
|
resources: ResourceSet(Vec::new()),
|
||||||
conditions: Functions::default(),
|
conditions: Functions::default(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -1947,7 +1883,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_policy_action_and_resource_serialization_is_stable() {
|
fn test_policy_action_and_resource_serialization_preserves_input_order() {
|
||||||
let policy = Policy::parse_config(
|
let policy = Policy::parse_config(
|
||||||
br#"{
|
br#"{
|
||||||
"Version":"2012-10-17",
|
"Version":"2012-10-17",
|
||||||
@@ -1975,8 +1911,8 @@ mod test {
|
|||||||
.collect();
|
.collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
actions,
|
actions,
|
||||||
vec!["s3:DeleteObject", "s3:GetObject", "s3:ListBucket", "s3:PutObject"],
|
vec!["s3:PutObject", "s3:DeleteObject", "s3:ListBucket", "s3:GetObject"],
|
||||||
"Action serialization should use a stable order"
|
"Action serialization should preserve input order"
|
||||||
);
|
);
|
||||||
|
|
||||||
let resources: Vec<_> = statement["Resource"]
|
let resources: Vec<_> = statement["Resource"]
|
||||||
@@ -1987,11 +1923,84 @@ mod test {
|
|||||||
.collect();
|
.collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resources,
|
resources,
|
||||||
vec!["arn:aws:s3:::example-bucket", "arn:aws:s3:::example-bucket/*"],
|
vec!["arn:aws:s3:::example-bucket/*", "arn:aws:s3:::example-bucket"],
|
||||||
"Resource serialization should use a stable order"
|
"Resource serialization should preserve input order"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_iam_policy_serialize_preserves_non_empty_optional_fields() {
|
||||||
|
let policy = Policy::parse_config(
|
||||||
|
br#"{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Statement":[{
|
||||||
|
"Sid":"keep-me",
|
||||||
|
"Effect":"Allow",
|
||||||
|
"NotAction":["s3:DeleteObject","s3:PutObject"],
|
||||||
|
"NotResource":["arn:aws:s3:::example-bucket/private/*","arn:aws:s3:::example-bucket/tmp/*"],
|
||||||
|
"Condition":{"StringEquals":{"s3:prefix":"home/"}}
|
||||||
|
}]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("policy with non-empty optional fields should parse");
|
||||||
|
|
||||||
|
let value = serde_json::to_value(&policy).expect("policy should serialize");
|
||||||
|
let statement = value["Statement"][0]
|
||||||
|
.as_object()
|
||||||
|
.expect("statement should serialize as an object");
|
||||||
|
assert_eq!(statement.get("Sid").and_then(|sid| sid.as_str()), Some("keep-me"));
|
||||||
|
assert!(statement.contains_key("Condition"), "non-empty Condition should be preserved");
|
||||||
|
assert!(!statement.contains_key("Action"), "empty Action should be omitted for NotAction policy");
|
||||||
|
assert!(
|
||||||
|
!statement.contains_key("Resource"),
|
||||||
|
"empty Resource should be omitted for NotResource policy"
|
||||||
|
);
|
||||||
|
|
||||||
|
let not_actions: Vec<_> = statement["NotAction"]
|
||||||
|
.as_array()
|
||||||
|
.expect("NotAction should serialize as an array")
|
||||||
|
.iter()
|
||||||
|
.map(|action| action.as_str().expect("NotAction entries should be strings"))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(not_actions, vec!["s3:DeleteObject", "s3:PutObject"]);
|
||||||
|
|
||||||
|
let not_resources: Vec<_> = statement["NotResource"]
|
||||||
|
.as_array()
|
||||||
|
.expect("NotResource should serialize as an array")
|
||||||
|
.iter()
|
||||||
|
.map(|resource| resource.as_str().expect("NotResource entries should be strings"))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
not_resources,
|
||||||
|
vec!["arn:aws:s3:::example-bucket/private/*", "arn:aws:s3:::example-bucket/tmp/*"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_iam_policy_serialize_keeps_empty_resource_omitted_for_action_families() {
|
||||||
|
let policy = Policy::parse_config(
|
||||||
|
br#"{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Statement":[
|
||||||
|
{"Effect":"Allow","Action":["sts:AssumeRole"]},
|
||||||
|
{"Effect":"Allow","Action":["admin:*"]},
|
||||||
|
{"Effect":"Allow","Action":["kms:*"]}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("STS/Admin/KMS statements without resources should parse");
|
||||||
|
|
||||||
|
let value = serde_json::to_value(&policy).expect("policy should serialize");
|
||||||
|
let statements = value["Statement"].as_array().expect("Statement should serialize as an array");
|
||||||
|
assert_eq!(statements.len(), 3);
|
||||||
|
|
||||||
|
for statement in statements {
|
||||||
|
let statement = statement.as_object().expect("statement should serialize as an object");
|
||||||
|
assert!(!statement.contains_key("Resource"), "empty Resource should be omitted");
|
||||||
|
assert!(!statement.contains_key("NotResource"), "empty NotResource should be omitted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bucket_policy_serialize_omits_empty_fields() {
|
fn test_bucket_policy_serialize_omits_empty_fields() {
|
||||||
use crate::policy::action::{Action, ActionSet, S3Action};
|
use crate::policy::action::{Action, ActionSet, S3Action};
|
||||||
@@ -2021,11 +2030,11 @@ mod test {
|
|||||||
policy.statements[0]
|
policy.statements[0]
|
||||||
.actions
|
.actions
|
||||||
.0
|
.0
|
||||||
.insert(Action::S3Action(S3Action::ListBucketAction));
|
.push(Action::S3Action(S3Action::ListBucketAction));
|
||||||
policy.statements[0]
|
policy.statements[0]
|
||||||
.resources
|
.resources
|
||||||
.0
|
.0
|
||||||
.insert(Resource::try_from("arn:aws:s3:::test/*").unwrap());
|
.push(Resource::try_from("arn:aws:s3:::test/*").expect("test resource should parse"));
|
||||||
|
|
||||||
let json = serde_json::to_string(&policy).expect("Should serialize");
|
let json = serde_json::to_string(&policy).expect("Should serialize");
|
||||||
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse");
|
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse");
|
||||||
@@ -2134,11 +2143,11 @@ mod test {
|
|||||||
policy.statements[0]
|
policy.statements[0]
|
||||||
.actions
|
.actions
|
||||||
.0
|
.0
|
||||||
.insert(Action::S3Action(S3Action::ListBucketAction));
|
.push(Action::S3Action(S3Action::ListBucketAction));
|
||||||
policy.statements[0]
|
policy.statements[0]
|
||||||
.resources
|
.resources
|
||||||
.0
|
.0
|
||||||
.insert(Resource::try_from("arn:aws:s3:::test/*").unwrap());
|
.push(Resource::try_from("arn:aws:s3:::test/*").expect("test resource should parse"));
|
||||||
|
|
||||||
let json = serde_json::to_string(&policy).expect("Should serialize");
|
let json = serde_json::to_string(&policy).expect("Should serialize");
|
||||||
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse");
|
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse");
|
||||||
|
|||||||
@@ -17,12 +17,7 @@ use serde::{
|
|||||||
Deserialize, Deserializer, Serialize,
|
Deserialize, Deserializer, Serialize,
|
||||||
de::{self, Error as DeError, Visitor},
|
de::{self, Error as DeError, Visitor},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{collections::HashMap, fmt, ops::Deref};
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
fmt,
|
|
||||||
hash::Hash,
|
|
||||||
ops::Deref,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Error as IamError, Validator,
|
Error as IamError, Validator,
|
||||||
@@ -32,7 +27,7 @@ use super::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Default, Debug)]
|
#[derive(Clone, Default, Debug)]
|
||||||
pub struct ResourceSet(pub HashSet<Resource>);
|
pub struct ResourceSet(pub Vec<Resource>);
|
||||||
|
|
||||||
impl Serialize for ResourceSet {
|
impl Serialize for ResourceSet {
|
||||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
@@ -41,19 +36,13 @@ impl Serialize for ResourceSet {
|
|||||||
{
|
{
|
||||||
use serde::ser::SerializeSeq;
|
use serde::ser::SerializeSeq;
|
||||||
|
|
||||||
let mut resources: Vec<String> = self
|
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
||||||
.0
|
for resource in &self.0 {
|
||||||
.iter()
|
let resource_str = match resource {
|
||||||
.map(|resource| match resource {
|
|
||||||
Resource::S3(value) => format!("{}{}", Resource::S3_PREFIX, value),
|
Resource::S3(value) => format!("{}{}", Resource::S3_PREFIX, value),
|
||||||
Resource::Kms(value) => value.clone(),
|
Resource::Kms(value) => value.clone(),
|
||||||
})
|
};
|
||||||
.collect();
|
seq.serialize_element(&resource_str)?;
|
||||||
resources.sort_unstable();
|
|
||||||
|
|
||||||
let mut seq = serializer.serialize_seq(Some(resources.len()))?;
|
|
||||||
for resource in resources {
|
|
||||||
seq.serialize_element(&resource)?;
|
|
||||||
}
|
}
|
||||||
seq.end()
|
seq.end()
|
||||||
}
|
}
|
||||||
@@ -65,6 +54,12 @@ impl ResourceSet {
|
|||||||
self.0.is_empty()
|
self.0.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_unique(&mut self, resource: Resource) {
|
||||||
|
if !self.0.contains(&resource) {
|
||||||
|
self.0.push(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn is_match(&self, resource: &str, conditions: &HashMap<String, Vec<String>>) -> bool {
|
pub async fn is_match(&self, resource: &str, conditions: &HashMap<String, Vec<String>>) -> bool {
|
||||||
self.is_match_with_resolver(resource, conditions, None).await
|
self.is_match_with_resolver(resource, conditions, None).await
|
||||||
}
|
}
|
||||||
@@ -96,7 +91,7 @@ impl ResourceSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for ResourceSet {
|
impl Deref for ResourceSet {
|
||||||
type Target = HashSet<Resource>;
|
type Target = [Resource];
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.0
|
&self.0
|
||||||
@@ -116,7 +111,7 @@ impl Validator for ResourceSet {
|
|||||||
|
|
||||||
impl PartialEq for ResourceSet {
|
impl PartialEq for ResourceSet {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.len() == other.len() && self.0.iter().all(|x| other.0.contains(x))
|
self.0.iter().all(|x| other.0.contains(x)) && other.0.iter().all(|x| self.0.contains(x))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,9 +134,9 @@ impl<'de> Deserialize<'de> for ResourceSet {
|
|||||||
E: de::Error,
|
E: de::Error,
|
||||||
{
|
{
|
||||||
let resource = Resource::try_from(value).map_err(|e| E::custom(format!("invalid resource: {}", e)))?;
|
let resource = Resource::try_from(value).map_err(|e| E::custom(format!("invalid resource: {}", e)))?;
|
||||||
let mut set = HashSet::new();
|
let mut resources = ResourceSet::default();
|
||||||
set.insert(resource);
|
resources.push_unique(resource);
|
||||||
Ok(ResourceSet(set))
|
Ok(resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
|
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
|
||||||
@@ -149,18 +144,18 @@ impl<'de> Deserialize<'de> for ResourceSet {
|
|||||||
A: de::SeqAccess<'de>,
|
A: de::SeqAccess<'de>,
|
||||||
A::Error: DeError,
|
A::Error: DeError,
|
||||||
{
|
{
|
||||||
let mut set = HashSet::with_capacity(seq.size_hint().unwrap_or(0));
|
let mut resources = ResourceSet(Vec::with_capacity(seq.size_hint().unwrap_or(0)));
|
||||||
while let Some(value) = seq.next_element::<String>()? {
|
while let Some(value) = seq.next_element::<String>()? {
|
||||||
match Resource::try_from(value.as_str()) {
|
match Resource::try_from(value.as_str()) {
|
||||||
Ok(resource) => {
|
Ok(resource) => {
|
||||||
set.insert(resource);
|
resources.push_unique(resource);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(A::Error::custom(format!("invalid resource: {}", e)));
|
return Err(A::Error::custom(format!("invalid resource: {}", e)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(ResourceSet(set))
|
Ok(resources)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use rustfs_iam::store::Store as IamStore;
|
|||||||
use rustfs_madmin::{
|
use rustfs_madmin::{
|
||||||
InfoAccessKeyResp, InfoServiceAccountResp, LDAPSpecificAccessKeyInfo, OpenIDSpecificAccessKeyInfo, ServiceAccountInfo,
|
InfoAccessKeyResp, InfoServiceAccountResp, LDAPSpecificAccessKeyInfo, OpenIDSpecificAccessKeyInfo, ServiceAccountInfo,
|
||||||
};
|
};
|
||||||
use rustfs_policy::policy::Policy;
|
use rustfs_policy::policy::{DEFAULT_VERSION, Policy};
|
||||||
use s3s::{S3Result, s3_error};
|
use s3s::{S3Result, s3_error};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
@@ -236,7 +236,10 @@ pub(crate) async fn build_info_service_account_resp<T: IamStore>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let policy = effective_policy
|
let policy = effective_policy
|
||||||
.map(|policy| {
|
.map(|mut policy| {
|
||||||
|
if policy.version.is_empty() {
|
||||||
|
policy.version = DEFAULT_VERSION.to_owned();
|
||||||
|
}
|
||||||
serde_json::to_string_pretty(&policy).map_err(|e| {
|
serde_json::to_string_pretty(&policy).map_err(|e| {
|
||||||
debug!("marshal policy failed, e: {:?}", e);
|
debug!("marshal policy failed, e: {:?}", e);
|
||||||
s3_error!(InternalError, "marshal policy failed")
|
s3_error!(InternalError, "marshal policy failed")
|
||||||
@@ -891,7 +894,10 @@ mod tests {
|
|||||||
assert_eq!(resp.open_id_specific_info.display_name.as_deref(), Some("RustFS User"));
|
assert_eq!(resp.open_id_specific_info.display_name.as_deref(), Some("RustFS User"));
|
||||||
assert_eq!(resp.open_id_specific_info.display_name_claim.as_deref(), Some("name"));
|
assert_eq!(resp.open_id_specific_info.display_name_claim.as_deref(), Some("name"));
|
||||||
assert_eq!(resp.open_id_specific_info.config_name, None);
|
assert_eq!(resp.open_id_specific_info.config_name, None);
|
||||||
assert_eq!(resp.info.policy, Some("{\n \"Version\": \"\",\n \"Statement\": []\n}".to_string()));
|
assert_eq!(
|
||||||
|
resp.info.policy,
|
||||||
|
Some("{\n \"Version\": \"2012-10-17\",\n \"Statement\": []\n}".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
let body = serde_json::to_value(&resp).expect("serialize openid sts response");
|
let body = serde_json::to_value(&resp).expect("serialize openid sts response");
|
||||||
assert_eq!(body.get("userType").and_then(|v| v.as_str()), Some("STS"));
|
assert_eq!(body.get("userType").and_then(|v| v.as_str()), Some("STS"));
|
||||||
@@ -929,7 +935,7 @@ mod tests {
|
|||||||
assert_eq!(body.get("description").and_then(|v| v.as_str()), Some("openid derived temporary account"));
|
assert_eq!(body.get("description").and_then(|v| v.as_str()), Some("openid derived temporary account"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
body.get("policy").and_then(|v| v.as_str()),
|
body.get("policy").and_then(|v| v.as_str()),
|
||||||
Some("{\n \"Version\": \"\",\n \"Statement\": []\n}")
|
Some("{\n \"Version\": \"2012-10-17\",\n \"Statement\": []\n}")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user