Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ATL-6833 integrate ZIO failures and defects in wallet event controller #1186

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,8 @@ object AgentInitialization {
walletService <- ZIO.service[WalletManagementService]
isDefaultWalletEnabled = config.enabled
isDefaultWalletExist <- walletService
.getWallet(defaultWalletId)
.findWallet(defaultWalletId)
.map(_.isDefined)
.mapError(_.toThrowable)
_ <- ZIO.logInfo(s"Default wallet not enabled.").when(!isDefaultWalletEnabled)
_ <- ZIO.logInfo(s"Default wallet already exist.").when(isDefaultWalletExist)
_ <- createDefaultWallet.when(isDefaultWalletEnabled && !isDefaultWalletExist)
Expand All @@ -213,14 +212,14 @@ object AgentInitialization {
_ <- ZIO.logInfo(s"Default wallet seed is not provided. New seed will be generated.").when(seed.isEmpty)
_ <- walletService
.createWallet(defaultWallet, seed)
.mapError(_.toThrowable)
.orDieAsUnmanagedFailure
_ <- entityService.create(defaultEntity).mapError(e => Exception(e.message))
_ <- apiKeyAuth.add(defaultEntity.id, config.authApiKey).mapError(e => Exception(e.message))
_ <- config.webhookUrl.fold(ZIO.unit) { url =>
val customHeaders = config.webhookApiKey.fold(Map.empty)(apiKey => Map("Authorization" -> s"Bearer $apiKey"))
walletService
.createWalletNotification(EventNotificationConfig(defaultWalletId, url, customHeaders))
.mapError(_.toThrowable)
.orDieAsUnmanagedFailure
.provide(ZLayer.succeed(WalletAccessContext(defaultWalletId)))
}
} yield ()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.hyperledger.identus.event.controller

import org.hyperledger.identus.agent.walletapi.service.{WalletManagementService, WalletManagementServiceError}
import org.hyperledger.identus.agent.walletapi.service.WalletManagementService
import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext}
import org.hyperledger.identus.api.http.model.{CollectionStats, PaginationInput}
import org.hyperledger.identus.api.util.PaginationUtils
Expand All @@ -10,7 +10,6 @@ import org.hyperledger.identus.event.controller.http.{
WebhookNotificationPage
}
import org.hyperledger.identus.event.notification.EventNotificationConfig
import org.hyperledger.identus.iam.wallet.http.controller.WalletManagementController
import org.hyperledger.identus.shared.models.WalletAccessContext
import zio.*

Expand All @@ -30,15 +29,8 @@ trait EventController {
def deleteWebhookNotification(id: UUID)(implicit rc: RequestContext): ZIO[WalletAccessContext, ErrorResponse, Unit]
}

object EventController {
given Conversion[WalletManagementServiceError, ErrorResponse] =
WalletManagementController.walletServiceErrorConversion
}

