• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

input-output-hk / atala-prism-building-blocks / 8795979133

23 Apr 2024 06:40AM UTC coverage: 31.837% (+0.3%) from 31.528%
8795979133

push

web-flow
fix: Check purpose of the keys (#968)

Signed-off-by: Bassam Riman <bassam.riman@iohk.io>
Signed-off-by: mineme0110 <shailesh.patil@iohk.io>

2 of 15 new or added lines in 1 file covered. (13.33%)

220 existing lines in 69 files now uncovered.

4512 of 14172 relevant lines covered (31.84%)

0.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

17.54
/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/IssueControllerImpl.scala
1
package io.iohk.atala.issue.controller
2

3
import io.iohk.atala.agent.server.ControllerHelper
4
import io.iohk.atala.agent.server.config.AppConfig
5
import io.iohk.atala.agent.walletapi.model.PublicationState
6
import io.iohk.atala.agent.walletapi.model.PublicationState.{Created, PublicationPending, Published}
7
import io.iohk.atala.agent.walletapi.model.error.GetManagedDIDError
8
import io.iohk.atala.agent.walletapi.service.ManagedDIDService
9
import io.iohk.atala.api.http.model.{CollectionStats, PaginationInput}
10
import io.iohk.atala.api.http.{ErrorResponse, RequestContext}
11
import io.iohk.atala.api.util.PaginationUtils
12
import io.iohk.atala.castor.core.model.did.{DIDData, DIDMetadata, PrismDID, VerificationRelationship}
13
import io.iohk.atala.castor.core.model.error.DIDResolutionError
14
import io.iohk.atala.castor.core.service.DIDService
15
import io.iohk.atala.connect.core.model.error.ConnectionServiceError
16
import io.iohk.atala.connect.core.service.ConnectionService
17
import io.iohk.atala.issue.controller.IssueController.toHttpError
18
import io.iohk.atala.issue.controller.http.{
19
  AcceptCredentialOfferRequest,
20
  CreateIssueCredentialRecordRequest,
21
  IssueCredentialRecord,
22
  IssueCredentialRecordPage
23
}
24
import io.iohk.atala.pollux.core.model.CredentialFormat.{AnonCreds, JWT}
25
import io.iohk.atala.pollux.core.model.error.CredentialServiceError
26
import io.iohk.atala.pollux.core.model.{CredentialFormat, DidCommID}
27
import io.iohk.atala.pollux.core.service.CredentialService
28
import io.iohk.atala.pollux.core.model.IssueCredentialRecord.Role
29
import io.iohk.atala.shared.models.WalletAccessContext
30
import zio.{URLayer, ZIO, ZLayer}
31

32
class IssueControllerImpl(
33
    credentialService: CredentialService,
34
    connectionService: ConnectionService,
35
    didService: DIDService,
36
    managedDIDService: ManagedDIDService,
37
    appConfig: AppConfig
38
) extends IssueController
39
    with ControllerHelper {
1✔
40
  override def createCredentialOffer(
41
      request: CreateIssueCredentialRecordRequest
42
  )(implicit rc: RequestContext): ZIO[WalletAccessContext, ErrorResponse, IssueCredentialRecord] = {
43
    val result: ZIO[
44
      WalletAccessContext,
45
      ConnectionServiceError | CredentialServiceError | ErrorResponse,
46
      IssueCredentialRecord
1✔
47
    ] = for {
×
48
      didIdPair <- getPairwiseDIDs(request.connectionId).provideSomeLayer(ZLayer.succeed(connectionService))
×
49
      jsonClaims <- ZIO // TODO Get read of Circe and use zio-json all the way down
×
50
        .fromEither(io.circe.parser.parse(request.claims.toString()))
×
51
        .mapError(e => ErrorResponse.badRequest(detail = Some(e.getMessage)))
×
52
      credentialFormat = request.credentialFormat.map(CredentialFormat.valueOf).getOrElse(CredentialFormat.JWT)
1✔
53
      outcome <-
54
        credentialFormat match
×
55
          case JWT =>
×
56
            for {
×
57
              issuingDID <- ZIO
58
                .fromOption(request.issuingDID)
×
59
                .mapError(_ => ErrorResponse.badRequest(detail = Some("Missing request parameter: issuingDID")))
×
60
                .flatMap(extractPrismDIDFromString)
×
NEW
61
              _ <- validatePrismDID(issuingDID, allowUnpublished = true, Role.Issuer)
×
62
              record <- credentialService
×
63
                .createJWTIssueCredentialRecord(
64
                  pairwiseIssuerDID = didIdPair.myDID,
65
                  pairwiseHolderDID = didIdPair.theirDid,
×
66
                  thid = DidCommID(),
67
                  maybeSchemaId = request.schemaId,
68
                  claims = jsonClaims,
69
                  validityPeriod = request.validityPeriod,
×
70
                  automaticIssuance = request.automaticIssuance.orElse(Some(true)),
×
71
                  issuingDID = issuingDID.asCanonical
72
                )
73
            } yield record
×
74
          case AnonCreds =>
×
75
            for {
×
76
              credentialDefinitionGUID <- ZIO
77
                .fromOption(request.credentialDefinitionId)
78
                .mapError(_ =>
×
79
                  ErrorResponse.badRequest(detail = Some("Missing request parameter: credentialDefinitionId"))
80
                )
81
              credentialDefinitionId = {
×
82
                val publicEndpointUrl = appConfig.agent.httpEndpoint.publicEndpointUrl.toExternalForm
83
                val urlSuffix =
×
84
                  s"credential-definition-registry/definitions/${credentialDefinitionGUID.toString}/definition"
×
85
                val urlPrefix = if (publicEndpointUrl.endsWith("/")) publicEndpointUrl else publicEndpointUrl + "/"
×
86
                s"$urlPrefix$urlSuffix"
87
              }
×
88
              record <- credentialService
×
89
                .createAnonCredsIssueCredentialRecord(
90
                  pairwiseIssuerDID = didIdPair.myDID,
91
                  pairwiseHolderDID = didIdPair.theirDid,
×
92
                  thid = DidCommID(),
93
                  credentialDefinitionGUID = credentialDefinitionGUID,
94
                  credentialDefinitionId = credentialDefinitionId,
95
                  claims = jsonClaims,
96
                  validityPeriod = request.validityPeriod,
×
97
                  automaticIssuance = request.automaticIssuance.orElse(Some(true))
98
                )
99
            } yield record
1✔
100
    } yield IssueCredentialRecord.fromDomain(outcome)
1✔
101
    mapIssueErrors(result)
102
  }
103

×
104
  override def getCredentialRecords(paginationInput: PaginationInput, thid: Option[String])(implicit
105
      rc: RequestContext
106
  ): ZIO[WalletAccessContext, ErrorResponse, IssueCredentialRecordPage] = {
×
107
    val uri = rc.request.uri
×
108
    val pagination = paginationInput.toPagination
×
109
    val result = for {
×
110
      pageResult <- thid match
×
111
        case None =>
112
          credentialService
×
113
            .getIssueCredentialRecords(
114
              ignoreWithZeroRetries = false,
115
              offset = Some(pagination.offset),
116
              limit = Some(pagination.limit)
117
            )
×
118
        case Some(thid) =>
×
119
          credentialService
×
120
            .getIssueCredentialRecordByThreadId(DidCommID(thid), ignoreWithZeroRetries = false)
×
121
            .map(_.toSeq)
×
122
            .map(records => records -> records.length)
×
123
      (records, totalCount) = pageResult
×
124
      stats = CollectionStats(totalCount = totalCount, filteredCount = totalCount)
125
    } yield IssueCredentialRecordPage(
×
126
      self = uri.toString(),
127
      kind = "Collection",
×
128
      pageOf = PaginationUtils.composePageOfUri(uri).toString,
×
129
      next = PaginationUtils.composeNextUri(uri, records, pagination, stats).map(_.toString),
×
130
      previous = PaginationUtils.composePreviousUri(uri, records, pagination, stats).map(_.toString),
×
131
      contents = records map IssueCredentialRecord.fromDomain
132
    )
×
133
    mapIssueErrors(result)
134
  }
135

×
136
  override def getCredentialRecord(
137
      recordId: String
138
  )(implicit rc: RequestContext): ZIO[WalletAccessContext, ErrorResponse, IssueCredentialRecord] = {
×
139
    val result: ZIO[WalletAccessContext, CredentialServiceError | ErrorResponse, Option[IssueCredentialRecord]] = for {
×
140
      id <- extractDidCommIdFromString(recordId)
×
141
      outcome <- credentialService.getIssueCredentialRecord(id)
×
142
    } yield (outcome map IssueCredentialRecord.fromDomain)
×
143
    mapIssueErrors(result) someOrFail toHttpError(
×
144
      CredentialServiceError.RecordIdNotFound(DidCommID(recordId))
145
    ) // TODO - Tech Debt - Review if this is safe. Currently is because DidCommID is opaque type => string with no validation
146
  }
147

1✔
148
  override def acceptCredentialOffer(recordId: String, request: AcceptCredentialOfferRequest)(implicit
149
      rc: RequestContext
150
  ): ZIO[WalletAccessContext, ErrorResponse, IssueCredentialRecord] = {
1✔
151
    val result: ZIO[WalletAccessContext, CredentialServiceError | ErrorResponse, IssueCredentialRecord] = for {
1✔
152
      _ <- request.subjectId match
×
NEW
153
        case Some(did) => extractPrismDIDFromString(did).flatMap(validatePrismDID(_, true, Role.Holder))
×
154
        case None      => ZIO.succeed(())
×
155
      id <- extractDidCommIdFromString(recordId)
1✔
156
      outcome <- credentialService.acceptCredentialOffer(id, request.subjectId)
1✔
157
    } yield IssueCredentialRecord.fromDomain(outcome)
1✔
158
    mapIssueErrors(result)
159
  }
160

×
161
  override def issueCredential(
162
      recordId: String
163
  )(implicit rc: RequestContext): ZIO[WalletAccessContext, ErrorResponse, IssueCredentialRecord] = {
×
164
    val result: ZIO[WalletAccessContext, ErrorResponse | CredentialServiceError, IssueCredentialRecord] = for {
×
165
      id <- extractDidCommIdFromString(recordId)
×
166
      outcome <- credentialService.acceptCredentialRequest(id)
×
167
    } yield IssueCredentialRecord.fromDomain(outcome)
×
168
    mapIssueErrors(result)
169
  }
170

1✔
171
  private def validatePrismDID(
172
      prismDID: PrismDID,
173
      allowUnpublished: Boolean,
174
      role: Role
175
  ): ZIO[WalletAccessContext, ErrorResponse, Unit] = {
1✔
176
    val result = for {
×
177
      maybeDIDState <- managedDIDService.getManagedDIDState(prismDID.asCanonical)
×
NEW
178
      mayBeResolveDID <- didService
×
NEW
179
        .resolveDID(prismDID)
×
NEW
180
      maybeDidData = mayBeResolveDID.map(_._2)
×
181
      maybeMetadata = mayBeResolveDID.map(_._1)
1✔
NEW
182
      _ <- ZIO.when(role == Role.Holder)(
×
NEW
183
        ZIO
×
NEW
184
          .fromOption(maybeDidData.flatMap(_.publicKeys.find(_.purpose == VerificationRelationship.Authentication)))
×
185
          .orElseFail(ErrorResponse.badRequest(detail = Some(s"Authentication key not found for the $prismDID")))
186
      )
1✔
NEW
187
      _ <- ZIO.when(role == Role.Issuer)(
×
NEW
188
        ZIO
×
NEW
189
          .fromOption(maybeDidData.flatMap(_.publicKeys.find(_.purpose == VerificationRelationship.AssertionMethod)))
×
190
          .orElseFail(ErrorResponse.badRequest(detail = Some(s"AssertionMethod key not found for the $prismDID")))
NEW
191
      )
×
NEW
192
      _ <- (maybeDIDState.map(_.publicationState), maybeMetadata.map(_.deactivated)) match {
×
UNCOV
193
        case (None, _) =>
×
194
          ZIO.fail(ErrorResponse.badRequest(detail = Some("The provided DID can't be found in the agent wallet")))
195

×
196
        case (Some(Created() | PublicationPending(_)), _) if allowUnpublished =>
×
197
          ZIO.succeed(())
198

×
199
        case (Some(Created() | PublicationPending(_)), _) =>
×
200
          ZIO.fail(ErrorResponse.badRequest(detail = Some("The provided DID is not published")))
201

×
202
        case (Some(Published(_)), None) =>
×
203
          ZIO.succeed(())
204

×
205
        case (Some(Published(_)), Some(true)) =>
×
206
          ZIO.fail(ErrorResponse.badRequest(detail = Some("The provided DID is published but deactivated")))
207

×
208
        case (Some(Published(_)), Some(false)) =>
×
209
          ZIO.succeed(())
210
      }
211
    } yield ()
212

1✔
213
    mapIssueErrors(result)
214
  }
215

1✔
216
  private def mapIssueErrors[R, T](
217
      result: ZIO[
218
        R,
219
        CredentialServiceError | ConnectionServiceError | GetManagedDIDError | DIDResolutionError | ErrorResponse,
220
        T
221
      ]
222
  ): ZIO[R, ErrorResponse, T] = {
1✔
223
    result mapError {
1✔
224
      case e: ErrorResponse                  => e
×
225
      case connError: ConnectionServiceError => connError.asInstanceOf[ErrorResponse] // use implicit conversion
×
226
      case credError: CredentialServiceError => toHttpError(credError)
×
227
      case resError: DIDResolutionError =>
×
228
        ErrorResponse.internalServerError(detail = Some(s"Unable to resolve PrismDID. ${resError.toString()}"))
×
229
      case getError: GetManagedDIDError =>
×
230
        ErrorResponse.internalServerError(detail = Some(s"Unable to get PrismDID from storage. ${getError.toString()}"))
231
    }
232
  }
233

234
}
235

236
object IssueControllerImpl {
237
  val layer
238
      : URLayer[CredentialService & ConnectionService & DIDService & ManagedDIDService & AppConfig, IssueController] =
1✔
239
    ZLayer.fromFunction(IssueControllerImpl(_, _, _, _, _))
240
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc