use super::*;
const MAX_BUCKET_METRICS_CONFIGURATIONS: usize = 1_000;
const MAX_BUCKET_METRICS_PAGE_SIZE: usize = 100;
impl BucketWarden {
pub fn put_bucket_metrics_configuration(
&mut self,
principal: &str,
bucket: &str,
mut configuration: MetricsConfiguration,
) -> Result<MetricsConfiguration, RuntimeError> {
self.authorize(principal, S3Action::PutBucketMetricsConfiguration, bucket)?;
validate_metrics_configuration(&configuration)?;
let bucket_state = self.require_bucket_mut(bucket)?;
if !bucket_state
.metrics_configurations
.contains_key(&configuration.id)
&& bucket_state.metrics_configurations.len() >= MAX_BUCKET_METRICS_CONFIGURATIONS
{
return Err(RuntimeError::TooManyConfigurations(bucket.to_string()));
}
configuration.bucket = bucket.to_string();
bucket_state
.metrics_configurations
.insert(configuration.id.clone(), configuration.clone());
self.audit_allowed(
principal,
S3Action::PutBucketMetricsConfiguration,
bucket,
Some(configuration.id.clone()),
);
Ok(configuration)
}
pub fn get_bucket_metrics_configuration(
&mut self,
principal: &str,
bucket: &str,
id: &str,
) -> Result<MetricsConfiguration, RuntimeError> {
self.authorize(principal, S3Action::GetBucketMetricsConfiguration, bucket)?;
let configuration = self
.require_bucket(bucket)?
.metrics_configurations
.get(id)
.cloned()
.ok_or_else(|| RuntimeError::NoSuchMetricsConfiguration {
bucket: bucket.to_string(),
id: id.to_string(),
})?;
self.audit_allowed(
principal,
S3Action::GetBucketMetricsConfiguration,
bucket,
Some(id.to_string()),
);
Ok(configuration)
}
pub fn delete_bucket_metrics_configuration(
&mut self,
principal: &str,
bucket: &str,
id: &str,
) -> Result<(), RuntimeError> {
self.authorize(
principal,
S3Action::DeleteBucketMetricsConfiguration,
bucket,
)?;
self.require_bucket_mut(bucket)?
.metrics_configurations
.remove(id);
self.audit_allowed(
principal,
S3Action::DeleteBucketMetricsConfiguration,
bucket,
Some(id.to_string()),
);
Ok(())
}
pub fn list_bucket_metrics_configurations(
&mut self,
principal: &str,
bucket: &str,
continuation_token: Option<&str>,
) -> Result<ListMetricsConfigurationsResult, RuntimeError> {
self.authorize(principal, S3Action::ListBucketMetricsConfigurations, bucket)?;
let state = self.require_bucket(bucket)?;
let all = state
.metrics_configurations
.values()
.cloned()
.collect::<Vec<_>>();
let start_index = continuation_token
.map(|token| all.partition_point(|configuration| configuration.id.as_str() <= token))
.unwrap_or(0);
let page = all
.iter()
.skip(start_index)
.take(MAX_BUCKET_METRICS_PAGE_SIZE)
.cloned()
.collect::<Vec<_>>();
let next_index = start_index + page.len();
let next_continuation_token = (next_index < all.len())
.then(|| page.last().map(|configuration| configuration.id.clone()))
.flatten();
let result = ListMetricsConfigurationsResult {
continuation_token: continuation_token.map(str::to_string),
is_truncated: next_index < all.len(),
metrics_configurations: page,
next_continuation_token,
};
self.audit_allowed(
principal,
S3Action::ListBucketMetricsConfigurations,
bucket,
Some(result.metrics_configurations.len().to_string()),
);
Ok(result)
}
}
fn validate_metrics_configuration(
configuration: &MetricsConfiguration,
) -> Result<(), RuntimeError> {
validate_metrics_configuration_id(&configuration.id)?;
if let Some(filter) = configuration.filter.as_ref() {
validate_metrics_filter(filter)?;
}
Ok(())
}
fn validate_metrics_configuration_id(id: &str) -> Result<(), RuntimeError> {
let trimmed = id.trim();
if trimmed.is_empty() || trimmed.len() > 64 {
return Err(RuntimeError::InvalidMetricsConfiguration(
"Metrics configuration Id must be 1-64 characters".to_string(),
));
}
if !trimmed
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
{
return Err(RuntimeError::InvalidMetricsConfiguration(
"Metrics configuration Id contains unsupported characters".to_string(),
));
}
Ok(())
}
fn validate_metrics_filter(filter: &MetricsFilter) -> Result<(), RuntimeError> {
let root_count = usize::from(filter.access_point_arn.is_some())
+ usize::from(filter.and.is_some())
+ usize::from(filter.prefix.is_some())
+ usize::from(filter.tag.is_some());
if root_count == 0 {
return Err(RuntimeError::InvalidMetricsConfiguration(
"Metrics filter must include at least one selector".to_string(),
));
}
if root_count > 1 {
return Err(RuntimeError::InvalidMetricsConfiguration(
"Metrics filter must contain exactly one top-level selector".to_string(),
));
}
if let Some(tag) = filter.tag.as_ref() {
validate_metrics_tag(tag)?;
}
if let Some(and) = filter.and.as_ref() {
let child_count = usize::from(and.access_point_arn.is_some())
+ usize::from(and.prefix.is_some())
+ and.tags.len();
if child_count < 2 {
return Err(RuntimeError::InvalidMetricsConfiguration(
"Metrics And filter must combine at least two predicates".to_string(),
));
}
for tag in &and.tags {
validate_metrics_tag(tag)?;
}
}
Ok(())
}
fn validate_metrics_tag(tag: &MetricsTag) -> Result<(), RuntimeError> {
if tag.key.trim().is_empty() {
return Err(RuntimeError::InvalidMetricsConfiguration(
"Metrics tag key must not be empty".to_string(),
));
}
Ok(())
}