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 6829 - Integrate ZIO failures and defects ADR in credential status list #1175

Merged
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.hyperledger.identus.castor.controller.{DIDRegistrarServerEndpoints, D
import org.hyperledger.identus.castor.core.service.DIDService
import org.hyperledger.identus.connect.controller.ConnectionServerEndpoints
import org.hyperledger.identus.connect.core.service.ConnectionService
import org.hyperledger.identus.credential.status.controller.CredentialStatusServiceEndpoints
import org.hyperledger.identus.credentialstatus.controller.CredentialStatusServiceEndpoints
import org.hyperledger.identus.event.controller.EventServerEndpoints
import org.hyperledger.identus.event.notification.EventNotificationConfig
import org.hyperledger.identus.iam.authentication.apikey.ApiKeyAuthenticator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import org.hyperledger.identus.castor.core.util.DIDOperationValidator
import org.hyperledger.identus.connect.controller.ConnectionControllerImpl
import org.hyperledger.identus.connect.core.service.{ConnectionServiceImpl, ConnectionServiceNotifier}
import org.hyperledger.identus.connect.sql.repository.{JdbcConnectionRepository, Migrations as ConnectMigrations}
import org.hyperledger.identus.credential.status.controller.CredentialStatusControllerImpl
import org.hyperledger.identus.credentialstatus.controller.CredentialStatusControllerImpl
import org.hyperledger.identus.didcomm.controller.DIDCommControllerImpl
import org.hyperledger.identus.event.controller.EventControllerImpl
import org.hyperledger.identus.event.notification.EventNotificationServiceImpl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ object StatusListJobs extends BackgroundJobsHelper {
credentialStatusListService <- ZIO.service[CredentialStatusListService]
credentialService <- ZIO.service[CredentialService]
credentialStatusListsWithCreds <- credentialStatusListService.getCredentialsAndItsStatuses
.mapError(_.toThrowable) @@ Metric
.gauge("revocation_status_list_sync_get_status_lists_w_creds_ms_gauge")
.trackDurationWith(_.toMetricsSeconds)
@@ Metric
.gauge("revocation_status_list_sync_get_status_lists_w_creds_ms_gauge")
.trackDurationWith(_.toMetricsSeconds)

updatedVcStatusListsCredsEffects = credentialStatusListsWithCreds.map { statusListWithCreds =>
val vcStatusListCredString = statusListWithCreds.statusListCredential
Expand Down Expand Up @@ -93,9 +93,9 @@ object StatusListJobs extends BackgroundJobsHelper {
}
_ <- credentialStatusListService
.markAsProcessedMany(unprocessedEntityIds)
.mapError(_.toThrowable) @@ Metric
.gauge("revocation_status_list_sync_mark_as_processed_many_ms_gauge")
.trackDurationWith(_.toMetricsSeconds)
@@ Metric
.gauge("revocation_status_list_sync_mark_as_processed_many_ms_gauge")
.trackDurationWith(_.toMetricsSeconds)

updatedVcStatusListCred <- vcStatusListCred.updateBitString(bitString).mapError {
case VCStatusList2021Error.EncodingError(msg: String) => new Throwable(msg)
Expand All @@ -105,10 +105,16 @@ object StatusListJobs extends BackgroundJobsHelper {
.map(_.spaces2)
_ <- credentialStatusListService
.updateStatusListCredential(statusListWithCreds.id, vcStatusListCredJsonString)
.mapError(_.toThrowable)
} yield ()

effect.provideSomeLayer(ZLayer.succeed(walletAccessContext))
effect
.catchAll(e =>
ZIO.logErrorCause(s"Error processing status list record: ${statusListWithCreds.id} ", Cause.fail(e))
)
.catchAllDefect(d =>
ZIO.logErrorCause(s"Defect processing status list record: ${statusListWithCreds.id}", Cause.fail(d))
)
.provideSomeLayer(ZLayer.succeed(walletAccessContext))

}
config <- ZIO.service[AppConfig]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package org.hyperledger.identus.credential.status.controller
package org.hyperledger.identus.credentialstatus.controller

import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext}
import org.hyperledger.identus.credential.status.controller.http.StatusListCredential
import org.hyperledger.identus.pollux.core.model.error.CredentialStatusListServiceError
import org.hyperledger.identus.credentialstatus.controller.http.StatusListCredential
import org.hyperledger.identus.pollux.core.model.DidCommID
import org.hyperledger.identus.shared.models.WalletAccessContext
import zio.*
Expand All @@ -19,19 +18,3 @@ trait CredentialStatusController {
): ZIO[WalletAccessContext, ErrorResponse, Unit]

}

object CredentialStatusController {
def toHttpError(error: CredentialStatusListServiceError): ErrorResponse =
bvoiturier marked this conversation as resolved.
Show resolved Hide resolved
error match
case CredentialStatusListServiceError.RepositoryError(cause) =>
ErrorResponse.internalServerError(title = "RepositoryError", detail = Some(cause.toString))
case CredentialStatusListServiceError.JsonCredentialParsingError(cause) =>
ErrorResponse.internalServerError(title = "JsonCredentialParsingError", detail = Some(cause.toString))
case CredentialStatusListServiceError.RecordIdNotFound(recordId) =>
ErrorResponse.notFound(detail = Some(s"Credential status list could not be found by id: $recordId"))
case CredentialStatusListServiceError.IssueCredentialRecordNotFound(issueCredentialRecordId: DidCommID) =>
ErrorResponse.notFound(detail =
Some(s"Credential with id $issueCredentialRecordId is either already revoked or does not exist")
)

}
Original file line number Diff line number Diff line change
@@ -1,35 +1,30 @@
package org.hyperledger.identus.credential.status.controller
package org.hyperledger.identus.credentialstatus.controller

import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext}
import org.hyperledger.identus.credential.status.controller.http.StatusListCredential
import org.hyperledger.identus.credentialstatus.controller.http.StatusListCredential
import org.hyperledger.identus.pollux.core.model.DidCommID
import org.hyperledger.identus.pollux.core.service.CredentialStatusListService
import org.hyperledger.identus.shared.models.WalletAccessContext
import zio.*

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

class CredentialStatusControllerImpl(
credentialStatusListService: CredentialStatusListService
) extends CredentialStatusController {

def getStatusListCredentialById(id: UUID)(implicit
rc: RequestContext
): IO[ErrorResponse, StatusListCredential] = {

): IO[ErrorResponse, StatusListCredential] =
credentialStatusListService
.findById(id)
.getById(id)
.flatMap(StatusListCredential.fromCredentialStatusListEntry)
.mapError(CredentialStatusController.toHttpError)

}

def revokeCredentialById(id: DidCommID)(implicit
rc: RequestContext
): ZIO[WalletAccessContext, ErrorResponse, Unit] = {
credentialStatusListService
.revokeByIssueCredentialRecordId(id)
.mapError(CredentialStatusController.toHttpError)
}
def revokeCredentialById(
id: DidCommID
)(implicit rc: RequestContext): ZIO[WalletAccessContext, ErrorResponse, Unit] =
credentialStatusListService.revokeByIssueCredentialRecordId(id)

}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package org.hyperledger.identus.credential.status.controller
package org.hyperledger.identus.credentialstatus.controller

import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext}
import org.hyperledger.identus.api.http.codec.DidCommIDCodec.given
import org.hyperledger.identus.api.http.EndpointOutputs.*
import org.hyperledger.identus.credential.status.controller.http.StatusListCredential
import org.hyperledger.identus.credentialstatus.controller.http.StatusListCredential
import org.hyperledger.identus.iam.authentication.apikey.ApiKeyCredentials
import org.hyperledger.identus.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader
import org.hyperledger.identus.iam.authentication.oidc.JwtCredentials
Expand Down Expand Up @@ -52,7 +52,7 @@ object CredentialStatusEndpoints {
"credential-status" / "revoke-credential" / path[DidCommID]("id").description("Revoke a credential by its ID")
)
.out(statusCode(sttp.model.StatusCode.Ok))
.errorOut(basicFailuresAndNotFound)
.errorOut(basicFailuresWith(FailureVariant.unprocessableEntity, FailureVariant.notFound))
.summary("Revoke a credential by its ID")
.description("Marks credential to be ready for revocation, it will be revoked automatically")
.tag("Credential status list")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package org.hyperledger.identus.credential.status.controller
package org.hyperledger.identus.credentialstatus.controller

