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

hyperledger / identus-cloud-agent / 10928456511

18 Sep 2024 06:45PM CUT coverage: 48.836% (-0.1%) from 48.942%
10928456511

Pull #1366

CryptoKnightIOG
ATL-7775: Default Backend API to Array Of Credential Schema

Signed-off-by: Bassam Riman <bassam.riman@iohk.io>
Pull Request #1366: feat: Default Backend API to Array Of Credential Schema

24 of 32 new or added lines in 6 files covered. (75.0%)

310 existing lines in 58 files now uncovered.

7570 of 15501 relevant lines covered (48.84%)

0.49 hits per line

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

83.91
/castor/src/main/scala/org/hyperledger/identus/castor/core/model/ProtoModelHelper.scala
1
package org.hyperledger.identus.castor.core.model
2

3
import com.google.protobuf.ByteString
4
import io.circe.Json
5
import io.iohk.atala.prism.protos.{common_models, node_api, node_models}
6
import io.iohk.atala.prism.protos.common_models.OperationStatus
7
import io.iohk.atala.prism.protos.node_models.KeyUsage
8
import io.iohk.atala.prism.protos.node_models.PublicKey.KeyData
9
import org.hyperledger.identus.castor.core.model.did.{
10
  DIDData,
11
  EllipticCurve,
12
  InternalKeyPurpose,
13
  InternalPublicKey,
14
  PrismDID,
15
  PrismDIDOperation,
16
  PublicKey,
17
  PublicKeyData,
18
  ScheduledDIDOperationDetail,
19
  ScheduledDIDOperationStatus,
20
  Service,
21
  ServiceEndpoint,
22
  ServiceType,
23
  SignedPrismDIDOperation,
24
  UpdateDIDAction,
25
  VerificationRelationship
26
}
27
import org.hyperledger.identus.castor.core.model.did.ServiceEndpoint.value
28
import org.hyperledger.identus.castor.core.model.did.ServiceEndpoint.UriOrJsonEndpoint
29
import org.hyperledger.identus.shared.models.Base64UrlString
30
import org.hyperledger.identus.shared.models.KeyId
31
import org.hyperledger.identus.shared.utils.Traverse.*
32
import zio.*
33

34
import java.time.Instant
35
import scala.language.implicitConversions
36

37
object ProtoModelHelper extends ProtoModelHelper
38

