Skip to content

Commit

Permalink
feat(pollux): Add background job to update revocation lists with revo…
Browse files Browse the repository at this point in the history
…ked credentials (#906)

Signed-off-by: Shota Jolbordi <[email protected]>
  • Loading branch information
shotexa committed Mar 18, 2024
1 parent 04759fd commit 7e4757f
Show file tree
Hide file tree
Showing 15 changed files with 331 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,38 @@ final case class CredentialStatusList(
lastUsedIndex: Int,
createdAt: Instant,
updatedAt: Option[Instant]
) {}
)

case class CredInStatusList(
id: UUID,
issueCredentialRecordId: DidCommID,
statusListIndex: Int,
isCanceled: Boolean,
)

case class CredentialStatusListWithCred(
credentialStatusListId: UUID,
issuer: CanonicalPrismDID,
issued: Instant,
purpose: StatusPurpose,
walletId: WalletId,
statusListCredential: String,
size: Int,
lastUsedIndex: Int,
credentialInStatusListId: UUID,
issueCredentialRecordId: DidCommID,
statusListIndex: Int,
isCanceled: Boolean,
)

case class CredentialStatusListWithCreds(
id: UUID,
walletId: WalletId,
issuer: CanonicalPrismDID,
issued: Instant,
purpose: StatusPurpose,
statusListCredential: String,
size: Int,
lastUsedIndex: Int,
credentials: Seq[CredInStatusList]
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ import io.iohk.atala.pollux.vc.jwt.W3cCredentialPayload

import java.util.UUID

sealed trait CredentialStatusListServiceError
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

}

object CredentialStatusListServiceError {
final case class RepositoryError(cause: Throwable) extends CredentialStatusListServiceError
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,10 @@ trait CredentialStatusListRepository {
issueCredentialRecordId: DidCommID
): RIO[WalletAccessContext, Boolean]

def getCredentialStatusListsWithCreds: Task[List[CredentialStatusListWithCreds]]

def updateStatusListCredential(
credentialStatusListId: UUID,
statusListCredential: String
): RIO[WalletAccessContext, Unit]
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ class CredentialStatusListRepositoryInMemory(
}(ZIO.succeed)
} yield walletRef

private def allStatusListsStorageRefs: Task[Ref[Map[UUID, CredentialStatusList]]] =
for {
refs <- walletToStatusListRefs.get
allRefs = refs.values.toList
allRefsMap <- ZIO
.collectAll(allRefs.map(_.get))
.map(_.foldLeft(Map.empty[UUID, CredentialStatusList]) { (acc, value) =>
acc ++ value
})
ref <- Ref.make(allRefsMap)
} yield ref

private def statusListToCredInStatusListStorageRefs(
statusListId: UUID
): Task[Ref[Map[UUID, CredentialInStatusList]]] =
Expand Down Expand Up @@ -171,6 +183,55 @@ class CredentialStatusListRepositoryInMemory(
} yield isUpdated
}

def getCredentialStatusListsWithCreds: Task[List[CredentialStatusListWithCreds]] = {
for {
statusListsRefs <- allStatusListsStorageRefs
statusLists <- statusListsRefs.get
statusListWithCredEffects = statusLists.map { (id, statusList) =>
val credsinStatusListEffect = statusListToCredInStatusListStorageRefs(id).flatMap(_.get.map(_.values.toList))
credsinStatusListEffect.map { credsInStatusList =>
CredentialStatusListWithCreds(
id = id,
walletId = statusList.walletId,
issuer = statusList.issuer,
issued = statusList.issued,
purpose = statusList.purpose,
statusListCredential = statusList.statusListCredential,
size = statusList.size,
lastUsedIndex = statusList.lastUsedIndex,
credentials = credsInStatusList.map { cred =>
CredInStatusList(
id = cred.id,
issueCredentialRecordId = cred.issueCredentialRecordId,
statusListIndex = cred.statusListIndex,
isCanceled = cred.isCanceled,
)
}
)
}

}.toList
res <- ZIO.collectAll(statusListWithCredEffects)
} yield res
}

def updateStatusListCredential(
credentialStatusListId: UUID,
statusListCredential: String
): RIO[WalletAccessContext, Unit] = {
for {
statusListsRefs <- walletToStatusListStorageRefs
_ <- statusListsRefs.update { statusLists =>
statusLists.updatedWith(credentialStatusListId) { maybeCredentialStatusList =>
maybeCredentialStatusList.map { credentialStatusList =>
credentialStatusList.copy(statusListCredential = statusListCredential, updatedAt = Some(Instant.now()))
}
}
}
} yield ()

}

}

