mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-21 20:06:37 +00:00
feat(policy): add Service principal, ArnLike/IfExists conditions, and logging error ordering (#2018)
This commit is contained in:
@@ -266,6 +266,10 @@ pub enum S3Action {
|
|||||||
PutBucketLifecycleAction,
|
PutBucketLifecycleAction,
|
||||||
#[strum(serialize = "s3:GetBucketLifecycle")]
|
#[strum(serialize = "s3:GetBucketLifecycle")]
|
||||||
GetBucketLifecycleAction,
|
GetBucketLifecycleAction,
|
||||||
|
#[strum(serialize = "s3:PutBucketLogging")]
|
||||||
|
PutBucketLoggingAction,
|
||||||
|
#[strum(serialize = "s3:GetBucketLogging")]
|
||||||
|
GetBucketLoggingAction,
|
||||||
#[strum(serialize = "s3:PutBucketNotification")]
|
#[strum(serialize = "s3:PutBucketNotification")]
|
||||||
PutBucketNotificationAction,
|
PutBucketNotificationAction,
|
||||||
#[strum(serialize = "s3:PutBucketPolicy")]
|
#[strum(serialize = "s3:PutBucketPolicy")]
|
||||||
|
|||||||
@@ -82,17 +82,17 @@ impl Serialize for Functions {
|
|||||||
serializer.serialize_map(Some(self.for_any_value.len() + self.for_all_values.len() + self.for_normal.len()))?;
|
serializer.serialize_map(Some(self.for_any_value.len() + self.for_all_values.len() + self.for_normal.len()))?;
|
||||||
|
|
||||||
for conditions in self.for_all_values.iter() {
|
for conditions in self.for_all_values.iter() {
|
||||||
se.serialize_key(format!("ForAllValues:{}", conditions.to_key()).as_str())?;
|
se.serialize_key(&format!("ForAllValues:{}", conditions.to_key_with_suffix()))?;
|
||||||
conditions.serialize_map(&mut se)?;
|
conditions.serialize_map(&mut se)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for conditions in self.for_any_value.iter() {
|
for conditions in self.for_any_value.iter() {
|
||||||
se.serialize_key(format!("ForAnyValue:{}", conditions.to_key()).as_str())?;
|
se.serialize_key(&format!("ForAnyValue:{}", conditions.to_key_with_suffix()))?;
|
||||||
conditions.serialize_map(&mut se)?;
|
conditions.serialize_map(&mut se)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for conditions in self.for_normal.iter() {
|
for conditions in self.for_normal.iter() {
|
||||||
se.serialize_key(conditions.to_key())?;
|
se.serialize_key(&conditions.to_key_with_suffix())?;
|
||||||
conditions.serialize_map(&mut se)?;
|
conditions.serialize_map(&mut se)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ pub enum Condition {
|
|||||||
StringNotEqualsIgnoreCase(StringFunc),
|
StringNotEqualsIgnoreCase(StringFunc),
|
||||||
StringLike(StringFunc),
|
StringLike(StringFunc),
|
||||||
StringNotLike(StringFunc),
|
StringNotLike(StringFunc),
|
||||||
|
ArnLike(StringFunc),
|
||||||
|
ArnNotLike(StringFunc),
|
||||||
|
ArnEquals(StringFunc),
|
||||||
|
ArnNotEquals(StringFunc),
|
||||||
BinaryEquals(BinaryFunc),
|
BinaryEquals(BinaryFunc),
|
||||||
IpAddress(AddrFunc),
|
IpAddress(AddrFunc),
|
||||||
NotIpAddress(AddrFunc),
|
NotIpAddress(AddrFunc),
|
||||||
@@ -47,6 +51,10 @@ pub enum Condition {
|
|||||||
DateLessThanEquals(DateFunc),
|
DateLessThanEquals(DateFunc),
|
||||||
DateGreaterThan(DateFunc),
|
DateGreaterThan(DateFunc),
|
||||||
DateGreaterThanEquals(DateFunc),
|
DateGreaterThanEquals(DateFunc),
|
||||||
|
/// Wraps any condition with IfExists semantics: if none of the
|
||||||
|
/// referenced keys are present in the request context, the
|
||||||
|
/// condition evaluates to true.
|
||||||
|
IfExists(Box<Condition>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Condition {
|
impl Condition {
|
||||||
@@ -58,6 +66,10 @@ impl Condition {
|
|||||||
"StringNotEqualsIgnoreCase" => Self::StringNotEqualsIgnoreCase(d.next_value()?),
|
"StringNotEqualsIgnoreCase" => Self::StringNotEqualsIgnoreCase(d.next_value()?),
|
||||||
"StringLike" => Self::StringLike(d.next_value()?),
|
"StringLike" => Self::StringLike(d.next_value()?),
|
||||||
"StringNotLike" => Self::StringNotLike(d.next_value()?),
|
"StringNotLike" => Self::StringNotLike(d.next_value()?),
|
||||||
|
"ArnLike" => Self::ArnLike(d.next_value()?),
|
||||||
|
"ArnNotLike" => Self::ArnNotLike(d.next_value()?),
|
||||||
|
"ArnEquals" => Self::ArnEquals(d.next_value()?),
|
||||||
|
"ArnNotEquals" => Self::ArnNotEquals(d.next_value()?),
|
||||||
"BinaryEquals" => Self::BinaryEquals(d.next_value()?),
|
"BinaryEquals" => Self::BinaryEquals(d.next_value()?),
|
||||||
"IpAddress" => Self::IpAddress(d.next_value()?),
|
"IpAddress" => Self::IpAddress(d.next_value()?),
|
||||||
"NotIpAddress" => Self::NotIpAddress(d.next_value()?),
|
"NotIpAddress" => Self::NotIpAddress(d.next_value()?),
|
||||||
@@ -74,6 +86,11 @@ impl Condition {
|
|||||||
"DateLessThanEquals" => Self::DateLessThanEquals(d.next_value()?),
|
"DateLessThanEquals" => Self::DateLessThanEquals(d.next_value()?),
|
||||||
"DateGreaterThan" => Self::DateGreaterThan(d.next_value()?),
|
"DateGreaterThan" => Self::DateGreaterThan(d.next_value()?),
|
||||||
"DateGreaterThanEquals" => Self::DateGreaterThanEquals(d.next_value()?),
|
"DateGreaterThanEquals" => Self::DateGreaterThanEquals(d.next_value()?),
|
||||||
|
_ if key.ends_with("IfExists") => {
|
||||||
|
let base = key.strip_suffix("IfExists").unwrap_or(key);
|
||||||
|
let inner = Self::from_deserializer(base, d)?;
|
||||||
|
Self::IfExists(Box::new(inner))
|
||||||
|
}
|
||||||
_ => Err(Error::custom(format!("unknown key: {key}")))?,
|
_ => Err(Error::custom(format!("unknown key: {key}")))?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -86,6 +103,10 @@ impl Condition {
|
|||||||
Condition::StringNotEqualsIgnoreCase(_) => "StringNotEqualsIgnoreCase",
|
Condition::StringNotEqualsIgnoreCase(_) => "StringNotEqualsIgnoreCase",
|
||||||
Condition::StringLike(_) => "StringLike",
|
Condition::StringLike(_) => "StringLike",
|
||||||
Condition::StringNotLike(_) => "StringNotLike",
|
Condition::StringNotLike(_) => "StringNotLike",
|
||||||
|
Condition::ArnLike(_) => "ArnLike",
|
||||||
|
Condition::ArnNotLike(_) => "ArnNotLike",
|
||||||
|
Condition::ArnEquals(_) => "ArnEquals",
|
||||||
|
Condition::ArnNotEquals(_) => "ArnNotEquals",
|
||||||
Condition::BinaryEquals(_) => "BinaryEquals",
|
Condition::BinaryEquals(_) => "BinaryEquals",
|
||||||
Condition::IpAddress(_) => "IpAddress",
|
Condition::IpAddress(_) => "IpAddress",
|
||||||
Condition::NotIpAddress(_) => "NotIpAddress",
|
Condition::NotIpAddress(_) => "NotIpAddress",
|
||||||
@@ -104,45 +125,98 @@ impl Condition {
|
|||||||
Condition::DateLessThanEquals(_) => "DateLessThanEquals",
|
Condition::DateLessThanEquals(_) => "DateLessThanEquals",
|
||||||
Condition::DateGreaterThan(_) => "DateGreaterThan",
|
Condition::DateGreaterThan(_) => "DateGreaterThan",
|
||||||
Condition::DateGreaterThanEquals(_) => "DateGreaterThanEquals",
|
Condition::DateGreaterThanEquals(_) => "DateGreaterThanEquals",
|
||||||
|
Condition::IfExists(_) => "IfExists",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn evaluate_with_resolver(
|
pub fn to_key_with_suffix(&self) -> String {
|
||||||
&self,
|
match self {
|
||||||
for_all: bool,
|
Condition::IfExists(inner) => format!("{}IfExists", inner.to_key()),
|
||||||
values: &HashMap<String, Vec<String>>,
|
_ => self.to_key().to_owned(),
|
||||||
resolver: Option<&dyn PolicyVariableResolver>,
|
}
|
||||||
) -> bool {
|
}
|
||||||
|
|
||||||
|
pub fn has_any_key_in(&self, values: &HashMap<String, Vec<String>>) -> bool {
|
||||||
use Condition::*;
|
use Condition::*;
|
||||||
|
match self {
|
||||||
|
StringEquals(s)
|
||||||
|
| StringNotEquals(s)
|
||||||
|
| StringEqualsIgnoreCase(s)
|
||||||
|
| StringNotEqualsIgnoreCase(s)
|
||||||
|
| StringLike(s)
|
||||||
|
| StringNotLike(s)
|
||||||
|
| ArnLike(s)
|
||||||
|
| ArnNotLike(s)
|
||||||
|
| ArnEquals(s)
|
||||||
|
| ArnNotEquals(s) => s.key_names().any(|k| values.contains_key(k.as_str())),
|
||||||
|
BinaryEquals(s) => s.key_names().any(|k| values.contains_key(k.as_str())),
|
||||||
|
IpAddress(s) | NotIpAddress(s) => s.key_names().any(|k| values.contains_key(k.as_str())),
|
||||||
|
Null(s) | Bool(s) => s.key_names().any(|k| values.contains_key(k.as_str())),
|
||||||
|
NumericEquals(s)
|
||||||
|
| NumericNotEquals(s)
|
||||||
|
| NumericLessThan(s)
|
||||||
|
| NumericLessThanEquals(s)
|
||||||
|
| NumericGreaterThan(s)
|
||||||
|
| NumericGreaterThanIfExists(s)
|
||||||
|
| NumericGreaterThanEquals(s) => s.key_names().any(|k| values.contains_key(k.as_str())),
|
||||||
|
DateEquals(s)
|
||||||
|
| DateNotEquals(s)
|
||||||
|
| DateLessThan(s)
|
||||||
|
| DateLessThanEquals(s)
|
||||||
|
| DateGreaterThan(s)
|
||||||
|
| DateGreaterThanEquals(s) => s.key_names().any(|k| values.contains_key(k.as_str())),
|
||||||
|
IfExists(inner) => inner.has_any_key_in(values),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let r = match self {
|
pub fn evaluate_with_resolver<'a>(
|
||||||
StringEquals(s) => s.evaluate_with_resolver(for_all, false, false, false, values, resolver).await,
|
&'a self,
|
||||||
StringNotEquals(s) => s.evaluate_with_resolver(for_all, false, false, true, values, resolver).await,
|
for_all: bool,
|
||||||
StringEqualsIgnoreCase(s) => s.evaluate_with_resolver(for_all, true, false, false, values, resolver).await,
|
values: &'a HashMap<String, Vec<String>>,
|
||||||
StringNotEqualsIgnoreCase(s) => s.evaluate_with_resolver(for_all, true, false, true, values, resolver).await,
|
resolver: Option<&'a dyn PolicyVariableResolver>,
|
||||||
StringLike(s) => s.evaluate_with_resolver(for_all, false, true, false, values, resolver).await,
|
) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send + 'a>> {
|
||||||
StringNotLike(s) => s.evaluate_with_resolver(for_all, false, true, true, values, resolver).await,
|
Box::pin(async move {
|
||||||
BinaryEquals(s) => s.evaluate(values),
|
use Condition::*;
|
||||||
IpAddress(s) => s.evaluate(values),
|
|
||||||
NotIpAddress(s) => s.evaluate(values),
|
|
||||||
Null(s) => s.evaluate_null(values),
|
|
||||||
Bool(s) => s.evaluate_bool(values),
|
|
||||||
NumericEquals(s) => s.evaluate(i64::eq, false, values),
|
|
||||||
NumericNotEquals(s) => s.evaluate(i64::ne, false, values),
|
|
||||||
NumericLessThan(s) => s.evaluate(i64::lt, false, values),
|
|
||||||
NumericLessThanEquals(s) => s.evaluate(i64::le, false, values),
|
|
||||||
NumericGreaterThan(s) => s.evaluate(i64::gt, false, values),
|
|
||||||
NumericGreaterThanIfExists(s) => s.evaluate(i64::ge, true, values),
|
|
||||||
NumericGreaterThanEquals(s) => s.evaluate(i64::ge, false, values),
|
|
||||||
DateEquals(s) => s.evaluate(OffsetDateTime::eq, values),
|
|
||||||
DateNotEquals(s) => s.evaluate(OffsetDateTime::ne, values),
|
|
||||||
DateLessThan(s) => s.evaluate(OffsetDateTime::lt, values),
|
|
||||||
DateLessThanEquals(s) => s.evaluate(OffsetDateTime::le, values),
|
|
||||||
DateGreaterThan(s) => s.evaluate(OffsetDateTime::gt, values),
|
|
||||||
DateGreaterThanEquals(s) => s.evaluate(OffsetDateTime::ge, values),
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.is_negate() { !r } else { r }
|
let r = match self {
|
||||||
|
StringEquals(s) => s.evaluate_with_resolver(for_all, false, false, false, values, resolver).await,
|
||||||
|
StringNotEquals(s) => s.evaluate_with_resolver(for_all, false, false, true, values, resolver).await,
|
||||||
|
StringEqualsIgnoreCase(s) => s.evaluate_with_resolver(for_all, true, false, false, values, resolver).await,
|
||||||
|
StringNotEqualsIgnoreCase(s) => s.evaluate_with_resolver(for_all, true, false, true, values, resolver).await,
|
||||||
|
StringLike(s) => s.evaluate_with_resolver(for_all, false, true, false, values, resolver).await,
|
||||||
|
StringNotLike(s) => s.evaluate_with_resolver(for_all, false, true, true, values, resolver).await,
|
||||||
|
ArnLike(s) => s.evaluate_with_resolver(for_all, false, true, false, values, resolver).await,
|
||||||
|
ArnNotLike(s) => s.evaluate_with_resolver(for_all, false, true, true, values, resolver).await,
|
||||||
|
ArnEquals(s) => s.evaluate_with_resolver(for_all, false, false, false, values, resolver).await,
|
||||||
|
ArnNotEquals(s) => s.evaluate_with_resolver(for_all, false, false, true, values, resolver).await,
|
||||||
|
BinaryEquals(s) => s.evaluate(values),
|
||||||
|
IpAddress(s) => s.evaluate(values),
|
||||||
|
NotIpAddress(s) => s.evaluate(values),
|
||||||
|
Null(s) => s.evaluate_null(values),
|
||||||
|
Bool(s) => s.evaluate_bool(values),
|
||||||
|
NumericEquals(s) => s.evaluate(i64::eq, false, values),
|
||||||
|
NumericNotEquals(s) => s.evaluate(i64::ne, false, values),
|
||||||
|
NumericLessThan(s) => s.evaluate(i64::lt, false, values),
|
||||||
|
NumericLessThanEquals(s) => s.evaluate(i64::le, false, values),
|
||||||
|
NumericGreaterThan(s) => s.evaluate(i64::gt, false, values),
|
||||||
|
NumericGreaterThanIfExists(s) => s.evaluate(i64::ge, true, values),
|
||||||
|
NumericGreaterThanEquals(s) => s.evaluate(i64::ge, false, values),
|
||||||
|
DateEquals(s) => s.evaluate(OffsetDateTime::eq, values),
|
||||||
|
DateNotEquals(s) => s.evaluate(OffsetDateTime::ne, values),
|
||||||
|
DateLessThan(s) => s.evaluate(OffsetDateTime::lt, values),
|
||||||
|
DateLessThanEquals(s) => s.evaluate(OffsetDateTime::le, values),
|
||||||
|
DateGreaterThan(s) => s.evaluate(OffsetDateTime::gt, values),
|
||||||
|
DateGreaterThanEquals(s) => s.evaluate(OffsetDateTime::ge, values),
|
||||||
|
IfExists(inner) => {
|
||||||
|
if !inner.has_any_key_in(values) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return inner.evaluate_with_resolver(for_all, values, resolver).await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.is_negate() { !r } else { r }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -161,6 +235,10 @@ impl Condition {
|
|||||||
Condition::StringNotEqualsIgnoreCase(s) => se.serialize_value(s),
|
Condition::StringNotEqualsIgnoreCase(s) => se.serialize_value(s),
|
||||||
Condition::StringLike(s) => se.serialize_value(s),
|
Condition::StringLike(s) => se.serialize_value(s),
|
||||||
Condition::StringNotLike(s) => se.serialize_value(s),
|
Condition::StringNotLike(s) => se.serialize_value(s),
|
||||||
|
Condition::ArnLike(s) => se.serialize_value(s),
|
||||||
|
Condition::ArnNotLike(s) => se.serialize_value(s),
|
||||||
|
Condition::ArnEquals(s) => se.serialize_value(s),
|
||||||
|
Condition::ArnNotEquals(s) => se.serialize_value(s),
|
||||||
Condition::BinaryEquals(s) => se.serialize_value(s),
|
Condition::BinaryEquals(s) => se.serialize_value(s),
|
||||||
Condition::IpAddress(s) => se.serialize_value(s),
|
Condition::IpAddress(s) => se.serialize_value(s),
|
||||||
Condition::NotIpAddress(s) => se.serialize_value(s),
|
Condition::NotIpAddress(s) => se.serialize_value(s),
|
||||||
@@ -179,6 +257,7 @@ impl Condition {
|
|||||||
Condition::DateLessThanEquals(s) => se.serialize_value(s),
|
Condition::DateLessThanEquals(s) => se.serialize_value(s),
|
||||||
Condition::DateGreaterThan(s) => se.serialize_value(s),
|
Condition::DateGreaterThan(s) => se.serialize_value(s),
|
||||||
Condition::DateGreaterThanEquals(s) => se.serialize_value(s),
|
Condition::DateGreaterThanEquals(s) => se.serialize_value(s),
|
||||||
|
Condition::IfExists(inner) => inner.serialize_map(se),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,6 +289,11 @@ impl PartialEq for Condition {
|
|||||||
(Self::DateLessThanEquals(l0), Self::DateLessThanEquals(r0)) => l0 == r0,
|
(Self::DateLessThanEquals(l0), Self::DateLessThanEquals(r0)) => l0 == r0,
|
||||||
(Self::DateGreaterThan(l0), Self::DateGreaterThan(r0)) => l0 == r0,
|
(Self::DateGreaterThan(l0), Self::DateGreaterThan(r0)) => l0 == r0,
|
||||||
(Self::DateGreaterThanEquals(l0), Self::DateGreaterThanEquals(r0)) => l0 == r0,
|
(Self::DateGreaterThanEquals(l0), Self::DateGreaterThanEquals(r0)) => l0 == r0,
|
||||||
|
(Self::ArnLike(l0), Self::ArnLike(r0)) => l0 == r0,
|
||||||
|
(Self::ArnNotLike(l0), Self::ArnNotLike(r0)) => l0 == r0,
|
||||||
|
(Self::ArnEquals(l0), Self::ArnEquals(r0)) => l0 == r0,
|
||||||
|
(Self::ArnNotEquals(l0), Self::ArnNotEquals(r0)) => l0 == r0,
|
||||||
|
(Self::IfExists(l0), Self::IfExists(r0)) => l0 == r0,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ impl<T: Clone> Clone for InnerFunc<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> InnerFunc<T> {
|
||||||
|
pub fn key_names(&self) -> impl Iterator<Item = String> + '_ {
|
||||||
|
self.0.iter().map(|kv| kv.key.name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: Serialize> Serialize for InnerFunc<T> {
|
impl<T: Serialize> Serialize for InnerFunc<T> {
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -336,6 +336,12 @@ pub enum AwsKeyName {
|
|||||||
|
|
||||||
#[strum(serialize = "aws:groups")]
|
#[strum(serialize = "aws:groups")]
|
||||||
AWSGroups,
|
AWSGroups,
|
||||||
|
|
||||||
|
#[strum(serialize = "aws:SourceArn")]
|
||||||
|
AWSSourceArn,
|
||||||
|
|
||||||
|
#[strum(serialize = "aws:SourceAccount")]
|
||||||
|
AWSSourceAccount,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ use std::collections::HashSet;
|
|||||||
|
|
||||||
/// Principal that serializes AWS field as single string when containing only "*",
|
/// Principal that serializes AWS field as single string when containing only "*",
|
||||||
/// or as an array otherwise (matching AWS S3 API format).
|
/// or as an array otherwise (matching AWS S3 API format).
|
||||||
|
/// Also supports Service principals (e.g., "logging.s3.amazonaws.com").
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
pub struct Principal {
|
pub struct Principal {
|
||||||
aws: HashSet<String>,
|
aws: HashSet<String>,
|
||||||
|
service: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for Principal {
|
impl Serialize for Principal {
|
||||||
@@ -31,15 +33,27 @@ impl Serialize for Principal {
|
|||||||
{
|
{
|
||||||
use serde::ser::SerializeMap;
|
use serde::ser::SerializeMap;
|
||||||
|
|
||||||
let mut map = serializer.serialize_map(Some(1))?;
|
let field_count = usize::from(!self.aws.is_empty()) + usize::from(!self.service.is_empty());
|
||||||
|
let mut map = serializer.serialize_map(Some(field_count))?;
|
||||||
|
|
||||||
// If single element, serialize as string; otherwise as array
|
if !self.aws.is_empty() {
|
||||||
if self.aws.len() == 1 {
|
if self.aws.len() == 1 {
|
||||||
if let Some(val) = self.aws.iter().next() {
|
if let Some(val) = self.aws.iter().next() {
|
||||||
map.serialize_entry("AWS", val)?;
|
map.serialize_entry("AWS", val)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
map.serialize_entry("AWS", &self.aws)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.service.is_empty() {
|
||||||
|
if self.service.len() == 1 {
|
||||||
|
if let Some(val) = self.service.iter().next() {
|
||||||
|
map.serialize_entry("Service", val)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
map.serialize_entry("Service", &self.service)?;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
map.serialize_entry("AWS", &self.aws)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
map.end()
|
map.end()
|
||||||
@@ -50,22 +64,33 @@ impl Serialize for Principal {
|
|||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
enum PrincipalFormat {
|
enum PrincipalFormat {
|
||||||
Wildcard(String),
|
Wildcard(String),
|
||||||
AwsObject(PrincipalAwsObject),
|
Object(PrincipalObject),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct PrincipalAwsObject {
|
struct PrincipalObject {
|
||||||
#[serde(rename = "AWS")]
|
#[serde(rename = "AWS", default)]
|
||||||
aws: AwsValues,
|
aws: Option<PrincipalValues>,
|
||||||
|
#[serde(rename = "Service", default)]
|
||||||
|
service: Option<PrincipalValues>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
enum AwsValues {
|
enum PrincipalValues {
|
||||||
Single(String),
|
Single(String),
|
||||||
Multiple(HashSet<String>),
|
Multiple(HashSet<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PrincipalValues {
|
||||||
|
fn into_set(self) -> HashSet<String> {
|
||||||
|
match self {
|
||||||
|
PrincipalValues::Single(s) => HashSet::from([s]),
|
||||||
|
PrincipalValues::Multiple(set) => set,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'de> serde::Deserialize<'de> for Principal {
|
impl<'de> serde::Deserialize<'de> for Principal {
|
||||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
@@ -77,6 +102,7 @@ impl<'de> serde::Deserialize<'de> for Principal {
|
|||||||
if s == "*" {
|
if s == "*" {
|
||||||
Ok(Principal {
|
Ok(Principal {
|
||||||
aws: HashSet::from(["*".to_string()]),
|
aws: HashSet::from(["*".to_string()]),
|
||||||
|
service: HashSet::new(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(serde::de::Error::custom(format!(
|
Err(serde::de::Error::custom(format!(
|
||||||
@@ -85,24 +111,28 @@ impl<'de> serde::Deserialize<'de> for Principal {
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PrincipalFormat::AwsObject(obj) => {
|
PrincipalFormat::Object(obj) => {
|
||||||
let aws = match obj.aws {
|
let aws = obj.aws.map(PrincipalValues::into_set).unwrap_or_default();
|
||||||
AwsValues::Single(s) => HashSet::from([s]),
|
let service = obj.service.map(PrincipalValues::into_set).unwrap_or_default();
|
||||||
AwsValues::Multiple(set) => set,
|
if aws.is_empty() && service.is_empty() {
|
||||||
};
|
return Err(serde::de::Error::custom("Principal must have at least one of AWS or Service"));
|
||||||
Ok(Principal { aws })
|
}
|
||||||
|
Ok(Principal { aws, service })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Principal {
|
impl Principal {
|
||||||
pub fn is_match(&self, parincipal: &str) -> bool {
|
pub fn is_match(&self, principal: &str) -> bool {
|
||||||
for pattern in self.aws.iter() {
|
for pattern in self.aws.iter() {
|
||||||
if wildcard::is_simple_match(pattern, parincipal) {
|
if wildcard::is_simple_match(pattern, principal) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Service principals (e.g., logging.s3.amazonaws.com) allow internal
|
||||||
|
// AWS services. Treat them as non-matching for user requests — they
|
||||||
|
// only apply to service-initiated actions.
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,7 +140,7 @@ impl Principal {
|
|||||||
impl Validator for Principal {
|
impl Validator for Principal {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
fn is_valid(&self) -> Result<(), Error> {
|
fn is_valid(&self) -> Result<(), Error> {
|
||||||
if self.aws.is_empty() {
|
if self.aws.is_empty() && self.service.is_empty() {
|
||||||
return Err(Error::other("Principal is empty"));
|
return Err(Error::other("Principal is empty"));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -126,26 +156,22 @@ mod test {
|
|||||||
#[test_case(r#""*""#, true ; "wildcard_string")]
|
#[test_case(r#""*""#, true ; "wildcard_string")]
|
||||||
#[test_case(r#"{"AWS": "*"}"#, true ; "aws_object_single_string")]
|
#[test_case(r#"{"AWS": "*"}"#, true ; "aws_object_single_string")]
|
||||||
#[test_case(r#"{"AWS": ["*"]}"#, true ; "aws_object_array")]
|
#[test_case(r#"{"AWS": ["*"]}"#, true ; "aws_object_array")]
|
||||||
|
#[test_case(r#"{"Service": "logging.s3.amazonaws.com"}"#, true ; "service_single")]
|
||||||
|
#[test_case(r#"{"Service": ["logging.s3.amazonaws.com"]}"#, true ; "service_array")]
|
||||||
|
#[test_case(r#"{"AWS": "*", "Service": "logging.s3.amazonaws.com"}"#, true ; "aws_and_service")]
|
||||||
#[test_case(r#""invalid""#, false ; "invalid_string")]
|
#[test_case(r#""invalid""#, false ; "invalid_string")]
|
||||||
#[test_case(r#""""#, false ; "empty_string")]
|
#[test_case(r#""""#, false ; "empty_string")]
|
||||||
#[test_case(r#"{"Other": "*"}"#, false ; "wrong_field")]
|
|
||||||
#[test_case(r#"{}"#, false ; "empty_object")]
|
#[test_case(r#"{}"#, false ; "empty_object")]
|
||||||
fn test_principal_parsing(json: &str, should_succeed: bool) {
|
fn test_principal_parsing(json: &str, should_succeed: bool) {
|
||||||
let result = match serde_json::from_str::<Principal>(json) {
|
let result = serde_json::from_str::<Principal>(json);
|
||||||
Ok(principal) => {
|
assert_eq!(result.is_ok(), should_succeed, "input: {json}, result: {result:?}");
|
||||||
assert!(principal.aws.contains("*"));
|
|
||||||
should_succeed
|
|
||||||
}
|
|
||||||
Err(_) => !should_succeed,
|
|
||||||
};
|
|
||||||
assert!(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_principal_serialize_single_element() {
|
fn test_principal_serialize_single_element() {
|
||||||
// Single element should serialize as string (AWS format)
|
|
||||||
let principal = Principal {
|
let principal = Principal {
|
||||||
aws: HashSet::from(["*".to_string()]),
|
aws: HashSet::from(["*".to_string()]),
|
||||||
|
service: HashSet::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let json = serde_json::to_string(&principal).expect("Should serialize");
|
let json = serde_json::to_string(&principal).expect("Should serialize");
|
||||||
@@ -154,16 +180,38 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_principal_serialize_multiple_elements() {
|
fn test_principal_serialize_multiple_elements() {
|
||||||
// Multiple elements should serialize as array
|
|
||||||
let principal = Principal {
|
let principal = Principal {
|
||||||
aws: HashSet::from(["*".to_string(), "arn:aws:iam::123456789012:root".to_string()]),
|
aws: HashSet::from(["*".to_string(), "arn:aws:iam::123456789012:root".to_string()]),
|
||||||
|
service: HashSet::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let json = serde_json::to_string(&principal).expect("Should serialize");
|
let json = serde_json::to_string(&principal).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");
|
||||||
let aws_value = parsed.get("AWS").expect("Should have AWS field");
|
let aws_value = parsed.get("AWS").expect("Should have AWS field");
|
||||||
assert!(aws_value.is_array());
|
assert!(aws_value.is_array());
|
||||||
let arr = aws_value.as_array().expect("Should be array");
|
assert_eq!(aws_value.as_array().unwrap().len(), 2);
|
||||||
assert_eq!(arr.len(), 2);
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_principal_serialize_service() {
|
||||||
|
let principal = Principal {
|
||||||
|
aws: HashSet::new(),
|
||||||
|
service: HashSet::from(["logging.s3.amazonaws.com".to_string()]),
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&principal).expect("Should serialize");
|
||||||
|
assert_eq!(json, r#"{"Service":"logging.s3.amazonaws.com"}"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_principal_roundtrip_service() {
|
||||||
|
let json = r#"{"Service": "logging.s3.amazonaws.com"}"#;
|
||||||
|
let principal: Principal = serde_json::from_str(json).expect("Should parse");
|
||||||
|
assert!(principal.service.contains("logging.s3.amazonaws.com"));
|
||||||
|
assert!(principal.aws.is_empty());
|
||||||
|
|
||||||
|
let serialized = serde_json::to_string(&principal).expect("Should serialize");
|
||||||
|
let reparsed: Principal = serde_json::from_str(&serialized).expect("Should re-parse");
|
||||||
|
assert_eq!(principal, reparsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1370,11 +1370,11 @@ impl S3Access for FS {
|
|||||||
authorize_request(req, Action::S3Action(S3Action::PutBucketLifecycleAction)).await
|
authorize_request(req, Action::S3Action(S3Action::PutBucketLifecycleAction)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks whether the PutBucketLogging request has accesses to the resources.
|
async fn put_bucket_logging(&self, req: &mut S3Request<PutBucketLoggingInput>) -> S3Result<()> {
|
||||||
///
|
let req_info = ext_req_info_mut(&mut req.extensions)?;
|
||||||
/// This method returns `Ok(())` by default.
|
req_info.bucket = Some(req.input.bucket.clone());
|
||||||
async fn put_bucket_logging(&self, _req: &mut S3Request<PutBucketLoggingInput>) -> S3Result<()> {
|
|
||||||
Ok(())
|
authorize_request(req, Action::S3Action(S3Action::PutBucketLoggingAction)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks whether the PutBucketMetricsConfiguration request has accesses to the resources.
|
/// Checks whether the PutBucketMetricsConfiguration request has accesses to the resources.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use rustfs_ecstore::{
|
|||||||
bucket::tagging::decode_tags_to_map,
|
bucket::tagging::decode_tags_to_map,
|
||||||
error::{is_err_object_not_found, is_err_version_not_found},
|
error::{is_err_object_not_found, is_err_version_not_found},
|
||||||
new_object_layer_fn,
|
new_object_layer_fn,
|
||||||
store_api::{ObjectOperations, ObjectOptions},
|
store_api::{BucketOperations, BucketOptions, ObjectOperations, ObjectOptions},
|
||||||
};
|
};
|
||||||
use s3s::{S3, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, dto::*, s3_error};
|
use s3s::{S3, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, dto::*, s3_error};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -1022,6 +1022,28 @@ impl S3 for FS {
|
|||||||
usecase.execute_put_bucket_cors(req).await
|
usecase.execute_put_bucket_cors(req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_bucket_logging(&self, req: S3Request<GetBucketLoggingInput>) -> S3Result<S3Response<GetBucketLoggingOutput>> {
|
||||||
|
let Some(store) = new_object_layer_fn() else {
|
||||||
|
return Err(s3_error!(InternalError, "Not init"));
|
||||||
|
};
|
||||||
|
store
|
||||||
|
.get_bucket_info(&req.input.bucket, &BucketOptions::default())
|
||||||
|
.await
|
||||||
|
.map_err(crate::error::ApiError::from)?;
|
||||||
|
Err(s3_error!(NotImplemented, "GetBucketLogging is not implemented yet"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn put_bucket_logging(&self, req: S3Request<PutBucketLoggingInput>) -> S3Result<S3Response<PutBucketLoggingOutput>> {
|
||||||
|
let Some(store) = new_object_layer_fn() else {
|
||||||
|
return Err(s3_error!(InternalError, "Not init"));
|
||||||
|
};
|
||||||
|
store
|
||||||
|
.get_bucket_info(&req.input.bucket, &BucketOptions::default())
|
||||||
|
.await
|
||||||
|
.map_err(crate::error::ApiError::from)?;
|
||||||
|
Err(s3_error!(NotImplemented, "PutBucketLogging is not implemented yet"))
|
||||||
|
}
|
||||||
|
|
||||||
async fn put_bucket_encryption(
|
async fn put_bucket_encryption(
|
||||||
&self,
|
&self,
|
||||||
req: S3Request<PutBucketEncryptionInput>,
|
req: S3Request<PutBucketEncryptionInput>,
|
||||||
|
|||||||
Reference in New Issue
Block a user