From f2e74cd1957e7d76f6dccadd02b1ca5b794d02b1 Mon Sep 17 00:00:00 2001 From: patlo-iog Date: Thu, 9 Nov 2023 15:24:21 +0700 Subject: [PATCH 1/2] feat(prism-agent): add multi-tenant wallet self-service capability (#779) Signed-off-by: Pat Losoponkul --- infrastructure/shared/keycloak/init-script.sh | 6 +- .../src/main/resources/application.conf | 21 +--- .../io/iohk/atala/agent/server/Main.scala | 5 + .../io/iohk/atala/agent/server/Modules.scala | 35 +++++- .../atala/agent/server/PrismAgentApp.scala | 3 + .../atala/agent/server/config/AppConfig.scala | 2 - .../DIDRegistrarServerEndpoints.scala | 17 +-- .../ConnectionServerEndpoints.scala | 13 ++- .../controller/EventServerEndpoints.scala | 11 +- .../iam/authentication/Authenticator.scala | 20 +++- .../authentication/DefaultAuthenticator.scala | 11 +- .../iam/authentication/SecurityLogic.scala | 47 +++++--- .../admin/AdminApiKeyAuthenticator.scala | 12 +- .../admin/AdminApiKeyCredentials.scala | 3 +- .../admin/AdminApiKeySecurityLogic.scala | 12 +- .../apikey/ApiKeyAuthenticatorImpl.scala | 6 +- .../apikey/ApiKeyEndpointSecurityLogic.scala | 12 +- .../oidc/KeycloakAuthenticator.scala | 3 +- .../oidc/KeycloakAuthenticatorImpl.scala | 87 +++++++++----- .../authentication/oidc/KeycloakClient.scala | 50 ++++---- .../DefaultPermissionManagementService.scala | 46 ++++++++ .../EntityPermissionManagementService.scala | 41 +++++++ .../core/PermissionManagement.scala | 27 ++++- .../keycloak/admin/KeycloakAdmin.scala | 10 +- .../KeycloakPermissionManagementService.scala | 110 ++++++++++++------ .../http/WalletManagementEndpoints.scala | 49 +++++++- .../WalletManagementServerEndpoints.scala | 57 ++++++--- .../WalletManagementController.scala | 102 +++++++++++++--- .../CreateWalletUmaPermissionRequest.scala | 29 +++++ .../controller/IssueServerEndpoints.scala | 15 +-- ...ialDefinitionRegistryServerEndpoints.scala | 8 +- .../SchemaRegistryServerEndpoints.scala | 12 +- .../VerificationPolicyServerEndpoints.scala | 15 +-- .../PresentProofServerEndpoints.scala | 13 ++- .../server/AgentInitializationSpec.scala | 4 +- .../apikey/ApiKeyAuthenticatorSpec.scala | 9 +- .../oidc/KeycloakAuthenticatorSpec.scala | 37 ++---- .../keycloak/admin/KeycloakConfigUtils.scala | 1 - ...cloakPermissionManagementServiceSpec.scala | 35 +++--- .../controller/IssueControllerTestTools.scala | 2 +- .../CredentialDefinitionTestTools.scala | 3 +- .../schema/CredentialSchemaTestTools.scala | 2 +- .../atala/agent/walletapi/model/Entity.scala | 2 +- .../service/WalletManagementService.scala | 15 ++- .../service/WalletManagementServiceImpl.scala | 53 ++++++--- .../service/ManagedDIDServiceSpec.scala | 7 +- .../service/WalletManagementServiceSpec.scala | 7 +- .../storage/DIDSecretStorageSpec.scala | 10 +- .../storage/GenericSecretStorageSpec.scala | 11 +- .../walletapi/storage/StorageSpecHelper.scala | 2 + .../KeycloakTestContainerSupport.scala | 1 + .../atala/shared/models/MultiTenancy.scala | 15 +++ 52 files changed, 805 insertions(+), 321 deletions(-) create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/DefaultPermissionManagementService.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/core/EntityPermissionManagementService.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/model/CreateWalletUmaPermissionRequest.scala diff --git a/infrastructure/shared/keycloak/init-script.sh b/infrastructure/shared/keycloak/init-script.sh index 21649c22f7..a952bac0e4 100755 --- a/infrastructure/shared/keycloak/init-script.sh +++ b/infrastructure/shared/keycloak/init-script.sh @@ -80,8 +80,8 @@ create_realm $ADMIN_ACCESS_TOKEN echo "Creating a new prism-agent client ..." 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" + +echo "Creating a new sample user ..." +create_user $ADMIN_ACCESS_TOKEN "bob" "1234" diff --git a/prism-agent/service/server/src/main/resources/application.conf b/prism-agent/service/server/src/main/resources/application.conf index cfdf89342c..9864903c4f 100644 --- a/prism-agent/service/server/src/main/resources/application.conf +++ b/prism-agent/service/server/src/main/resources/application.conf @@ -133,7 +133,7 @@ agent { # 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 = true autoUpgradeToRPT = ${?KEYCLOAK_UMA_AUTO_UPGRADE_RPT} } } @@ -219,22 +219,3 @@ agent { authApiKey = ${?DEFAULT_WALLET_AUTH_API_KEY} } } - -keycloakAdmin { - serverUrl = "http://localhost:8080/auth", - serverUrl = ${?KEYCLOAK_SERVER_URL} - realm = "master", - realm = ${?KEYCLOAK_REALM} - username = "admin", - username = ${?KEYCLOAK_ADMIN_USERNAME} - password = "admin", - password = ${?KEYCLOAK_ADMIN_PASSWORD} - clientId = "admin-cli", - clientId = ${?KEYCLOAK_ADMIN_CLIENT_ID} - clientSecret = "", - clientSecret = ${?KEYCLOAK_ADMIN_CLIENT_SECRET} - authToken= "", - authToken = ${?KEYCLOAK_ADMIN_AUTH_TOKEN} - scope= "" - scope = ${?KEYCLOAK_ADMIN_SCOPE} -} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index c12ec5f452..aebb6e4456 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 @@ -22,6 +22,8 @@ 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.apikey.JdbcAuthenticationRepository +import io.iohk.atala.iam.authorization.DefaultPermissionManagementService +import io.iohk.atala.iam.authorization.core.EntityPermissionManagementService 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 @@ -155,7 +157,10 @@ object MainApp extends ZIOAppDefault { // authentication AppModule.builtInAuthenticatorLayer, AppModule.keycloakAuthenticatorLayer, + AppModule.keycloakPermissionManagementLayer, DefaultAuthenticator.layer, + DefaultPermissionManagementService.layer, + EntityPermissionManagementService.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 5f0ad2117d..0e732eefc8 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 @@ -30,17 +30,21 @@ 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.KeycloakAuthenticator 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.iam.authentication.oidc.KeycloakEntity +import io.iohk.atala.iam.authorization.core.PermissionManagement +import io.iohk.atala.iam.authorization.keycloak.admin.KeycloakPermissionManagementService import io.iohk.atala.pollux.vc.jwt.{PrismDidResolver, DidResolver as JwtDidResolver} import io.iohk.atala.prism.protos.node_api.NodeServiceGrpc import io.iohk.atala.shared.db.{ContextAwareTask, DbConfig, TransactorLayer} +import org.keycloak.authorization.client.AuthzClient 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 { @@ -74,20 +78,45 @@ object AppModule { ApiKeyAuthenticatorImpl.layer, ) - val keycloakAuthenticatorLayer: RLayer[AppConfig & WalletManagementService & Client, KeycloakAuthenticator] = + val keycloakAuthenticatorLayer: RLayer[ + AppConfig & WalletManagementService & Client & PermissionManagement.Service[KeycloakEntity], + KeycloakAuthenticator + ] = ZLayer.fromZIO { ZIO .serviceWith[AppConfig](_.agent.authentication.keycloak.enabled) .map { isEnabled => if (!isEnabled) KeycloakAuthenticatorImpl.disabled else - ZLayer.makeSome[AppConfig & WalletManagementService & Client, KeycloakAuthenticator]( + ZLayer.makeSome[ + AppConfig & WalletManagementService & Client & PermissionManagement.Service[KeycloakEntity], + KeycloakAuthenticator + ]( KeycloakConfig.layer, KeycloakAuthenticatorImpl.layer, + KeycloakClientImpl.authzClientLayer, KeycloakClientImpl.layer ) } }.flatten + + val keycloakPermissionManagementLayer + : RLayer[AppConfig & WalletManagementService & Client, PermissionManagement.Service[KeycloakEntity]] = { + ZLayer.fromZIO { + ZIO + .serviceWith[AppConfig](_.agent.authentication.keycloak.enabled) + .map { isEnabled => + if (!isEnabled) KeycloakPermissionManagementService.disabled + else + ZLayer.makeSome[AppConfig & WalletManagementService & Client, PermissionManagement.Service[KeycloakEntity]]( + KeycloakClientImpl.authzClientLayer, + KeycloakClientImpl.layer, + KeycloakConfig.layer, + KeycloakPermissionManagementService.layer + ) + } + }.flatten + } } object GrpcModule { diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala index a7442310ae..41fb335f3a 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala @@ -29,6 +29,7 @@ import io.iohk.atala.pollux.credentialschema.{SchemaRegistryServerEndpoints, Ver import io.iohk.atala.pollux.vc.jwt.DidResolver as JwtDidResolver import io.iohk.atala.presentproof.controller.PresentProofServerEndpoints import io.iohk.atala.resolvers.DIDResolver +import io.iohk.atala.shared.models.WalletAdministrationContext import io.iohk.atala.shared.models.{HexString, WalletAccessContext, WalletId} import io.iohk.atala.shared.utils.DurationOps.toMetricsSeconds import io.iohk.atala.system.controller.SystemServerEndpoints @@ -104,6 +105,7 @@ object PrismAgentApp { .catchAll(e => ZIO.logError(s"error while syncing DID publication state: $e")) .repeat(Schedule.spaced(10.seconds)) .unit + .provideSomeLayer(ZLayer.succeed(WalletAdministrationContext.Admin())) } @@ -153,6 +155,7 @@ object AgentInitialization { for { _ <- validateAppConfig _ <- initializeDefaultWallet + .provideSomeLayer(ZLayer.succeed(WalletAdministrationContext.Admin())) } yield () private val validateAppConfig = 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 7f461f72ff..b13225e9ea 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/config/AppConfig.scala @@ -2,7 +2,6 @@ package io.iohk.atala.agent.server.config import io.iohk.atala.castor.core.model.did.VerificationRelationship import io.iohk.atala.iam.authentication.AuthenticationConfig -import io.iohk.atala.iam.authorization.keycloak.admin.KeycloakAdminConfig import io.iohk.atala.pollux.vc.jwt.* import io.iohk.atala.shared.db.DbConfig import zio.config.* @@ -18,7 +17,6 @@ final case class AppConfig( agent: AgentConfig, connect: ConnectConfig, prismNode: PrismNodeConfig, - keycloakAdmin: KeycloakAdminConfig ) { def validate: Either[String, Unit] = for { diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/DIDRegistrarServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/DIDRegistrarServerEndpoints.scala index a49aac9940..ffa80099f0 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 @@ -11,12 +11,13 @@ import zio.* class DIDRegistrarServerEndpoints( didRegistrarController: DIDRegistrarController, - authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] + authenticator: Authenticator[BaseEntity], + authorizer: Authorizer[BaseEntity] ) { private val listManagedDidServerEndpoint: ZServerEndpoint[Any, Any] = DIDRegistrarEndpoints.listManagedDid - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (rc, paginationInput) => didRegistrarController @@ -27,7 +28,7 @@ class DIDRegistrarServerEndpoints( private val createManagedDidServerEndpoint: ZServerEndpoint[Any, Any] = DIDRegistrarEndpoints.createManagedDid - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (rc, createManagedDidRequest) => didRegistrarController @@ -38,7 +39,7 @@ class DIDRegistrarServerEndpoints( private val getManagedDidServerEndpoint: ZServerEndpoint[Any, Any] = DIDRegistrarEndpoints.getManagedDid - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (rc, did) => didRegistrarController @@ -49,7 +50,7 @@ class DIDRegistrarServerEndpoints( private val publishManagedDidServerEndpoint: ZServerEndpoint[Any, Any] = DIDRegistrarEndpoints.publishManagedDid - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (rc, did) => didRegistrarController @@ -60,7 +61,7 @@ class DIDRegistrarServerEndpoints( private val updateManagedDidServerEndpoint: ZServerEndpoint[Any, Any] = DIDRegistrarEndpoints.updateManagedDid - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (rc, did, updateRequest) => didRegistrarController @@ -71,7 +72,7 @@ class DIDRegistrarServerEndpoints( private val deactivateManagedDidServerEndpoint: ZServerEndpoint[Any, Any] = DIDRegistrarEndpoints.deactivateManagedDid - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (rc, did) => didRegistrarController @@ -96,7 +97,7 @@ object DIDRegistrarServerEndpoints { for { authenticator <- ZIO.service[DefaultAuthenticator] didRegistrarController <- ZIO.service[DIDRegistrarController] - didRegistrarEndpoints = new DIDRegistrarServerEndpoints(didRegistrarController, authenticator) + didRegistrarEndpoints = new DIDRegistrarServerEndpoints(didRegistrarController, authenticator, authenticator) } yield didRegistrarEndpoints.all } } 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 e2d9031403..629b52d136 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 @@ -16,12 +16,13 @@ import java.util.UUID class ConnectionServerEndpoints( connectionController: ConnectionController, - authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] + authenticator: Authenticator[BaseEntity], + authorizer: Authorizer[BaseEntity] ) { private val createConnectionServerEndpoint: ZServerEndpoint[Any, Any] = createConnection - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, request: CreateConnectionRequest) => connectionController @@ -32,7 +33,7 @@ class ConnectionServerEndpoints( private val getConnectionServerEndpoint: ZServerEndpoint[Any, Any] = getConnection - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, connectionId: UUID) => connectionController @@ -43,7 +44,7 @@ class ConnectionServerEndpoints( private val getConnectionsServerEndpoint: ZServerEndpoint[Any, Any] = getConnections - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, paginationInput: PaginationInput, thid: Option[String]) => connectionController @@ -54,7 +55,7 @@ class ConnectionServerEndpoints( private val acceptConnectionInvitationServerEndpoint: ZServerEndpoint[Any, Any] = acceptConnectionInvitation - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, request: AcceptConnectionInvitationRequest) => connectionController @@ -76,7 +77,7 @@ object ConnectionServerEndpoints { for { authenticator <- ZIO.service[DefaultAuthenticator] connectionController <- ZIO.service[ConnectionController] - connectionEndpoints = new ConnectionServerEndpoints(connectionController, authenticator) + connectionEndpoints = new ConnectionServerEndpoints(connectionController, authenticator, authenticator) } yield connectionEndpoints.all } } 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 5ecb90bb71..1b955375db 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 @@ -11,12 +11,13 @@ import io.iohk.atala.iam.authentication.SecurityLogic class EventServerEndpoints( eventController: EventController, - authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] + authenticator: Authenticator[BaseEntity], + authorizer: Authorizer[BaseEntity] ) { val createWebhookNotificationServerEndpoint: ZServerEndpoint[Any, Any] = EventEndpoints.createWebhookNotification - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (rc, createWebhook) => eventController @@ -27,7 +28,7 @@ class EventServerEndpoints( val listWebhookNotificationServerEndpoint: ZServerEndpoint[Any, Any] = EventEndpoints.listWebhookNotification - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => rc => eventController .listWebhookNotifications(rc) @@ -36,7 +37,7 @@ class EventServerEndpoints( val deleteWebhookNotificationServerEndpoint: ZServerEndpoint[Any, Any] = EventEndpoints.deleteWebhookNotification - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (rc, id) => eventController @@ -58,7 +59,7 @@ object EventServerEndpoints { for { authenticator <- ZIO.service[DefaultAuthenticator] eventController <- ZIO.service[EventController] - eventEndpoints = new EventServerEndpoints(eventController, authenticator) + eventEndpoints = new EventServerEndpoints(eventController, authenticator, authenticator) } yield eventEndpoints.all } } 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 b2e0f3477c..5623b8773d 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 @@ -3,6 +3,8 @@ 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.WalletAccessContext +import io.iohk.atala.shared.models.WalletAdministrationContext import io.iohk.atala.shared.models.WalletId import zio.{IO, ZIO, ZLayer} @@ -43,14 +45,22 @@ trait Authenticator[E <: BaseEntity] { } trait Authorizer[E <: BaseEntity] { - def authorize(entity: E): IO[AuthenticationError, WalletId] + def authorize(entity: E): IO[AuthenticationError, WalletAccessContext] + def authorizeWalletAdmin(entity: E): IO[AuthenticationError, WalletAdministrationContext] } object EntityAuthorizer extends EntityAuthorizer trait EntityAuthorizer extends Authorizer[Entity] { - override def authorize(entity: Entity): IO[AuthenticationError, WalletId] = - ZIO.succeed(entity.walletId).map(WalletId.fromUUID) + override def authorize(entity: Entity): IO[AuthenticationError, WalletAccessContext] = + ZIO.succeed(entity.walletId).map(WalletId.fromUUID).map(WalletAccessContext.apply) + + override def authorizeWalletAdmin(entity: Entity): IO[AuthenticationError, WalletAdministrationContext] = { + val ctx = + if (entity == Entity.Admin) WalletAdministrationContext.Admin() + else WalletAdministrationContext.SelfService(Seq(WalletId.fromUUID(entity.walletId))) + ZIO.succeed(ctx) + } } trait AuthenticatorWithAuthZ[E <: BaseEntity] extends Authenticator[E], Authorizer[E] @@ -59,8 +69,10 @@ object DefaultEntityAuthenticator extends AuthenticatorWithAuthZ[BaseEntity] { override def isEnabled: Boolean = true override def authenticate(credentials: Credentials): IO[AuthenticationError, BaseEntity] = ZIO.succeed(Entity.Default) - override def authorize(entity: BaseEntity): IO[AuthenticationError, WalletId] = + override def authorize(entity: BaseEntity): IO[AuthenticationError, WalletAccessContext] = EntityAuthorizer.authorize(Entity.Default) + override def authorizeWalletAdmin(entity: BaseEntity): IO[AuthenticationError, WalletAdministrationContext] = + EntityAuthorizer.authorizeWalletAdmin(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 a15e370f03..7de565a1a2 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 @@ -6,7 +6,8 @@ import io.iohk.atala.iam.authentication.admin.{AdminApiKeyAuthenticator, AdminAp 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 io.iohk.atala.shared.models.WalletAccessContext +import io.iohk.atala.shared.models.WalletAdministrationContext import zio.* case class DefaultAuthenticator( @@ -23,11 +24,17 @@ case class DefaultAuthenticator( case keycloakCredentials: JwtCredentials => keycloakAuthenticator(keycloakCredentials) } - override def authorize(entity: BaseEntity): IO[AuthenticationError, WalletId] = entity match { + override def authorize(entity: BaseEntity): IO[AuthenticationError, WalletAccessContext] = entity match { case entity: Entity => EntityAuthorizer.authorize(entity) case kcEntity: KeycloakEntity => keycloakAuthenticator.authorize(kcEntity) } + override def authorizeWalletAdmin(entity: BaseEntity): IO[AuthenticationError, WalletAdministrationContext] = + entity match { + case entity: Entity => EntityAuthorizer.authorizeWalletAdmin(entity) + case kcEntity: KeycloakEntity => keycloakAuthenticator.authorizeWalletAdmin(kcEntity) + } + } object 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 index f8a653e476..fa0b6f28ad 100644 --- 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 @@ -4,9 +4,11 @@ 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.admin.AdminApiKeyCredentials import io.iohk.atala.iam.authentication.apikey.ApiKeyCredentials import io.iohk.atala.iam.authentication.oidc.JwtCredentials import io.iohk.atala.shared.models.WalletAccessContext +import io.iohk.atala.shared.models.WalletAdministrationContext import zio.* object SecurityLogic { @@ -32,26 +34,43 @@ object SecurityLogic { .mapError(AuthenticationError.toErrorResponse) } + def authorize[E <: BaseEntity](entity: E)(authorizer: Authorizer[E]): IO[ErrorResponse, WalletAccessContext] = + authorizer + .authorize(entity) + .mapError(AuthenticationError.toErrorResponse) + def authorize[E <: BaseEntity](credentials: Credentials, others: Credentials*)( - authenticator: Authenticator[E] & Authorizer[E], - ): IO[ErrorResponse, WalletAccessContext] = { + authenticator: Authenticator[E], + authorizer: 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) + case Left(entity) => authorize(entity)(EntityAuthorizer) + case Right(entity) => authorize(entity)(authorizer) } - .map(walletId => WalletAccessContext(walletId)) - } def authorizeWith[E <: BaseEntity](credentials: (ApiKeyCredentials, JwtCredentials))( - authenticator: Authenticator[E] & Authorizer[E] + authenticator: Authenticator[E], + authorizer: Authorizer[E] ): IO[ErrorResponse, WalletAccessContext] = - authorize[E](credentials._2, credentials._1)(authenticator) + authorize[E](credentials._2, credentials._1)(authenticator, authorizer) + def authorizeWalletAdmin[E <: BaseEntity]( + entity: E + )(authorizer: Authorizer[E]): IO[ErrorResponse, WalletAdministrationContext] = + authorizer + .authorizeWalletAdmin(entity) + .mapError(AuthenticationError.toErrorResponse) + + def authorizeWalletAdminWith[E <: BaseEntity]( + credentials: (AdminApiKeyCredentials, ApiKeyCredentials, JwtCredentials) + )( + authenticator: Authenticator[E], + authorizer: Authorizer[E] + ): IO[ErrorResponse, (BaseEntity, WalletAdministrationContext)] = + authenticate[E](credentials._3, credentials._2, credentials._1)(authenticator) + .flatMap { + case Left(entity) => authorizeWalletAdmin(entity)(EntityAuthorizer).map(entity -> _) + case Right(entity) => authorizeWalletAdmin(entity)(authorizer).map(entity -> _) + } } 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 26d16db975..4a6659e8ae 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 @@ -4,19 +4,17 @@ import io.iohk.atala.agent.walletapi.model.Entity 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 +import zio.* trait AdminApiKeyAuthenticator extends AuthenticatorWithAuthZ[Entity], EntityAuthorizer { def authenticate(credentials: Credentials): IO[AuthenticationError, Entity] = { credentials match { - case AdminApiKeyCredentials(apiKey) => authenticate(apiKey) + case AdminApiKeyCredentials(Some(apiKey)) => authenticate(apiKey) + case AdminApiKeyCredentials(None) => + ZIO.logInfo(s"AdminApiKey API authentication is enabled, but `x-admin-api-key` token is empty") *> + ZIO.fail(AdminApiKeyAuthenticationError.emptyAdminApiKey) } } def authenticate(adminApiKey: String): IO[AuthenticationError, Entity] } - -object AdminApiKeyAuthenticator { - // TODO: probably, we need to add the roles to the entities, for now, it works like this - val Admin = Entity(name = "admin") -} 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 8bf823eb07..dcc25da320 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,6 +6,7 @@ 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 `x-admin-apikey` header provided") } -case class AdminApiKeyCredentials(apiKey: String) extends Credentials +case class AdminApiKeyCredentials(apiKey: Option[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 7eee5d689d..399a5da55f 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 @@ -4,14 +4,20 @@ 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 +import sttp.tapir.EndpointInput.Auth +import sttp.tapir.EndpointInput.AuthType.ApiKey import sttp.tapir.ztapir.* import zio.* object AdminApiKeySecurityLogic { - val adminApiKeyHeader: EndpointIO.Header[AdminApiKeyCredentials] = header[String]("x-admin-api-key") - .mapTo[AdminApiKeyCredentials] - .description("Admin API Key") + val adminApiKeyHeader: Auth[AdminApiKeyCredentials, ApiKey] = auth + .apiKey( + header[Option[String]]("x-admin-api-key") + .mapTo[AdminApiKeyCredentials] + .description("Admin API Key") + ) + .securitySchemeName("adminApiKeyAuth") def securityLogic[E <: BaseEntity]( credentials: AdminApiKeyCredentials diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorImpl.scala index 68302a3668..2707795a51 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorImpl.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorImpl.scala @@ -1,13 +1,14 @@ package io.iohk.atala.iam.authentication.apikey import io.iohk.atala.agent.walletapi.model.Entity +import io.iohk.atala.agent.walletapi.model.Wallet import io.iohk.atala.agent.walletapi.service.{EntityService, WalletManagementService} import io.iohk.atala.iam.authentication.AuthenticationError import io.iohk.atala.iam.authentication.AuthenticationError.* import io.iohk.atala.prism.crypto.Sha256 -import zio.{IO, URLayer, ZIO, ZLayer} -import io.iohk.atala.agent.walletapi.model.Wallet +import io.iohk.atala.shared.models.WalletAdministrationContext import io.iohk.atala.shared.models.WalletId +import zio.{IO, URLayer, ZIO, ZLayer} import java.util.UUID import scala.util.Try @@ -58,6 +59,7 @@ case class ApiKeyAuthenticatorImpl( wallet <- walletManagementService .createWallet(Wallet("Auto provisioned wallet", WalletId.random)) .mapError(cause => AuthenticationRepositoryError.UnexpectedError(cause)) + .provide(ZLayer.succeed(WalletAdministrationContext.Admin())) entityToCreate = Entity(name = "Auto provisioned entity", walletId = wallet.id.toUUID) entity <- entityService .create(entityToCreate) 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 ea02323e3d..cc8585ef6d 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 @@ -6,9 +6,11 @@ import sttp.tapir.EndpointInput.AuthType.ApiKey import sttp.tapir.ztapir.* object ApiKeyEndpointSecurityLogic { - val apiKeyHeader: Auth[ApiKeyCredentials, ApiKey] = auth.apiKey( - header[Option[String]]("apikey") - .mapTo[ApiKeyCredentials] - .description("API key") - ) + val apiKeyHeader: Auth[ApiKeyCredentials, ApiKey] = auth + .apiKey( + header[Option[String]]("apikey") + .mapTo[ApiKeyCredentials] + .description("API key") + ) + .securitySchemeName("apiKeyAuth") } 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 index c402ed02d4..f66b8d0b1b 100644 --- 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 @@ -10,7 +10,8 @@ import zio.* import java.util.UUID -final case class KeycloakEntity(id: UUID, rawToken: String) extends BaseEntity +final case class KeycloakEntity(id: UUID, accessToken: Option[String] = None, rpt: Option[String] = None) + extends BaseEntity trait KeycloakAuthenticator extends AuthenticatorWithAuthZ[KeycloakEntity] { def authenticate(credentials: Credentials): 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 index 9254a868bf..b1d37ca142 100644 --- 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 @@ -1,21 +1,21 @@ 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 io.iohk.atala.iam.authorization.core.PermissionManagement +import io.iohk.atala.shared.models.WalletAccessContext +import io.iohk.atala.shared.models.WalletAdministrationContext 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 + keycloakPermissionService: PermissionManagement.Service[KeycloakEntity], ) extends KeycloakAuthenticator { override def isEnabled: Boolean = keycloakConfig.enabled @@ -23,7 +23,9 @@ class KeycloakAuthenticatorImpl( override def authenticate(token: String): IO[AuthenticationError, KeycloakEntity] = { if (isEnabled) { for { - introspection <- client.introspectToken(token) + introspection <- client + .introspectToken(token) + .mapError(e => AuthenticationError.UnexpectedError(e.message)) _ <- ZIO .fail(AuthenticationError.InvalidCredentials("The accessToken is invalid.")) .unless(introspection.active) @@ -35,38 +37,54 @@ class KeycloakAuthenticatorImpl( .attempt(UUID.fromString(id)) .mapError(e => AuthenticationError.UnexpectedError(s"Subject ID in accessToken is not a UUID. $e")) } - } yield KeycloakEntity(entityId, token) + } yield KeycloakEntity(entityId, accessToken = Some(token)) } else ZIO.fail(AuthenticationMethodNotEnabled("Keycloak authentication is not enabled")) } - override def authorize(entity: KeycloakEntity): IO[AuthenticationError, WalletId] = { - val token = entity.rawToken + override def authorize(entity: KeycloakEntity): IO[AuthenticationError, WalletAccessContext] = { for { - isRpt <- inferIsRpt(entity.rawToken) + entityWithRpt <- populateEntityRpt(entity) + walletId <- keycloakPermissionService + .listWalletPermissions(entityWithRpt) + .mapError(e => AuthenticationError.UnexpectedError(e.message)) + .flatMap { + case head +: Nil => ZIO.succeed(head) + 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.") + ) + } + .provide(ZLayer.succeed(WalletAdministrationContext.Admin())) + } yield WalletAccessContext(walletId) + } + + override def authorizeWalletAdmin(entity: KeycloakEntity): IO[AuthenticationError, WalletAdministrationContext] = { + for { + entityWithRpt <- populateEntityRpt(entity) + wallets <- keycloakPermissionService + .listWalletPermissions(entityWithRpt) + .mapError(e => AuthenticationError.UnexpectedError(e.message)) + .provide(ZLayer.succeed(WalletAdministrationContext.Admin())) + } yield WalletAdministrationContext.SelfService(wallets) + } + + private def populateEntityRpt(entity: KeycloakEntity): IO[AuthenticationError, KeycloakEntity] = { + for { + token <- ZIO + .fromOption(entity.accessToken) + .mapError(_ => AuthenticationError.InvalidCredentials("AccessToken is missing.")) + isRpt <- inferIsRpt(token) rptEffect = if (isRpt) ZIO.succeed(token) - else if (keycloakConfig.autoUpgradeToRPT) client.getRpt(token) + else if (keycloakConfig.autoUpgradeToRPT) + client + .getRpt(token) + .mapError(e => AuthenticationError.UnexpectedError(e.message)) 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.") - ) - } + } yield entity.copy(rpt = Some(rpt)) } /** Return true if the token is RPT. Check whether property '.authorization' exists. */ @@ -88,7 +106,10 @@ class KeycloakAuthenticatorImpl( } object KeycloakAuthenticatorImpl { - val layer: RLayer[KeycloakClient & KeycloakConfig & WalletManagementService, KeycloakAuthenticator] = + val layer: RLayer[ + KeycloakClient & KeycloakConfig & PermissionManagement.Service[KeycloakEntity], + KeycloakAuthenticator + ] = ZLayer.fromFunction(KeycloakAuthenticatorImpl(_, _, _)) val disabled: ULayer[KeycloakAuthenticator] = @@ -97,7 +118,11 @@ object KeycloakAuthenticatorImpl { 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 + override def authorize(entity: KeycloakEntity): IO[AuthenticationError, WalletAccessContext] = notEnabledError + override def authorizeWalletAdmin( + entity: KeycloakEntity + ): IO[AuthenticationError, WalletAdministrationContext] = + 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 index 632546ae0e..6f1562e1db 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala @@ -1,6 +1,5 @@ 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 @@ -24,16 +23,24 @@ object TokenResponse { given JsonDecoder[TokenResponse] = JsonDecoder.derived } +sealed trait KeycloakClientError { + def message: String +} + +object KeycloakClientError { + case class UnexpectedError(message: String) extends KeycloakClientError +} + trait KeycloakClient { - def getRpt(accessToken: String): IO[AuthenticationError, String] + def getRpt(accessToken: String): IO[KeycloakClientError, String] - def getAccessToken(username: String, password: String): IO[AuthenticationError, TokenResponse] + def getAccessToken(username: String, password: String): IO[KeycloakClientError, TokenResponse] - def introspectToken(token: String): IO[AuthenticationError, TokenIntrospection] + def introspectToken(token: String): IO[KeycloakClientError, TokenIntrospection] /** Return list of permitted resources */ - def checkPermissions(rpt: String): IO[AuthenticationError, List[String]] + def checkPermissions(rpt: String): IO[KeycloakClientError, List[String]] } @@ -46,9 +53,8 @@ class KeycloakClientImpl(client: AuthzClient, httpClient: Client, keycloakConfig 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] = { + override def introspectToken(token: String): IO[KeycloakClientError, TokenIntrospection] = { for { response <- Client .request( @@ -64,25 +70,25 @@ class KeycloakClientImpl(client: AuthzClient, httpClient: Client, keycloakConfig ) ) .logError("Fail to introspect token on keycloak.") - .mapError(e => AuthenticationError.UnexpectedError("Fail to introspect the token on keyclaok.")) + .mapError(e => KeycloakClientError.UnexpectedError("Fail to introspect the token on keycloak.")) .provide(ZLayer.succeed(httpClient)) body <- response.body.asString .logError("Fail parse keycloak introspection response.") - .mapError(e => AuthenticationError.UnexpectedError("Fail parse keycloak introspection response.")) + .mapError(e => KeycloakClientError.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)) + .mapError(e => KeycloakClientError.UnexpectedError(e)) } else { ZIO.logError(s"Keycloak token introspection was unsucessful. Status: ${response.status}. Response: $body") *> - ZIO.fail(AuthenticationError.UnexpectedError("Token introspection was unsuccessful.")) + ZIO.fail(KeycloakClientError.UnexpectedError("Token introspection was unsuccessful.")) } } yield result } - override def getAccessToken(username: String, password: String): IO[AuthenticationError, TokenResponse] = { + override def getAccessToken(username: String, password: String): IO[KeycloakClientError, TokenResponse] = { for { response <- Client .request( @@ -100,25 +106,25 @@ class KeycloakClientImpl(client: AuthzClient, httpClient: Client, keycloakConfig ) ) .logError("Fail to get the accessToken on keyclaok.") - .mapError(e => AuthenticationError.UnexpectedError("Fail to get the accessToken on keyclaok.")) + .mapError(e => KeycloakClientError.UnexpectedError("Fail to get the accessToken on keyclaok.")) .provide(ZLayer.succeed(httpClient)) body <- response.body.asString .logError("Fail parse keycloak token response.") - .mapError(e => AuthenticationError.UnexpectedError("Fail parse keycloak token response.")) + .mapError(e => KeycloakClientError.UnexpectedError("Fail parse keycloak token response.")) result <- if (response.status.code == 200) { ZIO .fromEither(body.fromJson[TokenResponse]) .logError("Fail to decode keycloak token response") - .mapError(e => AuthenticationError.UnexpectedError(e)) + .mapError(e => KeycloakClientError.UnexpectedError(e)) } else { ZIO.logError(s"Keycloak token introspection was unsucessful. Status: ${response.status}. Response: $body") *> - ZIO.fail(AuthenticationError.UnexpectedError("Token introspection was unsuccessful.")) + ZIO.fail(KeycloakClientError.UnexpectedError("Token introspection was unsuccessful.")) } } yield result } - override def getRpt(accessToken: String): IO[AuthenticationError, String] = + override def getRpt(accessToken: String): IO[KeycloakClientError, String] = ZIO .attemptBlocking { val authResource = client.authorization(accessToken) @@ -127,24 +133,24 @@ class KeycloakClientImpl(client: AuthzClient, httpClient: Client, keycloakConfig } .logError .mapBoth( - e => AuthenticationError.UnexpectedError(e.getMessage()), + e => KeycloakClientError.UnexpectedError(e.getMessage()), response => response.getToken() ) - override def checkPermissions(rpt: String): IO[AuthenticationError, List[String]] = + override def checkPermissions(rpt: String): IO[KeycloakClientError, List[String]] = for { introspection <- ZIO .attemptBlocking(client.protection().introspectRequestingPartyToken(rpt)) .logError - .mapError(e => AuthenticationError.UnexpectedError(e.getMessage())) + .mapError(e => KeycloakClientError.UnexpectedError(e.getMessage())) permissions = introspection.getPermissions().asScala.toList } yield permissions.map(_.getResourceId()) } object KeycloakClientImpl { - val layer: RLayer[KeycloakConfig & Client, KeycloakClient] = - authzClientLayer >>> ZLayer.fromFunction(KeycloakClientImpl(_, _, _)) + val layer: RLayer[KeycloakConfig & AuthzClient & Client, KeycloakClient] = + ZLayer.fromFunction(KeycloakClientImpl(_, _, _)) def authzClientLayer: RLayer[KeycloakConfig, AuthzClient] = ZLayer.fromZIO { for { diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/DefaultPermissionManagementService.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/DefaultPermissionManagementService.scala new file mode 100644 index 0000000000..b685bd438e --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/DefaultPermissionManagementService.scala @@ -0,0 +1,46 @@ +package io.iohk.atala.iam.authorization + +import io.iohk.atala.agent.walletapi.model.BaseEntity +import io.iohk.atala.agent.walletapi.model.Entity +import io.iohk.atala.iam.authentication.oidc.KeycloakEntity +import io.iohk.atala.iam.authorization.core.PermissionManagement +import io.iohk.atala.iam.authorization.core.PermissionManagement.Error +import io.iohk.atala.shared.models.WalletAdministrationContext +import io.iohk.atala.shared.models.WalletId +import zio.* + +class DefaultPermissionManagementService( + entityPermission: PermissionManagement.Service[Entity], + keycloakPermission: PermissionManagement.Service[KeycloakEntity] +) extends PermissionManagement.Service[BaseEntity] { + + def grantWalletToUser(walletId: WalletId, entity: BaseEntity): ZIO[WalletAdministrationContext, Error, Unit] = { + entity match { + case entity: Entity => entityPermission.grantWalletToUser(walletId, entity) + case kcEntity: KeycloakEntity => keycloakPermission.grantWalletToUser(walletId, kcEntity) + } + } + + def revokeWalletFromUser(walletId: WalletId, entity: BaseEntity): ZIO[WalletAdministrationContext, Error, Unit] = { + entity match { + case entity: Entity => entityPermission.revokeWalletFromUser(walletId, entity) + case kcEntity: KeycloakEntity => keycloakPermission.revokeWalletFromUser(walletId, kcEntity) + } + } + + def listWalletPermissions(entity: BaseEntity): ZIO[WalletAdministrationContext, Error, Seq[WalletId]] = { + entity match { + case entity: Entity => entityPermission.listWalletPermissions(entity) + case kcEntity: KeycloakEntity => keycloakPermission.listWalletPermissions(kcEntity) + } + } + +} + +object DefaultPermissionManagementService { + def layer: URLayer[ + PermissionManagement.Service[KeycloakEntity] & PermissionManagement.Service[Entity], + PermissionManagement.Service[BaseEntity] + ] = + ZLayer.fromFunction(DefaultPermissionManagementService(_, _)) +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/core/EntityPermissionManagementService.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/core/EntityPermissionManagementService.scala new file mode 100644 index 0000000000..e2a8d9a68a --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/core/EntityPermissionManagementService.scala @@ -0,0 +1,41 @@ +package io.iohk.atala.iam.authorization.core + +import io.iohk.atala.agent.walletapi.model.Entity +import io.iohk.atala.agent.walletapi.service.EntityService +import io.iohk.atala.iam.authorization.core.PermissionManagement.Error +import io.iohk.atala.iam.authorization.core.PermissionManagement.Error.ServiceError +import io.iohk.atala.iam.authorization.core.PermissionManagement.Error.WalletNotFoundById +import io.iohk.atala.shared.models.WalletAdministrationContext +import io.iohk.atala.shared.models.WalletId +import zio.* + +import scala.language.implicitConversions + +class EntityPermissionManagementService(entityService: EntityService) extends PermissionManagement.Service[Entity] { + + override def grantWalletToUser(walletId: WalletId, entity: Entity): ZIO[WalletAdministrationContext, Error, Unit] = { + for { + _ <- ZIO + .serviceWith[WalletAdministrationContext](_.isAuthorized(walletId)) + .filterOrFail(identity)(Error.WalletNotFoundById(walletId)) + _ <- entityService.assignWallet(entity.id, walletId.toUUID).mapError[Error](e => e) + } yield () + } + + override def revokeWalletFromUser(walletId: WalletId, entity: Entity): ZIO[WalletAdministrationContext, Error, Unit] = + ZIO.fail(Error.ServiceError(s"Revoking wallet permission for an Entity is not yet supported.")) + + override def listWalletPermissions(entity: Entity): ZIO[WalletAdministrationContext, Error, Seq[WalletId]] = { + val walletId = WalletId.fromUUID(entity.walletId) + ZIO + .serviceWith[WalletAdministrationContext](_.isAuthorized(walletId)) + .filterOrFail(identity)(Error.WalletNotFoundById(walletId)) + .as(Seq(walletId)) + } + +} + +object EntityPermissionManagementService { + val layer: URLayer[EntityService, PermissionManagement.Service[Entity]] = + ZLayer.fromFunction(EntityPermissionManagementService(_)) +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/core/PermissionManagement.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/core/PermissionManagement.scala index d745111ce8..977b418522 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/core/PermissionManagement.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/core/PermissionManagement.scala @@ -1,17 +1,25 @@ package io.iohk.atala.iam.authorization.core +import io.iohk.atala.agent.walletapi.model.BaseEntity +import io.iohk.atala.agent.walletapi.model.error.EntityServiceError +import io.iohk.atala.agent.walletapi.model.error.EntityServiceError.EntityAlreadyExists +import io.iohk.atala.agent.walletapi.model.error.EntityServiceError.EntityNotFound +import io.iohk.atala.agent.walletapi.model.error.EntityServiceError.EntityStorageError +import io.iohk.atala.agent.walletapi.model.error.EntityServiceError.EntityWalletNotFound +import io.iohk.atala.shared.models.WalletAdministrationContext import io.iohk.atala.shared.models.WalletId -import zio.IO +import zio.* import java.util.UUID object PermissionManagement { - trait Service { - def grantWalletToUser(walletId: WalletId, userId: UUID): IO[Error, Unit] - def revokeWalletFromUser(walletId: WalletId, userId: UUID): IO[Error, Unit] + trait Service[E <: BaseEntity] { + def grantWalletToUser(walletId: WalletId, entity: E): ZIO[WalletAdministrationContext, Error, Unit] + def revokeWalletFromUser(walletId: WalletId, entity: E): ZIO[WalletAdministrationContext, Error, Unit] + def listWalletPermissions(entity: E): ZIO[WalletAdministrationContext, Error, Seq[WalletId]] } - trait Error(message: String) + sealed trait Error(val message: String) object Error { case class UserNotFoundById(userId: UUID, cause: Option[Throwable] = None) @@ -30,6 +38,13 @@ object PermissionManagement { case class UnexpectedError(cause: Throwable) extends Error(cause.getMessage) - case class ServiceError(message: String) extends Error(message) + case class ServiceError(cause: String) extends Error(cause) + + given Conversion[EntityServiceError, Error] = { + case e: EntityNotFound => UserNotFoundById(e.id) + case e: EntityAlreadyExists => UnexpectedError(Exception(s"Entity with id ${e.id} already exists.")) + case e: EntityStorageError => UnexpectedError(Exception(s"Entity storage error: ${e.message}")) + case e: EntityWalletNotFound => WalletNotFoundById(WalletId.fromUUID(e.walletId)) + } } } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakAdmin.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakAdmin.scala index 189cff32a0..60abc23456 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakAdmin.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakAdmin.scala @@ -1,10 +1,9 @@ package io.iohk.atala.iam.authorization.keycloak.admin -import io.iohk.atala.agent.server.config.AppConfig import org.keycloak.admin.client.Keycloak -import zio.{RLayer, Task, TaskLayer, URLayer, ZIO, ZLayer} +import zio.{RLayer, Task, ZIO, ZLayer} -import scala.util.{Failure, Try} +import scala.util.Try type KeycloakAdmin = Keycloak @@ -34,11 +33,6 @@ case class KeycloakAdminConfig( def isHttps: Boolean = serverUrl.startsWith("https") } -object KeycloakAdminConfig { - val layer: URLayer[AppConfig, KeycloakAdminConfig] = - ZLayer.fromFunction((appConfig: AppConfig) => appConfig.keycloakAdmin) -} - object KeycloakAdmin { def apply(config: KeycloakAdminConfig): Task[KeycloakAdmin] = { diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementService.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementService.scala index 334b0d7483..9836612e31 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementService.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementService.scala @@ -1,36 +1,41 @@ package io.iohk.atala.iam.authorization.keycloak.admin +import io.iohk.atala.agent.walletapi.model.Wallet import io.iohk.atala.agent.walletapi.service.WalletManagementService +import io.iohk.atala.iam.authentication.oidc.KeycloakClient +import io.iohk.atala.iam.authentication.oidc.KeycloakEntity import io.iohk.atala.iam.authorization.core.PermissionManagement +import io.iohk.atala.iam.authorization.core.PermissionManagement.Error import io.iohk.atala.iam.authorization.core.PermissionManagement.Error.* +import io.iohk.atala.shared.models.WalletAdministrationContext import io.iohk.atala.shared.models.WalletId import org.keycloak.authorization.client.AuthzClient import org.keycloak.representations.idm.authorization.{ResourceRepresentation, UmaPermissionRepresentation} -import zio.ZIO.* -import zio.ZLayer.* -import zio.{IO, Task, URLayer, ZIO, ZLayer} +import zio.* import java.util.UUID import scala.jdk.CollectionConverters.* +import scala.util.Try case class KeycloakPermissionManagementService( authzClient: AuthzClient, + keycloakClient: KeycloakClient, walletManagementService: WalletManagementService -) extends PermissionManagement.Service { +) extends PermissionManagement.Service[KeycloakEntity] { private def walletResourceName(walletId: WalletId) = s"wallet-${walletId.toUUID.toString}" private def policyName(userId: String, resourceId: String) = s"user $userId on wallet $resourceId permission" - override def grantWalletToUser(walletId: WalletId, userId: UUID): IO[PermissionManagement.Error, Unit] = { + override def grantWalletToUser( + walletId: WalletId, + entity: KeycloakEntity + ): ZIO[WalletAdministrationContext, PermissionManagement.Error, Unit] = { for { - walletOpt <- walletManagementService + _ <- walletManagementService .getWallet(walletId) .mapError(wmse => ServiceError(wmse.toThrowable.getMessage)) - - wallet <- ZIO - .fromOption(walletOpt) - .orElseFail(WalletNotFoundById(walletId)) + .someOrFail(WalletNotFoundById(walletId)) walletResourceOpt <- findWalletResource(walletId) .logError("Error while finding wallet resource") @@ -43,22 +48,13 @@ case class KeycloakPermissionManagementService( .mapError(UnexpectedError.apply) _ <- ZIO.log(s"Wallet resource created ${walletResource.toString}") - permission <- createResourcePermission(walletResource.getId, userId.toString) + permission <- createResourcePermission(walletResource.getId, entity.id.toString) .mapError(UnexpectedError.apply) _ <- ZIO.log(s"Permission created with id ${permission.getId} and name ${permission.getName}") } yield () } - private def permissionDetails(permission: UmaPermissionRepresentation): String = { - s""" - |id: ${permission.getId} - |name: ${permission.getName} - |scopes: ${permission.getScopes.asScala.mkString(", ")} - |users: ${permission.getUsers.asScala.mkString(", ")} - |""".stripMargin - } - private def createResourcePermission(resourceId: String, userId: String): Task[UmaPermissionRepresentation] = { val policy = UmaPermissionRepresentation() policy.setName(policyName(userId, resourceId)) @@ -76,10 +72,16 @@ case class KeycloakPermissionManagementService( private def findWalletResource(walletId: WalletId): Task[Option[ResourceRepresentation]] = { for { - walletResourceOrNull <- ZIO.attemptBlocking( - authzClient.protection().resource().findByName(walletResourceName(walletId)) - ) - } yield Option(walletResourceOrNull) + walletResource <- ZIO + .attemptBlocking( + authzClient.protection().resource().findById(walletId.toUUID.toString()) + ) + .asSome + .catchSome { case e: RuntimeException => + if (e.getMessage().contains("Could not find resource")) ZIO.none + else ZIO.fail(e) + } + } yield walletResource } private def createWalletResource(walletId: WalletId): Task[ResourceRepresentation] = { @@ -107,15 +109,21 @@ case class KeycloakPermissionManagementService( } yield resource } - override def revokeWalletFromUser(walletId: WalletId, userId: UUID): IO[PermissionManagement.Error, Unit] = { + override def revokeWalletFromUser( + walletId: WalletId, + entity: KeycloakEntity + ): ZIO[WalletAdministrationContext, PermissionManagement.Error, Unit] = { + val userId = entity.id for { - walletResourceOpt <- findWalletResource(walletId) + _ <- walletManagementService + .getWallet(walletId) + .mapError(wmse => ServiceError(wmse.toThrowable.getMessage)) + .someOrFail(WalletNotFoundById(walletId)) + + walletResource <- findWalletResource(walletId) .logError("Error while finding wallet resource") .mapError(UnexpectedError.apply) - - walletResource <- ZIO - .fromOption(walletResourceOpt) - .orElseFail(WalletResourceNotFoundById(walletId)) + .someOrFail(WalletResourceNotFoundById(walletId)) permissionOpt <- ZIO .attemptBlocking( @@ -152,12 +160,48 @@ case class KeycloakPermissionManagementService( ) } yield () } + + override def listWalletPermissions(entity: KeycloakEntity): ZIO[WalletAdministrationContext, Error, Seq[WalletId]] = { + for { + token <- ZIO + .fromOption(entity.accessToken) + .mapError(_ => Error.ServiceError("AccessToken is missing for listing permissions.")) + rpt <- entity.rpt.fold( + keycloakClient + .getRpt(token) + .logError("Fail to obtail RPT for wallet permissions") + .mapError(e => Error.ServiceError(e.message)) + )(ZIO.succeed) + permittedResources <- keycloakClient + .checkPermissions(rpt) + .logError("Fail to list resource permissions on keycloak") + .mapError(e => Error.ServiceError(e.message)) + permittedWallet <- getPermittedWallet(permittedResources) + } yield permittedWallet.map(_.id) + } + + private def getPermittedWallet(resourceIds: Seq[String]): ZIO[WalletAdministrationContext, Error, Seq[Wallet]] = { + val walletIds = resourceIds.flatMap(id => Try(UUID.fromString(id)).toOption).map(WalletId.fromUUID) + walletManagementService + .getWallets(walletIds) + .mapError(e => Error.UnexpectedError(e.toThrowable)) + } } object KeycloakPermissionManagementService { val layer: URLayer[ - AuthzClient & WalletManagementService, - PermissionManagement.Service + AuthzClient & KeycloakClient & WalletManagementService, + PermissionManagement.Service[KeycloakEntity] ] = - ZLayer.fromFunction(KeycloakPermissionManagementService(_, _)) + ZLayer.fromFunction(KeycloakPermissionManagementService(_, _, _)) + + val disabled: ULayer[PermissionManagement.Service[KeycloakEntity]] = + ZLayer.succeed { + val notEnabledError = ZIO.fail(PermissionManagement.Error.ServiceError("Keycloak is not enabled")) + new PermissionManagement.Service[KeycloakEntity] { + override def grantWalletToUser(walletId: WalletId, entity: KeycloakEntity): IO[Error, Unit] = notEnabledError + override def revokeWalletFromUser(walletId: WalletId, entity: KeycloakEntity): IO[Error, Unit] = notEnabledError + override def listWalletPermissions(entity: KeycloakEntity): IO[Error, Seq[WalletId]] = notEnabledError + } + } } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/WalletManagementEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/WalletManagementEndpoints.scala index 8b2cfc10ee..717cf15e55 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/WalletManagementEndpoints.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/WalletManagementEndpoints.scala @@ -6,7 +6,12 @@ import io.iohk.atala.api.http.RequestContext import io.iohk.atala.api.http.model.PaginationInput import io.iohk.atala.iam.authentication.admin.AdminApiKeyCredentials import io.iohk.atala.iam.authentication.admin.AdminApiKeySecurityLogic.adminApiKeyHeader +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.iam.wallet.http.model.CreateWalletRequest +import io.iohk.atala.iam.wallet.http.model.CreateWalletUmaPermissionRequest import io.iohk.atala.iam.wallet.http.model.WalletDetail import io.iohk.atala.iam.wallet.http.model.WalletDetailPage import sttp.model.StatusCode @@ -20,13 +25,15 @@ object WalletManagementEndpoints { private val baseEndpoint = endpoint .tag("Wallet Management") .securityIn(adminApiKeyHeader) + .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) .in("wallets") .in(extractFromRequest[RequestContext](RequestContext.apply)) private val paginationInput: EndpointInput[PaginationInput] = EndpointInput.derived[PaginationInput] val listWallet: Endpoint[ - AdminApiKeyCredentials, + (AdminApiKeyCredentials, ApiKeyCredentials, JwtCredentials), (RequestContext, PaginationInput), ErrorResponse, WalletDetailPage, @@ -40,7 +47,7 @@ object WalletManagementEndpoints { .summary("List all wallets") val getWallet: Endpoint[ - AdminApiKeyCredentials, + (AdminApiKeyCredentials, ApiKeyCredentials, JwtCredentials), (RequestContext, UUID), ErrorResponse, WalletDetail, @@ -54,7 +61,7 @@ object WalletManagementEndpoints { .summary("Get the wallet by ID") val createWallet: Endpoint[ - AdminApiKeyCredentials, + (AdminApiKeyCredentials, ApiKeyCredentials, JwtCredentials), (RequestContext, CreateWalletRequest), ErrorResponse, WalletDetail, @@ -73,4 +80,40 @@ object WalletManagementEndpoints { |The seed will be used for DID key derivation inside the wallet.""".stripMargin ) + val createWalletUmaPermmission: Endpoint[ + (AdminApiKeyCredentials, ApiKeyCredentials, JwtCredentials), + (RequestContext, UUID, CreateWalletUmaPermissionRequest), + ErrorResponse, + Unit, + Any + ] = + baseEndpoint.post + .in(path[UUID]("walletId") / "uma-permissions") + .in(jsonBody[CreateWalletUmaPermissionRequest]) + .out( + statusCode(StatusCode.Ok) + .description("UMA resource permission is created on an authorization server.") + ) + .errorOut(EndpointOutputs.basicFailuresAndForbidden) + .name("createWalletUmaPermission") + .summary("Create a UMA resource permission on an authorization server for the wallet.") + + val deleteWalletUmaPermmission: Endpoint[ + (AdminApiKeyCredentials, ApiKeyCredentials, JwtCredentials), + (RequestContext, UUID, UUID), + ErrorResponse, + Unit, + Any + ] = + baseEndpoint.delete + .in(path[UUID]("walletId") / "uma-permissions") + .in(query[UUID]("subject")) + .out( + statusCode(StatusCode.Ok) + .description("UMA resource permission is removed from an authorization server.") + ) + .errorOut(EndpointOutputs.basicFailuresAndForbidden) + .name("deleteWalletUmaPermission") + .summary("Delete a UMA resource permission on an authorization server for the wallet.") + } 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 1fa98cecf5..a936a7a742 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 @@ -3,40 +3,69 @@ package io.iohk.atala.iam.wallet.http 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.Authorizer 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.authentication.SecurityLogic import io.iohk.atala.iam.wallet.http.controller.WalletManagementController +import io.iohk.atala.shared.models.WalletAdministrationContext import sttp.tapir.ztapir.* import zio.* class WalletManagementServerEndpoints( controller: WalletManagementController, - authenticator: Authenticator[BaseEntity] + authenticator: Authenticator[BaseEntity], + authorizer: Authorizer[BaseEntity] ) { - private def adminApiSecurityLogic(credentials: AdminApiKeyCredentials): IO[ErrorResponse, BaseEntity] = - AdminApiKeySecurityLogic.securityLogic(credentials)(authenticator) - val listWalletServerEndpoint: ZServerEndpoint[Any, Any] = WalletManagementEndpoints.listWallet - .zServerSecurityLogic(adminApiSecurityLogic) - .serverLogic { _ => { case (rc, paginationInput) => controller.listWallet(paginationInput)(rc) } } + .zServerSecurityLogic(SecurityLogic.authorizeWalletAdminWith(_)(authenticator, authorizer)) + .serverLogic { + case (_, wac) => { case (rc, paginationInput) => + controller.listWallet(paginationInput)(rc).provide(ZLayer.succeed(wac)) + } + } val getWalletServerEndpoint: ZServerEndpoint[Any, Any] = WalletManagementEndpoints.getWallet - .zServerSecurityLogic(adminApiSecurityLogic) - .serverLogic { _ => { case (rc, walletId) => controller.getWallet(walletId)(rc) } } + .zServerSecurityLogic(SecurityLogic.authorizeWalletAdminWith(_)(authenticator, authorizer)) + .serverLogic { + case (_, wac) => { case (rc, walletId) => controller.getWallet(walletId)(rc).provide(ZLayer.succeed(wac)) } + } val createWalletServerEndpoint: ZServerEndpoint[Any, Any] = WalletManagementEndpoints.createWallet - .zServerSecurityLogic(adminApiSecurityLogic) - .serverLogic { _ => { case (rc, createWalletRequest) => controller.createWallet(createWalletRequest)(rc) } } + .zServerSecurityLogic(SecurityLogic.authorizeWalletAdminWith(_)(authenticator, authorizer)) + .serverLogic { + case (me, wac) => { case (rc, createWalletRequest) => + controller.createWallet(createWalletRequest, me)(rc).provide(ZLayer.succeed(wac)) + } + } + + val createWalletUmaPermissionServerEndpoint: ZServerEndpoint[Any, Any] = + WalletManagementEndpoints.createWalletUmaPermmission + .zServerSecurityLogic(SecurityLogic.authorizeWalletAdminWith(_)(authenticator, authorizer)) + .serverLogic { + case (_, wac) => { case (rc, walletId, request) => + controller.createWalletUmaPermission(walletId, request)(rc).provide(ZLayer.succeed(wac)) + } + } + + val deleteWalletUmaPermissionServerEndpoint: ZServerEndpoint[Any, Any] = + WalletManagementEndpoints.deleteWalletUmaPermmission + .zServerSecurityLogic(SecurityLogic.authorizeWalletAdminWith(_)(authenticator, authorizer)) + .serverLogic { + case (_, wac) => { case (rc, walletId, subject) => + controller.deleteWalletUmaPermission(walletId, subject)(rc).provide(ZLayer.succeed(wac)) + } + } def all: List[ZServerEndpoint[Any, Any]] = List( listWalletServerEndpoint, getWalletServerEndpoint, - createWalletServerEndpoint + createWalletServerEndpoint, + createWalletUmaPermissionServerEndpoint, + deleteWalletUmaPermissionServerEndpoint ) } @@ -46,7 +75,7 @@ object WalletManagementServerEndpoints { for { walletManagementController <- ZIO.service[WalletManagementController] auth <- ZIO.service[DefaultAuthenticator] - walletManagementServerEndpoints = WalletManagementServerEndpoints(walletManagementController, auth) + walletManagementServerEndpoints = WalletManagementServerEndpoints(walletManagementController, auth, auth) } yield walletManagementServerEndpoints.all } } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/controller/WalletManagementController.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/controller/WalletManagementController.scala index 23df36a94d..1596175e0a 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/controller/WalletManagementController.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/controller/WalletManagementController.scala @@ -1,5 +1,6 @@ package io.iohk.atala.iam.wallet.http.controller +import io.iohk.atala.agent.walletapi.model.BaseEntity import io.iohk.atala.agent.walletapi.model.Wallet import io.iohk.atala.agent.walletapi.model.WalletSeed import io.iohk.atala.agent.walletapi.service.WalletManagementService @@ -9,20 +10,41 @@ import io.iohk.atala.api.http.RequestContext import io.iohk.atala.api.http.model.CollectionStats import io.iohk.atala.api.http.model.PaginationInput import io.iohk.atala.api.util.PaginationUtils +import io.iohk.atala.iam.authentication.oidc.KeycloakEntity +import io.iohk.atala.iam.authorization.core.PermissionManagement import io.iohk.atala.iam.wallet.http.model.CreateWalletRequest +import io.iohk.atala.iam.wallet.http.model.CreateWalletUmaPermissionRequest import io.iohk.atala.iam.wallet.http.model.WalletDetail import io.iohk.atala.iam.wallet.http.model.WalletDetailPage import io.iohk.atala.shared.models.HexString +import io.iohk.atala.shared.models.WalletAdministrationContext import io.iohk.atala.shared.models.WalletId import zio.* import java.util.UUID import scala.language.implicitConversions +import io.iohk.atala.agent.walletapi.service.WalletManagementServiceError.TooManyPermittedWallet +import io.iohk.atala.shared.models.WalletAdministrationContext.Admin trait WalletManagementController { - def listWallet(paginationInput: PaginationInput)(implicit rc: RequestContext): IO[ErrorResponse, WalletDetailPage] - def getWallet(walletId: UUID)(implicit rc: RequestContext): IO[ErrorResponse, WalletDetail] - def createWallet(request: CreateWalletRequest)(implicit rc: RequestContext): IO[ErrorResponse, WalletDetail] + def listWallet( + paginationInput: PaginationInput + )(implicit rc: RequestContext): ZIO[WalletAdministrationContext, ErrorResponse, WalletDetailPage] + def getWallet(walletId: UUID)(implicit + rc: RequestContext + ): ZIO[WalletAdministrationContext, ErrorResponse, WalletDetail] + def createWallet( + request: CreateWalletRequest, + me: BaseEntity + )(implicit rc: RequestContext): ZIO[WalletAdministrationContext, ErrorResponse, WalletDetail] + def createWalletUmaPermission( + walletId: UUID, + request: CreateWalletUmaPermissionRequest + )(implicit rc: RequestContext): ZIO[WalletAdministrationContext, ErrorResponse, Unit] + def deleteWalletUmaPermission( + walletId: UUID, + subject: UUID + )(implicit rc: RequestContext): ZIO[WalletAdministrationContext, ErrorResponse, Unit] } object WalletManagementController { @@ -36,25 +58,38 @@ object WalletManagementController { case WalletManagementServiceError.DuplicatedWalletId(id) => ErrorResponse.badRequest(s"Wallet id $id is not unique.") case WalletManagementServiceError.DuplicatedWalletSeed(id) => - // Should we return this error message? - // Returning less revealing message also doesn't help for open-source repo. ErrorResponse.badRequest(s"Wallet id $id cannot be created. The seed value is not unique.") + case TooManyPermittedWallet() => + ErrorResponse.badRequest( + s"The operation is not allowed because wallet access already exists for the current user." + ) + } + + given permissionManagementErrorConversion: Conversion[PermissionManagement.Error, ErrorResponse] = { + case e: PermissionManagement.Error.PermissionNotFoundById => ErrorResponse.badRequest(detail = Some(e.message)) + case e: PermissionManagement.Error.ServiceError => ErrorResponse.internalServerError(detail = Some(e.message)) + case e: PermissionManagement.Error.UnexpectedError => ErrorResponse.internalServerError(detail = Some(e.message)) + case e: PermissionManagement.Error.UserNotFoundById => ErrorResponse.badRequest(detail = Some(e.message)) + case e: PermissionManagement.Error.WalletNotFoundById => ErrorResponse.badRequest(detail = Some(e.message)) + case e: PermissionManagement.Error.WalletNotFoundByUserId => ErrorResponse.badRequest(detail = Some(e.message)) + case e: PermissionManagement.Error.WalletResourceNotFoundById => ErrorResponse.badRequest(detail = Some(e.message)) } } class WalletManagementControllerImpl( - service: WalletManagementService + walletService: WalletManagementService, + permissionService: PermissionManagement.Service[BaseEntity], ) extends WalletManagementController { import WalletManagementController.given override def listWallet( paginationInput: PaginationInput - )(implicit rc: RequestContext): IO[ErrorResponse, WalletDetailPage] = { + )(implicit rc: RequestContext): ZIO[WalletAdministrationContext, ErrorResponse, WalletDetailPage] = { val uri = rc.request.uri val pagination = paginationInput.toPagination for { - pageResult <- service + pageResult <- walletService .listWallets(offset = paginationInput.offset, limit = paginationInput.limit) .mapError[ErrorResponse](e => e) (items, totalCount) = pageResult @@ -68,9 +103,11 @@ class WalletManagementControllerImpl( ) } - override def getWallet(walletId: UUID)(implicit rc: RequestContext): IO[ErrorResponse, WalletDetail] = { + override def getWallet( + walletId: UUID + )(implicit rc: RequestContext): ZIO[WalletAdministrationContext, ErrorResponse, WalletDetail] = { for { - wallet <- service + wallet <- walletService .getWallet(WalletId.fromUUID(walletId)) .mapError[ErrorResponse](e => e) .someOrFail(ErrorResponse.notFound(detail = Some(s"Wallet id $walletId does not exist."))) @@ -78,12 +115,47 @@ class WalletManagementControllerImpl( } override def createWallet( - request: CreateWalletRequest - )(implicit rc: RequestContext): IO[ErrorResponse, WalletDetail] = { + request: CreateWalletRequest, + me: BaseEntity + )(implicit rc: RequestContext): ZIO[WalletAdministrationContext, ErrorResponse, WalletDetail] = { + ZIO.serviceWithZIO[WalletAdministrationContext] { + case WalletAdministrationContext.Admin() => doCreateWallet(request).map(i => i) + case WalletAdministrationContext.SelfService(_) => + for { + wallet <- doCreateWallet(request) + _ <- permissionService + .grantWalletToUser(wallet.id, me) + .mapError[ErrorResponse](e => e) + .provide(ZLayer.succeed(WalletAdministrationContext.Admin())) // First time to use must be admin + } yield wallet + } + } + + override def createWalletUmaPermission(walletId: UUID, request: CreateWalletUmaPermissionRequest)(implicit + rc: RequestContext + ): ZIO[WalletAdministrationContext, ErrorResponse, Unit] = { + val grantee = KeycloakEntity(request.subject) + permissionService + .grantWalletToUser(WalletId.fromUUID(walletId), grantee) + .mapError[ErrorResponse](e => e) + } + + override def deleteWalletUmaPermission(walletId: UUID, subject: UUID)(implicit + rc: RequestContext + ): ZIO[WalletAdministrationContext, ErrorResponse, Unit] = { + val grantee = KeycloakEntity(subject) + permissionService + .revokeWalletFromUser(WalletId.fromUUID(walletId), grantee) + .mapError[ErrorResponse](e => e) + } + + private def doCreateWallet(request: CreateWalletRequest): ZIO[WalletAdministrationContext, ErrorResponse, Wallet] = { for { providedSeed <- request.seed.fold(ZIO.none)(s => extractWalletSeed(s).asSome) walletId = request.id.map(WalletId.fromUUID).getOrElse(WalletId.random) - wallet <- service.createWallet(Wallet(request.name, walletId), providedSeed).mapError[ErrorResponse](e => e) + wallet <- walletService + .createWallet(Wallet(request.name, walletId), providedSeed) + .mapError[ErrorResponse](identity) } yield wallet } @@ -104,6 +176,6 @@ class WalletManagementControllerImpl( } object WalletManagementControllerImpl { - val layer: URLayer[WalletManagementService, WalletManagementController] = - ZLayer.fromFunction(WalletManagementControllerImpl(_)) + val layer: URLayer[WalletManagementService & PermissionManagement.Service[BaseEntity], WalletManagementController] = + ZLayer.fromFunction(WalletManagementControllerImpl(_, _)) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/model/CreateWalletUmaPermissionRequest.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/model/CreateWalletUmaPermissionRequest.scala new file mode 100644 index 0000000000..488e75b0e4 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/wallet/http/model/CreateWalletUmaPermissionRequest.scala @@ -0,0 +1,29 @@ +package io.iohk.atala.iam.wallet.http.model + +import io.iohk.atala.api.http.Annotation +import sttp.tapir.* +import sttp.tapir.Schema.annotations.{description, encodedExample, validate} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} + +import java.util.UUID + +final case class CreateWalletUmaPermissionRequest( + @description(CreateWalletUmaPermissionRequest.annotations.subject.description) + @encodedExample(CreateWalletUmaPermissionRequest.annotations.subject.example) + subject: UUID +) + +object CreateWalletUmaPermissionRequest { + given encoder: JsonEncoder[CreateWalletUmaPermissionRequest] = DeriveJsonEncoder.gen + given decoder: JsonDecoder[CreateWalletUmaPermissionRequest] = DeriveJsonDecoder.gen + given schema: Schema[CreateWalletUmaPermissionRequest] = Schema.derived + + object annotations { + object subject + extends Annotation[UUID]( + description = + "The subject ID that should be granted the permission to the wallet. This can be found in the `sub` claim of a JWT token.", + example = UUID.fromString("00000000-0000-0000-0000-000000000000") + ) + } +} 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 1e6fc90e10..4e4ee98e93 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 @@ -15,12 +15,13 @@ import zio.* class IssueServerEndpoints( issueController: IssueController, - authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] + authenticator: Authenticator[BaseEntity], + authorizer: Authorizer[BaseEntity] ) { val createCredentialOfferEndpoint: ZServerEndpoint[Any, Any] = createCredentialOffer - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, request: CreateIssueCredentialRecordRequest) => issueController @@ -31,7 +32,7 @@ class IssueServerEndpoints( val getCredentialRecordsEndpoint: ZServerEndpoint[Any, Any] = getCredentialRecords - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, paginationInput: PaginationInput, thid: Option[String]) => issueController @@ -42,7 +43,7 @@ class IssueServerEndpoints( val getCredentialRecordEndpoint: ZServerEndpoint[Any, Any] = getCredentialRecord - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, recordId: String) => issueController @@ -53,7 +54,7 @@ class IssueServerEndpoints( val acceptCredentialOfferEndpoint: ZServerEndpoint[Any, Any] = acceptCredentialOffer - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, recordId: String, request: AcceptCredentialOfferRequest) => issueController @@ -64,7 +65,7 @@ class IssueServerEndpoints( val issueCredentialEndpoint: ZServerEndpoint[Any, Any] = issueCredential - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, recordId: String) => issueController @@ -88,7 +89,7 @@ object IssueServerEndpoints { for { authenticator <- ZIO.service[DefaultAuthenticator] issueController <- ZIO.service[IssueController] - issueEndpoints = new IssueServerEndpoints(issueController, authenticator) + issueEndpoints = new IssueServerEndpoints(issueController, authenticator, authenticator) } yield issueEndpoints.all } } 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 2cc5d69e13..d300956f68 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 @@ -18,14 +18,15 @@ import java.util.UUID class CredentialDefinitionRegistryServerEndpoints( credentialDefinitionController: CredentialDefinitionController, - authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] + authenticator: Authenticator[BaseEntity], + authorizer: Authorizer[BaseEntity] ) { def throwableToInternalServerError(throwable: Throwable) = ZIO.fail[ErrorResponse](ErrorResponse.internalServerError(detail = Option(throwable.getMessage))) val createCredentialDefinitionServerEndpoint: ZServerEndpoint[Any, Any] = createCredentialDefinitionEndpoint - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { case wac => { case (ctx: RequestContext, credentialDefinitionInput: CredentialDefinitionInput) => credentialDefinitionController @@ -46,7 +47,7 @@ class CredentialDefinitionRegistryServerEndpoints( val lookupCredentialDefinitionsByQueryServerEndpoint: ZServerEndpoint[Any, Any] = lookupCredentialDefinitionsByQueryEndpoint - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { case wac => { case ( @@ -81,6 +82,7 @@ object CredentialDefinitionRegistryServerEndpoints { authenticator <- ZIO.service[DefaultAuthenticator] credentialDefinitionRegistryEndpoints = new CredentialDefinitionRegistryServerEndpoints( credentialDefinitionRegistryService, + authenticator, authenticator ) } yield credentialDefinitionRegistryEndpoints.all 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 5668a2c205..421fce792b 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 @@ -18,14 +18,15 @@ import java.util.UUID class SchemaRegistryServerEndpoints( credentialSchemaController: CredentialSchemaController, - authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] + authenticator: Authenticator[BaseEntity], + authorizer: Authorizer[BaseEntity] ) { def throwableToInternalServerError(throwable: Throwable) = ZIO.fail[ErrorResponse](ErrorResponse.internalServerError(detail = Option(throwable.getMessage))) val createSchemaServerEndpoint: ZServerEndpoint[Any, Any] = createSchemaEndpoint - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { case wac => { case (ctx: RequestContext, schemaInput: CredentialSchemaInput) => credentialSchemaController @@ -36,7 +37,7 @@ class SchemaRegistryServerEndpoints( val updateSchemaServerEndpoint: ZServerEndpoint[Any, Any] = updateSchemaEndpoint - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { case wac => { case (ctx: RequestContext, author: String, id: UUID, schemaInput: CredentialSchemaInput) => credentialSchemaController @@ -53,7 +54,7 @@ class SchemaRegistryServerEndpoints( val lookupSchemasByQueryServerEndpoint: ZServerEndpoint[Any, Any] = lookupSchemasByQueryEndpoint - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { case wac => { case ( @@ -74,7 +75,7 @@ class SchemaRegistryServerEndpoints( val testServerEndpoint: ZServerEndpoint[Any, Any] = testEndpoint - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { case wac => { case requestContext: RequestContext => ZIO.succeed(requestContext.request.toString + " " + wac.toString) @@ -98,6 +99,7 @@ object SchemaRegistryServerEndpoints { schemaRegistryService <- ZIO.service[CredentialSchemaController] schemaRegistryEndpoints = new SchemaRegistryServerEndpoints( schemaRegistryService, + authenticator, authenticator ) } yield schemaRegistryEndpoints.all 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 d546051b0d..7192a1f950 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 @@ -17,7 +17,8 @@ import zio.* class VerificationPolicyServerEndpoints( controller: VerificationPolicyController, - authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] + authenticator: Authenticator[BaseEntity], + authorizer: Authorizer[BaseEntity] ) { def throwableToInternalServerError(throwable: Throwable) = ZIO.fail[ErrorResponse](ErrorResponse.internalServerError(detail = Option(throwable.getMessage))) @@ -25,7 +26,7 @@ class VerificationPolicyServerEndpoints( // TODO: make the endpoint typed ZServerEndpoint[SchemaRegistryService, Any] val createVerificationPolicyServerEndpoint: ZServerEndpoint[Any, Any] = createVerificationPolicyEndpoint - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, input: VerificationPolicyInput) => controller @@ -36,7 +37,7 @@ class VerificationPolicyServerEndpoints( val updateVerificationPolicyServerEndpoint: ZServerEndpoint[Any, Any] = { updateVerificationPolicyEndpoint - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case ( @@ -54,7 +55,7 @@ class VerificationPolicyServerEndpoints( val getVerificationPolicyByIdServerEndpoint: ZServerEndpoint[Any, Any] = getVerificationPolicyByIdEndpoint - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, id: UUID) => controller @@ -65,7 +66,7 @@ class VerificationPolicyServerEndpoints( val deleteVerificationPolicyByIdServerEndpoint: ZServerEndpoint[Any, Any] = deleteVerificationPolicyByIdEndpoint - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, id: UUID) => controller @@ -76,7 +77,7 @@ class VerificationPolicyServerEndpoints( val lookupVerificationPoliciesByQueryServerEndpoint: ZServerEndpoint[Any, Any] = lookupVerificationPoliciesByQueryEndpoint - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case ( @@ -111,7 +112,7 @@ object VerificationPolicyServerEndpoints { for { authenticator <- ZIO.service[DefaultAuthenticator] controller <- ZIO.service[VerificationPolicyController] - endpoints = new VerificationPolicyServerEndpoints(controller, authenticator) + endpoints = new VerificationPolicyServerEndpoints(controller, authenticator, authenticator) } yield endpoints.all } } 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 509be10655..67206940b4 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 @@ -22,11 +22,12 @@ import java.util.UUID class PresentProofServerEndpoints( presentProofController: PresentProofController, - authenticator: Authenticator[BaseEntity] & Authorizer[BaseEntity] + authenticator: Authenticator[BaseEntity], + authorizer: Authorizer[BaseEntity] ) { private val requestPresentationEndpoint: ZServerEndpoint[Any, Any] = requestPresentation - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, request: RequestPresentationInput) => presentProofController @@ -37,7 +38,7 @@ class PresentProofServerEndpoints( private val getAllPresentationsEndpoint: ZServerEndpoint[Any, Any] = getAllPresentations - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, paginationInput: PaginationInput, thid: Option[String]) => presentProofController @@ -48,7 +49,7 @@ class PresentProofServerEndpoints( private val getPresentationEndpoint: ZServerEndpoint[Any, Any] = getPresentation - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, presentationId: UUID) => presentProofController @@ -59,7 +60,7 @@ class PresentProofServerEndpoints( private val updatePresentationEndpoint: ZServerEndpoint[Any, Any] = updatePresentation - .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator)) + .zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer)) .serverLogic { wac => { case (ctx: RequestContext, presentationId: UUID, action: RequestPresentationAction) => presentProofController @@ -81,7 +82,7 @@ object PresentProofServerEndpoints { for { authenticator <- ZIO.service[DefaultAuthenticator] presentProofController <- ZIO.service[PresentProofController] - presentProofEndpoints = new PresentProofServerEndpoints(presentProofController, authenticator) + presentProofEndpoints = new PresentProofServerEndpoints(presentProofController, authenticator, 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 bedc63ae57..62fc0ef47b 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 @@ -13,6 +13,7 @@ import io.iohk.atala.agent.walletapi.storage.WalletSecretStorage import io.iohk.atala.iam.authentication.apikey.ApiKeyAuthenticatorImpl import io.iohk.atala.iam.authentication.apikey.JdbcAuthenticationRepository import io.iohk.atala.shared.models.WalletAccessContext +import io.iohk.atala.shared.models.WalletAdministrationContext import io.iohk.atala.shared.models.WalletId import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.DBTestUtils @@ -44,7 +45,8 @@ object AgentInitializationSpec extends ZIOSpecDefault, PostgresTestContainerSupp contextAwareTransactorLayer, systemTransactorLayer, apolloLayer, - pgContainerLayer + pgContainerLayer, + ZLayer.succeed(WalletAdministrationContext.Admin()) ).provide(Runtime.removeDefaultLoggers) } diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorSpec.scala index 844faeeb7d..75973104de 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/apikey/ApiKeyAuthenticatorSpec.scala @@ -13,6 +13,7 @@ import io.iohk.atala.agent.walletapi.sql.{JdbcEntityRepository, JdbcWalletNonSec import io.iohk.atala.container.util.MigrationAspects.* import io.iohk.atala.iam.authentication.AuthenticationError import io.iohk.atala.iam.authentication.AuthenticationError.InvalidCredentials +import io.iohk.atala.shared.models.WalletAdministrationContext import io.iohk.atala.shared.models.WalletId import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import zio.Runtime.removeDefaultLoggers @@ -37,6 +38,8 @@ object ApiKeyAuthenticatorSpec extends ZIOSpecDefault, PostgresTestContainerSupp private def configLayer(config: ApiKeyConfig) = ZLayer.succeed(config) + private def walletAdminContextLayer = ZLayer.succeed(WalletAdministrationContext.Admin()) + def apiKeyAuthenticatorLayer(apiKeyConfig: ApiKeyConfig) = ZLayer.makeSome[AuthenticationRepository & EntityService & WalletManagementService, ApiKeyAuthenticator]( configLayer(apiKeyConfig) >>> ApiKeyAuthenticatorImpl.layer @@ -126,7 +129,7 @@ object ApiKeyAuthenticatorSpec extends ZIOSpecDefault, PostgresTestContainerSupp ), failWhenTheHeaderIsNotProvidedTest, failWhenTheHeaderIsAnEmptyStringTest - ).provideSomeLayer(apiKeyAuthenticatorLayer(apiKeyConfigEnabledMultiTenant)) + ).provideSomeLayer(apiKeyAuthenticatorLayer(apiKeyConfigEnabledMultiTenant) ++ walletAdminContextLayer) val authenticationEnabledMultiTenantSpecWithAutoProvisioning = suite("when authentication enabled in the multi-tenant mode with auto-provisioning")( @@ -162,6 +165,8 @@ object ApiKeyAuthenticatorSpec extends ZIOSpecDefault, PostgresTestContainerSupp ), failWhenTheHeaderIsNotProvidedTest, failWhenTheHeaderIsAnEmptyStringTest - ).provideSomeLayer(apiKeyAuthenticatorLayer(apiKeyConfigEnabledMultiTenantWithAutoProvisioning)) + ).provideSomeLayer( + apiKeyAuthenticatorLayer(apiKeyConfigEnabledMultiTenantWithAutoProvisioning) ++ walletAdminContextLayer + ) } diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorSpec.scala index 3e824158c7..4cd4cb4edf 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorSpec.scala @@ -7,6 +7,8 @@ import io.iohk.atala.agent.walletapi.service.WalletManagementServiceImpl import io.iohk.atala.agent.walletapi.sql.JdbcWalletNonSecretStorage import io.iohk.atala.agent.walletapi.sql.JdbcWalletSecretStorage import io.iohk.atala.iam.authentication.AuthenticationError +import io.iohk.atala.iam.authorization.keycloak.admin.KeycloakPermissionManagementService +import io.iohk.atala.shared.models.WalletAdministrationContext import io.iohk.atala.shared.models.WalletId import io.iohk.atala.sharedtest.containers.KeycloakAdminClient import io.iohk.atala.sharedtest.containers.KeycloakContainerCustom @@ -14,7 +16,6 @@ import io.iohk.atala.sharedtest.containers.KeycloakTestContainerSupport import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.DBTestUtils import org.keycloak.authorization.client.AuthzClient -import org.keycloak.representations.idm.CredentialRepresentation import org.keycloak.representations.idm.UserRepresentation import org.keycloak.representations.idm.authorization.ResourceRepresentation import org.keycloak.representations.idm.authorization.UmaPermissionRepresentation @@ -63,24 +64,6 @@ object KeycloakAuthenticatorSpec .attemptBlocking(authzClient.protection().resource().create(resource)) } yield () - private def createUser(userId: String, password: String, enabled: Boolean = true) = - for { - adminClient <- adminClientZIO - user = { - val cred = CredentialRepresentation() - cred.setTemporary(false) - cred.setValue(password) - - val user = UserRepresentation() - user.setId(userId) - user.setUsername(userId) - user.setEnabled(enabled) - user.setCredentials(List(cred).asJava) - user - } - _ <- ZIO.attemptBlocking(adminClient.realm(realmName).users().create(user)) - } yield () - private def createResourcePermission(walletId: WalletId, userId: String) = for { authzClient <- ZIO.service[AuthzClient] @@ -102,32 +85,36 @@ object KeycloakAuthenticatorSpec basicSpec .provide( KeycloakAuthenticatorImpl.layer, - ZLayer.fromZIO(initializeClient) >>> KeycloakClientImpl.layer ++ KeycloakClientImpl.authzClientLayer, + ZLayer.fromZIO(initializeClient) >>> KeycloakClientImpl.authzClientLayer >+> KeycloakClientImpl.layer, keycloakConfigLayer(), keycloakAdminClientLayer, keycloakContainerLayer, Client.default, + KeycloakPermissionManagementService.layer, WalletManagementServiceImpl.layer, JdbcWalletNonSecretStorage.layer, JdbcWalletSecretStorage.layer, contextAwareTransactorLayer, pgContainerLayer, - apolloLayer + apolloLayer, + ZLayer.succeed(WalletAdministrationContext.Admin()) ), disabledAutoRptSpec .provide( KeycloakAuthenticatorImpl.layer, - ZLayer.fromZIO(initializeClient) >>> KeycloakClientImpl.layer ++ KeycloakClientImpl.authzClientLayer, + ZLayer.fromZIO(initializeClient) >>> KeycloakClientImpl.authzClientLayer >+> KeycloakClientImpl.layer, keycloakConfigLayer(authUpgradeToRPT = false), keycloakAdminClientLayer, keycloakContainerLayer, Client.default, + KeycloakPermissionManagementService.layer, WalletManagementServiceImpl.layer, JdbcWalletNonSecretStorage.layer, JdbcWalletSecretStorage.layer, contextAwareTransactorLayer, pgContainerLayer, - apolloLayer + apolloLayer, + ZLayer.succeed(WalletAdministrationContext.Admin()) ) ) .provide(Runtime.removeDefaultLoggers) @@ -145,7 +132,7 @@ object KeycloakAuthenticatorSpec token <- client.getAccessToken("alice", "1234").map(_.access_token) entity <- authenticator.authenticate(token) permittedWallet <- authenticator.authorize(entity) - } yield assert(wallet.id)(equalTo(permittedWallet)) + } yield assert(wallet.id)(equalTo(permittedWallet.walletId)) }, test("reject token with a wallet that doesn't exist") { for { @@ -247,7 +234,7 @@ object KeycloakAuthenticatorSpec rpt <- client.getRpt(token) entity <- authenticator.authenticate(rpt) permittedWallet <- authenticator.authorize(entity) - } yield assert(wallet.id)(equalTo(permittedWallet)) + } yield assert(wallet.id)(equalTo(permittedWallet.walletId)) } ) diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakConfigUtils.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakConfigUtils.scala index 4beb70ca7e..5d3ca1b35e 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakConfigUtils.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakConfigUtils.scala @@ -4,7 +4,6 @@ import io.iohk.atala.iam.authentication.oidc.KeycloakConfig import io.iohk.atala.sharedtest.containers.{KeycloakContainerCustom, KeycloakTestContainerSupport} import zio.* import zio.ZIO.* -import zio.test.* import java.net.URI diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementServiceSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementServiceSpec.scala index d37ee2b5bf..beb91ce81e 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementServiceSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementServiceSpec.scala @@ -8,10 +8,12 @@ import io.iohk.atala.iam.authentication.oidc.{ KeycloakAuthenticator, KeycloakAuthenticatorImpl, KeycloakClient, - KeycloakClientImpl + KeycloakClientImpl, + KeycloakEntity } import io.iohk.atala.iam.authorization.core.PermissionManagement import io.iohk.atala.iam.authorization.core.PermissionManagement.Error.WalletNotFoundById +import io.iohk.atala.shared.models.WalletAdministrationContext import io.iohk.atala.shared.models.{WalletAccessContext, WalletId} import io.iohk.atala.sharedtest.containers.{KeycloakContainerCustom, KeycloakTestContainerSupport} import zio.* @@ -22,7 +24,6 @@ import zio.test.Assertion.* import zio.test.TestAspect.* import java.util.UUID -import scala.util.Try object KeycloakPermissionManagementServiceSpec extends ZIOSpecDefault @@ -33,6 +34,7 @@ object KeycloakPermissionManagementServiceSpec successfulCasesSuite, failureCasesSuite ) + .provide(Runtime.removeDefaultLoggers) val successfulCasesSuite = suite("Successful Cases")( test("grant wallet access to the user") { @@ -47,15 +49,16 @@ object KeycloakPermissionManagementServiceSpec username = "user_" + randomId password = randomId user <- createUser(username = username, password = password) + entity = KeycloakEntity(id = UUID.fromString(user.getId)) - permissionService <- ZIO.service[PermissionManagement.Service] - _ <- permissionService.grantWalletToUser(wallet.id, UUID.fromString(user.getId)) + permissionService <- ZIO.service[PermissionManagement.Service[KeycloakEntity]] + _ <- permissionService.grantWalletToUser(wallet.id, entity) token <- client.getAccessToken(username, password).map(_.access_token) entity <- authenticator.authenticate(token) permittedWallet <- authenticator.authorize(entity) - } yield assert(wallet.id)(equalTo(permittedWallet)) + } yield assert(wallet.id)(equalTo(permittedWallet.walletId)) }, test("revoke the wallet access from the user") { for { @@ -69,16 +72,17 @@ object KeycloakPermissionManagementServiceSpec username = "user_" + randomId password = randomId user <- createUser(username = username, password = password) + entity = KeycloakEntity(id = UUID.fromString(user.getId)) - permissionService <- ZIO.service[PermissionManagement.Service] - _ <- permissionService.grantWalletToUser(wallet.id, UUID.fromString(user.getId)) + permissionService <- ZIO.service[PermissionManagement.Service[KeycloakEntity]] + _ <- permissionService.grantWalletToUser(wallet.id, entity) token <- client.getAccessToken(username, password).map(_.access_token) entity <- authenticator.authenticate(token) permittedWallet <- authenticator.authorize(entity) - _ <- permissionService.revokeWalletFromUser(wallet.id, UUID.fromString(user.getId)) + _ <- permissionService.revokeWalletFromUser(wallet.id, entity) token2 <- client.getAccessToken(username, password).map(_.access_token) entity2 <- authenticator.authenticate(token) @@ -94,15 +98,17 @@ object KeycloakPermissionManagementServiceSpec KeycloakPermissionManagementService.layer, WalletManagementServiceStub.layer, KeycloakAuthenticatorImpl.layer, - ZLayer.fromZIO(initializeClient) >>> KeycloakClientImpl.layer ++ KeycloakClientImpl.authzClientLayer, - keycloakConfigLayer() + ZLayer.fromZIO(initializeClient) >>> KeycloakClientImpl.authzClientLayer >+> KeycloakClientImpl.layer, + keycloakConfigLayer(), + ZLayer.succeed(WalletAdministrationContext.Admin()) ) @@ sequential val failureCasesSuite = suite("Failure Cases Suite")( test("grant wallet access to the user with invalid wallet id") { for { - permissionService <- ZIO.service[PermissionManagement.Service] - exit <- permissionService.grantWalletToUser(WalletId.random, UUID.randomUUID()).exit + permissionService <- ZIO.service[PermissionManagement.Service[KeycloakEntity]] + entity = KeycloakEntity(id = UUID.randomUUID()) + exit <- permissionService.grantWalletToUser(WalletId.random, entity).exit } yield assert(exit)(fails(isSubtype[WalletNotFoundById](anything))) } ).provide( @@ -112,8 +118,9 @@ object KeycloakPermissionManagementServiceSpec KeycloakAdmin.layer, KeycloakPermissionManagementService.layer, WalletManagementServiceStub.layer, - ZLayer.fromZIO(initializeClient) >>> KeycloakClientImpl.layer ++ KeycloakClientImpl.authzClientLayer, - keycloakConfigLayer() + ZLayer.fromZIO(initializeClient) >>> KeycloakClientImpl.authzClientLayer >+> KeycloakClientImpl.layer, + keycloakConfigLayer(), + ZLayer.succeed(WalletAdministrationContext.Admin()) ) @@ sequential } 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 a643ed17a0..e8c9a52135 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 @@ -111,7 +111,7 @@ trait IssueControllerTestTools extends PostgresTestContainerSupport { } def httpBackend(controller: IssueController, authenticator: AuthenticatorWithAuthZ[BaseEntity]) = { - val issueEndpoints = IssueServerEndpoints(controller, authenticator) + val issueEndpoints = IssueServerEndpoints(controller, authenticator, authenticator) val backend = TapirStubInterpreter( 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 ad4e82a846..0eed7543e7 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 @@ -98,7 +98,8 @@ trait CredentialDefinitionTestTools extends PostgresTestContainerSupport { controller: CredentialDefinitionController, authenticator: AuthenticatorWithAuthZ[BaseEntity] ) = { - val credentialDefinitionRegistryEndpoints = CredentialDefinitionRegistryServerEndpoints(controller, authenticator) + val credentialDefinitionRegistryEndpoints = + CredentialDefinitionRegistryServerEndpoints(controller, authenticator, authenticator) val backend = TapirStubInterpreter( 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 d7e81bccc2..fab4e09bf5 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 @@ -88,7 +88,7 @@ trait CredentialSchemaTestTools extends PostgresTestContainerSupport { } def httpBackend(controller: CredentialSchemaController, authenticator: AuthenticatorWithAuthZ[BaseEntity]) = { - val schemaRegistryEndpoints = SchemaRegistryServerEndpoints(controller, authenticator) + val schemaRegistryEndpoints = SchemaRegistryServerEndpoints(controller, authenticator, authenticator) val backend = TapirStubInterpreter( 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 1f88ba1e65..56ce98de4e 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 @@ -8,7 +8,7 @@ import java.time.temporal.ChronoUnit import java.util.UUID trait BaseEntity { - val id: UUID + def id: UUID } case class Entity(id: UUID, name: String, walletId: UUID, createdAt: Instant, updatedAt: Instant) extends BaseEntity { 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 3fe2b79d4c..2974a14393 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 @@ -10,6 +10,7 @@ import zio.* import java.util.UUID import scala.language.implicitConversions +import io.iohk.atala.shared.models.WalletAdministrationContext sealed trait WalletManagementServiceError { final def toThrowable: Throwable = this @@ -21,6 +22,7 @@ object WalletManagementServiceError { final case class TooManyWebhookError(limit: Int, actual: Int) extends WalletManagementServiceError final case class DuplicatedWalletId(id: WalletId) extends WalletManagementServiceError final case class DuplicatedWalletSeed(id: WalletId) extends WalletManagementServiceError + final case class TooManyPermittedWallet() extends WalletManagementServiceError given Conversion[WalletNonSecretStorageError, WalletManagementServiceError] = { case WalletNonSecretStorageError.TooManyWebhook(limit, actual) => TooManyWebhookError(limit, actual) @@ -36,22 +38,27 @@ object WalletManagementServiceError { Exception(s"Too many webhook created for a wallet. Limit $limit, Actual $actual.") case DuplicatedWalletId(id) => Exception(s"Duplicated wallet id: $id") case DuplicatedWalletSeed(id) => Exception(s"Duplicated wallet seed for wallet id: $id") + case TooManyPermittedWallet() => + Exception(s"The operation is not allowed because wallet access already exists for the current user.") } } trait WalletManagementService { - def createWallet(wallet: Wallet, seed: Option[WalletSeed] = None): IO[WalletManagementServiceError, Wallet] + def createWallet( + wallet: Wallet, + seed: Option[WalletSeed] = None + ): ZIO[WalletAdministrationContext, WalletManagementServiceError, Wallet] - def getWallet(walletId: WalletId): IO[WalletManagementServiceError, Option[Wallet]] + def getWallet(walletId: WalletId): ZIO[WalletAdministrationContext, WalletManagementServiceError, Option[Wallet]] - def getWallets(walletIds: Seq[WalletId]): IO[WalletManagementServiceError, Seq[Wallet]] + def getWallets(walletIds: Seq[WalletId]): ZIO[WalletAdministrationContext, WalletManagementServiceError, Seq[Wallet]] /** @return A tuple containing a list of items and a count of total items */ def listWallets( offset: Option[Int] = None, limit: Option[Int] = None - ): IO[WalletManagementServiceError, (Seq[Wallet], Int)] + ): ZIO[WalletAdministrationContext, WalletManagementServiceError, (Seq[Wallet], Int)] def listWalletNotifications: ZIO[WalletAccessContext, WalletManagementServiceError, Seq[EventNotificationConfig]] 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 4f95bdbb35..94858f11e4 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 @@ -7,6 +7,7 @@ import io.iohk.atala.agent.walletapi.storage.WalletNonSecretStorage import io.iohk.atala.agent.walletapi.storage.WalletSecretStorage import io.iohk.atala.event.notification.EventNotificationConfig import io.iohk.atala.shared.models.WalletAccessContext +import io.iohk.atala.shared.models.WalletAdministrationContext import io.iohk.atala.shared.models.WalletId import zio.* @@ -19,8 +20,17 @@ class WalletManagementServiceImpl( secretStorage: WalletSecretStorage, ) extends WalletManagementService { - override def createWallet(wallet: Wallet, seed: Option[WalletSeed]): IO[WalletManagementServiceError, Wallet] = + override def createWallet( + wallet: Wallet, + seed: Option[WalletSeed] + ): ZIO[WalletAdministrationContext, WalletManagementServiceError, Wallet] = for { + _ <- ZIO.serviceWithZIO[WalletAdministrationContext] { + case WalletAdministrationContext.Admin() => ZIO.unit + case WalletAdministrationContext.SelfService(permittedWallets) if permittedWallets.isEmpty => ZIO.unit + case WalletAdministrationContext.SelfService(_) => + ZIO.fail(WalletManagementServiceError.TooManyPermittedWallet()) + } seed <- seed.fold( apollo.ecKeyFactory .randomBip32Seed() @@ -37,23 +47,40 @@ class WalletManagementServiceImpl( .provide(ZLayer.succeed(WalletAccessContext(wallet.id))) } yield createdWallet - override def getWallet(walletId: WalletId): IO[WalletManagementServiceError, Option[Wallet]] = - nonSecretStorage - .getWallet(walletId) - .mapError(e => e) + override def getWallet( + walletId: WalletId + ): ZIO[WalletAdministrationContext, WalletManagementServiceError, Option[Wallet]] = { + ZIO + .serviceWith[WalletAdministrationContext](_.isAuthorized(walletId)) + .flatMap { + case true => nonSecretStorage.getWallet(walletId).mapError(e => e) + case false => ZIO.none + } + } - override def getWallets(walletIds: Seq[WalletId]): IO[WalletManagementServiceError, Seq[Wallet]] = - nonSecretStorage - .getWallets(walletIds) - .mapError(e => e) + override def getWallets( + walletIds: Seq[WalletId] + ): ZIO[WalletAdministrationContext, WalletManagementServiceError, Seq[Wallet]] = { + ZIO + .serviceWith[WalletAdministrationContext](ctx => walletIds.filter(ctx.isAuthorized)) + .flatMap { filteredIds => nonSecretStorage.getWallets(filteredIds).mapError(e => e) } + } override def listWallets( offset: Option[Int], limit: Option[Int] - ): IO[WalletManagementServiceError, (Seq[Wallet], Int)] = - nonSecretStorage - .listWallet(offset = offset, limit = limit) - .mapError(e => e) + ): ZIO[WalletAdministrationContext, WalletManagementServiceError, (Seq[Wallet], Int)] = + ZIO.serviceWithZIO[WalletAdministrationContext] { + case WalletAdministrationContext.Admin() => + nonSecretStorage + .listWallet(offset = offset, limit = limit) + .mapError(e => e) + case WalletAdministrationContext.SelfService(permittedWallets) => + nonSecretStorage + .getWallets(permittedWallets) + .map(wallets => (wallets, wallets.length)) + .mapError(e => e) + } override def listWalletNotifications : ZIO[WalletAccessContext, WalletManagementServiceError, Seq[EventNotificationConfig]] = diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala index 11bd3cd096..2349f43db8 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala @@ -23,6 +23,7 @@ import zio.test.* import zio.test.Assertion.* import scala.collection.immutable.ArraySeq +import io.iohk.atala.shared.models.WalletAdministrationContext object ManagedDIDServiceSpec extends ZIOSpecDefault, @@ -153,7 +154,8 @@ object ManagedDIDServiceSpec serviceLayer, pgContainerLayer, jdbcSecretStorageLayer, - contextAwareTransactorLayer >+> systemTransactorLayer >>> JdbcDIDNonSecretStorage.layer + contextAwareTransactorLayer >+> systemTransactorLayer >>> JdbcDIDNonSecretStorage.layer, + ZLayer.succeed(WalletAdministrationContext.Admin()) ) .provide(Runtime.removeDefaultLoggers) @@ -162,7 +164,8 @@ object ManagedDIDServiceSpec serviceLayer, pgContainerLayer, vaultSecretStorageLayer, - contextAwareTransactorLayer >+> systemTransactorLayer >>> JdbcDIDNonSecretStorage.layer + contextAwareTransactorLayer >+> systemTransactorLayer >>> JdbcDIDNonSecretStorage.layer, + ZLayer.succeed(WalletAdministrationContext.Admin()) ) .provide(Runtime.removeDefaultLoggers) diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceSpec.scala index 08b07dc688..0d1bda9ebc 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/WalletManagementServiceSpec.scala @@ -9,6 +9,7 @@ import io.iohk.atala.agent.walletapi.sql.JdbcWalletSecretStorage import io.iohk.atala.agent.walletapi.storage.WalletSecretStorage import io.iohk.atala.agent.walletapi.vault.VaultWalletSecretStorage import io.iohk.atala.shared.models.WalletAccessContext +import io.iohk.atala.shared.models.WalletAdministrationContext import io.iohk.atala.shared.models.WalletId import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.DBTestUtils @@ -37,7 +38,8 @@ object WalletManagementServiceSpec JdbcWalletSecretStorage.layer, contextAwareTransactorLayer, pgContainerLayer, - apolloLayer + apolloLayer, + ZLayer.succeed(WalletAdministrationContext.Admin()) ) val suite2 = testSuite("vault as secret storage") @@ -48,7 +50,8 @@ object WalletManagementServiceSpec contextAwareTransactorLayer, pgContainerLayer, apolloLayer, - vaultKvClientLayer + vaultKvClientLayer, + ZLayer.succeed(WalletAdministrationContext.Admin()) ) suite("WalletManagementService")(suite1, suite2) diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorageSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorageSpec.scala index 0f617075e0..9304f9fc7b 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorageSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorageSpec.scala @@ -19,6 +19,7 @@ import io.iohk.atala.test.container.{DBTestUtils, VaultTestContainerSupport} import zio.* import zio.test.* import zio.test.Assertion.* +import io.iohk.atala.shared.models.WalletAdministrationContext object DIDSecretStorageSpec extends ZIOSpecDefault, @@ -44,7 +45,8 @@ object DIDSecretStorageSpec systemTransactorLayer, contextAwareTransactorLayer, pgContainerLayer, - walletManagementServiceLayer + walletManagementServiceLayer, + ZLayer.succeed(WalletAdministrationContext.Admin()) ) val vaultTestSuite = commonSpec("VaultDIDSecretStorage") @@ -56,7 +58,8 @@ object DIDSecretStorageSpec contextAwareTransactorLayer, pgContainerLayer, vaultKvClientLayer, - walletManagementServiceLayer + walletManagementServiceLayer, + ZLayer.succeed(WalletAdministrationContext.Admin()) ) val inMemoryTestSuite = commonSpec("InMemoryDIDSecretStorage") @@ -67,7 +70,8 @@ object DIDSecretStorageSpec systemTransactorLayer, contextAwareTransactorLayer, pgContainerLayer, - walletManagementServiceLayer + walletManagementServiceLayer, + ZLayer.succeed(WalletAdministrationContext.Admin()) ) suite("DIDSecretStorage")(jdbcTestSuite, vaultTestSuite, inMemoryTestSuite) @@ TestAspect.sequential diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorageSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorageSpec.scala index 7c3d0328e0..c44996a68b 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorageSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/GenericSecretStorageSpec.scala @@ -7,6 +7,7 @@ import io.iohk.atala.agent.walletapi.service.{WalletManagementService, WalletMan import io.iohk.atala.agent.walletapi.sql.{JdbcGenericSecretStorage, JdbcWalletNonSecretStorage, JdbcWalletSecretStorage} import io.iohk.atala.agent.walletapi.vault.{VaultGenericSecretStorage, VaultWalletSecretStorage} import io.iohk.atala.shared.models.WalletAccessContext +import io.iohk.atala.shared.models.WalletAdministrationContext import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport import io.iohk.atala.test.container.{DBTestUtils, VaultTestContainerSupport} import zio.* @@ -16,7 +17,6 @@ import zio.test.Assertion.* import java.util.UUID import scala.util.Try - object GenericSecretStorageSpec extends ZIOSpecDefault, StorageSpecHelper, @@ -49,7 +49,8 @@ object GenericSecretStorageSpec JdbcGenericSecretStorage.layer, contextAwareTransactorLayer, pgContainerLayer, - walletManagementServiceLayer + walletManagementServiceLayer, + ZLayer.succeed(WalletAdministrationContext.Admin()) ) val vaultTestSuite = commonSpec("VaultGenericSecretStorage") @@ -58,7 +59,8 @@ object GenericSecretStorageSpec VaultGenericSecretStorage.layer, pgContainerLayer, vaultKvClientLayer, - walletManagementServiceLayer + walletManagementServiceLayer, + ZLayer.succeed(WalletAdministrationContext.Admin()) ) val inMemoryTestSuite = commonSpec("InMemoryGenericSecretStorage") @@ -67,7 +69,8 @@ object GenericSecretStorageSpec GenericSecretStorageInMemory.layer, contextAwareTransactorLayer, pgContainerLayer, - walletManagementServiceLayer + walletManagementServiceLayer, + ZLayer.succeed(WalletAdministrationContext.Admin()) ) suite("GenericSecretStorage")(jdbcTestSuite, vaultTestSuite, inMemoryTestSuite) @@ TestAspect.sequential diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/StorageSpecHelper.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/StorageSpecHelper.scala index 54e7b48572..f7198664b0 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/StorageSpecHelper.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/StorageSpecHelper.scala @@ -15,6 +15,7 @@ import io.iohk.atala.castor.core.model.did.PrismDIDOperation import io.iohk.atala.castor.core.model.did.ScheduledDIDOperationStatus import io.iohk.atala.castor.core.model.did.VerificationRelationship import io.iohk.atala.shared.models.WalletAccessContext +import io.iohk.atala.shared.models.WalletAdministrationContext import zio.* import zio.test.* @@ -81,6 +82,7 @@ trait StorageSpecHelper extends ApolloSpecHelper { .map(wallet => WalletAccessContext(wallet.id)) .mapError(_.toThrowable) .orDie + .provideSomeLayer(ZLayer.succeed(WalletAdministrationContext.Admin())) ) ) } diff --git a/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainerSupport.scala b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainerSupport.scala index b009def33a..9e7fcdce5d 100644 --- a/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainerSupport.scala +++ b/shared-test/src/main/scala/io/iohk/atala/sharedtest/containers/KeycloakTestContainerSupport.scala @@ -1,4 +1,5 @@ package io.iohk.atala.sharedtest.containers + import org.keycloak.admin.client.Keycloak import org.keycloak.representations.idm.{ ClientRepresentation, diff --git a/shared/src/main/scala/io/iohk/atala/shared/models/MultiTenancy.scala b/shared/src/main/scala/io/iohk/atala/shared/models/MultiTenancy.scala index d59244acec..bafe9cdeee 100644 --- a/shared/src/main/scala/io/iohk/atala/shared/models/MultiTenancy.scala +++ b/shared/src/main/scala/io/iohk/atala/shared/models/MultiTenancy.scala @@ -15,3 +15,18 @@ object WalletId { } final case class WalletAccessContext(walletId: WalletId) + +// This might eventually be unified with WalletAccessContext and introduce some scope / role. +// For now this is only intended for wallet admin related operations. +sealed trait WalletAdministrationContext { + def isAuthorized(walletId: WalletId): Boolean +} + +object WalletAdministrationContext { + final case class Admin() extends WalletAdministrationContext { + def isAuthorized(walletId: WalletId): Boolean = true + } + final case class SelfService(permittedWallets: Seq[WalletId]) extends WalletAdministrationContext { + def isAuthorized(walletId: WalletId): Boolean = permittedWallets.contains(walletId) + } +} From 03d43c98d8ab64e5b47830d95a6356f9d6dd1b82 Mon Sep 17 00:00:00 2001 From: patlo-iog Date: Thu, 9 Nov 2023 17:55:30 +0700 Subject: [PATCH 2/2] fix(prism-agent): perform percent encoding on auth header for token introspection request (#780) Signed-off-by: Pat Losoponkul --- .../atala/iam/authentication/oidc/KeycloakClient.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala index 6f1562e1db..d912abf741 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala @@ -7,6 +7,8 @@ import zio.* import zio.http.* import zio.json.* +import java.net.URLEncoder +import java.nio.charset.StandardCharsets import scala.jdk.CollectionConverters.* final case class TokenIntrospection(active: Boolean, sub: Option[String]) @@ -61,7 +63,10 @@ class KeycloakClientImpl(client: AuthzClient, httpClient: Client, keycloakConfig url = introspectionUrl, method = Method.POST, headers = baseFormHeaders ++ Headers( - Header.Authorization.Basic(keycloakConfig.clientId, keycloakConfig.clientSecret) + Header.Authorization.Basic( + username = URLEncoder.encode(keycloakConfig.clientId, StandardCharsets.UTF_8), + password = URLEncoder.encode(keycloakConfig.clientSecret, StandardCharsets.UTF_8) + ) ), content = Body.fromURLEncodedForm( Form(