use hyper::body::Incoming; use hyper::{Method, Request}; use std::collections::HashMap; /// Storage actions that map to IAM permission strings. #[derive(Debug, Clone, PartialEq, Eq)] pub enum StorageAction { ListAllMyBuckets, CreateBucket, DeleteBucket, HeadBucket, ListBucket, GetObject, HeadObject, PutObject, DeleteObject, CopyObject, ListBucketMultipartUploads, AbortMultipartUpload, InitiateMultipartUpload, UploadPart, CompleteMultipartUpload, GetBucketPolicy, PutBucketPolicy, DeleteBucketPolicy, } impl StorageAction { /// Return the IAM-style action string (e.g. "s3:GetObject"). pub fn iam_action(&self) -> &'static str { match self { StorageAction::ListAllMyBuckets => "s3:ListAllMyBuckets", StorageAction::CreateBucket => "s3:CreateBucket", StorageAction::DeleteBucket => "s3:DeleteBucket", StorageAction::HeadBucket => "s3:ListBucket", StorageAction::ListBucket => "s3:ListBucket", StorageAction::GetObject => "s3:GetObject", StorageAction::HeadObject => "s3:GetObject", StorageAction::PutObject => "s3:PutObject", StorageAction::DeleteObject => "s3:DeleteObject", StorageAction::CopyObject => "s3:PutObject", StorageAction::ListBucketMultipartUploads => "s3:ListBucketMultipartUploads", StorageAction::AbortMultipartUpload => "s3:AbortMultipartUpload", StorageAction::InitiateMultipartUpload => "s3:PutObject", StorageAction::UploadPart => "s3:PutObject", StorageAction::CompleteMultipartUpload => "s3:PutObject", StorageAction::GetBucketPolicy => "s3:GetBucketPolicy", StorageAction::PutBucketPolicy => "s3:PutBucketPolicy", StorageAction::DeleteBucketPolicy => "s3:DeleteBucketPolicy", } } } /// Context extracted from a request, used for policy evaluation. #[derive(Debug, Clone)] pub struct RequestContext { pub action: StorageAction, pub bucket: Option, pub key: Option, } impl RequestContext { /// Build the ARN for this request's resource. pub fn resource_arn(&self) -> String { match (&self.bucket, &self.key) { (Some(bucket), Some(key)) => format!("arn:aws:s3:::{}/{}", bucket, key), (Some(bucket), None) => format!("arn:aws:s3:::{}", bucket), _ => "arn:aws:s3:::*".to_string(), } } } /// Resolve the storage action from an incoming HTTP request. pub fn resolve_action(req: &Request) -> RequestContext { let method = req.method().clone(); let path = req.uri().path().to_string(); let query_string = req.uri().query().unwrap_or("").to_string(); let query = parse_query_simple(&query_string); let segments: Vec<&str> = path .trim_start_matches('/') .splitn(2, '/') .filter(|s| !s.is_empty()) .collect(); match segments.len() { 0 => { // Root: GET / -> ListBuckets RequestContext { action: StorageAction::ListAllMyBuckets, bucket: None, key: None, } } 1 => { let bucket = percent_decode(segments[0]); let has_policy = query.contains_key("policy"); let has_uploads = query.contains_key("uploads"); let action = match (&method, has_policy, has_uploads) { (&Method::GET, true, _) => StorageAction::GetBucketPolicy, (&Method::PUT, true, _) => StorageAction::PutBucketPolicy, (&Method::DELETE, true, _) => StorageAction::DeleteBucketPolicy, (&Method::GET, _, true) => StorageAction::ListBucketMultipartUploads, (&Method::GET, _, _) => StorageAction::ListBucket, (&Method::PUT, _, _) => StorageAction::CreateBucket, (&Method::DELETE, _, _) => StorageAction::DeleteBucket, (&Method::HEAD, _, _) => StorageAction::HeadBucket, _ => StorageAction::ListBucket, }; RequestContext { action, bucket: Some(bucket), key: None, } } 2 => { let bucket = percent_decode(segments[0]); let key = percent_decode(segments[1]); let has_copy_source = req.headers().contains_key("x-amz-copy-source"); let has_part_number = query.contains_key("partNumber"); let has_upload_id = query.contains_key("uploadId"); let has_uploads = query.contains_key("uploads"); let action = match &method { &Method::PUT if has_part_number && has_upload_id => StorageAction::UploadPart, &Method::PUT if has_copy_source => StorageAction::CopyObject, &Method::PUT => StorageAction::PutObject, &Method::GET => StorageAction::GetObject, &Method::HEAD => StorageAction::HeadObject, &Method::DELETE if has_upload_id => StorageAction::AbortMultipartUpload, &Method::DELETE => StorageAction::DeleteObject, &Method::POST if has_uploads => StorageAction::InitiateMultipartUpload, &Method::POST if has_upload_id => StorageAction::CompleteMultipartUpload, _ => StorageAction::GetObject, }; RequestContext { action, bucket: Some(bucket), key: Some(key), } } _ => RequestContext { action: StorageAction::ListAllMyBuckets, bucket: None, key: None, }, } } fn parse_query_simple(query_string: &str) -> HashMap { let mut map = HashMap::new(); if query_string.is_empty() { return map; } for pair in query_string.split('&') { let mut parts = pair.splitn(2, '='); let key = parts.next().unwrap_or(""); let value = parts.next().unwrap_or(""); map.insert(key.to_string(), value.to_string()); } map } fn percent_decode(s: &str) -> String { percent_encoding::percent_decode_str(s) .decode_utf8_lossy() .to_string() }