feat(auth,policy): add AWS SigV4 authentication and S3 bucket policy support
This commit is contained in:
172
rust/src/action.rs
Normal file
172
rust/src/action.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user