From a1846aaa84202b55d48ea8556aad8cbbb8260f4d Mon Sep 17 00:00:00 2001 From: Yurii Shynbuiev - IOHK Date: Mon, 6 Nov 2023 13:54:30 +0700 Subject: [PATCH] feat: Keycloak container support with clients and PermissionManagement service (#755) Signed-off-by: Yurii Shynbuiev Signed-off-by: Pat Losoponkul Co-authored-by: Pat Losoponkul --- build.sbt | 40 +++ .../JdbcConnectionRepositorySpec.scala | 2 +- .../container/PostgresTestContainer.scala | 2 +- infrastructure/shared/docker-compose-demo.yml | 1 - ...edentialDefinitionSqlIntegrationSpec.scala | 2 +- .../CredentialSchemaSqlIntegrationSpec.scala | 2 +- ...VerificationPolicySqlIntegrationSpec.scala | 2 +- .../JdbcCredentialRepositorySpec.scala | 2 +- .../JdbcPresentationRepositorySpec.scala | 2 +- .../pollux/vc/jwt/JWTVerificationTest.scala | 2 +- .../src/main/resources/application.conf | 19 ++ .../atala/agent/server/config/AppConfig.scala | 2 + .../authentication/oidc/KeycloakClient.scala | 55 +++- .../core/PermissionManagement.scala | 35 +++ .../keycloak/admin/KeycloakAdmin.scala | 64 +++++ .../KeycloakPermissionManagementService.scala | 163 +++++++++++ .../server/AgentInitializationSpec.scala | 2 +- .../authentication/SecurityLogicSpec.scala | 69 +++++ .../apikey/ApiKeyAuthenticatorSpec.scala | 2 +- .../JdbcAuthenticationRepositorySpec.scala | 12 +- .../oidc/KeycloakAuthenticatorSpec.scala | 254 ++++++++++++++++++ .../keycloak/admin/KeycloakAdminSpec.scala | 28 ++ .../keycloak/admin/KeycloakConfigUtils.scala | 48 ++++ ...cloakPermissionManagementServiceSpec.scala | 155 +++++++++++ .../controller/IssueControllerTestTools.scala | 2 +- .../CredentialDefinitionTestTools.scala | 2 +- .../schema/CredentialSchemaTestTools.scala | 2 +- .../service/ManagedDIDServiceSpec.scala | 3 +- .../service/WalletManagementServiceSpec.scala | 2 +- .../storage/DIDSecretStorageSpec.scala | 8 +- .../storage/GenericSecretStorageSpec.scala | 2 +- .../storage/JdbcDIDNonSecretStorageSpec.scala | 2 +- .../storage/JdbcEntityRepositorySpec.scala | 6 +- .../JdbcWalletNonSecretStorageSpec.scala | 2 +- .../storage/WalletSecretStorageSpec.scala | 2 +- .../atala/test/container/VaultLayer.scala | 4 +- .../container/VaultTestContainerSupport.scala | 2 +- .../containers/KeycloakContainerCustom.scala | 40 +++ .../containers/KeycloakTestContainer.scala | 22 ++ .../KeycloakTestContainerSupport.scala | 106 ++++++++ .../PostgreSQLContainerCustom.scala | 2 +- .../containers/PostgresLayer.scala | 8 +- .../containers/PostgresTestContainer.scala | 2 +- .../PostgresTestContainerSupport.scala | 2 +- .../containers/VaultContainerCustom.scala | 2 +- .../containers/VaultTestContainer.scala | 2 +- .../KeycloakTestContainerSupportSpec.scala | 43 +++ 47 files changed, 1180 insertions(+), 53 deletions(-) create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/core/PermissionManagement.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakAdmin.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementService.scala create mode 100644 prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/SecurityLogicSpec.scala create mode 100644 prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorSpec.scala create mode 100644 prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakAdminSpec.scala create mode 100644 prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakConfigUtils.scala create mode 100644 prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementServiceSpec.scala create mode 100644 shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakContainerCustom.scala create mode 100644 shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainer.scala create mode 100644 shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainerSupport.scala rename {shared/src/main/scala/io/iohk/atala/shared/test => shared-test/src/main/scala/io/iohk/atala/sharedtest}/containers/PostgreSQLContainerCustom.scala (97%) rename {shared/src/main/scala/io/iohk/atala/shared/test => shared-test/src/main/scala/io/iohk/atala/sharedtest}/containers/PostgresLayer.scala (81%) rename {shared/src/main/scala/io/iohk/atala/shared/test => shared-test/src/main/scala/io/iohk/atala/sharedtest}/containers/PostgresTestContainer.scala (95%) rename {shared/src/main/scala/io/iohk/atala/shared/test => shared-test/src/main/scala/io/iohk/atala/sharedtest}/containers/PostgresTestContainerSupport.scala (97%) rename {shared/src/main/scala/io/iohk/atala/shared/test => shared-test/src/main/scala/io/iohk/atala/sharedtest}/containers/VaultContainerCustom.scala (96%) rename {shared/src/main/scala/io/iohk/atala/shared/test => shared-test/src/main/scala/io/iohk/atala/sharedtest}/containers/VaultTestContainer.scala (94%) create mode 100644 shared-test/src/test/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainerSupportSpec.scala diff --git a/build.sbt b/build.sbt index 59ef51fd9e..69304ca23d 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,18 @@ lazy val shared = (project in file("shared")) ) .enablePlugins(BuildInfoPlugin) +lazy val sharedTest = (project in file("shared-test")) + .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 ### // ######################### @@ -676,6 +711,7 @@ lazy val polluxDoobie = project ) .dependsOn(polluxCore % "compile->compile;test->test") .dependsOn(shared) + .dependsOn(sharedTest % Test) // ######################## // ### Pollux Anoncreds ### @@ -725,6 +761,7 @@ lazy val connectDoobie = project libraryDependencies ++= D_Connect.sqlDoobieDependencies ) .dependsOn(shared) + .dependsOn(sharedTest % Test) .dependsOn(connectCore % "compile->compile;test->test") // ############################ @@ -760,6 +797,7 @@ lazy val prismAgentWalletAPI = project castorCore, eventNotification ) + .dependsOn(sharedTest % Test) lazy val prismAgentServer = project .in(file("prism-agent/service/server")) @@ -792,6 +830,7 @@ lazy val prismAgentServer = project castorCore, eventNotification ) + .dependsOn(sharedTest % Test) // ############################ // #### Release process ##### @@ -809,6 +848,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 a6ab6e0625..723cb82023 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: 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/resources/application.conf b/prism-agent/service/server/src/main/resources/application.conf index f220464738..cfdf89342c 100644 --- a/prism-agent/service/server/src/main/resources/application.conf +++ b/prism-agent/service/server/src/main/resources/application.conf @@ -219,3 +219,22 @@ agent { authApiKey = ${?DEFAULT_WALLET_AUTH_API_KEY} } } + +keycloakAdmin { + serverUrl = "http://localhost:8080/auth", + serverUrl = ${?KEYCLOAK_SERVER_URL} + realm = "master", + realm = ${?KEYCLOAK_REALM} + username = "admin", + username = ${?KEYCLOAK_ADMIN_USERNAME} + password = "admin", + password = ${?KEYCLOAK_ADMIN_PASSWORD} + clientId = "admin-cli", + clientId = ${?KEYCLOAK_ADMIN_CLIENT_ID} + clientSecret = "", + clientSecret = ${?KEYCLOAK_ADMIN_CLIENT_SECRET} + authToken= "", + authToken = ${?KEYCLOAK_ADMIN_AUTH_TOKEN} + scope= "" + scope = ${?KEYCLOAK_ADMIN_SCOPE} +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala index b13225e9ea..7f461f72ff 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala @@ -2,6 +2,7 @@ package io.iohk.atala.agent.server.config import io.iohk.atala.castor.core.model.did.VerificationRelationship import io.iohk.atala.iam.authentication.AuthenticationConfig +import io.iohk.atala.iam.authorization.keycloak.admin.KeycloakAdminConfig import io.iohk.atala.pollux.vc.jwt.* import io.iohk.atala.shared.db.DbConfig import zio.config.* @@ -17,6 +18,7 @@ final case class AppConfig( agent: AgentConfig, connect: ConnectConfig, prismNode: PrismNodeConfig, + keycloakAdmin: KeycloakAdminConfig ) { def validate: Either[String, Unit] = for { 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/main/scala/io/iohk/atala/iam/authorization/core/PermissionManagement.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/core/PermissionManagement.scala new file mode 100644 index 0000000000..d745111ce8 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/core/PermissionManagement.scala @@ -0,0 +1,35 @@ +package io.iohk.atala.iam.authorization.core + +import io.iohk.atala.shared.models.WalletId +import zio.IO + +import java.util.UUID + +object PermissionManagement { + trait Service { + def grantWalletToUser(walletId: WalletId, userId: UUID): IO[Error, Unit] + def revokeWalletFromUser(walletId: WalletId, userId: UUID): IO[Error, Unit] + } + + trait Error(message: String) + + object Error { + case class UserNotFoundById(userId: UUID, cause: Option[Throwable] = None) + extends Error(s"User $userId is not found" + cause.map(t => s" Cause: ${t.getMessage}")) + case class WalletNotFoundByUserId(userId: UUID) extends Error(s"Wallet for user $userId is not found") + + case class WalletNotFoundById(walletId: WalletId) extends Error(s"Wallet not found by ${walletId.toUUID}") + + case class WalletResourceNotFoundById(walletId: WalletId) + extends Error(s"Wallet resource not found by ${walletId.toUUID}") + + case class PermissionNotFoundById(userId: UUID, walletId: WalletId, walletResourceId: String) + extends Error( + s"Permission not found by userId: $userId, walletId: ${walletId.toUUID}, walletResourceId: $walletResourceId" + ) + + case class UnexpectedError(cause: Throwable) extends Error(cause.getMessage) + + case class ServiceError(message: String) extends Error(message) + } +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakAdmin.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakAdmin.scala new file mode 100644 index 0000000000..189cff32a0 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakAdmin.scala @@ -0,0 +1,64 @@ +package io.iohk.atala.iam.authorization.keycloak.admin + +import io.iohk.atala.agent.server.config.AppConfig +import org.keycloak.admin.client.Keycloak +import zio.{RLayer, Task, TaskLayer, URLayer, ZIO, ZLayer} + +import scala.util.{Failure, Try} + +type KeycloakAdmin = Keycloak + +// The following arguments are available: +// serverUrl: String, +// realm: String, +// username: String, +// password: String, +// clientId: String, +// clientSecret: String, +// sslContext: SSLContext, +// customJacksonProvider: AnyRef, - should be skipped +// disableTrustManager: Boolean, - false by default +// authToken: String, - can be skipped +// scope: String - can be skipped +// TODO: Ssl context is not supported yet +case class KeycloakAdminConfig( + serverUrl: String, + realm: String, + username: String, + password: String, + clientId: String, + clientSecret: Option[String], + authToken: Option[String], + scope: Option[String] +) { + def isHttps: Boolean = serverUrl.startsWith("https") +} + +object KeycloakAdminConfig { + val layer: URLayer[AppConfig, KeycloakAdminConfig] = + ZLayer.fromFunction((appConfig: AppConfig) => appConfig.keycloakAdmin) +} + +object KeycloakAdmin { + + def apply(config: KeycloakAdminConfig): Task[KeycloakAdmin] = { + if (config.isHttps) + ZIO.fail(new Exception("Ssl is not supported yet")) + else + ZIO.fromTry[KeycloakAdmin]( + Try[KeycloakAdmin]( + Keycloak.getInstance( + config.serverUrl, + config.realm, + config.username, + config.password, + config.clientId, + config.clientSecret.orNull + ) + ) + ) + } + + val layer: RLayer[KeycloakAdminConfig, KeycloakAdmin] = + ZLayer.fromZIO(ZIO.service[KeycloakAdminConfig].flatMap(config => apply(config))) +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementService.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementService.scala new file mode 100644 index 0000000000..334b0d7483 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementService.scala @@ -0,0 +1,163 @@ +package io.iohk.atala.iam.authorization.keycloak.admin + +import io.iohk.atala.agent.walletapi.service.WalletManagementService +import io.iohk.atala.iam.authorization.core.PermissionManagement +import io.iohk.atala.iam.authorization.core.PermissionManagement.Error.* +import io.iohk.atala.shared.models.WalletId +import org.keycloak.authorization.client.AuthzClient +import org.keycloak.representations.idm.authorization.{ResourceRepresentation, UmaPermissionRepresentation} +import zio.ZIO.* +import zio.ZLayer.* +import zio.{IO, Task, URLayer, ZIO, ZLayer} + +import java.util.UUID +import scala.jdk.CollectionConverters.* + +case class KeycloakPermissionManagementService( + authzClient: AuthzClient, + walletManagementService: WalletManagementService +) extends PermissionManagement.Service { + + private def walletResourceName(walletId: WalletId) = s"wallet-${walletId.toUUID.toString}" + + private def policyName(userId: String, resourceId: String) = s"user $userId on wallet $resourceId permission" + + override def grantWalletToUser(walletId: WalletId, userId: UUID): IO[PermissionManagement.Error, Unit] = { + for { + walletOpt <- walletManagementService + .getWallet(walletId) + .mapError(wmse => ServiceError(wmse.toThrowable.getMessage)) + + wallet <- ZIO + .fromOption(walletOpt) + .orElseFail(WalletNotFoundById(walletId)) + + walletResourceOpt <- findWalletResource(walletId) + .logError("Error while finding wallet resource") + .mapError(UnexpectedError.apply) + + walletResource <- ZIO + .fromOption(walletResourceOpt) + .orElse(createWalletResource(walletId)) + .logError("Error while creating wallet resource") + .mapError(UnexpectedError.apply) + _ <- ZIO.log(s"Wallet resource created ${walletResource.toString}") + + permission <- createResourcePermission(walletResource.getId, userId.toString) + .mapError(UnexpectedError.apply) + + _ <- ZIO.log(s"Permission created with id ${permission.getId} and name ${permission.getName}") + } yield () + } + + private def permissionDetails(permission: UmaPermissionRepresentation): String = { + s""" + |id: ${permission.getId} + |name: ${permission.getName} + |scopes: ${permission.getScopes.asScala.mkString(", ")} + |users: ${permission.getUsers.asScala.mkString(", ")} + |""".stripMargin + } + + private def createResourcePermission(resourceId: String, userId: String): Task[UmaPermissionRepresentation] = { + val policy = UmaPermissionRepresentation() + policy.setName(policyName(userId, resourceId)) + policy.setUsers(Set(userId).asJava) + + for { + umaPermissionRepresentation <- ZIO.attemptBlocking( + authzClient + .protection() + .policy(resourceId) + .create(policy) + ) + } yield umaPermissionRepresentation + } + + private def findWalletResource(walletId: WalletId): Task[Option[ResourceRepresentation]] = { + for { + walletResourceOrNull <- ZIO.attemptBlocking( + authzClient.protection().resource().findByName(walletResourceName(walletId)) + ) + } yield Option(walletResourceOrNull) + } + + private def createWalletResource(walletId: WalletId): Task[ResourceRepresentation] = { + val walletResource = ResourceRepresentation() + walletResource.setId(walletId.toUUID.toString) + walletResource.setUris(Set(s"/wallets/${walletResourceName(walletId)}").asJava) + walletResource.setName(walletResourceName(walletId)) + walletResource.setOwnerManagedAccess(true) + + for { + _ <- ZIO.log(s"Creating resource for the wallet ${walletId.toUUID.toString}") + response <- ZIO.attemptBlocking( + authzClient + .protection() + .resource() + .create(walletResource) + ) + resource <- ZIO.attemptBlocking( + authzClient + .protection() + .resource() + .findById(walletResource.getId) + ) + _ <- ZIO.log(s"Resource for the wallet created id: ${resource.getId}, name ${resource.getName}") + } yield resource + } + + override def revokeWalletFromUser(walletId: WalletId, userId: UUID): IO[PermissionManagement.Error, Unit] = { + for { + walletResourceOpt <- findWalletResource(walletId) + .logError("Error while finding wallet resource") + .mapError(UnexpectedError.apply) + + walletResource <- ZIO + .fromOption(walletResourceOpt) + .orElseFail(WalletResourceNotFoundById(walletId)) + + permissionOpt <- ZIO + .attemptBlocking( + authzClient + .protection() + .policy(walletResource.getId) + .find( + policyName(userId.toString, walletResource.getId), + null, + 0, + 1 + ) + ) + .map(_.asScala.headOption) + .logError(s"Error while finding permission by name ${policyName(userId.toString, walletResource.getId)}") + .mapError(UnexpectedError.apply) + + permission <- ZIO + .fromOption(permissionOpt) + .orElseFail(PermissionNotFoundById(userId, walletId, walletResource.getId)) + + _ <- ZIO + .attemptBlocking( + authzClient + .protection() + .policy(walletResource.getId) + .delete(permission.getId) + ) + .logError(s"Error while deleting permission ${permission.getId}") + .mapError(UnexpectedError.apply) + + _ <- ZIO.log( + s"Permission ${permission.getId} deleted for user ${userId.toString} and wallet ${walletResource.getId}" + ) + } yield () + } +} + +object KeycloakPermissionManagementService { + val layer: URLayer[ + AuthzClient & WalletManagementService, + PermissionManagement.Service + ] = + ZLayer.fromFunction(KeycloakPermissionManagementService(_, _)) +} 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/iam/authorization/keycloak/admin/KeycloakAdminSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakAdminSpec.scala new file mode 100644 index 0000000000..2f98cb9ef4 --- /dev/null +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakAdminSpec.scala @@ -0,0 +1,28 @@ +package io.iohk.atala.iam.authorization.keycloak.admin + +import io.iohk.atala.sharedtest.containers.{KeycloakContainerCustom, KeycloakTestContainerSupport} +import zio.* +import zio.ZIO.* +import zio.test.* +import zio.test.Assertion.equalTo +import zio.test.TestAspect.* + +import scala.util.Try + +object KeycloakAdminSpec extends ZIOSpecDefault with KeycloakTestContainerSupport with KeycloakConfigUtils { + + override def spec = suite("KeycloakAdminSpec")( + test("KeycloakAdmin can be created from the container") { + for { + keycloakContainer <- ZIO.service[KeycloakContainerCustom] + config <- ZIO.service[KeycloakAdminConfig] + + keycloakAdmin <- KeycloakAdmin(config) + usersCount <- ZIO.fromTry( + Try(keycloakAdmin.realm("master").users().count()).map(_.toInt) + ) + } yield assertTrue(keycloakContainer.container.isRunning) && assert(usersCount)(equalTo(1)) + } + ).provideLayerShared(keycloakContainerLayer >+> keycloakAdminConfigLayer) @@ sequential + +} diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakConfigUtils.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakConfigUtils.scala new file mode 100644 index 0000000000..4beb70ca7e --- /dev/null +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakConfigUtils.scala @@ -0,0 +1,48 @@ +package io.iohk.atala.iam.authorization.keycloak.admin + +import io.iohk.atala.iam.authentication.oidc.KeycloakConfig +import io.iohk.atala.sharedtest.containers.{KeycloakContainerCustom, KeycloakTestContainerSupport} +import zio.* +import zio.ZIO.* +import zio.test.* + +import java.net.URI + +trait KeycloakConfigUtils { + this: KeycloakTestContainerSupport => + + protected def keycloakAdminConfig: RIO[KeycloakContainerCustom, KeycloakAdminConfig] = + for { + keycloakContainer <- ZIO.service[KeycloakContainerCustom] + keycloakAdminConfig = KeycloakAdminConfig( + serverUrl = keycloakContainer.container.getAuthServerUrl, + realm = "master", + username = keycloakContainer.container.getAdminUsername, + password = keycloakContainer.container.getAdminPassword, + clientId = "admin-cli", + clientSecret = Option.empty, + authToken = Option.empty, + scope = Option.empty + ) + } yield keycloakAdminConfig + + protected val keycloakAdminConfigLayer = ZLayer.fromZIO(keycloakAdminConfig) + + protected 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 + ) + } + } + +} diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementServiceSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementServiceSpec.scala new file mode 100644 index 0000000000..d37ee2b5bf --- /dev/null +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementServiceSpec.scala @@ -0,0 +1,155 @@ +package io.iohk.atala.iam.authorization.keycloak.admin + +import io.iohk.atala.agent.walletapi.model.{Wallet, WalletSeed} +import io.iohk.atala.agent.walletapi.service.{WalletManagementService, WalletManagementServiceError} +import io.iohk.atala.event.notification.EventNotificationConfig +import io.iohk.atala.iam.authentication.AuthenticationError.ResourceNotPermitted +import io.iohk.atala.iam.authentication.oidc.{ + KeycloakAuthenticator, + KeycloakAuthenticatorImpl, + KeycloakClient, + KeycloakClientImpl +} +import io.iohk.atala.iam.authorization.core.PermissionManagement +import io.iohk.atala.iam.authorization.core.PermissionManagement.Error.WalletNotFoundById +import io.iohk.atala.shared.models.{WalletAccessContext, WalletId} +import io.iohk.atala.sharedtest.containers.{KeycloakContainerCustom, KeycloakTestContainerSupport} +import zio.* +import zio.ZIO.* +import zio.http.Client +import zio.test.* +import zio.test.Assertion.* +import zio.test.TestAspect.* + +import java.util.UUID +import scala.util.Try + +object KeycloakPermissionManagementServiceSpec + extends ZIOSpecDefault + with KeycloakTestContainerSupport + with KeycloakConfigUtils { + + override def spec = suite("KeycloakPermissionManagementServiceSpec")( + successfulCasesSuite, + failureCasesSuite + ) + + val successfulCasesSuite = suite("Successful Cases")( + test("grant wallet access to the user") { + for { + client <- ZIO.service[KeycloakClient] + authenticator <- ZIO.service[KeycloakAuthenticator] + walletService <- ZIO.service[WalletManagementService] + + wallet <- walletService.createWallet(Wallet("test_1")) + + randomId = UUID.randomUUID().toString + username = "user_" + randomId + password = randomId + user <- createUser(username = username, password = password) + + permissionService <- ZIO.service[PermissionManagement.Service] + _ <- permissionService.grantWalletToUser(wallet.id, UUID.fromString(user.getId)) + + token <- client.getAccessToken(username, password).map(_.access_token) + + entity <- authenticator.authenticate(token) + permittedWallet <- authenticator.authorize(entity) + } yield assert(wallet.id)(equalTo(permittedWallet)) + }, + test("revoke the wallet access from the user") { + for { + client <- ZIO.service[KeycloakClient] + authenticator <- ZIO.service[KeycloakAuthenticator] + walletService <- ZIO.service[WalletManagementService] + + wallet <- walletService.createWallet(Wallet("test_2")) + + randomId = UUID.randomUUID().toString + username = "user_" + randomId + password = randomId + user <- createUser(username = username, password = password) + + permissionService <- ZIO.service[PermissionManagement.Service] + _ <- permissionService.grantWalletToUser(wallet.id, UUID.fromString(user.getId)) + + token <- client.getAccessToken(username, password).map(_.access_token) + + entity <- authenticator.authenticate(token) + permittedWallet <- authenticator.authorize(entity) + + _ <- permissionService.revokeWalletFromUser(wallet.id, UUID.fromString(user.getId)) + + token2 <- client.getAccessToken(username, password).map(_.access_token) + entity2 <- authenticator.authenticate(token) + permittedWallet2 <- authenticator.authorize(entity).exit + + } yield assert(permittedWallet2)(fails(isSubtype[ResourceNotPermitted](anything))) + } + ).provide( + Client.default, + keycloakContainerLayer, + keycloakAdminConfigLayer, + KeycloakAdmin.layer, + KeycloakPermissionManagementService.layer, + WalletManagementServiceStub.layer, + KeycloakAuthenticatorImpl.layer, + ZLayer.fromZIO(initializeClient) >>> KeycloakClientImpl.layer ++ KeycloakClientImpl.authzClientLayer, + keycloakConfigLayer() + ) @@ sequential + + val failureCasesSuite = suite("Failure Cases Suite")( + test("grant wallet access to the user with invalid wallet id") { + for { + permissionService <- ZIO.service[PermissionManagement.Service] + exit <- permissionService.grantWalletToUser(WalletId.random, UUID.randomUUID()).exit + } yield assert(exit)(fails(isSubtype[WalletNotFoundById](anything))) + } + ).provide( + Client.default, + keycloakContainerLayer, + keycloakAdminConfigLayer, + KeycloakAdmin.layer, + KeycloakPermissionManagementService.layer, + WalletManagementServiceStub.layer, + ZLayer.fromZIO(initializeClient) >>> KeycloakClientImpl.layer ++ KeycloakClientImpl.authzClientLayer, + keycloakConfigLayer() + ) @@ sequential +} + +class WalletManagementServiceStub extends WalletManagementService { + private var wallets: Map[WalletId, Wallet] = Map.empty + override def createWallet(wallet: Wallet, seed: Option[WalletSeed]): IO[WalletManagementServiceError, Wallet] = { + val wallet = Wallet(name = "test") + wallets = wallets + (wallet.id -> wallet) + ZIO.succeed(wallet) + } + + override def getWallet(walletId: WalletId): IO[WalletManagementServiceError, Option[Wallet]] = { + ZIO.succeed(wallets.get(walletId)) + } + + override def getWallets(walletIds: Seq[WalletId]): IO[WalletManagementServiceError, Seq[Wallet]] = { + ZIO.succeed(wallets.filter(w => walletIds.contains(w._1)).values.toSeq) + } + + override def listWallets( + offset: Option[RuntimeFlags], + limit: Option[RuntimeFlags] + ): IO[WalletManagementServiceError, (Seq[Wallet], RuntimeFlags)] = ??? + + override def listWalletNotifications + : ZIO[WalletAccessContext, WalletManagementServiceError, Seq[EventNotificationConfig]] = ??? + + override def createWalletNotification( + config: EventNotificationConfig + ): ZIO[WalletAccessContext, WalletManagementServiceError, EventNotificationConfig] = ??? + + override def deleteWalletNotification( + id: _root_.java.util.UUID + ): ZIO[WalletAccessContext, WalletManagementServiceError, Unit] = ??? +} + +object WalletManagementServiceStub { + val layer = ZLayer.succeed(new WalletManagementServiceStub) +} 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 3ffba16d91..a643ed17a0 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 @@ -22,7 +22,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..f39af6f44c --- /dev/null +++ b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakContainerCustom.scala @@ -0,0 +1,40 @@ +package io.iohk.atala.sharedtest.containers + +import com.dimafeng.testcontainers.SingleContainer +import dasniko.testcontainers.keycloak.ExtendableKeycloakContainer +import io.iohk.atala.sharedtest.containers.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) 8080 + 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..b009def33a --- /dev/null +++ b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainerSupport.scala @@ -0,0 +1,106 @@ +package io.iohk.atala.sharedtest.containers +import org.keycloak.admin.client.Keycloak +import org.keycloak.representations.idm.{ + ClientRepresentation, + CredentialRepresentation, + RealmRepresentation, + UserRepresentation +} +import zio.* +import zio.ZIO.attemptBlocking +import zio.test.TestAspect.beforeAll +import zio.test.TestAspectAtLeastR + +import java.util.UUID +import scala.jdk.CollectionConverters.* + +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 () + + def bootstrapKeycloakRealm = adminClientZIO.flatMap(keycloak => + ZIO.attemptBlocking { + keycloak.realms().create(realmRepresentation) + keycloak.realm(realmName).clients().create(agentClientRepresentation) + () + } + ) + + def bootstrapKeycloakRealmAspect: TestAspectAtLeastR[KeycloakAdminClient] = { + val run = for { + _ <- ZIO.log("Bootstrapping the Keycloak realm...") + _ <- bootstrapKeycloakRealm + _ <- ZIO.log("Bootstrap finished") + } yield () + + beforeAll(run.orDie) + } + + def createUser(username: String, password: String): ZIO[KeycloakAdminClient, Throwable, UserRepresentation] = + val userRepresentation = { + val creds = new CredentialRepresentation() + creds.setTemporary(false) + creds.setValue(password) + + val ur = new UserRepresentation() + ur.setId(UUID.nameUUIDFromBytes(username.getBytes).toString) + ur.setUsername(username) + ur.setEnabled(true) + ur.setCredentials(List(creds).asJava) + ur + } + + for { + adminClient <- adminClientZIO + users = adminClient.realm(realmName).users() + _ <- ZIO.log(s"Creating user ${userRepresentation.getId}") + _ <- attemptBlocking(users.create(userRepresentation)) + createdUser <- attemptBlocking(users.search(username).asScala.head) + _ <- ZIO.log(s"Created user ${createdUser.getId}") + } yield createdUser +} 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 81% 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..0a7b3912eb 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,9 @@ -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.shared.db.{ContextAwareTask, DbConfig, TransactorLayer} +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..46722b6641 --- /dev/null +++ b/shared-test/src/test/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainerSupportSpec.scala @@ -0,0 +1,43 @@ +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 +}