use std::collections::BTreeMap;
use std::sync::Arc;
use s3s::access::{S3Access, S3AccessContext};
use s3s::path::S3Path;
use s3s::{s3_error, S3Result};
use crate::auth_provider::CredentialStore;
use crate::iam::{AuthorizationContext, IamState, PrincipalContext};
#[derive(Clone)]
pub struct GatewayAccessControl {
pub credentials: Arc<CredentialStore>,
pub iam: Arc<IamState>,
}
#[async_trait::async_trait]
impl S3Access for GatewayAccessControl {
async fn check(&self, cx: &mut S3AccessContext<'_>) -> S3Result<()> {
let creds = cx
.credentials()
.ok_or_else(|| s3_error!(AccessDenied, "Signature is required"))?;
let Some(record) = self.credentials.get(&creds.access_key) else {
return Err(s3_error!(AccessDenied));
};
if let Some(expected_token) = record.session_token.as_ref() {
let provided = cx
.headers()
.get("x-amz-security-token")
.and_then(|value| value.to_str().ok());
if provided != Some(expected_token.as_str()) {
return Err(s3_error!(AccessDenied));
}
}
let action = operation_to_action(cx.s3_op().name());
let resource = path_to_resource(cx.s3_path());
let mut attrs = BTreeMap::new();
attrs.insert("http:method".to_string(), cx.method().as_str().to_string());
attrs.insert(
"sts:authentication".to_string(),
if record.session_token.is_some() {
"true".to_string()
} else {
"false".to_string()
},
);
let allowed = self.iam.evaluate(&AuthorizationContext {
principal: PrincipalContext {
principal: record.principal,
access_key: creds.access_key.clone(),
},
action,
resource,
attributes: attrs,
});
if allowed {
Ok(())
} else {
Err(s3_error!(AccessDenied))
}
}
}
pub fn operation_to_action(operation_name: &str) -> String {
match operation_name {
"ListBuckets" => "s3:ListAllMyBuckets".to_string(),
"ListMultipartUploads" => "s3:ListBucketMultipartUploads".to_string(),
"ListParts" => "s3:ListMultipartUploadParts".to_string(),
"CreateMultipartUpload" | "UploadPart" | "CompleteMultipartUpload" => {
"s3:PutObject".to_string()
}
"HeadObject" => "s3:ListBucket".to_string(),
other => format!("s3:{other}"),
}
}
pub fn path_to_resource(path: &S3Path) -> String {
match path {
S3Path::Root => "arn:aws:s3:::*".to_string(),
S3Path::Bucket { bucket } => format!("arn:aws:s3:::{bucket}"),
S3Path::Object { bucket, key } => format!("arn:aws:s3:::{bucket}/{key}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn operation_to_action_mapping() {
assert_eq!(operation_to_action("PutObject"), "s3:PutObject");
}
#[test]
fn path_to_resource_mapping() {
assert_eq!(
path_to_resource(&S3Path::object("bucket", "key")),
"arn:aws:s3:::bucket/key"
);
}
}