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

hyperledger / identus-cloud-agent / 11407164708

18 Oct 2024 04:02PM CUT coverage: 48.619% (-0.1%) from 48.741%
11407164708

Pull #1400

FabioPinheiro
build: add job Update Dependency Graph

Signed-off-by: FabioPinheiro <fabiomgpinheiro@gmail.com>
Pull Request #1400: build: add job Update Dependency Graph

7867 of 16181 relevant lines covered (48.62%)

0.49 hits per line

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

83.33
/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, UriOrJsonEndpoint}
28
import org.hyperledger.identus.shared.models.{Base64UrlString, KeyId}
29
import org.hyperledger.identus.shared.utils.Traverse.*
30
import zio.*
31

32
import java.time.Instant
33
import scala.language.implicitConversions
34

35
object ProtoModelHelper extends ProtoModelHelper
36

37
private[castor] trait ProtoModelHelper {
38

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

408
        parsedObject.orElse(parsedArray)
1✔
409
      }
410

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

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