diff --git a/.mega-linter.yml b/.mega-linter.yml index 9c6e9cd614..7a10e55508 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -17,6 +17,17 @@ DISABLE_LINTERS: - CPP_CPPLINT # For pollux/lib/anoncreds/src/main/c - JAVA_CHECKSTYLE # For pollux/lib/anoncreds/src/main/java - GHERKIN_GHERKIN_LINT + # For python, disable all except PYTHON_BLACK linter + - PYTHON_PYLINT + - PYTHON_FLAKE8 + - PYTHON_ISORT + - PYTHON_BANDIT + - PYTHON_MYPY + - PYTHON_PYRIGHT + - PYTHON_RUFF + # TODO: revert before merging to `main`. Disabled to ease the development of keycloak extension + - JAVA_CHECKSTYLE + - JAVA_PMD DISABLE_ERRORS_LINTERS: - KOTLIN_KTLINT diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala index 3f56690b5e..f60a833fe7 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/CloudAgentApp.scala @@ -19,7 +19,7 @@ import org.hyperledger.identus.iam.entity.http.EntityServerEndpoints import org.hyperledger.identus.iam.wallet.http.WalletManagementServerEndpoints import org.hyperledger.identus.issue.controller.IssueServerEndpoints import org.hyperledger.identus.mercury.{DidOps, HttpClient} -import org.hyperledger.identus.oidc4vc.CredentialIssuerServerEndpoints +import org.hyperledger.identus.oid4vci.CredentialIssuerServerEndpoints import org.hyperledger.identus.pollux.core.service.{CredentialService, PresentationService} import org.hyperledger.identus.pollux.credentialdefinition.CredentialDefinitionRegistryServerEndpoints import org.hyperledger.identus.pollux.credentialschema.{ diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala index 0fa8a06f24..c64861fd51 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/MainApp.scala @@ -34,10 +34,9 @@ import org.hyperledger.identus.iam.entity.http.controller.{EntityController, Ent import org.hyperledger.identus.iam.wallet.http.controller.WalletManagementControllerImpl import org.hyperledger.identus.issue.controller.IssueControllerImpl import org.hyperledger.identus.mercury.* -import org.hyperledger.identus.oidc4vc.controller.CredentialIssuerControllerImpl -import org.hyperledger.identus.oidc4vc.service.OIDCCredentialIssuerServiceImpl -import org.hyperledger.identus.oidc4vc.storage.InMemoryIssuanceSessionService -import org.hyperledger.identus.pollux.core.repository.InMemoryOIDC4VCIssuerMetadataRepository +import org.hyperledger.identus.oid4vci.controller.CredentialIssuerControllerImpl +import org.hyperledger.identus.oid4vci.service.OIDCCredentialIssuerServiceImpl +import org.hyperledger.identus.oid4vci.storage.InMemoryIssuanceSessionService import org.hyperledger.identus.pollux.core.service.* import org.hyperledger.identus.pollux.core.service.verification.VcVerificationServiceImpl import org.hyperledger.identus.pollux.credentialdefinition.controller.CredentialDefinitionControllerImpl @@ -46,6 +45,7 @@ import org.hyperledger.identus.pollux.credentialschema.controller.{ CredentialSchemaControllerImpl, VerificationPolicyControllerImpl } +import org.hyperledger.identus.pollux.sql.repository.JdbcOID4VCIIssuerMetadataRepository import org.hyperledger.identus.pollux.sql.repository.{ JdbcCredentialDefinitionRepository, JdbcCredentialRepository, @@ -200,12 +200,12 @@ object MainApp extends ZIOAppDefault { RepoModule.polluxContextAwareTransactorLayer ++ RepoModule.polluxTransactorLayer >>> JdbcCredentialSchemaRepository.layer, RepoModule.polluxContextAwareTransactorLayer ++ RepoModule.polluxTransactorLayer >>> JdbcCredentialDefinitionRepository.layer, RepoModule.polluxContextAwareTransactorLayer ++ RepoModule.polluxTransactorLayer >>> JdbcPresentationRepository.layer, + RepoModule.polluxContextAwareTransactorLayer ++ RepoModule.polluxTransactorLayer >>> JdbcOID4VCIIssuerMetadataRepository.layer, RepoModule.polluxContextAwareTransactorLayer >>> JdbcVerificationPolicyRepository.layer, // oidc CredentialIssuerControllerImpl.layer, InMemoryIssuanceSessionService.layer, - InMemoryOIDC4VCIssuerMetadataRepository.layer, - OIDC4VCIssuerMetadataServiceImpl.layer, + OID4VCIIssuerMetadataServiceImpl.layer, OIDCCredentialIssuerServiceImpl.layer, // event notification service ZLayer.succeed(500) >>> EventNotificationServiceImpl.layer, diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/CredentialIssuerEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerEndpoints.scala similarity index 62% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/CredentialIssuerEndpoints.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerEndpoints.scala index d7ea895db2..e2491008b1 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/CredentialIssuerEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerEndpoints.scala @@ -1,4 +1,4 @@ -package org.hyperledger.identus.oidc4vc +package org.hyperledger.identus.oid4vci import org.hyperledger.identus.api.http.{EndpointOutputs, ErrorResponse, RequestContext} import org.hyperledger.identus.castor.controller.http.DIDInput @@ -7,7 +7,7 @@ import org.hyperledger.identus.iam.authentication.apikey.ApiKeyCredentials import org.hyperledger.identus.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader import org.hyperledger.identus.iam.authentication.oidc.JwtCredentials import org.hyperledger.identus.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader -import org.hyperledger.identus.oidc4vc.http.* +import org.hyperledger.identus.oid4vci.http.* import sttp.apispec.Tag import sttp.model.StatusCode import sttp.tapir.* @@ -17,7 +17,7 @@ import java.util.UUID object CredentialIssuerEndpoints { - private val tagName = "OIDC Credential Issuer" + private val tagName = "OpenID for Verifiable Credential Issuance" private val tagDescription = s""" |The __${tagName}__ is a service that issues credentials to users by implementing the [OIDC for Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html) specification. @@ -32,13 +32,17 @@ object CredentialIssuerEndpoints { type ExtendedErrorResponse = Either[ErrorResponse, CredentialErrorResponse] private val issuerIdPathSegment = path[UUID]("issuerId") - .description("An issuer identifier in the oidc4vc protocol") + .description("An issuer identifier in the oid4vci protocol") .example(UUID.fromString("f47ac10b-58cc-4372-a567-0e02b2c3d479")) + private val credentialConfigIdSegment = path[String]("credentialConfigId") + .description("An identifier for the credential configuration") + .example("UniversityDegree") + private val baseEndpoint = endpoint .tag(tagName) .in(extractFromRequest[RequestContext](RequestContext.apply)) - .in("oidc4vc" / "issuers") + .in("oid4vci" / "issuers") private val baseIssuerPrivateEndpoint = baseEndpoint .securityIn(apiKeyHeader) @@ -96,6 +100,10 @@ object CredentialIssuerEndpoints { .errorOut(EndpointOutputs.basicFailureAndNotFoundAndForbidden) .name("createCredentialOffer") .summary("Create a new credential offer") + .description( + """Create a new credential offer and return a compliant `CredentialOffer` for the holder's + |[Credential Offer Endpoint](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer-endpoint).""".stripMargin + ) val nonceEndpoint: Endpoint[ (ApiKeyCredentials, JwtCredentials), @@ -126,13 +134,56 @@ object CredentialIssuerEndpoints { ] = baseIssuerPrivateEndpoint.post .in(jsonBody[CreateCredentialIssuerRequest]) .out( - statusCode(StatusCode.Created).description("Credential Issuer created successfully") + statusCode(StatusCode.Created).description("Credential issuer created successfully") ) .out(jsonBody[CredentialIssuer]) .errorOut(EndpointOutputs.basicFailureAndNotFoundAndForbidden) .name("createCredentialIssuer") .summary("Create a new credential issuer") + val getCredentialIssuersEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + RequestContext, + ErrorResponse, + CredentialIssuerPage, + Any + ] = baseIssuerPrivateEndpoint.get + .errorOut(EndpointOutputs.basicFailuresAndForbidden) + .out(statusCode(StatusCode.Ok).description("List the credential issuers")) + .out(jsonBody[CredentialIssuerPage]) + .name("getCredentialIssuers") + .summary("List all credential issuers") + + val updateCredentialIssuerEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + (RequestContext, UUID, PatchCredentialIssuerRequest), + ErrorResponse, + CredentialIssuer, + Any + ] = baseIssuerPrivateEndpoint.patch + .in(issuerIdPathSegment) + .in(jsonBody[PatchCredentialIssuerRequest]) + .out( + statusCode(StatusCode.Ok).description("Credential issuer updated successfully") + ) + .out(jsonBody[CredentialIssuer]) + .errorOut(EndpointOutputs.basicFailureAndNotFoundAndForbidden) + .name("updateCredentialIssuer") + .summary("Update the credential issuer") + + val deleteCredentialIssuerEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + (RequestContext, UUID), + ErrorResponse, + Unit, + Any + ] = baseIssuerPrivateEndpoint.delete + .in(issuerIdPathSegment) + .errorOut(EndpointOutputs.basicFailureAndNotFoundAndForbidden) + .out(statusCode(StatusCode.Ok).description("Credential issuer deleted successfully")) + .name("deleteCredentialIssuer") + .summary("Delete the credential issuer") + val createCredentialConfigurationEndpoint: Endpoint[ (ApiKeyCredentials, JwtCredentials), (RequestContext, UUID, CreateCredentialConfigurationRequest), @@ -149,6 +200,42 @@ object CredentialIssuerEndpoints { .errorOut(EndpointOutputs.basicFailureAndNotFoundAndForbidden) .name("createCredentialConfiguration") .summary("Create a new credential configuration") + .description( + """Create a new credential configuration for the issuer. + |It represents the configuration of the credential that can be issued by the issuer. + |This credential configuration object will be displayed in the credential issuer metadata.""".stripMargin + ) + + val getCredentialConfigurationEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + (RequestContext, UUID, String), + ErrorResponse, + CredentialConfiguration, + Any + ] = baseIssuerPrivateEndpoint.get + .in(issuerIdPathSegment / "credential-configurations" / credentialConfigIdSegment) + .out( + statusCode(StatusCode.Ok).description("Get credential configuration successfully") + ) + .out(jsonBody[CredentialConfiguration]) + .errorOut(EndpointOutputs.basicFailureAndNotFoundAndForbidden) + .name("getCredentialConfiguration") + .summary("Get the credential configuration") + + val deleteCredentialConfigurationEndpoint: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + (RequestContext, UUID, String), + ErrorResponse, + Unit, + Any + ] = baseIssuerPrivateEndpoint.delete + .in(issuerIdPathSegment / "credential-configurations" / credentialConfigIdSegment) + .out( + statusCode(StatusCode.Ok).description("Credential configuration deleted successfully") + ) + .errorOut(EndpointOutputs.basicFailureAndNotFoundAndForbidden) + .name("deleteCredentialConfiguration") + .summary("Delete the credential configuration") val issuerMetadataEndpoint: Endpoint[ Unit, @@ -164,6 +251,6 @@ object CredentialIssuerEndpoints { .out(jsonBody[IssuerMetadata]) .errorOut(EndpointOutputs.basicFailuresAndNotFound) .name("getIssuerMetadata") - .summary("Get oidc4vc credential issuer metadata") + .summary("Get the credential issuer metadata") } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/CredentialIssuerServerEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerServerEndpoints.scala similarity index 59% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/CredentialIssuerServerEndpoints.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerServerEndpoints.scala index 3b498fbcce..85c6770d8e 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/CredentialIssuerServerEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/CredentialIssuerServerEndpoints.scala @@ -1,11 +1,11 @@ -package org.hyperledger.identus.oidc4vc +package org.hyperledger.identus.oid4vci import org.hyperledger.identus.LogUtils.* import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext} import org.hyperledger.identus.iam.authentication.{Authenticator, Authorizer, DefaultAuthenticator, SecurityLogic} -import org.hyperledger.identus.oidc4vc.controller.CredentialIssuerController -import org.hyperledger.identus.oidc4vc.http.{CredentialErrorResponse, CredentialRequest, NonceResponse} +import org.hyperledger.identus.oid4vci.controller.CredentialIssuerController +import org.hyperledger.identus.oid4vci.http.{CredentialErrorResponse, CredentialRequest, NonceResponse} import sttp.tapir.ztapir.* import zio.* @@ -69,6 +69,42 @@ case class CredentialIssuerServerEndpoints( } } + val getCredentialIssuersServerEndpoint: ZServerEndpoint[Any, Any] = + CredentialIssuerEndpoints.getCredentialIssuersEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case rc => + credentialIssuerController + .getCredentialIssuers(rc) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(rc) + } + } + + val updateCredentialIssuerServerEndpoint: ZServerEndpoint[Any, Any] = + CredentialIssuerEndpoints.updateCredentialIssuerEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (rc, issuerId, request) => + credentialIssuerController + .updateCredentialIssuer(rc, issuerId, request) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(rc) + } + } + + val deleteCredentialIssuerServerEndpoint: ZServerEndpoint[Any, Any] = + CredentialIssuerEndpoints.deleteCredentialIssuerEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (rc, issuerId) => + credentialIssuerController + .deleteCredentialIssuer(rc, issuerId) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(rc) + } + } + val createCredentialConfigurationServerEndpoint: ZServerEndpoint[Any, Any] = CredentialIssuerEndpoints.createCredentialConfigurationEndpoint .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) @@ -81,6 +117,30 @@ case class CredentialIssuerServerEndpoints( } } + val getCredentialConfigurationServerEndpoint: ZServerEndpoint[Any, Any] = + CredentialIssuerEndpoints.getCredentialConfigurationEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (rc, issuerId, configurationId) => + credentialIssuerController + .getCredentialConfiguration(rc, issuerId, configurationId) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(rc) + } + } + + val deleteCredentialConfigurationServerEndpoint: ZServerEndpoint[Any, Any] = + CredentialIssuerEndpoints.deleteCredentialConfigurationEndpoint + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (rc, issuerId, configurationId) => + credentialIssuerController + .deleteCredentialConfiguration(rc, issuerId, configurationId) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(rc) + } + } + val issuerMetadataServerEndpoint: ZServerEndpoint[Any, Any] = CredentialIssuerEndpoints.issuerMetadataEndpoint .zServerLogic { { case (rc, didRef) => credentialIssuerController.getIssuerMetadata(rc, didRef).logTrace(rc) } @@ -91,7 +151,12 @@ case class CredentialIssuerServerEndpoints( createCredentialOfferServerEndpoint, nonceServerEndpoint, createCredentialIssuerServerEndpoint, + getCredentialIssuersServerEndpoint, + updateCredentialIssuerServerEndpoint, + deleteCredentialIssuerServerEndpoint, createCredentialConfigurationServerEndpoint, + getCredentialConfigurationServerEndpoint, + deleteCredentialConfigurationServerEndpoint, issuerMetadataServerEndpoint ) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/controller/CredentialIssuerController.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala similarity index 71% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/controller/CredentialIssuerController.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala index 21359b2728..3b44685893 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/controller/CredentialIssuerController.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala @@ -1,15 +1,16 @@ -package org.hyperledger.identus.oidc4vc.controller +package org.hyperledger.identus.oid4vci.controller import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.api.http.ErrorResponse.badRequest import org.hyperledger.identus.api.http.ErrorResponse.internalServerError import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext} +import org.hyperledger.identus.api.util.PaginationUtils import org.hyperledger.identus.castor.core.model.did.{CanonicalPrismDID, PrismDID} -import org.hyperledger.identus.oidc4vc.CredentialIssuerEndpoints.ExtendedErrorResponse -import org.hyperledger.identus.oidc4vc.http.* -import org.hyperledger.identus.oidc4vc.http.CredentialErrorCode.* -import org.hyperledger.identus.oidc4vc.service.OIDCCredentialIssuerService -import org.hyperledger.identus.pollux.core.service.OIDC4VCIssuerMetadataService +import org.hyperledger.identus.oid4vci.CredentialIssuerEndpoints.ExtendedErrorResponse +import org.hyperledger.identus.oid4vci.http.* +import org.hyperledger.identus.oid4vci.http.CredentialErrorCode.* +import org.hyperledger.identus.oid4vci.service.OIDCCredentialIssuerService +import org.hyperledger.identus.pollux.core.service.OID4VCIIssuerMetadataService import org.hyperledger.identus.shared.models.WalletAccessContext import zio.{IO, URLayer, ZIO, ZLayer} @@ -42,12 +43,39 @@ trait CredentialIssuerController { request: CreateCredentialIssuerRequest ): ZIO[WalletAccessContext, ErrorResponse, CredentialIssuer] + def getCredentialIssuers( + ctx: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, CredentialIssuerPage] + + def updateCredentialIssuer( + ctx: RequestContext, + issuerId: UUID, + request: PatchCredentialIssuerRequest + ): ZIO[WalletAccessContext, ErrorResponse, CredentialIssuer] + + def deleteCredentialIssuer( + ctx: RequestContext, + issuerId: UUID, + ): ZIO[WalletAccessContext, ErrorResponse, Unit] + def createCredentialConfiguration( ctx: RequestContext, issuerId: UUID, request: CreateCredentialConfigurationRequest ): ZIO[WalletAccessContext, ErrorResponse, CredentialConfiguration] + def getCredentialConfiguration( + ctx: RequestContext, + issuerId: UUID, + configurationId: String + ): ZIO[WalletAccessContext, ErrorResponse, CredentialConfiguration] + + def deleteCredentialConfiguration( + ctx: RequestContext, + issuerId: UUID, + configurationId: String + ): ZIO[WalletAccessContext, ErrorResponse, Unit] + def getIssuerMetadata( ctx: RequestContext, issuerId: UUID @@ -100,13 +128,18 @@ object CredentialIssuerController { case class CredentialIssuerControllerImpl( credentialIssuerService: OIDCCredentialIssuerService, - issuerMetadataService: OIDC4VCIssuerMetadataService, + issuerMetadataService: OID4VCIIssuerMetadataService, agentBaseUrl: URL ) extends CredentialIssuerController { import CredentialIssuerController.Errors.* import OIDCCredentialIssuerService.Errors.* + private def parseURL(url: String): IO[ErrorResponse, URL] = + ZIO + .attempt(URI.create(url).toURL()) + .mapError(ue => badRequest(detail = Some(s"Invalid URL: $url"))) + private def parseIssuerDID[E](didRef: String, errorFn: (String, String) => E): IO[E, CanonicalPrismDID] = { for { prismDID <- ZIO @@ -213,14 +246,45 @@ case class CredentialIssuerControllerImpl( override def createCredentialIssuer( ctx: RequestContext, request: CreateCredentialIssuerRequest - ): ZIO[WalletAccessContext, ErrorResponse, CredentialIssuer] = { + ): ZIO[WalletAccessContext, ErrorResponse, CredentialIssuer] = for { - authServerUrl <- ZIO - .attempt(URI.create(request.authorizationServer).toURL()) - .mapError(ue => badRequest(detail = Some(s"Invalid URL: ${request.authorizationServer}"))) + authServerUrl <- parseURL(request.authorizationServer) issuer <- issuerMetadataService.createCredentialIssuer(authServerUrl) } yield issuer - } + + override def getCredentialIssuers( + ctx: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, CredentialIssuerPage] = + val uri = ctx.request.uri + for { + issuers <- issuerMetadataService.getCredentialIssuers + } yield CredentialIssuerPage( + self = uri.toString(), + pageOf = PaginationUtils.composePageOfUri(uri).toString, + contents = issuers.map(i => i) + ) + + override def updateCredentialIssuer( + ctx: RequestContext, + issuerId: UUID, + request: PatchCredentialIssuerRequest + ): ZIO[WalletAccessContext, ErrorResponse, CredentialIssuer] = + for { + maybeAuthServerUrl <- ZIO + .succeed(request.authorizationServer) + .flatMap { + case Some(url) => parseURL(url).asSome + case None => ZIO.none + } + issuer <- issuerMetadataService.updateCredentialIssuer(issuerId, maybeAuthServerUrl) + } yield issuer: CredentialIssuer + + override def deleteCredentialIssuer( + ctx: RequestContext, + issuerId: UUID + ): ZIO[WalletAccessContext, ErrorResponse, Unit] = + for _ <- issuerMetadataService.deleteCredentialIssuer(issuerId) + yield () override def createCredentialConfiguration( ctx: RequestContext, @@ -237,22 +301,37 @@ case class CredentialIssuerControllerImpl( } yield credentialConfiguration: CredentialConfiguration } + override def getCredentialConfiguration( + ctx: RequestContext, + issuerId: UUID, + configurationId: String + ): ZIO[WalletAccessContext, ErrorResponse, CredentialConfiguration] = + for credentialConfiguration <- issuerMetadataService.getCredentialConfigurationById(issuerId, configurationId) + yield credentialConfiguration: CredentialConfiguration + + override def deleteCredentialConfiguration( + ctx: RequestContext, + issuerId: UUID, + configurationId: String + ): ZIO[WalletAccessContext, ErrorResponse, Unit] = + issuerMetadataService.deleteCredentialConfiguration(issuerId, configurationId) + override def getIssuerMetadata(ctx: RequestContext, issuerId: UUID): IO[ErrorResponse, IssuerMetadata] = { - for + for { credentialIssuer <- issuerMetadataService.getCredentialIssuer(issuerId) - credentialConfigurations <- issuerMetadataService.listCredentialConfiguration(issuerId) - yield IssuerMetadata.fromIssuer(agentBaseUrl, credentialIssuer, credentialConfigurations) + credentialConfigurations <- issuerMetadataService.getCredentialConfigurations(issuerId) + } yield IssuerMetadata.fromIssuer(agentBaseUrl, credentialIssuer, credentialConfigurations) } } object CredentialIssuerControllerImpl { val layer - : URLayer[AppConfig & OIDCCredentialIssuerService & OIDC4VCIssuerMetadataService, CredentialIssuerController] = + : URLayer[AppConfig & OIDCCredentialIssuerService & OID4VCIIssuerMetadataService, CredentialIssuerController] = ZLayer.fromZIO( for { agentBaseUrl <- ZIO.serviceWith[AppConfig](_.agent.httpEndpoint.publicEndpointUrl) oidcIssuerService <- ZIO.service[OIDCCredentialIssuerService] - oidcIssuerMetadataService <- ZIO.service[OIDC4VCIssuerMetadataService] + oidcIssuerMetadataService <- ZIO.service[OID4VCIIssuerMetadataService] } yield CredentialIssuerControllerImpl(oidcIssuerService, oidcIssuerMetadataService, agentBaseUrl) ) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/domain/IssuanceSession.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/domain/IssuanceSession.scala similarity index 88% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/domain/IssuanceSession.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/domain/IssuanceSession.scala index 303aee7ba1..e0d3a129e5 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/domain/IssuanceSession.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/domain/IssuanceSession.scala @@ -1,4 +1,4 @@ -package org.hyperledger.identus.oidc4vc.domain +package org.hyperledger.identus.oid4vci.domain import org.hyperledger.identus.castor.core.model.did.CanonicalPrismDID import org.hyperledger.identus.castor.core.model.did.DID diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/AuthorizationErrors.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/AuthorizationErrors.scala similarity index 98% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/AuthorizationErrors.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/AuthorizationErrors.scala index f8cd550ef0..4648a738e7 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/AuthorizationErrors.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/AuthorizationErrors.scala @@ -1,4 +1,4 @@ -package org.hyperledger.identus.oidc4vc.http +package org.hyperledger.identus.oid4vci.http import org.hyperledger.identus.api.http.EndpointOutputs.statusCodeMatcher import org.hyperledger.identus.api.http.ErrorResponse diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialConfiguration.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialConfiguration.scala similarity index 65% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialConfiguration.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialConfiguration.scala index e23e247906..a5347eded0 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialConfiguration.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialConfiguration.scala @@ -1,9 +1,11 @@ -package org.hyperledger.identus.oidc4vc.http +package org.hyperledger.identus.oid4vci.http -import org.hyperledger.identus.pollux.core.model.oidc4vc.CredentialConfiguration as PolluxCredentialConfiguration +import org.hyperledger.identus.pollux.core.model.oid4vci.CredentialConfiguration as PolluxCredentialConfiguration import sttp.tapir.Schema import zio.json.* +import java.time.OffsetDateTime +import java.time.ZoneOffset import scala.language.implicitConversions final case class CreateCredentialConfigurationRequest( @@ -19,11 +21,11 @@ object CreateCredentialConfigurationRequest { } final case class CredentialConfiguration( + configurationId: String, format: CredentialFormat, scope: String, - credential_definition: CredentialDefinition, - cryptographic_binding_methods_supported: Seq[String] = Seq("did:prism"), - credential_signing_alg_values_supported: Seq[String] = Seq("ES256K") + schemaId: String, + createdAt: OffsetDateTime ) object CredentialConfiguration { @@ -33,12 +35,10 @@ object CredentialConfiguration { given Conversion[PolluxCredentialConfiguration, CredentialConfiguration] = cc => CredentialConfiguration( + configurationId = cc.configurationId, format = cc.format, scope = cc.scope, - credential_definition = CredentialDefinition( - `@context` = Some(Seq("https://www.w3.org/2018/credentials/v1")), - `type` = Seq("VerifiableCredential"), - credentialSubject = Some(Map.empty) // TODO: implement conversion from JsonSchhema - ) + schemaId = cc.schemaId.toString(), + createdAt = cc.createdAt.atOffset(ZoneOffset.UTC), ) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialIssuer.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialIssuer.scala similarity index 50% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialIssuer.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialIssuer.scala index 4360375bf5..aadb34993f 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialIssuer.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialIssuer.scala @@ -1,6 +1,6 @@ -package org.hyperledger.identus.oidc4vc.http +package org.hyperledger.identus.oid4vci.http -import org.hyperledger.identus.pollux.core.model.oidc4vc.CredentialIssuer as PolluxCredentialIssuer +import org.hyperledger.identus.pollux.core.model.oid4vci.CredentialIssuer as PolluxCredentialIssuer import sttp.tapir.Schema import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} @@ -16,6 +16,14 @@ object CreateCredentialIssuerRequest { case class CredentialIssuer(id: UUID, authorizationServer: String) +case class PatchCredentialIssuerRequest(authorizationServer: Option[String] = None) + +object PatchCredentialIssuerRequest { + given schema: Schema[PatchCredentialIssuerRequest] = Schema.derived + given encoder: JsonEncoder[PatchCredentialIssuerRequest] = DeriveJsonEncoder.gen + given decoder: JsonDecoder[PatchCredentialIssuerRequest] = DeriveJsonDecoder.gen +} + object CredentialIssuer { given schema: Schema[CredentialIssuer] = Schema.derived given encoder: JsonEncoder[CredentialIssuer] = DeriveJsonEncoder.gen @@ -24,3 +32,18 @@ object CredentialIssuer { given Conversion[PolluxCredentialIssuer, CredentialIssuer] = domain => CredentialIssuer(domain.id, domain.authorizationServer.toString) } + +case class CredentialIssuerPage( + self: String, + kind: String = "CredentialIssuerPage", + pageOf: String, + next: Option[String] = None, + previous: Option[String] = None, + contents: Seq[CredentialIssuer] +) + +object CredentialIssuerPage { + given schema: Schema[CredentialIssuerPage] = Schema.derived + given encoder: JsonEncoder[CredentialIssuerPage] = DeriveJsonEncoder.gen + given decoder: JsonDecoder[CredentialIssuerPage] = DeriveJsonDecoder.gen +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialOffer.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialOffer.scala similarity index 97% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialOffer.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialOffer.scala index 5f68e58098..73d0c3a371 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialOffer.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialOffer.scala @@ -1,4 +1,4 @@ -package org.hyperledger.identus.oidc4vc.http +package org.hyperledger.identus.oid4vci.http import sttp.tapir.Schema import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialOfferRequest.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialOfferRequest.scala similarity index 95% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialOfferRequest.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialOfferRequest.scala index 06f0a4b16e..0c4ab1efe5 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialOfferRequest.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialOfferRequest.scala @@ -1,4 +1,4 @@ -package org.hyperledger.identus.oidc4vc.http +package org.hyperledger.identus.oid4vci.http import sttp.tapir.Schema import sttp.tapir.json.zio.schemaForZioJsonValue diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialRequest.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialRequest.scala similarity index 99% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialRequest.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialRequest.scala index de5daf5b3d..c79221ad73 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialRequest.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialRequest.scala @@ -1,4 +1,4 @@ -package org.hyperledger.identus.oidc4vc.http +package org.hyperledger.identus.oid4vci.http import org.hyperledger.identus.pollux.core.model.CredentialFormat as PolluxCredentialFormat import sttp.tapir.Schema diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialResponse.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialResponse.scala similarity index 98% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialResponse.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialResponse.scala index 493ffa2255..715fff5092 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/CredentialResponse.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/CredentialResponse.scala @@ -1,4 +1,4 @@ -package org.hyperledger.identus.oidc4vc.http +package org.hyperledger.identus.oid4vci.http import sttp.tapir.Schema import sttp.tapir.Schema.annotations.encodedName diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/IssuerMetadata.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/IssuerMetadata.scala new file mode 100644 index 0000000000..adbb3b8319 --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/IssuerMetadata.scala @@ -0,0 +1,80 @@ +package org.hyperledger.identus.oid4vci.http + +import org.hyperledger.identus.pollux.core.model.oid4vci as pollux +import sttp.tapir.Schema +import sttp.tapir.Schema.annotations.encodedName +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} + +import java.net.URL +import scala.language.implicitConversions + +case class IssuerMetadata( + credential_issuer: String, + authorization_servers: Option[Seq[String]], + credential_endpoint: String, + credential_configurations_supported: Map[String, SupportedCredentialConfiguration] +) + +object IssuerMetadata { + given schema: Schema[IssuerMetadata] = Schema.derived + given encoder: JsonEncoder[IssuerMetadata] = DeriveJsonEncoder.gen + given decoder: JsonDecoder[IssuerMetadata] = DeriveJsonDecoder.gen + + def fromIssuer( + agentBaseUrl: URL, + issuer: pollux.CredentialIssuer, + credentialConfigurations: Seq[pollux.CredentialConfiguration] + ): IssuerMetadata = { + val credentialIssuerBaseUrl = agentBaseUrl.toURI().resolve(s"oid4vci/issuers/${issuer.id}").toString + IssuerMetadata( + credential_issuer = credentialIssuerBaseUrl, + authorization_servers = Some(Seq(issuer.authorizationServer.toString())), + credential_endpoint = s"$credentialIssuerBaseUrl/credentials", + credential_configurations_supported = + credentialConfigurations.map(cc => (cc.configurationId, cc: SupportedCredentialConfiguration)).toMap + ) + } +} + +final case class SupportedCredentialConfiguration( + format: CredentialFormat, + scope: String, + credential_definition: CredentialDefinition, + cryptographic_binding_methods_supported: Seq[String] = Seq("did:prism"), + credential_signing_alg_values_supported: Seq[String] = Seq("ES256K"), + proof_types_supported: SupportProofType = + SupportProofType(jwt = ProofTypeConfiguration(proof_signing_alg_values_supported = Seq("ES256K"))) +) + +object SupportedCredentialConfiguration { + given schema: Schema[SupportedCredentialConfiguration] = Schema.derived + given encoder: JsonEncoder[SupportedCredentialConfiguration] = DeriveJsonEncoder.gen + given decoder: JsonDecoder[SupportedCredentialConfiguration] = DeriveJsonDecoder.gen + + given Conversion[pollux.CredentialConfiguration, SupportedCredentialConfiguration] = cc => + SupportedCredentialConfiguration( + format = cc.format, + scope = cc.scope, + credential_definition = CredentialDefinition( + `@context` = Some(Seq("https://www.w3.org/2018/credentials/v1")), + `type` = Seq("VerifiableCredential"), + credentialSubject = None + ) + ) +} + +final case class SupportProofType(jwt: ProofTypeConfiguration) + +object SupportProofType { + given schema: Schema[SupportProofType] = Schema.derived + given encoder: JsonEncoder[SupportProofType] = DeriveJsonEncoder.gen + given decoder: JsonDecoder[SupportProofType] = DeriveJsonDecoder.gen +} + +final case class ProofTypeConfiguration(proof_signing_alg_values_supported: Seq[String]) + +object ProofTypeConfiguration { + given schema: Schema[ProofTypeConfiguration] = Schema.derived + given encoder: JsonEncoder[ProofTypeConfiguration] = DeriveJsonEncoder.gen + given decoder: JsonDecoder[ProofTypeConfiguration] = DeriveJsonDecoder.gen +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/NonceRequest.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/NonceRequest.scala similarity index 89% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/NonceRequest.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/NonceRequest.scala index 3c1ae4f612..ef9ddc9468 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/NonceRequest.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/NonceRequest.scala @@ -1,4 +1,4 @@ -package org.hyperledger.identus.oidc4vc.http +package org.hyperledger.identus.oid4vci.http import sttp.tapir.Schema import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/NonceResponse.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/NonceResponse.scala similarity index 92% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/NonceResponse.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/NonceResponse.scala index f48b2c786f..33239d8089 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/NonceResponse.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/http/NonceResponse.scala @@ -1,4 +1,4 @@ -package org.hyperledger.identus.oidc4vc.http +package org.hyperledger.identus.oid4vci.http import sttp.tapir.Schema import sttp.tapir.Schema.annotations.encodedName diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/service/NonceService.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/NonceService.scala similarity index 91% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/service/NonceService.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/NonceService.scala index 95c13df127..90f28058de 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/service/NonceService.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/NonceService.scala @@ -1,6 +1,6 @@ -package org.hyperledger.identus.oidc4vc.service +package org.hyperledger.identus.oid4vci.service -import org.hyperledger.identus.oidc4vc.service.NonceService.NonceGenerator +import org.hyperledger.identus.oid4vci.service.NonceService.NonceGenerator import zio.Task import java.time.Instant diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/service/OIDCCredentialIssuerService.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala similarity index 97% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/service/OIDCCredentialIssuerService.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala index 97c6e2381e..e53b776eec 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/service/OIDCCredentialIssuerService.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala @@ -1,12 +1,12 @@ -package org.hyperledger.identus.oidc4vc.service +package org.hyperledger.identus.oid4vci.service import io.circe.Json import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage import org.hyperledger.identus.castor.core.model.did.CanonicalPrismDID import org.hyperledger.identus.castor.core.model.did.{PrismDID, VerificationRelationship} -import org.hyperledger.identus.oidc4vc.domain.IssuanceSession -import org.hyperledger.identus.oidc4vc.http.* -import org.hyperledger.identus.oidc4vc.storage.IssuanceSessionStorage +import org.hyperledger.identus.oid4vci.domain.IssuanceSession +import org.hyperledger.identus.oid4vci.http.* +import org.hyperledger.identus.oid4vci.storage.IssuanceSessionStorage import org.hyperledger.identus.pollux.core.service.CredentialService import org.hyperledger.identus.pollux.vc.jwt.{DID, Issuer, JWT, JwtCredential, W3cCredentialPayload} import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/storage/IssuanceSessionStorage.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/storage/IssuanceSessionStorage.scala similarity index 95% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/storage/IssuanceSessionStorage.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/storage/IssuanceSessionStorage.scala index be6b35f394..4b2f385c5b 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/storage/IssuanceSessionStorage.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/storage/IssuanceSessionStorage.scala @@ -1,6 +1,6 @@ -package org.hyperledger.identus.oidc4vc.storage +package org.hyperledger.identus.oid4vci.storage -import org.hyperledger.identus.oidc4vc.domain.IssuanceSession +import org.hyperledger.identus.oid4vci.domain.IssuanceSession import zio.{IO, ULayer, ZIO, ZLayer} import scala.collection.concurrent.TrieMap diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/storage/NonceStorage.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/storage/NonceStorage.scala similarity index 79% rename from cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/storage/NonceStorage.scala rename to cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/storage/NonceStorage.scala index 40bcb4468d..23137db6b7 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/storage/NonceStorage.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/storage/NonceStorage.scala @@ -1,4 +1,4 @@ -package org.hyperledger.identus.oidc4vc.storage +package org.hyperledger.identus.oid4vci.storage trait NonceStorage { def getNonce(nonceExpiresAt: Long): String diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/IssuerMetadata.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/IssuerMetadata.scala deleted file mode 100644 index 687004703e..0000000000 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oidc4vc/http/IssuerMetadata.scala +++ /dev/null @@ -1,37 +0,0 @@ -package org.hyperledger.identus.oidc4vc.http - -import org.hyperledger.identus.pollux.core.model.oidc4vc as pollux -import sttp.tapir.Schema -import sttp.tapir.Schema.annotations.encodedName -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} - -import java.net.URL -import scala.language.implicitConversions - -case class IssuerMetadata( - credential_issuer: String, - authorization_servers: Option[Seq[String]], - credential_endpoint: String, - credential_configurations_supported: Map[String, CredentialConfiguration] -) - -object IssuerMetadata { - given schema: Schema[IssuerMetadata] = Schema.derived - given encoder: JsonEncoder[IssuerMetadata] = DeriveJsonEncoder.gen - given decoder: JsonDecoder[IssuerMetadata] = DeriveJsonDecoder.gen - - def fromIssuer( - agentBaseUrl: URL, - issuer: pollux.CredentialIssuer, - credentialConfigurations: Seq[pollux.CredentialConfiguration] - ): IssuerMetadata = { - val credentialIssuerBaseUrl = agentBaseUrl.toURI().resolve(s"oidc4vc/issuers/${issuer.id}").toString - IssuerMetadata( - credential_issuer = credentialIssuerBaseUrl, - authorization_servers = Some(Seq(issuer.authorizationServer.toString())), - credential_endpoint = s"$credentialIssuerBaseUrl/credentials", - credential_configurations_supported = - credentialConfigurations.map(cc => (cc.configurationId, cc: CredentialConfiguration)).toMap - ) - } -} diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/api/util/Tapir2StaticOAS.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/api/util/Tapir2StaticOAS.scala index aeed6e2960..1f3c9580d0 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/api/util/Tapir2StaticOAS.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/api/util/Tapir2StaticOAS.scala @@ -10,7 +10,7 @@ import org.hyperledger.identus.iam.authentication.DefaultAuthenticator import org.hyperledger.identus.iam.entity.http.controller.EntityController import org.hyperledger.identus.iam.wallet.http.controller.WalletManagementController import org.hyperledger.identus.issue.controller.IssueController -import org.hyperledger.identus.oidc4vc.controller.CredentialIssuerController +import org.hyperledger.identus.oid4vci.controller.CredentialIssuerController import org.hyperledger.identus.pollux.credentialdefinition.controller.CredentialDefinitionController import org.hyperledger.identus.pollux.credentialschema.controller.{ CredentialSchemaController, diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oidc4vc/domain/OIDCCredentialIssuerServiceSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala similarity index 94% rename from cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oidc4vc/domain/OIDCCredentialIssuerServiceSpec.scala rename to cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala index c9b047b0a8..3f74ba9acd 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oidc4vc/domain/OIDCCredentialIssuerServiceSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala @@ -1,13 +1,13 @@ -package org.hyperledger.identus.oidc4vc.domain +package org.hyperledger.identus.oid4vci.domain import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemory import org.hyperledger.identus.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService} import org.hyperledger.identus.agent.walletapi.storage.{DIDNonSecretStorage, MockDIDNonSecretStorage} import org.hyperledger.identus.castor.core.model.did.VerificationRelationship import org.hyperledger.identus.castor.core.service.{DIDService, MockDIDService} -import org.hyperledger.identus.oidc4vc.http.{ClaimDescriptor, CredentialDefinition, Localization} -import org.hyperledger.identus.oidc4vc.service.{OIDCCredentialIssuerService, OIDCCredentialIssuerServiceImpl} -import org.hyperledger.identus.oidc4vc.storage.InMemoryIssuanceSessionService +import org.hyperledger.identus.oid4vci.http.{ClaimDescriptor, CredentialDefinition, Localization} +import org.hyperledger.identus.oid4vci.service.{OIDCCredentialIssuerService, OIDCCredentialIssuerServiceImpl} +import org.hyperledger.identus.oid4vci.storage.InMemoryIssuanceSessionService import org.hyperledger.identus.pollux.core.repository.{ CredentialRepository, CredentialRepositoryInMemory, diff --git a/examples/.nickel/agent.ncl b/examples/.nickel/agent.ncl index 88772c1b3b..1c1a49a124 100644 --- a/examples/.nickel/agent.ncl +++ b/examples/.nickel/agent.ncl @@ -70,9 +70,10 @@ let AgentServiceArgs = { } in { - makeAgentService | AgentServiceArgs -> _ = fun args => + makeAgentService | AgentServiceArgs -> _ + = fun args => { - image = "ghcr.io/input-output-hk/prism-agent:%{args.version}", + image = "ghcr.io/hyperledger/identus-cloud-agent:%{args.version}", restart = "always", environment = { diff --git a/examples/.nickel/bootstrap.ncl b/examples/.nickel/bootstrap.ncl index 2b221b1a25..3c9a04b2c1 100644 --- a/examples/.nickel/bootstrap.ncl +++ b/examples/.nickel/bootstrap.ncl @@ -8,7 +8,8 @@ let HurlBootstrapServiceArgs = { } in { - makeHurlBootstrapService | HurlBootstrapServiceArgs -> _ = fun args => + makeHurlBootstrapService | HurlBootstrapServiceArgs -> _ + = fun args => { image = "ghcr.io/orange-opensource/hurl:%{args.version}", volumes = ["%{args.hurlDir}:/hurl"], diff --git a/examples/.nickel/build.sh b/examples/.nickel/build.sh index 83137384cb..e947f4d88e 100755 --- a/examples/.nickel/build.sh +++ b/examples/.nickel/build.sh @@ -3,7 +3,7 @@ find . -name "*.ncl" | xargs -I _ nickel format _ nickel export ./root.ncl -f yaml --field st > ../st/compose.yaml nickel export ./root.ncl -f yaml --field st-vault > ../st-vault/compose.yaml nickel export ./root.ncl -f yaml --field st-multi > ../st-multi/compose.yaml -nickel export ./root.ncl -f yaml --field st-oidc4vc > ../st-oidc4vc/compose.yaml +nickel export ./root.ncl -f yaml --field st-oid4vci > ../st-oid4vci/compose.yaml nickel export ./root.ncl -f yaml --field mt > ../mt/compose.yaml nickel export ./root.ncl -f yaml --field mt-keycloak > ../mt-keycloak/compose.yaml diff --git a/examples/.nickel/caddy.ncl b/examples/.nickel/caddy.ncl index ca189ad7a8..1f3ccf94b7 100644 --- a/examples/.nickel/caddy.ncl +++ b/examples/.nickel/caddy.ncl @@ -16,7 +16,8 @@ let CaddyServiceArgs = { } in { - makeCaddyConfig | CaddyServiceArgs -> _ = fun args => + makeCaddyConfig | CaddyServiceArgs -> _ + = fun args => { "caddyfile_%{args.name}" = { content = m%" @@ -37,7 +38,8 @@ in "% } }, - makeCaddyService | CaddyServiceArgs -> _ = fun args => + makeCaddyService | CaddyServiceArgs -> _ + = fun args => { image = "caddy:%{args.version}", restart = "always", diff --git a/examples/.nickel/db.ncl b/examples/.nickel/db.ncl index 9d623fd8b2..1df83ebab4 100644 --- a/examples/.nickel/db.ncl +++ b/examples/.nickel/db.ncl @@ -8,7 +8,8 @@ let DbServiceArgs = { } in { - makeDbService | DbServiceArgs -> _ = fun args => + makeDbService | DbServiceArgs -> _ + = fun args => { image = "postgres:%{args.version}", restart = "always", diff --git a/examples/.nickel/node.ncl b/examples/.nickel/node.ncl index 8f6d3f7e18..62110c50b3 100644 --- a/examples/.nickel/node.ncl +++ b/examples/.nickel/node.ncl @@ -21,7 +21,8 @@ let NodeServiceArgs = { } in { - makeNodeService | NodeServiceArgs -> _ = fun args => + makeNodeService | NodeServiceArgs -> _ + = fun args => { image = "ghcr.io/input-output-hk/prism-node:%{args.version}", restart = "always", diff --git a/examples/.nickel/root.ncl b/examples/.nickel/root.ncl index 04081518d1..d719a05ab1 100644 --- a/examples/.nickel/root.ncl +++ b/examples/.nickel/root.ncl @@ -12,7 +12,7 @@ in & (stack.makeAgentStack { name = "holder", port = 8081 }) & (stack.makeAgentStack { name = "verifier", port = 8082 }), - st-oidc4vc = + st-oid4vci = (stack.makeAgentStack { name = "issuer", port = 8080 }) & (stack.makeMockServerStack { port = 7777 }) & ( @@ -21,7 +21,7 @@ in name = "issuer", port = 9980, realm = "students", - build = "../../extensions/keycloak-oidc4vc", + build = "../../extensions/keycloak-oid4vci", extraEnvs = { IDENTUS_URL = "http://caddy-issuer:8080/prism-agent" } } ), diff --git a/examples/.nickel/stack.ncl b/examples/.nickel/stack.ncl index 49ae8396f0..68af4e8615 100644 --- a/examples/.nickel/stack.ncl +++ b/examples/.nickel/stack.ncl @@ -26,7 +26,8 @@ let AgentStackConfig = { } in { - makeMockServerStack | { port | Number } -> _ = fun args => + makeMockServerStack | { port | Number } -> _ + = fun args => { services = { mockserver = { @@ -35,7 +36,8 @@ in } } }, - makeIssuerKeycloakStack | ExternalKeycloakStackConfig -> _ = fun args => + makeIssuerKeycloakStack | ExternalKeycloakStackConfig -> _ + = fun args => { services = { "external-keycloak-%{args.name}" = @@ -70,7 +72,8 @@ in }, } }, - makeAgentStack | AgentStackConfig -> _ = fun args => + makeAgentStack | AgentStackConfig -> _ + = fun args => let pgDockerVolumeName = "pg_data_%{args.name}" in let hosts = { caddy = "caddy-%{args.name}", diff --git a/examples/.nickel/vault.ncl b/examples/.nickel/vault.ncl index c2a3ee5429..3b0514748b 100644 --- a/examples/.nickel/vault.ncl +++ b/examples/.nickel/vault.ncl @@ -10,7 +10,8 @@ let VaultServiceArgs = { } in { - makeVaultService | VaultServiceArgs -> _ = fun args => + makeVaultService | VaultServiceArgs -> _ + = fun args => { image = "hashicorp/vault:%{args.version}", ports = ["%{std.to_string args.hostPort}:8200"], diff --git a/examples/.nickel/versions.ncl b/examples/.nickel/versions.ncl index 0e0acc3c30..72f0030940 100644 --- a/examples/.nickel/versions.ncl +++ b/examples/.nickel/versions.ncl @@ -1,6 +1,6 @@ { # identus - agent = "1.31.0-SNAPSHOT", + agent = "1.32.0-SNAPSHOT", node = "2.2.1", # 3rd party caddy = "2.7.6-alpine", diff --git a/examples/mt-keycloak-vault/compose.yaml b/examples/mt-keycloak-vault/compose.yaml index 0b24750d8f..5867ce8275 100644 --- a/examples/mt-keycloak-vault/compose.yaml +++ b/examples/mt-keycloak-vault/compose.yaml @@ -53,7 +53,7 @@ services: SECRET_STORAGE_BACKEND: vault VAULT_ADDR: http://vault-default:8200 VAULT_TOKEN: admin - image: ghcr.io/input-output-hk/prism-agent:1.31.0-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.32.0-SNAPSHOT restart: always caddy-default: configs: diff --git a/examples/mt-keycloak/compose.yaml b/examples/mt-keycloak/compose.yaml index 9c044c574c..8720f8a793 100644 --- a/examples/mt-keycloak/compose.yaml +++ b/examples/mt-keycloak/compose.yaml @@ -51,7 +51,7 @@ services: PRISM_NODE_PORT: '50053' REST_SERVICE_URL: http://caddy-default:8080/prism-agent SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/input-output-hk/prism-agent:1.31.0-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.32.0-SNAPSHOT restart: always caddy-default: configs: diff --git a/examples/mt-keycloak/tests/02_jwt_flow.hurl b/examples/mt-keycloak/tests/02_jwt_flow.hurl index c1ceb033b4..2169d2f777 100644 --- a/examples/mt-keycloak/tests/02_jwt_flow.hurl +++ b/examples/mt-keycloak/tests/02_jwt_flow.hurl @@ -56,6 +56,20 @@ HTTP 201 [Captures] issuer_did: jsonpath "$.longFormDid" regex "(did:prism:[a-z0-9]+):.+$" +# Issuer publish DID +POST {{ agent_url }}/prism-agent/did-registrar/dids/{{ issuer_did }}/publications +Authorization: Bearer {{ issuer_access_token }} +HTTP 202 + +# Issuer wait for DID to be published +GET {{ agent_url }}/prism-agent/did-registrar/dids/{{ issuer_did }} +Authorization: Bearer {{ issuer_access_token }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.status" == "PUBLISHED" + # Holder create DID POST {{ agent_url }}/prism-agent/did-registrar/dids Authorization: Bearer {{ holder_access_token }} @@ -72,7 +86,7 @@ Authorization: Bearer {{ holder_access_token }} } HTTP 201 [Captures] -holder_did: jsonpath "$.longFormDid" regex "(did:prism:[a-z0-9]+):.+$" +holder_did: jsonpath "$.longFormDid" ############################## # Issuance Connection diff --git a/examples/mt/compose.yaml b/examples/mt/compose.yaml index 616b0fc56b..aa0391df5d 100644 --- a/examples/mt/compose.yaml +++ b/examples/mt/compose.yaml @@ -44,7 +44,7 @@ services: PRISM_NODE_PORT: '50053' REST_SERVICE_URL: http://caddy-default:8080/prism-agent SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/input-output-hk/prism-agent:1.31.0-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.32.0-SNAPSHOT restart: always caddy-default: configs: diff --git a/examples/readme.md b/examples/readme.md deleted file mode 100644 index 2344b5197c..0000000000 --- a/examples/readme.md +++ /dev/null @@ -1,79 +0,0 @@ -# Examples of OpenAPI specifications -This directory contains OpenAPI/Swagger specifications of Identus competitors. -Not all the companies are real competitors, but number of them are experts in particular area, so we have a good opportunity to get inspiration and build a better solution. - -### godiddy-api.yaml -[Godiddy](https://godiddy.com/) is a hosted platform that makes it easy for SSI developers and solution providers to work with DIDs -It provides the following functionality: -- Resolve DIDs -- Manage DIDs -- Search DIDs - -`Castor` is going to provide slightly similar functionality - -Points to consider: -- routes are grouped by service name -- path convention: {service-name}/{resource}: `/wallet-service/keys` -- each schema contain a prefix of the service name: `WalletService.Key` -- did methods are `verbs`, DIDs is not a collection. The path for methods looks like `RPC` -- `diddocuments` is a collection of all the `versions` of the DID -- wallet service implementation contains path with verbs: `sign` and `verify` - -### aries-cloud-agent.json -[Aries Cloud Agent]() is the SSI solution from Hyperledger - -Hyperledger Aries Cloud Agent Python (ACA-Py) is a foundation for building Verifiable Credential (VC) ecosystems. It operates in the second and third layers of the Trust Over IP framework (PDF) using DIDComm messaging and Hyperledger Aries protocols. The "cloud" in the name means that ACA-Py runs on servers (cloud, enterprise, IoT devices, and so forth), and is not designed to run on mobile devices. - -`Castor`, `Pollux`, `Mercury` are going to provide similar functionality - -Aries Cloud Agent API is a mix of everything you need for SSI in a single place. - -Pros: -- everything is in the single place -- easy to deploy and integrate - -Cons: -- monolith architecture -- probably, it's hard to evolve it because of coupling -- mix of all the agents for Issuer/Verifier/Holder/Mediator/Other APIs - -Points to consider: -- part of the routes are grouped by entity/protocol/service: `did-exchange`, `action-menu`, `mediation` -- part of the routes are started from the root and it's hard to figure out what exactly you are going to do: look at `/connections` -- mix of resource and verbs for protocol facades which is hard to understand: `present-proof`, `issue-credentials` - -### sipca-essif-bridge-api.json -[Essif Bridge](https://github.com/sicpa-dlab/essif-bridge) - -For the BRIDGE-project, SICPA proposes 3 technological building blocks that will enhance interoperability and scalability in the SSI ecosystem by giving freedom of choice between verifiable credentials exchange protocols (DIDcomm & CHAPI), credential types (JSON-LD & Anoncreds) and DID-methods. - -This API documentation is a good example of the integration solution. - -### trinsic-credentials-v1-resolved.json - -[Trinsic](https://docs.trinsic.id/reference/authentication) - -Trinsic is the proof of anything platform. We make it easy for people and organizations to prove things about themselves with technology instead of paper documents. Our software is based on Decentralized Identifiers (DIDs) and Verifiable Credentials (VCs), a new digital identity standard. We use the open-source Hyperledger Aries project, to which we are a primary contributor. (c) - -Trinsic is a competitor of Atala Prism that provides similar functionality. - -It's focused on commodity wallet solution, uses Hyperledger Aries under the hood, provides Open API spec for all the endpoints and SDK in number of languages. - -`Castor`, `Pollux`, `Mercury` are going to provide similar functionality. -Number of Atala products provides similar functionality - -Pros: -- good REST API design -- one level of resources in REST API -- attention to documentation -- solid and simple models without overloading -- should be easy and build the solutions - -Cons: -- simplicity of solution causes lack of `offline` functionality - -Points to consider: - -- Would be nice to have the same quality of API in Atala Prism -- API specification is much simple and engineer-friendly compared to Aries -- Schemas are intuitive, well described and easy-for-quick-start \ No newline at end of file diff --git a/examples/st-multi/compose.yaml b/examples/st-multi/compose.yaml index 5b03c9603d..efd0d5cbfb 100644 --- a/examples/st-multi/compose.yaml +++ b/examples/st-multi/compose.yaml @@ -76,7 +76,7 @@ services: PRISM_NODE_PORT: '50053' REST_SERVICE_URL: http://caddy-holder:8081/prism-agent SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/input-output-hk/prism-agent:1.31.0-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.32.0-SNAPSHOT restart: always agent-issuer: depends_on: @@ -106,7 +106,7 @@ services: PRISM_NODE_PORT: '50053' REST_SERVICE_URL: http://caddy-issuer:8080/prism-agent SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/input-output-hk/prism-agent:1.31.0-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.32.0-SNAPSHOT restart: always agent-verifier: depends_on: @@ -136,7 +136,7 @@ services: PRISM_NODE_PORT: '50053' REST_SERVICE_URL: http://caddy-verifier:8082/prism-agent SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/input-output-hk/prism-agent:1.31.0-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.32.0-SNAPSHOT restart: always caddy-holder: configs: diff --git a/examples/st-multi/tests/01_jwt_flow.hurl b/examples/st-multi/tests/01_jwt_flow.hurl index 83c0f4ac13..94bc540275 100644 --- a/examples/st-multi/tests/01_jwt_flow.hurl +++ b/examples/st-multi/tests/01_jwt_flow.hurl @@ -18,6 +18,18 @@ HTTP 201 [Captures] issuer_did: jsonpath "$.longFormDid" regex "(did:prism:[a-z0-9]+):.+$" +# Issuer publish DID +POST {{ issuer_url }}/prism-agent/did-registrar/dids/{{ issuer_did }}/publications +HTTP 202 + +# Issuer wait for DID to be published +GET {{ issuer_url }}/prism-agent/did-registrar/dids/{{ issuer_did }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.status" == "PUBLISHED" + # Holder create DID POST {{ holder_url }}/prism-agent/did-registrar/dids { @@ -33,7 +45,7 @@ POST {{ holder_url }}/prism-agent/did-registrar/dids } HTTP 201 [Captures] -holder_did: jsonpath "$.longFormDid" regex "(did:prism:[a-z0-9]+):.+$" +holder_did: jsonpath "$.longFormDid" ############################## # Issuance Connection diff --git a/examples/st-oidc4vc/README.md b/examples/st-oid4vci/README.md similarity index 91% rename from examples/st-oidc4vc/README.md rename to examples/st-oid4vci/README.md index 6401164c4b..867ba64c81 100644 --- a/examples/st-oidc4vc/README.md +++ b/examples/st-oid4vci/README.md @@ -11,8 +11,8 @@ Example of the script to install the required packages in a virtual environment: ```shell -python -m venv {path-to-the-project-dir}/open-enterprise-agent/examples/st-oidc4vc/python-env -source {path-to-the-project-dir}/open-enterprise-agent/examples/st-oidc4vc/python-env/bin/activate +python -m venv {path-to-the-project-dir}/open-enterprise-agent/examples/st-oid4vci/python-env +source {path-to-the-project-dir}/open-enterprise-agent/examples/st-oid4vci/python-env/bin/activate pip install requests pyjwt cryptography ``` @@ -28,7 +28,7 @@ sbt docker:publishLocal docker-compose up --build ``` -This builds a custom Keycloak image with OIDC4VC plugin. +This builds a custom Keycloak image with OID4VCI plugin. The Keycloak UI is available at `http://localhost:9980` and the admin username is `admin` with password `admin`. ### 2. Run the issuance demo script diff --git a/examples/st-oidc4vc/bootstrap/01_init_realm.hurl b/examples/st-oid4vci/bootstrap/01_init_realm.hurl similarity index 100% rename from examples/st-oidc4vc/bootstrap/01_init_realm.hurl rename to examples/st-oid4vci/bootstrap/01_init_realm.hurl diff --git a/examples/st-oidc4vc/compose.yaml b/examples/st-oid4vci/compose.yaml similarity index 97% rename from examples/st-oidc4vc/compose.yaml rename to examples/st-oid4vci/compose.yaml index 6e26cfd867..092d0180c1 100644 --- a/examples/st-oidc4vc/compose.yaml +++ b/examples/st-oid4vci/compose.yaml @@ -44,7 +44,7 @@ services: PRISM_NODE_PORT: '50053' REST_SERVICE_URL: http://caddy-issuer:8080/prism-agent SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/input-output-hk/prism-agent:1.31.0-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.32.0-SNAPSHOT restart: always caddy-issuer: configs: @@ -93,7 +93,7 @@ services: volumes: - ./bootstrap:/hurl external-keycloak-issuer: - build: ../../extensions/keycloak-oidc4vc + build: ../../extensions/keycloak-oid4vci command: - start-dev - --features=preview diff --git a/examples/st-oidc4vc/demo.py b/examples/st-oid4vci/demo.py similarity index 97% rename from examples/st-oidc4vc/demo.py rename to examples/st-oid4vci/demo.py index 1f7cb33438..c6484322bd 100755 --- a/examples/st-oidc4vc/demo.py +++ b/examples/st-oid4vci/demo.py @@ -1,7 +1,6 @@ import json import jwt import requests -import threading import time import urllib @@ -66,7 +65,7 @@ def prepare_issuer(): global CREDENTIAL_ISSUER canonical_did = issuer_did["did"] - CREDENTIAL_ISSUER = f"{AGENT_URL}/oidc4vc/{canonical_did}" + CREDENTIAL_ISSUER = f"{AGENT_URL}/oid4vci/issuers/{canonical_did}" print(f"CREDENTIAL_ISSUER: {CREDENTIAL_ISSUER}") @@ -79,7 +78,7 @@ def issuer_create_credential_offer(claims): def holder_get_issuer_metadata(credential_issuer: str): - metadata_url = f"{credential_issuer}/.well-known/openid-credential-issuer" + # metadata_url = f"{credential_issuer}/.well-known/openid-credential-issuer" # TODO: OEA should return these instead of hardcoded values return { "credential_issuer": CREDENTIAL_ISSUER, @@ -175,7 +174,6 @@ def holder_extract_credential_offer(offer_uri: str): def holder_get_credential(credential_endpoint: str, token_response): access_token = token_response["access_token"] c_nonce = token_response["c_nonce"] - c_nonce_expires_in = token_response["c_nonce_expires_in"] # generate proof private_key = ec.generate_private_key(ec.SECP256K1()) diff --git a/examples/st-vault/compose.yaml b/examples/st-vault/compose.yaml index 2aceb24723..f0bfcc4644 100644 --- a/examples/st-vault/compose.yaml +++ b/examples/st-vault/compose.yaml @@ -46,7 +46,7 @@ services: SECRET_STORAGE_BACKEND: vault VAULT_ADDR: http://vault-issuer:8200 VAULT_TOKEN: admin - image: ghcr.io/input-output-hk/prism-agent:1.31.0-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.32.0-SNAPSHOT restart: always caddy-issuer: configs: diff --git a/examples/st/compose.yaml b/examples/st/compose.yaml index 63495c8ae5..614999a55e 100644 --- a/examples/st/compose.yaml +++ b/examples/st/compose.yaml @@ -44,7 +44,7 @@ services: PRISM_NODE_PORT: '50053' REST_SERVICE_URL: http://caddy-issuer:8080/prism-agent SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/input-output-hk/prism-agent:1.31.0-SNAPSHOT + image: ghcr.io/hyperledger/identus-cloud-agent:1.32.0-SNAPSHOT restart: always caddy-issuer: configs: diff --git a/extensions/keycloak-oidc4vc/.dockerignore b/extensions/keycloak-oid4vci/.dockerignore similarity index 100% rename from extensions/keycloak-oidc4vc/.dockerignore rename to extensions/keycloak-oid4vci/.dockerignore diff --git a/extensions/keycloak-oidc4vc/.gitignore b/extensions/keycloak-oid4vci/.gitignore similarity index 100% rename from extensions/keycloak-oidc4vc/.gitignore rename to extensions/keycloak-oid4vci/.gitignore diff --git a/extensions/keycloak-oidc4vc/Dockerfile b/extensions/keycloak-oid4vci/Dockerfile similarity index 100% rename from extensions/keycloak-oidc4vc/Dockerfile rename to extensions/keycloak-oid4vci/Dockerfile diff --git a/extensions/keycloak-oidc4vc/build.sbt b/extensions/keycloak-oid4vci/build.sbt similarity index 100% rename from extensions/keycloak-oidc4vc/build.sbt rename to extensions/keycloak-oid4vci/build.sbt diff --git a/extensions/keycloak-oidc4vc/project/build.properties b/extensions/keycloak-oid4vci/project/build.properties similarity index 100% rename from extensions/keycloak-oidc4vc/project/build.properties rename to extensions/keycloak-oid4vci/project/build.properties diff --git a/extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/IdentusClient.java b/extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/IdentusClient.java similarity index 91% rename from extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/IdentusClient.java rename to extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/IdentusClient.java index 547b59f370..9742665705 100644 --- a/extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/IdentusClient.java +++ b/extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/IdentusClient.java @@ -36,7 +36,7 @@ public static CloseableHttpClient newCloseableHttpClient() { public NonceResponse syncTokenDetails(String issuerState) { try (CloseableHttpClient client = httpClient.get()) { - HttpPost post = new HttpPost(identusUrl + "/oidc4vc/did:prism:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/nonces"); + HttpPost post = new HttpPost(identusUrl + "/oid4vci/issuers/08877304-a111-4fe4-a84c-8666610b1665/nonces"); // TODO: use real issuer URL post.setEntity(new StringEntity(JsonSerialization.writeValueAsString(new NonceRequest(issuerState)), ContentType.APPLICATION_JSON)); return NonceResponse.fromResponse(client.execute(post)); } catch (IOException e) { diff --git a/extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCAuthorizationEndpoint.java b/extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCIAuthorizationEndpoint.java similarity index 71% rename from extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCAuthorizationEndpoint.java rename to extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCIAuthorizationEndpoint.java index 8680a7531a..3978386706 100644 --- a/extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCAuthorizationEndpoint.java +++ b/extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCIAuthorizationEndpoint.java @@ -7,20 +7,20 @@ import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint; import org.keycloak.sessions.AuthenticationSessionModel; -public class OIDC4VCAuthorizationEndpoint extends AuthorizationEndpoint { - private static final Logger logger = Logger.getLogger(OIDC4VCTokenEndpoint.class); +public class OID4VCIAuthorizationEndpoint extends AuthorizationEndpoint { + private static final Logger logger = Logger.getLogger(OID4VCITokenEndpoint.class); private final String issuerState; - public OIDC4VCAuthorizationEndpoint(KeycloakSession session, EventBuilder event) { + public OID4VCIAuthorizationEndpoint(KeycloakSession session, EventBuilder event) { super(session, event); - this.issuerState = session.getContext().getUri().getQueryParameters().getFirst(OIDC4VCConstants.ISSUER_STATE); + this.issuerState = session.getContext().getUri().getQueryParameters().getFirst(OID4VCIConstants.ISSUER_STATE); } @Override protected AuthenticationSessionModel createAuthenticationSession(ClientModel client, String requestState) { AuthenticationSessionModel authSession = super.createAuthenticationSession(client, requestState); - authSession.setClientNote(OIDC4VCConstants.ISSUER_STATE, issuerState); + authSession.setClientNote(OID4VCIConstants.ISSUER_STATE, issuerState); return super.createAuthenticationSession(client, requestState); } } diff --git a/extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCConstants.java b/extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCIConstants.java similarity index 87% rename from extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCConstants.java rename to extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCIConstants.java index f3f7cd3571..8c6f39a6cf 100644 --- a/extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCConstants.java +++ b/extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCIConstants.java @@ -1,6 +1,6 @@ package io.iohk.atala.keycloak; -public class OIDC4VCConstants { +public class OID4VCIConstants { public static final String ISSUER_STATE = "issuer_state"; public static final String C_NONCE = "c_nonce"; public static final String C_NONCE_EXPIRE = "c_nonce_expires_in"; diff --git a/extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCLoginProtocolFactory.java b/extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCILoginProtocolFactory.java similarity index 75% rename from extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCLoginProtocolFactory.java rename to extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCILoginProtocolFactory.java index 88e5c97761..849bd9dfe7 100644 --- a/extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCLoginProtocolFactory.java +++ b/extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCILoginProtocolFactory.java @@ -7,8 +7,8 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; import org.keycloak.protocol.oidc.OIDCProviderConfig; -public class OIDC4VCLoginProtocolFactory extends OIDCLoginProtocolFactory { - private static final Logger logger = Logger.getLogger(OIDC4VCLoginProtocolFactory.class); +public class OID4VCILoginProtocolFactory extends OIDCLoginProtocolFactory { + private static final Logger logger = Logger.getLogger(OID4VCILoginProtocolFactory.class); private OIDCProviderConfig providerConfig; @Override @@ -24,6 +24,6 @@ public void init(Config.Scope config) { @Override public Object createProtocolEndpoint(KeycloakSession session, EventBuilder event) { - return new OIDC4VCLoginProtocolService(session, event, providerConfig); + return new OID4VCILoginProtocolService(session, event, providerConfig); } } diff --git a/extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCLoginProtocolService.java b/extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCILoginProtocolService.java similarity index 66% rename from extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCLoginProtocolService.java rename to extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCILoginProtocolService.java index e7be9f6575..869f292883 100644 --- a/extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCLoginProtocolService.java +++ b/extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCILoginProtocolService.java @@ -8,27 +8,26 @@ import org.keycloak.protocol.oidc.OIDCProviderConfig; import org.keycloak.protocol.oidc.TokenManager; -public class OIDC4VCLoginProtocolService extends OIDCLoginProtocolService { - private static final Logger logger = Logger.getLogger(OIDC4VCLoginProtocolService.class); +public class OID4VCILoginProtocolService extends OIDCLoginProtocolService { + private static final Logger logger = Logger.getLogger(OID4VCILoginProtocolService.class); private final TokenManager tokenManager; private final KeycloakSession session; private final EventBuilder event; - public OIDC4VCLoginProtocolService(KeycloakSession session, EventBuilder event, OIDCProviderConfig providerConfig) { + public OID4VCILoginProtocolService(KeycloakSession session, EventBuilder event, OIDCProviderConfig providerConfig) { super(session, event, providerConfig); this.tokenManager = new TokenManager(); this.session = session; this.event = event; - logger.warn("OIDC4VCLoginProtocolService is created !!!!"); } @Path("auth") public Object auth() { - return new OIDC4VCAuthorizationEndpoint(session, event); + return new OID4VCIAuthorizationEndpoint(session, event); } @Path("token") public Object token() { - return new OIDC4VCTokenEndpoint(session, tokenManager, event); + return new OID4VCITokenEndpoint(session, tokenManager, event); } } diff --git a/extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCTokenEndpoint.java b/extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCITokenEndpoint.java similarity index 85% rename from extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCTokenEndpoint.java rename to extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCITokenEndpoint.java index 25f5b00765..61fa6b61de 100644 --- a/extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/OIDC4VCTokenEndpoint.java +++ b/extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/OID4VCITokenEndpoint.java @@ -16,12 +16,12 @@ import java.util.function.Function; -public class OIDC4VCTokenEndpoint extends TokenEndpoint { - private static final Logger logger = Logger.getLogger(OIDC4VCTokenEndpoint.class); +public class OID4VCITokenEndpoint extends TokenEndpoint { + private static final Logger logger = Logger.getLogger(OID4VCITokenEndpoint.class); private final IdentusClient identusClient; - public OIDC4VCTokenEndpoint(KeycloakSession session, TokenManager tokenManager, EventBuilder event) { + public OID4VCITokenEndpoint(KeycloakSession session, TokenManager tokenManager, EventBuilder event) { super(session, tokenManager, event); this.identusClient = new IdentusClient(); } @@ -29,7 +29,7 @@ public OIDC4VCTokenEndpoint(KeycloakSession session, TokenManager tokenManager, @Override public Response createTokenResponse(UserModel user, UserSessionModel userSession, ClientSessionContext clientSessionCtx, String scopeParam, boolean code, Function clientPolicyContextGenerator) { - String noteKey = AuthorizationEndpoint.LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + OIDC4VCConstants.ISSUER_STATE; + String noteKey = AuthorizationEndpoint.LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + OID4VCIConstants.ISSUER_STATE; String issuerState = clientSessionCtx.getClientSession().getNote(noteKey); if (code && issuerState != null) { @@ -40,8 +40,8 @@ public Response createTokenResponse(UserModel user, UserSessionModel userSession NonceResponse nonceResponse = identusClient.syncTokenDetails(issuerState); AccessTokenResponse responseEntity = (AccessTokenResponse) originalResponse.getEntity(); - responseEntity.setOtherClaims(OIDC4VCConstants.C_NONCE, nonceResponse.getNonce()); - responseEntity.setOtherClaims(OIDC4VCConstants.C_NONCE_EXPIRE, nonceResponse.getNonceExpiresIn()); + responseEntity.setOtherClaims(OID4VCIConstants.C_NONCE, nonceResponse.getNonce()); + responseEntity.setOtherClaims(OID4VCIConstants.C_NONCE_EXPIRE, nonceResponse.getNonceExpiresIn()); return Response.fromResponse(originalResponse) .entity(responseEntity) .build(); diff --git a/extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/model/NonceRequest.java b/extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/model/NonceRequest.java similarity index 100% rename from extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/model/NonceRequest.java rename to extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/model/NonceRequest.java diff --git a/extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/model/NonceResponse.java b/extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/model/NonceResponse.java similarity index 100% rename from extensions/keycloak-oidc4vc/src/main/java/io/iohk/atala/keycloak/model/NonceResponse.java rename to extensions/keycloak-oid4vci/src/main/java/io/iohk/atala/keycloak/model/NonceResponse.java diff --git a/extensions/keycloak-oid4vci/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory b/extensions/keycloak-oid4vci/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory new file mode 100644 index 0000000000..db2d2029e5 --- /dev/null +++ b/extensions/keycloak-oid4vci/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory @@ -0,0 +1 @@ +io.iohk.atala.keycloak.OID4VCILoginProtocolFactory \ No newline at end of file diff --git a/extensions/keycloak-oidc4vc/version.sbt b/extensions/keycloak-oid4vci/version.sbt similarity index 100% rename from extensions/keycloak-oidc4vc/version.sbt rename to extensions/keycloak-oid4vci/version.sbt diff --git a/extensions/keycloak-oidc4vc/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory b/extensions/keycloak-oidc4vc/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory deleted file mode 100644 index 8e93eb2d05..0000000000 --- a/extensions/keycloak-oidc4vc/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory +++ /dev/null @@ -1 +0,0 @@ -io.iohk.atala.keycloak.OIDC4VCLoginProtocolFactory \ No newline at end of file diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oidc4vc/CredentialConfiguration.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oid4vci/CredentialConfiguration.scala similarity index 69% rename from pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oidc4vc/CredentialConfiguration.scala rename to pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oid4vci/CredentialConfiguration.scala index c74befa82b..321665e666 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oidc4vc/CredentialConfiguration.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oid4vci/CredentialConfiguration.scala @@ -1,15 +1,15 @@ -package org.hyperledger.identus.pollux.core.model.oidc4vc +package org.hyperledger.identus.pollux.core.model.oid4vci import org.hyperledger.identus.pollux.core.model.CredentialFormat -import zio.json.ast.Json import java.net.URI +import java.time.Instant final case class CredentialConfiguration( configurationId: String, format: CredentialFormat, schemaId: URI, - dereferencedSchema: Json + createdAt: Instant ) { def scope: String = configurationId } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oidc4vc/CredentialIssuer.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oid4vci/CredentialIssuer.scala similarity index 86% rename from pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oidc4vc/CredentialIssuer.scala rename to pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oid4vci/CredentialIssuer.scala index e5ae1c0ed7..a66e8b3979 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oidc4vc/CredentialIssuer.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oid4vci/CredentialIssuer.scala @@ -1,4 +1,4 @@ -package org.hyperledger.identus.pollux.core.model.oidc4vc +package org.hyperledger.identus.pollux.core.model.oid4vci import java.net.URL import java.time.Instant diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/OID4VCIIssuerMetadataRepository.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/OID4VCIIssuerMetadataRepository.scala new file mode 100644 index 0000000000..1b3fb9a9d6 --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/OID4VCIIssuerMetadataRepository.scala @@ -0,0 +1,24 @@ +package org.hyperledger.identus.pollux.core.repository + +import org.hyperledger.identus.pollux.core.model.oid4vci.CredentialConfiguration +import org.hyperledger.identus.pollux.core.model.oid4vci.CredentialIssuer +import org.hyperledger.identus.shared.models.WalletAccessContext +import zio.* + +import java.net.URL +import java.util.UUID + +trait OID4VCIIssuerMetadataRepository { + def findIssuerById(issuerId: UUID): UIO[Option[CredentialIssuer]] + def createIssuer(issuer: CredentialIssuer): URIO[WalletAccessContext, Unit] + def findWalletIssuers: URIO[WalletAccessContext, Seq[CredentialIssuer]] + def updateIssuer(issuerId: UUID, authorizationServer: Option[URL] = None): URIO[WalletAccessContext, Unit] + def deleteIssuer(issuerId: UUID): URIO[WalletAccessContext, Unit] + def createCredentialConfiguration(issuerId: UUID, config: CredentialConfiguration): URIO[WalletAccessContext, Unit] + def findCredentialConfigurationsByIssuer(issuerId: UUID): UIO[Seq[CredentialConfiguration]] + def findCredentialConfigurationById( + issuerId: UUID, + configurationId: String + ): URIO[WalletAccessContext, Option[CredentialConfiguration]] + def deleteCredentialConfiguration(issuerId: UUID, configurationId: String): URIO[WalletAccessContext, Unit] +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/OIDC4VCIssuerMetadataRepository.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/OIDC4VCIssuerMetadataRepository.scala deleted file mode 100644 index 2742f2bb70..0000000000 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/OIDC4VCIssuerMetadataRepository.scala +++ /dev/null @@ -1,65 +0,0 @@ -package org.hyperledger.identus.pollux.core.repository - -import org.hyperledger.identus.pollux.core.model.oidc4vc.CredentialConfiguration -import org.hyperledger.identus.pollux.core.model.oidc4vc.CredentialIssuer -import org.hyperledger.identus.shared.models.WalletAccessContext -import zio.* - -import java.util.UUID - -trait OIDC4VCIssuerMetadataRepository { - def createIssuer(issuer: CredentialIssuer): URIO[WalletAccessContext, Unit] - def findAllIssuerForWallet: URIO[WalletAccessContext, Seq[CredentialIssuer]] - def findIssuer(issuerId: UUID): UIO[Option[CredentialIssuer]] - - def createCredentialConfiguration(issuerId: UUID, config: CredentialConfiguration): URIO[WalletAccessContext, Unit] - def findAllCredentialConfigurations(issuerId: UUID): UIO[Seq[CredentialConfiguration]] -} - -class InMemoryOIDC4VCIssuerMetadataRepository( - issuerStore: Ref[Map[UUID, CredentialIssuer]], - credentialConfigStore: Ref[Map[(UUID, String), CredentialConfiguration]] -) extends OIDC4VCIssuerMetadataRepository { - - override def createIssuer(issuer: CredentialIssuer): URIO[WalletAccessContext, Unit] = - issuerStore.modify(m => () -> m.updated(issuer.id, issuer)) - - override def findAllIssuerForWallet: URIO[WalletAccessContext, Seq[CredentialIssuer]] = - issuerStore.get.map(_.values.toSeq) - - override def findIssuer(issuerId: UUID): UIO[Option[CredentialIssuer]] = - issuerStore.get.map(_.get(issuerId)) - - override def createCredentialConfiguration( - issuerId: UUID, - config: CredentialConfiguration - ): URIO[WalletAccessContext, Unit] = { - for { - issuerExists <- issuerStore.get.map(_.contains(issuerId)) - configExists <- credentialConfigStore.get.map(_.contains((issuerId, config.configurationId))) - _ <- ZIO - .cond(issuerExists, (), s"Issuer with id $issuerId does not exist") - .orDieWith(Exception(_)) - _ <- ZIO - .cond(!configExists, (), s"Configuration with id ${config.configurationId} already exists") - .orDieWith(Exception(_)) - _ <- credentialConfigStore.update(_.updated((issuerId, config.configurationId), config)) - } yield () - } - - override def findAllCredentialConfigurations( - issuerId: UUID - ): UIO[Seq[CredentialConfiguration]] = - credentialConfigStore.get.map(_.filter(_._1._1 == issuerId).values.toSeq) - -} - -object InMemoryOIDC4VCIssuerMetadataRepository { - def layer: ULayer[OIDC4VCIssuerMetadataRepository] = - ZLayer.fromZIO( - for { - issuerStore <- Ref.make(Map.empty[UUID, CredentialIssuer]) - credentialConfigStore <- Ref.make(Map.empty[(UUID, String), CredentialConfiguration]) - } yield InMemoryOIDC4VCIssuerMetadataRepository(issuerStore, credentialConfigStore) - ) -} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala new file mode 100644 index 0000000000..e4b2b73edd --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala @@ -0,0 +1,182 @@ +package org.hyperledger.identus.pollux.core.service + +import org.hyperledger.identus.pollux.core.model.CredentialFormat +import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.CredentialSchemaParsingError +import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.SchemaError +import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.URISyntaxError +import org.hyperledger.identus.pollux.core.model.oid4vci.CredentialConfiguration +import org.hyperledger.identus.pollux.core.model.oid4vci.CredentialIssuer +import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema +import org.hyperledger.identus.pollux.core.repository.OID4VCIIssuerMetadataRepository +import org.hyperledger.identus.pollux.core.service.OID4VCIIssuerMetadataServiceError.CredentialConfigurationNotFound +import org.hyperledger.identus.pollux.core.service.OID4VCIIssuerMetadataServiceError.InvalidSchemaId +import org.hyperledger.identus.pollux.core.service.OID4VCIIssuerMetadataServiceError.IssuerIdNotFound +import org.hyperledger.identus.pollux.core.service.OID4VCIIssuerMetadataServiceError.UnsupportedCredentialFormat +import org.hyperledger.identus.shared.db.Errors.UnexpectedAffectedRow +import org.hyperledger.identus.shared.models.Failure +import org.hyperledger.identus.shared.models.StatusCode +import org.hyperledger.identus.shared.models.WalletAccessContext +import zio.* + +import java.net.URI +import java.net.URL +import java.util.UUID + +sealed trait OID4VCIIssuerMetadataServiceError( + val statusCode: StatusCode, + val userFacingMessage: String +) extends Failure + +object OID4VCIIssuerMetadataServiceError { + final case class IssuerIdNotFound(issuerId: UUID) + extends OID4VCIIssuerMetadataServiceError( + StatusCode.NotFound, + s"There is no credential issuer matching the provided identifier: issuerId=$issuerId" + ) + + final case class CredentialConfigurationNotFound(issuerId: UUID, configurationId: String) + extends OID4VCIIssuerMetadataServiceError( + StatusCode.NotFound, + s"There is no credential configuration matching the provided identifier: issuerId=$issuerId, configurationId=$configurationId" + ) + + final case class InvalidSchemaId(schemaId: String, msg: String) + extends OID4VCIIssuerMetadataServiceError( + StatusCode.BadRequest, + s"The schemaId $schemaId is not a valid URI syntax: $msg" + ) + + final case class UnsupportedCredentialFormat(format: CredentialFormat) + extends OID4VCIIssuerMetadataServiceError( + StatusCode.BadRequest, + s"Unsupported credential format in OID4VCI protocol: $format" + ) +} + +trait OID4VCIIssuerMetadataService { + def getCredentialIssuer(issuerId: UUID): IO[IssuerIdNotFound, CredentialIssuer] + def createCredentialIssuer(authorizationServer: URL): URIO[WalletAccessContext, CredentialIssuer] + def getCredentialIssuers: URIO[WalletAccessContext, Seq[CredentialIssuer]] + def updateCredentialIssuer( + issuerId: UUID, + authorizationServer: Option[URL] = None + ): ZIO[WalletAccessContext, IssuerIdNotFound, CredentialIssuer] + def deleteCredentialIssuer(issuerId: UUID): ZIO[WalletAccessContext, IssuerIdNotFound, Unit] + def createCredentialConfiguration( + issuerId: UUID, + format: CredentialFormat, + configurationId: String, + schemaId: String + ): ZIO[WalletAccessContext, InvalidSchemaId | UnsupportedCredentialFormat, CredentialConfiguration] + def getCredentialConfigurations( + issuerId: UUID + ): IO[IssuerIdNotFound, Seq[CredentialConfiguration]] + def getCredentialConfigurationById( + issuerId: UUID, + configurationId: String + ): ZIO[WalletAccessContext, CredentialConfigurationNotFound, CredentialConfiguration] + def deleteCredentialConfiguration( + issuerId: UUID, + configurationId: String, + ): ZIO[WalletAccessContext, CredentialConfigurationNotFound, Unit] +} + +class OID4VCIIssuerMetadataServiceImpl(repository: OID4VCIIssuerMetadataRepository, uriDereferencer: URIDereferencer) + extends OID4VCIIssuerMetadataService { + + override def createCredentialIssuer(authorizationServer: URL): URIO[WalletAccessContext, CredentialIssuer] = { + val issuer = CredentialIssuer(authorizationServer) + repository.createIssuer(issuer).as(issuer) + } + + override def getCredentialIssuers: URIO[WalletAccessContext, Seq[CredentialIssuer]] = + repository.findWalletIssuers + + override def getCredentialIssuer(issuerId: UUID): IO[IssuerIdNotFound, CredentialIssuer] = + repository + .findIssuerById(issuerId) + .someOrFail(IssuerIdNotFound(issuerId)) + + override def updateCredentialIssuer( + issuerId: UUID, + authorizationServer: Option[URL] + ): ZIO[WalletAccessContext, IssuerIdNotFound, CredentialIssuer] = + for { + _ <- repository + .updateIssuer(issuerId, authorizationServer = authorizationServer) + .catchSomeDefect { case _: UnexpectedAffectedRow => + ZIO.fail(IssuerIdNotFound(issuerId)) + } + updatedIssuer <- getCredentialIssuer(issuerId) + } yield updatedIssuer + + override def deleteCredentialIssuer(issuerId: UUID): ZIO[WalletAccessContext, IssuerIdNotFound, Unit] = + repository + .deleteIssuer(issuerId) + .catchSomeDefect { case _: UnexpectedAffectedRow => + ZIO.fail(IssuerIdNotFound(issuerId)) + } + + override def createCredentialConfiguration( + issuerId: UUID, + format: CredentialFormat, + configurationId: String, + schemaId: String + ): ZIO[WalletAccessContext, InvalidSchemaId | UnsupportedCredentialFormat, CredentialConfiguration] = { + for { + _ <- format match { + case CredentialFormat.JWT => ZIO.unit + case f => ZIO.fail(UnsupportedCredentialFormat(f)) + } + schemaUri <- ZIO.attempt(new URI(schemaId)).mapError(t => InvalidSchemaId(schemaId, t.getMessage)) + _ <- CredentialSchema + .validSchemaValidator(schemaUri.toString(), uriDereferencer) + .catchAll { + case e: URISyntaxError => ZIO.fail(InvalidSchemaId(schemaId, e.message)) + case _: CredentialSchemaParsingError | _: SchemaError => + ZIO.fail(InvalidSchemaId(schemaId, "The schema URI does not contain a valid schema response")) + case e => ZIO.dieMessage(s"Unexpected error when resolving schema $schemaId: $e") + } + now <- ZIO.clockWith(_.instant) + config = CredentialConfiguration( + configurationId = configurationId, + format = CredentialFormat.JWT, + schemaId = schemaUri, + createdAt = now + ) + _ <- repository.createCredentialConfiguration(issuerId, config) + } yield config + } + + override def getCredentialConfigurations( + issuerId: UUID + ): IO[IssuerIdNotFound, Seq[CredentialConfiguration]] = + repository + .findIssuerById(issuerId) + .someOrFail(IssuerIdNotFound(issuerId)) + .flatMap(_ => repository.findCredentialConfigurationsByIssuer(issuerId)) + + override def getCredentialConfigurationById( + issuerId: UUID, + configurationId: String + ): ZIO[WalletAccessContext, CredentialConfigurationNotFound, CredentialConfiguration] = + repository + .findCredentialConfigurationById(issuerId, configurationId) + .someOrFail(CredentialConfigurationNotFound(issuerId, configurationId)) + + override def deleteCredentialConfiguration( + issuerId: UUID, + configurationId: String + ): ZIO[WalletAccessContext, CredentialConfigurationNotFound, Unit] = + repository + .deleteCredentialConfiguration(issuerId, configurationId) + .catchSomeDefect { case _: UnexpectedAffectedRow => + ZIO.fail(CredentialConfigurationNotFound(issuerId, configurationId)) + } +} + +object OID4VCIIssuerMetadataServiceImpl { + def layer: URLayer[OID4VCIIssuerMetadataRepository & URIDereferencer, OID4VCIIssuerMetadataService] = { + ZLayer.fromFunction(OID4VCIIssuerMetadataServiceImpl(_, _)) + } +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OIDC4VCIssuerMetadataService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OIDC4VCIssuerMetadataService.scala deleted file mode 100644 index 754748ad15..0000000000 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OIDC4VCIssuerMetadataService.scala +++ /dev/null @@ -1,115 +0,0 @@ -package org.hyperledger.identus.pollux.core.service - -import org.hyperledger.identus.pollux.core.model.CredentialFormat -import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.URISyntaxError -import org.hyperledger.identus.pollux.core.model.oidc4vc.CredentialConfiguration -import org.hyperledger.identus.pollux.core.model.oidc4vc.CredentialIssuer -import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema -import org.hyperledger.identus.pollux.core.repository.OIDC4VCIssuerMetadataRepository -import org.hyperledger.identus.pollux.core.service.OIDC4VCIssuerMetadataServiceError.InvalidSchemaId -import org.hyperledger.identus.pollux.core.service.OIDC4VCIssuerMetadataServiceError.IssuerIdNotFound -import org.hyperledger.identus.pollux.core.service.OIDC4VCIssuerMetadataServiceError.UnsupportedCredentialFormat -import org.hyperledger.identus.shared.models.Failure -import org.hyperledger.identus.shared.models.StatusCode -import org.hyperledger.identus.shared.models.WalletAccessContext -import zio.* - -import java.net.URI -import java.net.URL -import java.util.UUID - -sealed trait OIDC4VCIssuerMetadataServiceError( - val statusCode: StatusCode, - val userFacingMessage: String -) extends Failure - -object OIDC4VCIssuerMetadataServiceError { - final case class IssuerIdNotFound(issuerId: UUID) - extends OIDC4VCIssuerMetadataServiceError( - StatusCode.NotFound, - s"There is no credential issuer matching the provided identifier: issuerId=$issuerId" - ) - - final case class InvalidSchemaId(schemaId: String, msg: String) - extends OIDC4VCIssuerMetadataServiceError( - StatusCode.BadRequest, - s"The schemaId $schemaId is not a valid URI syntax: $msg" - ) - - final case class UnsupportedCredentialFormat(format: CredentialFormat) - extends OIDC4VCIssuerMetadataServiceError( - StatusCode.BadRequest, - s"Unsupported credential format in OIDC4VC protocol: $format" - ) -} - -trait OIDC4VCIssuerMetadataService { - def createCredentialIssuer(authorizationServer: URL): URIO[WalletAccessContext, CredentialIssuer] - def getCredentialIssuer(issuerId: UUID): IO[IssuerIdNotFound, CredentialIssuer] - def createCredentialConfiguration( - issuerId: UUID, - format: CredentialFormat, - configurationId: String, - schemaId: String - ): ZIO[WalletAccessContext, InvalidSchemaId | UnsupportedCredentialFormat, CredentialConfiguration] - def listCredentialConfiguration( - issuerId: UUID - ): IO[IssuerIdNotFound, Seq[CredentialConfiguration]] -} - -class OIDC4VCIssuerMetadataServiceImpl(repository: OIDC4VCIssuerMetadataRepository, uriDereferencer: URIDereferencer) - extends OIDC4VCIssuerMetadataService { - - override def createCredentialIssuer(authorizationServer: URL): URIO[WalletAccessContext, CredentialIssuer] = { - val issuer = CredentialIssuer(authorizationServer) - repository.createIssuer(issuer).as(issuer) - } - - override def getCredentialIssuer(issuerId: UUID): IO[IssuerIdNotFound, CredentialIssuer] = - repository - .findIssuer(issuerId) - .someOrFail(IssuerIdNotFound(issuerId)) - - override def createCredentialConfiguration( - issuerId: UUID, - format: CredentialFormat, - configurationId: String, - schemaId: String - ): ZIO[WalletAccessContext, InvalidSchemaId | UnsupportedCredentialFormat, CredentialConfiguration] = { - for { - _ <- format match { - case CredentialFormat.JWT => ZIO.unit - case f => ZIO.fail(UnsupportedCredentialFormat(f)) - } - schemaUri <- ZIO.attempt(new URI(schemaId)).mapError(t => InvalidSchemaId(schemaId, t.getMessage)) - jsonSchema <- CredentialSchema - .resolveJWTSchema(schemaUri, uriDereferencer) - .catchAll { - case e: URISyntaxError => ZIO.fail(InvalidSchemaId(schemaId, e.message)) - case e => ZIO.dieMessage(s"Unexpected error when resolving schema $schemaId: $e") - } - config = CredentialConfiguration( - configurationId = configurationId, - format = CredentialFormat.JWT, - schemaId = schemaUri, - dereferencedSchema = jsonSchema - ) - _ <- repository.createCredentialConfiguration(issuerId, config) - } yield config - } - - override def listCredentialConfiguration( - issuerId: UUID - ): IO[IssuerIdNotFound, Seq[CredentialConfiguration]] = - repository - .findIssuer(issuerId) - .someOrFail(IssuerIdNotFound(issuerId)) - .flatMap(_ => repository.findAllCredentialConfigurations(issuerId)) - -} - -object OIDC4VCIssuerMetadataServiceImpl { - def layer: URLayer[OIDC4VCIssuerMetadataRepository & URIDereferencer, OIDC4VCIssuerMetadataService] = { - ZLayer.fromFunction(OIDC4VCIssuerMetadataServiceImpl(_, _)) - } -} diff --git a/pollux/sql-doobie/src/main/resources/sql/pollux/V20__add_issuer_metadata.sql b/pollux/sql-doobie/src/main/resources/sql/pollux/V20__add_issuer_metadata.sql new file mode 100644 index 0000000000..76d35ac219 --- /dev/null +++ b/pollux/sql-doobie/src/main/resources/sql/pollux/V20__add_issuer_metadata.sql @@ -0,0 +1,36 @@ +CREATE TABLE public.issuer_metadata ( + id UUID PRIMARY KEY, + authorization_server VARCHAR(500) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL, + wallet_id UUID NOT NULL +); + +CREATE TABLE public.issuer_credential_configuration ( + configuration_id VARCHAR(100) NOT NULL, + issuer_id UUID NOT NULL, + format VARCHAR(9) NOT NULL, + schema_id VARCHAR(500) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + UNIQUE (configuration_id, issuer_id), + CONSTRAINT fk_issuer FOREIGN KEY (issuer_id) REFERENCES public.issuer_metadata(id) ON DELETE CASCADE +); + +ALTER TABLE public.issuer_metadata + ENABLE ROW LEVEL SECURITY; + +ALTER TABLE public.issuer_credential_configuration + ENABLE ROW LEVEL SECURITY; + +CREATE POLICY issuer_metadata_wallet_isolation + ON public.issuer_metadata + USING (wallet_id = current_setting('app.current_wallet_id')::UUID); + +CREATE POLICY issuer_credential_configuration_wallet_isolation + ON public.issuer_credential_configuration + USING ( + EXISTS (SELECT 1 + FROM public.issuer_metadata AS im + WHERE im.wallet_id = current_setting('app.current_wallet_id')::UUID + AND im.id = public.issuer_credential_configuration.issuer_id) + ); diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala index 10064bf2e5..665bf8cf30 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala @@ -4,14 +4,13 @@ import doobie.util.{Get, Put} import org.hyperledger.identus.castor.core.model.did.{CanonicalPrismDID, PrismDID} import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.vc.jwt.StatusPurpose -import org.hyperledger.identus.shared.models.WalletId + +import java.net.URI +import java.net.URL given didCommIDGet: Get[DidCommID] = Get[String].map(DidCommID(_)) given didCommIDPut: Put[DidCommID] = Put[String].contramap(_.value) -given walletIdGet: Get[WalletId] = Get[String].map(WalletId.fromUUIDString) -given walletIdPut: Put[WalletId] = Put[String].contramap(_.toString) - given prismDIDGet: Get[CanonicalPrismDID] = Get[String].map(s => PrismDID.fromString(s).fold(e => throw RuntimeException(e), _.asCanonical)) given prismDIDPut: Put[CanonicalPrismDID] = Put[String].contramap(_.toString) @@ -26,3 +25,12 @@ given statusPurposePut: Put[StatusPurpose] = Put[String].contramap { case StatusPurpose.Revocation => StatusPurpose.Revocation.str case StatusPurpose.Suspension => StatusPurpose.Suspension.str } + +given urlGet: Get[URL] = Get[String].map(s => URI.create(s).toURL()) +given urlPut: Put[URL] = Put[String].contramap(_.toString()) + +given uriGet: Get[URI] = Get[String].map(s => URI.create(s)) +given uriPut: Put[URI] = Put[String].contramap(_.toString()) + +given credFormatGet: Get[CredentialFormat] = Get[String].map(CredentialFormat.valueOf) +given credFormatPut: Put[CredentialFormat] = Put[String].contramap(_.toString()) diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala index 2d2884e3f9..ba463f7082 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala @@ -10,7 +10,7 @@ import org.hyperledger.identus.castor.core.model.did.* import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.core.repository.CredentialStatusListRepository import org.hyperledger.identus.shared.db.ContextAwareTask -import org.hyperledger.identus.shared.db.Implicits.* +import org.hyperledger.identus.shared.db.Implicits.{*, given} import org.hyperledger.identus.pollux.vc.jwt.revocation.BitStringError.* import zio.* import zio.interop.catz.* diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcOID4VCIIssuerMetadataRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcOID4VCIIssuerMetadataRepository.scala new file mode 100644 index 0000000000..794b72c1df --- /dev/null +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcOID4VCIIssuerMetadataRepository.scala @@ -0,0 +1,197 @@ +package org.hyperledger.identus.pollux.sql.repository + +import doobie.* +import doobie.implicits.* +import doobie.postgres.implicits.* +import doobie.util.transactor.Transactor +import org.hyperledger.identus.pollux.core.model.oid4vci.CredentialConfiguration +import org.hyperledger.identus.pollux.core.model.oid4vci.CredentialIssuer +import org.hyperledger.identus.pollux.core.repository.OID4VCIIssuerMetadataRepository +import org.hyperledger.identus.shared.db.ContextAwareTask +import org.hyperledger.identus.shared.db.Implicits.* +import org.hyperledger.identus.shared.models.WalletAccessContext +import zio.* +import zio.interop.catz.* + +import java.net.URL +import java.time.Instant +import java.util.UUID + +class JdbcOID4VCIIssuerMetadataRepository(xa: Transactor[ContextAwareTask], xb: Transactor[Task]) + extends OID4VCIIssuerMetadataRepository { + + override def findIssuerById(issuerId: UUID): UIO[Option[CredentialIssuer]] = { + val cxnIO = sql""" + |SELECT + | id, + | authorization_server, + | created_at, + | updated_at + |FROM public.issuer_metadata + |WHERE id = $issuerId + """.stripMargin + .query[CredentialIssuer] + .option + + cxnIO + .transact(xb) + .orDie + } + + override def findWalletIssuers: URIO[WalletAccessContext, Seq[CredentialIssuer]] = { + val cxnIO = sql""" + |SELECT + | id, + | authorization_server, + | created_at, + | updated_at + |FROM public.issuer_metadata + """.stripMargin + .query[CredentialIssuer] + .to[Seq] + + cxnIO + .transactWallet(xa) + .orDie + } + + override def createIssuer(issuer: CredentialIssuer): URIO[WalletAccessContext, Unit] = { + val cxnIO = sql""" + |INSERT INTO public.issuer_metadata ( + | id, + | authorization_server, + | created_at, + | updated_at, + | wallet_id + |) VALUES ( + | ${issuer.id}, + | ${issuer.authorizationServer}, + | ${issuer.createdAt}, + | ${issuer.updatedAt}, + | current_setting('app.current_wallet_id')::UUID + |) + """.stripMargin.update + + cxnIO.run + .transactWallet(xa) + .ensureOneAffectedRowOrDie + } + + override def updateIssuer( + issuerId: UUID, + authorizationServer: Option[URL] + ): URIO[WalletAccessContext, Unit] = { + val setFr = (now: Instant) => + Fragments.setOpt( + Some(fr"updated_at = $now"), + authorizationServer.map(url => fr"authorization_server = $url") + ) + val cxnIO = (setFr: Fragment) => sql""" + |UPDATE public.issuer_metadata + |$setFr + |WHERE id = $issuerId + """.stripMargin.update + + for { + now <- ZIO.clockWith(_.instant) + _ <- cxnIO(setFr(now)).run + .transactWallet(xa) + .ensureOneAffectedRowOrDie + } yield () + } + + override def deleteIssuer(issuerId: UUID): URIO[WalletAccessContext, Unit] = { + val cxnIO = sql""" + | DELETE FROM public.issuer_metadata + | WHERE id = $issuerId + """.stripMargin.update + + cxnIO.run + .transactWallet(xa) + .ensureOneAffectedRowOrDie + } + + override def createCredentialConfiguration( + issuerId: UUID, + config: CredentialConfiguration + ): URIO[WalletAccessContext, Unit] = { + val cxnIO = sql""" + |INSERT INTO public.issuer_credential_configuration ( + | configuration_id, + | issuer_id, + | format, + | schema_id, + | created_at + |) VALUES ( + | ${config.configurationId}, + | ${issuerId}, + | ${config.format}, + | ${config.schemaId}, + | ${config.createdAt} + |) + """.stripMargin.update + + cxnIO.run + .transactWallet(xa) + .ensureOneAffectedRowOrDie + } + + override def findCredentialConfigurationsByIssuer(issuerId: UUID): UIO[Seq[CredentialConfiguration]] = { + val cxnIO = sql""" + |SELECT + | configuration_id, + | format, + | schema_id, + | created_at + |FROM public.issuer_credential_configuration + |WHERE issuer_id = $issuerId + """.stripMargin + .query[CredentialConfiguration] + .to[Seq] + + cxnIO + .transact(xb) + .orDie + } + + override def findCredentialConfigurationById( + issuerId: UUID, + configurationId: String + ): URIO[WalletAccessContext, Option[CredentialConfiguration]] = { + val cxnIO = sql""" + |SELECT + | configuration_id, + | format, + | schema_id, + | created_at + |FROM public.issuer_credential_configuration + |WHERE issuer_id = $issuerId AND configuration_id = $configurationId + """.stripMargin + .query[CredentialConfiguration] + .option + + cxnIO + .transactWallet(xa) + .orDie + } + + override def deleteCredentialConfiguration( + issuerId: UUID, + configurationId: String + ): URIO[WalletAccessContext, Unit] = { + val cxnIO = sql""" + | DELETE FROM public.issuer_credential_configuration + | WHERE issuer_id = $issuerId AND configuration_id = $configurationId + """.stripMargin.update + + cxnIO.run + .transactWallet(xa) + .ensureOneAffectedRowOrDie + } + +} + +object JdbcOID4VCIIssuerMetadataRepository { + val layer: URLayer[Transactor[ContextAwareTask] & Transactor[Task], OID4VCIIssuerMetadataRepository] = + ZLayer.fromFunction(new JdbcOID4VCIIssuerMetadataRepository(_, _)) +} diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/db/ContextAwareTask.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/db/ContextAwareTask.scala index 239e2b1c70..7d3aa5a9ee 100644 --- a/shared/core/src/main/scala/org/hyperledger/identus/shared/db/ContextAwareTask.scala +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/db/ContextAwareTask.scala @@ -14,6 +14,10 @@ import java.util.UUID trait ContextAware type ContextAwareTask[T] = Task[T] with ContextAware +object Errors { + final case class UnexpectedAffectedRow(count: Int) extends RuntimeException(s"Unexpected affected row count: $count") +} + object Implicits { given walletIdGet: Get[WalletId] = Get[UUID].map(WalletId.fromUUID) @@ -44,10 +48,10 @@ object Implicits { } - extension [Int](ma: RIO[WalletAccessContext, Int]) { + extension (ma: RIO[WalletAccessContext, Int]) { def ensureOneAffectedRowOrDie: URIO[WalletAccessContext, Unit] = ma.flatMap { case 1 => ZIO.unit - case count => ZIO.fail(RuntimeException(s"Unexpected affected row count: $count")) + case count => ZIO.fail(Errors.UnexpectedAffectedRow(count)) }.orDie }