diff --git a/README.md b/README.md index e0d126e2..c34e5fd5 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,18 @@ the project's root you can specify the directory like so: If you omit `s3_id` from your `s3_website.yml`, S3_website will fall back to reading from the [default AWS SDK locations](http://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html). For instance, if you've used `aws configure` to set up credentials in `~/.aws/credentials`, S3_website can use these. +### Using an AWS profile or a profile that assumes a role + +If you omit `s3_id`, `s3_secret`, and `session_token` you can specify an AWS credentials profile to use via the `profile` configuration variable, eg: + + profile: name_of_aws_profile + +In addition, if you want this profile to assume a role before executing against S3, use the `profile_assume_role_arn` variable, eg: + + profile_assume_role_arn: arn_of_role_to_assume + +(Note: you have to use a regular profile with an ID and SECRET and specify the role ARN via a variable like this instead of a profile that specifies a `role_arn` as documented [here](http://docs.aws.amazon.com/cli/latest/userguide/cli-roles.html) since it does not look like the Java SDK supports that format, yet...) + ### Using environment variables You can use ERB in your `s3_website.yml` file which incorporates environment variables: diff --git a/build.sbt b/build.sbt index 603f2a62..17a54bd1 100644 --- a/build.sbt +++ b/build.sbt @@ -18,7 +18,7 @@ libraryDependencies += "org.yaml" % "snakeyaml" % "1.13" libraryDependencies += "org.jruby" % "jruby" % "1.7.11" -libraryDependencies += "com.amazonaws" % "aws-java-sdk" % "1.11.32" +libraryDependencies += "com.amazonaws" % "aws-java-sdk" % "1.11.172" libraryDependencies += "log4j" % "log4j" % "1.2.17" diff --git a/src/main/scala/s3/website/model/Config.scala b/src/main/scala/s3/website/model/Config.scala index 0c3af2c8..df0989df 100644 --- a/src/main/scala/s3/website/model/Config.scala +++ b/src/main/scala/s3/website/model/Config.scala @@ -8,12 +8,15 @@ import scala.util.{Failure, Try} import scala.collection.JavaConversions._ import s3.website.Ruby.rubyRuntime import s3.website._ -import com.amazonaws.auth.{AWSCredentialsProvider, BasicAWSCredentials, BasicSessionCredentials, AWSStaticCredentialsProvider, DefaultAWSCredentialsProviderChain} +import com.amazonaws.auth.{AWSCredentialsProvider, BasicAWSCredentials, BasicSessionCredentials, AWSStaticCredentialsProvider, DefaultAWSCredentialsProviderChain, STSAssumeRoleSessionCredentialsProvider} +import com.amazonaws.auth.profile.ProfileCredentialsProvider case class Config( s3_id: Option[String], // If undefined, use IAM Roles (http://docs.aws.amazon.com/AWSSdkDocsJava/latest/DeveloperGuide/java-dg-roles.html) s3_secret: Option[String], // If undefined, use IAM Roles (http://docs.aws.amazon.com/AWSSdkDocsJava/latest/DeveloperGuide/java-dg-roles.html) session_token: Option[String], // If defined, the AWS Security Token Service session token (http://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html) + profile: Option[String], // If defined, the AWS profile to use for credentials + profile_assume_role_arn: Option[String], // If defined, the ARN of the role to assume s3_bucket: String, s3_endpoint: S3Endpoint, site: Option[String], @@ -37,20 +40,30 @@ case class Config( object Config { def awsCredentials(config: Config): AWSCredentialsProvider = { - val credentialsFromConfigFile: Option[AWSStaticCredentialsProvider] = - if (config.s3_id.isEmpty) { - None - } else if (config.session_token.isEmpty) { + val credentialsFromConfigFile: Option[AWSCredentialsProvider] = + if (config.s3_id.nonEmpty && config.s3_secret.nonEmpty && config.session_token.nonEmpty) { for { s3_id <- config.s3_id s3_secret <- config.s3_secret - } yield new AWSStaticCredentialsProvider(new BasicAWSCredentials(s3_id, s3_secret)) - } else { + session_token <- config.session_token + } yield new AWSStaticCredentialsProvider(new BasicSessionCredentials(s3_id, s3_secret, session_token)) + } else if (config.s3_id.nonEmpty && config.s3_secret.nonEmpty) { for { s3_id <- config.s3_id s3_secret <- config.s3_secret - session_token <- config.session_token - } yield new AWSStaticCredentialsProvider(new BasicSessionCredentials(s3_id, s3_secret, session_token)) + } yield new AWSStaticCredentialsProvider(new BasicAWSCredentials(s3_id, s3_secret)) + } else if (config.profile_assume_role_arn.nonEmpty) { + for { + profile <- config.profile + profile_assume_role_arn <- config.profile_assume_role_arn + } yield new STSAssumeRoleSessionCredentialsProvider.Builder(profile_assume_role_arn, "s3_website_assume_role_session") + .withLongLivedCredentialsProvider(new ProfileCredentialsProvider(profile)).build() + } else if (config.profile.nonEmpty) { + for { + profile <- config.profile + } yield new ProfileCredentialsProvider(profile) + } else { + None } credentialsFromConfigFile getOrElse new DefaultAWSCredentialsProviderChain } @@ -235,4 +248,4 @@ object Config { case class S3_website_yml(file: File) { override def toString = file.getPath } -} \ No newline at end of file +} diff --git a/src/main/scala/s3/website/model/Site.scala b/src/main/scala/s3/website/model/Site.scala index f91ea6db..07b06fcc 100644 --- a/src/main/scala/s3/website/model/Site.scala +++ b/src/main/scala/s3/website/model/Site.scala @@ -36,6 +36,8 @@ object Site { s3_id <- loadOptionalString("s3_id").right s3_secret <- loadOptionalString("s3_secret").right session_token <- loadOptionalString("session_token").right + profile <- loadOptionalString("profile").right + profile_assume_role_arn <- loadOptionalString("profile_assume_role_arn").right s3_bucket <- loadRequiredString("s3_bucket").right s3_endpoint <- loadEndpoint.right site <- loadOptionalString("site").right @@ -67,6 +69,8 @@ object Site { s3_id, s3_secret, session_token, + profile, + profile_assume_role_arn, s3_bucket, s3_endpoint getOrElse S3Endpoint.defaultEndpoint, site, @@ -152,4 +156,4 @@ object Site { Right(None) } } -} \ No newline at end of file +} diff --git a/src/test/scala/s3/website/ConfigSpec.scala b/src/test/scala/s3/website/ConfigSpec.scala index a057c61a..6b646604 100644 --- a/src/test/scala/s3/website/ConfigSpec.scala +++ b/src/test/scala/s3/website/ConfigSpec.scala @@ -6,11 +6,41 @@ import s3.website.model.{Config, S3Endpoint} class ConfigSpec extends Specification { "Config#awsCredentials" should { + s"return ${classOf[BasicSessionCredentials]} when s3_id, s3_secret and session_token are defined in the config" in { + Config.awsCredentials(Config( + s3_id = Some("test"), + s3_secret = Some("secret"), + session_token = Some("Token"), + profile = None, + profile_assume_role_arn = None, + s3_bucket = "foo", + s3_endpoint = S3Endpoint.defaultEndpoint, + site = None, + max_age = None, + cache_control = None, + gzip = None, + gzip_zopfli = None, + s3_key_prefix = None, + ignore_on_server = None, + exclude_from_upload = None, + s3_reduced_redundancy = None, + cloudfront_distribution_id = None, + cloudfront_invalidate_root = None, + content_type = None, + redirects = None, + concurrency_level = 1, + cloudfront_wildcard_invalidation = None, + treat_zero_length_objects_as_redirects = None + )).getCredentials must beAnInstanceOf[BasicSessionCredentials] + } + s"return ${classOf[BasicAWSCredentials]} when s3_id and s3_secret are defined in the config" in { Config.awsCredentials(Config( s3_id = Some("test"), s3_secret = Some("secret"), session_token = None, + profile = None, + profile_assume_role_arn = None, s3_bucket = "foo", s3_endpoint = S3Endpoint.defaultEndpoint, site = None, @@ -32,11 +62,13 @@ class ConfigSpec extends Specification { )).getCredentials must beAnInstanceOf[BasicAWSCredentials] } - s"return ${classOf[BasicSessionCredentials]} when s3_id, s3_secret and session_token are defined in the config" in { + s"return ${classOf[STSAssumeRoleSessionCredentialsProvider]} when profile and profile_assume_role_arn are defined in the config" in { Config.awsCredentials(Config( - s3_id = Some("test"), - s3_secret = Some("secret"), - session_token = Some("Token"), + s3_id = None, + s3_secret = None, + session_token = None, + profile = "profile_name", + profile_assume_role_arn = "arn:aws:iam::account-id:role/role-name", s3_bucket = "foo", s3_endpoint = S3Endpoint.defaultEndpoint, site = None, @@ -55,14 +87,44 @@ class ConfigSpec extends Specification { concurrency_level = 1, cloudfront_wildcard_invalidation = None, treat_zero_length_objects_as_redirects = None - )).getCredentials must beAnInstanceOf[BasicSessionCredentials] + )) must beAnInstanceOf[ProfileCredentialsProvider] + } + + s"return ${classOf[STSAssumeRoleSessionCredentialsProvider]} when profile is defined in the config" in { + Config.awsCredentials(Config( + s3_id = None, + s3_secret = None, + session_token = None, + profile = "profile_name", + profile_assume_role_arn = None, + s3_bucket = "foo", + s3_endpoint = S3Endpoint.defaultEndpoint, + site = None, + max_age = None, + cache_control = None, + gzip = None, + gzip_zopfli = None, + s3_key_prefix = None, + ignore_on_server = None, + exclude_from_upload = None, + s3_reduced_redundancy = None, + cloudfront_distribution_id = None, + cloudfront_invalidate_root = None, + content_type = None, + redirects = None, + concurrency_level = 1, + cloudfront_wildcard_invalidation = None, + treat_zero_length_objects_as_redirects = None + )) must beAnInstanceOf[ProfileCredentialsProvider] } - s"return ${classOf[DefaultAWSCredentialsProviderChain]} when s3_id and s3_secret are not defined in the config" in { + s"return ${classOf[DefaultAWSCredentialsProviderChain]} when s3_id, s3_secret, profile and profile_assume_role_arn are not defined in the config" in { Config.awsCredentials(Config( s3_id = None, s3_secret = None, session_token = None, + profile = None, + profile_assume_role_arn = None, s3_bucket = "foo", s3_endpoint = S3Endpoint.defaultEndpoint, site = None,