import org.hyperledger.identus.agent.walletapi.model.BaseEntity
import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext}
import org.hyperledger.identus.credential.status.controller.CredentialStatusEndpoints.*
import org.hyperledger.identus.api.http.RequestContext
import org.hyperledger.identus.credentialstatus.controller.CredentialStatusEndpoints.*
import org.hyperledger.identus.iam.authentication.{Authenticator, Authorizer, DefaultAuthenticator, SecurityLogic}
import org.hyperledger.identus.pollux.core.model.DidCommID
import org.hyperledger.identus.shared.models.WalletAccessContext
import sttp.model.StatusCode
import sttp.tapir.ztapir.*
import zio.*

Expand All @@ -18,34 +17,23 @@ class CredentialStatusServiceEndpoints(
authorizer: Authorizer[BaseEntity]
) {

private def obfuscateInternalServerError(e: ErrorResponse): ErrorResponse =
if e.status == StatusCode.InternalServerError.code then e.copy(detail = Some("Something went wrong"))
else e

private val getCredentialStatusListById: ZServerEndpoint[Any, Any] =
getCredentialStatusListEndpoint
.zServerLogic { case (ctx: RequestContext, id: UUID) =>
credentialStatusController
.getStatusListCredentialById(id)(ctx)
.logError
.mapError(obfuscateInternalServerError)

}

private val revokeCredentialById: ZServerEndpoint[Any, Any] = {
private val revokeCredentialById: ZServerEndpoint[Any, Any] =
revokeCredentialByIdEndpoint
.zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer))
.serverLogic { wac =>
{ case (ctx: RequestContext, id: DidCommID) =>
credentialStatusController
.revokeCredentialById(id)(ctx)
.logError
.mapError(obfuscateInternalServerError)
.provideSomeLayer(ZLayer.succeed(wac))
}

}
}

