diff --git a/build.sbt b/build.sbt index 0f04fe823d..c6fec62547 100644 --- a/build.sbt +++ b/build.sbt @@ -62,6 +62,7 @@ lazy val V = new { val typesafeConfig = "1.4.2" val protobuf = "3.1.9" val testContainersScala = "0.41.0" + val testContainersJavaKeycloak = "3.0.0" val doobie = "1.0.0-RC2" val quill = "4.7.3" @@ -127,6 +128,7 @@ lazy val D = new { // TODO we are adding test stuff to the main dependencies val testcontainersPostgres: ModuleID = "com.dimafeng" %% "testcontainers-scala-postgresql" % V.testContainersScala val testcontainersVault: ModuleID = "com.dimafeng" %% "testcontainers-scala-vault" % V.testContainersScala + val testcontainersKeycloak: ModuleID = "com.github.dasniko" % "testcontainers-keycloak" % V.testContainersJavaKeycloak val doobiePostgres: ModuleID = "org.tpolecat" %% "doobie-postgres" % V.doobie val doobieHikari: ModuleID = "org.tpolecat" %% "doobie-hikari" % V.doobie @@ -155,6 +157,7 @@ lazy val D_Shared = new { D.scalaPbGrpc, D.testcontainersPostgres, D.testcontainersVault, + D.testcontainersKeycloak, D.zio, // FIXME: split shared DB stuff as subproject? D.doobieHikari, @@ -163,6 +166,26 @@ lazy val D_Shared = new { ) } +lazy val D_SharedTest = new { + lazy val dependencies: Seq[ModuleID] = + Seq( + D.typesafeConfig, + D.testcontainersPostgres, + D.testcontainersVault, + D.testcontainersKeycloak, + D.zio, + D.doobieHikari, + D.doobiePostgres, + D.zioCatsInterop, + D.zioJson, + D.zioHttp, + D.zioTest, + D.zioTestSbt, + D.zioTestMagnolia, + D.zioMock + ) +} + lazy val D_Connect = new { private lazy val logback = "ch.qos.logback" % "logback-classic" % V.logback % Test @@ -403,6 +426,19 @@ lazy val shared = (project in file("shared")) ) .enablePlugins(BuildInfoPlugin) +lazy val sharedTest = (project in file("shared-test")) + // .configure(publishConfigure) + .settings( + organization := "io.iohk.atala", + organizationName := "Input Output Global", + buildInfoPackage := "io.iohk.atala.sharedtest", + name := "sharedtest", + crossPaths := false, + libraryDependencies ++= D_SharedTest.dependencies + ) + .dependsOn(shared) + .enablePlugins(BuildInfoPlugin) + // ######################### // ### Models & Services ### // ######################### @@ -691,6 +727,7 @@ lazy val polluxDoobie = project ) .dependsOn(polluxCore % "compile->compile;test->test") .dependsOn(shared) + .dependsOn(sharedTest % Test) // ######################## // ### Pollux Anoncreds ### @@ -740,6 +777,7 @@ lazy val connectDoobie = project libraryDependencies ++= D_Connect.sqlDoobieDependencies ) .dependsOn(shared) + .dependsOn(sharedTest % Test) .dependsOn(connectCore % "compile->compile;test->test") // ############################ @@ -775,6 +813,7 @@ lazy val prismAgentWalletAPI = project castorCore, eventNotification ) + .dependsOn(sharedTest % Test) lazy val prismAgentServer = project .in(file("prism-agent/service/server")) @@ -794,7 +833,7 @@ lazy val prismAgentServer = project buildInfoPackage := "io.iohk.atala.agent.server.buildinfo", Compile / packageDoc / publishArtifact := false ) - .enablePlugins(JavaAppPackaging, DockerPlugin) + .enablePlugins(JavaAppPackaging, DockerPlugin, AshScriptPlugin) .enablePlugins(BuildInfoPlugin) .dependsOn(prismAgentWalletAPI % "compile->compile;test->test") .dependsOn( @@ -807,6 +846,7 @@ lazy val prismAgentServer = project castorCore, eventNotification ) + .dependsOn(sharedTest % Test) // ############################ // #### Release process ##### @@ -824,6 +864,7 @@ releaseProcess := Seq[ReleaseStep]( lazy val aggregatedProjects: Seq[ProjectReference] = Seq( shared, + sharedTest, models, protocolConnection, protocolCoordinateMediation, diff --git a/connect/lib/sql-doobie/src/test/scala/io/iohk/atala/connect/sql/repository/JdbcConnectionRepositorySpec.scala b/connect/lib/sql-doobie/src/test/scala/io/iohk/atala/connect/sql/repository/JdbcConnectionRepositorySpec.scala index d4864bf975..32e4d8d6f0 100644 --- a/connect/lib/sql-doobie/src/test/scala/io/iohk/atala/connect/sql/repository/JdbcConnectionRepositorySpec.scala +++ b/connect/lib/sql-doobie/src/test/scala/io/iohk/atala/connect/sql/repository/JdbcConnectionRepositorySpec.scala @@ -3,7 +3,7 @@ package io.iohk.atala.connect.sql.repository import com.dimafeng.testcontainers.PostgreSQLContainer import io.iohk.atala.connect.core.repository.{ConnectionRepository, ConnectionRepositorySpecSuite} import io.iohk.atala.shared.db.DbConfig -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import zio.* import zio.test.* diff --git a/connect/lib/sql-doobie/src/test/scala/io/iohk/atala/test/container/PostgresTestContainer.scala b/connect/lib/sql-doobie/src/test/scala/io/iohk/atala/test/container/PostgresTestContainer.scala index f821b418a3..a3f74c949a 100644 --- a/connect/lib/sql-doobie/src/test/scala/io/iohk/atala/test/container/PostgresTestContainer.scala +++ b/connect/lib/sql-doobie/src/test/scala/io/iohk/atala/test/container/PostgresTestContainer.scala @@ -1,9 +1,9 @@ package io.iohk.atala.test.container import com.dimafeng.testcontainers.PostgreSQLContainer +import io.iohk.atala.sharedtest.containers.PostgresTestContainer.postgresContainer import zio.* import zio.ZIO.* -import io.iohk.atala.shared.test.containers.PostgresTestContainer.postgresContainer object PostgresLayer { diff --git a/infrastructure/shared/docker-compose-demo.yml b/infrastructure/shared/docker-compose-demo.yml index 0fc0461c3e..ff7d9ef339 100644 --- a/infrastructure/shared/docker-compose-demo.yml +++ b/infrastructure/shared/docker-compose-demo.yml @@ -2,7 +2,6 @@ version: "3.8" services: - db: image: postgres:13 environment: @@ -56,7 +55,16 @@ services: prism-node: condition: service_started healthcheck: - test: ["CMD", "curl", "-f", "http://prism-agent:8085/_system/health"] + test: + [ + "CMD", + "wget", + "--timeout=5", + "--tries=3", + "-O", + "/dev/null", + "http://prism-agent:8085/_system/health", + ] interval: 30s timeout: 10s retries: 5 diff --git a/infrastructure/shared/docker-compose.yml b/infrastructure/shared/docker-compose.yml index 8b626ea58c..9681d3e0bb 100644 --- a/infrastructure/shared/docker-compose.yml +++ b/infrastructure/shared/docker-compose.yml @@ -121,7 +121,16 @@ services: vault-server: condition: service_healthy healthcheck: - test: ["CMD", "curl", "-f", "http://prism-agent:8085/_system/health"] + test: + [ + "CMD", + "wget", + "--timeout=5", + "--tries=3", + "-O", + "/dev/null", + "http://prism-agent:8085/_system/health", + ] interval: 30s timeout: 10s retries: 5 diff --git a/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/CredentialDefinitionSqlIntegrationSpec.scala b/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/CredentialDefinitionSqlIntegrationSpec.scala index 8e4b55443f..4f1e96510e 100644 --- a/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/CredentialDefinitionSqlIntegrationSpec.scala +++ b/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/CredentialDefinitionSqlIntegrationSpec.scala @@ -8,7 +8,7 @@ import io.iohk.atala.pollux.sql.model.db.{CredentialDefinition, CredentialDefini import io.iohk.atala.shared.db.ContextAwareTask import io.iohk.atala.shared.db.Implicits.* import io.iohk.atala.shared.models.{WalletAccessContext, WalletId} -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.MigrationAspects.* import zio.* import zio.json.ast.Json diff --git a/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/CredentialSchemaSqlIntegrationSpec.scala b/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/CredentialSchemaSqlIntegrationSpec.scala index 02d8009d62..6d11833ef9 100644 --- a/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/CredentialSchemaSqlIntegrationSpec.scala +++ b/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/CredentialSchemaSqlIntegrationSpec.scala @@ -9,7 +9,7 @@ import io.iohk.atala.shared.db.ContextAwareTask import io.iohk.atala.shared.db.Implicits.* import io.iohk.atala.shared.models.WalletAccessContext import io.iohk.atala.shared.models.WalletId -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.MigrationAspects.* import zio.* import zio.json.ast.Json diff --git a/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/VerificationPolicySqlIntegrationSpec.scala b/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/VerificationPolicySqlIntegrationSpec.scala index 665815dc2e..ac24d4851b 100644 --- a/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/VerificationPolicySqlIntegrationSpec.scala +++ b/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/VerificationPolicySqlIntegrationSpec.scala @@ -15,7 +15,7 @@ import io.iohk.atala.shared.db.ContextAwareTask import io.iohk.atala.shared.db.Implicits.* import io.iohk.atala.shared.models.WalletAccessContext import io.iohk.atala.shared.models.WalletId -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.MigrationAspects.* import zio.* import zio.test.* diff --git a/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/repository/JdbcCredentialRepositorySpec.scala b/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/repository/JdbcCredentialRepositorySpec.scala index 5d012cf38e..49e66d9c24 100644 --- a/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/repository/JdbcCredentialRepositorySpec.scala +++ b/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/repository/JdbcCredentialRepositorySpec.scala @@ -3,7 +3,7 @@ package io.iohk.atala.pollux.sql.repository import com.dimafeng.testcontainers.PostgreSQLContainer import io.iohk.atala.pollux.core.repository._ import io.iohk.atala.shared.db.DbConfig -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import zio._ import zio.test._ diff --git a/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/repository/JdbcPresentationRepositorySpec.scala b/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/repository/JdbcPresentationRepositorySpec.scala index a50d9deef0..6d4e2c3cc7 100644 --- a/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/repository/JdbcPresentationRepositorySpec.scala +++ b/pollux/lib/sql-doobie/src/test/scala/io/iohk/atala/pollux/sql/repository/JdbcPresentationRepositorySpec.scala @@ -3,7 +3,7 @@ package io.iohk.atala.pollux.sql.repository import com.dimafeng.testcontainers.PostgreSQLContainer import io.iohk.atala.pollux.core.repository._ import io.iohk.atala.shared.db.DbConfig -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import zio._ import zio.test._ diff --git a/pollux/lib/vc-jwt/src/test/scala/io/iohk/atala/pollux/vc/jwt/JWTVerificationTest.scala b/pollux/lib/vc-jwt/src/test/scala/io/iohk/atala/pollux/vc/jwt/JWTVerificationTest.scala index fe16e9ef64..0e45138ed6 100644 --- a/pollux/lib/vc-jwt/src/test/scala/io/iohk/atala/pollux/vc/jwt/JWTVerificationTest.scala +++ b/pollux/lib/vc-jwt/src/test/scala/io/iohk/atala/pollux/vc/jwt/JWTVerificationTest.scala @@ -256,6 +256,6 @@ object JWTVerificationTest extends ZIOSpecDefault { validation <- JwtCredential.validateEncodedJWT(jwtCredential)(resolver) } yield assert(validation.fold(_ => false, _ => true))(equalTo(false)) } - ).when(!sys.props.get("os.name").contains("Mac OS X")) // Mac OS X throws `Curve not supported: secp256k1` + ) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala index 40cb947598..632546ae0e 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala @@ -17,10 +17,19 @@ object TokenIntrospection { given JsonDecoder[TokenIntrospection] = JsonDecoder.derived } +final case class TokenResponse(access_token: String, refresh_token: String) + +object TokenResponse { + given JsonEncoder[TokenResponse] = JsonEncoder.derived + given JsonDecoder[TokenResponse] = JsonDecoder.derived +} + trait KeycloakClient { def getRpt(accessToken: String): IO[AuthenticationError, String] + def getAccessToken(username: String, password: String): IO[AuthenticationError, TokenResponse] + def introspectToken(token: String): IO[AuthenticationError, TokenIntrospection] /** Return list of permitted resources */ @@ -32,6 +41,7 @@ class KeycloakClientImpl(client: AuthzClient, httpClient: Client, keycloakConfig extends KeycloakClient { private val introspectionUrl = client.getServerConfiguration().getIntrospectionEndpoint() + private val tokenUrl = client.getServerConfiguration().getTokenEndpoint() private val baseFormHeaders = Headers(Header.ContentType(MediaType.application.`x-www-form-urlencoded`)) @@ -72,6 +82,42 @@ class KeycloakClientImpl(client: AuthzClient, httpClient: Client, keycloakConfig } yield result } + override def getAccessToken(username: String, password: String): IO[AuthenticationError, TokenResponse] = { + for { + response <- Client + .request( + url = tokenUrl, + method = Method.POST, + headers = baseFormHeaders, + content = Body.fromURLEncodedForm( + Form( + FormField.simpleField("grant_type", "password"), + FormField.simpleField("client_id", keycloakConfig.clientId), + FormField.simpleField("client_secret", keycloakConfig.clientSecret), + FormField.simpleField("username", username), + FormField.simpleField("password", password), + ) + ) + ) + .logError("Fail to get the accessToken on keyclaok.") + .mapError(e => AuthenticationError.UnexpectedError("Fail to get the accessToken on keyclaok.")) + .provide(ZLayer.succeed(httpClient)) + body <- response.body.asString + .logError("Fail parse keycloak token response.") + .mapError(e => AuthenticationError.UnexpectedError("Fail parse keycloak token response.")) + result <- + if (response.status.code == 200) { + ZIO + .fromEither(body.fromJson[TokenResponse]) + .logError("Fail to decode keycloak token response") + .mapError(e => AuthenticationError.UnexpectedError(e)) + } else { + ZIO.logError(s"Keycloak token introspection was unsucessful. Status: ${response.status}. Response: $body") *> + ZIO.fail(AuthenticationError.UnexpectedError("Token introspection was unsuccessful.")) + } + } yield result + } + override def getRpt(accessToken: String): IO[AuthenticationError, String] = ZIO .attemptBlocking { @@ -97,9 +143,11 @@ class KeycloakClientImpl(client: AuthzClient, httpClient: Client, keycloakConfig } object KeycloakClientImpl { - val layer: RLayer[KeycloakConfig & Client, KeycloakClient] = ZLayer.fromZIO { + val layer: RLayer[KeycloakConfig & Client, KeycloakClient] = + authzClientLayer >>> ZLayer.fromFunction(KeycloakClientImpl(_, _, _)) + + def authzClientLayer: RLayer[KeycloakConfig, AuthzClient] = ZLayer.fromZIO { for { - httpClient <- ZIO.service[Client] keycloakConfig <- ZIO.service[KeycloakConfig] config = KeycloakAuthzConfig( keycloakConfig.keycloakUrl.toString(), @@ -109,7 +157,6 @@ object KeycloakClientImpl { null ) client <- ZIO.attempt(AuthzClient.create(config)) - } yield KeycloakClientImpl(client, httpClient, keycloakConfig) + } yield client } - } diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/agent/server/AgentInitializationSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/agent/server/AgentInitializationSpec.scala index 5e494fd159..bedc63ae57 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/agent/server/AgentInitializationSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/agent/server/AgentInitializationSpec.scala @@ -14,7 +14,7 @@ import io.iohk.atala.iam.authentication.apikey.ApiKeyAuthenticatorImpl import io.iohk.atala.iam.authentication.apikey.JdbcAuthenticationRepository import io.iohk.atala.shared.models.WalletAccessContext import io.iohk.atala.shared.models.WalletId -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.DBTestUtils import zio.* import zio.test.* diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/SecurityLogicSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/SecurityLogicSpec.scala new file mode 100644 index 0000000000..cc76961723 --- /dev/null +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/SecurityLogicSpec.scala @@ -0,0 +1,69 @@ +package io.iohk.atala.iam.authentication + +import io.iohk.atala.agent.walletapi.model.Entity +import io.iohk.atala.iam.authentication.AuthenticationError.InvalidCredentials +import io.iohk.atala.iam.authentication.apikey.ApiKeyCredentials +import zio.* +import zio.test.* +import zio.test.Assertion.* + +import java.util.UUID + +object SecurityLogicSpec extends ZIOSpecDefault { + + /** Authenticate if apiKey is the same as entity ID */ + private def testAuthenticator(entity: Entity): Authenticator[Entity] = { + new Authenticator[Entity] { + override def isEnabled: Boolean = true + override def authenticate(credentials: Credentials): IO[AuthenticationError, Entity] = { + credentials match { + case ApiKeyCredentials(Some(apiKey)) if apiKey == entity.id.toString() => ZIO.succeed(entity) + case _ => ZIO.fail(InvalidCredentials("ApiKey key is invalid")) + } + } + } + } + + private val disabledAuthenticator: Authenticator[Entity] = { + new Authenticator[Entity] { + override def isEnabled: Boolean = false + override def authenticate(credentials: Credentials): IO[AuthenticationError, Entity] = + ZIO.fail(AuthenticationError.AuthenticationMethodNotEnabled("not enabled")) + } + } + + override def spec = suite("SecurityLogicSpec")( + test("fallback to default entity when all authentication results are disabled") { + for { + authenticatedEntity <- SecurityLogic.authenticate( + ApiKeyCredentials(Some("key-1")), + ApiKeyCredentials(Some("key-2")), + ApiKeyCredentials(Some("key-3")) + )(disabledAuthenticator) + } yield assert(authenticatedEntity)(isLeft(equalTo(Entity.Default))) + }, + test("authenticate all credentials until authenticated") { + val entity = Entity("alice", UUID.randomUUID()) + for { + authenticatedEntity <- SecurityLogic.authenticate( + ApiKeyCredentials(Some("key-1")), + ApiKeyCredentials(Some("key-2")), + ApiKeyCredentials(Some(entity.id.toString())) + )(testAuthenticator(entity)) + } yield assert(authenticatedEntity)(isRight(equalTo(entity))) + }, + test("reject if none of the credentials can be authenticated") { + val entity = Entity("alice", UUID.randomUUID()) + for { + exit <- SecurityLogic + .authenticate( + ApiKeyCredentials(Some("key-1")), + ApiKeyCredentials(Some("key-2")), + ApiKeyCredentials(Some("key-3")) + )(testAuthenticator(entity)) + .exit + } yield assert(exit)(fails(hasField("status", _.status, equalTo(sttp.model.StatusCode.Forbidden.code)))) + } + ) + +} diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorSpec.scala index c566c9db6b..844faeeb7d 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorSpec.scala @@ -14,7 +14,7 @@ import io.iohk.atala.container.util.MigrationAspects.* import io.iohk.atala.iam.authentication.AuthenticationError import io.iohk.atala.iam.authentication.AuthenticationError.InvalidCredentials import io.iohk.atala.shared.models.WalletId -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import zio.Runtime.removeDefaultLoggers import zio.test.Assertion.* import zio.test.TestAspect.sequential diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/apikey/JdbcAuthenticationRepositorySpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/apikey/JdbcAuthenticationRepositorySpec.scala index d697683b68..b5e2f0626b 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/apikey/JdbcAuthenticationRepositorySpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/apikey/JdbcAuthenticationRepositorySpec.scala @@ -1,14 +1,14 @@ package io.iohk.atala.iam.authentication.apikey -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport -import zio.test.{TestAspect, ZIOSpecDefault} -import zio.ZIO -import zio.test.* -import zio.test.TestAspect.* -import zio.test.Assertion.* import io.iohk.atala.container.util.MigrationAspects.migrate import io.iohk.atala.iam.authentication.apikey.AuthenticationMethodType.ApiKey +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import zio.Runtime.removeDefaultLoggers +import zio.ZIO +import zio.test.* +import zio.test.Assertion.* +import zio.test.TestAspect.* +import zio.test.{TestAspect, ZIOSpecDefault} object JdbcAuthenticationRepositorySpec extends ZIOSpecDefault, PostgresTestContainerSupport { diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorSpec.scala new file mode 100644 index 0000000000..3e824158c7 --- /dev/null +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorSpec.scala @@ -0,0 +1,254 @@ +package io.iohk.atala.iam.authentication.oidc + +import io.iohk.atala.agent.walletapi.crypto.ApolloSpecHelper +import io.iohk.atala.agent.walletapi.model.Wallet +import io.iohk.atala.agent.walletapi.service.WalletManagementService +import io.iohk.atala.agent.walletapi.service.WalletManagementServiceImpl +import io.iohk.atala.agent.walletapi.sql.JdbcWalletNonSecretStorage +import io.iohk.atala.agent.walletapi.sql.JdbcWalletSecretStorage +import io.iohk.atala.iam.authentication.AuthenticationError +import io.iohk.atala.shared.models.WalletId +import io.iohk.atala.sharedtest.containers.KeycloakAdminClient +import io.iohk.atala.sharedtest.containers.KeycloakContainerCustom +import io.iohk.atala.sharedtest.containers.KeycloakTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport +import io.iohk.atala.test.container.DBTestUtils +import org.keycloak.authorization.client.AuthzClient +import org.keycloak.representations.idm.CredentialRepresentation +import org.keycloak.representations.idm.UserRepresentation +import org.keycloak.representations.idm.authorization.ResourceRepresentation +import org.keycloak.representations.idm.authorization.UmaPermissionRepresentation +import zio.* +import zio.http.Client +import zio.test.* +import zio.test.Assertion.* + +import java.net.URI +import scala.jdk.CollectionConverters.* + +object KeycloakAuthenticatorSpec + extends ZIOSpecDefault, + KeycloakTestContainerSupport, + PostgresTestContainerSupport, + ApolloSpecHelper { + + private def keycloakConfigLayer(authUpgradeToRPT: Boolean = true) = + ZLayer.fromZIO { + ZIO.serviceWith[KeycloakContainerCustom] { container => + val host = container.container.getHost() + val port = container.container.getHttpPort() + val url = s"http://${host}:${port}" + KeycloakConfig( + enabled = true, + keycloakUrl = URI(url).toURL(), + realmName = realmName, + clientId = agentClientRepresentation.getClientId(), + clientSecret = agentClientSecret, + autoUpgradeToRPT = authUpgradeToRPT + ) + } + } + + private def createWalletResource(walletId: WalletId, name: String) = + for { + authzClient <- ZIO.service[AuthzClient] + resource = { + val resource = ResourceRepresentation() + resource.setId(walletId.toUUID.toString()) + resource.setName(name) + resource.setOwnerManagedAccess(true) + resource + } + _ <- ZIO + .attemptBlocking(authzClient.protection().resource().create(resource)) + } yield () + + private def createUser(userId: String, password: String, enabled: Boolean = true) = + for { + adminClient <- adminClientZIO + user = { + val cred = CredentialRepresentation() + cred.setTemporary(false) + cred.setValue(password) + + val user = UserRepresentation() + user.setId(userId) + user.setUsername(userId) + user.setEnabled(enabled) + user.setCredentials(List(cred).asJava) + user + } + _ <- ZIO.attemptBlocking(adminClient.realm(realmName).users().create(user)) + } yield () + + private def createResourcePermission(walletId: WalletId, userId: String) = + for { + authzClient <- ZIO.service[AuthzClient] + resourceId = walletId.toUUID.toString() + policy = { + val policy = UmaPermissionRepresentation() + policy.setName(s"${userId} on wallet ${resourceId} permission") + policy.setUsers(Set(userId).asJava) + policy + } + _ <- ZIO.attemptBlocking(authzClient.protection().policy(resourceId).create(policy)) + } yield () + + override def spec = { + val basicSpec = authenticateSpec @@ TestAspect.before(DBTestUtils.runMigrationAgentDB) + val disabledAutoRptSpec = authenticateDisabledAutoRptSpec @@ TestAspect.before(DBTestUtils.runMigrationAgentDB) + + suite("KeycloakAuthenticatorSepc")( + basicSpec + .provide( + KeycloakAuthenticatorImpl.layer, + ZLayer.fromZIO(initializeClient) >>> KeycloakClientImpl.layer ++ KeycloakClientImpl.authzClientLayer, + keycloakConfigLayer(), + keycloakAdminClientLayer, + keycloakContainerLayer, + Client.default, + WalletManagementServiceImpl.layer, + JdbcWalletNonSecretStorage.layer, + JdbcWalletSecretStorage.layer, + contextAwareTransactorLayer, + pgContainerLayer, + apolloLayer + ), + disabledAutoRptSpec + .provide( + KeycloakAuthenticatorImpl.layer, + ZLayer.fromZIO(initializeClient) >>> KeycloakClientImpl.layer ++ KeycloakClientImpl.authzClientLayer, + keycloakConfigLayer(authUpgradeToRPT = false), + keycloakAdminClientLayer, + keycloakContainerLayer, + Client.default, + WalletManagementServiceImpl.layer, + JdbcWalletNonSecretStorage.layer, + JdbcWalletSecretStorage.layer, + contextAwareTransactorLayer, + pgContainerLayer, + apolloLayer + ) + ) + .provide(Runtime.removeDefaultLoggers) + } + + private val authenticateSpec = suite("authenticate")( + test("allow token with a permitted wallet") { + for { + client <- ZIO.service[KeycloakClient] + authenticator <- ZIO.service[KeycloakAuthenticator] + wallet <- ZIO.serviceWithZIO[WalletManagementService](_.createWallet(Wallet("wallet-1"))) + _ <- createWalletResource(wallet.id, "wallet-1") + _ <- createUser("alice", "1234") + _ <- createResourcePermission(wallet.id, "alice") + token <- client.getAccessToken("alice", "1234").map(_.access_token) + entity <- authenticator.authenticate(token) + permittedWallet <- authenticator.authorize(entity) + } yield assert(wallet.id)(equalTo(permittedWallet)) + }, + test("reject token with a wallet that doesn't exist") { + for { + client <- ZIO.service[KeycloakClient] + authenticator <- ZIO.service[KeycloakAuthenticator] + walletId = WalletId.random + _ <- createWalletResource(walletId, "wallet-1") + _ <- createUser("alice", "1234") + _ <- createResourcePermission(walletId, "alice") + token <- client.getAccessToken("alice", "1234").map(_.access_token) + entity <- authenticator.authenticate(token) + exit <- authenticator.authorize(entity).exit + } yield assert(exit)(fails(isSubtype[AuthenticationError.ResourceNotPermitted](anything))) + }, + test("reject token with multiple permitted wallets") { + for { + client <- ZIO.service[KeycloakClient] + authenticator <- ZIO.service[KeycloakAuthenticator] + wallet1 <- ZIO.serviceWithZIO[WalletManagementService](_.createWallet(Wallet("wallet-1"))) + wallet2 <- ZIO.serviceWithZIO[WalletManagementService](_.createWallet(Wallet("wallet-2"))) + _ <- createWalletResource(wallet1.id, "wallet-1") + _ <- createWalletResource(wallet2.id, "wallet-2") + _ <- createUser("alice", "1234") + _ <- createResourcePermission(wallet1.id, "alice") + _ <- createResourcePermission(wallet2.id, "alice") + token <- client.getAccessToken("alice", "1234").map(_.access_token) + entity <- authenticator.authenticate(token) + exit <- authenticator.authorize(entity).exit + } yield assert(exit)( + fails( + isSubtype[AuthenticationError.UnexpectedError]( + hasField("message", _.message, containsString("Too many wallet")) + ) + ) + ) + }, + test("reject malformed token") { + for { + client <- ZIO.service[KeycloakClient] + authenticator <- ZIO.service[KeycloakAuthenticator] + wallet <- ZIO.serviceWithZIO[WalletManagementService](_.createWallet(Wallet("wallet-1"))) + exit1 <- authenticator.authenticate("").exit + exit2 <- authenticator.authenticate("123").exit + } yield assert(exit1)(fails(isSubtype[AuthenticationError.InvalidCredentials](anything))) + && assert(exit2)(fails(isSubtype[AuthenticationError.InvalidCredentials](anything))) + }, + test("reject expired token") { + for { + client <- ZIO.service[KeycloakClient] + authenticator <- ZIO.service[KeycloakAuthenticator] + wallet <- ZIO.serviceWithZIO[WalletManagementService](_.createWallet(Wallet("wallet-1"))) + exit <- authenticator + .authenticate( + "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItTk9wRUNXdXhZMVg2b0ZGeUQyeWMzNzZZa1A5cWttY1JWU3prMjFmbm9rIn0.eyJleHAiOjE2OTc2OTAzNDQsImlhdCI6MTY5NzY5MDA0NCwianRpIjoiOTQ0Yjk0OTctNDA4NS00MjI0LTgxYWUtMjJhNjMwNzRhMmRhIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDozNjUxMy9yZWFsbXMvYXRhbGEtdGVzdCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJkYjQzYzQ2Mi1iMWE1LTQxYzQtYjAxYi00ZWZlNjM5NzUwMjEiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJwcmlzbS1hZ2VudCIsInNlc3Npb25fc3RhdGUiOiIxMmFlOTE4Ny1jYzYzLTQ3ZTItOWNlNC05ZGZhNmE3MmEyN2UiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJkZWZhdWx0LXJvbGVzLWF0YWxhLXRlc3QiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6InByb2ZpbGUgZW1haWwiLCJzaWQiOiIxMmFlOTE4Ny1jYzYzLTQ3ZTItOWNlNC05ZGZhNmE3MmEyN2UiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6ImFsaWNlIn0.GuFycLUFtMqyzk6OyYnB30b8jAAuIcLp60mzbEwppv82B54_ymh1EinU3T_urQLtLh31nWaPM6QU4yBK_mBF1Kbc35eXiEiDZKMwiJhEgXHQboHaiqKNCcoHrT2XjQTg4epT8Gv72a5PzsDw5BiILNDeAQlv-1YArOFSEB9uLjRYVFzE3dSDpd6kWCaISui6c9OaOsHuHJWbxVt9P8HxXA19Xu0HWSITxSHBTXDotwHBFZE32KYIF8aBZtgrZULsc8dyk6pHnDNh0fkPmoWhpgZ4hgakwAyhWURQQ0qloVscaL9joFmLzNsOEZAaN4ML9x1KtWygMYEcUa44Ses03w" + ) + .exit + } yield assert(exit)(fails(isSubtype[AuthenticationError.InvalidCredentials](anything))) + }, + test("reject token with no permitted wallet") { + for { + client <- ZIO.service[KeycloakClient] + authenticator <- ZIO.service[KeycloakAuthenticator] + wallet <- ZIO.serviceWithZIO[WalletManagementService](_.createWallet(Wallet("wallet-1"))) + _ <- createUser("alice", "1234") + token <- client.getAccessToken("alice", "1234").map(_.access_token) + entity <- authenticator.authenticate(token) + exit <- authenticator.authorize(entity).exit + } yield assert(exit)(fails(isSubtype[AuthenticationError.ResourceNotPermitted](anything))) + } + ) + + private val authenticateDisabledAutoRptSpec = suite("authenticate with auto-upgrade RPT disabled")( + test("reject non-RPT token") { + for { + client <- ZIO.service[KeycloakClient] + authenticator <- ZIO.service[KeycloakAuthenticator] + wallet <- ZIO.serviceWithZIO[WalletManagementService](_.createWallet(Wallet("wallet-1"))) + _ <- createUser("alice", "1234") + token <- client.getAccessToken("alice", "1234").map(_.access_token) + entity <- authenticator.authenticate(token) + exit <- authenticator.authorize(entity).exit + } yield assert(exit)( + fails( + isSubtype[AuthenticationError.InvalidCredentials]( + hasField("message", _.message, containsString("not RPT")) + ) + ) + ) + }, + test("accecpt RPT token with a permitted wallet") { + for { + client <- ZIO.service[KeycloakClient] + authenticator <- ZIO.service[KeycloakAuthenticator] + wallet <- ZIO.serviceWithZIO[WalletManagementService](_.createWallet(Wallet("wallet-1"))) + _ <- createWalletResource(wallet.id, "wallet-1") + _ <- createUser("alice", "1234") + _ <- createResourcePermission(wallet.id, "alice") + token <- client.getAccessToken("alice", "1234").map(_.access_token) + rpt <- client.getRpt(token) + entity <- authenticator.authenticate(rpt) + permittedWallet <- authenticator.authorize(entity) + } yield assert(wallet.id)(equalTo(permittedWallet)) + } + ) + +} diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/issue/controller/IssueControllerTestTools.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/issue/controller/IssueControllerTestTools.scala index 5883b36e7e..b45b74358f 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/issue/controller/IssueControllerTestTools.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/issue/controller/IssueControllerTestTools.scala @@ -24,7 +24,7 @@ import io.iohk.atala.pollux.core.repository.{CredentialDefinitionRepositoryInMem import io.iohk.atala.pollux.core.service.* import io.iohk.atala.pollux.vc.jwt.* import io.iohk.atala.shared.models.{WalletAccessContext, WalletId} -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import sttp.client3.testing.SttpBackendStub import sttp.client3.{DeserializationException, Response, UriContext} import sttp.monad.MonadError diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionTestTools.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionTestTools.scala index 9f95169b62..ad4e82a846 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionTestTools.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionTestTools.scala @@ -27,7 +27,7 @@ import io.iohk.atala.pollux.credentialdefinition.http.{ } import io.iohk.atala.pollux.sql.repository.JdbcCredentialDefinitionRepository import io.iohk.atala.shared.models.WalletAccessContext -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import sttp.client3.testing.SttpBackendStub import sttp.client3.ziojson.* import sttp.client3.{DeserializationException, Response, UriContext, basicRequest} diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaTestTools.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaTestTools.scala index 3d75024769..d7e81bccc2 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaTestTools.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaTestTools.scala @@ -20,7 +20,7 @@ import io.iohk.atala.pollux.credentialschema.http.{ } import io.iohk.atala.pollux.sql.repository.JdbcCredentialSchemaRepository import io.iohk.atala.shared.models.WalletAccessContext -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import sttp.client3.testing.SttpBackendStub import sttp.client3.ziojson.* import sttp.client3.{DeserializationException, Response, UriContext, basicRequest} diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala index c474468d18..11bd3cd096 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala @@ -10,14 +10,13 @@ import io.iohk.atala.agent.walletapi.model.error.{ } import io.iohk.atala.agent.walletapi.sql.* import io.iohk.atala.agent.walletapi.storage.* -import io.iohk.atala.agent.walletapi.storage.JdbcEntityRepositorySpec.pgContainerLayer import io.iohk.atala.agent.walletapi.vault.{VaultDIDSecretStorage, VaultWalletSecretStorage} import io.iohk.atala.castor.core.model.did.* import io.iohk.atala.castor.core.model.error import io.iohk.atala.castor.core.service.DIDService import io.iohk.atala.castor.core.util.DIDOperationValidator import io.iohk.atala.shared.models.WalletAccessContext -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.{DBTestUtils, VaultTestContainerSupport} import zio.* import zio.test.* diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceSpec.scala index a5973ecb87..08b07dc688 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceSpec.scala @@ -10,7 +10,7 @@ import io.iohk.atala.agent.walletapi.storage.WalletSecretStorage import io.iohk.atala.agent.walletapi.vault.VaultWalletSecretStorage import io.iohk.atala.shared.models.WalletAccessContext import io.iohk.atala.shared.models.WalletId -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.DBTestUtils import io.iohk.atala.test.container.VaultTestContainerSupport import zio.* diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorageSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorageSpec.scala index 851668899b..0f617075e0 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorageSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorageSpec.scala @@ -1,6 +1,9 @@ package io.iohk.atala.agent.walletapi.storage import io.iohk.atala.agent.walletapi.crypto.ApolloSpecHelper +import io.iohk.atala.agent.walletapi.memory.DIDSecretStorageInMemory +import io.iohk.atala.agent.walletapi.memory.WalletSecretStorageInMemory +import io.iohk.atala.agent.walletapi.model.Wallet import io.iohk.atala.agent.walletapi.service.{WalletManagementService, WalletManagementServiceImpl} import io.iohk.atala.agent.walletapi.sql.{ JdbcDIDNonSecretStorage, @@ -11,14 +14,11 @@ import io.iohk.atala.agent.walletapi.sql.{ import io.iohk.atala.agent.walletapi.vault.{VaultDIDSecretStorage, VaultWalletSecretStorage} import io.iohk.atala.mercury.PeerDID import io.iohk.atala.shared.models.WalletAccessContext -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.{DBTestUtils, VaultTestContainerSupport} import zio.* import zio.test.* import zio.test.Assertion.* -import io.iohk.atala.agent.walletapi.memory.DIDSecretStorageInMemory -import io.iohk.atala.agent.walletapi.memory.WalletSecretStorageInMemory -import io.iohk.atala.agent.walletapi.model.Wallet object DIDSecretStorageSpec extends ZIOSpecDefault, diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorageSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorageSpec.scala index 8c9e343e93..7c3d0328e0 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorageSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorageSpec.scala @@ -7,7 +7,7 @@ import io.iohk.atala.agent.walletapi.service.{WalletManagementService, WalletMan import io.iohk.atala.agent.walletapi.sql.{JdbcGenericSecretStorage, JdbcWalletNonSecretStorage, JdbcWalletSecretStorage} import io.iohk.atala.agent.walletapi.vault.{VaultGenericSecretStorage, VaultWalletSecretStorage} import io.iohk.atala.shared.models.WalletAccessContext -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.{DBTestUtils, VaultTestContainerSupport} import zio.* import zio.json.ast.Json diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcDIDNonSecretStorageSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcDIDNonSecretStorageSpec.scala index 7d69d3a69c..f7c08b81f2 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcDIDNonSecretStorageSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcDIDNonSecretStorageSpec.scala @@ -12,7 +12,7 @@ import io.iohk.atala.castor.core.model.did.PrismDID import io.iohk.atala.castor.core.model.did.PrismDIDOperation import io.iohk.atala.castor.core.model.did.ScheduledDIDOperationStatus import io.iohk.atala.shared.models.WalletAccessContext -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.DBTestUtils import org.postgresql.util.PSQLException import zio.* diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcEntityRepositorySpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcEntityRepositorySpec.scala index b46e47a480..a1da66a1fa 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcEntityRepositorySpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcEntityRepositorySpec.scala @@ -7,12 +7,8 @@ import io.iohk.atala.agent.walletapi.model.error.EntityServiceError.{ } import io.iohk.atala.agent.walletapi.model.{Entity, Wallet} import io.iohk.atala.agent.walletapi.sql.{EntityRepository, JdbcEntityRepository, JdbcWalletNonSecretStorage} -import io.iohk.atala.agent.walletapi.storage.JdbcWalletNonSecretStorageSpec.{ - contextAwareTransactorLayer, - pgContainerLayer -} import io.iohk.atala.shared.models.WalletId -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.DBTestUtils import zio.* import zio.test.* diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcWalletNonSecretStorageSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcWalletNonSecretStorageSpec.scala index 1cd4bd2cfd..b23ad0a2c6 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcWalletNonSecretStorageSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcWalletNonSecretStorageSpec.scala @@ -8,7 +8,7 @@ import io.iohk.atala.agent.walletapi.storage.WalletNonSecretStorageError.Duplica import io.iohk.atala.event.notification.EventNotificationConfig import io.iohk.atala.shared.models.WalletAccessContext import io.iohk.atala.shared.models.WalletId -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.DBTestUtils import zio.* import zio.test.* diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/WalletSecretStorageSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/WalletSecretStorageSpec.scala index a5c53b6283..ca46a9db48 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/WalletSecretStorageSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/WalletSecretStorageSpec.scala @@ -6,7 +6,7 @@ import io.iohk.atala.agent.walletapi.sql.JdbcWalletNonSecretStorage import io.iohk.atala.agent.walletapi.sql.JdbcWalletSecretStorage import io.iohk.atala.agent.walletapi.vault.VaultWalletSecretStorage import io.iohk.atala.shared.models.WalletAccessContext -import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.DBTestUtils import io.iohk.atala.test.container.VaultTestContainerSupport import zio.* diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/test/container/VaultLayer.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/test/container/VaultLayer.scala index 1f36f54ce2..12868fb7af 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/test/container/VaultLayer.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/test/container/VaultLayer.scala @@ -1,8 +1,8 @@ package io.iohk.atala.test.container import zio.* -import io.iohk.atala.shared.test.containers.VaultTestContainer -import io.iohk.atala.shared.test.containers.VaultContainerCustom +import io.iohk.atala.sharedtest.containers.VaultTestContainer +import io.iohk.atala.sharedtest.containers.VaultContainerCustom object VaultLayer { diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/test/container/VaultTestContainerSupport.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/test/container/VaultTestContainerSupport.scala index d053e932aa..c251f09ab9 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/test/container/VaultTestContainerSupport.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/test/container/VaultTestContainerSupport.scala @@ -3,7 +3,7 @@ package io.iohk.atala.test.container import zio.* import io.iohk.atala.agent.walletapi.vault.VaultKVClient import io.iohk.atala.agent.walletapi.vault.VaultKVClientImpl -import io.iohk.atala.shared.test.containers.VaultContainerCustom +import io.iohk.atala.sharedtest.containers.VaultContainerCustom trait VaultTestContainerSupport { diff --git a/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakContainerCustom.scala b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakContainerCustom.scala new file mode 100644 index 0000000000..aefa20a621 --- /dev/null +++ b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakContainerCustom.scala @@ -0,0 +1,39 @@ +package io.iohk.atala.sharedtest.containers + +import com.dimafeng.testcontainers.SingleContainer +import dasniko.testcontainers.keycloak.ExtendableKeycloakContainer +import KeycloakTestContainer.keycloakContainer +import org.testcontainers.utility.DockerImageName +import zio.{TaskLayer, ZIO, ZLayer} + +final class KeycloakContainerCustom( + dockerImageNameOverride: DockerImageName, + isOnGithubRunner: Boolean = false +) extends SingleContainer[ExtendableKeycloakContainer[_]] { + + private val keycloakContainer: ExtendableKeycloakContainer[_] = new ExtendableKeycloakContainer( + dockerImageNameOverride.toString + ) { + override def getHost: String = { + if (isOnGithubRunner) super.getContainerId.take(12) + else super.getHost + } +// override def getMappedPort(originalPort: Int): Integer = { +// if (isOnGithubRunner) 8300 +// else super.getMappedPort(originalPort) +// } + } + + override val container: ExtendableKeycloakContainer[_] = keycloakContainer +} + +object KeycloakContainerCustom { + val layer: TaskLayer[KeycloakContainerCustom] = + ZLayer.scoped { + ZIO + .acquireRelease(ZIO.attemptBlockingIO { + keycloakContainer() + })(container => ZIO.attemptBlockingIO(container.stop()).orDie) + .tap(container => ZIO.attemptBlocking(container.start())) + } +} diff --git a/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainer.scala b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainer.scala new file mode 100644 index 0000000000..27274f589d --- /dev/null +++ b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainer.scala @@ -0,0 +1,22 @@ +package io.iohk.atala.sharedtest.containers + +import org.testcontainers.utility.DockerImageName + +object KeycloakTestContainer { + def keycloakContainer( + imageName: String = "quay.io/keycloak/keycloak:22.0.4", + ): KeycloakContainerCustom = { + val isOnGithubRunner = sys.env.contains("GITHUB_NETWORK") + val container = + new KeycloakContainerCustom( + dockerImageNameOverride = DockerImageName.parse(imageName), + isOnGithubRunner = isOnGithubRunner + ) + + sys.env.get("GITHUB_NETWORK").map { network => + container.container.withNetworkMode(network) + } + + container + } +} diff --git a/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainerSupport.scala b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainerSupport.scala new file mode 100644 index 0000000000..65f4853c46 --- /dev/null +++ b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainerSupport.scala @@ -0,0 +1,53 @@ +package io.iohk.atala.sharedtest.containers + +import org.keycloak.admin.client.Keycloak +import org.keycloak.representations.idm.{ClientRepresentation, RealmRepresentation} +import zio.* + +type KeycloakAdminClient = Keycloak + +trait KeycloakTestContainerSupport { + protected val keycloakContainerLayer: TaskLayer[KeycloakContainerCustom] = + KeycloakContainerCustom.layer + + protected val keycloakAdminClientLayer: URLayer[KeycloakContainerCustom, KeycloakAdminClient] = + ZLayer.fromZIO(ZIO.service[KeycloakContainerCustom].map(_.container.getKeycloakAdminClient)) + + protected val adminClientZIO = ZIO.service[KeycloakAdminClient] + + protected val realmName = "atala-test" + protected val realmRepresentation = { + val rr = new RealmRepresentation() + rr.setRealm(realmName) + rr.setEnabled(true) + rr + } + + protected val agentClientSecret = "prism-agent-demo-secret" + protected val agentClientRepresentation: ClientRepresentation = { + val acr = new ClientRepresentation() + acr.setClientId("prism-agent") + acr.setAuthorizationServicesEnabled(true) + acr.setDirectAccessGrantsEnabled(true) + acr.setServiceAccountsEnabled(true) + acr.setSecret(agentClientSecret) + acr + } + + protected def initializeClient = + for { + adminClient <- adminClientZIO + _ <- ZIO.attemptBlocking( + adminClient + .realms() + .create(realmRepresentation) + ) + _ <- ZIO + .attemptBlocking( + adminClient + .realm(realmRepresentation.getRealm) + .clients() + .create(agentClientRepresentation) + ) + } yield () +} diff --git a/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgreSQLContainerCustom.scala b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/PostgreSQLContainerCustom.scala similarity index 97% rename from shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgreSQLContainerCustom.scala rename to shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/PostgreSQLContainerCustom.scala index 22d54f7f71..b77d3094e6 100644 --- a/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgreSQLContainerCustom.scala +++ b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/PostgreSQLContainerCustom.scala @@ -1,4 +1,4 @@ -package io.iohk.atala.shared.test.containers +package io.iohk.atala.sharedtest.containers import com.dimafeng.testcontainers.{JdbcDatabaseContainer, PostgreSQLContainer} import org.testcontainers.utility.DockerImageName diff --git a/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresLayer.scala b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/PostgresLayer.scala similarity index 90% rename from shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresLayer.scala rename to shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/PostgresLayer.scala index cf9e2608c9..6f35a343ea 100644 --- a/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresLayer.scala +++ b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/PostgresLayer.scala @@ -1,11 +1,11 @@ -package io.iohk.atala.shared.test.containers +package io.iohk.atala.sharedtest.containers import com.dimafeng.testcontainers.PostgreSQLContainer import doobie.util.transactor.Transactor import io.iohk.atala.shared.db.ContextAwareTask import io.iohk.atala.shared.db.DbConfig import io.iohk.atala.shared.db.TransactorLayer -import io.iohk.atala.shared.test.containers.PostgresTestContainer.postgresContainer +import io.iohk.atala.sharedtest.containers.PostgresTestContainer.postgresContainer import zio.* object PostgresLayer { diff --git a/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresTestContainer.scala b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/PostgresTestContainer.scala similarity index 95% rename from shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresTestContainer.scala rename to shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/PostgresTestContainer.scala index 132ab26b92..546b44804f 100644 --- a/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresTestContainer.scala +++ b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/PostgresTestContainer.scala @@ -1,4 +1,4 @@ -package io.iohk.atala.shared.test.containers +package io.iohk.atala.sharedtest.containers import com.dimafeng.testcontainers.PostgreSQLContainer import org.testcontainers.containers.output.OutputFrame diff --git a/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresTestContainerSupport.scala b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/PostgresTestContainerSupport.scala similarity index 97% rename from shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresTestContainerSupport.scala rename to shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/PostgresTestContainerSupport.scala index 30f2310be5..2527765319 100644 --- a/shared/src/main/scala/io/iohk/atala/shared/test/containers/PostgresTestContainerSupport.scala +++ b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/PostgresTestContainerSupport.scala @@ -1,4 +1,4 @@ -package io.iohk.atala.shared.test.containers +package io.iohk.atala.sharedtest.containers import com.dimafeng.testcontainers.PostgreSQLContainer import doobie.util.transactor.Transactor diff --git a/shared/src/main/scala/io/iohk/atala/shared/test/containers/VaultContainerCustom.scala b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/VaultContainerCustom.scala similarity index 96% rename from shared/src/main/scala/io/iohk/atala/shared/test/containers/VaultContainerCustom.scala rename to shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/VaultContainerCustom.scala index 631d7a0103..3500076bfd 100644 --- a/shared/src/main/scala/io/iohk/atala/shared/test/containers/VaultContainerCustom.scala +++ b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/VaultContainerCustom.scala @@ -1,4 +1,4 @@ -package io.iohk.atala.shared.test.containers +package io.iohk.atala.sharedtest.containers import com.dimafeng.testcontainers.{SingleContainer, VaultContainer} import org.testcontainers.vault.{VaultContainer => JavaVaultContainer} diff --git a/shared/src/main/scala/io/iohk/atala/shared/test/containers/VaultTestContainer.scala b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/VaultTestContainer.scala similarity index 94% rename from shared/src/main/scala/io/iohk/atala/shared/test/containers/VaultTestContainer.scala rename to shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/VaultTestContainer.scala index 7af5ce65fc..afb8dcdd7b 100644 --- a/shared/src/main/scala/io/iohk/atala/shared/test/containers/VaultTestContainer.scala +++ b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/VaultTestContainer.scala @@ -1,4 +1,4 @@ -package io.iohk.atala.shared.test.containers +package io.iohk.atala.sharedtest.containers import org.testcontainers.containers.output.OutputFrame import org.testcontainers.utility.DockerImageName diff --git a/shared-test/src/test/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainerSupportSpec.scala b/shared-test/src/test/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainerSupportSpec.scala new file mode 100644 index 0000000000..e1711d6675 --- /dev/null +++ b/shared-test/src/test/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainerSupportSpec.scala @@ -0,0 +1,44 @@ +package io.iohk.atala.sharedtest.containers + +import zio.ZIO +import zio.test.* +import zio.test.TestAspect.* + +import scala.jdk.CollectionConverters.* +import scala.util.Try + +object KeycloakTestContainerSupportSpec extends ZIOSpecDefault with KeycloakTestContainerSupport { + + override def spec = suite("KeycloakTestContainerSupportSpec")( + test("Keycloak container should be started") { + for { + keycloakContainer <- ZIO.service[KeycloakContainerCustom] + } yield assertTrue(keycloakContainer.container.isRunning) + }, + test("Keycloak admin-client is initialized") { + for { + adminClient <- adminClientZIO + usersCount <- ZIO.fromTry(Try(adminClient.realm("master").users().count())) + } yield assertCompletes + }, + test("`atala-test` realm is created") { + for { + adminClient <- adminClientZIO + _ = adminClient.realms().create(realmRepresentation) + realmCreated = adminClient.realms().findAll().asScala.exists(_.getRealm == realmRepresentation.getRealm) + } yield assertTrue(realmCreated) + }, + test("The Agent client is created") { + for { + adminClient <- adminClientZIO + _ = adminClient.realm(realmRepresentation.getRealm).clients().create(agentClientRepresentation) + clientCreated = adminClient + .realm(realmRepresentation.getRealm) + .clients() + .findAll() + .asScala + .exists(_.getClientId == agentClientRepresentation.getClientId) + } yield assertTrue(clientCreated) + } + ).provideLayerShared(keycloakContainerLayer >+> keycloakAdminClientLayer) @@ sequential +}