Skip to content

Commit 6313d80

Browse files
committed
refetching of rotated credentials with fallback to config values
1 parent d6ad867 commit 6313d80

File tree

11 files changed

+147
-181
lines changed

11 files changed

+147
-181
lines changed

server/src/main/resources/reference.conf

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
{
22
postgres {
3-
# The JDBC driver class
4-
dataSourceClass=org.postgresql.Driver
3+
dataSourceClass="za.co.absa.atum.server.api.database.PostgresDataSourceWithPasswordFromSecretsManager"
54
# host.docker.internal for local run against db running in docker on its host machine; localhost otherwise for testing and for the gh pipeline
65
serverName=localhost
76
portNumber=5432
87
databaseName=atum_db
98
user=atum_user
109
password=changeme
10+
passwordSecretId="bdtools-atum-service-dev/atum_service_user_password"
1111
# maximum number of connections that HikariCP will keep in the pool, including both idle and in-use connections
1212
maxPoolSize=10
1313
}
1414
aws {
1515
region = "af-south-1"
16-
dbPasswordSecretName = "serviceUserSecretKey"
1716
}
1817
ssl {
1918
enabled=false

server/src/main/scala/za/co/absa/atum/server/Main.scala

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,6 @@ import za.co.absa.atum.server.api.v2.repository.{
5353
import za.co.absa.atum.server.api.database.flows.functions.{GetFlowCheckpoints, GetFlowPartitionings}
5454
import za.co.absa.atum.server.api.database.runs.functions._
5555
import za.co.absa.atum.server.api.database.{PostgresDatabaseProvider, TransactorProvider}
56-
57-
import za.co.absa.atum.server.aws.AwsSecretsProviderImpl
5856
import za.co.absa.atum.server.config.JvmMonitoringConfig
5957
import zio._
6058
import zio.config.typesafe.TypesafeConfigProvider
@@ -111,8 +109,6 @@ object Main extends ZIOAppDefault {
111109
GetCheckpointProperties.layer,
112110
PostgresDatabaseProvider.layer,
113111
TransactorProvider.layer,
114-
// aws
115-
AwsSecretsProviderImpl.layer,
116112
// scope
117113
zio.Scope.default,
118114
// prometheus
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package za.co.absa.atum.server.api.database
2+
3+
import com.typesafe.config.ConfigFactory
4+
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider
5+
import software.amazon.awssdk.regions.Region
6+
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient
7+
8+
object AWSSDKs {
9+
10+
private val config = ConfigFactory
11+
.load()
12+
.getConfig(s"aws")
13+
14+
private val defaultRegion = config.getString("region")
15+
16+
private val credentialsProvider = DefaultCredentialsProvider.create
17+
18+
val secretsManagerSyncClient: SecretsManagerClient = SecretsManagerClient.builder
19+
.region(Region.of(defaultRegion))
20+
.credentialsProvider(credentialsProvider)
21+
.build
22+
23+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package za.co.absa.atum.server.api.database
2+
3+
import com.typesafe.config.ConfigFactory
4+
import org.postgresql.ds.PGSimpleDataSource
5+
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient
6+
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest
7+
8+
import java.sql.{Connection, SQLException}
9+
import scala.util.{Failure, Try}
10+
import org.slf4j.Logger
11+
import org.slf4j.LoggerFactory
12+
13+
/**
14+
* `PGSimpleDataSource` but with password being fetched from AWS Secrets Manager.
15+
*
16+
* Refreshes the password when `PGSimpleDataSource.getConnection` fails,
17+
* thus it works with secrets with rotation enabled.
18+
*
19+
* Expects the same set of properties as `PGSimpleDataSource` +
20+
* `passwordSecretId` - ID of the secret containing password.
21+
*/
22+
class PostgresDataSourceWithPasswordFromSecretsManager extends PGSimpleDataSource {
23+
24+
protected val logger: Logger = LoggerFactory.getLogger(this.getClass())
25+
26+
private var password: String = _
27+
private var passwordSecretId: String = _
28+
29+
override def getConnection(): Connection = {
30+
if (Option(password).isEmpty) {
31+
val pw = getPasswordFromSecretsManagerOrConfig()
32+
setInternalPassword(pw)
33+
}
34+
35+
val connectionTry = Try(baseGetConnection(user, password)).recoverWith { case _: SQLException =>
36+
logger.info("Failed to create Postgres connection, attempting to refresh the password and try again...")
37+
for {
38+
passwordFromSecretsManager <- getPasswordFromSecretsManager()
39+
connection <- Try(baseGetConnection(user, passwordFromSecretsManager)).recoverWith { case e =>
40+
logger.error("Failed to create Postgres connection even after password refresh")
41+
Failure(e)
42+
}
43+
} yield {
44+
setInternalPassword(passwordFromSecretsManager)
45+
connection
46+
}
47+
}
48+
49+
connectionTry.get
50+
}
51+
52+
// getter and setter for passwordSecretId are needed as this class is usually constructed by reflection
53+
def getPasswordSecretId(): String = passwordSecretId
54+
55+
def setPasswordSecretId(passwordSecretId: String): Unit = {
56+
this.passwordSecretId = passwordSecretId
57+
}
58+
59+
override def setProperty(name: String, value: String): Unit = name match {
60+
case "passwordSecretId" => setPasswordSecretId(value)
61+
case _ => baseSetProperty(name, value)
62+
}
63+
64+
// the following protected defs are for easier unit tests
65+
protected def baseSetProperty(name: String, value: String): Unit = super.setProperty(name, value)
66+
protected def baseGetConnection(username: String, password: String): Connection =
67+
super.getConnection(username, password)
68+
protected def user: String = this.getUser
69+
protected def secretsManagerClient: SecretsManagerClient = AWSSDKs.secretsManagerSyncClient
70+
71+
private[database] def setInternalPassword(password: String): Unit = {
72+
this.password = password
73+
}
74+
75+
private def getPasswordFromSecretsManager(): Try[String] = {
76+
val secretID = getPasswordSecretId()
77+
78+
val secretValueTry = Try {
79+
logger.info(s"Fetching password for Postgres from Secrets Manager (secret id: $secretID)")
80+
val response = secretsManagerClient.getSecretValue(
81+
GetSecretValueRequest.builder
82+
.secretId(secretID)
83+
.build
84+
)
85+
logger.info("Successfully fetched password for Postgres from Secrets Manager")
86+
response.secretString
87+
}
88+
89+
secretValueTry.recoverWith { case e =>
90+
logger.error(s"Failed to fetch password for Postgres from Secrets Manager (secret id: $secretID)")
91+
Failure(e)
92+
}
93+
}
94+
95+
private def getPasswordFromSecretsManagerOrConfig(): String = {
96+
getPasswordFromSecretsManager().getOrElse {
97+
logger.error(
98+
s"Failed to fetch password from Secrets Manager (secret id: ${getPasswordSecretId()}). " +
99+
s"Falling back to config value."
100+
)
101+
val configPassword = ConfigFactory.load().getConfig("postgres").getString("password")
102+
configPassword
103+
}
104+
}
105+
106+
}

server/src/main/scala/za/co/absa/atum/server/api/database/TransactorProvider.scala

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,38 +18,30 @@ package za.co.absa.atum.server.api.database
1818

1919
import com.zaxxer.hikari.HikariConfig
2020
import doobie.hikari.HikariTransactor
21-
import za.co.absa.atum.server.aws.AwsSecretsProvider
22-
import za.co.absa.atum.server.config.{AwsConfig, PostgresConfig}
21+
import za.co.absa.atum.server.config.PostgresConfig
2322
import zio.Runtime.defaultBlockingExecutor
2423
import zio._
2524
import zio.interop.catz._
2625

2726
object TransactorProvider {
2827

29-
val layer: ZLayer[Any with Scope with AwsSecretsProvider, Throwable, HikariTransactor[Task]] = ZLayer {
28+
val layer: ZLayer[Any with Scope, Throwable, HikariTransactor[Task]] = ZLayer {
3029
for {
3130
postgresConfig <- ZIO.config[PostgresConfig](PostgresConfig.config)
32-
awsConfig <- ZIO.config[AwsConfig](AwsConfig.config)
33-
34-
awsSecretsProvider <- ZIO.service[AwsSecretsProvider]
35-
password <- awsSecretsProvider
36-
.getSecretValue(awsConfig.dbPasswordSecretName)
37-
// fallback to password property's value from postgres section of reference.conf; useful for local testing
38-
.orElse {
39-
ZIO
40-
.logError("Credentials were not retrieved from AWS, falling back to config value.")
41-
.as(postgresConfig.password)
42-
}
4331

4432
hikariConfig = {
33+
val dataSourceProperties = new java.util.Properties()
34+
dataSourceProperties.setProperty("serverName", postgresConfig.serverName)
35+
dataSourceProperties.setProperty("portNumber", postgresConfig.portNumber.toString)
36+
dataSourceProperties.setProperty("databaseName", postgresConfig.databaseName)
37+
dataSourceProperties.setProperty("user", postgresConfig.user)
38+
dataSourceProperties.setProperty("passwordSecretId", postgresConfig.passwordSecretId)
39+
4540
val config = new HikariConfig()
46-
config.setDriverClassName(postgresConfig.dataSourceClass)
47-
config.setJdbcUrl(
48-
s"jdbc:postgresql://${postgresConfig.serverName}:${postgresConfig.portNumber}/${postgresConfig.databaseName}"
49-
)
50-
config.setUsername(postgresConfig.user)
51-
config.setPassword(password)
41+
config.setDataSourceClassName(postgresConfig.dataSourceClass)
42+
config.setDataSourceProperties(dataSourceProperties)
5243
config.setMaximumPoolSize(postgresConfig.maxPoolSize)
44+
config.setPoolName("DoobiePostgresHikariPool")
5345
config
5446
}
5547

server/src/main/scala/za/co/absa/atum/server/aws/AwsSecretsProvider.scala

Lines changed: 0 additions & 25 deletions
This file was deleted.

server/src/main/scala/za/co/absa/atum/server/aws/AwsSecretsProviderImpl.scala

Lines changed: 0 additions & 42 deletions
This file was deleted.

server/src/main/scala/za/co/absa/atum/server/config/AwsConfig.scala

Lines changed: 0 additions & 29 deletions
This file was deleted.

server/src/main/scala/za/co/absa/atum/server/config/PostgresConfig.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ case class PostgresConfig(
2626
databaseName: String,
2727
user: String,
2828
password: String,
29-
maxPoolSize: Int
29+
maxPoolSize: Int,
30+
passwordSecretId: String
3031
)
3132

3233
object PostgresConfig {

server/src/test/resources/reference.conf

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
{
22
postgres {
3-
# The JDBC driver class
4-
dataSourceClass=org.postgresql.Driver
3+
dataSourceClass="za.co.absa.atum.server.api.database.PostgresDataSourceWithPasswordFromSecretsManager"
54
serverName=localhost
65
portNumber=5432
76
databaseName=atum_db
87
# tests have to be run with atum_owner so we can execute not only plpgsql functions granted to atum_user
98
user=atum_owner
109
password=changeme
10+
passwordSecretId="bdtools-atum-service-dev/atum_service_user_password"
1111
# maximum number of connections that HikariCP will keep in the pool, including both idle and in-use connections
1212
maxPoolSize=10
1313
}
1414
aws {
1515
region = "af-south-1"
16-
dbPasswordSecretName = "serviceUserSecretKey"
1716
}
1817
ssl {
1918
enabled=false

0 commit comments

Comments
 (0)