From 3e7534ff1a75e9ecaa0c2b670c1c158890021f8d Mon Sep 17 00:00:00 2001 From: patlo-iog Date: Mon, 16 Oct 2023 19:42:00 +0700 Subject: [PATCH] feat(prism-agent): add keycloak authorization support to endpoints (#753) Signed-off-by: Pat Losoponkul --- build.sbt | 27 ++-- .../shared/docker-compose-mt-keycloak.yml | 25 ++-- infrastructure/shared/keycloak/init-script.sh | 34 +++++- .../src/main/resources/application.conf | 48 ++++++-- .../io/iohk/atala/agent/server/Main.scala | 11 +- .../io/iohk/atala/agent/server/Modules.scala | 64 ++++++++-- .../atala/agent/server/config/AppConfig.scala | 25 +++- .../agent/server/http/ZHttpEndpoints.scala | 20 ++- .../controller/DIDRegistrarEndpoints.scala | 15 ++- .../DIDRegistrarServerEndpoints.scala | 47 +++---- .../controller/ConnectionEndpoints.scala | 15 ++- .../ConnectionServerEndpoints.scala | 39 +++--- .../event/controller/EventEndpoints.scala | 9 +- .../controller/EventServerEndpoints.scala | 29 +++-- .../authentication/AuthenticationConfig.scala | 12 +- .../iam/authentication/Authenticator.scala | 30 ++++- .../authentication/DefaultAuthenticator.scala | 40 +++--- .../iam/authentication/SecurityLogic.scala | 57 +++++++++ .../admin/AdminApiKeyAuthenticator.scala | 12 +- .../admin/AdminApiKeyCredentials.scala | 6 +- .../admin/AdminApiKeySecurityLogic.scala | 12 +- .../authentication/admin/AdminConfig.scala | 4 +- .../iam/authentication/admin/package.scala | 2 +- .../apikey/ApiKeyAuthenticator.scala | 10 +- .../authentication/apikey/ApiKeyConfig.scala | 6 +- .../apikey/ApiKeyEndpointSecurityLogic.scala | 17 ++- .../DefaultEntityApiKeyAuthenticator.scala | 20 --- .../authentication/oidc/JwtCredentials.scala | 12 ++ .../oidc/JwtSecurityLogic.scala | 12 ++ .../oidc/KeycloakAuthenticator.scala | 33 +++++ .../oidc/KeycloakAuthenticatorImpl.scala | 103 ++++++++++++++++ .../authentication/oidc/KeycloakClient.scala | 115 ++++++++++++++++++ .../authentication/oidc/KeycloakConfig.scala | 19 +++ .../entity/http/EntityServerEndpoints.scala | 27 ++-- .../WalletManagementServerEndpoints.scala | 14 ++- .../issue/controller/IssueEndpoints.scala | 17 ++- .../controller/IssueServerEndpoints.scala | 44 ++++--- ...redentialDefinitionRegistryEndpoints.scala | 8 +- ...ialDefinitionRegistryServerEndpoints.scala | 24 ++-- .../SchemaRegistryEndpoints.scala | 16 ++- .../SchemaRegistryServerEndpoints.scala | 41 ++++--- .../VerificationPolicyEndpoints.scala | 17 ++- .../VerificationPolicyServerEndpoints.scala | 44 ++++--- .../controller/PresentProofEndpoints.scala | 15 ++- .../PresentProofServerEndpoints.scala | 35 +++--- .../server/AgentInitializationSpec.scala | 20 ++- .../iohk/atala/api/util/Tapir2StaticOAS.scala | 4 +- .../controller/IssueControllerImplSpec.scala | 5 +- .../controller/IssueControllerTestTools.scala | 6 +- .../CredentialDefinitionBasicSpec.scala | 5 +- .../CredentialDefinitionFailureSpec.scala | 5 +- ...ialDefinitionLookupAndPaginationSpec.scala | 11 +- .../CredentialDefinitionTestTools.scala | 19 ++- .../schema/CredentialSchemaAnoncredSpec.scala | 9 +- .../schema/CredentialSchemaBasicSpec.scala | 5 +- .../schema/CredentialSchemaFailureSpec.scala | 7 +- ...dentialSchemaLookupAndPaginationSpec.scala | 11 +- .../schema/CredentialSchemaTestTools.scala | 14 ++- .../atala/agent/walletapi/model/Entity.scala | 6 +- .../service/WalletManagementService.scala | 2 + .../service/WalletManagementServiceImpl.scala | 5 + .../sql/JdbcWalletNonSecretStorage.scala | 26 ++++ .../storage/WalletNonSecretStorage.scala | 1 + .../agent/walletapi/util/SeedResolver.scala | 69 ----------- 64 files changed, 1018 insertions(+), 444 deletions(-) create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/SecurityLogic.scala delete mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/DefaultEntityApiKeyAuthenticator.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/JwtCredentials.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/JwtSecurityLogic.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticator.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorImpl.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakConfig.scala delete mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/SeedResolver.scala diff --git a/build.sbt b/build.sbt index 0702ffe290..11d6307fd0 100644 --- a/build.sbt +++ b/build.sbt @@ -82,6 +82,9 @@ lazy val V = new { val vaultDriver = "6.1.0" val micrometer = "1.11.2" + + val nimbusJwt = "10.0.0" + val keycloak = "22.0.4" } /** Dependencies */ @@ -106,13 +109,16 @@ lazy val D = new { val circeGeneric: ModuleID = "io.circe" %% "circe-generic" % V.circe val circeParser: ModuleID = "io.circe" %% "circe-parser" % V.circe + val jwtCirce = "com.github.jwt-scala" %% "jwt-circe" % V.jwtCirceVersion + // https://mvnrepository.com/artifact/org.didcommx/didcomm/0.3.2 val didcommx: ModuleID = "org.didcommx" % "didcomm" % "0.3.1" val peerDidcommx: ModuleID = "org.didcommx" % "peerdid" % "0.3.0" val didScala: ModuleID = "app.fmgp" %% "did" % "0.0.0+113-61efa271-SNAPSHOT" + // Customized version of numbus jose jwt // from https://github.com/goncalo-frade-iohk/Nimbus-JWT_Fork/commit/8a6665c25979e771afae29ce8c965c8b0312fefb - val jwk: ModuleID = "io.iohk.atala" % "nimbus-jose-jwt" % "10.0.0" + val nimbusJwt: ModuleID = "io.iohk.atala" % "nimbus-jose-jwt" % V.nimbusJwt val typesafeConfig: ModuleID = "com.typesafe" % "config" % V.typesafeConfig val scalaPbRuntime: ModuleID = @@ -268,8 +274,6 @@ lazy val D_Pollux_VC_JWT = new { .exclude("io.circe", "circe-generic_2.13") .exclude("io.circe", "circe-parser_2.13") - val jwtCirce = "com.github.jwt-scala" %% "jwt-circe" % V.jwtCirceVersion - val zio = "dev.zio" %% "zio" % V.zio val zioPrelude = "dev.zio" %% "zio-prelude" % V.zioPreludeVersion @@ -285,7 +289,7 @@ lazy val D_Pollux_VC_JWT = new { val zioDependencies: Seq[ModuleID] = Seq(zio, zioPrelude, zioTest, zioTestSbt, zioTestMagnolia) val circeDependencies: Seq[ModuleID] = Seq(D.circeCore, D.circeGeneric, D.circeParser) val baseDependencies: Seq[ModuleID] = - circeDependencies ++ zioDependencies :+ jwtCirce :+ circeJsonSchema :+ networkntJsonSchemaValidator :+ D.jwk :+ scalaTest + circeDependencies ++ zioDependencies :+ D.jwtCirce :+ circeJsonSchema :+ networkntJsonSchemaValidator :+ D.nimbusJwt :+ scalaTest // Project Dependencies lazy val polluxVcJwtDependencies: Seq[ModuleID] = baseDependencies @@ -337,6 +341,7 @@ lazy val D_PrismAgent = new { val flyway = "org.flywaydb" % "flyway-core" % V.flyway val vaultDriver = "io.github.jopenlibs" % "vault-java-driver" % V.vaultDriver + val keycloakAuthz = "org.keycloak" % "keycloak-authz-client" % V.keycloak // Dependency Modules val baseDependencies: Seq[ModuleID] = Seq( @@ -375,6 +380,8 @@ lazy val D_PrismAgent = new { lazy val keyManagementDependencies: Seq[ModuleID] = baseDependencies ++ bouncyDependencies ++ D.doobieDependencies ++ Seq(D.zioCatsInterop, D.zioMock, vaultDriver) + lazy val iamDependencies: Seq[ModuleID] = Seq(keycloakAuthz, D.jwtCirce) + lazy val serverDependencies: Seq[ModuleID] = baseDependencies ++ tapirDependencies ++ postgresDependencies ++ Seq(D.zioMock, D.mockito) } @@ -417,7 +424,7 @@ lazy val models = project ), // TODO try to remove this from this module // libraryDependencies += D.didScala ) - .settings(libraryDependencies += D.jwk) //FIXME just for the DidAgent + .settings(libraryDependencies += D.nimbusJwt) //FIXME just for the DidAgent /* TODO move code from agentDidcommx to here models implementation for didcommx () */ @@ -539,7 +546,7 @@ lazy val resolver = project // maybe merge into models D.peerDidcommx, D.munit, D.munitZio, - D.jwk, + D.nimbusJwt, ), testFrameworks += new TestFramework("munit.Framework") ) @@ -758,9 +765,11 @@ lazy val prismAgentWalletAPI = project .settings(prismAgentConnectCommonSettings) .settings( name := "prism-agent-wallet-api", - libraryDependencies ++= D_PrismAgent.keyManagementDependencies ++ D_PrismAgent.postgresDependencies ++ Seq( - D.zioMock - ) + libraryDependencies ++= + D_PrismAgent.keyManagementDependencies ++ + D_PrismAgent.iamDependencies ++ + D_PrismAgent.postgresDependencies ++ + Seq(D.zioMock) ) .dependsOn( agentDidcommx, diff --git a/infrastructure/shared/docker-compose-mt-keycloak.yml b/infrastructure/shared/docker-compose-mt-keycloak.yml index 542d936f3a..da6033d048 100644 --- a/infrastructure/shared/docker-compose-mt-keycloak.yml +++ b/infrastructure/shared/docker-compose-mt-keycloak.yml @@ -148,20 +148,23 @@ services: - swagger-ui keycloak: - image: bitnami/keycloak:22.0.3 + image: quay.io/keycloak/keycloak:22.0.4 ports: - "9980:8080" environment: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin - KEYCLOAK_DATABASE_VENDOR: dev-mem - KEYCLOAK_EXTRA_ARGS: --health-enabled=true - KEYCLOAK_EXTRA_ARGS_PREPENDED: --verbose - healthcheck: - test: ["CMD", "curl", "-f", "http://keycloak:8080/health"] - interval: 5s - timeout: 5s - retries: 10 + command: start-dev --health-enabled=true + + keycloak-wait: + image: badouralix/curl-jq:ubuntu + command: + - /bin/bash + - -c + - until curl http://keycloak:8080/health; do sleep 1; done && echo "Keycloak is ready." + depends_on: + keycloak: + condition: service_started keycloak-init: image: badouralix/curl-jq:ubuntu @@ -175,8 +178,8 @@ services: volumes: - ./keycloak/init-script.sh:/workspace/init-script.sh depends_on: - keycloak: - condition: service_healthy + keycloak-wait: + condition: service_completed_successfully volumes: pg_data_db: diff --git a/infrastructure/shared/keycloak/init-script.sh b/infrastructure/shared/keycloak/init-script.sh index be3716ee88..21649c22f7 100755 --- a/infrastructure/shared/keycloak/init-script.sh +++ b/infrastructure/shared/keycloak/init-script.sh @@ -35,19 +35,39 @@ function create_realm() { }" } -function create_prism_agent_client() { +function create_client() { local access_token=$1 + local client_id=$2 + local client_secret=$3 curl --request POST "$KEYCLOAK_BASE_URL/admin/realms/$REALM_NAME/clients" \ --fail -s \ -H "Authorization: Bearer $access_token" \ -H "Content-Type: application/json" \ --data-raw "{ - \"id\": \"prism-agent\", + \"id\": \"$client_id\", \"directAccessGrantsEnabled\": true, \"authorizationServicesEnabled\": true, \"serviceAccountsEnabled\": true, - \"secret\": \"$PRISM_AGENT_CLIENT_SECRET\" + \"secret\": \"$client_secret\" + }" +} + +function create_user() { + local access_token=$1 + local username=$2 + local password=$3 + + curl --request POST "$KEYCLOAK_BASE_URL/admin/realms/$REALM_NAME/users" \ + --fail -s \ + -H "Authorization: Bearer $access_token" \ + -H "Content-Type: application/json" \ + --data-raw "{ + \"id\": \"$username\", + \"username\": \"$username\", + \"firstName\": \"$username\", + \"enabled\": true, + \"credentials\": [{\"value\": $password, \"temporary\": false}] }" } @@ -58,4 +78,10 @@ echo "Creating a new test realm ..." create_realm $ADMIN_ACCESS_TOKEN echo "Creating a new prism-agent client ..." -create_prism_agent_client $ADMIN_ACCESS_TOKEN +create_client $ADMIN_ACCESS_TOKEN "prism-agent" $PRISM_AGENT_CLIENT_SECRET + +echo "Creating a new prism-manage client ..." +create_client $ADMIN_ACCESS_TOKEN "prism-manage" $PRISM_AGENT_CLIENT_SECRET + +echo "Creating a new sample user ..." +create_user $ADMIN_ACCESS_TOKEN "alice" "1234" diff --git a/prism-agent/service/server/src/main/resources/application.conf b/prism-agent/service/server/src/main/resources/application.conf index 51d74a26a0..15bb0db1a2 100644 --- a/prism-agent/service/server/src/main/resources/application.conf +++ b/prism-agent/service/server/src/main/resources/application.conf @@ -123,6 +123,28 @@ agent { autoProvisioning = true autoProvisioning = ${?API_KEY_AUTO_PROVISIONING} } + keycloak { + enabled = false + enabled = ${?KEYCLOAK_ENABLED} + + keycloakUrl = "http://localhost:9980" + keycloakUrl = ${?KEYCLOAK_URL} + + realmName = "atala-demo" + realmName = ${?KEYCLOAK_REALM} + + clientId = "prism-agent" + clientId = ${?KEYCLOAK_CLIENT_ID} + + clientSecret = "prism-agent-demo-secret" + clientSecret = ${?KEYCLOAK_CLIENT_SECRET} + + # autoUpgradeToRPT is used to enable the auto RPT (requesting party token) logic. + # if enabled, normal accessToken can be used to perform permission checks by obtaining RPT from accessToken. + # if disabled, accessToken must be RPT which already include the permission claims. + autoUpgradeToRPT = false + autoUpgradeToRPT = ${?KEYCLOAK_UMA_AUTO_UPGRADE_RPT} + } } database { host = "localhost" @@ -164,13 +186,13 @@ agent { } } secretStorage { - // Supports the following backend: [vault, postgres] - // If 'postgres' is used as a backend, it uses the agent db configuration - // If any other backend is used, its corresponding configuration must be configured. + # Supports the following backend: [vault, postgres, memory] + # If 'postgres' is used as a backend, it uses the agent db configuration + # If any other backend is used, its corresponding configuration must be configured. backend = "vault" backend = ${?SECRET_STORAGE_BACKEND} - // Configuration for Vault as a secret storage + # Configuration for Vault as a secret storage vault { address = "http://localhost:8200" address = ${?VAULT_ADDR} @@ -185,20 +207,20 @@ agent { } defaultWallet { - // A configuration for initializing default wallet. - // - // Once the default wallet is initialized, the agent will use persisted configurations - // from its storage and may ignore these parameters. + # A configuration for initializing default wallet. + # + # Once the default wallet is initialized, the agent will use persisted configurations + # from its storage and may ignore these parameters. enabled = true enabled = ${?DEFAULT_WALLET_ENABLED} - // Wallet seed to be used for the default wallet. If not provided, it will be generated. + # Wallet seed to be used for the default wallet. If not provided, it will be generated. seed = ${?DEFAULT_WALLET_SEED} - // Webhook url of the default wallet. - // If provided, webhook notification will be created when wallet is initialized. - // If not provided, webhook will not be created. - // If provided after the default wallet has been initialized, it will not have any effect. + # Webhook url of the default wallet. + # If provided, webhook notification will be created when wallet is initialized. + # If not provided, webhook will not be created. + # If provided after the default wallet has been initialized, it will not have any effect. webhookUrl = ${?DEFAULT_WALLET_WEBHOOK_URL} webhookApiKey = ${?DEFAULT_WALLET_WEBHOOK_API_KEY} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 74f633da18..526edf6096 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -21,8 +21,7 @@ import io.iohk.atala.connect.sql.repository.{JdbcConnectionRepository, Migration import io.iohk.atala.event.controller.EventControllerImpl import io.iohk.atala.event.notification.EventNotificationServiceImpl import io.iohk.atala.iam.authentication.DefaultAuthenticator -import io.iohk.atala.iam.authentication.admin.{AdminApiKeyAuthenticatorImpl, AdminConfig} -import io.iohk.atala.iam.authentication.apikey.{ApiKeyAuthenticatorImpl, ApiKeyConfig, JdbcAuthenticationRepository} +import io.iohk.atala.iam.authentication.apikey.JdbcAuthenticationRepository import io.iohk.atala.iam.entity.http.controller.{EntityController, EntityControllerImpl} import io.iohk.atala.iam.wallet.http.controller.WalletManagementControllerImpl import io.iohk.atala.issue.controller.IssueControllerImpl @@ -113,8 +112,6 @@ object MainApp extends ZIOAppDefault { DidCommX.liveLayer, // infra SystemModule.configLayer, - AdminConfig.layer, - ApiKeyConfig.layer, ZioHttpClient.layer, // observability DefaultJvmMetrics.live.unit, @@ -136,7 +133,7 @@ object MainApp extends ZIOAppDefault { EventControllerImpl.layer, // domain AppModule.apolloLayer, - AppModule.didJwtResolverlayer, + AppModule.didJwtResolverLayer, DIDOperationValidator.layer(), DIDResolver.layer, HttpURIDereferencerImpl.layer, @@ -152,7 +149,9 @@ object MainApp extends ZIOAppDefault { VerificationPolicyServiceImpl.layer, WalletManagementServiceImpl.layer, // authentication - AdminApiKeyAuthenticatorImpl.layer >+> ApiKeyAuthenticatorImpl.layer >+> DefaultAuthenticator.layer, + AppModule.builtInAuthenticatorLayer, + AppModule.keycloakAuthenticatorLayer, + DefaultAuthenticator.layer, // grpc GrpcModule.prismNodeStubLayer, // storage diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala index c67d4e22af..a8854e0a8b 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala @@ -4,16 +4,36 @@ import com.typesafe.config.ConfigFactory import doobie.util.transactor.Transactor import io.grpc.ManagedChannelBuilder import io.iohk.atala.agent.server.config.AppConfig +import io.iohk.atala.agent.server.config.SecretStorageBackend import io.iohk.atala.agent.walletapi.crypto.Apollo import io.iohk.atala.agent.walletapi.memory.{ DIDSecretStorageInMemory, GenericSecretStorageInMemory, WalletSecretStorageInMemory } +import io.iohk.atala.agent.walletapi.service.EntityService +import io.iohk.atala.agent.walletapi.service.WalletManagementService import io.iohk.atala.agent.walletapi.sql.{JdbcDIDSecretStorage, JdbcGenericSecretStorage, JdbcWalletSecretStorage} import io.iohk.atala.agent.walletapi.storage.{DIDSecretStorage, GenericSecretStorage, WalletSecretStorage} import io.iohk.atala.agent.walletapi.vault.* +import io.iohk.atala.agent.walletapi.vault.{ + VaultDIDSecretStorage, + VaultKVClient, + VaultKVClientImpl, + VaultWalletSecretStorage +} import io.iohk.atala.castor.core.service.DIDService +import io.iohk.atala.iam.authentication.DefaultAuthenticator +import io.iohk.atala.iam.authentication.admin.AdminApiKeyAuthenticator +import io.iohk.atala.iam.authentication.admin.AdminApiKeyAuthenticatorImpl +import io.iohk.atala.iam.authentication.admin.AdminConfig +import io.iohk.atala.iam.authentication.apikey.ApiKeyAuthenticator +import io.iohk.atala.iam.authentication.apikey.ApiKeyAuthenticatorImpl +import io.iohk.atala.iam.authentication.apikey.ApiKeyConfig +import io.iohk.atala.iam.authentication.apikey.AuthenticationRepository +import io.iohk.atala.iam.authentication.oidc.KeycloakAuthenticatorImpl +import io.iohk.atala.iam.authentication.oidc.KeycloakClientImpl +import io.iohk.atala.iam.authentication.oidc.KeycloakConfig import io.iohk.atala.iris.proto.service.IrisServiceGrpc import io.iohk.atala.iris.proto.service.IrisServiceGrpc.IrisServiceStub import io.iohk.atala.pollux.vc.jwt.{PrismDidResolver, DidResolver as JwtDidResolver} @@ -22,6 +42,8 @@ import io.iohk.atala.shared.db.{ContextAwareTask, DbConfig, TransactorLayer} import zio.* import zio.config.typesafe.TypesafeConfigSource import zio.config.{ReadError, read} +import zio.http.Client +import io.iohk.atala.iam.authentication.oidc.KeycloakAuthenticator object SystemModule { val configLayer: Layer[ReadError[String], AppConfig] = ZLayer.fromZIO { @@ -38,8 +60,37 @@ object SystemModule { object AppModule { val apolloLayer: ULayer[Apollo] = Apollo.prism14Layer - val didJwtResolverlayer: URLayer[DIDService, JwtDidResolver] = + val didJwtResolverLayer: URLayer[DIDService, JwtDidResolver] = ZLayer.fromFunction(PrismDidResolver(_)) + + val builtInAuthenticatorLayer: URLayer[ + AppConfig & AuthenticationRepository & EntityService & WalletManagementService, + ApiKeyAuthenticator & AdminApiKeyAuthenticator + ] = + ZLayer.makeSome[ + AppConfig & AuthenticationRepository & EntityService & WalletManagementService, + ApiKeyAuthenticator & AdminApiKeyAuthenticator + ]( + AdminConfig.layer, + ApiKeyConfig.layer, + AdminApiKeyAuthenticatorImpl.layer, + ApiKeyAuthenticatorImpl.layer, + ) + + val keycloakAuthenticatorLayer: RLayer[AppConfig & WalletManagementService & Client, KeycloakAuthenticator] = + ZLayer.fromZIO { + ZIO + .serviceWith[AppConfig](_.agent.authentication.keycloak.enabled) + .map { isEnabled => + if (!isEnabled) KeycloakAuthenticatorImpl.disabled + else + ZLayer.makeSome[AppConfig & WalletManagementService & Client, KeycloakAuthenticator]( + KeycloakConfig.layer, + KeycloakAuthenticatorImpl.layer, + KeycloakClientImpl.layer + ) + } + }.flatten } object GrpcModule { @@ -137,7 +188,7 @@ object RepoModule { .map(_.agent.secretStorage.backend) .tap(backend => ZIO.logInfo(s"Using '$backend' as a secret storage backend")) .flatMap { - case "vault" => + case SecretStorageBackend.vault => ZIO.succeed( ZLayer.make[DIDSecretStorage & WalletSecretStorage & GenericSecretStorage]( VaultDIDSecretStorage.layer, @@ -146,7 +197,7 @@ object RepoModule { vaultClientLayer, ) ) - case "postgres" => + case SecretStorageBackend.postgres => ZIO.succeed( ZLayer.make[DIDSecretStorage & WalletSecretStorage & GenericSecretStorage]( JdbcDIDSecretStorage.layer, @@ -155,7 +206,7 @@ object RepoModule { agentContextAwareTransactorLayer, ) ) - case "memory" => + case SecretStorageBackend.memory => ZIO.succeed( ZLayer.make[DIDSecretStorage & WalletSecretStorage & GenericSecretStorage]( DIDSecretStorageInMemory.layer, @@ -163,11 +214,6 @@ object RepoModule { GenericSecretStorageInMemory.layer ) ) - case backend => - ZIO - .fail(s"Unsupported secret storage backend $backend. Available options are 'postgres', 'vault'") - .tapError(msg => ZIO.logError(msg)) - .mapError(msg => Exception(msg)) } .provide(SystemModule.configLayer) }.flatten 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 4b92900a80..931caf43e8 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 @@ -9,6 +9,7 @@ import zio.config.magnolia.Descriptor import java.net.URL import java.time.Duration +import scala.util.Try final case class AppConfig( devMode: Boolean, @@ -133,8 +134,10 @@ final case class AgentConfig( defaultWallet: DefaultWalletConfig ) { def validate: Either[String, Unit] = { - if (!defaultWallet.enabled && !authentication.apiKey.enabled) - Left("The default wallet cannot be disabled if the apikey authentication is disabled.") + if (!defaultWallet.enabled && !authentication.isEnabledAny) + Left( + "The default wallet must be enabled if all the authentication methods are disabled. Default wallet is required for the single-tenant mode." + ) else Right(()) } @@ -147,6 +150,22 @@ final case class DidCommEndpointConfig(http: HttpConfig, publicEndpointUrl: Stri final case class HttpConfig(port: Int) final case class SecretStorageConfig( - backend: String, + backend: SecretStorageBackend, vault: Option[VaultConfig], ) + +enum SecretStorageBackend { + case vault, postgres, memory +} + +object SecretStorageBackend { + given Descriptor[SecretStorageBackend] = + Descriptor.from( + Descriptor[String].transformOrFailLeft { s => + Try(SecretStorageBackend.valueOf(s)).toOption + .toRight( + s"Invalid configuration value '$s'. Possible values: ${SecretStorageBackend.values.mkString("[", ", ", "]")}" + ) + }(_.toString()) + ) +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala index 93a7b9a8ce..d298c71241 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/ZHttpEndpoints.scala @@ -1,7 +1,9 @@ package io.iohk.atala.agent.server.http +import io.iohk.atala.iam.authentication.oidc.JwtSecurityLogic import sttp.apispec.SecurityScheme import sttp.apispec.openapi.{OpenAPI, Server} +import sttp.model.headers.AuthenticationScheme import sttp.tapir.redoc.RedocUIOptions import sttp.tapir.redoc.bundle.RedocInterpreter import sttp.tapir.server.ServerEndpoint @@ -33,14 +35,16 @@ object ZHttpEndpoints { .copy(securitySchemes = ListMap( "apiKeyAuth" -> Right(apiKeySecuritySchema), - "adminApiKeyAuth" -> Right(adminApiKeySecuritySchema) + "adminApiKeyAuth" -> Right(adminApiKeySecuritySchema), + "jwtAuth" -> Right(jwtSecurityScheme) ) ) ) .addSecurity( ListMap( "apiKeyAuth" -> Vector.empty[String], - "adminApiKeyAuth" -> Vector.empty[String] + "adminApiKeyAuth" -> Vector.empty[String], + "jwtAuth" -> Vector.empty[String] ) ) @@ -69,6 +73,18 @@ object ZHttpEndpoints { openIdConnectUrl = None ) + private val jwtSecurityScheme = SecurityScheme( + `type` = "http", + description = + Some("JWT Authentication. The header `Authorization` must be set with the JWT token using `Bearer` scheme"), + name = Some("Authorization"), + in = Some("header"), + scheme = Some(AuthenticationScheme.Bearer.name), + bearerFormat = None, + flows = None, + openIdConnectUrl = None + ) + def swaggerEndpoints[F[_]](apiEndpoints: List[ServerEndpoint[Any, F]]): List[ServerEndpoint[Any, F]] = SwaggerInterpreter(swaggerUIOptions = swaggerUIOptions, customiseDocsModel = customiseDocsModel) .fromServerEndpoints[F](apiEndpoints, "Prism Agent", "1.0.0") diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/DIDRegistrarEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/DIDRegistrarEndpoints.scala index bc7fd59b6e..e7a0abee17 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/DIDRegistrarEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/DIDRegistrarEndpoints.scala @@ -14,6 +14,8 @@ import io.iohk.atala.castor.controller.http.{ } import io.iohk.atala.iam.authentication.apikey.ApiKeyCredentials import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader +import io.iohk.atala.iam.authentication.oidc.JwtCredentials +import io.iohk.atala.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader import sttp.model.StatusCode import sttp.tapir.* import sttp.tapir.json.zio.jsonBody @@ -25,11 +27,12 @@ object DIDRegistrarEndpoints { .in("did-registrar" / "dids") .in(extractFromRequest[RequestContext](RequestContext.apply)) .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) private val paginationInput: EndpointInput[PaginationInput] = EndpointInput.derived[PaginationInput] val listManagedDid: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, PaginationInput), ErrorResponse, ManagedDIDPage, @@ -47,7 +50,7 @@ object DIDRegistrarEndpoints { ) val createManagedDid: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, CreateManagedDidRequest), ErrorResponse, CreateManagedDIDResponse, @@ -67,7 +70,7 @@ object DIDRegistrarEndpoints { ) val getManagedDid: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, String), ErrorResponse, ManagedDID, @@ -81,7 +84,7 @@ object DIDRegistrarEndpoints { .description("Get DID stored in Prism Agent's wallet") val publishManagedDid: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, String), ErrorResponse, DIDOperationResponse, @@ -95,7 +98,7 @@ object DIDRegistrarEndpoints { .description("Publish the DID stored in Prism Agent's wallet to the VDR.") val updateManagedDid: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, String, UpdateManagedDIDRequest), ErrorResponse, DIDOperationResponse, @@ -123,7 +126,7 @@ object DIDRegistrarEndpoints { ) val deactivateManagedDid: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, String), ErrorResponse, DIDOperationResponse, diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/DIDRegistrarServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/DIDRegistrarServerEndpoints.scala index 4649928b15..a49aac9940 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/DIDRegistrarServerEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/DIDRegistrarServerEndpoints.scala @@ -1,79 +1,82 @@ package io.iohk.atala.castor.controller +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.iam.authentication.Authenticator -import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic +import io.iohk.atala.iam.authentication.Authorizer +import io.iohk.atala.iam.authentication.DefaultAuthenticator +import io.iohk.atala.iam.authentication.SecurityLogic import io.iohk.atala.shared.models.WalletAccessContext import sttp.tapir.ztapir.* import zio.* class DIDRegistrarServerEndpoints( didRegistrarController: DIDRegistrarController, - authenticator: Authenticator + authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] ) { private val listManagedDidServerEndpoint: ZServerEndpoint[Any, Any] = DIDRegistrarEndpoints.listManagedDid - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (rc, paginationInput) => didRegistrarController .listManagedDid(paginationInput)(rc) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } private val createManagedDidServerEndpoint: ZServerEndpoint[Any, Any] = DIDRegistrarEndpoints.createManagedDid - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (rc, createManagedDidRequest) => didRegistrarController .createManagedDid(createManagedDidRequest)(rc) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } private val getManagedDidServerEndpoint: ZServerEndpoint[Any, Any] = DIDRegistrarEndpoints.getManagedDid - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (rc, did) => didRegistrarController .getManagedDid(did)(rc) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } private val publishManagedDidServerEndpoint: ZServerEndpoint[Any, Any] = DIDRegistrarEndpoints.publishManagedDid - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (rc, did) => didRegistrarController .publishManagedDid(did)(rc) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } private val updateManagedDidServerEndpoint: ZServerEndpoint[Any, Any] = DIDRegistrarEndpoints.updateManagedDid - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (rc, did, updateRequest) => didRegistrarController .updateManagedDid(did, updateRequest)(rc) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } private val deactivateManagedDidServerEndpoint: ZServerEndpoint[Any, Any] = DIDRegistrarEndpoints.deactivateManagedDid - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (rc, did) => didRegistrarController .deactivateManagedDid(did)(rc) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } @@ -89,9 +92,9 @@ class DIDRegistrarServerEndpoints( } object DIDRegistrarServerEndpoints { - def all: URIO[DIDRegistrarController & Authenticator, List[ZServerEndpoint[Any, Any]]] = { + def all: URIO[DIDRegistrarController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { for { - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[DefaultAuthenticator] didRegistrarController <- ZIO.service[DIDRegistrarController] didRegistrarEndpoints = new DIDRegistrarServerEndpoints(didRegistrarController, authenticator) } yield didRegistrarEndpoints.all diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionEndpoints.scala index f5cc382e78..2c557c56e3 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionEndpoints.scala @@ -11,6 +11,8 @@ import io.iohk.atala.connect.controller.http.{ } import io.iohk.atala.iam.authentication.apikey.ApiKeyCredentials import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader +import io.iohk.atala.iam.authentication.oidc.JwtCredentials +import io.iohk.atala.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader import sttp.model.StatusCode import sttp.tapir.* import sttp.tapir.json.zio.jsonBody @@ -22,7 +24,7 @@ object ConnectionEndpoints { private val paginationInput: EndpointInput[PaginationInput] = EndpointInput.derived[PaginationInput] val createConnection: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, CreateConnectionRequest), ErrorResponse, Connection, @@ -30,6 +32,7 @@ object ConnectionEndpoints { ] = endpoint.post .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in("connections") .in( @@ -55,9 +58,11 @@ object ConnectionEndpoints { |""".stripMargin) .tag("Connections Management") - val getConnection: Endpoint[ApiKeyCredentials, (RequestContext, UUID), ErrorResponse, Connection, Any] = + val getConnection + : Endpoint[(ApiKeyCredentials, JwtCredentials), (RequestContext, UUID), ErrorResponse, Connection, Any] = endpoint.get .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( "connections" / path[UUID]("connectionId").description( @@ -72,7 +77,7 @@ object ConnectionEndpoints { .tag("Connections Management") val getConnections: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, PaginationInput, Option[String]), ErrorResponse, ConnectionsPage, @@ -80,6 +85,7 @@ object ConnectionEndpoints { ] = endpoint.get .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in("connections") .in(paginationInput) @@ -92,7 +98,7 @@ object ConnectionEndpoints { .tag("Connections Management") val acceptConnectionInvitation: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, AcceptConnectionInvitationRequest), ErrorResponse, Connection, @@ -100,6 +106,7 @@ object ConnectionEndpoints { ] = endpoint.post .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in("connection-invitations") .in( diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionServerEndpoints.scala index 1fa97d37dc..e2d9031403 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionServerEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/connect/controller/ConnectionServerEndpoints.scala @@ -1,60 +1,65 @@ package io.iohk.atala.connect.controller +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.api.http.RequestContext import io.iohk.atala.api.http.model.PaginationInput import io.iohk.atala.connect.controller.ConnectionEndpoints.* import io.iohk.atala.connect.controller.http.{AcceptConnectionInvitationRequest, CreateConnectionRequest} import io.iohk.atala.iam.authentication.Authenticator -import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic -import io.iohk.atala.shared.models.WalletAccessContext +import io.iohk.atala.iam.authentication.Authorizer +import io.iohk.atala.iam.authentication.DefaultAuthenticator +import io.iohk.atala.iam.authentication.SecurityLogic import sttp.tapir.ztapir.* import zio.* import java.util.UUID -class ConnectionServerEndpoints(connectionController: ConnectionController, authenticator: Authenticator) { +class ConnectionServerEndpoints( + connectionController: ConnectionController, + authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] +) { private val createConnectionServerEndpoint: ZServerEndpoint[Any, Any] = createConnection - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, request: CreateConnectionRequest) => connectionController .createConnection(request)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } private val getConnectionServerEndpoint: ZServerEndpoint[Any, Any] = getConnection - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, connectionId: UUID) => connectionController .getConnection(connectionId)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } private val getConnectionsServerEndpoint: ZServerEndpoint[Any, Any] = getConnections - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, paginationInput: PaginationInput, thid: Option[String]) => connectionController .getConnections(paginationInput, thid)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } private val acceptConnectionInvitationServerEndpoint: ZServerEndpoint[Any, Any] = acceptConnectionInvitation - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, request: AcceptConnectionInvitationRequest) => connectionController .acceptConnectionInvitation(request)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } @@ -67,9 +72,9 @@ class ConnectionServerEndpoints(connectionController: ConnectionController, auth } object ConnectionServerEndpoints { - def all: URIO[ConnectionController & Authenticator, List[ZServerEndpoint[Any, Any]]] = { + def all: URIO[ConnectionController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { for { - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[DefaultAuthenticator] connectionController <- ZIO.service[ConnectionController] connectionEndpoints = new ConnectionServerEndpoints(connectionController, authenticator) } yield connectionEndpoints.all diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/event/controller/EventEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/event/controller/EventEndpoints.scala index afe25fe71b..88b886d0f4 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/event/controller/EventEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/event/controller/EventEndpoints.scala @@ -9,6 +9,8 @@ import io.iohk.atala.event.controller.http.WebhookNotification import io.iohk.atala.event.controller.http.WebhookNotificationPage import io.iohk.atala.iam.authentication.apikey.ApiKeyCredentials import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader +import io.iohk.atala.iam.authentication.oidc.JwtCredentials +import io.iohk.atala.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader import sttp.model.StatusCode import sttp.tapir.* import sttp.tapir.json.zio.jsonBody @@ -21,10 +23,11 @@ object EventEndpoints { .tag("Events") .in("events") .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) val createWebhookNotification: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, CreateWebhookNotification), ErrorResponse, WebhookNotification, @@ -38,7 +41,7 @@ object EventEndpoints { .summary("Create wallet webhook notifications") val listWebhookNotification: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), RequestContext, ErrorResponse, WebhookNotificationPage, @@ -51,7 +54,7 @@ object EventEndpoints { .summary("List wallet webhook notifications") val deleteWebhookNotification: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, UUID), ErrorResponse, Unit, diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/event/controller/EventServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/event/controller/EventServerEndpoints.scala index 69f0cce900..5ecb90bb71 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/event/controller/EventServerEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/event/controller/EventServerEndpoints.scala @@ -1,44 +1,47 @@ package io.iohk.atala.event.controller +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.iam.authentication.Authenticator -import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic +import io.iohk.atala.iam.authentication.Authorizer +import io.iohk.atala.iam.authentication.DefaultAuthenticator import io.iohk.atala.shared.models.WalletAccessContext import sttp.tapir.ztapir.* import zio.* +import io.iohk.atala.iam.authentication.SecurityLogic class EventServerEndpoints( eventController: EventController, - authenticator: Authenticator + authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] ) { val createWebhookNotificationServerEndpoint: ZServerEndpoint[Any, Any] = EventEndpoints.createWebhookNotification - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (rc, createWebhook) => eventController .createWebhookNotification(createWebhook)(rc) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } val listWebhookNotificationServerEndpoint: ZServerEndpoint[Any, Any] = EventEndpoints.listWebhookNotification - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => rc => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => rc => eventController .listWebhookNotifications(rc) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } val deleteWebhookNotificationServerEndpoint: ZServerEndpoint[Any, Any] = EventEndpoints.deleteWebhookNotification - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (rc, id) => eventController .deleteWebhookNotification(id)(rc) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } @@ -51,9 +54,9 @@ class EventServerEndpoints( } object EventServerEndpoints { - def all: URIO[EventController & Authenticator, List[ZServerEndpoint[Any, Any]]] = { + def all: URIO[EventController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { for { - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[DefaultAuthenticator] eventController <- ZIO.service[EventController] eventEndpoints = new EventServerEndpoints(eventController, authenticator) } yield eventEndpoints.all diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/AuthenticationConfig.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/AuthenticationConfig.scala index 976bf22f2f..37ac429eb1 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/AuthenticationConfig.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/AuthenticationConfig.scala @@ -2,5 +2,15 @@ package io.iohk.atala.iam.authentication import io.iohk.atala.iam.authentication.admin.AdminConfig import io.iohk.atala.iam.authentication.apikey.ApiKeyConfig +import io.iohk.atala.iam.authentication.oidc.KeycloakConfig -final case class AuthenticationConfig(admin: AdminConfig, apiKey: ApiKeyConfig) +final case class AuthenticationConfig( + admin: AdminConfig, + apiKey: ApiKeyConfig, + keycloak: KeycloakConfig +) { + + /** Return true if at least 1 authentication method is enabled (exlcuding admin auth method) */ + def isEnabledAny: Boolean = apiKey.enabled || keycloak.enabled + +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/Authenticator.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/Authenticator.scala index 30e7662b13..b2e0f3477c 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/Authenticator.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/Authenticator.scala @@ -1,7 +1,9 @@ package io.iohk.atala.iam.authentication +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.agent.walletapi.model.Entity import io.iohk.atala.api.http.ErrorResponse +import io.iohk.atala.shared.models.WalletId import zio.{IO, ZIO, ZLayer} trait Credentials @@ -20,6 +22,8 @@ object AuthenticationError { case class ServiceError(message: String) extends AuthenticationError + case class ResourceNotPermitted(message: String) extends AuthenticationError + def toErrorResponse(error: AuthenticationError): ErrorResponse = ErrorResponse( status = sttp.model.StatusCode.Forbidden.code, @@ -30,17 +34,33 @@ object AuthenticationError { ) } -trait Authenticator { - def authenticate(credentials: Credentials): IO[AuthenticationError, Entity] +trait Authenticator[E <: BaseEntity] { + def authenticate(credentials: Credentials): IO[AuthenticationError, E] def isEnabled: Boolean - def apply(credentials: Credentials): IO[AuthenticationError, Entity] = authenticate(credentials) + def apply(credentials: Credentials): IO[AuthenticationError, E] = authenticate(credentials) +} + +trait Authorizer[E <: BaseEntity] { + def authorize(entity: E): IO[AuthenticationError, WalletId] +} + +object EntityAuthorizer extends EntityAuthorizer + +trait EntityAuthorizer extends Authorizer[Entity] { + override def authorize(entity: Entity): IO[AuthenticationError, WalletId] = + ZIO.succeed(entity.walletId).map(WalletId.fromUUID) } -object DefaultEntityAuthenticator extends Authenticator { +trait AuthenticatorWithAuthZ[E <: BaseEntity] extends Authenticator[E], Authorizer[E] + +object DefaultEntityAuthenticator extends AuthenticatorWithAuthZ[BaseEntity] { + override def isEnabled: Boolean = true - override def authenticate(credentials: Credentials): IO[AuthenticationError, Entity] = ZIO.succeed(Entity.Default) + override def authenticate(credentials: Credentials): IO[AuthenticationError, BaseEntity] = ZIO.succeed(Entity.Default) + override def authorize(entity: BaseEntity): IO[AuthenticationError, WalletId] = + EntityAuthorizer.authorize(Entity.Default) val layer = ZLayer.apply(ZIO.succeed(DefaultEntityAuthenticator)) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/DefaultAuthenticator.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/DefaultAuthenticator.scala index 617b23c96c..a15e370f03 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/DefaultAuthenticator.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/DefaultAuthenticator.scala @@ -1,34 +1,36 @@ package io.iohk.atala.iam.authentication +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.agent.walletapi.model.Entity -import io.iohk.atala.iam.authentication.AuthenticationError.AuthenticationMethodNotEnabled import io.iohk.atala.iam.authentication.admin.{AdminApiKeyAuthenticator, AdminApiKeyCredentials} -import io.iohk.atala.iam.authentication.apikey.* +import io.iohk.atala.iam.authentication.apikey.{ApiKeyAuthenticator, ApiKeyCredentials} +import io.iohk.atala.iam.authentication.oidc.KeycloakEntity +import io.iohk.atala.iam.authentication.oidc.{KeycloakAuthenticator, JwtCredentials} +import io.iohk.atala.shared.models.WalletId import zio.* -import zio.ZIO.* -import zio.ZLayer.* case class DefaultAuthenticator( adminApiKeyAuthenticator: AdminApiKeyAuthenticator, - apiKeyAuthenticator: ApiKeyAuthenticator -) extends Authenticator { + apiKeyAuthenticator: ApiKeyAuthenticator, + keycloakAuthenticator: KeycloakAuthenticator +) extends AuthenticatorWithAuthZ[BaseEntity] { + override def isEnabled = true - override def authenticate(credentials: Credentials): IO[AuthenticationError, Entity] = credentials match { + + override def authenticate(credentials: Credentials): IO[AuthenticationError, BaseEntity] = credentials match { case adminApiKeyCredentials: AdminApiKeyCredentials => adminApiKeyAuthenticator(adminApiKeyCredentials) - case apiKeyCredentials: ApiKeyCredentials => - apiKeyAuthenticator(apiKeyCredentials) - .catchSome { case AuthenticationMethodNotEnabled(_: String) => - ZIO.succeed(Entity.Default) - } + case apiKeyCredentials: ApiKeyCredentials => apiKeyAuthenticator(apiKeyCredentials) + case keycloakCredentials: JwtCredentials => keycloakAuthenticator(keycloakCredentials) } + + override def authorize(entity: BaseEntity): IO[AuthenticationError, WalletId] = entity match { + case entity: Entity => EntityAuthorizer.authorize(entity) + case kcEntity: KeycloakEntity => keycloakAuthenticator.authorize(kcEntity) + } + } object DefaultAuthenticator { - val layer: URLayer[AdminApiKeyAuthenticator & ApiKeyAuthenticator, Authenticator] = - ZLayer.fromZIO { - for { - adminApiKeyAuthenticator <- ZIO.service[AdminApiKeyAuthenticator] - apiKeyAuthenticator <- ZIO.service[ApiKeyAuthenticator] - } yield DefaultAuthenticator(adminApiKeyAuthenticator, apiKeyAuthenticator) - } + val layer: URLayer[AdminApiKeyAuthenticator & ApiKeyAuthenticator & KeycloakAuthenticator, DefaultAuthenticator] = + ZLayer.fromFunction(DefaultAuthenticator(_, _, _)) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/SecurityLogic.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/SecurityLogic.scala new file mode 100644 index 0000000000..f8a653e476 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/SecurityLogic.scala @@ -0,0 +1,57 @@ +package io.iohk.atala.iam.authentication + +import io.iohk.atala.agent.walletapi.model.BaseEntity +import io.iohk.atala.agent.walletapi.model.Entity +import io.iohk.atala.api.http.ErrorResponse +import io.iohk.atala.iam.authentication.AuthenticationError.AuthenticationMethodNotEnabled +import io.iohk.atala.iam.authentication.apikey.ApiKeyCredentials +import io.iohk.atala.iam.authentication.oidc.JwtCredentials +import io.iohk.atala.shared.models.WalletAccessContext +import zio.* + +object SecurityLogic { + + def authenticate[E <: BaseEntity](credentials: Credentials, others: Credentials*)( + authenticator: Authenticator[E], + ): IO[ErrorResponse, Either[Entity, E]] = { + val creds = credentials :: others.toList + ZIO + .validateFirst(creds)(authenticator.authenticate) + .map(Right(_)) + .catchAll { errors => + val isAllMethodsDisabled = errors.forall { + case AuthenticationMethodNotEnabled(_) => true + case _ => false + } + + // if the alternative authentication method is not configured, + // apikey authentication is disabled the default user is used + if (isAllMethodsDisabled) ZIO.left(Entity.Default) + else ZIO.fail(errors.head) // cannot fail, always non-empty + } + .mapError(AuthenticationError.toErrorResponse) + } + + def authorize[E <: BaseEntity](credentials: Credentials, others: Credentials*)( + authenticator: Authenticator[E] & Authorizer[E], + ): IO[ErrorResponse, WalletAccessContext] = { + authenticate[E](credentials, others: _*)(authenticator) + .flatMap { + case Left(entity) => + EntityAuthorizer + .authorize(entity) + .mapError(AuthenticationError.toErrorResponse) + case Right(entity) => + authenticator + .authorize(entity) + .mapError(AuthenticationError.toErrorResponse) + } + .map(walletId => WalletAccessContext(walletId)) + } + + def authorizeWith[E <: BaseEntity](credentials: (ApiKeyCredentials, JwtCredentials))( + authenticator: Authenticator[E] & Authorizer[E] + ): IO[ErrorResponse, WalletAccessContext] = + authorize[E](credentials._2, credentials._1)(authenticator) + +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminApiKeyAuthenticator.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminApiKeyAuthenticator.scala index 8354cfbed6..26d16db975 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminApiKeyAuthenticator.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminApiKeyAuthenticator.scala @@ -1,17 +1,15 @@ package io.iohk.atala.iam.authentication.admin import io.iohk.atala.agent.walletapi.model.Entity -import io.iohk.atala.iam.authentication.AuthenticationError.* -import io.iohk.atala.iam.authentication.{AuthenticationError, Authenticator, Credentials} -import zio.{IO, ZIO} +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ +import io.iohk.atala.iam.authentication.EntityAuthorizer +import io.iohk.atala.iam.authentication.{AuthenticationError, Credentials} +import zio.IO -trait AdminApiKeyAuthenticator extends Authenticator { +trait AdminApiKeyAuthenticator extends AuthenticatorWithAuthZ[Entity], EntityAuthorizer { def authenticate(credentials: Credentials): IO[AuthenticationError, Entity] = { credentials match { - case EmptyAdminApiKeyCredentials() => - ZIO.logInfo("Admin API authentication enabled, but `x-admin-api` token is not provided") *> - ZIO.fail(InvalidCredentials("Admin API key is not provided")) case AdminApiKeyCredentials(apiKey) => authenticate(apiKey) } } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminApiKeyCredentials.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminApiKeyCredentials.scala index c98accd2aa..8bf823eb07 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminApiKeyCredentials.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminApiKeyCredentials.scala @@ -6,10 +6,6 @@ case class AdminApiKeyAuthenticationError(message: String) extends Authenticatio object AdminApiKeyAuthenticationError { val invalidAdminApiKey = AdminApiKeyAuthenticationError("Invalid Admin API key in header `x-admin-api-key`") - val emptyAdminApiKey = AdminApiKeyAuthenticationError("Empty Admin API key header `x-admin-api-key`") } -case class AdminApiKeyCredentials( - apiKey: String -) extends Credentials -case class EmptyAdminApiKeyCredentials() extends Credentials +case class AdminApiKeyCredentials(apiKey: String) extends Credentials diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminApiKeySecurityLogic.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminApiKeySecurityLogic.scala index 4bbc9cf2f5..7eee5d689d 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminApiKeySecurityLogic.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminApiKeySecurityLogic.scala @@ -1,6 +1,6 @@ package io.iohk.atala.iam.authentication.admin -import io.iohk.atala.agent.walletapi.model.Entity +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.api.http.ErrorResponse import io.iohk.atala.iam.authentication.{AuthenticationError, Authenticator} import sttp.tapir.EndpointIO @@ -13,13 +13,9 @@ object AdminApiKeySecurityLogic { .mapTo[AdminApiKeyCredentials] .description("Admin API Key") - def securityLogic(credentials: AdminApiKeyCredentials): ZIO[Authenticator, ErrorResponse, Entity] = - ZIO - .service[Authenticator] - .flatMap(_.authenticate(credentials)) - .mapError(error => AuthenticationError.toErrorResponse(error)) - - def securityLogic(credentials: AdminApiKeyCredentials)(authenticator: Authenticator): IO[ErrorResponse, Entity] = + def securityLogic[E <: BaseEntity]( + credentials: AdminApiKeyCredentials + )(authenticator: Authenticator[E]): IO[ErrorResponse, E] = ZIO .succeed(authenticator) .flatMap(_.authenticate(credentials)) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminConfig.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminConfig.scala index 2e02f7baa3..f1de628d5b 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminConfig.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/AdminConfig.scala @@ -1,11 +1,11 @@ package io.iohk.atala.iam.authentication.admin import io.iohk.atala.agent.server.config.AppConfig -import zio.{URLayer, ZIO, ZLayer} +import zio.{URLayer, ZLayer} final case class AdminConfig(token: String) //TODO: after moving the classes to separated package, derive the adminConfig from the authenticationConfig object AdminConfig { - val layer: URLayer[AppConfig, AdminConfig] = ZLayer.fromZIO(ZIO.service[AppConfig].map(_.agent.authentication.admin)) + val layer: URLayer[AppConfig, AdminConfig] = ZLayer.fromFunction((conf: AppConfig) => conf.agent.authentication.admin) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/package.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/package.scala index a15e2be4f0..ec7a0acc6e 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/package.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/admin/package.scala @@ -3,5 +3,5 @@ package io.iohk.atala.iam.authentication import io.iohk.atala.agent.walletapi.model.Entity package object admin { - val Admin = Entity("admin") + val Admin = Entity.Admin } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticator.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticator.scala index e7726d1e59..e546380597 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticator.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticator.scala @@ -2,12 +2,14 @@ package io.iohk.atala.iam.authentication.apikey import io.iohk.atala.agent.walletapi.model.Entity import io.iohk.atala.iam.authentication.AuthenticationError.* -import io.iohk.atala.iam.authentication.{AuthenticationError, Authenticator, Credentials} +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ +import io.iohk.atala.iam.authentication.EntityAuthorizer +import io.iohk.atala.iam.authentication.{AuthenticationError, Credentials} import zio.{IO, ZIO} import java.util.UUID -trait ApiKeyAuthenticator extends Authenticator { +trait ApiKeyAuthenticator extends AuthenticatorWithAuthZ[Entity], EntityAuthorizer { def authenticate(credentials: Credentials): IO[AuthenticationError, Entity] = { if (isEnabled) { @@ -25,11 +27,11 @@ trait ApiKeyAuthenticator extends Authenticator { case other => ZIO.fail(InvalidCredentials("ApiKey key is not provided")) } - } else - ZIO.fail(AuthenticationMethodNotEnabled("ApiKey API authentication is not enabled")) + } else ZIO.fail(AuthenticationMethodNotEnabled("ApiKey API authentication is not enabled")) } def isEnabled: Boolean + def authenticate(apiKey: String): IO[AuthenticationError, Entity] def add(entityId: UUID, apiKey: String): IO[AuthenticationError, Unit] diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyConfig.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyConfig.scala index 1310a78d6c..ec760b5d6f 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyConfig.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyConfig.scala @@ -1,13 +1,11 @@ package io.iohk.atala.iam.authentication.apikey import io.iohk.atala.agent.server.config.AppConfig -import zio.{URLayer, ZLayer} -import zio.ZIO +import zio.* case class ApiKeyConfig(salt: String, enabled: Boolean, authenticateAsDefaultUser: Boolean, autoProvisioning: Boolean) object ApiKeyConfig { val layer: URLayer[AppConfig, ApiKeyConfig] = - ZLayer.fromZIO(ZIO.service[AppConfig].map(_.agent.authentication.apiKey)) - + ZLayer.fromFunction((conf: AppConfig) => conf.agent.authentication.apiKey) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyEndpointSecurityLogic.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyEndpointSecurityLogic.scala index 021e39d2a9..ea02323e3d 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyEndpointSecurityLogic.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyEndpointSecurityLogic.scala @@ -1,17 +1,14 @@ package io.iohk.atala.iam.authentication.apikey -import io.iohk.atala.agent.walletapi.model.Entity -import io.iohk.atala.api.http.ErrorResponse -import io.iohk.atala.iam.authentication.{AuthenticationError, Authenticator} import sttp.tapir.EndpointIO +import sttp.tapir.EndpointInput.Auth +import sttp.tapir.EndpointInput.AuthType.ApiKey import sttp.tapir.ztapir.* -import zio.* object ApiKeyEndpointSecurityLogic { - val apiKeyHeader: EndpointIO.Header[ApiKeyCredentials] = header[Option[String]]("apikey") - .mapTo[ApiKeyCredentials] - .description("API key") - - def securityLogic(credentials: ApiKeyCredentials)(authenticator: Authenticator): IO[ErrorResponse, Entity] = - authenticator.authenticate(credentials).mapError(error => AuthenticationError.toErrorResponse(error)) + val apiKeyHeader: Auth[ApiKeyCredentials, ApiKey] = auth.apiKey( + header[Option[String]]("apikey") + .mapTo[ApiKeyCredentials] + .description("API key") + ) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/DefaultEntityApiKeyAuthenticator.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/DefaultEntityApiKeyAuthenticator.scala deleted file mode 100644 index b7d8d70f5d..0000000000 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/DefaultEntityApiKeyAuthenticator.scala +++ /dev/null @@ -1,20 +0,0 @@ -package io.iohk.atala.iam.authentication.apikey - -import io.iohk.atala.agent.walletapi.model.Entity -import io.iohk.atala.iam.authentication.AuthenticationError -import zio.{IO, ZIO} - -import java.util.UUID - -object DefaultEntityApiKeyAuthenticator extends ApiKeyAuthenticator { - private val DefaultEntity = Entity(name = "default") - override def authenticate(apiKey: String): IO[AuthenticationError, Entity] = { - ZIO.succeed(DefaultEntity) - } - - override def add(entityId: UUID, apiKey: String): IO[AuthenticationError, Unit] = ZIO.succeed(()) - - override def delete(entityId: UUID, apiKey: String): IO[AuthenticationError, Unit] = ZIO.succeed(()) - - override def isEnabled = true -} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/JwtCredentials.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/JwtCredentials.scala new file mode 100644 index 0000000000..a303dfd522 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/JwtCredentials.scala @@ -0,0 +1,12 @@ +package io.iohk.atala.iam.authentication.oidc + +import io.iohk.atala.iam.authentication.AuthenticationError +import io.iohk.atala.iam.authentication.Credentials + +final case class JwtCredentials(token: Option[String]) extends Credentials + +final case class JwtAuthenticationError(message: String) extends AuthenticationError + +object JwtAuthenticationError { + val emptyToken = JwtAuthenticationError("Empty bearer token header provided") +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/JwtSecurityLogic.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/JwtSecurityLogic.scala new file mode 100644 index 0000000000..64a141d803 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/JwtSecurityLogic.scala @@ -0,0 +1,12 @@ +package io.iohk.atala.iam.authentication.oidc + +import sttp.tapir.EndpointInput.Auth +import sttp.tapir.EndpointInput.AuthType.Http +import sttp.tapir.ztapir.* + +object JwtSecurityLogic { + val jwtAuthHeader: Auth[JwtCredentials, Http] = auth + .bearer[Option[String]]() + .mapTo[JwtCredentials] + .securitySchemeName("jwtAuth") +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticator.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticator.scala new file mode 100644 index 0000000000..c402ed02d4 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticator.scala @@ -0,0 +1,33 @@ +package io.iohk.atala.iam.authentication.oidc + +import io.iohk.atala.agent.walletapi.model.BaseEntity +import io.iohk.atala.iam.authentication.AuthenticationError +import io.iohk.atala.iam.authentication.AuthenticationError.AuthenticationMethodNotEnabled +import io.iohk.atala.iam.authentication.AuthenticationError.InvalidCredentials +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ +import io.iohk.atala.iam.authentication.Credentials +import zio.* + +import java.util.UUID + +final case class KeycloakEntity(id: UUID, rawToken: String) extends BaseEntity + +trait KeycloakAuthenticator extends AuthenticatorWithAuthZ[KeycloakEntity] { + def authenticate(credentials: Credentials): IO[AuthenticationError, KeycloakEntity] = { + if (isEnabled) { + credentials match { + case JwtCredentials(Some(token)) if token.nonEmpty => authenticate(token) + case JwtCredentials(Some(_)) => + ZIO.logInfo(s"Keycloak authentication is enabled, but bearer token is empty") *> + ZIO.fail(JwtAuthenticationError.emptyToken) + case JwtCredentials(None) => + ZIO.logInfo(s"Keycloak authentication is enabled, but bearer token is not provided") *> + ZIO.fail(InvalidCredentials("Bearer token is not provided")) + case other => + ZIO.fail(InvalidCredentials("Bearer token is not provided")) + } + } else ZIO.fail(AuthenticationMethodNotEnabled("Keycloak authentication is not enabled")) + } + + def authenticate(token: String): IO[AuthenticationError, KeycloakEntity] +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorImpl.scala new file mode 100644 index 0000000000..9254a868bf --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorImpl.scala @@ -0,0 +1,103 @@ +package io.iohk.atala.iam.authentication.oidc + +import io.iohk.atala.agent.walletapi.service.WalletManagementService +import io.iohk.atala.iam.authentication.AuthenticationError +import io.iohk.atala.iam.authentication.AuthenticationError.AuthenticationMethodNotEnabled +import io.iohk.atala.shared.models.WalletId +import pdi.jwt.JwtCirce +import pdi.jwt.JwtOptions +import zio.* +import zio.json.ast.Json + +import java.util.UUID +import scala.util.Try + +class KeycloakAuthenticatorImpl( + client: KeycloakClient, + keycloakConfig: KeycloakConfig, + walletService: WalletManagementService +) extends KeycloakAuthenticator { + + override def isEnabled: Boolean = keycloakConfig.enabled + + override def authenticate(token: String): IO[AuthenticationError, KeycloakEntity] = { + if (isEnabled) { + for { + introspection <- client.introspectToken(token) + _ <- ZIO + .fail(AuthenticationError.InvalidCredentials("The accessToken is invalid.")) + .unless(introspection.active) + entityId <- ZIO + .fromOption(introspection.sub) + .mapError(_ => AuthenticationError.UnexpectedError("Subject ID is not found in the accessToken.")) + .flatMap { id => + ZIO + .attempt(UUID.fromString(id)) + .mapError(e => AuthenticationError.UnexpectedError(s"Subject ID in accessToken is not a UUID. $e")) + } + } yield KeycloakEntity(entityId, token) + } else ZIO.fail(AuthenticationMethodNotEnabled("Keycloak authentication is not enabled")) + } + + override def authorize(entity: KeycloakEntity): IO[AuthenticationError, WalletId] = { + val token = entity.rawToken + for { + isRpt <- inferIsRpt(entity.rawToken) + rptEffect = + if (isRpt) ZIO.succeed(token) + else if (keycloakConfig.autoUpgradeToRPT) client.getRpt(token) + else ZIO.fail(AuthenticationError.InvalidCredentials(s"AccessToken is not RPT.")) + rpt <- rptEffect.logError("Fail to obtail RPT for wallet permissions") + permittedResources <- client.checkPermissions(rpt) + walletId <- getPermittedWallet(permittedResources) + } yield walletId + } + + private def getPermittedWallet(resourceIds: Seq[String]): IO[AuthenticationError, WalletId] = { + val walletIds = resourceIds.flatMap(id => Try(UUID.fromString(id)).toOption).map(WalletId.fromUUID) + walletService + .getWallets(walletIds) + .mapError(e => AuthenticationError.UnexpectedError(e.toThrowable.getMessage())) + .flatMap { + case head +: Nil => ZIO.succeed(head.id) + case Nil => + ZIO.fail(AuthenticationError.ResourceNotPermitted("No wallet permissions found.")) + case ls => + ZIO.fail( + AuthenticationError.UnexpectedError("Too many wallet access granted, the wallet access is ambiguous.") + ) + } + } + + /** Return true if the token is RPT. Check whether property '.authorization' exists. */ + private def inferIsRpt(token: String): IO[AuthenticationError, Boolean] = + ZIO + .fromTry(JwtCirce.decode(token, JwtOptions(false, false, false))) + .mapError(e => AuthenticationError.InvalidCredentials(s"JWT token cannot be decoded. ${e.getMessage()}")) + .flatMap { claims => + ZIO + .fromEither(Json.decoder.decodeJson(claims.content)) + .mapError(s => AuthenticationError.InvalidCredentials(s"Unable to decode JWT payload to JSON. $s")) + } + .flatMap { json => + ZIO + .fromOption(json.asObject) + .mapError(_ => AuthenticationError.InvalidCredentials(s"JWT payload must be a JSON object")) + .map(obj => obj.contains("authorization")) + } +} + +object KeycloakAuthenticatorImpl { + val layer: RLayer[KeycloakClient & KeycloakConfig & WalletManagementService, KeycloakAuthenticator] = + ZLayer.fromFunction(KeycloakAuthenticatorImpl(_, _, _)) + + val disabled: ULayer[KeycloakAuthenticator] = + ZLayer.succeed { + val notEnabledError = ZIO.fail(AuthenticationMethodNotEnabled("Keycloak authentication is not enabled")) + new KeycloakAuthenticator { + override def isEnabled: Boolean = false + override def authenticate(token: String): IO[AuthenticationError, KeycloakEntity] = notEnabledError + override def authorize(entity: KeycloakEntity): IO[AuthenticationError, WalletId] = notEnabledError + } + } +} 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 new file mode 100644 index 0000000000..40cb947598 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala @@ -0,0 +1,115 @@ +package io.iohk.atala.iam.authentication.oidc + +import io.iohk.atala.iam.authentication.AuthenticationError +import org.keycloak.authorization.client.AuthzClient +import org.keycloak.authorization.client.{Configuration => KeycloakAuthzConfig} +import org.keycloak.representations.idm.authorization.AuthorizationRequest +import zio.* +import zio.http.* +import zio.json.* + +import scala.jdk.CollectionConverters.* + +final case class TokenIntrospection(active: Boolean, sub: Option[String]) + +object TokenIntrospection { + given JsonEncoder[TokenIntrospection] = JsonEncoder.derived + given JsonDecoder[TokenIntrospection] = JsonDecoder.derived +} + +trait KeycloakClient { + + def getRpt(accessToken: String): IO[AuthenticationError, String] + + def introspectToken(token: String): IO[AuthenticationError, TokenIntrospection] + + /** Return list of permitted resources */ + def checkPermissions(rpt: String): IO[AuthenticationError, List[String]] + +} + +class KeycloakClientImpl(client: AuthzClient, httpClient: Client, keycloakConfig: KeycloakConfig) + extends KeycloakClient { + + private val introspectionUrl = client.getServerConfiguration().getIntrospectionEndpoint() + + private val baseFormHeaders = Headers(Header.ContentType(MediaType.application.`x-www-form-urlencoded`)) + + // TODO: support offline introspection + // TODO: tests + // https://www.keycloak.org/docs/22.0.4/securing_apps/#_token_introspection_endpoint + override def introspectToken(token: String): IO[AuthenticationError, TokenIntrospection] = { + for { + response <- Client + .request( + url = introspectionUrl, + method = Method.POST, + headers = baseFormHeaders ++ Headers( + Header.Authorization.Basic(keycloakConfig.clientId, keycloakConfig.clientSecret) + ), + content = Body.fromURLEncodedForm( + Form( + FormField.simpleField("token", token) + ) + ) + ) + .logError("Fail to introspect token on keycloak.") + .mapError(e => AuthenticationError.UnexpectedError("Fail to introspect the token on keyclaok.")) + .provide(ZLayer.succeed(httpClient)) + body <- response.body.asString + .logError("Fail parse keycloak introspection response.") + .mapError(e => AuthenticationError.UnexpectedError("Fail parse keycloak introspection response.")) + result <- + if (response.status.code == 200) { + ZIO + .fromEither(body.fromJson[TokenIntrospection]) + .logError("Fail to decode keycloak token introspection 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 { + val authResource = client.authorization(accessToken) + val request = AuthorizationRequest() + authResource.authorize(request) + } + .logError + .mapBoth( + e => AuthenticationError.UnexpectedError(e.getMessage()), + response => response.getToken() + ) + + override def checkPermissions(rpt: String): IO[AuthenticationError, List[String]] = + for { + introspection <- ZIO + .attemptBlocking(client.protection().introspectRequestingPartyToken(rpt)) + .logError + .mapError(e => AuthenticationError.UnexpectedError(e.getMessage())) + permissions = introspection.getPermissions().asScala.toList + } yield permissions.map(_.getResourceId()) + +} + +object KeycloakClientImpl { + val layer: RLayer[KeycloakConfig & Client, KeycloakClient] = ZLayer.fromZIO { + for { + httpClient <- ZIO.service[Client] + keycloakConfig <- ZIO.service[KeycloakConfig] + config = KeycloakAuthzConfig( + keycloakConfig.keycloakUrl.toString(), + keycloakConfig.realmName, + keycloakConfig.clientId, + Map("secret" -> keycloakConfig.clientSecret).asJava, + null + ) + client <- ZIO.attempt(AuthzClient.create(config)) + } yield KeycloakClientImpl(client, httpClient, keycloakConfig) + } + +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakConfig.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakConfig.scala new file mode 100644 index 0000000000..9ebcf4f7ea --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakConfig.scala @@ -0,0 +1,19 @@ +package io.iohk.atala.iam.authentication.oidc + +import zio.* +import io.iohk.atala.agent.server.config.AppConfig +import java.net.URL + +final case class KeycloakConfig( + enabled: Boolean, + keycloakUrl: URL, + realmName: String, + clientId: String, + clientSecret: String, + autoUpgradeToRPT: Boolean +) + +object KeycloakConfig { + val layer: URLayer[AppConfig, KeycloakConfig] = + ZLayer.fromFunction((conf: AppConfig) => conf.agent.authentication.keycloak) +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/entity/http/EntityServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/entity/http/EntityServerEndpoints.scala index bc04b3ebe7..57920cfb56 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/entity/http/EntityServerEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/entity/http/EntityServerEndpoints.scala @@ -1,9 +1,10 @@ package io.iohk.atala.iam.entity.http -import io.iohk.atala.agent.walletapi.model.Entity +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.api.http.model.PaginationInput import io.iohk.atala.api.http.{ErrorResponse, RequestContext} import io.iohk.atala.iam.authentication.Authenticator +import io.iohk.atala.iam.authentication.DefaultAuthenticator import io.iohk.atala.iam.authentication.admin.{AdminApiKeyCredentials, AdminApiKeySecurityLogic} import io.iohk.atala.iam.entity.http.EntityEndpoints.* import io.iohk.atala.iam.entity.http.controller.EntityController @@ -18,15 +19,15 @@ import zio.{IO, URIO, ZIO} import java.util.UUID -class EntityServerEndpoints(entityController: EntityController, authenticator: Authenticator) { +class EntityServerEndpoints(entityController: EntityController, authenticator: Authenticator[BaseEntity]) { - private def adminApiSecurityLogic(credentials: AdminApiKeyCredentials): IO[ErrorResponse, Entity] = + private def adminApiSecurityLogic(credentials: AdminApiKeyCredentials): IO[ErrorResponse, BaseEntity] = AdminApiKeySecurityLogic.securityLogic(credentials)(authenticator) val createEntityServerEndpoint: ZServerEndpoint[Any, Any] = createEntityEndpoint .zServerSecurityLogic(adminApiSecurityLogic) .serverLogic { - case entity: Entity => { case (rc: RequestContext, request: CreateEntityRequest) => + case entity => { case (rc: RequestContext, request: CreateEntityRequest) => entityController.createEntity(request)(rc) } } @@ -34,7 +35,7 @@ class EntityServerEndpoints(entityController: EntityController, authenticator: A val updateEntityNameServerEndpoint: ZServerEndpoint[Any, Any] = updateEntityNameEndpoint .zServerSecurityLogic(adminApiSecurityLogic) .serverLogic { - case entity: Entity => { case (rc: RequestContext, id: UUID, request: UpdateEntityNameRequest) => + case entity => { case (rc: RequestContext, id: UUID, request: UpdateEntityNameRequest) => entityController.updateEntityName(id, request.name)(rc) } @@ -43,7 +44,7 @@ class EntityServerEndpoints(entityController: EntityController, authenticator: A val updateEntityWalletIdServerEndpoint: ZServerEndpoint[Any, Any] = updateEntityWalletIdEndpoint .zServerSecurityLogic(adminApiSecurityLogic) .serverLogic { - case entity: Entity => { case (rc: RequestContext, id: UUID, request: UpdateEntityWalletIdRequest) => + case entity => { case (rc: RequestContext, id: UUID, request: UpdateEntityWalletIdRequest) => entityController.updateEntityWalletId(id, request.walletId)(rc) } } @@ -51,7 +52,7 @@ class EntityServerEndpoints(entityController: EntityController, authenticator: A val getEntityByIdServerEndpoint: ZServerEndpoint[Any, Any] = getEntityByIdEndpoint .zServerSecurityLogic(adminApiSecurityLogic) .serverLogic { - case entity: Entity => { case (rc: RequestContext, id: UUID) => + case entity => { case (rc: RequestContext, id: UUID) => entityController.getEntity(id)(rc) } } @@ -59,7 +60,7 @@ class EntityServerEndpoints(entityController: EntityController, authenticator: A val getEntitiesServerEndpoint: ZServerEndpoint[Any, Any] = getEntitiesEndpoint .zServerSecurityLogic(adminApiSecurityLogic) .serverLogic { - case entity: Entity => { case (rc: RequestContext, paginationIn: PaginationInput) => + case entity => { case (rc: RequestContext, paginationIn: PaginationInput) => entityController.getEntities(paginationIn)(rc) } } @@ -67,7 +68,7 @@ class EntityServerEndpoints(entityController: EntityController, authenticator: A val deleteEntityByIdServerEndpoint: ZServerEndpoint[Any, Any] = deleteEntityByIdEndpoint .zServerSecurityLogic(adminApiSecurityLogic) .serverLogic { - case entity: Entity => { case (rc: RequestContext, id: UUID) => + case entity => { case (rc: RequestContext, id: UUID) => entityController.deleteEntity(id)(rc) } } @@ -75,7 +76,7 @@ class EntityServerEndpoints(entityController: EntityController, authenticator: A val addEntityApiKeyAuthenticationServerEndpoint: ZServerEndpoint[Any, Any] = addEntityApiKeyAuthenticationEndpoint .zServerSecurityLogic(adminApiSecurityLogic) .serverLogic { - case entity: Entity => { case (rc: RequestContext, request: ApiKeyAuthenticationRequest) => + case entity => { case (rc: RequestContext, request: ApiKeyAuthenticationRequest) => entityController.addApiKeyAuth(request.entityId, request.apiKey)(rc) } } @@ -84,7 +85,7 @@ class EntityServerEndpoints(entityController: EntityController, authenticator: A deleteEntityApiKeyAuthenticationEndpoint .zServerSecurityLogic(adminApiSecurityLogic) .serverLogic { - case entity: Entity => { case (rc: RequestContext, request: ApiKeyAuthenticationRequest) => + case entity => { case (rc: RequestContext, request: ApiKeyAuthenticationRequest) => entityController.deleteApiKeyAuth(request.entityId, request.apiKey)(rc) } } @@ -102,10 +103,10 @@ class EntityServerEndpoints(entityController: EntityController, authenticator: A } object EntityServerEndpoints { - def all: URIO[EntityController & Authenticator, List[ZServerEndpoint[Any, Any]]] = { + def all: URIO[EntityController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { for { entityController <- ZIO.service[EntityController] - auth <- ZIO.service[Authenticator] + auth <- ZIO.service[DefaultAuthenticator] entityEndpoints = new EntityServerEndpoints(entityController, auth) } yield entityEndpoints.all } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/WalletManagementServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/WalletManagementServerEndpoints.scala index d6d13603ae..1fa98cecf5 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/WalletManagementServerEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/WalletManagementServerEndpoints.scala @@ -1,17 +1,21 @@ package io.iohk.atala.iam.wallet.http -import io.iohk.atala.agent.walletapi.model.Entity +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.api.http.ErrorResponse import io.iohk.atala.iam.authentication.Authenticator +import io.iohk.atala.iam.authentication.DefaultAuthenticator import io.iohk.atala.iam.authentication.admin.AdminApiKeyCredentials import io.iohk.atala.iam.authentication.admin.AdminApiKeySecurityLogic import io.iohk.atala.iam.wallet.http.controller.WalletManagementController import sttp.tapir.ztapir.* import zio.* -class WalletManagementServerEndpoints(controller: WalletManagementController, authenticator: Authenticator) { +class WalletManagementServerEndpoints( + controller: WalletManagementController, + authenticator: Authenticator[BaseEntity] +) { - private def adminApiSecurityLogic(credentials: AdminApiKeyCredentials): IO[ErrorResponse, Entity] = + private def adminApiSecurityLogic(credentials: AdminApiKeyCredentials): IO[ErrorResponse, BaseEntity] = AdminApiKeySecurityLogic.securityLogic(credentials)(authenticator) val listWalletServerEndpoint: ZServerEndpoint[Any, Any] = @@ -38,10 +42,10 @@ class WalletManagementServerEndpoints(controller: WalletManagementController, au } object WalletManagementServerEndpoints { - def all: URIO[WalletManagementController & Authenticator, List[ZServerEndpoint[Any, Any]]] = { + def all: URIO[WalletManagementController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { for { walletManagementController <- ZIO.service[WalletManagementController] - auth <- ZIO.service[Authenticator] + auth <- ZIO.service[DefaultAuthenticator] walletManagementServerEndpoints = WalletManagementServerEndpoints(walletManagementController, auth) } yield walletManagementServerEndpoints.all } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/IssueEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/IssueEndpoints.scala index 2b87f5cd70..d50f8ff9aa 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/IssueEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/IssueEndpoints.scala @@ -5,6 +5,8 @@ import io.iohk.atala.api.http.model.PaginationInput import io.iohk.atala.api.http.{ErrorResponse, RequestContext} import io.iohk.atala.iam.authentication.apikey.ApiKeyCredentials import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader +import io.iohk.atala.iam.authentication.oidc.JwtCredentials +import io.iohk.atala.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader import io.iohk.atala.issue.controller.http.* import sttp.model.StatusCode import sttp.tapir.* @@ -15,7 +17,7 @@ object IssueEndpoints { private val paginationInput: EndpointInput[PaginationInput] = EndpointInput.derived[PaginationInput] val createCredentialOffer: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, CreateIssueCredentialRecordRequest), ErrorResponse, IssueCredentialRecord, @@ -23,6 +25,7 @@ object IssueEndpoints { ] = endpoint.post .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in("issue-credentials" / "credential-offers") .in(jsonBody[CreateIssueCredentialRecordRequest].description("The credential offer object.")) @@ -35,7 +38,7 @@ object IssueEndpoints { .name("createCredentialOffer") val getCredentialRecords: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, PaginationInput, Option[String]), ErrorResponse, IssueCredentialRecordPage, @@ -43,6 +46,7 @@ object IssueEndpoints { ] = endpoint.get .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in("issue-credentials" / "records") .in(paginationInput) @@ -55,7 +59,7 @@ object IssueEndpoints { .name("getCredentialRecords") val getCredentialRecord: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, String), ErrorResponse, IssueCredentialRecord, @@ -63,6 +67,7 @@ object IssueEndpoints { ] = endpoint.get .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( "issue-credentials" / "records" / path[String]("recordId").description( @@ -77,7 +82,7 @@ object IssueEndpoints { .name("getCredentialRecord") val acceptCredentialOffer: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, String, AcceptCredentialOfferRequest), ErrorResponse, IssueCredentialRecord, @@ -85,6 +90,7 @@ object IssueEndpoints { ] = endpoint.post .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( "issue-credentials" / "records" / path[String]("recordId").description( @@ -101,7 +107,7 @@ object IssueEndpoints { .name("acceptCredentialOffer") val issueCredential: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, String), ErrorResponse, IssueCredentialRecord, @@ -109,6 +115,7 @@ object IssueEndpoints { ] = endpoint.post .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( "issue-credentials" / "records" / path[String]("recordId").description( diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/IssueServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/IssueServerEndpoints.scala index 03c4e88a6e..1e6fc90e10 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/IssueServerEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/IssueServerEndpoints.scala @@ -1,69 +1,75 @@ package io.iohk.atala.issue.controller +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.api.http.RequestContext import io.iohk.atala.api.http.model.PaginationInput import io.iohk.atala.iam.authentication.Authenticator -import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic +import io.iohk.atala.iam.authentication.Authorizer +import io.iohk.atala.iam.authentication.DefaultAuthenticator +import io.iohk.atala.iam.authentication.SecurityLogic import io.iohk.atala.issue.controller.IssueEndpoints.* import io.iohk.atala.issue.controller.http.{AcceptCredentialOfferRequest, CreateIssueCredentialRecordRequest} import io.iohk.atala.shared.models.WalletAccessContext import sttp.tapir.ztapir.* import zio.* -class IssueServerEndpoints(issueController: IssueController, authenticator: Authenticator) { +class IssueServerEndpoints( + issueController: IssueController, + authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] +) { val createCredentialOfferEndpoint: ZServerEndpoint[Any, Any] = createCredentialOffer - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, request: CreateIssueCredentialRecordRequest) => issueController .createCredentialOffer(request)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } val getCredentialRecordsEndpoint: ZServerEndpoint[Any, Any] = getCredentialRecords - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, paginationInput: PaginationInput, thid: Option[String]) => issueController .getCredentialRecords(paginationInput, thid)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } val getCredentialRecordEndpoint: ZServerEndpoint[Any, Any] = getCredentialRecord - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, recordId: String) => issueController .getCredentialRecord(recordId)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } val acceptCredentialOfferEndpoint: ZServerEndpoint[Any, Any] = acceptCredentialOffer - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, recordId: String, request: AcceptCredentialOfferRequest) => issueController .acceptCredentialOffer(recordId, request)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } val issueCredentialEndpoint: ZServerEndpoint[Any, Any] = issueCredential - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, recordId: String) => issueController .issueCredential(recordId)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } @@ -78,9 +84,9 @@ class IssueServerEndpoints(issueController: IssueController, authenticator: Auth } object IssueServerEndpoints { - def all: URIO[IssueController & Authenticator, List[ZServerEndpoint[Any, Any]]] = { + def all: URIO[IssueController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { for { - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[DefaultAuthenticator] issueController <- ZIO.service[IssueController] issueEndpoints = new IssueServerEndpoints(issueController, authenticator) } yield issueEndpoints.all diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionRegistryEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionRegistryEndpoints.scala index 4cc0099b3a..73a9b4d40b 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionRegistryEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionRegistryEndpoints.scala @@ -6,6 +6,8 @@ import io.iohk.atala.api.http.codec.OrderCodec.* import io.iohk.atala.api.http.model.{Order, PaginationInput} import io.iohk.atala.iam.authentication.apikey.ApiKeyCredentials import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader +import io.iohk.atala.iam.authentication.oidc.JwtCredentials +import io.iohk.atala.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader import io.iohk.atala.pollux.credentialdefinition.http.{ CredentialDefinitionInput, CredentialDefinitionResponse, @@ -31,7 +33,7 @@ import java.util.UUID object CredentialDefinitionRegistryEndpoints { val createCredentialDefinitionEndpoint: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, CredentialDefinitionInput), ErrorResponse, CredentialDefinitionResponse, @@ -39,6 +41,7 @@ object CredentialDefinitionRegistryEndpoints { ] = endpoint.post .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in("credential-definition-registry" / "definitions") .in( @@ -111,7 +114,7 @@ object CredentialDefinitionRegistryEndpoints { private val credentialDefinitionFilterInput: EndpointInput[http.FilterInput] = EndpointInput.derived[http.FilterInput] private val paginationInput: EndpointInput[PaginationInput] = EndpointInput.derived[PaginationInput] val lookupCredentialDefinitionsByQueryEndpoint: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), ( RequestContext, FilterInput, @@ -124,6 +127,7 @@ object CredentialDefinitionRegistryEndpoints { ] = endpoint.get .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( "credential-definition-registry" / "definitions".description( diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionRegistryServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionRegistryServerEndpoints.scala index 98305f6939..2cc5d69e13 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionRegistryServerEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionRegistryServerEndpoints.scala @@ -1,10 +1,12 @@ package io.iohk.atala.pollux.credentialdefinition -import io.iohk.atala.agent.walletapi.model.Entity +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.api.http.model.{Order, PaginationInput} import io.iohk.atala.api.http.{ErrorResponse, RequestContext} import io.iohk.atala.iam.authentication.Authenticator -import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic +import io.iohk.atala.iam.authentication.Authorizer +import io.iohk.atala.iam.authentication.DefaultAuthenticator +import io.iohk.atala.iam.authentication.SecurityLogic import io.iohk.atala.pollux.credentialdefinition import io.iohk.atala.pollux.credentialdefinition.CredentialDefinitionRegistryEndpoints.* import io.iohk.atala.pollux.credentialdefinition.controller.CredentialDefinitionController @@ -16,19 +18,19 @@ import java.util.UUID class CredentialDefinitionRegistryServerEndpoints( credentialDefinitionController: CredentialDefinitionController, - authenticator: Authenticator + authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] ) { def throwableToInternalServerError(throwable: Throwable) = ZIO.fail[ErrorResponse](ErrorResponse.internalServerError(detail = Option(throwable.getMessage))) val createCredentialDefinitionServerEndpoint: ZServerEndpoint[Any, Any] = createCredentialDefinitionEndpoint - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) .serverLogic { - case entity: Entity => { case (ctx: RequestContext, credentialDefinitionInput: CredentialDefinitionInput) => + case wac => { case (ctx: RequestContext, credentialDefinitionInput: CredentialDefinitionInput) => credentialDefinitionController .createCredentialDefinition(credentialDefinitionInput)(ctx) - .provideSomeLayer(entity.wacLayer) + .provideSomeLayer(ZLayer.succeed(wac)) } } @@ -44,9 +46,9 @@ class CredentialDefinitionRegistryServerEndpoints( val lookupCredentialDefinitionsByQueryServerEndpoint: ZServerEndpoint[Any, Any] = lookupCredentialDefinitionsByQueryEndpoint - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) .serverLogic { - case entity: Entity => { + case wac => { case ( ctx: RequestContext, filter: FilterInput, @@ -59,7 +61,7 @@ class CredentialDefinitionRegistryServerEndpoints( paginationInput.toPagination, order )(ctx) - .provideSomeLayer(entity.wacLayer) + .provideSomeLayer(ZLayer.succeed(wac)) } } @@ -73,10 +75,10 @@ class CredentialDefinitionRegistryServerEndpoints( } object CredentialDefinitionRegistryServerEndpoints { - def all: URIO[CredentialDefinitionController & Authenticator, List[ZServerEndpoint[Any, Any]]] = { + def all: URIO[CredentialDefinitionController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { for { credentialDefinitionRegistryService <- ZIO.service[CredentialDefinitionController] - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[DefaultAuthenticator] credentialDefinitionRegistryEndpoints = new CredentialDefinitionRegistryServerEndpoints( credentialDefinitionRegistryService, authenticator diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/SchemaRegistryEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/SchemaRegistryEndpoints.scala index 9c7a2c2af0..d77b0f0068 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/SchemaRegistryEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/SchemaRegistryEndpoints.scala @@ -5,6 +5,9 @@ import io.iohk.atala.api.http.EndpointOutputs.* import io.iohk.atala.api.http.codec.OrderCodec.* import io.iohk.atala.api.http.model.{Order, PaginationInput} import io.iohk.atala.iam.authentication.apikey.ApiKeyCredentials +import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader +import io.iohk.atala.iam.authentication.oidc.JwtCredentials +import io.iohk.atala.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader import io.iohk.atala.pollux.credentialschema.http.{ CredentialSchemaInput, CredentialSchemaResponse, @@ -24,14 +27,13 @@ import sttp.tapir.{ statusCode, stringToPath } -import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader import java.util.UUID object SchemaRegistryEndpoints { val createSchemaEndpoint: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, CredentialSchemaInput), ErrorResponse, CredentialSchemaResponse, @@ -39,6 +41,7 @@ object SchemaRegistryEndpoints { ] = endpoint.post .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in("schema-registry" / "schemas") .in( @@ -65,7 +68,7 @@ object SchemaRegistryEndpoints { .tag("Schema Registry") val updateSchemaEndpoint: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, String, UUID, CredentialSchemaInput), ErrorResponse, CredentialSchemaResponse, @@ -73,6 +76,7 @@ object SchemaRegistryEndpoints { ] = endpoint.put .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( "schema-registry" / @@ -127,7 +131,7 @@ object SchemaRegistryEndpoints { private val credentialSchemaFilterInput: EndpointInput[FilterInput] = EndpointInput.derived[FilterInput] private val paginationInput: EndpointInput[PaginationInput] = EndpointInput.derived[PaginationInput] val lookupSchemasByQueryEndpoint: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), ( RequestContext, FilterInput, @@ -140,6 +144,7 @@ object SchemaRegistryEndpoints { ] = endpoint.get .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in("schema-registry" / "schemas".description("Lookup schemas by query")) .in(credentialSchemaFilterInput) @@ -155,7 +160,7 @@ object SchemaRegistryEndpoints { .tag("Schema Registry") val testEndpoint: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), RequestContext, ErrorResponse, String, @@ -163,6 +168,7 @@ object SchemaRegistryEndpoints { ] = endpoint.get .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in( "schema-registry" / "test" .description("Debug endpoint") diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/SchemaRegistryServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/SchemaRegistryServerEndpoints.scala index 5b943b3cd0..5668a2c205 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/SchemaRegistryServerEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/SchemaRegistryServerEndpoints.scala @@ -1,46 +1,47 @@ package io.iohk.atala.pollux.credentialschema +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.api.http.model.{Order, PaginationInput} import io.iohk.atala.api.http.{ErrorResponse, RequestContext} import io.iohk.atala.iam.authentication.Authenticator -import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic +import io.iohk.atala.iam.authentication.Authorizer +import io.iohk.atala.iam.authentication.DefaultAuthenticator +import io.iohk.atala.iam.authentication.SecurityLogic import io.iohk.atala.pollux.credentialschema.SchemaRegistryEndpoints.* import io.iohk.atala.pollux.credentialschema.controller.CredentialSchemaController import io.iohk.atala.pollux.credentialschema.http.{CredentialSchemaInput, FilterInput} import io.iohk.atala.shared.models.WalletAccessContext import sttp.tapir.ztapir.* import zio.* -import io.iohk.atala.agent.walletapi.model.Entity import java.util.UUID class SchemaRegistryServerEndpoints( credentialSchemaController: CredentialSchemaController, - authenticator: Authenticator + authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] ) { def throwableToInternalServerError(throwable: Throwable) = ZIO.fail[ErrorResponse](ErrorResponse.internalServerError(detail = Option(throwable.getMessage))) val createSchemaServerEndpoint: ZServerEndpoint[Any, Any] = createSchemaEndpoint - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) .serverLogic { - case entity: Entity => { case (ctx: RequestContext, schemaInput: CredentialSchemaInput) => + case wac => { case (ctx: RequestContext, schemaInput: CredentialSchemaInput) => credentialSchemaController .createSchema(schemaInput)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } val updateSchemaServerEndpoint: ZServerEndpoint[Any, Any] = updateSchemaEndpoint - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) .serverLogic { - case entity: Entity => { - case (ctx: RequestContext, author: String, id: UUID, schemaInput: CredentialSchemaInput) => - credentialSchemaController - .updateSchema(author, id, schemaInput)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + case wac => { case (ctx: RequestContext, author: String, id: UUID, schemaInput: CredentialSchemaInput) => + credentialSchemaController + .updateSchema(author, id, schemaInput)(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) } } @@ -52,9 +53,9 @@ class SchemaRegistryServerEndpoints( val lookupSchemasByQueryServerEndpoint: ZServerEndpoint[Any, Any] = lookupSchemasByQueryEndpoint - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) .serverLogic { - case entity: Entity => { + case wac => { case ( ctx: RequestContext, filter: FilterInput, @@ -67,16 +68,16 @@ class SchemaRegistryServerEndpoints( paginationInput.toPagination, order )(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } val testServerEndpoint: ZServerEndpoint[Any, Any] = testEndpoint - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) .serverLogic { - case entity: Entity => { case requestContext: RequestContext => - ZIO.succeed(requestContext.request.toString + " " + entity.walletAccessContext.toString) + case wac => { case requestContext: RequestContext => + ZIO.succeed(requestContext.request.toString + " " + wac.toString) } } @@ -91,9 +92,9 @@ class SchemaRegistryServerEndpoints( } object SchemaRegistryServerEndpoints { - def all: URIO[CredentialSchemaController & Authenticator, List[ZServerEndpoint[Any, Any]]] = { + def all: URIO[CredentialSchemaController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { for { - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[DefaultAuthenticator] schemaRegistryService <- ZIO.service[CredentialSchemaController] schemaRegistryEndpoints = new SchemaRegistryServerEndpoints( schemaRegistryService, diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/VerificationPolicyEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/VerificationPolicyEndpoints.scala index 69f61eeeba..781771bae5 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/VerificationPolicyEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/VerificationPolicyEndpoints.scala @@ -6,6 +6,8 @@ import io.iohk.atala.api.http.codec.OrderCodec.* import io.iohk.atala.api.http.model.{Order, PaginationInput} import io.iohk.atala.iam.authentication.apikey.ApiKeyCredentials import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader +import io.iohk.atala.iam.authentication.oidc.JwtCredentials +import io.iohk.atala.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader import io.iohk.atala.pollux.credentialschema.http.* import sttp.model.StatusCode import sttp.tapir.* @@ -16,13 +18,14 @@ import java.util.UUID object VerificationPolicyEndpoints { val createVerificationPolicyEndpoint: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, VerificationPolicyInput), ErrorResponse, VerificationPolicy, Any ] = endpoint.post .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in("verification" / "policies") .in( @@ -47,7 +50,7 @@ object VerificationPolicyEndpoints { .tag("Verification") val updateVerificationPolicyEndpoint: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, UUID, Int, VerificationPolicyInput), ErrorResponse, VerificationPolicy, @@ -55,6 +58,7 @@ object VerificationPolicyEndpoints { ] = endpoint.put .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in("verification" / "policies" / path[UUID]("id")) .in( @@ -78,7 +82,7 @@ object VerificationPolicyEndpoints { .tag("Verification") val getVerificationPolicyByIdEndpoint: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, UUID), ErrorResponse, VerificationPolicy, @@ -86,6 +90,7 @@ object VerificationPolicyEndpoints { ] = endpoint.get .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( "verification" / "policies" / path[UUID]("id") @@ -101,7 +106,7 @@ object VerificationPolicyEndpoints { .tag("Verification") val deleteVerificationPolicyByIdEndpoint: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, UUID), ErrorResponse, Unit, @@ -109,6 +114,7 @@ object VerificationPolicyEndpoints { ] = endpoint.delete .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( "verification" / "policies" / path[UUID]("id") @@ -128,7 +134,7 @@ object VerificationPolicyEndpoints { .tag("Verification") val lookupVerificationPoliciesByQueryEndpoint: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, VerificationPolicy.Filter, PaginationInput, Option[Order]), ErrorResponse, VerificationPolicyPage, @@ -136,6 +142,7 @@ object VerificationPolicyEndpoints { ] = endpoint.get .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( "verification" / "policies" diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/VerificationPolicyServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/VerificationPolicyServerEndpoints.scala index d49f093ab9..d546051b0d 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/VerificationPolicyServerEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/pollux/credentialschema/VerificationPolicyServerEndpoints.scala @@ -1,9 +1,12 @@ package io.iohk.atala.pollux.credentialschema +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.api.http.model.{Order, PaginationInput} import io.iohk.atala.api.http.{ErrorResponse, RequestContext} import io.iohk.atala.iam.authentication.Authenticator -import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic +import io.iohk.atala.iam.authentication.Authorizer +import io.iohk.atala.iam.authentication.DefaultAuthenticator +import io.iohk.atala.iam.authentication.SecurityLogic import io.iohk.atala.pollux.credentialschema.VerificationPolicyEndpoints.* import io.iohk.atala.pollux.credentialschema.controller.VerificationPolicyController import io.iohk.atala.pollux.credentialschema.http.{VerificationPolicy, VerificationPolicyInput} @@ -12,26 +15,29 @@ import java.util.UUID import sttp.tapir.ztapir.* import zio.* -class VerificationPolicyServerEndpoints(controller: VerificationPolicyController, authenticator: Authenticator) { +class VerificationPolicyServerEndpoints( + controller: VerificationPolicyController, + authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] +) { def throwableToInternalServerError(throwable: Throwable) = ZIO.fail[ErrorResponse](ErrorResponse.internalServerError(detail = Option(throwable.getMessage))) // TODO: make the endpoint typed ZServerEndpoint[SchemaRegistryService, Any] val createVerificationPolicyServerEndpoint: ZServerEndpoint[Any, Any] = createVerificationPolicyEndpoint - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, input: VerificationPolicyInput) => controller .createVerificationPolicy(ctx, input) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } val updateVerificationPolicyServerEndpoint: ZServerEndpoint[Any, Any] = { updateVerificationPolicyEndpoint - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case ( ctx: RequestContext, @@ -41,37 +47,37 @@ class VerificationPolicyServerEndpoints(controller: VerificationPolicyController ) => controller .updateVerificationPolicyById(ctx, id, nonce, update) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } } val getVerificationPolicyByIdServerEndpoint: ZServerEndpoint[Any, Any] = getVerificationPolicyByIdEndpoint - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, id: UUID) => controller .getVerificationPolicyById(ctx, id) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } val deleteVerificationPolicyByIdServerEndpoint: ZServerEndpoint[Any, Any] = deleteVerificationPolicyByIdEndpoint - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, id: UUID) => controller .deleteVerificationPolicyById(ctx, id) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } val lookupVerificationPoliciesByQueryServerEndpoint: ZServerEndpoint[Any, Any] = lookupVerificationPoliciesByQueryEndpoint - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case ( ctx: RequestContext, @@ -86,7 +92,7 @@ class VerificationPolicyServerEndpoints(controller: VerificationPolicyController paginationInput.toPagination, order ) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } @@ -101,9 +107,9 @@ class VerificationPolicyServerEndpoints(controller: VerificationPolicyController } object VerificationPolicyServerEndpoints { - def all: URIO[VerificationPolicyController & Authenticator, List[ZServerEndpoint[Any, Any]]] = { + def all: URIO[VerificationPolicyController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { for { - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[DefaultAuthenticator] controller <- ZIO.service[VerificationPolicyController] endpoints = new VerificationPolicyServerEndpoints(controller, authenticator) } yield endpoints.all diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofEndpoints.scala index 2f1836ff16..03a98d29ef 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofEndpoints.scala @@ -5,6 +5,8 @@ import io.iohk.atala.api.http.model.PaginationInput import io.iohk.atala.api.http.{ErrorResponse, RequestContext} import io.iohk.atala.iam.authentication.apikey.ApiKeyCredentials import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader +import io.iohk.atala.iam.authentication.oidc.JwtCredentials +import io.iohk.atala.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader import io.iohk.atala.presentproof.controller.http.* import sttp.model.StatusCode import sttp.tapir.* @@ -17,7 +19,7 @@ object PresentProofEndpoints { private val paginationInput: EndpointInput[PaginationInput] = EndpointInput.derived[PaginationInput] val requestPresentation: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, RequestPresentationInput), ErrorResponse, PresentationStatus, @@ -29,6 +31,7 @@ object PresentProofEndpoints { .summary("As a Verifier, create a new proof presentation request and send it to the Prover.") .description("Holder presents proof derived from the verifiable credential to verifier.") .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in("present-proof" / "presentations") .in(extractFromRequest[RequestContext](RequestContext.apply)) .in(jsonBody[RequestPresentationInput].description("The present proof creation request.")) @@ -41,7 +44,7 @@ object PresentProofEndpoints { .errorOut(basicFailureAndNotFoundAndForbidden) val getAllPresentations: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, PaginationInput, Option[String]), ErrorResponse, PresentationStatusPage, @@ -53,6 +56,7 @@ object PresentProofEndpoints { .summary("Gets the list of proof presentation records.") .description("list of presentation statuses") .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in("present-proof" / "presentations") .in(extractFromRequest[RequestContext](RequestContext.apply)) .in(paginationInput) @@ -61,7 +65,8 @@ object PresentProofEndpoints { .out(jsonBody[PresentationStatusPage]) .errorOut(basicFailuresAndForbidden) - val getPresentation: Endpoint[ApiKeyCredentials, (RequestContext, UUID), ErrorResponse, PresentationStatus, Any] = + val getPresentation + : Endpoint[(ApiKeyCredentials, JwtCredentials), (RequestContext, UUID), ErrorResponse, PresentationStatus, Any] = endpoint.get .tag("Present Proof") .name("getPresentation") @@ -71,6 +76,7 @@ object PresentProofEndpoints { ) .description("Returns an existing presentation record by id.") .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( "present-proof" / "presentations" / path[UUID]("presentationId").description( @@ -82,7 +88,7 @@ object PresentProofEndpoints { .errorOut(basicFailureAndNotFoundAndForbidden) val updatePresentation: Endpoint[ - ApiKeyCredentials, + (ApiKeyCredentials, JwtCredentials), (RequestContext, UUID, RequestPresentationAction), ErrorResponse, PresentationStatus, @@ -97,6 +103,7 @@ object PresentProofEndpoints { ) .description("Accept or reject presentation of proof request.") .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in(extractFromRequest[RequestContext](RequestContext.apply)) .in( "present-proof" / "presentations" / path[UUID]("presentationId").description( diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofServerEndpoints.scala index 8b303fbe62..509be10655 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofServerEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofServerEndpoints.scala @@ -1,9 +1,12 @@ package io.iohk.atala.presentproof.controller +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.api.http.RequestContext import io.iohk.atala.api.http.model.PaginationInput import io.iohk.atala.iam.authentication.Authenticator -import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic +import io.iohk.atala.iam.authentication.Authorizer +import io.iohk.atala.iam.authentication.DefaultAuthenticator +import io.iohk.atala.iam.authentication.SecurityLogic import io.iohk.atala.presentproof.controller.PresentProofEndpoints.{ getAllPresentations, getPresentation, @@ -19,49 +22,49 @@ import java.util.UUID class PresentProofServerEndpoints( presentProofController: PresentProofController, - authenticator: Authenticator + authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] ) { private val requestPresentationEndpoint: ZServerEndpoint[Any, Any] = requestPresentation - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, request: RequestPresentationInput) => presentProofController .requestPresentation(request)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } private val getAllPresentationsEndpoint: ZServerEndpoint[Any, Any] = getAllPresentations - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, paginationInput: PaginationInput, thid: Option[String]) => presentProofController .getPresentations(paginationInput, thid)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } private val getPresentationEndpoint: ZServerEndpoint[Any, Any] = getPresentation - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, presentationId: UUID) => presentProofController .getPresentation(presentationId)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } private val updatePresentationEndpoint: ZServerEndpoint[Any, Any] = updatePresentation - .zServerSecurityLogic(ApiKeyEndpointSecurityLogic.securityLogic(_)(authenticator)) - .serverLogic { entity => + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .serverLogic { wac => { case (ctx: RequestContext, presentationId: UUID, action: RequestPresentationAction) => presentProofController .updatePresentation(presentationId, action)(ctx) - .provideSomeLayer(ZLayer.succeed(entity.walletAccessContext)) + .provideSomeLayer(ZLayer.succeed(wac)) } } @@ -74,9 +77,9 @@ class PresentProofServerEndpoints( } object PresentProofServerEndpoints { - def all: URIO[PresentProofController & Authenticator, List[ZServerEndpoint[Any, Any]]] = { + def all: URIO[PresentProofController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { for { - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[DefaultAuthenticator] presentProofController <- ZIO.service[PresentProofController] presentProofEndpoints = new PresentProofServerEndpoints(presentProofController, authenticator) } yield presentProofEndpoints.all 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 561a40c690..5e494fd159 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 @@ -54,12 +54,21 @@ object AgentInitializationSpec extends ZIOSpecDefault, PostgresTestContainerSupp _ <- AgentInitialization.run.overrideConfig() } yield assertCompletes ), - test("fail when both apikey and default wallet are disabled")( + test("do not fail the default wallet is disabled, but any authentication is enabled") { + for { + _ <- AgentInitialization.run + .overrideConfig(enableDefaultWallet = false, enableApiKey = true, enableKeycloak = false) + _ <- AgentInitialization.run + .overrideConfig(enableDefaultWallet = false, enableApiKey = false, enableKeycloak = true) + } yield assertCompletes + }, + test("fail when the default wallet is disabled and authentication is set to apiKey but disabled")( for { exit <- AgentInitialization.run .overrideConfig( + enableDefaultWallet = false, enableApiKey = false, - enableDefaultWallet = false + enableKeycloak = false ) .exit } yield assert(exit)(fails(isSubtype[RuntimeException](anything))) @@ -133,7 +142,8 @@ object AgentInitializationSpec extends ZIOSpecDefault, PostgresTestContainerSupp seed: Option[String] = None, webhookUrl: Option[URL] = None, webhookApiKey: Option[String] = None, - enableApiKey: Boolean = true + enableApiKey: Boolean = true, + enableKeycloak: Boolean = false, ): ZIO[R, E, A] = { for { appConfig <- ZIO.service[AppConfig] @@ -141,6 +151,7 @@ object AgentInitializationSpec extends ZIOSpecDefault, PostgresTestContainerSupp defaultWalletConfig = agentConfig.defaultWallet authConfig = agentConfig.authentication apiKeyConfig = authConfig.apiKey + keycloakConfig = authConfig.keycloak // consider using lens result <- effect.provideSomeLayer( ZLayer.succeed( @@ -149,6 +160,9 @@ object AgentInitializationSpec extends ZIOSpecDefault, PostgresTestContainerSupp authentication = authConfig.copy( apiKey = apiKeyConfig.copy( enabled = enableApiKey + ), + keycloak = keycloakConfig.copy( + enabled = enableKeycloak ) ), defaultWallet = defaultWalletConfig.copy( diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala index d015d6dd40..6d07a1bb41 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala @@ -4,7 +4,7 @@ import io.iohk.atala.agent.server.AgentHttpServer import io.iohk.atala.castor.controller.{DIDController, DIDRegistrarController} import io.iohk.atala.connect.controller.ConnectionController import io.iohk.atala.event.controller.EventController -import io.iohk.atala.iam.authentication.Authenticator +import io.iohk.atala.iam.authentication.DefaultAuthenticator import io.iohk.atala.iam.entity.http.controller.EntityController import io.iohk.atala.iam.wallet.http.controller.WalletManagementController import io.iohk.atala.issue.controller.IssueController @@ -44,7 +44,7 @@ object Tapir2StaticOAS extends ZIOAppDefault { ZLayer.succeed(mock[SystemController]) ++ ZLayer.succeed(mock[EntityController]) ++ ZLayer.succeed(mock[WalletManagementController]) ++ - ZLayer.succeed(mock[Authenticator]) ++ + ZLayer.succeed(mock[DefaultAuthenticator]) ++ ZLayer.succeed(mock[EventController]) ) } diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/issue/controller/IssueControllerImplSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/issue/controller/IssueControllerImplSpec.scala index 47d03f3b10..e56aee9703 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/issue/controller/IssueControllerImplSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/issue/controller/IssueControllerImplSpec.scala @@ -1,10 +1,11 @@ package io.iohk.atala.issue.controller +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService} import io.iohk.atala.api.http.ErrorResponse import io.iohk.atala.castor.core.service.MockDIDService import io.iohk.atala.container.util.MigrationAspects.migrate -import io.iohk.atala.iam.authentication.Authenticator +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ import io.iohk.atala.issue.controller.http.AcceptCredentialOfferRequest import sttp.client3.ziojson.* import sttp.client3.{DeserializationException, UriContext, basicRequest} @@ -25,7 +26,7 @@ object IssueControllerImplSpec extends ZIOSpecDefault with IssueControllerTestTo test("provide incorrect recordId to endpoint") { for { issueControllerService <- ZIO.service[IssueController] - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] backend = httpBackend(issueControllerService, authenticator) response: IssueCredentialBadRequestResponse <- basicRequest .post(uri"${issueUriBase}/records/12345/accept-offer") 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 20c8f4f545..5883b36e7e 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 @@ -4,12 +4,14 @@ import com.typesafe.config.ConfigFactory import io.grpc.ManagedChannelBuilder import io.iohk.atala.agent.server.config.AppConfig import io.iohk.atala.agent.walletapi.memory.GenericSecretStorageInMemory +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.agent.walletapi.service.MockManagedDIDService import io.iohk.atala.api.http.ErrorResponse import io.iohk.atala.castor.core.service.MockDIDService import io.iohk.atala.connect.core.repository.ConnectionRepositoryInMemory import io.iohk.atala.connect.core.service.ConnectionServiceImpl -import io.iohk.atala.iam.authentication.{Authenticator, DefaultEntityAuthenticator} +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ +import io.iohk.atala.iam.authentication.DefaultEntityAuthenticator import io.iohk.atala.iris.proto.service.IrisServiceGrpc import io.iohk.atala.issue.controller.http.{ CreateIssueCredentialRecordRequest, @@ -115,7 +117,7 @@ trait IssueControllerTestTools extends PostgresTestContainerSupport { .defaultHandlers(ErrorResponse.failureResponseHandler) } - def httpBackend(controller: IssueController, authenticator: Authenticator) = { + def httpBackend(controller: IssueController, authenticator: AuthenticatorWithAuthZ[BaseEntity]) = { val issueEndpoints = IssueServerEndpoints(controller, authenticator) val backend = diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala index c7996f371b..4b53450a27 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala @@ -1,10 +1,11 @@ package io.iohk.atala.pollux.credentialdefinition +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.agent.walletapi.model.Entity import io.iohk.atala.agent.walletapi.storage.GenericSecretStorage import io.iohk.atala.api.http.ErrorResponse import io.iohk.atala.container.util.MigrationAspects.* -import io.iohk.atala.iam.authentication.Authenticator +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ import io.iohk.atala.pollux.core.model.secret.CredentialDefinitionSecret import io.iohk.atala.pollux.core.service.serdes.{ PrivateCredentialDefinitionSchemaSerDesV1, @@ -65,7 +66,7 @@ object CredentialDefinitionBasicSpec extends ZIOSpecDefault with CredentialDefin val backendZIO = for { controller <- ZIO.service[CredentialDefinitionController] - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] } yield httpBackend(controller, authenticator) def createCredentialDefinitionResponseZIO = for { diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionFailureSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionFailureSpec.scala index 0ca39c620b..ba8202862f 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionFailureSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionFailureSpec.scala @@ -1,9 +1,10 @@ package io.iohk.atala.pollux.credentialdefinition +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.agent.walletapi.service.MockManagedDIDService import io.iohk.atala.api.http.ErrorResponse import io.iohk.atala.container.util.MigrationAspects.migrate -import io.iohk.atala.iam.authentication.Authenticator +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ import io.iohk.atala.pollux.credentialdefinition.controller.CredentialDefinitionController import sttp.client3.ziojson.* import sttp.client3.{DeserializationException, basicRequest} @@ -26,7 +27,7 @@ object CredentialDefinitionFailureSpec extends ZIOSpecDefault with CredentialDef test("create the credential definition with wrong json body returns BadRequest as json") { for { credentialDefinitionRegistryService <- ZIO.service[CredentialDefinitionController] - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] backend = httpBackend(credentialDefinitionRegistryService, authenticator) response: CredentialDefinitionBadRequestResponse <- basicRequest .post(credentialDefinitionUriBase) diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionLookupAndPaginationSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionLookupAndPaginationSpec.scala index e13b2c6c3f..525b7bfa70 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionLookupAndPaginationSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionLookupAndPaginationSpec.scala @@ -1,7 +1,8 @@ package io.iohk.atala.pollux.credentialdefinition +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.container.util.MigrationAspects.migrate -import io.iohk.atala.iam.authentication.Authenticator +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ import io.iohk.atala.pollux.credentialdefinition.controller.CredentialDefinitionController import io.iohk.atala.pollux.credentialdefinition.http.{CredentialDefinitionResponse, CredentialDefinitionResponsePage} import io.iohk.atala.shared.models.{WalletAccessContext, WalletId} @@ -21,10 +22,12 @@ object CredentialDefinitionLookupAndPaginationSpec def fetchAllPages( uri: Uri - ): ZIO[CredentialDefinitionController & Authenticator, Throwable, List[CredentialDefinitionResponsePage]] = { + ): ZIO[CredentialDefinitionController & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ + CredentialDefinitionResponsePage + ]] = { for { controller <- ZIO.service[CredentialDefinitionController] - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] backend = httpBackend(controller, authenticator) response: CredentialDefinitionResponsePageType <- for { @@ -76,7 +79,7 @@ object CredentialDefinitionLookupAndPaginationSpec for { _ <- deleteAllCredentialDefinitions controller <- ZIO.service[CredentialDefinitionController] - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] backend = httpBackend(controller, authenticator) inputs <- Generator.credentialDefinitionInput.runCollectN(10) 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 4b609533d9..9f95169b62 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 @@ -2,12 +2,14 @@ package io.iohk.atala.pollux.credentialdefinition import com.dimafeng.testcontainers.PostgreSQLContainer import io.iohk.atala.agent.walletapi.memory.GenericSecretStorageInMemory +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.agent.walletapi.model.{ManagedDIDState, PublicationState} import io.iohk.atala.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService} import io.iohk.atala.agent.walletapi.storage.GenericSecretStorage import io.iohk.atala.api.http.ErrorResponse import io.iohk.atala.castor.core.model.did.PrismDIDOperation -import io.iohk.atala.iam.authentication.{Authenticator, DefaultEntityAuthenticator} +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ +import io.iohk.atala.iam.authentication.DefaultEntityAuthenticator import io.iohk.atala.pollux.core.repository.CredentialDefinitionRepository import io.iohk.atala.pollux.core.service.{ CredentialDefinitionService, @@ -73,12 +75,12 @@ trait CredentialDefinitionTestTools extends PostgresTestContainerSupport { ) ) - val authenticatorLayer: TaskLayer[Authenticator] = DefaultEntityAuthenticator.layer + val authenticatorLayer: TaskLayer[AuthenticatorWithAuthZ[BaseEntity]] = DefaultEntityAuthenticator.layer lazy val testEnvironmentLayer = ZLayer.makeSome[ ManagedDIDService, CredentialDefinitionController & CredentialDefinitionRepository & CredentialDefinitionService & - PostgreSQLContainer & Authenticator & GenericSecretStorage + PostgreSQLContainer & AuthenticatorWithAuthZ[BaseEntity] & GenericSecretStorage ]( controllerLayer, pgContainerLayer, @@ -92,7 +94,10 @@ trait CredentialDefinitionTestTools extends PostgresTestContainerSupport { .defaultHandlers(ErrorResponse.failureResponseHandler) } - def httpBackend(controller: CredentialDefinitionController, authenticator: Authenticator) = { + def httpBackend( + controller: CredentialDefinitionController, + authenticator: AuthenticatorWithAuthZ[BaseEntity] + ) = { val credentialDefinitionRegistryEndpoints = CredentialDefinitionRegistryServerEndpoints(controller, authenticator) val backend = @@ -181,10 +186,12 @@ trait CredentialDefinitionGen { def generateCredentialDefinitionsN( count: Int - ): ZIO[CredentialDefinitionController & Authenticator, Throwable, List[CredentialDefinitionInput]] = + ): ZIO[CredentialDefinitionController & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ + CredentialDefinitionInput + ]] = for { controller <- ZIO.service[CredentialDefinitionController] - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] backend = httpBackend(controller, authenticator) inputs <- Generator.credentialDefinitionInput.runCollectN(count) _ <- inputs diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaAnoncredSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaAnoncredSpec.scala index d5fc29523b..a39b89241f 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaAnoncredSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaAnoncredSpec.scala @@ -1,8 +1,9 @@ package io.iohk.atala.pollux.schema +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.api.http.ErrorResponse import io.iohk.atala.container.util.MigrationAspects.* -import io.iohk.atala.iam.authentication.Authenticator +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ import io.iohk.atala.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1 import io.iohk.atala.pollux.core.model.schema.`type`.{AnoncredSchemaType, CredentialJsonSchemaType} import io.iohk.atala.pollux.credentialschema.* @@ -50,7 +51,7 @@ object CredentialSchemaAnoncredSpec extends ZIOSpecDefault with CredentialSchema + wrapSpec(unsupportedSchemaSpec) + wrapSpec(wrongSchemaSpec) - private def wrapSpec(spec: Spec[CredentialSchemaController & Authenticator, Throwable]) = { + private def wrapSpec(spec: Spec[CredentialSchemaController & AuthenticatorWithAuthZ[BaseEntity], Throwable]) = { (spec @@ nondeterministic @@ sequential @@ timed @@ migrateEach( schema = "public", @@ -63,7 +64,7 @@ object CredentialSchemaAnoncredSpec extends ZIOSpecDefault with CredentialSchema private val schemaCreateAndGetOperationsSpec = { def getSchemaZIO(uuid: UUID) = for { controller <- ZIO.service[CredentialSchemaController] - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] backend = httpBackend(controller, authenticator) response <- basicRequest .get(credentialSchemaUriBase.addPath(uuid.toString)) @@ -130,7 +131,7 @@ object CredentialSchemaAnoncredSpec extends ZIOSpecDefault with CredentialSchema private def createResponse[B: JsonDecoder](schemaType: String) = { for { controller <- ZIO.service[CredentialSchemaController] - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] backend = httpBackend(controller, authenticator) response <- basicRequest diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaBasicSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaBasicSpec.scala index 41bb921d9b..083a57eeb3 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaBasicSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaBasicSpec.scala @@ -1,10 +1,11 @@ package io.iohk.atala.pollux.schema import com.dimafeng.testcontainers.PostgreSQLContainer +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.agent.walletapi.service.ManagedDIDService import io.iohk.atala.api.http.ErrorResponse import io.iohk.atala.container.util.MigrationAspects.* -import io.iohk.atala.iam.authentication.Authenticator +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ import io.iohk.atala.pollux.core.model.schema.`type`.CredentialJsonSchemaType import io.iohk.atala.pollux.credentialschema.* import io.iohk.atala.pollux.credentialschema.controller.CredentialSchemaController @@ -65,7 +66,7 @@ object CredentialSchemaBasicSpec extends ZIOSpecDefault with CredentialSchemaTes val backendZIO = for { controller <- ZIO.service[CredentialSchemaController] - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] } yield httpBackend(controller, authenticator) def createSchemaResponseZIO = for { diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaFailureSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaFailureSpec.scala index 8b20b3830c..49312e9591 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaFailureSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaFailureSpec.scala @@ -1,15 +1,16 @@ package io.iohk.atala.pollux.schema import com.dimafeng.testcontainers.PostgreSQLContainer +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.agent.walletapi.service.MockManagedDIDService import io.iohk.atala.api.http.ErrorResponse import io.iohk.atala.container.util.MigrationAspects.migrate -import io.iohk.atala.iam.authentication.Authenticator +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ import io.iohk.atala.pollux.credentialschema.* import io.iohk.atala.pollux.credentialschema.controller.CredentialSchemaController -import sttp.client3.ziojson.* import sttp.client3.DeserializationException import sttp.client3.basicRequest +import sttp.client3.ziojson.* import sttp.model.StatusCode import zio.* import zio.test.* @@ -26,7 +27,7 @@ object CredentialSchemaFailureSpec extends ZIOSpecDefault with CredentialSchemaT test("create the schema with wrong json body returns BadRequest as json") { for { controller <- ZIO.service[CredentialSchemaController] - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] backend = httpBackend(controller, authenticator) response: SchemaBadRequestResponse <- basicRequest .post(credentialSchemaUriBase) diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaLookupAndPaginationSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaLookupAndPaginationSpec.scala index 82f8adb3fd..160bc75633 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaLookupAndPaginationSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/schema/CredentialSchemaLookupAndPaginationSpec.scala @@ -1,8 +1,9 @@ package io.iohk.atala.pollux.schema import com.dimafeng.testcontainers.PostgreSQLContainer +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.container.util.MigrationAspects.migrate -import io.iohk.atala.iam.authentication.Authenticator +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ import io.iohk.atala.pollux.credentialschema.* import io.iohk.atala.pollux.credentialschema.controller.CredentialSchemaController import io.iohk.atala.pollux.credentialschema.http.{ @@ -27,10 +28,12 @@ object CredentialSchemaLookupAndPaginationSpec def fetchAllPages( uri: Uri - ): ZIO[CredentialSchemaController & Authenticator, Throwable, List[CredentialSchemaResponsePage]] = { + ): ZIO[CredentialSchemaController & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[ + CredentialSchemaResponsePage + ]] = { for { controller <- ZIO.service[CredentialSchemaController] - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] backend = httpBackend(controller, authenticator) response: SchemaPageResponse <- basicRequest .get(uri) @@ -73,7 +76,7 @@ object CredentialSchemaLookupAndPaginationSpec for { _ <- deleteAllCredentialSchemas controller <- ZIO.service[CredentialSchemaController] - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] backend = httpBackend(controller, authenticator) inputs <- Generator.schemaInput.runCollectN(101) 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 ee95147210..3d75024769 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 @@ -1,11 +1,13 @@ package io.iohk.atala.pollux.schema import com.dimafeng.testcontainers.PostgreSQLContainer +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.agent.walletapi.model.{ManagedDIDState, PublicationState} import io.iohk.atala.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService} import io.iohk.atala.api.http.ErrorResponse import io.iohk.atala.castor.core.model.did.PrismDIDOperation -import io.iohk.atala.iam.authentication.{Authenticator, DefaultEntityAuthenticator} +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ +import io.iohk.atala.iam.authentication.DefaultEntityAuthenticator import io.iohk.atala.pollux.core.model.schema.`type`.CredentialJsonSchemaType import io.iohk.atala.pollux.core.repository.CredentialSchemaRepository import io.iohk.atala.pollux.core.service.{CredentialSchemaService, CredentialSchemaServiceImpl} @@ -61,13 +63,13 @@ trait CredentialSchemaTestTools extends PostgresTestContainerSupport { ) ) - val authenticatorLayer: TaskLayer[Authenticator] = DefaultEntityAuthenticator.layer + val authenticatorLayer: TaskLayer[AuthenticatorWithAuthZ[BaseEntity]] = DefaultEntityAuthenticator.layer lazy val testEnvironmentLayer = ZLayer.makeSome[ ManagedDIDService, CredentialSchemaController & CredentialSchemaRepository & CredentialSchemaService & PostgreSQLContainer & - Authenticator + AuthenticatorWithAuthZ[BaseEntity] ]( CredentialSchemaControllerImpl.layer, CredentialSchemaServiceImpl.layer, @@ -85,7 +87,7 @@ trait CredentialSchemaTestTools extends PostgresTestContainerSupport { .defaultHandlers(ErrorResponse.failureResponseHandler) } - def httpBackend(controller: CredentialSchemaController, authenticator: Authenticator) = { + def httpBackend(controller: CredentialSchemaController, authenticator: AuthenticatorWithAuthZ[BaseEntity]) = { val schemaRegistryEndpoints = SchemaRegistryServerEndpoints(controller, authenticator) val backend = @@ -169,10 +171,10 @@ trait CredentialSchemaGen { def generateSchemasN( count: Int - ): ZIO[CredentialSchemaController & Authenticator, Throwable, List[CredentialSchemaInput]] = + ): ZIO[CredentialSchemaController & AuthenticatorWithAuthZ[BaseEntity], Throwable, List[CredentialSchemaInput]] = for { controller <- ZIO.service[CredentialSchemaController] - authenticator <- ZIO.service[Authenticator] + authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] backend = httpBackend(controller, authenticator) inputs <- Generator.schemaInput.runCollectN(count) _ <- inputs diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/Entity.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/Entity.scala index 785f915a1a..08f87d76eb 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/Entity.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/Entity.scala @@ -6,7 +6,11 @@ import zio.{ULayer, ZLayer} import java.time.Instant import java.util.UUID -case class Entity(id: UUID, name: String, walletId: UUID, createdAt: Instant, updatedAt: Instant) { +trait BaseEntity { + val id: UUID +} + +case class Entity(id: UUID, name: String, walletId: UUID, createdAt: Instant, updatedAt: Instant) extends BaseEntity { def withUpdatedAt(updatedAt: Instant = Instant.now()): Entity = copy(updatedAt = updatedAt) } diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/WalletManagementService.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/WalletManagementService.scala index 079442f10a..3fe2b79d4c 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/WalletManagementService.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/WalletManagementService.scala @@ -45,6 +45,8 @@ trait WalletManagementService { def getWallet(walletId: WalletId): IO[WalletManagementServiceError, Option[Wallet]] + def getWallets(walletIds: Seq[WalletId]): IO[WalletManagementServiceError, Seq[Wallet]] + /** @return A tuple containing a list of items and a count of total items */ def listWallets( offset: Option[Int] = None, diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceImpl.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceImpl.scala index 1e9161ca0f..4f95bdbb35 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceImpl.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceImpl.scala @@ -42,6 +42,11 @@ class WalletManagementServiceImpl( .getWallet(walletId) .mapError(e => e) + override def getWallets(walletIds: Seq[WalletId]): IO[WalletManagementServiceError, Seq[Wallet]] = + nonSecretStorage + .getWallets(walletIds) + .mapError(e => e) + override def listWallets( offset: Option[Int], limit: Option[Int] diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcWalletNonSecretStorage.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcWalletNonSecretStorage.scala index 31c62ac7f3..d760776156 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcWalletNonSecretStorage.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcWalletNonSecretStorage.scala @@ -20,6 +20,7 @@ import zio.* import java.net.URL import java.time.Instant import java.util.UUID +import cats.data.NonEmptyList class JdbcWalletNonSecretStorage(xa: Transactor[ContextAwareTask]) extends WalletNonSecretStorage { @@ -68,6 +69,31 @@ class JdbcWalletNonSecretStorage(xa: Transactor[ContextAwareTask]) extends Walle .mapError(WalletNonSecretStorageError.UnexpectedError.apply) } + override def getWallets(walletIds: Seq[WalletId]): IO[WalletNonSecretStorageError, Seq[Wallet]] = { + walletIds match + case Nil => ZIO.succeed(Nil) + case head +: tail => + val nel = NonEmptyList.of(head, tail: _*) + val conditionFragment = Fragments.in(fr"wallet_id", nel) + val cxnIO = + sql""" + | SELECT + | wallet_id, + | name, + | created_at, + | updated_at + | FROM public.wallet + | WHERE $conditionFragment + """.stripMargin + .query[WalletRow] + .to[List] + + cxnIO + .transactWithoutContext(xa) + .map(_.map(_.toDomain)) + .mapError(WalletNonSecretStorageError.UnexpectedError.apply) + } + override def listWallet( offset: Option[Int], limit: Option[Int] diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/WalletNonSecretStorage.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/WalletNonSecretStorage.scala index 71ccf1bb8c..7c48f4f506 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/WalletNonSecretStorage.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/WalletNonSecretStorage.scala @@ -36,6 +36,7 @@ object WalletNonSecretStorageError { trait WalletNonSecretStorage { def createWallet(wallet: Wallet, seedDigest: Array[Byte]): IO[WalletNonSecretStorageError, Wallet] def getWallet(walletId: WalletId): IO[WalletNonSecretStorageError, Option[Wallet]] + def getWallets(walletIds: Seq[WalletId]): IO[WalletNonSecretStorageError, Seq[Wallet]] def listWallet( offset: Option[Int] = None, limit: Option[Int] = None diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/SeedResolver.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/SeedResolver.scala deleted file mode 100644 index eba08156e3..0000000000 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/SeedResolver.scala +++ /dev/null @@ -1,69 +0,0 @@ -package io.iohk.atala.agent.walletapi.util - -import zio.* -import io.iohk.atala.shared.models.HexString -import io.iohk.atala.agent.walletapi.crypto.Apollo - -trait SeedResolver { - def resolve: Task[Array[Byte]] -} - -object SeedResolver { - def layer(isDevMode: Boolean = false): URLayer[Apollo, SeedResolver] = - ZLayer.fromFunction(SeedResolverImpl(_, isDevMode)) -} - -private class SeedResolverImpl(apollo: Apollo, isDevMode: Boolean) extends SeedResolver { - override def resolve: Task[Array[Byte]] = { - val seedEnv = - for { - _ <- ZIO.logInfo("Resolving a wallet seed using WALLET_SEED environment variable") - maybeSeed <- System - .env("WALLET_SEED") - .tapSome(seed => ZIO.logInfo(s"got wallet seed - ${seed}")) - .flatMap { - case Some(hex) => ZIO.fromTry(HexString.fromString(hex)).map(_.toByteArray).asSome - case None => ZIO.none - } - .tapError(e => - ZIO.logError( - "Failed to parse WALLET_SEED. The WALLET_SEED must be a hex string representing 64-bytes (128-characters) BIP39 seed" - ) - ) - _ <- ZIO.logInfo("WALLET_SEED environment is not found.").when(maybeSeed.isEmpty) - // When DEV_MODE=fase, the WALLET_SEED must be set. - _ <- ZIO - .fail(Exception("WALLET_SEED must be present when running with DEV_MODE=false")) - .when(maybeSeed.isEmpty && !isDevMode) - } yield maybeSeed - - val seedRand = - for { - _ <- ZIO.logInfo("Generating a new wallet seed") - seedWithMnemonic <- apollo.ecKeyFactory.randomBip32Seed() - (seed, mnemonic) = seedWithMnemonic - seedHex = HexString.fromByteArray(seed) - _ <- ZIO - .logInfo(s"New seed generated : $seedHex (${mnemonic.mkString("[", ", ", "]")})") - .when(isDevMode) - } yield seed - - seedEnv - .flatMap { - case Some(seed) => ZIO.succeed(seed) - case None => seedRand - } - .tap(seed => - ZIO - .fromEither(validateSeed(seed)) - .tapError(e => ZIO.logError(e)) - .mapError(Exception(_)) - ) - } - - private def validateSeed(seed: Array[Byte]): Either[String, Unit] = { - if (seed.length != 64) Left(s"The seed must be 64-bytes (got ${seed.length} bytes)") - else Right(()) - } - -}