app/com/gu/itunes/SecretKeeper.scala (75 lines of code) (raw):

package com.gu.itunes import com.gu.{ AppIdentity, AwsIdentity, DevIdentity } import org.slf4j.LoggerFactory import play.api.Configuration import software.amazon.awssdk.auth.credentials.{ AwsCredentialsProviderChain, EnvironmentVariableCredentialsProvider, InstanceProfileCredentialsProvider, ProfileCredentialsProvider, SystemPropertyCredentialsProvider } import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest import scala.util.{ Failure, Success, Try } object SecretKeeper { private val logger = LoggerFactory.getLogger(getClass) lazy val initialStage = Option(System.getProperty("stage")) lazy val credentialsProviderChain = AwsCredentialsProviderChain.builder().credentialsProviders( EnvironmentVariableCredentialsProvider.create(), SystemPropertyCredentialsProvider.create(), ProfileCredentialsProvider.create("capi"), ProfileCredentialsProvider.create(), InstanceProfileCredentialsProvider.create()).build() def getIdentity() = if (initialStage.contains("DEV") || initialStage.contains("LOCAL")) { Success(DevIdentity("podcasts-rss")) } else { AppIdentity.whoAmI("podcasts-rss", () => credentialsProviderChain.resolveCredentials()) } private def loadFromSecretsManagerImp(lookupKey: String): Try[String] = { for { identity <- getIdentity() result <- identity match { case AwsIdentity(app, stack, stage, region) => //we'll just use a basic, blocking client here as it's only used in startup val client = SecretsManagerClient.builder().credentialsProvider(credentialsProviderChain).region(Region.of(region)).build() val ssmKey = s"/$stage/$stack/$app/$lookupKey" logger.info(s"Loading $lookupKey key from secrets manager at $ssmKey") Try { client.getSecretValue(GetSecretValueRequest.builder().secretId(ssmKey).build()) } case _ => if (lookupKey == "capiKey") { logger.warn("When running locally you should set the API_KEY environment variable or apiKey in application.conf") } else { logger.warn(s"When running locally you should set $lookupKey in application.conf") } Failure(new RuntimeException("Not running in AWS")) } } yield result.secretString() } private def loadKeyFromSecretsManager(): Option[String] = loadFromSecretsManagerImp("capiKey") match { case Success(result) if result != "" => Some(result) case Success(result) if result == "" => logger.error("Loaded API key but it was an empty string") None case Failure(err) => logger.warn(s"Could not load API key: ${err.getMessage}") None } private def loadFastlySignatureSaltFromSecretsManager(): Option[String] = loadFromSecretsManagerImp("fastlyImageResizerSignatureSalt") match { case Success(result) if result != "" => Some(result) // In the event we can't load the signature salt, or we load // an empty value, this shouldn't crash the application. // Instead we just suppress the generation of episodic artwork // images if we determine that the salt is NONE (or an empty // string which we treat as NONE) case Success(result) if result == "" => logger.warn("Loaded the fastly image resizer signature salt but it was an empty string") None case Failure(err) => logger.warn(s"Could not load Fastly image resizer signature salt: ${err.getMessage}") None } def getApiKey(config: Configuration): Option[String] = config.getOptional[String]("apiKey") match { case fromConfig @ Some(apiKey) if apiKey != "" => logger.info("Using CAPI key from configuration file") fromConfig case _ => loadKeyFromSecretsManager() } def getImageResizerSignatureSalt(config: Configuration): Option[String] = config.getOptional[String]("fastlyImageResizerSignatureSalt") match { case fromConfig @ Some(sigSalt) if sigSalt != "" => logger.info("Loaded the fastly image resizer signature salt from configuration") fromConfig case _ => loadFastlySignatureSaltFromSecretsManager() } }