Skip to content

Commit

Permalink
fix(cloud-agent): check purpose of the keys
Browse files Browse the repository at this point in the history
Signed-off-by: mineme0110 <shailesh.patil@iohk.io>

rebase main fix mergeconflict

Signed-off-by: mineme0110 <shailesh.patil@iohk.io>
  • Loading branch information
mineme0110 committed Apr 22, 2024
1 parent cd30b8a commit 5a2cb77
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ object MockConnectionService extends Mock[ConnectionService] {
object AcceptConnectionRequest extends Effect[UUID, RecordIdNotFound | InvalidStateForOperation, ConnectionRecord]
object MarkConnectionResponseSent extends Effect[UUID, RecordIdNotFound | InvalidStateForOperation, ConnectionRecord]
object MarkConnectionInvitationExpired extends Effect[UUID, Nothing, ConnectionRecord]
object FindById extends Effect[UUID, Nothing, Option[ConnectionRecord]]

object ReceiveConnectionResponse
extends Effect[
Expand Down Expand Up @@ -112,7 +113,7 @@ object MockConnectionService extends Mock[ConnectionService] {

override def findRecordById(
recordId: UUID
): URIO[WalletAccessContext, Option[ConnectionRecord]] = ???
): URIO[WalletAccessContext, Option[ConnectionRecord]] = proxy(FindById, recordId)

override def findRecordByThreadId(
thid: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import io.iohk.atala.agent.walletapi.service.ManagedDIDService
import io.iohk.atala.api.http.model.{CollectionStats, PaginationInput}
import io.iohk.atala.api.http.{ErrorResponse, RequestContext}
import io.iohk.atala.api.util.PaginationUtils
import io.iohk.atala.castor.core.model.did.PrismDID
import io.iohk.atala.castor.core.model.did.{DIDData, DIDMetadata, PrismDID, VerificationRelationship}
import io.iohk.atala.castor.core.model.error.DIDResolutionError
import io.iohk.atala.castor.core.service.DIDService
import io.iohk.atala.connect.core.model.error.ConnectionServiceError
Expand All @@ -25,6 +25,7 @@ import io.iohk.atala.pollux.core.model.CredentialFormat.{AnonCreds, JWT}
import io.iohk.atala.pollux.core.model.error.CredentialServiceError
import io.iohk.atala.pollux.core.model.{CredentialFormat, DidCommID}
import io.iohk.atala.pollux.core.service.CredentialService
import io.iohk.atala.pollux.core.model.IssueCredentialRecord.Role
import io.iohk.atala.shared.models.WalletAccessContext
import zio.{URLayer, ZIO, ZLayer}

Expand Down Expand Up @@ -57,7 +58,7 @@ class IssueControllerImpl(
.fromOption(request.issuingDID)
.mapError(_ => ErrorResponse.badRequest(detail = Some("Missing request parameter: issuingDID")))
.flatMap(extractPrismDIDFromString)
_ <- validatePrismDID(issuingDID, allowUnpublished = true)
_ <- validatePrismDID(issuingDID, allowUnpublished = true, Role.Issuer)
record <- credentialService
.createJWTIssueCredentialRecord(
pairwiseIssuerDID = didIdPair.myDID,
Expand Down Expand Up @@ -149,7 +150,7 @@ class IssueControllerImpl(
): ZIO[WalletAccessContext, ErrorResponse, IssueCredentialRecord] = {
val result: ZIO[WalletAccessContext, CredentialServiceError | ErrorResponse, IssueCredentialRecord] = for {
_ <- request.subjectId match
case Some(did) => extractPrismDIDFromString(did).flatMap(validatePrismDID(_, true))
case Some(did) => extractPrismDIDFromString(did).flatMap(validatePrismDID(_, true, Role.Holder))
case None => ZIO.succeed(())
id <- extractDidCommIdFromString(recordId)
outcome <- credentialService.acceptCredentialOffer(id, request.subjectId)
Expand All @@ -169,12 +170,26 @@ class IssueControllerImpl(

private def validatePrismDID(
prismDID: PrismDID,
allowUnpublished: Boolean
allowUnpublished: Boolean,
role: Role
): ZIO[WalletAccessContext, ErrorResponse, Unit] = {
val result = for {
maybeDIDState <- managedDIDService.getManagedDIDState(prismDID.asCanonical)
maybeMetadata <- didService.resolveDID(prismDID).map(_.map(_._1))
result <- (maybeDIDState.map(_.publicationState), maybeMetadata.map(_.deactivated)) match {
mayBeResolveDID <- didService
.resolveDID(prismDID)
maybeDidData = mayBeResolveDID.map(_._2)
maybeMetadata = mayBeResolveDID.map(_._1)
_ <- ZIO.when(role == Role.Holder)(
ZIO
.fromOption(maybeDidData.flatMap(_.publicKeys.find(_.purpose == VerificationRelationship.Authentication)))
.orElseFail(ErrorResponse.badRequest(detail = Some(s"Authentication key not found for the $prismDID")))
)
_ <- ZIO.when(role == Role.Issuer)(
ZIO
.fromOption(maybeDidData.flatMap(_.publicKeys.find(_.purpose == VerificationRelationship.AssertionMethod)))
.orElseFail(ErrorResponse.badRequest(detail = Some(s"AssertionMethod key not found for the $prismDID")))
)
_ <- (maybeDIDState.map(_.publicationState), maybeMetadata.map(_.deactivated)) match {
case (None, _) =>
ZIO.fail(ErrorResponse.badRequest(detail = Some("The provided DID can't be found in the agent wallet")))

Expand All @@ -193,7 +208,7 @@ class IssueControllerImpl(
case (Some(Published(_)), Some(false)) =>
ZIO.succeed(())
}
} yield result
} yield ()

mapIssueErrors(result)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,171 @@
package io.iohk.atala.issue.controller

import io.iohk.atala.agent.walletapi.model.BaseEntity
import io.iohk.atala.agent.walletapi.model.{BaseEntity, ManagedDIDState, PublicationState}
import io.iohk.atala.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService}
import io.iohk.atala.api.http.ErrorResponse
import io.iohk.atala.castor.core.model.did.{DIDData, DIDMetadata, PrismDIDOperation, VerificationRelationship}
import io.iohk.atala.castor.core.service.MockDIDService
import io.iohk.atala.connect.core.model.ConnectionRecord
import io.iohk.atala.connect.core.model.ConnectionRecord.ProtocolState
import io.iohk.atala.connect.core.service
import io.iohk.atala.connect.core.service.MockConnectionService
import io.iohk.atala.container.util.MigrationAspects.migrate
import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ
import io.iohk.atala.issue.controller.http.AcceptCredentialOfferRequest
import io.iohk.atala.issue.controller.http.{AcceptCredentialOfferRequest, CreateIssueCredentialRecordRequest}
import io.iohk.atala.mercury.model.DidId
import io.iohk.atala.mercury.protocol.connection.ConnectionResponse
import io.iohk.atala.mercury.protocol.invitation.v2.Invitation
import io.iohk.atala.pollux.core.model.IssueCredentialRecord.{ProtocolState, Role}
import io.iohk.atala.pollux.core.model.{CredentialFormat, DidCommID, IssueCredentialRecord}
import io.iohk.atala.pollux.core.service.MockCredentialService
import sttp.client3.ziojson.*
import sttp.client3.{DeserializationException, UriContext, basicRequest}
import sttp.model.StatusCode
import zio.*
import zio.json.EncoderOps
import zio.mock.Expectation
import zio.test.*
import zio.test.Assertion.*

import java.time.Instant
import java.util.UUID

object IssueControllerImplSpec extends ZIOSpecDefault with IssueControllerTestTools {
val json: String =
"""{
| "emailAddress": "alice@wonderland.com",
| "givenName": "Alice",
| "familyName": "Wonderland",
| "dateOfIssuance": "2020-11-13T20:20:39+00:00",
| "drivingLicenseID": "12345",
| "drivingClass": "3"
| }""".stripMargin

val createIssueCredentialRecordRequest: CreateIssueCredentialRecordRequest = CreateIssueCredentialRecordRequest(
validityPeriod = Some(24.5),
schemaId = Some("mySchemaId"),
credentialDefinitionId = Some(UUID.fromString("123e4567-e89b-12d3-a456-426614174000")),
credentialFormat = Some("JWT"),
claims = json.toJsonAST.toOption.get,
automaticIssuance = Some(true),
issuingDID = Some(
"did:prism:332518729a7b7805f73a788e0944802527911901d9b7c16152281be9bc62d944:CosBCogBEkkKFW15LWtleS1hdXRoZW50aWNhdGlvbhAESi4KCXNlY3AyNTZrMRIhAuYoRIefsLhkvYwHz8gDtkG2b0kaZTDOLj_SExWX1fOXEjsKB21hc3RlcjAQAUouCglzZWNwMjU2azESIQLOzab8f0ibt1P0zdMfoWDQTSlPc8_tkV9Jk5BBsXB8fA"
),
connectionId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000")
)
private val issueCredentialRecord = IssueCredentialRecord(
DidCommID(),
Instant.now,
None,
DidCommID(),
None,
None,
None,
CredentialFormat.JWT,
IssueCredentialRecord.Role.Issuer,
None,
None,
None,
IssueCredentialRecord.ProtocolState.OfferPending,
None,
None,
None,
None,
None,
None,
5,
None,
None
)

val connectionResponse = ConnectionResponse(
id = "b7878bfc-16d5-49dd-a443-4e87a3c4c8c6",
from = DidId(
"did:peer:2.Ez6LSpwvTbwvMF5xtSZ6uNoZvWNcPGx1J2ziuais63CpB1UDe.Vz6MkmutH2XW9ybLtSyYRvYcyUbUPWneev6oVu9zfoEmFxQ2y.SeyJ0IjoiZG0iLCJzIjoiaHR0cDovL2hvc3QuZG9ja2VyLmludGVybmFsOjgwODAvZGlkY29tbSIsInIiOltdLCJhIjpbImRpZGNvbW0vdjIiXX0"
),
to = DidId(
"did:peer:2.Ez6LSr1TzNDH5S4GMtn1ELG6P6xBdLcFxQ8wBaZCn8bead7iK.Vz6MknkPqgbvK4c7GhsKzi2EyBV4rZbvtygJqxM4Eh8EF5DGB.SeyJyIjpbImRpZDpwZWVyOjIuRXo2TFNrV05SZ3k1d1pNTTJOQjg4aDRqakJwN0U4N0xLTXdkUGVCTFRjbUNabm5uby5WejZNa2pqQ3F5SkZUSHFpWGtZUndYcVhTZlo2WWtVMjFyMzdENkFCN1hLMkhZNXQyLlNleUpwWkNJNkltNWxkeTFwWkNJc0luUWlPaUprYlNJc0luTWlPaUpvZEhSd2N6b3ZMMjFsWkdsaGRHOXlMbkp2YjNSemFXUXVZMnh2ZFdRaUxDSmhJanBiSW1ScFpHTnZiVzB2ZGpJaVhYMCM2TFNrV05SZ3k1d1pNTTJOQjg4aDRqakJwN0U4N0xLTXdkUGVCTFRjbUNabm5ubyJdLCJzIjoiaHR0cHM6Ly9tZWRpYXRvci5yb290c2lkLmNsb3VkIiwiYSI6WyJkaWNvbW0vdjIiXSwidCI6ImRtIn0"
),
thid = None,
pthid = Some("52dc177a-05dc-4deb-ab57-ac9d5e3ff10c"),
body = ConnectionResponse.Body(
goal_code = Some("connect"),
goal = Some("Establish a trust connection between two peers"),
accept = Some(Seq.empty)
),
)
private val record = ConnectionRecord(
UUID.fromString("123e4567-e89b-12d3-a456-426614174000"),
Instant.now,
None,
UUID.randomUUID().toString,
None,
None,
None,
ConnectionRecord.Role.Inviter,
ConnectionRecord.ProtocolState.ConnectionResponseSent,
Invitation(from = DidId("did:peer:INVITER"), Invitation.Body(None, None, Nil)),
None,
Some(connectionResponse),
5,
None,
None
)
val acceptCredentialOfferRequest = AcceptCredentialOfferRequest(
Some(
"did:prism:332518729a7b7805f73a788e0944802527911901d9b7c16152281be9bc62d944:CosBCogBEkkKFW15LWtleS1hdXRoZW50aWNhdGlvbhAESi4KCXNlY3AyNTZrMRIhAuYoRIefsLhkvYwHz8gDtkG2b0kaZTDOLj_SExWX1fOXEjsKB21hc3RlcjAQAUouCglzZWNwMjU2azESIQLOzab8f0ibt1P0zdMfoWDQTSlPc8_tkV9Jk5BBsXB8fA"
)
)

private val mockConnectionServiceLayer =
MockConnectionService.FindById(
assertion = Assertion.anything,
result = Expectation.value(Some(record))
)

private def createDid(rel: VerificationRelationship): (DIDMetadata, DIDData) = {
val x = MockDIDService.createDID(rel)
(x._3, x._4)
}

private def mockDIDServiceExpectations(relationship: VerificationRelationship) = {
val (x: DIDMetadata, y: DIDData) = createDid(relationship)
MockDIDService.resolveDIDExpectation(x, y)
}
private val mockCredentialServiceExpectations =
MockCredentialService.CreateJWTIssueCredentialRecord(
assertion = Assertion.anything,
result = Expectation.value(issueCredentialRecord)
)
private val mockCredentialServiceExpectationsAcceptCredentialOffer = MockCredentialService.AcceptCredentialOffer(
assertion = Assertion.anything,
result =
Expectation.value(issueCredentialRecord.copy(protocolState = IssueCredentialRecord.ProtocolState.RequestPending))
)

private val mockManagedDIDServiceExpectations: Expectation[ManagedDIDService] = MockManagedDIDService
.GetManagedDIDState(
assertion = Assertion.anything,
result = Expectation.value(
Some(
ManagedDIDState(
PrismDIDOperation.Create(Nil, Nil, Nil),
0,
PublicationState.Published(scala.collection.immutable.ArraySeq.empty)
)
)
)
)
val baseLayer =
MockManagedDIDService.empty
>+> MockDIDService.empty
>+> MockCredentialService.empty
>+> MockConnectionService.empty

def spec = (httpErrorResponses @@ migrate(
schema = "public",
paths = "classpath:sql/pollux"
)).provideSomeLayerShared(MockDIDService.empty ++ MockManagedDIDService.empty >>> testEnvironmentLayer)
)).provideLayer(baseLayer >+> testEnvironmentLayer)

private val httpErrorResponses = suite("IssueControllerImp http failure cases")(
test("provide incorrect recordId to endpoint") {
Expand All @@ -43,7 +188,102 @@ object IssueControllerImplSpec extends ZIOSpecDefault with IssueControllerTestTo
)
)
} yield isItABadRequestStatusCode && theBodyWasParsedFromJsonAsABadRequest
}
)
},
test("createCredentialOffer for issuer PrismDid without AssertionMethod should return 400") {
for {
issueControllerService <- ZIO.service[IssueController]
authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]]
backend = httpBackend(issueControllerService, authenticator)
response: IssueCredentialBadRequestResponse <- basicRequest
.post(uri"${issueUriBase}/credential-offers")
.body(createIssueCredentialRecordRequest.toJsonPretty)
.response(asJsonAlways[ErrorResponse])
.send(backend)

isItABadRequestStatusCode = assert(response.code)(equalTo(StatusCode.BadRequest))
theBodyWasParsedFromJsonAsABadRequest = assert(response.body)(
isRight(
isSubtype[ErrorResponse](
hasField("status", _.status, equalTo(StatusCode.BadRequest.code))
)
)
)
} yield isItABadRequestStatusCode && theBodyWasParsedFromJsonAsABadRequest
}.provideLayer(
baseLayer
++ mockConnectionServiceLayer.toLayer
++ mockManagedDIDServiceExpectations.toLayer
++ mockDIDServiceExpectations(VerificationRelationship.Authentication).toLayer
>+> testEnvironmentLayer
),
test("createCredentialOffer for issuer PrismDid with AssertionMethod should return 201") {
for {
issueControllerService <- ZIO.service[IssueController]
authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]]
backend = httpBackend(issueControllerService, authenticator)
response: IssueCredentialBadRequestResponse <- basicRequest
.post(uri"${issueUriBase}/credential-offers")
.body(createIssueCredentialRecordRequest.toJsonPretty)
.response(asJsonAlways[ErrorResponse])
.send(backend)

isSuccessRequestStatusCode = assert(response.code)(equalTo(StatusCode.Created))

} yield isSuccessRequestStatusCode
}.provideLayer(
baseLayer
++ mockConnectionServiceLayer.toLayer
++ mockManagedDIDServiceExpectations.toLayer
++ mockDIDServiceExpectations(VerificationRelationship.AssertionMethod).toLayer
++ mockCredentialServiceExpectations.toLayer
>+> testEnvironmentLayer
),
test("AcceptCredentialOffer for Holder PrismDid with Authentication should return 200") {
for {
issueControllerService <- ZIO.service[IssueController]
authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]]
backend = httpBackend(issueControllerService, authenticator)
response: IssueCredentialBadRequestResponse <- basicRequest
.post(uri"${issueUriBase}/records/123e4567-e89b-12d3-a456-426614174000/accept-offer")
.body(acceptCredentialOfferRequest.toJsonPretty)
.response(asJsonAlways[ErrorResponse])
.send(backend)