val all: List[ZServerEndpoint[Any, Any]] = List(
getCredentialStatusListById,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.hyperledger.identus.credential.status.controller.http
package org.hyperledger.identus.credentialstatus.controller.http

import org.hyperledger.identus.api.http.Annotation
import org.hyperledger.identus.credential.status.controller.http.StatusListCredential.annotations
import org.hyperledger.identus.credentialstatus.controller.http.StatusListCredential.annotations
import org.hyperledger.identus.pollux.core.model.error.CredentialStatusListServiceError
import org.hyperledger.identus.pollux.core.model.CredentialStatusList
import org.hyperledger.identus.pollux.vc.jwt.StatusPurpose
Expand Down Expand Up @@ -56,14 +56,9 @@ object StatusListCredential {

def fromCredentialStatusListEntry(
domain: CredentialStatusList
): IO[CredentialStatusListServiceError, StatusListCredential] = {

val res = ZIO
.fromEither(domain.statusListCredential.fromJson[StatusListCredential])
.mapError(err => CredentialStatusListServiceError.JsonCredentialParsingError(new Throwable(err)))

res
}
): UIO[StatusListCredential] = ZIO
.fromEither(domain.statusListCredential.fromJson[StatusListCredential])
.orDieWith(err => RuntimeException(s"An error occurred when parsing the status list credential: $err"))

object annotations {
object `@context`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import org.hyperledger.identus.agent.server.http.DocModels
import org.hyperledger.identus.agent.server.AgentHttpServer
import org.hyperledger.identus.castor.controller.{DIDController, DIDRegistrarController}
import org.hyperledger.identus.connect.controller.ConnectionController
import org.hyperledger.identus.credential.status.controller.CredentialStatusController
import org.hyperledger.identus.credentialstatus.controller.CredentialStatusController
import org.hyperledger.identus.event.controller.EventController
import org.hyperledger.identus.iam.authentication.DefaultAuthenticator
import org.hyperledger.identus.iam.entity.http.controller.EntityController
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
package org.hyperledger.identus.pollux.core.model.error

import org.hyperledger.identus.pollux.core.model.DidCommID
import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.Role
import org.hyperledger.identus.shared.models.{Failure, StatusCode}

import java.util.UUID

sealed trait CredentialStatusListServiceError {
def toThrowable: Throwable = this match
case CredentialStatusListServiceError.RepositoryError(cause) => cause
case CredentialStatusListServiceError.RecordIdNotFound(id) =>
new Exception(s"Credential status list with id: $id not found")
case CredentialStatusListServiceError.IssueCredentialRecordNotFound(id) =>
new Exception(s"Issue credential record with id: $id not found")
case CredentialStatusListServiceError.JsonCredentialParsingError(cause) => cause

sealed trait CredentialStatusListServiceError(
val statusCode: StatusCode,
val userFacingMessage: String
) extends Failure {
override val namespace: String = "CredentialStatusListError"
}

object CredentialStatusListServiceError {
final case class RepositoryError(cause: Throwable) extends CredentialStatusListServiceError
final case class RecordIdNotFound(id: UUID) extends CredentialStatusListServiceError
final case class IssueCredentialRecordNotFound(id: DidCommID) extends CredentialStatusListServiceError
final case class JsonCredentialParsingError(cause: Throwable) extends CredentialStatusListServiceError
final case class StatusListNotFound(id: UUID)
extends CredentialStatusListServiceError(
StatusCode.NotFound,
s"There is no credential status record matching the provided identifier: id=$id"
)

final case class StatusListNotFoundForIssueCredentialRecord(id: DidCommID)
extends CredentialStatusListServiceError(
StatusCode.NotFound,
s"There is no credential status record matching the provided issue credential record identifier: id=${id.value}"
)

final case class InvalidRoleForOperation(role: Role)
extends CredentialStatusListServiceError(
StatusCode.UnprocessableContent,
s"The role is invalid to complete the request operation: role=${role.toString}"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ import zio.*
import java.util.UUID

trait CredentialStatusListRepository {
def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]]
def getCredentialStatusListsWithCreds: UIO[List[CredentialStatusListWithCreds]]

def findById(
id: UUID
): UIO[Option[CredentialStatusList]]

def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]]

def existsForIssueCredentialRecordId(
id: DidCommID
): URIO[WalletAccessContext, Boolean]

def createNewForTheWallet(
jwtIssuer: Issuer,
statusListRegistryUrl: String
Expand All @@ -27,9 +33,7 @@ trait CredentialStatusListRepository {

def revokeByIssueCredentialRecordId(
issueCredentialRecordId: DidCommID
): URIO[WalletAccessContext, Boolean]

def getCredentialStatusListsWithCreds: UIO[List[CredentialStatusListWithCreds]]
): URIO[WalletAccessContext, Unit]

def updateStatusListCredential(
credentialStatusListId: UUID,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.hyperledger.identus.pollux.core.repository

import org.hyperledger.identus.castor.core.model.did.{CanonicalPrismDID, PrismDID}
import org.hyperledger.identus.pollux.core.model.{CredentialStatusList, *}
import org.hyperledger.identus.pollux.core.model.*
import org.hyperledger.identus.pollux.vc.jwt.{revocation, Issuer, StatusPurpose}
import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, VCStatusList2021}
import org.hyperledger.identus.pollux.vc.jwt.revocation.BitStringError.{
Expand Down Expand Up @@ -67,6 +67,12 @@ class CredentialStatusListRepositoryInMemory(
found = stores.flatMap(_.values).find(_.id == id)
} yield found

override def existsForIssueCredentialRecordId(id: DidCommID): UIO[Boolean] = for {
refs <- statusListToCredInStatusListRefs.get
stores <- ZIO.foreach(refs.values)(_.get)
exists = stores.flatMap(_.values).exists(_.issueCredentialRecordId == id)
} yield exists

def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] = for {
storageRef <- walletToStatusListStorageRefs
storage <- storageRef.get
Expand Down Expand Up @@ -159,8 +165,7 @@ class CredentialStatusListRepositoryInMemory(

def revokeByIssueCredentialRecordId(
issueCredentialRecordId: DidCommID
): URIO[WalletAccessContext, Boolean] = {
var isUpdated = false
): URIO[WalletAccessContext, Unit] = {
for {
statusListsRefs <- walletToStatusListStorageRefs
statusLists <- statusListsRefs.get
Expand All @@ -173,14 +178,13 @@ class CredentialStatusListRepositoryInMemory(
maybeFound.fold(credInStatusListsMap) { case (id, value) =>
if (!value.isCanceled) {
credInStatusListsMap.updated(id, value.copy(isCanceled = true, updatedAt = Some(Instant.now())))
isUpdated = true
credInStatusListsMap
} else credInStatusListsMap
}

}
)
} yield isUpdated
} yield ()
}

def getCredentialStatusListsWithCreds: UIO[List[CredentialStatusListWithCreds]] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ trait CredentialService {

def getById(
recordId: DidCommID
): URIO[WalletAccessContext, IssueCredentialRecord]
): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord]

def getIssueCredentialRecordByThreadId(
thid: DidCommID,
Expand Down
Loading
Loading