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-6832 ZIO failures and defects in entity controller #1203

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,8 @@ object AgentInitialization {
_ <- walletService
.createWallet(defaultWallet, seed)
.orDieAsUnmanagedFailure
_ <- entityService.create(defaultEntity).mapError(e => Exception(e.message))
_ <- apiKeyAuth.add(defaultEntity.id, config.authApiKey).mapError(e => Exception(e.message))
_ <- entityService.create(defaultEntity).orDieAsUnmanagedFailure
_ <- apiKeyAuth.add(defaultEntity.id, config.authApiKey).mapError(e => Exception(e.userFacingMessage))
_ <- config.webhookUrl.fold(ZIO.unit) { url =>
val customHeaders = config.webhookApiKey.fold(Map.empty)(apiKey => Map("Authorization" -> s"Bearer $apiKey"))
walletService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,62 @@ package org.hyperledger.identus.iam.authentication

import org.hyperledger.identus.agent.walletapi.model.{BaseEntity, Entity, EntityRole}
import org.hyperledger.identus.api.http.ErrorResponse
import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletAdministrationContext, WalletId}
import org.hyperledger.identus.shared.models.*
import zio.{IO, ZIO, ZLayer}

trait Credentials

trait AuthenticationError {
def message: String
trait AuthenticationError(
val statusCode: StatusCode,
val userFacingMessage: String
) extends Failure {
override val namespace: String = "AuthenticationError"
}

object AuthenticationError {

case class InvalidCredentials(message: String) extends AuthenticationError
case class InvalidCredentials(message: String)
extends AuthenticationError(
StatusCode.Unauthorized,
message
)

case class AuthenticationMethodNotEnabled(message: String) extends AuthenticationError
case class AuthenticationMethodNotEnabled(message: String)
extends AuthenticationError(
StatusCode.Unauthorized,
message
)

case class UnexpectedError(message: String) extends AuthenticationError
case class UnexpectedError(message: String)
extends AuthenticationError(
StatusCode.InternalServerError,
message
)

case class ServiceError(message: String) extends AuthenticationError
case class ServiceError(message: String)
extends AuthenticationError(
StatusCode.InternalServerError,
message
)

case class ResourceNotPermitted(message: String) extends AuthenticationError
case class ResourceNotPermitted(message: String)
extends AuthenticationError(
StatusCode.Forbidden,
message
)

case class InvalidRole(message: String) extends AuthenticationError
case class InvalidRole(message: String)
extends AuthenticationError(
StatusCode.Forbidden,
message
)

def toErrorResponse(error: AuthenticationError): ErrorResponse =
ErrorResponse(
status = sttp.model.StatusCode.Forbidden.code,
`type` = "authentication_error",
title = "",
detail = Option(error.message)
detail = Option(error.userFacingMessage)
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package org.hyperledger.identus.iam.authentication.admin

import org.hyperledger.identus.iam.authentication.{AuthenticationError, Credentials}
import org.hyperledger.identus.shared.models.StatusCode

case class AdminApiKeyAuthenticationError(message: String) extends AuthenticationError
case class AdminApiKeyAuthenticationError(message: String)
extends AuthenticationError(
StatusCode.Unauthorized,
message
)

object AdminApiKeyAuthenticationError {
val invalidAdminApiKey = AdminApiKeyAuthenticationError("Invalid Admin API key in header `x-admin-api-key`")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,9 @@ case class ApiKeyAuthenticatorImpl(
.orDieAsUnmanagedFailure
.provide(ZLayer.succeed(WalletAdministrationContext.Admin()))
entityToCreate = Entity(name = "Auto provisioned entity", walletId = wallet.id.toUUID)
entity <- entityService
.create(entityToCreate)
.mapError(entityServiceError => AuthenticationRepositoryError.ServiceError(entityServiceError.message))
entity <- entityService.create(entityToCreate).orDieAsUnmanagedFailure
_ <- add(entity.id, apiKey)
.mapError(are => AuthenticationRepositoryError.ServiceError(are.message))
.mapError(are => AuthenticationRepositoryError.ServiceError(are.userFacingMessage))
} yield entity
}

Expand All @@ -76,9 +74,7 @@ case class ApiKeyAuthenticatorImpl(
.mapError(cause => AuthenticationRepositoryError.UnexpectedError(cause))
entityId <- repository
.getEntityIdByMethodAndSecret(AuthenticationMethodType.ApiKey, secret)
entity <- entityService
.getById(entityId)
.mapError(entityServiceError => AuthenticationRepositoryError.ServiceError(entityServiceError.message))
entity <- entityService.getById(entityId).orDieAsUnmanagedFailure
} yield entity
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package org.hyperledger.identus.iam.authentication

import org.hyperledger.identus.shared.models.StatusCode

package object apikey {
case class ApiKeyCredentials(apiKey: Option[String]) extends Credentials

case class ApiKeyAuthenticationError(message: String) extends AuthenticationError
case class ApiKeyAuthenticationError(message: String)
extends AuthenticationError(
StatusCode.Unauthorized,
message
)

object ApiKeyAuthenticationError {
val invalidApiKey = ApiKeyAuthenticationError("Invalid `apikey` header provided")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package org.hyperledger.identus.iam.authentication.oidc

import org.hyperledger.identus.iam.authentication.{AuthenticationError, Credentials}
import org.hyperledger.identus.shared.models.StatusCode

final case class JwtCredentials(token: Option[String]) extends Credentials

final case class JwtAuthenticationError(message: String) extends AuthenticationError
final case class JwtAuthenticationError(message: String)
extends AuthenticationError(
StatusCode.Unauthorized,
message
)

object JwtAuthenticationError {
val emptyToken = JwtAuthenticationError("Empty bearer token header provided")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class EntityPermissionManagementService(entityService: EntityService) extends Pe
_ <- ZIO
.serviceWith[WalletAdministrationContext](_.isAuthorized(walletId))
.filterOrFail(identity)(Error.WalletNotFoundById(walletId))
_ <- entityService.assignWallet(entity.id, walletId.toUUID).mapError[Error](e => e)
_ <- entityService.assignWallet(entity.id, walletId.toUUID).orDieAsUnmanagedFailure
} yield ()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
package org.hyperledger.identus.iam.authorization.core

import org.hyperledger.identus.agent.walletapi.model.error.EntityServiceError
import org.hyperledger.identus.agent.walletapi.model.error.EntityServiceError.{
EntityAlreadyExists,
EntityNotFound,
EntityStorageError,
EntityWalletNotFound
}
import org.hyperledger.identus.agent.walletapi.model.BaseEntity
import org.hyperledger.identus.shared.models.{WalletAdministrationContext, WalletId}
import zio.*
Expand Down Expand Up @@ -42,12 +36,5 @@ object PermissionManagement {
case class UnexpectedError(cause: Throwable) extends Error(cause.getMessage)

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))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,3 @@ trait EntityController {
def addApiKeyAuth(id: UUID, apiKey: String)(implicit rc: RequestContext): IO[ErrorResponse, Unit]
def deleteApiKeyAuth(id: UUID, apiKey: String)(implicit rc: RequestContext): IO[ErrorResponse, Unit]
}

object EntityController {
def domainToHttpError(error: EntityServiceError): ErrorResponse = {
error match {
case EntityServiceError.EntityStorageError(message: String) =>
ErrorResponse.internalServerError("RepositoryError", detail = Option(message))
case EntityServiceError.EntityNotFound(id, message) =>
ErrorResponse.notFound(detail = Option(message))
case EntityServiceError.EntityAlreadyExists(id, message) =>
ErrorResponse.badRequest(detail = Option(message))
case ewnf: EntityServiceError.EntityWalletNotFound =>
ErrorResponse.badRequest(detail = Option(ewnf.message))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package org.hyperledger.identus.iam.entity.http.controller

import org.hyperledger.identus.agent.walletapi.model.error.EntityServiceError
import org.hyperledger.identus.agent.walletapi.model.Entity
import org.hyperledger.identus.agent.walletapi.service.EntityService
import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext}
import org.hyperledger.identus.api.http.model.PaginationInput
import org.hyperledger.identus.iam.authentication.apikey.ApiKeyAuthenticator
import org.hyperledger.identus.iam.authentication.AuthenticationError
import org.hyperledger.identus.iam.entity.http.model.{CreateEntityRequest, EntityResponse, EntityResponsePage}
import zio.{IO, URLayer, ZLayer}
import zio.ZIO.succeed

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

case class EntityControllerImpl(service: EntityService, apiKeyAuthenticator: ApiKeyAuthenticator)
extends EntityController {
Expand All @@ -23,14 +22,14 @@ case class EntityControllerImpl(service: EntityService, apiKeyAuthenticator: Api
createdEntity <- service.create(entityToCreate)
self = rc.request.uri.addPath(createdEntity.id.toString).toString
} yield EntityResponse.fromDomain(createdEntity).withSelf(self)
} mapError (EntityController.domainToHttpError)
}

override def getEntity(id: UUID)(implicit rc: RequestContext): IO[ErrorResponse, EntityResponse] = {
for {
entity <- service.getById(id)
self = rc.request.uri.toString
} yield EntityResponse.fromDomain(entity).withSelf(self)
} mapError (EntityController.domainToHttpError)
}

// TODO: add the missing pagination fields to the response
override def getEntities(paginationIn: PaginationInput)(implicit
Expand All @@ -40,7 +39,7 @@ case class EntityControllerImpl(service: EntityService, apiKeyAuthenticator: Api
entities <- service.getAll(paginationIn.offset, paginationIn.limit)
self = rc.request.uri.toString
} yield EntityResponsePage.fromDomain(entities).withSelf(self)
} mapError (EntityController.domainToHttpError)
}

override def updateEntityName(id: UUID, name: String)(implicit
rc: RequestContext
Expand All @@ -50,7 +49,7 @@ case class EntityControllerImpl(service: EntityService, apiKeyAuthenticator: Api
updatedEntity <- service.getById(id)
self = rc.request.uri.toString
} yield EntityResponse.fromDomain(updatedEntity).withSelf(self)
} mapError (EntityController.domainToHttpError)
}

override def updateEntityWalletId(id: UUID, walletId: UUID)(implicit
rc: RequestContext
Expand All @@ -60,36 +59,24 @@ case class EntityControllerImpl(service: EntityService, apiKeyAuthenticator: Api
updatedEntity <- service.getById(id)
self = rc.request.uri.toString
} yield EntityResponse.fromDomain(updatedEntity).withSelf(self)
} mapError (EntityController.domainToHttpError)
}

override def deleteEntity(id: UUID)(implicit rc: RequestContext): IO[ErrorResponse, Unit] = {
for {
_ <- service.deleteById(id)
} yield ()
} mapError (EntityController.domainToHttpError)
}

override def addApiKeyAuth(id: UUID, apiKey: String)(implicit rc: RequestContext): IO[ErrorResponse, Unit] = {
service
.getById(id)
.flatMap(entity => apiKeyAuthenticator.add(entity.id, apiKey))
.mapError {
case ae: AuthenticationError =>
ErrorResponse.internalServerError("AuthenticationRepositoryError", detail = Option(ae.message))
case ese: EntityServiceError =>
EntityController.domainToHttpError(ese)
}
}

override def deleteApiKeyAuth(id: UUID, apiKey: String)(implicit rc: RequestContext): IO[ErrorResponse, Unit] = {
service
.getById(id)
.flatMap(entity => apiKeyAuthenticator.delete(entity.id, apiKey))
.mapError {
case ae: AuthenticationError =>
ErrorResponse.internalServerError("AuthenticationRepositoryError", detail = Option(ae.message))
case ese: EntityServiceError =>
EntityController.domainToHttpError(ese)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ object CredentialErrorResponse {
case _: InvalidCredentials => CredentialErrorCode.invalid_token
case _ => CredentialErrorCode.invalid_request
}
CredentialErrorResponse(error, Some(ae.message))
CredentialErrorResponse(error, Some(ae.userFacingMessage))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@ import org.hyperledger.identus.agent.walletapi.service.{
WalletManagementServiceImpl
}
import org.hyperledger.identus.agent.walletapi.sql.{JdbcWalletNonSecretStorage, JdbcWalletSecretStorage}
import org.hyperledger.identus.iam.authentication.oidc.{
KeycloakAuthenticator,
KeycloakAuthenticatorImpl,
KeycloakClient,
KeycloakClientImpl,
KeycloakEntity
}
import org.hyperledger.identus.iam.authentication.oidc.*
import org.hyperledger.identus.iam.authentication.AuthenticationError.ResourceNotPermitted
import org.hyperledger.identus.iam.authorization.core.PermissionManagement
import org.hyperledger.identus.iam.authorization.core.PermissionManagement.Error.{UnexpectedError, WalletNotFoundById}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
package org.hyperledger.identus.agent.walletapi.model.error

import org.hyperledger.identus.shared.models.{Failure, StatusCode}

import java.util.UUID

sealed trait EntityServiceError {
def message: String
sealed trait EntityServiceError(
val statusCode: StatusCode,
val userFacingMessage: String
) extends Failure {
override val namespace: String = "EntityServiceError"
}

object EntityServiceError {
final case class EntityNotFound(id: UUID, message: String) extends EntityServiceError
final case class EntityAlreadyExists(id: UUID, message: String) extends EntityServiceError
final case class EntityStorageError(message: String) extends EntityServiceError
final case class EntityWalletNotFound(entityId: UUID, walletId: UUID) extends EntityServiceError {
override def message: String = s"Wallet with id:$walletId not found for entity with id:$entityId"
}
final case class EntityNotFound(id: UUID)
extends EntityServiceError(
StatusCode.NotFound,
s"There is no entity matching the given identifier: id=$id"
)

final case class WalletNotFound(walletId: UUID)
extends EntityServiceError(
StatusCode.NotFound,
s"There is no wallet matching the given identifier: walletId:$walletId"
)
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
package org.hyperledger.identus.agent.walletapi.service

import org.hyperledger.identus.agent.walletapi.model.error.EntityServiceError
import org.hyperledger.identus.agent.walletapi.model.error.EntityServiceError.{EntityNotFound, WalletNotFound}
import org.hyperledger.identus.agent.walletapi.model.Entity
import zio.IO
import zio.{IO, UIO}

import java.util.UUID

trait EntityService {
def create(entity: Entity): IO[EntityServiceError, Entity]
def create(entity: Entity): IO[WalletNotFound, Entity]

def getById(entityId: UUID): IO[EntityServiceError, Entity]
def getById(entityId: UUID): IO[EntityNotFound, Entity]

def getAll(offset: Option[Int], limit: Option[Int]): IO[EntityServiceError, Seq[Entity]]
def getAll(offset: Option[Int], limit: Option[Int]): UIO[Seq[Entity]]

def deleteById(entityId: UUID): IO[EntityServiceError, Unit]
def deleteById(entityId: UUID): IO[EntityNotFound, Unit]

def updateName(entityId: UUID, name: String): IO[EntityServiceError, Unit]
def updateName(entityId: UUID, name: String): IO[EntityNotFound, Unit]

def assignWallet(entityId: UUID, walletId: UUID): IO[EntityServiceError, Unit]
def assignWallet(entityId: UUID, walletId: UUID): IO[EntityNotFound | WalletNotFound, Unit]
}
Loading
Loading