Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pollux): update storage for static resources to be wrapped in an envelope #1283

Merged
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.hyperledger.identus.issue.controller

import io.lemonlabs.uri.Url
import org.hyperledger.identus.agent.server.config.AppConfig
import org.hyperledger.identus.agent.server.ControllerHelper
import org.hyperledger.identus.agent.walletapi.model.PublicationState
Expand All @@ -9,7 +8,7 @@ import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService
import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext}
import org.hyperledger.identus.api.http.model.{CollectionStats, PaginationInput}
import org.hyperledger.identus.api.util.PaginationUtils
import org.hyperledger.identus.castor.core.model.did.{PrismDID, VerificationRelationship}
import org.hyperledger.identus.castor.core.model.did.{DIDUrl, PrismDID, VerificationRelationship}
import org.hyperledger.identus.castor.core.service.DIDService
import org.hyperledger.identus.connect.core.service.ConnectionService
import org.hyperledger.identus.issue.controller.http.{
Expand All @@ -18,17 +17,22 @@ import org.hyperledger.identus.issue.controller.http.{
IssueCredentialRecord,
IssueCredentialRecordPage
}
import org.hyperledger.identus.pollux.core.model.{CredentialFormat, DidCommID}
import org.hyperledger.identus.pollux.core.model.{CredentialFormat, DidCommID, ResourceResolutionMethod}
import org.hyperledger.identus.pollux.core.model.CredentialFormat.{AnonCreds, JWT, SDJWT}
import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.Role
import org.hyperledger.identus.pollux.core.service.CredentialService
import org.hyperledger.identus.pollux.core.service.{CredentialDefinitionService, CredentialService}
import org.hyperledger.identus.shared.crypto.Sha256Hash
import org.hyperledger.identus.shared.models.{KeyId, WalletAccessContext}
import zio.{URLayer, ZIO, ZLayer}
import org.hyperledger.identus.shared.utils.{Base64Utils, Json as JsonUtils}
import zio.*
import zio.json.given

import scala.collection.immutable.ListMap
import scala.language.implicitConversions

class IssueControllerImpl(
credentialService: CredentialService,
credentialDefinitionService: CredentialDefinitionService,
connectionService: ConnectionService,
didService: DIDService,
managedDIDService: ManagedDIDService,
Expand Down Expand Up @@ -92,16 +96,42 @@ class IssueControllerImpl(
.mapError(_ =>
ErrorResponse.badRequest(detail = Some("Missing request parameter: credentialDefinitionId"))
)
credentialDefinitionId = {
credentialDefinition <- credentialDefinitionService.getByGUID(credentialDefinitionGUID)
credentialDefinitionId <- {

val publicEndpointServiceName = appConfig.agent.httpEndpoint.serviceName
val resourcePath =
s"credential-definition-registry/definitions/${credentialDefinitionGUID.toString}/definition"
val didUrl = Url
.parse(s"$issuingDID?resourceService=$publicEndpointServiceName&resourcePath=$resourcePath")
.toString
credentialDefinition.resolutionMethod match
case ResourceResolutionMethod.DID =>
val publicEndpointServiceName = appConfig.agent.httpEndpoint.serviceName
val didUrlResourcePath =
s"credential-definition-registry/definitions/did-url/${credentialDefinitionGUID.toString}/definition"
val didUrl = for {
canonicalized <- JsonUtils.canonicalizeToJcs(credentialDefinition.definition.toJson)
encoded = Base64Utils.encodeURL(canonicalized.getBytes)
hash = Sha256Hash.compute(encoded.getBytes).hexEncoded
didUrl = DIDUrl(
issuingDID.did,
Seq(),
ListMap(
"resourceService" -> Seq(publicEndpointServiceName),
"resourcePath" -> Seq(
s"$didUrlResourcePath?resourceHash=$hash"
),
),
None
).toString
} yield didUrl

ZIO
.fromEither(didUrl)
.mapError(_ => ErrorResponse.badRequest(detail = Some("Could not parse credential definition")))

case ResourceResolutionMethod.HTTP =>
val publicEndpointUrl = appConfig.agent.httpEndpoint.publicEndpointUrl.toExternalForm
val httpUrlSuffix =
s"credential-definition-registry/definitions/${credentialDefinitionGUID.toString}/definition"
val urlPrefix = if (publicEndpointUrl.endsWith("/")) publicEndpointUrl else publicEndpointUrl + "/"
ZIO.succeed(s"$urlPrefix$httpUrlSuffix")

didUrl
}
record <- credentialService
.createAnonCredsIssueCredentialRecord(
Expand Down Expand Up @@ -244,7 +274,9 @@ class IssueControllerImpl(
}

object IssueControllerImpl {
val layer
: URLayer[CredentialService & ConnectionService & DIDService & ManagedDIDService & AppConfig, IssueController] =
ZLayer.fromFunction(IssueControllerImpl(_, _, _, _, _))
val layer: URLayer[
CredentialService & CredentialDefinitionService & ConnectionService & DIDService & ManagedDIDService & AppConfig,
IssueController
] =
ZLayer.fromFunction(IssueControllerImpl(_, _, _, _, _, _))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.hyperledger.identus.pollux

import org.hyperledger.identus.api.http.*
import org.hyperledger.identus.pollux.PrismEnvelopeResponse.annotations
import org.hyperledger.identus.shared.models.PrismEnvelope
import sttp.tapir.Schema
import sttp.tapir.Schema.annotations.{default, description, encodedExample, encodedName}
import zio.json.*

case class PrismEnvelopeResponse(
@description(annotations.resource.description)
@encodedExample(annotations.resource.example)
resource: String,
@description(annotations.resource.description)
@encodedExample(annotations.url.example)
url: String
) extends PrismEnvelope

object PrismEnvelopeResponse {
given encoder: JsonEncoder[PrismEnvelopeResponse] =
DeriveJsonEncoder.gen[PrismEnvelopeResponse]

given decoder: JsonDecoder[PrismEnvelopeResponse] =
DeriveJsonDecoder.gen[PrismEnvelopeResponse]

given schema: Schema[PrismEnvelopeResponse] = Schema.derived

object annotations {
object resource
extends Annotation[String](
description = "JCS normalized and base64url encoded json of the resource",
example = "" // TODO Add example
)

object url
extends Annotation[String](
description = "DID url that can be used to resolve this resource",
example =
"did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a?resourceService=agent-base-url&resourcePath=credential-definition-registry/definitions/did-url/ef3e4135-8fcf-3ce7-b5bb-df37defc13f6?resourceHash=4074bb1a8e0ea45437ad86763cd7e12de3fe8349ef19113df773b0d65c8a9c46"
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import org.hyperledger.identus.iam.authentication.apikey.ApiKeyEndpointSecurityL
import org.hyperledger.identus.iam.authentication.oidc.JwtCredentials
import org.hyperledger.identus.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader
import org.hyperledger.identus.pollux.credentialdefinition.http.{
CredentialDefinitionDidUrlResponse,
CredentialDefinitionDidUrlResponsePage,
CredentialDefinitionInnerDefinitionDidUrlResponse,
CredentialDefinitionInput,
CredentialDefinitionResponse,
CredentialDefinitionResponsePage,
FilterInput
}
import org.hyperledger.identus.pollux.PrismEnvelopeResponse
import sttp.apispec.{ExternalDocumentation, Tag}
import sttp.model.StatusCode
import sttp.tapir.{
Expand Down Expand Up @@ -52,7 +56,7 @@ object CredentialDefinitionRegistryEndpoints {

val tag = Tag(name = tagName, description = Option(tagDescription), externalDocs = Option(tagExternalDocumentation))

val createCredentialDefinitionEndpoint: Endpoint[
val createCredentialDefinitionHttpUrlEndpoint: Endpoint[
(ApiKeyCredentials, JwtCredentials),
(RequestContext, CredentialDefinitionInput),
ErrorResponse,
Expand All @@ -79,15 +83,52 @@ object CredentialDefinitionRegistryEndpoints {
.out(jsonBody[http.CredentialDefinitionResponse])
.description("Credential definition record")
.errorOut(basicFailureAndNotFoundAndForbidden)
.name("createCredentialDefinition")
.summary("Publish new definition to the definition registry")
.name("createCredentialDefinitionHttpUrl")
.summary("Publish new definition to the definition registry, resolvable by HTTP url")
.description(
"Create the new credential definition record with metadata and internal JSON Schema on behalf of Cloud Agent. " +
"The credential definition will be signed by the keys of Cloud Agent and issued by the DID that corresponds to it."
)
.tag(tagName)

val getCredentialDefinitionByIdEndpoint: PublicEndpoint[
val createCredentialDefinitionDidUrlEndpoint: Endpoint[
(ApiKeyCredentials, JwtCredentials),
(RequestContext, CredentialDefinitionInput),
ErrorResponse,
CredentialDefinitionResponse,
Any
] =
endpoint.post
.securityIn(apiKeyHeader)
.securityIn(jwtAuthHeader)
.in(extractFromRequest[RequestContext](RequestContext.apply))
.in("credential-definition-registry" / "definitions" / "did-url")
.in(
jsonBody[CredentialDefinitionInput]
.description(
"JSON object required for the credential definition creation"
)
)
.out(
statusCode(StatusCode.Created)
.description(
"The new credential definition record is successfully created"
)
)
.out(
jsonBody[http.CredentialDefinitionResponse]
) // We use same response as for HTTP url on DID url for definitions
.description("Credential definition record")
.errorOut(basicFailureAndNotFoundAndForbidden)
.name("createCredentialDefinitionDidUrl")
.summary("Publish new definition to the definition registry, resolvable by DID url")
.description(
"Create the new credential definition record with metadata and internal JSON Schema on behalf of the Cloud Agent. " +
"The credential definition will be signed by the keys of Cloud Agent and issued by the DID that corresponds to it."
shotexa marked this conversation as resolved.
Show resolved Hide resolved
)
.tag(tagName)

val getCredentialDefinitionByIdHttpUrlEndpoint: PublicEndpoint[
(RequestContext, UUID),
ErrorResponse,
CredentialDefinitionResponse,
Expand All @@ -102,14 +143,40 @@ object CredentialDefinitionRegistryEndpoints {
)
.out(jsonBody[CredentialDefinitionResponse].description("CredentialDefinition found by `guid`"))
.errorOut(basicFailuresAndNotFound)
.name("getCredentialDefinitionById")
.name("getCredentialDefinitionByIdHttpUrl")
.summary("Fetch the credential definition from the registry by `guid`")
.description(
"Fetch the credential definition by the unique identifier"
)
.tag(tagName)

val getCredentialDefinitionInnerDefinitionByIdEndpoint: PublicEndpoint[
val getCredentialDefinitionByIdDidUrlEndpoint: PublicEndpoint[
(RequestContext, UUID),
ErrorResponse,
PrismEnvelopeResponse,
Any
] =
endpoint.get
.in(extractFromRequest[RequestContext](RequestContext.apply))
.in(
"credential-definition-registry" / "definitions" / "did-url" / path[UUID]("guid").description(
"Globally unique identifier of the credential definition record"
)
)
.out(
jsonBody[PrismEnvelopeResponse].description(
"CredentialDefinition found by `guid`, wrapped in an envelope"
)
)
.errorOut(basicFailuresAndNotFound)
.name("getCredentialDefinitionByIdDidUrl")
.summary("Fetch the credential definition from the registry by `guid`, wrapped in an envelope")
.description(
"Fetch the credential definition by the unique identifier, it should have been crated via DID url, otherwise not found error is returned."
)
.tag(tagName)

val getCredentialDefinitionInnerDefinitionByIdHttpUrlEndpoint: PublicEndpoint[
(RequestContext, UUID),
ErrorResponse,
zio.json.ast.Json,
Expand All @@ -124,16 +191,42 @@ object CredentialDefinitionRegistryEndpoints {
)
.out(jsonBody[zio.json.ast.Json].description("CredentialDefinition found by `guid`"))
.errorOut(basicFailuresAndNotFound)
.name("getCredentialDefinitionInnerDefinitionById")
.name("getCredentialDefinitionInnerDefinitionByIdHttpUrl")
.summary("Fetch the inner definition field of the credential definition from the registry by `guid`")
.description(
"Fetch the inner definition fields of the credential definition by the unique identifier"
)
.tag(tagName)

val getCredentialDefinitionInnerDefinitionByIdDidUrlEndpoint: PublicEndpoint[
(RequestContext, UUID),
ErrorResponse,
PrismEnvelopeResponse,
Any
] =
endpoint.get
.in(extractFromRequest[RequestContext](RequestContext.apply))
.in(
"credential-definition-registry" / "definitions" / "did-url" / path[UUID]("guid") / "definition".description(
"Globally unique identifier of the credential definition record"
)
)
.out(
jsonBody[PrismEnvelopeResponse].description("CredentialDefinition found by `guid`")
)
.errorOut(basicFailuresAndNotFound)
.name("getCredentialDefinitionInnerDefinitionByIdDidUrl")
.summary(
"Fetch the inner definition field of the credential definition from the registry by `guid`, wrapped in an envelope"
)
.description(
"Fetch the inner definition fields of the credential definition by the unique identifier, it should have been crated via DID url, otherwise not found error is returned."
)
.tag(tagName)

private val credentialDefinitionFilterInput: EndpointInput[http.FilterInput] = EndpointInput.derived[http.FilterInput]
private val paginationInput: EndpointInput[PaginationInput] = EndpointInput.derived[PaginationInput]
val lookupCredentialDefinitionsByQueryEndpoint: Endpoint[
val lookupCredentialDefinitionsByQueryHttpUrlEndpoint: Endpoint[
(ApiKeyCredentials, JwtCredentials),
(
RequestContext,
Expand All @@ -159,10 +252,43 @@ object CredentialDefinitionRegistryEndpoints {
.in(query[Option[Order]]("order"))
.out(jsonBody[CredentialDefinitionResponsePage].description("Collection of CredentialDefinitions records."))
.errorOut(basicFailures)
.name("lookupCredentialDefinitionsByQuery")
.name("lookupCredentialDefinitionsByQueryHttpUrl")
.summary("Lookup credential definitions by indexed fields")
.description(
"Lookup credential definitions by `author`, `name`, `tag` parameters and control the pagination by `offset` and `limit` parameters "
)
.tag(tagName)

val lookupCredentialDefinitionsByQueryDidUrlEndpoint: Endpoint[
(ApiKeyCredentials, JwtCredentials),
(
RequestContext,
FilterInput,
PaginationInput,
Option[Order]
),
ErrorResponse,
CredentialDefinitionDidUrlResponsePage,
Any
] =
endpoint.get
.securityIn(apiKeyHeader)
.securityIn(jwtAuthHeader)
.in(extractFromRequest[RequestContext](RequestContext.apply))
.in(
"credential-definition-registry" / "definitions" / "did-url".description(
"Lookup credential definitions by query"
)
)
.in(credentialDefinitionFilterInput)
.in(paginationInput)
.in(query[Option[Order]]("order"))
.out(jsonBody[CredentialDefinitionDidUrlResponsePage].description("Collection of CredentialDefinitions records."))
.errorOut(basicFailures)
.name("lookupCredentialDefinitionsByQueryDidUrl")
.summary("Lookup credential definitions by indexed fields")
.description(
"Lookup DID url resolvable credential definitions by `author`, `name`, `tag` parameters and control the pagination by `offset` and `limit` parameters "
)
.tag(tagName)
}
Loading
Loading