isSuccessRequestStatusCode = assert(response.code)(equalTo(StatusCode.Ok))

} yield isSuccessRequestStatusCode
}.provideLayer(
baseLayer
++ mockManagedDIDServiceExpectations.toLayer
++ mockDIDServiceExpectations(VerificationRelationship.Authentication).toLayer
++ mockCredentialServiceExpectationsAcceptCredentialOffer.toLayer
>+> testEnvironmentLayer
),
test("AcceptCredentialOffer for Holder PrismDid without Authentication key should return 400") {
for {
issueControllerService <- ZIO.service[IssueController]
authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]]
backend = httpBackend(issueControllerService, authenticator)
response: IssueCredentialBadRequestResponse <- basicRequest
.post(uri"${issueUriBase}/records/123e4567-e89b-12d3-a456-426614174000/accept-offer")
.body(acceptCredentialOfferRequest.toJsonPretty)
.response(asJsonAlways[ErrorResponse])
.send(backend)

isItABadRequestStatusCode = assert(response.code)(equalTo(StatusCode.BadRequest))
theBodyWasParsedFromJsonAsABadRequest = assert(response.body)(
isRight(
isSubtype[ErrorResponse](
hasField("status", _.status, equalTo(StatusCode.BadRequest.code))
)
)
)
} yield isItABadRequestStatusCode && theBodyWasParsedFromJsonAsABadRequest
}.provideLayer(
baseLayer
++ mockManagedDIDServiceExpectations.toLayer
++ mockDIDServiceExpectations(VerificationRelationship.AssertionMethod).toLayer
>+> testEnvironmentLayer
),
)
}
Loading

0 comments on commit 5a2cb77

Please sign in to comment.