object CredentialStatusListRepositoryInMemory {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package io.iohk.atala.pollux.core.service

import io.iohk.atala.pollux.core.model.CredentialStatusList
import io.iohk.atala.pollux.core.model.{CredentialStatusList, CredentialStatusListWithCreds, DidCommID}
import io.iohk.atala.pollux.core.model.error.CredentialStatusListServiceError
import zio.*
import io.iohk.atala.pollux.core.model.DidCommID
import io.iohk.atala.shared.models.WalletAccessContext

import java.util.UUID
Expand All @@ -12,4 +11,11 @@ trait CredentialStatusListService {
def findById(id: UUID): IO[CredentialStatusListServiceError, CredentialStatusList]

def revokeByIssueCredentialRecordId(id: DidCommID): ZIO[WalletAccessContext, CredentialStatusListServiceError, Unit]

def getCredentialsAndItsStatuses: IO[CredentialStatusListServiceError, Seq[CredentialStatusListWithCreds]]

def updateStatusListCredential(
id: UUID,
statusListCredential: String
): ZIO[WalletAccessContext, CredentialStatusListServiceError, Unit]
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package io.iohk.atala.pollux.core.service

import io.iohk.atala.pollux.core.model.CredentialStatusList
import io.iohk.atala.pollux.core.model.{CredentialStatusList, CredentialStatusListWithCreds, DidCommID}
import io.iohk.atala.pollux.core.repository.CredentialStatusListRepository
import zio.*
import io.iohk.atala.pollux.core.model.error.CredentialStatusListServiceError
import io.iohk.atala.pollux.core.model.error.CredentialStatusListServiceError.*
import io.iohk.atala.pollux.core.model.DidCommID
import io.iohk.atala.shared.models.WalletAccessContext

import java.util.UUID
Expand Down Expand Up @@ -33,6 +32,19 @@ class CredentialStatusListServiceImpl(

}

def getCredentialsAndItsStatuses: IO[CredentialStatusListServiceError, Seq[CredentialStatusListWithCreds]] = {
credentialStatusListRepository.getCredentialStatusListsWithCreds.mapError(RepositoryError.apply)
}

def updateStatusListCredential(
id: UUID,
statusListCredential: String
): ZIO[WalletAccessContext, CredentialStatusListServiceError, Unit] = {
credentialStatusListRepository
.updateStatusListCredential(id, statusListCredential)
.mapError(RepositoryError.apply)
}

}

object CredentialStatusListServiceImpl {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,81 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T

}

def getCredentialStatusListsWithCreds: Task[List[CredentialStatusListWithCreds]] = {

// Might need to add wallet Id in the select query, because I'm selecting all of them
val cxnIO =
sql"""
| SELECT
| csl.id as credential_status_list_id,
| csl.issuer,
| csl.issued,
| csl.purpose,
| csl.wallet_id,
| csl.status_list_credential,
| csl.size,
| csl.last_used_index,
| cisl.id as credential_in_status_list_id,
| cisl.issue_credential_record_id,
| cisl.status_list_index,
| cisl.is_canceled
| FROM public.credential_status_lists csl
| LEFT JOIN public.credentials_in_status_list cisl ON csl.id = cisl.credential_status_list_id
|""".stripMargin
.query[CredentialStatusListWithCred]
.to[List]

val credentialStatusListsWithCredZio = cxnIO
.transact(xb)

for {
credentialStatusListsWithCred <- credentialStatusListsWithCredZio
} yield {
credentialStatusListsWithCred
.groupBy(_.credentialStatusListId)
.map { case (id, items) =>
CredentialStatusListWithCreds(
id,
items.head.walletId,
items.head.issuer,
items.head.issued,
items.head.purpose,
items.head.statusListCredential,
items.head.size,
items.head.lastUsedIndex,
items.map { item =>
CredInStatusList(
item.credentialInStatusListId,
item.issueCredentialRecordId,
item.statusListIndex,
item.isCanceled
)
}
)
}
.toList
}
}

def updateStatusListCredential(
credentialStatusListId: UUID,
statusListCredential: String
): RIO[WalletAccessContext, Unit] = {

val updateQuery =
sql"""
| UPDATE public.credential_status_lists
| SET
| status_list_credential = $statusListCredential::JSON,
| updated_at = ${Instant.now()}
| WHERE
| id = $credentialStatusListId
|""".stripMargin.update.run

updateQuery.transactWallet(xa).unit

}

}