39
private[castor] trait ProtoModelHelper {
40

41
  extension (bytes: Array[Byte]) {
42
    def toProto: ByteString = ByteString.copyFrom(bytes)
1✔
43
  }
44

45
  extension (signedOperation: SignedPrismDIDOperation) {
46
    def toProto: node_models.SignedAtalaOperation =
1✔
47
      node_models.SignedAtalaOperation(
1✔
48
        signedWith = signedOperation.signedWithKey,
49
        signature = signedOperation.signature.toArray.toProto,
1✔
50
        operation = Some(signedOperation.operation.toAtalaOperation)
1✔
51
      )
52
  }
53

54
  extension (operation: PrismDIDOperation.Create) {
55
    def toProto: node_models.AtalaOperation.Operation.CreateDid = {
1✔
56
      node_models.AtalaOperation.Operation.CreateDid(
57
        value = node_models.CreateDIDOperation(
1✔
58
          didData = Some(
59
            node_models.CreateDIDOperation.DIDCreationData(
1✔
60
              publicKeys = operation.publicKeys.map {
1✔
61
                case pk: PublicKey         => pk.toProto
1✔
62
                case pk: InternalPublicKey => pk.toProto
1✔
63
              },
64
              services = operation.services.map(_.toProto),
1✔
65
              context = operation.context
66
            )
67
          )
68
        )
69
      )
70
    }
71
  }
72

73
  extension (operation: PrismDIDOperation.Deactivate) {
74
    def toProto: node_models.AtalaOperation.Operation.DeactivateDid = {
1✔
75
      node_models.AtalaOperation.Operation.DeactivateDid(
76
        value = node_models.DeactivateDIDOperation(
1✔
77
          previousOperationHash = operation.previousOperationHash.toArray.toProto,
1✔
78
          id = operation.did.suffix.toString
79
        )
80
      )
81
    }
82
  }
83

84
  extension (operation: PrismDIDOperation.Update) {
85
    def toProto: node_models.AtalaOperation.Operation.UpdateDid = {
1✔
86
      node_models.AtalaOperation.Operation.UpdateDid(
87
        value = node_models.UpdateDIDOperation(
1✔
88
          previousOperationHash = operation.previousOperationHash.toArray.toProto,
1✔
89
          id = operation.did.suffix.toString,
90
          actions = operation.actions.map(_.toProto)
1✔
91
        )
92
      )
93
    }
94
  }
95

96
  extension (action: UpdateDIDAction) {
97
    def toProto: node_models.UpdateDIDAction = {
1✔
98
      val a = action match {
99
        case UpdateDIDAction.AddKey(publicKey) =>
1✔
100
          node_models.UpdateDIDAction.Action.AddKey(node_models.AddKeyAction(Some(publicKey.toProto)))
1✔
101
        case UpdateDIDAction.AddInternalKey(publicKey) =>
×
102
          node_models.UpdateDIDAction.Action.AddKey(node_models.AddKeyAction(Some(publicKey.toProto)))
×
103
        case UpdateDIDAction.RemoveKey(id) =>
1✔
104
          node_models.UpdateDIDAction.Action.RemoveKey(node_models.RemoveKeyAction(id))
1✔
105
        case UpdateDIDAction.AddService(service) =>
×
106
          node_models.UpdateDIDAction.Action.AddService(node_models.AddServiceAction(Some(service.toProto)))
×
107
        case UpdateDIDAction.RemoveService(id) =>
×
108
          node_models.UpdateDIDAction.Action.RemoveService(node_models.RemoveServiceAction(id))
×
109
        case UpdateDIDAction.UpdateService(serviceId, serviceType, endpoint) =>
×
110
          node_models.UpdateDIDAction.Action.UpdateService(
111
            node_models.UpdateServiceAction(
×
112
              serviceId = serviceId,
113
              `type` = serviceType.fold("")(_.toProto),
×
114
              serviceEndpoints = endpoint.fold("")(_.toProto)
×
115
            )
116
          )
117
        case UpdateDIDAction.PatchContext(context) =>
×
118
          node_models.UpdateDIDAction.Action.PatchContext(node_models.PatchContextAction(context))
×
119
      }
120
      node_models.UpdateDIDAction(action = a)
1✔
121
    }
122
  }
123

124
  extension (publicKey: PublicKey) {
125
    def toProto: node_models.PublicKey = {
1✔
126
      node_models.PublicKey(
1✔
127
        id = publicKey.id.value,
1✔
128
        usage = publicKey.purpose match {
129
          case VerificationRelationship.Authentication       => node_models.KeyUsage.AUTHENTICATION_KEY
1✔
130
          case VerificationRelationship.AssertionMethod      => node_models.KeyUsage.ISSUING_KEY
1✔
131
          case VerificationRelationship.KeyAgreement         => node_models.KeyUsage.KEY_AGREEMENT_KEY
1✔
132
          case VerificationRelationship.CapabilityInvocation => node_models.KeyUsage.CAPABILITY_INVOCATION_KEY
1✔
133
          case VerificationRelationship.CapabilityDelegation => node_models.KeyUsage.CAPABILITY_DELEGATION_KEY
1✔
134
        },
135
        addedOn = None,
136
        revokedOn = None,
137
        keyData = publicKey.publicKeyData.toProto
1✔
138
      )
139
    }
140
  }
141

142
  extension (internalPublicKey: InternalPublicKey) {
143
    def toProto: node_models.PublicKey = {
1✔
144
      node_models.PublicKey(
1✔
145
        id = internalPublicKey.id.value,
1✔
146
        usage = internalPublicKey.purpose match {
147
          case InternalKeyPurpose.Master     => node_models.KeyUsage.MASTER_KEY
1✔
148
          case InternalKeyPurpose.Revocation => node_models.KeyUsage.REVOCATION_KEY
1✔
149
        },
150
        addedOn = None,
151
        revokedOn = None,
152
        keyData = internalPublicKey.publicKeyData.toProto
1✔
153
      )
154
    }
155
  }
156

157
  extension (publicKeyData: PublicKeyData) {
158
    def toProto: node_models.PublicKey.KeyData = {
1✔
159
      publicKeyData match {
160
        case PublicKeyData.ECKeyData(crv, x, y) =>
1✔
161
          node_models.PublicKey.KeyData.EcKeyData(
162
            value = node_models.ECKeyData(
1✔
163
              curve = crv.name,
164
              x = x.toByteArray.toProto,
1✔
165
              y = y.toByteArray.toProto
1✔
166
            )
167
          )
168
        case PublicKeyData.ECCompressedKeyData(crv, data) =>
1✔
169
          node_models.PublicKey.KeyData.CompressedEcKeyData(
170
            value = node_models.CompressedECKeyData(
1✔
171
              curve = crv.name,
172
              data = data.toByteArray.toProto
1✔
173
            )
174
          )
175
      }
176
    }
177
  }
178

179
  extension (service: Service) {
180
    def toProto: node_models.Service = {
1✔
181
      node_models.Service(
1✔
182
        id = service.id,
183
        `type` = service.`type`.toProto,
1✔
184
        serviceEndpoint = service.serviceEndpoint.toProto,
1✔
185
        addedOn = None,
186
        deletedOn = None
187
      )
188
    }
189
  }
190

191
  extension (serviceType: ServiceType) {
192
    def toProto: String = {
1✔
193
      serviceType match {
194
        case ServiceType.Single(name) => name.value
1✔
195
        case ts: ServiceType.Multiple =>
196
          val names = ts.values.map(_.value).map(Json.fromString)
1✔
197
          Json.arr(names*).noSpaces
1✔
198
      }
199
    }
200
  }
201

202
  extension (serviceEndpoint: ServiceEndpoint) {
203
    def toProto: String = {
1✔
204
      serviceEndpoint match {
205
        case ServiceEndpoint.Single(value) =>
1✔
206
          value match {
207
            case UriOrJsonEndpoint.Uri(uri)   => uri.value
1✔
208
            case UriOrJsonEndpoint.Json(json) => Json.fromJsonObject(json).noSpaces
1✔
209
          }
210
        case endpoints: ServiceEndpoint.Multiple =>
211
          val uris = endpoints.values.map {
1✔
212
            case UriOrJsonEndpoint.Uri(uri)   => Json.fromString(uri.value)
1✔
213
            case UriOrJsonEndpoint.Json(json) => Json.fromJsonObject(json)
1✔
214
          }
215
          Json.arr(uris*).noSpaces
1✔
216
      }
217
    }
218
  }
219

220
  extension (resp: node_api.GetOperationInfoResponse) {
221
    def toDomain: Either[String, Option[ScheduledDIDOperationDetail]] = {
×
222
      val status = resp.operationStatus match {
223
        case OperationStatus.UNKNOWN_OPERATION      => Right(None)
×
224
        case OperationStatus.PENDING_SUBMISSION     => Right(Some(ScheduledDIDOperationStatus.Pending))
×
225
        case OperationStatus.AWAIT_CONFIRMATION     => Right(Some(ScheduledDIDOperationStatus.AwaitingConfirmation))
×
226
        case OperationStatus.CONFIRMED_AND_APPLIED  => Right(Some(ScheduledDIDOperationStatus.Confirmed))
×
227
        case OperationStatus.CONFIRMED_AND_REJECTED => Right(Some(ScheduledDIDOperationStatus.Rejected))
×
228
        case OperationStatus.Unrecognized(unrecognizedValue) =>
×
229
          Left(s"unrecognized status of GetOperationInfoResponse: $unrecognizedValue")
×
230
      }
231
      status.map(s => s.map(ScheduledDIDOperationDetail.apply))
×
232
    }
233
  }
234

235
  extension (didData: node_models.DIDData) {
236
    def toDomain: Either[String, DIDData] = {
1✔
237
      for {
1✔
238
        canonicalDID <- PrismDID.buildCanonicalFromSuffix(didData.id)
1✔
239
        allKeys <- didData.publicKeys.traverse(_.toDomain)
1✔
240
        services <- didData.services.traverse(_.toDomain)
1✔
241
      } yield DIDData(
242
        id = canonicalDID,
243
        publicKeys = allKeys.collect { case key: PublicKey => key },
×
244
        internalKeys = allKeys.collect { case key: InternalPublicKey => key },
1✔
245
        services = services,
246
        context = didData.context
247
      )
248
    }
249

250
    /** Return DIDData with keys and services removed by checking revocation time against the current time */
251
    def filterRevokedKeysAndServices: UIO[node_models.DIDData] = {
1✔
252
      Clock.instant.map { now =>
1✔
253
        didData
254
          .withPublicKeys(didData.publicKeys.filter { publicKey =>
1✔
255
            publicKey.revokedOn.flatMap(_.toInstant).forall(revokeTime => revokeTime `isAfter` now)
1✔
256
          })
257
          .withServices(didData.services.filter { service =>
1✔
258
            service.deletedOn.flatMap(_.toInstant).forall(revokeTime => revokeTime `isAfter` now)
1✔
259
          })
260
      }
261
    }
262
  }
263

264
  extension (ledgerData: node_models.LedgerData) {
265
    def toInstant: Option[Instant] = ledgerData.timestampInfo
1✔
266
      .flatMap(_.blockTimestamp)
1✔
267
      .map(ts => Instant.ofEpochSecond(ts.seconds).plusNanos(ts.nanos))
1✔
268
  }
269

270
  extension (operation: node_models.CreateDIDOperation) {
271
    def toDomain: Either[String, PrismDIDOperation.Create] = {
1✔
272
      for {
1✔
273
        allKeys <- operation.didData.map(_.publicKeys.traverse(_.toDomain)).getOrElse(Right(Nil))
1✔
274
        services <- operation.didData.map(_.services.traverse(_.toDomain)).getOrElse(Right(Nil))
×
275
        context = operation.didData.map(_.context).getOrElse(Nil)
1✔
276
      } yield PrismDIDOperation.Create(
1✔
277
        publicKeys = allKeys,
278
        services = services,
279
        context = context
280
      )
281
    }
282
  }
283

284
  extension (service: node_models.Service) {
285
    def toDomain: Either[String, Service] = {
1✔
286
      for {
1✔
287
        serviceType <- parseServiceType(service.`type`)
1✔
288
        serviceEndpoint <- parseServiceEndpoint(service.serviceEndpoint)
1✔
289
      } yield Service(
290
        id = service.id,
291
        `type` = serviceType,
292
        serviceEndpoint = serviceEndpoint
293
      )
294
    }
295
  }
296

297
  extension (publicKey: node_models.PublicKey) {
298
    def toDomain: Either[String, PublicKey | InternalPublicKey] = {
1✔
299
      val purpose: Either[String, VerificationRelationship | InternalKeyPurpose] = publicKey.usage match {
300
        case node_models.KeyUsage.UNKNOWN_KEY => Left(s"unsupported use of KeyUsage.UNKNOWN_KEY on key ${publicKey.id}")
×
301
        case node_models.KeyUsage.MASTER_KEY  => Right(InternalKeyPurpose.Master)
1✔
302
        case node_models.KeyUsage.ISSUING_KEY => Right(VerificationRelationship.AssertionMethod)
1✔
303
        case node_models.KeyUsage.KEY_AGREEMENT_KEY         => Right(VerificationRelationship.KeyAgreement)
1✔
304
        case node_models.KeyUsage.AUTHENTICATION_KEY        => Right(VerificationRelationship.Authentication)
1✔
305
        case node_models.KeyUsage.CAPABILITY_INVOCATION_KEY => Right(VerificationRelationship.CapabilityInvocation)
1✔
306
        case node_models.KeyUsage.CAPABILITY_DELEGATION_KEY => Right(VerificationRelationship.CapabilityDelegation)
1✔
307
        case node_models.KeyUsage.REVOCATION_KEY            => Right(InternalKeyPurpose.Revocation)
1✔
308
        case node_models.KeyUsage.Unrecognized(unrecognizedValue) =>
×
309
          Left(s"unrecognized KeyUsage: $unrecognizedValue on key ${publicKey.id}")
×
310
      }
311

312
      for {
1✔
313
        purpose <- purpose
314
        keyData <- publicKey.keyData.toDomain
1✔
315
      } yield purpose match {
316
        case purpose: VerificationRelationship =>
1✔
317
          PublicKey(
318
            id = KeyId(publicKey.id),
1✔
319
            purpose = purpose,
320
            publicKeyData = keyData
321
          )
322
        case purpose: InternalKeyPurpose =>
1✔
323
          InternalPublicKey(
324
            id = KeyId(publicKey.id),
1✔
325
            purpose = purpose,
326
            publicKeyData = keyData
327
          )
328
      }
329
    }
330
  }
331

332
  extension (publicKeyData: node_models.PublicKey.KeyData) {
333
    def toDomain: Either[String, PublicKeyData] = {
1✔
334
      publicKeyData match {
335
        case KeyData.Empty => Left(s"unable to convert KeyData.Emtpy to PublicKeyData")
×
336
        case KeyData.EcKeyData(ecKeyData) =>
1✔
337
          for {
1✔
338
            curve <- EllipticCurve
339
              .parseString(ecKeyData.curve)
1✔
UNCOV
340
              .toRight(s"unsupported elliptic curve ${ecKeyData.curve}")
×
341
          } yield PublicKeyData.ECKeyData(
342
            crv = curve,
343
            x = Base64UrlString.fromByteArray(ecKeyData.x.toByteArray),
1✔
344
            y = Base64UrlString.fromByteArray(ecKeyData.y.toByteArray)
1✔
345
          )
346
        case KeyData.CompressedEcKeyData(ecKeyData) =>
1✔
347
          for {
1✔
348
            curve <- EllipticCurve
349
              .parseString(ecKeyData.curve)
1✔
350
              .toRight(s"unsupported elliptic curve ${ecKeyData.curve}")
1✔
351
          } yield PublicKeyData.ECCompressedKeyData(
352
            crv = curve,
353
            data = Base64UrlString.fromByteArray(ecKeyData.data.toByteArray)
1✔
354
          )
355
      }
356
    }
357
  }
358

359
  def parseServiceType(s: String): Either[String, ServiceType] = {
1✔
360
    // The type field MUST be a string or a non-empty JSON array of strings.
361
    val parsedJson: Option[Either[String, ServiceType.Multiple]] = io.circe.parser
362
      .parse(s)
1✔
363
      .toOption // it's OK to let parsing fail (e.g. LinkedDomains without quote is not a JSON string)
1✔
364
      .flatMap(_.asArray)
1✔
365
      .map { jsonArr =>
1✔
366
        jsonArr
1✔
367
          .traverse(_.asString.toRight("the service type is not a JSON array of strings"))
1✔
368
          .flatMap(_.traverse(ServiceType.Name.fromString))
1✔
369
          .map(_.toList)
1✔
370
          .flatMap {
1✔
371
            case head :: tail => Right(ServiceType.Multiple(head, tail))
1✔
372
            case Nil          => Left("the service type cannot be an empty JSON array")
1✔
373
          }
374
          .filterOrElse(
1✔
375
            _ => s == io.circe.Json.arr(jsonArr*).noSpaces,
1✔
376
            "the service type is a valid JSON array of strings, but not conform to the ABNF"
377
          )
378
      }
379

380
    parsedJson match {
381
      // serviceType is a valid JSON array of strings
382
      case Some(Right(parsed)) => Right(parsed)
1✔
383
      // serviceType is a valid JSON array but contains invalid items
384
      case Some(Left(error)) => Left(error)
1✔
385
      // serviceType is a string (raw string, not JSON quoted string)
386
      case None => ServiceType.Name.fromString(s).map(name => ServiceType.Single(name))
1✔
387
    }
388
  }
389

390
  def parseServiceEndpoint(s: String): Either[String, ServiceEndpoint] = {
1✔
391
    /* The service_endpoint field MUST contain one of:
392
     * 1. a URI
393
     * 2. a JSON object
394
     * 3. a non-empty JSON array of URIs and/or JSON objects
395
     */
396
    val parsedJson: Option[Either[String, ServiceEndpoint]] = io.circe.parser
397
      .parse(s)
1✔
398
      .toOption // it's OK to let parsing fail (e.g. http://example.com without quote is not a JSON string)
1✔
399
      .flatMap { json =>
1✔
400
        val parsedObject = json.asObject.map(obj => Right(ServiceEndpoint.Single(obj)))
1✔
401
        val parsedArray = json.asArray.map(_.traverse[String, UriOrJsonEndpoint] { js =>
1✔
402
          val obj = js.asObject.map(obj => Right(obj: UriOrJsonEndpoint))
1✔
403
          val str = js.asString.map(str => ServiceEndpoint.UriValue.fromString(str).map[UriOrJsonEndpoint](i => i))
1✔
404
          obj.orElse(str).getOrElse(Left("the service endpoint is not a JSON array of URIs and/or JSON objects"))
1✔
405
        }.map(_.toList).flatMap {
1✔
406
          case head :: tail => Right(ServiceEndpoint.Multiple(head, tail))
1✔
407
          case Nil          => Left("the service endpoint cannot be an empty JSON array")
1✔
408
        })
409

410
        parsedObject.orElse(parsedArray)
1✔
411
      }
412

413
    parsedJson match {
414
      // serviceEndpoint is a valid JSON object or array
415
      case Some(Right(parsed)) => Right(parsed)
1✔
416
      // serviceEndpoint is a valid JSON but contains invalid values
417
      case Some(Left(error)) => Left(error)
1✔
418
      // serviceEndpoint is a string (raw string, not JSON quoted string)
419
      case None => ServiceEndpoint.UriValue.fromString(s).map(ServiceEndpoint.Single(_))
1✔
420
    }
421
  }
422

423
}
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