Repository: rusoto/rusoto (language: rust)

Issue:
Support for anonymous credentials
It would be useful to have parity with other AWS sdks on supporting the notion of anonymous requests for the services that support it. Example from the jvm sdk https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/AnonymousAWSCredentials.html


Discussion / hints:
Do services other than S3 support anonymous access? This has no real bearing on your request but I'm curious how generic this functionality actually is.
Yes. 

My specific usecase is in developing an [aws xray sdk for rust](https://github.com/softprops/xray). Among rays sdks its a common attribute that they have some mechanisms for fetching sampling rules, typically using [anonymous credentials](https://github.com/aws/aws-xray-sdk-java/blob/901f2f93cfcfabf43bbbb61f74f076c9158f1bac/aws-xray-recorder-sdk-core/src/main/java/com/amazonaws/xray/strategy/sampling/XRayClient.java#L15) This is used to [poll a lambda api](https://github.com/aws/aws-xray-sdk-java/blob/f227f20b70e33fe448cd31335170cf477f7054c0/aws-xray-recorder-sdk-core/src/main/java/com/amazonaws/xray/strategy/sampling/pollers/RulePoller.java#L65-L72) periodically. 

I can't speak to how widely anonymous access is among services but ask its a part of other boto like aws sdks like [java](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/AnonymousAWSCredentials.html) so only assumed it should exist here.  



Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65fea0d5f24..feaad19f794 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,8 @@ params for s3 `list_objects` methods
 - Add SecurityHub service
 - Add Transfer service
 - Introducing `rusoto_signature`, a standalone crate for signing HTTP requests.
+- Make static credentials into a credential provider
+- Add anonymous credentials support
 
 ## [0.41.0] - 2019-10-07
 
diff --git a/rusoto/core/src/client.rs b/rusoto/core/src/client.rs
index 92b9c020513..f2036a95135 100644
--- a/rusoto/core/src/client.rs
+++ b/rusoto/core/src/client.rs
@@ -4,7 +4,7 @@ use std::time::Duration;
 use futures::{Async, Future, Poll};
 
 use crate::credential::{
-    CredentialsError, DefaultCredentialsProvider, ProvideAwsCredentials, StaticProvider,
+    Anonymous, CredentialsError, DefaultCredentialsProvider, ProvideAwsCredentials, StaticProvider,
 };
 use crate::error::RusotoError;
 use crate::future::{self, RusotoFuture};
@@ -208,7 +208,11 @@ where
                     Ok(Async::NotReady)
                 }
                 Ok(Async::Ready(credentials)) => {
-                    request.sign_with_plus(&credentials, true);
+                    if credentials.is_anonymous() {
+                        request.complement_with_plus(true);
+                    } else {
+                        request.sign_with_plus(&credentials, true);
+                    }
                     let future = self.inner.dispatcher.dispatch(request, self.timeout);
                     self.state = Some(SignAndDispatchState::Dispatching { future });
                     self.poll()
diff --git a/rusoto/credential/Cargo.toml b/rusoto/credential/Cargo.toml
index 7ad6afcafc7..59bdea19952 100644
--- a/rusoto/credential/Cargo.toml
+++ b/rusoto/credential/Cargo.toml
@@ -36,6 +36,8 @@ lazy_static = "1.4.0"
 [dev-dependencies]
 quickcheck = "0.6"
 tokio-core = "0.1"
+rusoto_core = { path = "../core" }
+rusoto_s3 = { path = "../services/s3" }
 
 [dependencies.clippy]
 optional = true
diff --git a/rusoto/credential/src/lib.rs b/rusoto/credential/src/lib.rs
index 6fc8e5537d9..af32c10b419 100644
--- a/rusoto/credential/src/lib.rs
+++ b/rusoto/credential/src/lib.rs
@@ -50,9 +50,43 @@ use futures::future::{err, Either, Shared, SharedItem};
 use futures::{Async, Future, Poll};
 use hyper::Error as HyperError;
 
+/// Representation of anonymity
+pub trait Anonymous {
+    /// Return true if a type is anonymous, false otherwise
+    fn is_anonymous(&self) -> bool;
+}
+
+impl Anonymous for AwsCredentials {
+    fn is_anonymous(&self) -> bool {
+        self.aws_access_key_id().is_empty() && self.aws_secret_access_key().is_empty()
+    }
+}
+
 /// AWS API access credentials, including access key, secret key, token (for IAM profiles),
 /// expiration timestamp, and claims from federated login.
-#[derive(Clone, Deserialize)]
+///
+/// # Anonymous example
+///
+/// Some AWS services, like [s3](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html)
+/// do not require authenticated credential identity. For these
+/// cases you can use a default set which are considered anonymous
+///
+/// ```rust,2018edition
+/// use rusoto_core::request::HttpClient;
+/// use rusoto_s3::S3Client;
+/// use rusoto_credential::{StaticProvider, AwsCredentials};
+/// # use std::error::Error;
+///
+/// # fn main() -> Result<(), Box<dyn Error>> {
+/// let s3 = S3Client::new_with(
+///     HttpClient::new()?,
+///     StaticProvider::from(AwsCredentials::default()),
+///     Default::default()
+/// );
+/// # Ok(())
+/// # }
+/// ```
+#[derive(Clone, Deserialize, Default)]
 pub struct AwsCredentials {
     #[serde(rename = "AccessKeyId")]
     key: String,
@@ -549,6 +583,16 @@ mod tests {
 
     use super::*;
 
+    #[test]
+    fn default_empty_credentials_are_considered_anonymous() {
+        assert!(AwsCredentials::default().is_anonymous())
+    }
+
+    #[test]
+    fn credentials_with_values_are_not_considered_anonymous() {
+        assert!(!AwsCredentials::new("foo", "bar", None, None).is_anonymous())
+    }
+
     #[test]
     fn providers_are_send_and_sync() {
         fn is_send_and_sync<T: Send + Sync>() {}
diff --git a/rusoto/credential/src/static_provider.rs b/rusoto/credential/src/static_provider.rs
index 5a8ef8440c0..fd88a704548 100644
--- a/rusoto/credential/src/static_provider.rs
+++ b/rusoto/credential/src/static_provider.rs
@@ -77,6 +77,12 @@ impl ProvideAwsCredentials for StaticProvider {
     }
 }
 
+impl From<AwsCredentials> for StaticProvider {
+    fn from(credentials: AwsCredentials) -> Self {
+        StaticProvider{ credentials, valid_for: None }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use futures::Future;
@@ -87,6 +93,13 @@ mod tests {
     use crate::test_utils::{is_secret_hidden_behind_asterisks, SECRET};
     use crate::ProvideAwsCredentials;
 
+    #[test]
+    fn static_provider_impl_from_for_awscredentials() {
+        let provider = StaticProvider::from(AwsCredentials::default());
+        assert_eq!(provider.get_aws_access_key_id(), "");
+        assert_eq!(*provider.is_valid_for(), None);
+    }
+
     #[test]
     fn test_static_provider_creation() {
         let result = StaticProvider::new(


Test patch:
diff --git a/integration_tests/tests/s3.rs b/integration_tests/tests/s3.rs
index 22bac838ab7..f2c5ecad716 100644
--- a/integration_tests/tests/s3.rs
+++ b/integration_tests/tests/s3.rs
@@ -19,7 +19,7 @@ use time::get_time;
 
 use futures::{Future, Stream};
 use futures_fs::FsPool;
-use rusoto_core::credential::{AwsCredentials, DefaultCredentialsProvider};
+use rusoto_core::credential::{AwsCredentials, DefaultCredentialsProvider, StaticProvider};
 use rusoto_core::{ProvideAwsCredentials, Region, RusotoError};
 use rusoto_s3::util::{PreSignedRequest, PreSignedRequestOption};
 use rusoto_s3::{
@@ -63,6 +63,21 @@ impl TestS3Client {
         }
     }
 
+    // construct an anonymous client for testing acls
+    fn create_anonymous_client(&self) -> S3Client {
+        if cfg!(feature = "disable_minio_unsupported") {
+            // Minio does not support setting acls, so to make tests pass, return a client that has
+            // the credentials of the bucket owner.
+            self.s3.clone()
+        } else {
+            S3Client::new_with(
+                rusoto_core::request::HttpClient::new().expect("Failed to creat HTTP client"),
+                StaticProvider::from(AwsCredentials::default()),
+                self.region.clone(),
+            )
+        }
+    }
+
     fn create_test_bucket(&self, name: String) {
         let create_bucket_req = CreateBucketRequest {
             bucket: name.clone(),
@@ -74,6 +89,18 @@ impl TestS3Client {
             .expect("Failed to create test bucket");
     }
 
+    fn create_test_bucket_with_acl(&self, name: String, acl: Option<String>) {
+        let create_bucket_req = CreateBucketRequest {
+            bucket: name.clone(),
+            acl,
+            ..Default::default()
+        };
+        self.s3
+            .create_bucket(create_bucket_req)
+            .sync()
+            .expect("Failed to create test bucket");
+    }
+
     fn delete_object(&self, key: String) {
         let delete_object_req = DeleteObjectRequest {
             bucket: self.bucket_name.to_owned(),
@@ -180,7 +207,7 @@ fn test_puts_gets_deletes() {
 
     let bucket_name = format!("test-bucket-{}-{}", "default".to_owned(), get_time().sec);
     let test_client = TestS3Client::new(bucket_name.clone());
-    test_client.create_test_bucket(bucket_name.clone());
+    test_client.create_test_bucket_with_acl(bucket_name.clone(), Some("public-read".to_owned()));
 
     // modify the bucket's CORS properties
     if cfg!(not(feature = "disable_minio_unsupported")) {
@@ -190,24 +217,45 @@ fn test_puts_gets_deletes() {
 
     // file used for testing puts/gets
     let filename = format!("test_file_{}", get_time().sec);
+    let filename2 = format!("test_file_2_{}", get_time().sec);
 
     // test failure responses on empty bucket
     test_get_object_no_such_object(&test_client.s3, &test_client.bucket_name, &filename);
 
     // PUT an object via buffer (no_credentials is an arbitrary choice)
-    test_put_object_with_filename(
+    test_put_object_with_filename_and_acl(
         &test_client.s3,
         &test_client.bucket_name,
         &filename,
         &"tests/sample-data/no_credentials",
+        Some("public-read".to_owned()),
+    );
+
+    // PUT a second copy of the object with tighter acls
+    test_put_object_with_filename(
+        &test_client.s3,
+        &test_client.bucket_name,
+        &filename2,
+        &"tests/sample-data/no_credentials",
     );
 
+    // create an anonymous reader to test the acls
+    let ro_s3client = test_client.create_anonymous_client();
+
     // HEAD the object that was PUT
-    test_head_object(&test_client.s3, &test_client.bucket_name, &filename);
+    test_head_object(&ro_s3client, &test_client.bucket_name, &filename);
+
+    if cfg!(not(feature = "disable_minio_unsupported")) {
+        // HEAD the object that cannot be read, should return 403
+        assert!(try_head_object(&ro_s3client, &test_client.bucket_name, &filename2).is_err());
+    }
+
+    // ... but it can be as the original owner
+    test_head_object(&test_client.s3, &test_client.bucket_name, &filename2);
 
     // GET the object
-    test_get_object(&test_client.s3, &test_client.bucket_name, &filename);
-    test_get_object_range(&test_client.s3, &test_client.bucket_name, &filename);
+    test_get_object(&ro_s3client, &test_client.bucket_name, &filename);
+    test_get_object_range(&ro_s3client, &test_client.bucket_name, &filename);
 
     // add two objects to test the listing by paging
     for i in 1..3 {
@@ -215,12 +263,12 @@ fn test_puts_gets_deletes() {
     }
 
     // list items with paging using list object API v1
-    list_items_in_bucket_paged_v1(&test_client.s3, &test_client.bucket_name);
+    list_items_in_bucket_paged_v1(&ro_s3client, &test_client.bucket_name);
 
     // list items with paging using list object API v2
     if cfg!(not(feature = "disable_ceph_unsupported")) {
         // Ceph support: this test depends on the list object v2 API which is not implemented by Ceph
-        list_items_in_bucket_paged_v2(&test_client.s3, &test_client.bucket_name);
+        list_items_in_bucket_paged_v2(&ro_s3client, &test_client.bucket_name);
     }
 
     // copy the object to change its settings
@@ -737,6 +785,31 @@ fn test_put_object_with_filename(
     }
 }
 
+fn test_put_object_with_filename_and_acl(
+    client: &S3Client,
+    bucket: &str,
+    dest_filename: &str,
+    local_filename: &str,
+    acl: Option<String>,
+) {
+    let mut f = File::open(local_filename).unwrap();
+    let mut contents: Vec<u8> = Vec::new();
+    match f.read_to_end(&mut contents) {
+        Err(why) => panic!("Error opening file to send to S3: {}", why),
+        Ok(_) => {
+            let req = PutObjectRequest {
+                bucket: bucket.to_owned(),
+                key: dest_filename.to_owned(),
+                body: Some(contents.into()),
+                acl,
+                ..Default::default()
+            };
+            let result = client.put_object(req).sync().expect("Couldn't PUT object");
+            println!("{:#?}", result);
+        }
+    }
+}
+
 fn test_put_object_stream_with_filename(
     client: &S3Client,
     bucket: &str,
@@ -757,17 +830,22 @@ fn test_put_object_stream_with_filename(
     println!("{:#?}", result);
 }
 
-fn test_head_object(client: &S3Client, bucket: &str, filename: &str) {
+fn try_head_object(
+    client: &S3Client,
+    bucket: &str,
+    filename: &str,
+) -> Result<rusoto_s3::HeadObjectOutput, rusoto_core::RusotoError<rusoto_s3::HeadObjectError>> {
     let head_req = HeadObjectRequest {
         bucket: bucket.to_owned(),
         key: filename.to_owned(),
         ..Default::default()
     };
 
-    let result = client
-        .head_object(head_req)
-        .sync()
-        .expect("Couldn't HEAD object");
+    client.head_object(head_req).sync()
+}
+
+fn test_head_object(client: &S3Client, bucket: &str, filename: &str) {
+    let result = try_head_object(client, bucket, filename).expect("Couldn't HEAD object");
     println!("{:#?}", result);
 }