From 3928117c8f54d972d154162f22fda30635473d93 Mon Sep 17 00:00:00 2001 From: GatewayJ <835269233@qq.com> Date: Sun, 14 Jun 2026 16:36:28 +0800 Subject: [PATCH] fix(policy): preserve IAM policy readback shape (#3431) --- crates/policy/src/policy/action.rs | 53 +++--- crates/policy/src/policy/policy.rs | 223 ++++++++++++------------ crates/policy/src/policy/resource.rs | 47 +++-- rustfs/src/admin/access_key_identity.rs | 14 +- 4 files changed, 173 insertions(+), 164 deletions(-) diff --git a/crates/policy/src/policy/action.rs b/crates/policy/src/policy/action.rs index 148e40d06..19f9bc098 100644 --- a/crates/policy/src/policy/action.rs +++ b/crates/policy/src/policy/action.rs @@ -17,7 +17,7 @@ use serde::{ Deserialize, Deserializer, Serialize, de::{self, Error as DeError, Visitor}, }; -use std::{collections::HashSet, fmt, ops::Deref}; +use std::{fmt, ops::Deref}; use strum::{EnumString, IntoStaticStr}; 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, /// conforming to the S3 policy specification for consistency and compatibility. #[derive(Clone, Default, Debug)] -pub struct ActionSet(pub HashSet); +pub struct ActionSet(pub Vec); impl Serialize for ActionSet { fn serialize(&self, serializer: S) -> std::result::Result @@ -36,12 +36,10 @@ impl Serialize for ActionSet { // Always serialize as array, even for single action, to match S3 specification // and ensure compatibility with AWS SDK clients that expect array format - let mut actions: Vec<&str> = self.0.iter().map(Into::into).collect(); - actions.sort_unstable(); - - let mut seq = serializer.serialize_seq(Some(actions.len()))?; - for action in actions { - seq.serialize_element(action)?; + let mut seq = serializer.serialize_seq(Some(self.0.len()))?; + for action in &self.0 { + let action_str: &str = action.into(); + seq.serialize_element(action_str)?; } seq.end() } @@ -53,6 +51,12 @@ impl ActionSet { 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 { for act in self.0.iter() { if act.is_match(action) { @@ -71,7 +75,7 @@ impl ActionSet { } impl Deref for ActionSet { - type Target = HashSet; + type Target = [Action]; fn deref(&self) -> &Self::Target { &self.0 @@ -87,7 +91,7 @@ impl Validator for ActionSet { impl PartialEq for ActionSet { 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, { 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)) + let mut actions = ActionSet::default(); + actions.push_unique(action); + Ok(actions) } fn visit_seq(self, mut seq: A) -> std::result::Result @@ -120,18 +124,18 @@ impl<'de> Deserialize<'de> for ActionSet { A: de::SeqAccess<'de>, 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::()? { match Action::try_from(value.as_str()) { Ok(action) => { - set.insert(action); + actions.push_unique(action); } Err(e) => { return Err(A::Error::custom(format!("invalid action: {}", e))); } } } - Ok(ActionSet(set)) + Ok(actions) } } @@ -725,7 +729,6 @@ pub enum KmsAction { #[cfg(test)] mod tests { use super::*; - use std::collections::HashSet; #[test] fn test_action_wildcard_parsing() { @@ -774,9 +777,7 @@ mod tests { #[test] fn test_actionset_serialize_single_element() { // Single element should serialize as array for S3 specification compliance - let mut set = HashSet::new(); - set.insert(Action::S3Action(S3Action::GetObjectAction)); - let actionset = ActionSet(set); + let actionset = ActionSet(vec![Action::S3Action(S3Action::GetObjectAction)]); let json = serde_json::to_string(&actionset).expect("Should serialize"); let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse"); @@ -789,10 +790,10 @@ mod tests { #[test] fn test_actionset_serialize_multiple_elements() { // Multiple elements should serialize as array - let mut set = HashSet::new(); - set.insert(Action::S3Action(S3Action::GetObjectAction)); - set.insert(Action::S3Action(S3Action::PutObjectAction)); - let actionset = ActionSet(set); + let actionset = ActionSet(vec![ + Action::S3Action(S3Action::GetObjectAction), + Action::S3Action(S3Action::PutObjectAction), + ]); let json = serde_json::to_string(&actionset).expect("Should serialize"); let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse"); @@ -804,9 +805,7 @@ mod tests { #[test] fn test_actionset_wildcard_serialization() { // Wildcard action should serialize as array for S3 specification compliance - let mut set = HashSet::new(); - set.insert(Action::try_from("*").expect("Should parse wildcard")); - let actionset = ActionSet(set); + let actionset = ActionSet(vec![Action::try_from("*").expect("Should parse wildcard")]); let json = serde_json::to_string(&actionset).expect("Should serialize"); let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse"); diff --git a/crates/policy/src/policy/policy.rs b/crates/policy/src/policy/policy.rs index d90fdf420..b2e766f5a 100644 --- a/crates/policy/src/policy/policy.rs +++ b/crates/policy/src/policy/policy.rs @@ -355,7 +355,7 @@ pub async fn bucket_policy_needs_existing_object_tag_for_args(policy: &BucketPol } pub mod default { - use std::{collections::HashSet, sync::LazyLock}; + use std::sync::LazyLock; use crate::policy::{ ActionSet, DEFAULT_VERSION, Effect, Functions, ResourceSet, Statement, @@ -377,28 +377,16 @@ pub mod default { Statement { sid: "".into(), effect: Effect::Allow, - actions: ActionSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Action::S3Action(S3Action::AllActions)); - hash_set - }), + actions: ActionSet(vec![Action::S3Action(S3Action::AllActions)]), not_actions: ActionSet(Default::default()), - resources: ResourceSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Resource::S3("*".into())); - hash_set - }), + resources: ResourceSet(vec![Resource::S3("*".into())]), conditions: Functions::default(), ..Default::default() }, Statement { sid: "".into(), effect: Effect::Allow, - actions: ActionSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Action::StsAction(StsAction::AssumeRoleAction)); - hash_set - }), + actions: ActionSet(vec![Action::StsAction(StsAction::AssumeRoleAction)]), not_actions: ActionSet(Default::default()), resources: ResourceSet(Default::default()), conditions: Functions::default(), @@ -416,30 +404,20 @@ pub mod default { Statement { sid: "".into(), effect: Effect::Allow, - actions: ActionSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Action::S3Action(S3Action::GetBucketLocationAction)); - hash_set.insert(Action::S3Action(S3Action::GetObjectAction)); - hash_set.insert(Action::S3Action(S3Action::GetBucketQuotaAction)); - hash_set - }), + actions: ActionSet(vec![ + Action::S3Action(S3Action::GetBucketLocationAction), + Action::S3Action(S3Action::GetObjectAction), + Action::S3Action(S3Action::GetBucketQuotaAction), + ]), not_actions: ActionSet(Default::default()), - resources: ResourceSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Resource::S3("*".into())); - hash_set - }), + resources: ResourceSet(vec![Resource::S3("*".into())]), conditions: Functions::default(), ..Default::default() }, Statement { sid: "".into(), effect: Effect::Allow, - actions: ActionSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Action::StsAction(StsAction::AssumeRoleAction)); - hash_set - }), + actions: ActionSet(vec![Action::StsAction(StsAction::AssumeRoleAction)]), not_actions: ActionSet(Default::default()), resources: ResourceSet(Default::default()), conditions: Functions::default(), @@ -457,28 +435,16 @@ pub mod default { Statement { sid: "".into(), effect: Effect::Allow, - actions: ActionSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Action::S3Action(S3Action::PutObjectAction)); - hash_set - }), + actions: ActionSet(vec![Action::S3Action(S3Action::PutObjectAction)]), not_actions: ActionSet(Default::default()), - resources: ResourceSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Resource::S3("*".into())); - hash_set - }), + resources: ResourceSet(vec![Resource::S3("*".into())]), conditions: Functions::default(), ..Default::default() }, Statement { sid: "".into(), effect: Effect::Allow, - actions: ActionSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Action::StsAction(StsAction::AssumeRoleAction)); - hash_set - }), + actions: ActionSet(vec![Action::StsAction(StsAction::AssumeRoleAction)]), not_actions: ActionSet(Default::default()), resources: ResourceSet(Default::default()), conditions: Functions::default(), @@ -496,35 +462,25 @@ pub mod default { Statement { sid: "".into(), effect: Effect::Allow, - actions: ActionSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Action::AdminAction(AdminAction::ProfilingAdminAction)); - hash_set.insert(Action::AdminAction(AdminAction::TraceAdminAction)); - hash_set.insert(Action::AdminAction(AdminAction::ConsoleLogAdminAction)); - hash_set.insert(Action::AdminAction(AdminAction::ServerInfoAdminAction)); - hash_set.insert(Action::AdminAction(AdminAction::TopLocksAdminAction)); - hash_set.insert(Action::AdminAction(AdminAction::HealthInfoAdminAction)); - hash_set.insert(Action::AdminAction(AdminAction::PrometheusAdminAction)); - hash_set.insert(Action::AdminAction(AdminAction::BandwidthMonitorAction)); - hash_set - }), + actions: ActionSet(vec![ + Action::AdminAction(AdminAction::ProfilingAdminAction), + Action::AdminAction(AdminAction::TraceAdminAction), + Action::AdminAction(AdminAction::ConsoleLogAdminAction), + Action::AdminAction(AdminAction::ServerInfoAdminAction), + Action::AdminAction(AdminAction::TopLocksAdminAction), + Action::AdminAction(AdminAction::HealthInfoAdminAction), + Action::AdminAction(AdminAction::PrometheusAdminAction), + Action::AdminAction(AdminAction::BandwidthMonitorAction), + ]), not_actions: ActionSet(Default::default()), - resources: ResourceSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Resource::S3("*".into())); - hash_set - }), + resources: ResourceSet(vec![Resource::S3("*".into())]), conditions: Functions::default(), ..Default::default() }, Statement { sid: "".into(), effect: Effect::Allow, - actions: ActionSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Action::StsAction(StsAction::AssumeRoleAction)); - hash_set - }), + actions: ActionSet(vec![Action::StsAction(StsAction::AssumeRoleAction)]), not_actions: ActionSet(Default::default()), resources: ResourceSet(Default::default()), conditions: Functions::default(), @@ -542,56 +498,36 @@ pub mod default { Statement { sid: "".into(), effect: Effect::Allow, - actions: ActionSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Action::AdminAction(AdminAction::AllAdminActions)); - hash_set - }), + actions: ActionSet(vec![Action::AdminAction(AdminAction::AllAdminActions)]), not_actions: ActionSet(Default::default()), - resources: ResourceSet(HashSet::new()), + resources: ResourceSet(Vec::new()), conditions: Functions::default(), ..Default::default() }, Statement { sid: "".into(), effect: Effect::Allow, - actions: ActionSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Action::KmsAction(KmsAction::AllActions)); - hash_set - }), + actions: ActionSet(vec![Action::KmsAction(KmsAction::AllActions)]), not_actions: ActionSet(Default::default()), - resources: ResourceSet(HashSet::new()), + resources: ResourceSet(Vec::new()), conditions: Functions::default(), ..Default::default() }, Statement { sid: "".into(), effect: Effect::Allow, - actions: ActionSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Action::S3Action(S3Action::AllActions)); - hash_set - }), + actions: ActionSet(vec![Action::S3Action(S3Action::AllActions)]), not_actions: ActionSet(Default::default()), - resources: ResourceSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Resource::S3("*".into())); - hash_set - }), + resources: ResourceSet(vec![Resource::S3("*".into())]), conditions: Functions::default(), ..Default::default() }, Statement { sid: "".into(), effect: Effect::Allow, - actions: ActionSet({ - let mut hash_set = HashSet::new(); - hash_set.insert(Action::StsAction(StsAction::AssumeRoleAction)); - hash_set - }), + actions: ActionSet(vec![Action::StsAction(StsAction::AssumeRoleAction)]), not_actions: ActionSet(Default::default()), - resources: ResourceSet(HashSet::new()), + resources: ResourceSet(Vec::new()), conditions: Functions::default(), ..Default::default() }, @@ -1947,7 +1883,7 @@ mod 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( br#"{ "Version":"2012-10-17", @@ -1975,8 +1911,8 @@ mod test { .collect(); assert_eq!( actions, - vec!["s3:DeleteObject", "s3:GetObject", "s3:ListBucket", "s3:PutObject"], - "Action serialization should use a stable order" + vec!["s3:PutObject", "s3:DeleteObject", "s3:ListBucket", "s3:GetObject"], + "Action serialization should preserve input order" ); let resources: Vec<_> = statement["Resource"] @@ -1987,11 +1923,84 @@ mod test { .collect(); assert_eq!( resources, - vec!["arn:aws:s3:::example-bucket", "arn:aws:s3:::example-bucket/*"], - "Resource serialization should use a stable order" + vec!["arn:aws:s3:::example-bucket/*", "arn:aws:s3:::example-bucket"], + "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] fn test_bucket_policy_serialize_omits_empty_fields() { use crate::policy::action::{Action, ActionSet, S3Action}; @@ -2021,11 +2030,11 @@ mod test { policy.statements[0] .actions .0 - .insert(Action::S3Action(S3Action::ListBucketAction)); + .push(Action::S3Action(S3Action::ListBucketAction)); policy.statements[0] .resources .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 parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse"); @@ -2134,11 +2143,11 @@ mod test { policy.statements[0] .actions .0 - .insert(Action::S3Action(S3Action::ListBucketAction)); + .push(Action::S3Action(S3Action::ListBucketAction)); policy.statements[0] .resources .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 parsed: serde_json::Value = serde_json::from_str(&json).expect("Should parse"); diff --git a/crates/policy/src/policy/resource.rs b/crates/policy/src/policy/resource.rs index 223c62499..2b6c03e72 100644 --- a/crates/policy/src/policy/resource.rs +++ b/crates/policy/src/policy/resource.rs @@ -17,12 +17,7 @@ use serde::{ Deserialize, Deserializer, Serialize, de::{self, Error as DeError, Visitor}, }; -use std::{ - collections::{HashMap, HashSet}, - fmt, - hash::Hash, - ops::Deref, -}; +use std::{collections::HashMap, fmt, ops::Deref}; use super::{ Error as IamError, Validator, @@ -32,7 +27,7 @@ use super::{ }; #[derive(Clone, Default, Debug)] -pub struct ResourceSet(pub HashSet); +pub struct ResourceSet(pub Vec); impl Serialize for ResourceSet { fn serialize(&self, serializer: S) -> std::result::Result @@ -41,19 +36,13 @@ impl Serialize for ResourceSet { { use serde::ser::SerializeSeq; - let mut resources: Vec = self - .0 - .iter() - .map(|resource| match resource { + let mut seq = serializer.serialize_seq(Some(self.0.len()))?; + for resource in &self.0 { + let resource_str = match resource { Resource::S3(value) => format!("{}{}", Resource::S3_PREFIX, value), Resource::Kms(value) => value.clone(), - }) - .collect(); - resources.sort_unstable(); - - let mut seq = serializer.serialize_seq(Some(resources.len()))?; - for resource in resources { - seq.serialize_element(&resource)?; + }; + seq.serialize_element(&resource_str)?; } seq.end() } @@ -65,6 +54,12 @@ impl ResourceSet { 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>) -> bool { self.is_match_with_resolver(resource, conditions, None).await } @@ -96,7 +91,7 @@ impl ResourceSet { } impl Deref for ResourceSet { - type Target = HashSet; + type Target = [Resource]; fn deref(&self) -> &Self::Target { &self.0 @@ -116,7 +111,7 @@ impl Validator for ResourceSet { impl PartialEq for ResourceSet { 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, { 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)) + let mut resources = ResourceSet::default(); + resources.push_unique(resource); + Ok(resources) } fn visit_seq(self, mut seq: A) -> std::result::Result @@ -149,18 +144,18 @@ impl<'de> Deserialize<'de> for ResourceSet { A: de::SeqAccess<'de>, 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::()? { match Resource::try_from(value.as_str()) { Ok(resource) => { - set.insert(resource); + resources.push_unique(resource); } Err(e) => { return Err(A::Error::custom(format!("invalid resource: {}", e))); } } } - Ok(ResourceSet(set)) + Ok(resources) } } diff --git a/rustfs/src/admin/access_key_identity.rs b/rustfs/src/admin/access_key_identity.rs index bbf6ada78..616a7adce 100644 --- a/rustfs/src/admin/access_key_identity.rs +++ b/rustfs/src/admin/access_key_identity.rs @@ -21,7 +21,7 @@ use rustfs_iam::store::Store as IamStore; use rustfs_madmin::{ InfoAccessKeyResp, InfoServiceAccountResp, LDAPSpecificAccessKeyInfo, OpenIDSpecificAccessKeyInfo, ServiceAccountInfo, }; -use rustfs_policy::policy::Policy; +use rustfs_policy::policy::{DEFAULT_VERSION, Policy}; use s3s::{S3Result, s3_error}; use std::collections::HashMap; use time::OffsetDateTime; @@ -236,7 +236,10 @@ pub(crate) async fn build_info_service_account_resp( }; 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| { debug!("marshal policy failed, e: {:?}", e); 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_claim.as_deref(), Some("name")); 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"); 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("policy").and_then(|v| v.as_str()), - Some("{\n \"Version\": \"\",\n \"Statement\": []\n}") + Some("{\n \"Version\": \"2012-10-17\",\n \"Statement\": []\n}") ); } }