diff --git a/modules/r53/src/it/scala/vinyldns/route53/backend/Route53IntegrationSpec.scala b/modules/r53/src/it/scala/vinyldns/route53/backend/Route53IntegrationSpec.scala index cd799f182..9f9edac96 100644 --- a/modules/r53/src/it/scala/vinyldns/route53/backend/Route53IntegrationSpec.scala +++ b/modules/r53/src/it/scala/vinyldns/route53/backend/Route53IntegrationSpec.scala @@ -30,7 +30,7 @@ import vinyldns.core.domain.{Fqdn, record} import vinyldns.core.domain.record.{RecordSet, RecordType} class Route53IntegrationSpec - extends AnyWordSpec + extends AnyWordSpec with BeforeAndAfterAll with BeforeAndAfterEach with Matchers { @@ -52,6 +52,8 @@ class Route53IntegrationSpec "test", Option("access"), Option("secret"), + None, + None, sys.env.getOrElse("R53_SERVICE_ENDPOINT", "http://localhost:19003"), "us-east-1" ) diff --git a/modules/r53/src/main/scala/vinyldns/route53/backend/Route53Backend.scala b/modules/r53/src/main/scala/vinyldns/route53/backend/Route53Backend.scala index 4ed2e7c21..b3ebbe481 100644 --- a/modules/r53/src/main/scala/vinyldns/route53/backend/Route53Backend.scala +++ b/modules/r53/src/main/scala/vinyldns/route53/backend/Route53Backend.scala @@ -16,13 +16,9 @@ package vinyldns.route53.backend +import java.util.UUID import cats.data.OptionT import cats.effect.IO -import com.amazonaws.auth.{ - AWSStaticCredentialsProvider, - BasicAWSCredentials, - DefaultAWSCredentialsProviderChain -} import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration import com.amazonaws.handlers.AsyncHandler import com.amazonaws.services.route53.{AmazonRoute53Async, AmazonRoute53AsyncClientBuilder} @@ -278,21 +274,23 @@ object Route53Backend { r53ClientBuilder.withEndpointConfiguration( new EndpointConfiguration(config.serviceEndpoint, config.signingRegion) ) - // If either of accessKey or secretKey are empty in conf file; then use AWSCredentialsProviderChain to figure out - // credentials. - // https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html - val credProvider = config.accessKey - .zip(config.secretKey) - .map { - case (key, secret) => - new AWSStaticCredentialsProvider( - new BasicAWSCredentials(key, secret) - ) - } - .headOption - .getOrElse { - new DefaultAWSCredentialsProviderChain() + + val r53CredBuilder = Route53Credentials.builder + for { + accessKey <- config.accessKey + secretKey <- config.secretKey + } r53CredBuilder.basicCredentials(accessKey, secretKey) + + for (role <- config.roleArn) { + config.externalId match { + case Some(externalId) => + r53CredBuilder.withRole(role, UUID.randomUUID().toString, externalId) + case None => r53CredBuilder.withRole(role, UUID.randomUUID().toString) } + } + + val credProvider = r53CredBuilder.build().provider + r53ClientBuilder.withCredentials(credProvider).build() } diff --git a/modules/r53/src/main/scala/vinyldns/route53/backend/Route53Credentials.scala b/modules/r53/src/main/scala/vinyldns/route53/backend/Route53Credentials.scala new file mode 100644 index 000000000..0ce413c8b --- /dev/null +++ b/modules/r53/src/main/scala/vinyldns/route53/backend/Route53Credentials.scala @@ -0,0 +1,111 @@ +/* + * Copyright 2018 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package vinyldns.route53.backend + +import com.amazonaws.auth._ +import org.slf4j.LoggerFactory +import com.amazonaws.services.securitytoken.{ + AWSSecurityTokenService, + AWSSecurityTokenServiceClientBuilder +} + +private[backend] sealed trait Route53Credentials extends Serializable { + def provider: AWSCredentialsProvider +} + +private[backend] final case object DefaultCredentials extends Route53Credentials { + def provider: AWSCredentialsProvider = new DefaultAWSCredentialsProviderChain +} + +private[backend] final case class BasicCredentials(accessKeyId: String, secretKey: String) + extends Route53Credentials { + + private final val logger = LoggerFactory.getLogger(classOf[Route53Backend]) + + def provider: AWSCredentialsProvider = + try { + new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKeyId, secretKey)) + } catch { + case e: IllegalArgumentException => + logger.error( + "Error when using accessKey/secret: {}. Using DefaultProviderChain.", + e.getMessage() + ) + new DefaultAWSCredentialsProviderChain + } +} + +private[backend] final case class STSCredentials( + roleArn: String, + sessionName: String, + externalId: Option[String] = None, + longLivedCreds: Route53Credentials = DefaultCredentials +) extends Route53Credentials { + + def provider: AWSCredentialsProvider = { + lazy val stsClient: AWSSecurityTokenService = + AWSSecurityTokenServiceClientBuilder + .standard() + .withCredentials(longLivedCreds.provider) + .build() + val builder = new STSAssumeRoleSessionCredentialsProvider.Builder(roleArn, sessionName) + .withStsClient(stsClient) + externalId match { + case Some(externalId) => + builder + .withExternalId(externalId) + .build() + case None => + builder.build() + } + } +} + +object Route53Credentials { + class Builder { + private var basicCreds: Option[BasicCredentials] = None + private var stsCreds: Option[STSCredentials] = None + + def basicCredentials(accessKeyId: String, secretKey: String): Builder = { + basicCreds = Option(BasicCredentials(accessKeyId, secretKey)) + this + } + + def withRole(roleArn: String, sessionName: String): Builder = { + stsCreds = Option(STSCredentials(roleArn, sessionName)) + this + } + + def withRole(roleArn: String, sessionName: String, externalId: String): Builder = { + stsCreds = Option( + STSCredentials( + roleArn, + sessionName, + Option(externalId) + ) + ) + this + } + + def build(): Route53Credentials = + stsCreds.map(_.copy(longLivedCreds = longLivedCreds)).getOrElse(longLivedCreds) + + private def longLivedCreds: Route53Credentials = basicCreds.getOrElse(DefaultCredentials) + } + + def builder: Builder = new Builder +} diff --git a/modules/r53/src/main/scala/vinyldns/route53/backend/Route53ProviderConfig.scala b/modules/r53/src/main/scala/vinyldns/route53/backend/Route53ProviderConfig.scala index 3992f781e..316e94950 100644 --- a/modules/r53/src/main/scala/vinyldns/route53/backend/Route53ProviderConfig.scala +++ b/modules/r53/src/main/scala/vinyldns/route53/backend/Route53ProviderConfig.scala @@ -27,6 +27,8 @@ final case class Route53BackendConfig( id: String, accessKey: Option[String], secretKey: Option[String], + roleArn: Option[String], + externalId: Option[String], serviceEndpoint: String, signingRegion: String ) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index a6162f0a3..72918670b 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -94,6 +94,7 @@ object Dependencies { lazy val r53Dependencies = Seq( "com.amazonaws" % "aws-java-sdk-core" % awsV withSources(), + "com.amazonaws" % "aws-java-sdk-sts" % awsV withSources(), "com.amazonaws" % "aws-java-sdk-route53" % awsV withSources() )