object JdbcCredentialStatusListRepository {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import java.util.Base64
import java.util.zip.{GZIPInputStream, GZIPOutputStream}

class BitString private (val bitSet: util.BitSet, val size: Int) {
def setRevoked(index: Int, value: Boolean): IO[IndexOutOfBounds, Unit] =
def setRevokedInPlace(index: Int, value: Boolean): IO[IndexOutOfBounds, Unit] =
if (index >= size) ZIO.fail(IndexOutOfBounds(s"bitIndex >= $size: $index"))
else ZIO.attempt(bitSet.set(index, value)).mapError(t => IndexOutOfBounds(t.getMessage))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package io.iohk.atala.pollux.vc.jwt.revocation
import io.circe.syntax.*
import io.circe.{Json, JsonObject}
import io.iohk.atala.pollux.vc.jwt.*
import io.iohk.atala.pollux.vc.jwt.revocation.VCStatusList2021.Purpose.Revocation
import io.iohk.atala.pollux.vc.jwt.revocation.VCStatusList2021Error.{DecodingError, EncodingError}
import zio.*

Expand All @@ -15,6 +14,22 @@ class VCStatusList2021 private (val vcPayload: W3cCredentialPayload, jwtIssuer:

def toJsonWithEmbeddedProof: Task[Json] = W3CCredential.toJsonWithEmbeddedProof(vcPayload, jwtIssuer)

def updateBitString(bitString: BitString): IO[VCStatusList2021Error, VCStatusList2021] = {
import CredentialPayload.Implicits.*

val res = for {
vcId <- ZIO.fromOption(vcPayload.maybeId).mapError(_ => DecodingError("VC id not found"))
slId <- ZIO
.fromEither(vcPayload.credentialSubject.hcursor.downField("id").as[String])
.mapError(x => DecodingError(x.message))
purpose <- ZIO
.fromEither(vcPayload.credentialSubject.hcursor.downField("statusPurpose").as[StatusPurpose])
.mapError(x => DecodingError(x.message))
} yield VCStatusList2021.build(vcId, slId, jwtIssuer, bitString, purpose)

res.flatten
}

def getBitString: IO[DecodingError, BitString] = {
for {
encodedBitString <- ZIO
Expand All @@ -29,24 +44,20 @@ class VCStatusList2021 private (val vcPayload: W3cCredentialPayload, jwtIssuer:

object VCStatusList2021 {

enum Purpose(val name: String):
case Revocation extends Purpose("Revocation")
case Suspension extends Purpose("Suspension")

def build(
vcId: String,
slId: String,
jwtIssuer: Issuer,
revocationData: BitString,
purpose: Purpose = Revocation
purpose: StatusPurpose = StatusPurpose.Revocation
): IO[EncodingError, VCStatusList2021] = {
for {
encodedBitString <- revocationData.encoded.mapError(e => EncodingError(e.message))
} yield {
val claims = JsonObject()
.add("id", slId.asJson)
.add("type", "StatusList2021".asJson)
.add("statusPurpose", purpose.name.asJson)
.add("statusPurpose", purpose.str.asJson)
.add("encodedList", encodedBitString.asJson)
val w3Credential = W3cCredentialPayload(
`@context` = Set(
Expand All @@ -69,6 +80,15 @@ object VCStatusList2021 {
}
}

def decodeFromJson(json: Json, issuer: Issuer): IO[DecodingError, VCStatusList2021] = {
import CredentialPayload.Implicits.*
for {
w3cCredentialPayload <- ZIO
.fromEither(io.circe.parser.decode[W3cCredentialPayload](json.noSpaces))
.mapError(t => DecodingError(t.getMessage))
} yield VCStatusList2021(w3cCredentialPayload, issuer)
}

def decode(encodedJwtVC: JWT, issuer: Issuer): IO[DecodingError, VCStatusList2021] = {
for {
jwtCredentialPayload <- ZIO
Expand Down
Loading

0 comments on commit 7e4757f

Please sign in to comment.