Skip to content

Commit

Permalink
fix(prism-agent): refine multi-tenant error response and validations (#…
Browse files Browse the repository at this point in the history
…719)

Signed-off-by: Pat Losoponkul <pat.losoponkul@iohk.io>
  • Loading branch information
patlo-iog committed Sep 15, 2023
1 parent 4fe6677 commit 1f9ede3
Show file tree
Hide file tree
Showing 19 changed files with 256 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ object PrismAgentApp {
config <- ZIO.service[AppConfig]
_ <- ZIO
.serviceWithZIO[WalletManagementService](_.listWallets().map(_._1))
.mapError(_.toThrowable)
.flatMap { wallets =>
ZIO.foreach(wallets) { wallet =>
BackgroundJobs.issueCredentialDidCommExchanges
Expand All @@ -74,6 +75,7 @@ object PrismAgentApp {
config <- ZIO.service[AppConfig]
_ <- ZIO
.serviceWithZIO[WalletManagementService](_.listWallets().map(_._1))
.mapError(_.toThrowable)
.flatMap { wallets =>
ZIO.foreach(wallets) { wallet =>
BackgroundJobs.presentProofExchanges
Expand All @@ -94,6 +96,7 @@ object PrismAgentApp {
config <- ZIO.service[AppConfig]
_ <- ZIO
.serviceWithZIO[WalletManagementService](_.listWallets().map(_._1))
.mapError(_.toThrowable)
.flatMap { wallets =>
ZIO.foreach(wallets) { wallet =>
ConnectBackgroundJobs.didCommExchanges
Expand Down Expand Up @@ -170,23 +173,22 @@ object AgentInitialization {
} yield ()

private val validateAppConfig =
for {
config <- ZIO.service[AppConfig]
isApiKeyEnabled = config.agent.authentication.apiKey.enabled
isDefaultWalletEnabled = config.agent.defaultWallet.enabled
_ <- ZIO
.fail(RuntimeException("The default wallet cannot be disabled if the apikey authentication is disabled."))
.when(!isApiKeyEnabled && !isDefaultWalletEnabled)
.unit
} yield ()
ZIO.serviceWithZIO[AppConfig](conf =>
ZIO
.fromEither(conf.validate)
.mapError(msg => RuntimeException(s"Application configuration is invalid. $msg"))
)

private val initializeDefaultWallet =
for {
_ <- ZIO.logInfo("Initializing default wallet.")
config <- ZIO.serviceWith[AppConfig](_.agent.defaultWallet)
walletService <- ZIO.service[WalletManagementService]
isDefaultWalletEnabled = config.enabled
isDefaultWalletExist <- walletService.getWallet(defaultWalletId).map(_.isDefined)
isDefaultWalletExist <- walletService
.getWallet(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 @@ -206,13 +208,16 @@ object AgentInitialization {
.asSome
}
_ <- ZIO.logInfo(s"Default wallet seed is not provided. New seed will be generated.").when(seed.isEmpty)
_ <- walletService.createWallet(defaultWallet, seed)
_ <- walletService
.createWallet(defaultWallet, seed)
.mapError(_.toThrowable)
_ <- 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)
.provide(ZLayer.succeed(WalletAccessContext(defaultWalletId)))
}
} yield ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ final case class AppConfig(
agent: AgentConfig,
connect: ConnectConfig,
prismNode: PrismNodeConfig,
)
) {
def validate: Either[String, Unit] =
for {
_ <- agent.validate
} yield ()
}

object AppConfig {
val descriptor: ConfigDescriptor[AppConfig] = Descriptor[AppConfig]
Expand Down Expand Up @@ -126,7 +131,14 @@ final case class AgentConfig(
secretStorage: SecretStorageConfig,
webhookPublisher: WebhookPublisherConfig,
defaultWallet: DefaultWalletConfig
)
) {
def validate: Either[String, Unit] = {
if (!defaultWallet.enabled && !authentication.apiKey.enabled)
Left("The default wallet cannot be disabled if the apikey authentication is disabled.")
else
Right(())
}
}

final case class HttpEndpointConfig(http: HttpConfig)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ case class AdminApiKeyAuthenticatorImpl(adminConfig: AdminConfig) extends AdminA

def authenticate(adminApiKey: String): IO[AuthenticationError, Entity] = {
if (adminApiKey == adminConfig.token) {
ZIO.logInfo(s"Admin API key authentication successful") *>
ZIO.logDebug(s"Admin API key authentication successful") *>
ZIO.succeed(Admin)
} else ZIO.fail(AdminApiKeyAuthenticationError.invalidAdminApiKey)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import io.iohk.atala.api.http.Annotation
import io.iohk.atala.iam.entity.http.model.CreateEntityRequest.annotations
import sttp.tapir.Schema
import sttp.tapir.Schema.annotations.{description, encodedExample, validate, validateEach}
import sttp.tapir.Validator.*
import sttp.tapir.Validator
import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder}

import java.util.UUID
Expand All @@ -13,9 +13,9 @@ case class CreateEntityRequest(
@description(annotations.id.description)
@encodedExample(annotations.id.example)
id: Option[UUID],
@description(annotations.id.description)
@encodedExample(annotations.id.example)
@validate(nonEmptyString)
@description(annotations.name.description)
@encodedExample(annotations.name.example)
@validate(annotations.name.validator)
name: String,
@description(annotations.walletId.description)
@encodedExample(annotations.walletId.example)
Expand Down Expand Up @@ -44,7 +44,8 @@ object CreateEntityRequest {
extends Annotation[String](
description =
"The new `name` of the entity to be created. If this field is not provided, the server will generate a random name for the entity",
example = "John Doe"
example = "John Doe",
validator = Validator.all(Validator.nonEmptyString, Validator.maxLength(128))
)

object walletId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import io.iohk.atala.api.http.Annotation
import io.iohk.atala.iam.entity.http.model.UpdateEntityNameRequest.annotations
import sttp.tapir.Schema
import sttp.tapir.Schema.annotations.{description, encodedExample, validate, validateEach}
import sttp.tapir.Validator.*
import sttp.tapir.Validator
import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder}

case class UpdateEntityNameRequest(
@description(annotations.name.description)
@encodedExample(annotations.name.example)
@validate(nonEmptyString)
@validate(annotations.name.validator)
name: String
)

Expand All @@ -24,6 +24,11 @@ object UpdateEntityNameRequest {
given schema: Schema[UpdateEntityNameRequest] = Schema.derived

object annotations {
object name extends Annotation[String](description = "New name of the entity", example = "John Doe")
object name
extends Annotation[String](
description = "New name of the entity",
example = "John Doe",
validator = Validator.all(Validator.nonEmptyString, Validator.maxLength(128))
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ object WalletManagementEndpoints {
baseEndpoint.get
.in(paginationInput)
.errorOut(EndpointOutputs.basicFailuresAndForbidden)
.out(statusCode(StatusCode.Ok).description("List Prism Agent managed DIDs"))
.out(statusCode(StatusCode.Ok).description("Successfully list all the wallets"))
.out(jsonBody[WalletDetailPage])
.summary("List all wallets")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,17 @@ trait WalletManagementController {
object WalletManagementController {
given walletServiceErrorConversion: Conversion[WalletManagementServiceError, ErrorResponse] = {
case WalletManagementServiceError.SeedGenerationError(cause) =>
ErrorResponse.internalServerError(detail = Some(cause.toString()))
case WalletManagementServiceError.WalletStorageError(cause) =>
ErrorResponse.internalServerError(detail = Some(cause.toString()))
ErrorResponse.internalServerError(detail = Some(cause.getMessage()))
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) =>
// 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.")
}
}

Expand Down Expand Up @@ -88,7 +94,11 @@ class WalletManagementControllerImpl(
.map(_.toByteArray)
.map(WalletSeed.fromByteArray)
.absolve
.mapError(e => ErrorResponse.badRequest(detail = Some(e)))
.mapError(e =>
ErrorResponse.badRequest(detail =
Some(s"The provided wallet seed is not valid hex string representing a BIP-32 seed. ($e)")
)
)
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ 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}
import sttp.tapir.Schema.annotations.{description, encodedExample, validate}
import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder}

import java.util.UUID

final case class CreateWalletRequest(
Expand All @@ -12,6 +13,7 @@ final case class CreateWalletRequest(
seed: Option[String],
@description(CreateWalletRequest.annotations.name.description)
@encodedExample(CreateWalletRequest.annotations.name.example)
@validate(CreateWalletRequest.annotations.name.validator)
name: String,
@description(CreateWalletRequest.annotations.id.description)
@encodedExample(CreateWalletRequest.annotations.id.example)
Expand All @@ -35,7 +37,8 @@ object CreateWalletRequest {
object name
extends Annotation[String](
description = "A name of the wallet",
example = "my-wallet-1"
example = "my-wallet-1",
validator = Validator.all(Validator.nonEmptyString, Validator.maxLength(128))
)

object id
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
ALTER TABLE public.wallet
ADD COLUMN "seed_digest" BYTEA;

-- Fill the seed digest with a dummy value just to make it unique
UPDATE public.wallet
SET "seed_digest" = decode(replace("wallet_id"::text, '-', ''), 'hex')
WHERE "seed_digest" IS NULL;

ALTER TABLE public.wallet
ALTER COLUMN "seed_digest" SET NOT NULL;

ALTER TABLE public.wallet
ADD CONSTRAINT wallet_seed_digest UNIQUE ("seed_digest");
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.iohk.atala.agent.walletapi.crypto.DerivationPath
import io.iohk.atala.agent.walletapi.crypto.ECKeyPair
import io.iohk.atala.castor.core.model.did.InternalKeyPurpose
import io.iohk.atala.castor.core.model.did.VerificationRelationship
import io.iohk.atala.prism.crypto.Sha256
import scala.collection.immutable.ArraySeq
import scala.language.implicitConversions

Expand All @@ -13,6 +14,7 @@ object WalletSeed {
extension (s: WalletSeed) {
final def toString(): String = "<REDACTED>"
def toByteArray: Array[Byte] = s.toArray
def sha256Digest: Array[Byte] = Sha256.compute(toByteArray).getValue()
}

def fromByteArray(bytes: Array[Byte]): Either[String, WalletSeed] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,42 @@ package io.iohk.atala.agent.walletapi.service

import io.iohk.atala.agent.walletapi.model.Wallet
import io.iohk.atala.agent.walletapi.model.WalletSeed
import io.iohk.atala.shared.models.WalletId
import zio.*
import io.iohk.atala.agent.walletapi.storage.WalletNonSecretStorageError
import io.iohk.atala.event.notification.EventNotificationConfig
import io.iohk.atala.shared.models.WalletAccessContext
import io.iohk.atala.shared.models.WalletId
import zio.*

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

sealed trait WalletManagementServiceError {
final def toThrowable: Throwable = this
}

sealed trait WalletManagementServiceError extends Throwable
object WalletManagementServiceError {
final case class SeedGenerationError(cause: Throwable) extends WalletManagementServiceError
final case class WalletStorageError(cause: Throwable) extends 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

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 SeedGenerationError(cause) => Exception("Unable to generate wallet seed.", cause)
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")
}

}

trait WalletManagementService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ package io.iohk.atala.agent.walletapi.service
import io.iohk.atala.agent.walletapi.crypto.Apollo
import io.iohk.atala.agent.walletapi.model.Wallet
import io.iohk.atala.agent.walletapi.model.WalletSeed
import io.iohk.atala.agent.walletapi.service.WalletManagementServiceError.TooManyWebhookError
import io.iohk.atala.agent.walletapi.storage.WalletNonSecretStorage
import io.iohk.atala.agent.walletapi.storage.WalletNonSecretStorageCustomError
import io.iohk.atala.agent.walletapi.storage.WalletNonSecretStorageCustomError.TooManyWebhook
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.WalletId
import zio.*

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

class WalletManagementServiceImpl(
apollo: Apollo,
Expand All @@ -31,46 +29,43 @@ class WalletManagementServiceImpl(
.mapError(WalletManagementServiceError.SeedGenerationError.apply)
)(ZIO.succeed)
createdWallet <- nonSecretStorage
.createWallet(wallet)
.mapError(WalletManagementServiceError.WalletStorageError.apply)
.createWallet(wallet, seed.sha256Digest)
.mapError[WalletManagementServiceError](e => e)
_ <- secretStorage
.setWalletSeed(seed)
.mapError(WalletManagementServiceError.WalletStorageError.apply)
.mapError(WalletManagementServiceError.UnexpectedStorageError.apply)
.provide(ZLayer.succeed(WalletAccessContext(wallet.id)))
} yield createdWallet

override def getWallet(walletId: WalletId): IO[WalletManagementServiceError, Option[Wallet]] =
nonSecretStorage
.getWallet(walletId)
.mapError(WalletManagementServiceError.WalletStorageError.apply)
.mapError(e => e)

override def listWallets(
offset: Option[Int],
limit: Option[Int]
): IO[WalletManagementServiceError, (Seq[Wallet], Int)] =
nonSecretStorage
.listWallet(offset = offset, limit = limit)
.mapError(WalletManagementServiceError.WalletStorageError.apply)
.mapError(e => e)

override def listWalletNotifications
: ZIO[WalletAccessContext, WalletManagementServiceError, Seq[EventNotificationConfig]] =
nonSecretStorage.walletNotification
.mapError(WalletManagementServiceError.WalletStorageError.apply)
.mapError(e => e)

override def createWalletNotification(
config: EventNotificationConfig
): ZIO[WalletAccessContext, WalletManagementServiceError, EventNotificationConfig] =
nonSecretStorage
.createWalletNotification(config)
.mapError {
case TooManyWebhook(limit, actual) => WalletManagementServiceError.TooManyWebhookError(limit, actual)
case e => WalletManagementServiceError.WalletStorageError(e)
}
.mapError(e => e)

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

}

Expand Down
Loading

0 comments on commit 1f9ede3

Please sign in to comment.