use super::*;
const MAX_BUCKET_INVENTORY_CONFIGURATIONS: usize = 1_000;
const MAX_BUCKET_INVENTORY_PAGE_SIZE: usize = 100;
impl BucketWarden {
pub fn put_bucket_inventory_configuration(
&mut self,
principal: &str,
bucket: &str,
mut configuration: InventoryConfiguration,
) -> Result<InventoryConfiguration, RuntimeError> {
self.authorize(principal, S3Action::PutBucketInventoryConfiguration, bucket)?;
validate_inventory_configuration(&configuration)?;
let bucket_state = self.require_bucket_mut(bucket)?;
if !bucket_state
.inventory_configurations
.contains_key(&configuration.id)
&& bucket_state.inventory_configurations.len() >= MAX_BUCKET_INVENTORY_CONFIGURATIONS
{
return Err(RuntimeError::TooManyConfigurations(bucket.to_string()));
}
configuration.bucket = bucket.to_string();
bucket_state
.inventory_configurations
.insert(configuration.id.clone(), configuration.clone());
self.audit_allowed(
principal,
S3Action::PutBucketInventoryConfiguration,
bucket,
Some(configuration.id.clone()),
);
Ok(configuration)
}
pub fn get_bucket_inventory_configuration(
&mut self,
principal: &str,
bucket: &str,
id: &str,
) -> Result<InventoryConfiguration, RuntimeError> {
self.authorize(principal, S3Action::GetBucketInventoryConfiguration, bucket)?;
let configuration = self
.require_bucket(bucket)?
.inventory_configurations
.get(id)
.cloned()
.ok_or_else(|| RuntimeError::NoSuchInventoryConfiguration {
bucket: bucket.to_string(),
id: id.to_string(),
})?;
self.audit_allowed(
principal,
S3Action::GetBucketInventoryConfiguration,
bucket,
Some(id.to_string()),
);
Ok(configuration)
}
pub fn delete_bucket_inventory_configuration(
&mut self,
principal: &str,
bucket: &str,
id: &str,
) -> Result<(), RuntimeError> {
self.authorize(
principal,
S3Action::DeleteBucketInventoryConfiguration,
bucket,
)?;
self.require_bucket_mut(bucket)?
.inventory_configurations
.remove(id);
self.audit_allowed(
principal,
S3Action::DeleteBucketInventoryConfiguration,
bucket,
Some(id.to_string()),
);
Ok(())
}
pub fn list_bucket_inventory_configurations(
&mut self,
principal: &str,
bucket: &str,
continuation_token: Option<&str>,
) -> Result<ListInventoryConfigurationsResult, RuntimeError> {
self.authorize(
principal,
S3Action::ListBucketInventoryConfigurations,
bucket,
)?;
let state = self.require_bucket(bucket)?;
let all = state
.inventory_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_INVENTORY_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 = ListInventoryConfigurationsResult {
continuation_token: continuation_token.map(str::to_string),
inventory_configurations: page,
is_truncated: next_index < all.len(),
next_continuation_token,
};
self.audit_allowed(
principal,
S3Action::ListBucketInventoryConfigurations,
bucket,
Some(result.inventory_configurations.len().to_string()),
);
Ok(result)
}
}
fn validate_inventory_configuration(
configuration: &InventoryConfiguration,
) -> Result<(), RuntimeError> {
validate_inventory_configuration_id(&configuration.id)?;
match configuration.included_object_versions.as_str() {
"All" | "Current" => {}
other => {
return Err(RuntimeError::InvalidInventoryConfiguration(format!(
"IncludedObjectVersions must be All or Current, got {other}"
)));
}
}
match configuration.schedule.frequency.as_str() {
"Daily" | "Weekly" => {}
other => {
return Err(RuntimeError::InvalidInventoryConfiguration(format!(
"Schedule frequency must be Daily or Weekly, got {other}"
)));
}
}
let destination = &configuration.destination.s3_bucket_destination;
if !destination.bucket_arn.starts_with("arn:aws:s3:::") {
return Err(RuntimeError::InvalidInventoryConfiguration(
"Inventory destination Bucket must be an S3 bucket ARN".to_string(),
));
}
if destination.format != "CSV" {
return Err(RuntimeError::InvalidInventoryConfiguration(
"Inventory destination Format must be CSV".to_string(),
));
}
if let Some(account_id) = destination.account_id.as_ref() {
if account_id.len() != 12 || !account_id.chars().all(|ch| ch.is_ascii_digit()) {
return Err(RuntimeError::InvalidInventoryConfiguration(
"Inventory destination AccountId must be a 12-digit AWS account ID".to_string(),
));
}
}
if let Some(filter) = configuration.filter.as_ref() {
if filter
.prefix
.as_ref()
.is_some_and(|prefix| prefix.trim().is_empty())
{
return Err(RuntimeError::InvalidInventoryConfiguration(
"Inventory filter Prefix must not be empty".to_string(),
));
}
}
for field in &configuration.optional_fields {
if field.trim().is_empty() {
return Err(RuntimeError::InvalidInventoryConfiguration(
"Inventory optional fields must not be empty".to_string(),
));
}
}
Ok(())
}
fn validate_inventory_configuration_id(id: &str) -> Result<(), RuntimeError> {
let trimmed = id.trim();
if trimmed.is_empty() || trimmed.len() > 64 {
return Err(RuntimeError::InvalidInventoryConfiguration(
"Inventory configuration Id must be 1-64 characters".to_string(),
));
}
if !trimmed
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
{
return Err(RuntimeError::InvalidInventoryConfiguration(
"Inventory configuration Id contains unsupported characters".to_string(),
));
}
Ok(())
}