173 lines
6.0 KiB
Rust
173 lines
6.0 KiB
Rust
use hyper::body::Incoming;
|
|
use hyper::{Method, Request};
|
|
use std::collections::HashMap;
|
|
|
|
/// S3 actions that map to IAM permission strings.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum S3Action {
|
|
ListAllMyBuckets,
|
|
CreateBucket,
|
|
DeleteBucket,
|
|
HeadBucket,
|
|
ListBucket,
|
|
GetObject,
|
|
HeadObject,
|
|
PutObject,
|
|
DeleteObject,
|
|
CopyObject,
|
|
ListBucketMultipartUploads,
|
|
AbortMultipartUpload,
|
|
InitiateMultipartUpload,
|
|
UploadPart,
|
|
CompleteMultipartUpload,
|
|
GetBucketPolicy,
|
|
PutBucketPolicy,
|
|
DeleteBucketPolicy,
|
|
}
|
|
|
|
impl S3Action {
|
|
/// Return the IAM-style action string (e.g. "s3:GetObject").
|
|
pub fn iam_action(&self) -> &'static str {
|
|
match self {
|
|
S3Action::ListAllMyBuckets => "s3:ListAllMyBuckets",
|
|
S3Action::CreateBucket => "s3:CreateBucket",
|
|
S3Action::DeleteBucket => "s3:DeleteBucket",
|
|
S3Action::HeadBucket => "s3:ListBucket",
|
|
S3Action::ListBucket => "s3:ListBucket",
|
|
S3Action::GetObject => "s3:GetObject",
|
|
S3Action::HeadObject => "s3:GetObject",
|
|
S3Action::PutObject => "s3:PutObject",
|
|
S3Action::DeleteObject => "s3:DeleteObject",
|
|
S3Action::CopyObject => "s3:PutObject",
|
|
S3Action::ListBucketMultipartUploads => "s3:ListBucketMultipartUploads",
|
|
S3Action::AbortMultipartUpload => "s3:AbortMultipartUpload",
|
|
S3Action::InitiateMultipartUpload => "s3:PutObject",
|
|
S3Action::UploadPart => "s3:PutObject",
|
|
S3Action::CompleteMultipartUpload => "s3:PutObject",
|
|
S3Action::GetBucketPolicy => "s3:GetBucketPolicy",
|
|
S3Action::PutBucketPolicy => "s3:PutBucketPolicy",
|
|
S3Action::DeleteBucketPolicy => "s3:DeleteBucketPolicy",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Context extracted from a request, used for policy evaluation.
|
|
#[derive(Debug, Clone)]
|
|
pub struct RequestContext {
|
|
pub action: S3Action,
|
|
pub bucket: Option<String>,
|
|
pub key: Option<String>,
|
|
}
|
|
|
|
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 S3 action from an incoming HTTP request.
|
|
pub fn resolve_action(req: &Request<Incoming>) -> 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: S3Action::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, _) => S3Action::GetBucketPolicy,
|
|
(&Method::PUT, true, _) => S3Action::PutBucketPolicy,
|
|
(&Method::DELETE, true, _) => S3Action::DeleteBucketPolicy,
|
|
(&Method::GET, _, true) => S3Action::ListBucketMultipartUploads,
|
|
(&Method::GET, _, _) => S3Action::ListBucket,
|
|
(&Method::PUT, _, _) => S3Action::CreateBucket,
|
|
(&Method::DELETE, _, _) => S3Action::DeleteBucket,
|
|
(&Method::HEAD, _, _) => S3Action::HeadBucket,
|
|
_ => S3Action::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 => S3Action::UploadPart,
|
|
&Method::PUT if has_copy_source => S3Action::CopyObject,
|
|
&Method::PUT => S3Action::PutObject,
|
|
&Method::GET => S3Action::GetObject,
|
|
&Method::HEAD => S3Action::HeadObject,
|
|
&Method::DELETE if has_upload_id => S3Action::AbortMultipartUpload,
|
|
&Method::DELETE => S3Action::DeleteObject,
|
|
&Method::POST if has_uploads => S3Action::InitiateMultipartUpload,
|
|
&Method::POST if has_upload_id => S3Action::CompleteMultipartUpload,
|
|
_ => S3Action::GetObject,
|
|
};
|
|
|
|
RequestContext {
|
|
action,
|
|
bucket: Some(bucket),
|
|
key: Some(key),
|
|
}
|
|
}
|
|
_ => RequestContext {
|
|
action: S3Action::ListAllMyBuckets,
|
|
bucket: None,
|
|
key: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
fn parse_query_simple(query_string: &str) -> HashMap<String, String> {
|
|
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()
|
|
}
|