class EventControllerImpl(service: WalletManagementService) extends EventController {

import EventController.given

override def createWebhookNotification(
request: CreateWebhookNotification
)(implicit rc: RequestContext): ZIO[WalletAccessContext, ErrorResponse, WebhookNotification] = {
Expand All @@ -61,7 +53,7 @@ class EventControllerImpl(service: WalletManagementService) extends EventControl
// Return paginated result for consistency and to make it future-proof
val pagination = PaginationInput().toPagination
for {
items <- service.listWalletNotifications.mapError[ErrorResponse](e => e)
items <- service.listWalletNotifications
totalCount = items.length
stats = CollectionStats(totalCount = totalCount, filteredCount = totalCount)
} yield WebhookNotificationPage(
Expand All @@ -75,11 +67,8 @@ class EventControllerImpl(service: WalletManagementService) extends EventControl

override def deleteWebhookNotification(
id: UUID
)(implicit rc: RequestContext): ZIO[WalletAccessContext, ErrorResponse, Unit] = {
service
.deleteWalletNotification(id)
.mapError[ErrorResponse](e => e)
}
)(implicit rc: RequestContext): ZIO[WalletAccessContext, ErrorResponse, Unit] =
service.deleteWalletNotification(id)

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ case class ApiKeyAuthenticatorImpl(
for {
wallet <- walletManagementService
.createWallet(Wallet("Auto provisioned wallet", WalletId.random))
.mapError(cause => AuthenticationRepositoryError.UnexpectedError(cause))
.orDieAsUnmanagedFailure
.provide(ZLayer.succeed(WalletAdministrationContext.Admin()))
entityToCreate = Entity(name = "Auto provisioned entity", walletId = wallet.id.toUUID)
entity <- entityService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ case class KeycloakPermissionManagementService(
): ZIO[WalletAdministrationContext, PermissionManagement.Error, Unit] = {
for {
_ <- walletManagementService
.getWallet(walletId)
.mapError(wmse => ServiceError(wmse.toThrowable.getMessage))
.findWallet(walletId)
.someOrFail(WalletNotFoundById(walletId))

walletResourceOpt <- findWalletResource(walletId)
Expand Down Expand Up @@ -114,8 +113,7 @@ case class KeycloakPermissionManagementService(
val userId = entity.id
for {
_ <- walletManagementService
.getWallet(walletId)
.mapError(wmse => ServiceError(wmse.toThrowable.getMessage))
.findWallet(walletId)
.someOrFail(WalletNotFoundById(walletId))

walletResource <- findWalletResource(walletId)
Expand Down Expand Up @@ -185,7 +183,6 @@ case class KeycloakPermissionManagementService(
val walletIds = resourceIds.flatMap(id => Try(UUID.fromString(id)).toOption).map(WalletId.fromUUID)
walletManagementService
.getWallets(walletIds)
.mapError(e => Error.UnexpectedError(e.toThrowable))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,6 @@ trait WalletManagementController {
}

object WalletManagementController {
given walletServiceErrorConversion: Conversion[WalletManagementServiceError, ErrorResponse] = {
case WalletManagementServiceError.UnexpectedStorageError(cause) =>
ErrorResponse.internalServerError(detail = Some(cause.getMessage()))
case WalletManagementServiceError.TooManyWebhookError(limit, actual) =>
ErrorResponse.conflict(detail = Some(s"Too many webhook created for a wallet. (limit $limit, actual $actual)"))
case WalletManagementServiceError.DuplicatedWalletId(id) =>
ErrorResponse.badRequest(s"Wallet id $id is not unique.")
case WalletManagementServiceError.DuplicatedWalletSeed(id) =>
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))
Expand Down Expand Up @@ -85,7 +70,6 @@ class WalletManagementControllerImpl(
for {
pageResult <- walletService
.listWallets(offset = paginationInput.offset, limit = paginationInput.limit)
.mapError[ErrorResponse](e => e)
(items, totalCount) = pageResult
stats = CollectionStats(totalCount = totalCount, filteredCount = totalCount)
} yield WalletDetailPage(
Expand All @@ -102,8 +86,7 @@ class WalletManagementControllerImpl(
)(implicit rc: RequestContext): ZIO[WalletAdministrationContext, ErrorResponse, WalletDetail] = {
for {
wallet <- walletService
.getWallet(WalletId.fromUUID(walletId))
.mapError[ErrorResponse](e => e)
.findWallet(WalletId.fromUUID(walletId))
.someOrFail(ErrorResponse.notFound(detail = Some(s"Wallet id $walletId does not exist.")))
} yield wallet
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import zio.*

class WalletSecretStorageInMemory(storeRef: Ref[Map[WalletId, WalletSeed]]) extends WalletSecretStorage {

override def setWalletSeed(seed: WalletSeed): RIO[WalletAccessContext, Unit] = {
override def setWalletSeed(seed: WalletSeed): URIO[WalletAccessContext, Unit] = {
for {
walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId)
_ <- storeRef.update(_.updated(walletId, seed))
} yield ()
}

override def getWalletSeed: RIO[WalletAccessContext, Option[WalletSeed]] = {
override def getWalletSeed: URIO[WalletAccessContext, Option[WalletSeed]] = {
for {
walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId)
seed <- storeRef.get.map(_.get(walletId))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,64 @@
package org.hyperledger.identus.agent.walletapi.service

import org.hyperledger.identus.agent.walletapi.model.{Wallet, WalletSeed}
import org.hyperledger.identus.agent.walletapi.storage.WalletNonSecretStorageError
import org.hyperledger.identus.agent.walletapi.service.WalletManagementServiceError.{
DuplicatedWalletSeed,
TooManyPermittedWallet,
TooManyWebhookError
}
import org.hyperledger.identus.event.notification.EventNotificationConfig
import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletAdministrationContext, WalletId}
import org.hyperledger.identus.shared.models.*
import zio.*

import java.util.UUID
import scala.language.implicitConversions

sealed trait WalletManagementServiceError {
final def toThrowable: Throwable = this
sealed trait WalletManagementServiceError(
val statusCode: StatusCode,
val userFacingMessage: String
) extends Failure {
override val namespace: String = "WalletManagementServiceError"
}

object WalletManagementServiceError {
final case class UnexpectedStorageError(cause: Throwable) extends 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)
case WalletNonSecretStorageError.DuplicatedWalletId(id) => DuplicatedWalletId(id)
case WalletNonSecretStorageError.DuplicatedWalletSeed(id) => DuplicatedWalletSeed(id)
case WalletNonSecretStorageError.UnexpectedError(cause) => UnexpectedStorageError(cause)
}

given Conversion[WalletManagementServiceError, Throwable] = {
case UnexpectedStorageError(cause) => Exception(cause)
case TooManyWebhookError(limit, actual) =>
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.")
}

final case class TooManyWebhookError(walletId: WalletId, limit: Int)
extends WalletManagementServiceError(
StatusCode.UnprocessableContent,
s"The maximum number of webhooks has been reached for the wallet: walletId=$walletId, limit=$limit"
)
final case class DuplicatedWalletSeed()
extends WalletManagementServiceError(
StatusCode.UnprocessableContent,
s"A wallet with the same seed already exist"
)
final case class TooManyPermittedWallet()
extends WalletManagementServiceError(
StatusCode.BadRequest,
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
): ZIO[WalletAdministrationContext, WalletManagementServiceError, Wallet]
): ZIO[WalletAdministrationContext, TooManyPermittedWallet | DuplicatedWalletSeed, Wallet]

def getWallet(walletId: WalletId): ZIO[WalletAdministrationContext, WalletManagementServiceError, Option[Wallet]]
def findWallet(walletId: WalletId): URIO[WalletAdministrationContext, Option[Wallet]]

def getWallets(walletIds: Seq[WalletId]): ZIO[WalletAdministrationContext, WalletManagementServiceError, Seq[Wallet]]
def getWallets(walletIds: Seq[WalletId]): URIO[WalletAdministrationContext, 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
): ZIO[WalletAdministrationContext, WalletManagementServiceError, (Seq[Wallet], Int)]
): URIO[WalletAdministrationContext, (Seq[Wallet], Int)]

def listWalletNotifications: ZIO[WalletAccessContext, WalletManagementServiceError, Seq[EventNotificationConfig]]
def listWalletNotifications: URIO[WalletAccessContext, Seq[EventNotificationConfig]]

def createWalletNotification(
config: EventNotificationConfig
): ZIO[WalletAccessContext, WalletManagementServiceError, EventNotificationConfig]
): ZIO[WalletAccessContext, TooManyWebhookError, Unit]

def deleteWalletNotification(id: UUID): ZIO[WalletAccessContext, WalletManagementServiceError, Unit]
def deleteWalletNotification(id: UUID): URIO[WalletAccessContext, Unit]
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package org.hyperledger.identus.agent.walletapi.service

import org.hyperledger.identus.agent.walletapi.model.{Wallet, WalletSeed}
import org.hyperledger.identus.agent.walletapi.service.WalletManagementServiceError.{
DuplicatedWalletSeed,
TooManyPermittedWallet,
TooManyWebhookError
}
import org.hyperledger.identus.agent.walletapi.service.WalletManagementServiceImpl.MAX_WEBHOOK_PER_WALLET
import org.hyperledger.identus.agent.walletapi.storage.{WalletNonSecretStorage, WalletSecretStorage}
import org.hyperledger.identus.event.notification.EventNotificationConfig
import org.hyperledger.identus.shared.crypto.Apollo
Expand All @@ -19,13 +25,13 @@ class WalletManagementServiceImpl(
override def createWallet(
wallet: Wallet,
seed: Option[WalletSeed]
): ZIO[WalletAdministrationContext, WalletManagementServiceError, Wallet] =
): ZIO[WalletAdministrationContext, TooManyPermittedWallet | DuplicatedWalletSeed, 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())
ZIO.fail(TooManyPermittedWallet())
}
seed <- seed.fold(
apollo.secp256k1.randomBip32Seed
Expand All @@ -35,70 +41,71 @@ class WalletManagementServiceImpl(
.orDieWith(msg => Exception(s"Wallet seed generation failed: $msg"))
}
)(ZIO.succeed)
_ <- nonSecretStorage.findWalletBySeed(seed.sha256Digest).flatMap {
case Some(w) => ZIO.fail(DuplicatedWalletSeed())
case None => ZIO.unit
}
createdWallet <- nonSecretStorage
.createWallet(wallet, seed.sha256Digest)
.mapError[WalletManagementServiceError](e => e)
_ <- secretStorage
.setWalletSeed(seed)
.mapError(WalletManagementServiceError.UnexpectedStorageError.apply)
.provide(ZLayer.succeed(WalletAccessContext(wallet.id)))
} yield createdWallet

override def getWallet(
override def findWallet(
walletId: WalletId
): ZIO[WalletAdministrationContext, WalletManagementServiceError, Option[Wallet]] = {
): URIO[WalletAdministrationContext, Option[Wallet]] = {
ZIO
.serviceWith[WalletAdministrationContext](_.isAuthorized(walletId))
.flatMap {
case true => nonSecretStorage.getWallet(walletId).mapError(e => e)
case true => nonSecretStorage.findWalletById(walletId)
case false => ZIO.none
}
}

override def getWallets(
walletIds: Seq[WalletId]
): ZIO[WalletAdministrationContext, WalletManagementServiceError, Seq[Wallet]] = {
): URIO[WalletAdministrationContext, Seq[Wallet]] = {
ZIO
.serviceWith[WalletAdministrationContext](ctx => walletIds.filter(ctx.isAuthorized))
.flatMap { filteredIds => nonSecretStorage.getWallets(filteredIds).mapError(e => e) }
.flatMap { filteredIds => nonSecretStorage.getWallets(filteredIds) }
}

override def listWallets(
offset: Option[Int],
limit: Option[Int]
): ZIO[WalletAdministrationContext, WalletManagementServiceError, (Seq[Wallet], Int)] =
): URIO[WalletAdministrationContext, (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]] =
override def listWalletNotifications: URIO[WalletAccessContext, Seq[EventNotificationConfig]] =
nonSecretStorage.walletNotification
.mapError(e => e)

override def createWalletNotification(
config: EventNotificationConfig
): ZIO[WalletAccessContext, WalletManagementServiceError, EventNotificationConfig] =
nonSecretStorage
.createWalletNotification(config)
.mapError(e => e)
): ZIO[WalletAccessContext, TooManyWebhookError, Unit] =
for {
count <- nonSecretStorage.countWalletNotification
_ <-
if (count < MAX_WEBHOOK_PER_WALLET) nonSecretStorage.createWalletNotification(config)
else ZIO.fail(TooManyWebhookError(config.walletId, MAX_WEBHOOK_PER_WALLET))
} yield ()

override def deleteWalletNotification(id: UUID): ZIO[WalletAccessContext, WalletManagementServiceError, Unit] =
override def deleteWalletNotification(id: UUID): URIO[WalletAccessContext, Unit] =
nonSecretStorage
.deleteWalletNotification(id)
.mapError(e => e)

}

object WalletManagementServiceImpl {
val MAX_WEBHOOK_PER_WALLET = 1
val layer: URLayer[Apollo & WalletNonSecretStorage & WalletSecretStorage, WalletManagementService] = {
ZLayer.fromFunction(WalletManagementServiceImpl(_, _, _))
}
Expand Down
Loading
Loading