app/conf/Configuration.scala (317 lines of code) (raw):

package conf import java.io.{File, FileInputStream, InputStream} import java.net.URL import com.amazonaws.AmazonClientException import com.amazonaws.auth._ import com.amazonaws.auth.profile.ProfileCredentialsProvider import org.apache.commons.io.IOUtils import play.api.{Configuration => PlayConfiguration} import logging.Logging import scala.jdk.CollectionConverters._ import scala.language.reflectiveCalls import com.amazonaws.services.rds.model.DescribeDBInstancesRequest import com.amazonaws.services.rds.AmazonRDSClientBuilder import com.amazonaws.services.s3.AmazonS3ClientBuilder import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagementClientBuilder import com.amazonaws.services.simplesystemsmanagement.model.GetParameterRequest import software.amazon.awssdk.auth.credentials.{ DefaultCredentialsProvider, AwsCredentialsProviderChain => NewAwsCredentialsProviderChain, ProfileCredentialsProvider => NewProfileCredentialsProvider } import java.nio.charset.StandardCharsets class BadConfigurationException(msg: String) extends RuntimeException(msg) class ApplicationConfiguration( val playConfiguration: PlayConfiguration, val isProd: Boolean, // Override properties defined in configuration. Useful for testing. val propertyOverrides: Map[String, String] = Map.empty ) extends Logging { private val propertiesFile = "/etc/gu/facia-tool.properties" private val installVars = new File(propertiesFile) match { case f if f.exists => IOUtils.toString(new FileInputStream(f), "UTF-8") case _ => logger.warn("Missing configuration file $propertiesFile") "" } private val properties = Properties(installVars) ++ propertyOverrides private val stageFromProperties = properties.getOrElse("STAGE", "CODE") private val stsRoleToAssumeFromProperties = properties.getOrElse("STS_ROLE", "unknown") private val frontPressedDynamoTable = properties.getOrElse("FRONT_PRESSED_TABLE", "unknown") private val userTable = properties.getOrElse("USER_DATA_TABLE", "unknown") private def getString(property: String): Option[String] = playConfiguration .getOptional[String](stageFromProperties + "." + property) .orElse(playConfiguration.getOptional[String](property)) private def getMandatoryString(property: String): String = getString(property) .getOrElse( throw new BadConfigurationException( s"$property of type string not configured for stage $stageFromProperties" ) ) private def getBoolean(property: String): Option[Boolean] = playConfiguration .getOptional[Boolean](stageFromProperties + "." + property) .orElse(playConfiguration.getOptional[Boolean](property)) private def getMandatoryBoolean(property: String): Boolean = getBoolean( property ) .getOrElse( throw new BadConfigurationException( s"$property of type boolean not configured for stage $stageFromProperties" ) ) def getMandatoryStringPropertiesSplitByComma( propertyName: String ): List[String] = { getMandatoryString(propertyName).split(",").toList.filter(_.nonEmpty) } object environment { val stage = stageFromProperties.toLowerCase val applicationName = "facia-tool" // isProd is derived from the environment mode which is given // to us by play, it is true for both prod and code. Stage is a variable coming // from the config and tells us which bucket we are reading fronts and collections from. // Stage is prod for production environment and code for code and dev environemnts. // These two variables together allow us to determine the application url. val correspondingToolsDomainSuffix = if (isProd && stage == "code") "code.dev-gutools.co.uk" else if (isProd) "gutools.co.uk" else "local.dev-gutools.co.uk" val applicationUrl = s"https://fronts.${correspondingToolsDomainSuffix}" } object ophanApi { lazy val key = getString("ophan.api.key") lazy val host = getString("ophan.api.host") } object recipesApi { lazy val key = getString("recipes.api.key") lazy val url = getString("recipes.api.url") } object analytics { lazy val secret = getMandatoryString("analytics.secret") } object aws { lazy val region = getMandatoryString("aws.region") lazy val bucket = getMandatoryString("aws.bucket") lazy val frontsBucket = getMandatoryString("aws.frontsBucket") lazy val publishedEditionsIssuesBucket = getMandatoryString( "aws.publishedEditionsIssuesBucket" ) lazy val previewEditionsIssuesBucket = getMandatoryString( "aws.previewEditionsIssuesBucket" ) lazy val feastAppPublicationTopic = getMandatoryString( "feast_app.publication_topic" ) def cmsFrontsAccountCredentials: AWSCredentialsProvider = credentials.getOrElse( throw new BadConfigurationException( "AWS credentials are not configured for CMS Fronts" ) ) val credentials: Option[AWSCredentialsProvider] = { val provider = new AWSCredentialsProviderChain( new ProfileCredentialsProvider("cmsFronts"), new DefaultAWSCredentialsProviderChain() ) // this is a bit of a convoluted way to check whether we actually have credentials. // I guess in an ideal world there would be some sort of isConfigued() method... try { val creds = provider.getCredentials Some(provider) } catch { case ex: AmazonClientException => logger.error("amazon client exception") // We really, really want to ensure that PROD is configured before saying a box is OK if (isProd) throw ex // this means that on dev machines you only need to configure keys if you are actually going to use them None } } // NB this does not fail with exception (as the 'old' credentials above). It is assumed that the code // above would have already failed. // If and when this code is rewritten to remove the 'old' approach, then that behaviour may be duplicated here. def newStyleCmsFrontsAccountCredentials = NewAwsCredentialsProviderChain .builder() .addCredentialsProvider(NewProfileCredentialsProvider.create("cmsFronts")) .addCredentialsProvider(DefaultCredentialsProvider.create()) .build() def frontendAccountCredentials: AWSCredentialsProvider = crossAccount.getOrElse( throw new BadConfigurationException( "AWS credentials are not configured for cross account Frontend" ) ) var crossAccount: Option[AWSCredentialsProvider] = { val provider = new AWSCredentialsProviderChain( new ProfileCredentialsProvider("frontend"), new STSAssumeRoleSessionCredentialsProvider.Builder( faciatool.stsRoleToAssume, "frontend" ).build() ) // this is a bit of a convoluted way to check whether we actually have credentials. // I guess in an ideal world there would be some sort of isConfigued() method... try { val creds = provider.getCredentials Some(provider) } catch { case ex: AmazonClientException => logger.error("amazon client cross account exception") // We really, really want to ensure that PROD is configured before saying a box is OK if (isProd) throw ex // this means that on dev machines you only need to configure keys if you are actually going to use them None } } lazy val rdsClient = AmazonRDSClientBuilder .standard() .withCredentials(cmsFrontsAccountCredentials) .withRegion(region) .build() lazy val ssmClient = AWSSimpleSystemsManagementClientBuilder .standard() .withCredentials(cmsFrontsAccountCredentials) .withRegion(region) .build() lazy val s3Client = AmazonS3ClientBuilder .standard() .withCredentials(cmsFrontsAccountCredentials) .withRegion(region) .build() } object postgres { val (hostname, port) = findRDSEndpointAndPort() val url = s"jdbc:postgresql://$hostname:$port/faciatool" val user = "faciatool" val password = getPassword private def getPassword: String = { // In fronts tool 'isProd' means is CODE or PROD because fuck it why not if (isProd) { val request = new GetParameterRequest() .withName(s"/facia-tool/cms-fronts/$stageFromProperties/db/password") .withWithDecryption(true) val response = aws.ssmClient.getParameter(request) response.getParameter.getValue } else { getMandatoryString("db.default.password") } } private def findRDSEndpointAndPort(): (String, String) = { // In fronts tool 'isProd' means is CODE or PROD because fuck it why not if (isProd) { val dbIdentifier = if (stageFromProperties == "PROD") "facia-prod-db" else "facia-code-db" val request = new DescribeDBInstancesRequest().withDBInstanceIdentifier( dbIdentifier ) val instances = aws.rdsClient .describeDBInstances(request) .getDBInstances .asScala .toList if (instances.length != 1) { throw new IllegalStateException( s"Invalid number of RDS instances, expected 1, found ${instances.length}" ) } val instance = instances.head val awsHost = instance.getEndpoint.getAddress val awsPort = instance.getEndpoint.getPort.toString (awsHost, awsPort) } else { val host = getMandatoryString("db.default.hostname") val port = getMandatoryString("db.default.port") (host, port) } } def credentialsProviderChain( accessKey: Option[String] = None, secretKey: Option[String] = None ): AWSCredentialsProviderChain = { new AWSCredentialsProviderChain( new AWSCredentialsProvider { override def getCredentials: AWSCredentials = (for { key <- accessKey secret <- secretKey } yield new BasicAWSCredentials(key, secret)).orNull override def refresh(): Unit = {} }, new EnvironmentVariableCredentialsProvider, new SystemPropertiesCredentialsProvider, new ProfileCredentialsProvider("cmsFronts"), InstanceProfileCredentialsProvider.getInstance() ) } } object contentApi { case class Auth(user: String, password: String) val contentApiLiveHost: String = getMandatoryString("content.api.host") def contentApiDraftHost: String = getMandatoryString( "content.api.draft.iam-host" ) lazy val editionsKey: String = getMandatoryString( "content.api.editions.apiKey" ) lazy val key: Option[String] = getString("content.api.key") lazy val timeout: Int = 2000 lazy val previewRole: String = getMandatoryString("content.api.draft.role") } object facia { lazy val stage = getString("facia.stage").getOrElse(stageFromProperties) lazy val collectionCap: Int = 20 lazy val navListCap: Int = 40 lazy val navListType: String = "nav/list" } object faciatool { lazy val breakingNewsFront = "breaking-news" lazy val canEditEditions = "edit-editions" lazy val frontPressToolTopic = getString("faciatool.sns.tool_topic_arn") lazy val publishEventsQueue = getMandatoryString("publish_events.queue_url") lazy val showTestContainers = getBoolean("faciatool.show_test_containers").getOrElse(false) lazy val stsRoleToAssume = getString("faciatool.sts.role.to.assume").getOrElse( stsRoleToAssumeFromProperties ) lazy val frontPressUpdateTable = frontPressedDynamoTable lazy val userDataTable = userTable } object media { lazy val baseUrl = getString("media.base.url") lazy val apiUrl = getMandatoryString("media.api.url") lazy val usageUrl = getMandatoryString("media.usage.url") lazy val key = getMandatoryString("media.key") } object notification { lazy val host = getMandatoryString("notification.host") lazy val key = getMandatoryString("notification.key") } object pandomain { lazy val host = getMandatoryString("pandomain.host") lazy val domain = getMandatoryString("pandomain.domain") lazy val service = getMandatoryString("pandomain.service") lazy val roleArn = getMandatoryString("pandomain.roleArn") lazy val bucketName = getMandatoryString("pandomain.bucketName") lazy val settingsFileKey = s"$domain.settings" lazy val userGroups = getMandatoryStringPropertiesSplitByComma( "pandomain.user.groups" ) } object permission { lazy val cache = getMandatoryString("permissions.cache") } object sentry { lazy val publicDSN = getString("sentry.publicDSN").getOrElse("") } object switchBoard { val bucket = getMandatoryString("switchboard.bucket") val objectKey = getMandatoryString("switchboard.object") } } object Properties extends AutomaticResourceManagement { def apply(is: InputStream): Map[String, String] = { val properties = new java.util.Properties() withCloseable(is) { properties load _ } properties.asScala.toMap } def apply(text: String): Map[String, String] = apply( IOUtils.toInputStream(text) ) def apply(file: File): Map[String, String] = apply(new FileInputStream(file)) def apply(url: URL): Map[String, String] = apply(url.openStream) } trait AutomaticResourceManagement { def withCloseable[T <: { def close(): Unit }](closeable: T) = new { def apply[S](body: T => S) = try { body(closeable) } finally { closeable.close() } } }