Skip to content

Commit

Permalink
feat: support vault AppRole authentication (#884)
Browse files Browse the repository at this point in the history
Signed-off-by: Pat Losoponkul <[email protected]>
  • Loading branch information
patlo-iog authored Feb 7, 2024
1 parent acdd7d4 commit 441f878
Show file tree
Hide file tree
Showing 17 changed files with 350 additions and 89 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ megalinter-reports/
.bsp
.jvmopts
.metals
sbt-launch.jar
**/project/metals.sbt
target
**/.DS_Store
Expand Down
7 changes: 5 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ lazy val V = new {
val zioMetricsConnector = "2.1.0"
val zioMock = "1.0.0-RC11"
val mockito = "3.2.16.0"
val monocle = "3.1.0"

// https://mvnrepository.com/artifact/io.circe/circe-core
val circe = "0.14.6"
Expand Down Expand Up @@ -85,7 +86,7 @@ lazy val V = new {

val jsonSchemaValidator = "1.0.86"

val vaultDriver = "6.1.0"
val vaultDriver = "6.2.0"
val micrometer = "1.11.2"

val nimbusJwt = "10.0.0"
Expand Down Expand Up @@ -151,6 +152,8 @@ lazy val D = new {
val zioTestMagnolia: ModuleID = "dev.zio" %% "zio-test-magnolia" % V.zio % Test
val zioMock: ModuleID = "dev.zio" %% "zio-mock" % V.zioMock
val mockito: ModuleID = "org.scalatestplus" %% "mockito-4-11" % V.mockito % Test
val monocle: ModuleID = "dev.optics" %% "monocle-core" % V.monocle % Test
val monocleMacro: ModuleID = "dev.optics" %% "monocle-macro" % V.monocle % Test

// LIST of Dependencies
val doobieDependencies: Seq[ModuleID] =
Expand Down Expand Up @@ -405,7 +408,7 @@ lazy val D_PrismAgent = new {
lazy val iamDependencies: Seq[ModuleID] = Seq(keycloakAuthz, D.jwtCirce)

lazy val serverDependencies: Seq[ModuleID] =
baseDependencies ++ tapirDependencies ++ postgresDependencies ++ Seq(D.zioMock, D.mockito)
baseDependencies ++ tapirDependencies ++ postgresDependencies ++ Seq(D.zioMock, D.mockito, D.monocle, D.monocleMacro)
}

publish / skip := true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package io.iohk.atala.pollux.core.service
import io.iohk.atala.pollux.core.service.URIDereferencerError.{ConnectionError, ResourceNotFound, UnexpectedError}
import zio.*
import zio.http.*
import zio.stream.ZSink

import java.net.URI
import java.nio.charset.StandardCharsets
Expand Down
11 changes: 9 additions & 2 deletions prism-agent/service/server/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ agent {
}
secretStorage {
# Supports the following backend: [vault, postgres, memory]
# If 'postgres' is used as a backend, it uses the agent db configuration
# If 'postgres' is used as a backend, it uses the agent db configuration.
# If any other backend is used, its corresponding configuration must be configured.
backend = "vault"
backend = ${?SECRET_STORAGE_BACKEND}
Expand All @@ -199,8 +199,15 @@ agent {
vault {
address = "http://localhost:8200"
address = ${?VAULT_ADDR}
token= "root"

# Vault token authentication.
# Can be omitted if other authentication mechanism is provided.
token = ${?VAULT_TOKEN}

# Vault AppRole authentication.
# Can be omitted if other authentication mechanism is provided.
appRoleRoleId = ${?VAULT_APPROLE_ROLE_ID}
appRoleSecretId = ${?VAULT_APPROLE_SECRET_ID}
}
}
webhookPublisher {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,26 +84,6 @@ object MainApp extends ZIOAppDefault {
_ <- AgentMigrations.validateRLS.provide(RepoModule.agentContextAwareTransactorLayer)
} yield ()

private val zioHttpClientLayer = {
import zio.http.netty.NettyConfig
import zio.http.{ConnectionPoolConfig, DnsResolver, ZClient}
(ZLayer.fromZIO(
for {
appConfig <- ZIO.service[AppConfig].provide(SystemModule.configLayer)
} yield ZClient.Config.default.copy(
connectionPool = {
val cpSize = appConfig.agent.httpClient.connectionPoolSize
if (cpSize > 0) ConnectionPoolConfig.Fixed(cpSize)
else ConnectionPoolConfig.Disabled
},
idleTimeout = Some(appConfig.agent.httpClient.idleTimeout),
connectionTimeout = Some(appConfig.agent.httpClient.connectionTimeout),
)
) ++
ZLayer.succeed(NettyConfig.default) ++
DnsResolver.default) >>> ZClient.live
}

override def run: ZIO[Any, Throwable, Unit] = {

val app = for {
Expand Down Expand Up @@ -196,7 +176,7 @@ object MainApp extends ZIOAppDefault {
// event notification service
ZLayer.succeed(500) >>> EventNotificationServiceImpl.layer,
// HTTP client
zioHttpClientLayer,
SystemModule.zioHttpClientLayer,
Scope.default,
)
} yield app
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import doobie.util.transactor.Transactor
import io.grpc.ManagedChannelBuilder
import io.iohk.atala.agent.server.config.AppConfig
import io.iohk.atala.agent.server.config.SecretStorageBackend
import io.iohk.atala.agent.server.config.ValidatedVaultConfig
import io.iohk.atala.agent.server.config.VaultConfig
import io.iohk.atala.agent.walletapi.crypto.Apollo
import io.iohk.atala.agent.walletapi.memory.{
DIDSecretStorageInMemory,
Expand Down Expand Up @@ -56,6 +58,26 @@ object SystemModule {
)
)
}

val zioHttpClientLayer = {
import zio.http.netty.NettyConfig
import zio.http.{ConnectionPoolConfig, DnsResolver, ZClient}
(ZLayer.fromZIO(
for {
appConfig <- ZIO.service[AppConfig].provide(SystemModule.configLayer)
} yield ZClient.Config.default.copy(
connectionPool = {
val cpSize = appConfig.agent.httpClient.connectionPoolSize
if (cpSize > 0) ConnectionPoolConfig.Fixed(cpSize)
else ConnectionPoolConfig.Disabled
},
idleTimeout = Some(appConfig.agent.httpClient.idleTimeout),
connectionTimeout = Some(appConfig.agent.httpClient.connectionTimeout),
)
) ++
ZLayer.succeed(NettyConfig.default) ++
DnsResolver.default) >>> ZClient.live
}
}

object AppModule {
Expand Down Expand Up @@ -183,19 +205,28 @@ object RepoModule {
agentDbConfigLayer(appUser = false) >>> TransactorLayer.task

val vaultClientLayer: TaskLayer[VaultKVClient] = {
val vaultClientConfig = ZLayer {
val vaultClient = ZLayer {
for {
config <- ZIO
.service[AppConfig]
.map(_.agent.secretStorage.vault)
.someOrFailException
.tapError(_ => ZIO.logError("Vault config is not found"))
.logError("Vault config is not found")
_ <- ZIO.logInfo("Vault client config loaded. Address: " + config.address)
vaultKVClient <- VaultKVClientImpl.fromAddressAndToken(config.address, config.token)
vaultKVClient <- ZIO
.fromEither(config.validate)
.mapError(Exception(_))
.flatMap {
case ValidatedVaultConfig.TokenAuth(address, token) =>
ZIO.logInfo("Using Vault token authentication") *> VaultKVClientImpl.fromToken(address, token)
case ValidatedVaultConfig.AppRoleAuth(address, roleId, secretId) =>
ZIO.logInfo("Using Vault AppRole authentication") *>
VaultKVClientImpl.fromAppRole(address, roleId, secretId)
}
} yield vaultKVClient
}

SystemModule.configLayer >>> vaultClientConfig
SystemModule.configLayer >>> vaultClient
}

val allSecretStorageLayer: TaskLayer[DIDSecretStorage & WalletSecretStorage & GenericSecretStorage] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ import io.iohk.atala.shared.utils.DurationOps.toMetricsSeconds
import io.iohk.atala.system.controller.SystemServerEndpoints
import zio.*
import zio.metrics.*
import java.util.concurrent.Executors
import scala.concurrent.ExecutionContext

object PrismAgentApp {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,31 @@ object AppConfig {
val descriptor: ConfigDescriptor[AppConfig] = Descriptor[AppConfig]
}

final case class VaultConfig(address: String, token: String)
final case class VaultConfig(
address: String,
token: Option[String],
appRoleRoleId: Option[String],
appRoleSecretId: Option[String]
) {
def validate: Either[String, ValidatedVaultConfig] =
val tokenConfig = token.map(ValidatedVaultConfig.TokenAuth(address, _))
val appRoleConfig =
for {
roleId <- appRoleRoleId
secretId <- appRoleSecretId
} yield ValidatedVaultConfig.AppRoleAuth(address, roleId, secretId)

tokenConfig
.orElse(appRoleConfig)
.toRight("Vault configuration is invalid. Vault token or AppRole authentication must be provided.")
}

sealed trait ValidatedVaultConfig

object ValidatedVaultConfig {
final case class TokenAuth(address: String, token: String) extends ValidatedVaultConfig
final case class AppRoleAuth(address: String, roleId: String, secretId: String) extends ValidatedVaultConfig
}

final case class PolluxConfig(
database: DatabaseConfig,
Expand Down Expand Up @@ -130,14 +154,16 @@ final case class AgentConfig(
webhookPublisher: WebhookPublisherConfig,
defaultWallet: DefaultWalletConfig
) {
def validate: Either[String, Unit] = {
if (!defaultWallet.enabled && !authentication.isEnabledAny)
Left(
def validate: Either[String, Unit] =
for {
_ <- Either.cond(
defaultWallet.enabled || authentication.isEnabledAny,
(),
"The default wallet must be enabled if all the authentication methods are disabled. Default wallet is required for the single-tenant mode."
)
else
Right(())
}
_ <- secretStorage.validate
} yield ()

}

final case class HttpEndpointConfig(http: HttpConfig, publicEndpointUrl: String)
Expand All @@ -151,7 +177,17 @@ final case class HttpClientConfig(connectionPoolSize: Int, idleTimeout: Duration
final case class SecretStorageConfig(
backend: SecretStorageBackend,
vault: Option[VaultConfig],
)
) {
def validate: Either[String, Unit] =
backend match {
case SecretStorageBackend.vault =>
vault
.toRight("SecretStorage backend is set to 'vault', but vault config is not provided.")
.flatMap(_.validate)
.map(_ => ())
case _ => Right(())
}
}

enum SecretStorageBackend {
case vault, postgres, memory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import io.iohk.atala.mercury.*
import zio.*
import zio.http.{Header as _, *}

import java.time.Instant

object ZioHttpClient {
val layer: URLayer[Client, ZioHttpClient] = ZLayer.fromFunction(new ZioHttpClient(_))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ final case class AuthenticationConfig(
keycloak: KeycloakConfig
) {

/** Return true if at least 1 authentication method is enabled (exlcuding admin auth method) */
/** Return true if at least 1 authentication method is enabled (excluding admin auth method) */
def isEnabledAny: Boolean = apiKey.enabled || keycloak.enabled

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.iohk.atala.agent.server

import io.iohk.atala.agent.server.config.AppConfig
import io.iohk.atala.agent.server.config.VaultConfig
import io.iohk.atala.agent.walletapi.crypto.ApolloSpecHelper
import io.iohk.atala.agent.walletapi.service.EntityServiceImpl
import io.iohk.atala.agent.walletapi.service.WalletManagementService
Expand All @@ -23,6 +24,7 @@ import zio.test.Assertion.*
import zio.test.ZIOSpecDefault

import java.net.URL
import io.iohk.atala.agent.server.config.SecretStorageBackend

object AgentInitializationSpec extends ZIOSpecDefault, PostgresTestContainerSupport, ApolloSpecHelper {

Expand Down Expand Up @@ -147,36 +149,25 @@ object AgentInitializationSpec extends ZIOSpecDefault, PostgresTestContainerSupp
enableApiKey: Boolean = true,
enableKeycloak: Boolean = false,
): ZIO[R, E, A] = {
import monocle.syntax.all.*
for {
appConfig <- ZIO.service[AppConfig]
agentConfig = appConfig.agent
defaultWalletConfig = agentConfig.defaultWallet
authConfig = agentConfig.authentication
apiKeyConfig = authConfig.apiKey
keycloakConfig = authConfig.keycloak
// consider using lens
result <- effect.provideSomeLayer(
ZLayer.succeed(
appConfig.copy(
agent = agentConfig.copy(
authentication = authConfig.copy(
apiKey = apiKeyConfig.copy(
enabled = enableApiKey
),
keycloak = keycloakConfig.copy(
enabled = enableKeycloak
)
),
defaultWallet = defaultWalletConfig.copy(
enabled = enableDefaultWallet,
seed = seed,
webhookUrl = webhookUrl,
webhookApiKey = webhookApiKey
)
)
)
)
appConfig <- ZIO.serviceWith[AppConfig](
_.focus(_.agent.authentication.apiKey.enabled)
.replace(enableApiKey)
.focus(_.agent.authentication.keycloak.enabled)
.replace(enableKeycloak)
.focus(_.agent.secretStorage.backend)
.replace(SecretStorageBackend.postgres)
.focus(_.agent.defaultWallet.enabled)
.replace(enableDefaultWallet)
.focus(_.agent.defaultWallet.seed)
.replace(seed)
.focus(_.agent.defaultWallet.webhookUrl)
.replace(webhookUrl)
.focus(_.agent.defaultWallet.webhookApiKey)
.replace(webhookApiKey)
)
result <- effect.provideSomeLayer(ZLayer.succeed(appConfig))
} yield result
}
}
Expand Down
Loading

0 comments on commit 441f878

Please sign in to comment.