diff --git a/ledger-service/http-json/BUILD.bazel b/ledger-service/http-json/BUILD.bazel index e92a63d6b9d8..81eb4d5a0399 100644 --- a/ledger-service/http-json/BUILD.bazel +++ b/ledger-service/http-json/BUILD.bazel @@ -65,6 +65,7 @@ hj_scalacopts = lf_scalacopts + [ "//ledger-service/jwt", "//ledger-service/lf-value-json", "//ledger-service/utils", + "//ledger/error", "//ledger/ledger-api-auth", "//ledger/ledger-api-common", "//ledger/ledger-resources", @@ -253,6 +254,7 @@ daml_compile( "//ledger-service/jwt", "//ledger-service/lf-value-json", "//ledger-service/utils", + "//ledger/error", "//ledger/ledger-api-common", "//libs-scala/contextualized-logging", "//libs-scala/db-utils", @@ -312,6 +314,7 @@ alias( "//ledger-api/rs-grpc-bridge", "//ledger-api/testing-utils", "//ledger-service/fetch-contracts", + "//ledger/error", "//ledger/ledger-resources", "//ledger/metrics", "//ledger/sandbox-common", diff --git a/ledger-service/http-json/src/it/scala/http/HttpServiceIntegrationTest.scala b/ledger-service/http-json/src/it/scala/http/HttpServiceIntegrationTest.scala index c0a42d635fdd..76e1d3e19919 100644 --- a/ledger-service/http-json/src/it/scala/http/HttpServiceIntegrationTest.scala +++ b/ledger-service/http-json/src/it/scala/http/HttpServiceIntegrationTest.scala @@ -155,7 +155,7 @@ abstract class HttpServiceIntegrationTest val Status = StatusCodes.BadRequest discard { exerciseTest._1 should ===(Status) } inside(exerciseTest._2.convertTo[domain.ErrorResponse]) { - case domain.ErrorResponse(Seq(lookup), None, Status) => + case domain.ErrorResponse(Seq(lookup), None, Status, _) => lookup should include regex raw"Cannot resolve Template Key type, given: TemplateId\([0-9a-f]{64},IIou,IIou\)" } } diff --git a/ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala b/ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala index 1a7f9fc05635..6685d8d74ab9 100644 --- a/ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala +++ b/ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala @@ -7,6 +7,7 @@ import java.time.{Instant, LocalDate} import akka.http.scaladsl.Http import akka.http.scaladsl.model._ import com.daml.api.util.TimestampConversion +import com.daml.error.utils.ErrorDetails.{ErrorInfoDetail, RequestInfoDetail, ResourceInfoDetail} import com.daml.lf.data.Ref import com.daml.http.domain.ContractId import com.daml.http.domain.TemplateId.OptionalPkg @@ -347,14 +348,15 @@ abstract class AbstractHttpServiceIntegrationTestTokenIndependent encoder, headers, ).map { response => - inside(response) { case domain.ErrorResponse(errors, warnings, StatusCodes.BadRequest) => - errors shouldBe List(ErrorMessages.cannotResolveAnyTemplateId) - inside(warnings) { case Some(domain.UnknownTemplateIds(unknownTemplateIds)) => - unknownTemplateIds.toSet shouldBe Set( - domain.TemplateId(None, "AAA", "BBB"), - domain.TemplateId(None, "XXX", "YYY"), - ) - } + inside(response) { + case domain.ErrorResponse(errors, warnings, StatusCodes.BadRequest, _) => + errors shouldBe List(ErrorMessages.cannotResolveAnyTemplateId) + inside(warnings) { case Some(domain.UnknownTemplateIds(unknownTemplateIds)) => + unknownTemplateIds.toSet shouldBe Set( + domain.TemplateId(None, "AAA", "BBB"), + domain.TemplateId(None, "XXX", "YYY"), + ) + } } } } @@ -494,6 +496,7 @@ abstract class AbstractHttpServiceIntegrationTestTokenIndependent Seq(_), Some(domain.UnknownTemplateIds(Seq(TpId.IIou.IIou))), StatusCodes.BadRequest, + _, ) => succeed } @@ -704,6 +707,28 @@ abstract class AbstractHttpServiceIntegrationTestTokenIndependent expectedOneErrorMessage(output) should include( s"Contract could not be found with id $contractIdString" ) + val ledgerApiError = + output.asJsObject.fields("ledgerApiError").convertTo[domain.LedgerApiError] + ledgerApiError.message should include("CONTRACT_NOT_FOUND") + ledgerApiError.message should include( + "Contract could not be found with id 000000000000000000000000000000000000000000000000000000000000000000" + ) + import org.scalatest.Inspectors._ + forExactly(1, ledgerApiError.details) { + case ErrorInfoDetail(errorCodeId, _) => + errorCodeId shouldBe "CONTRACT_NOT_FOUND" + case _ => fail() + } + forExactly(1, ledgerApiError.details) { + case RequestInfoDetail(_) => succeed + case _ => fail() + } + forExactly(1, ledgerApiError.details) { + case ResourceInfoDetail(name, typ) => + name shouldBe "000000000000000000000000000000000000000000000000000000000000000000" + typ shouldBe "CONTRACT_ID" + case _ => fail() + } }: Future[Assertion] } @@ -951,7 +976,7 @@ abstract class AbstractHttpServiceIntegrationTestTokenIndependent ).flatMap { case (status, output) => status shouldBe StatusCodes.BadRequest inside(decode1[domain.SyncResponse, List[domain.PartyDetails]](output)) { - case \/-(domain.ErrorResponse(List(error), None, StatusCodes.BadRequest)) => + case \/-(domain.ErrorResponse(List(error), None, StatusCodes.BadRequest, _)) => error should include("Daml-LF Party is empty") } }: Future[Assertion] diff --git a/ledger-service/http-json/src/main/scala/com/digitalasset/http/CommandService.scala b/ledger-service/http-json/src/main/scala/com/digitalasset/http/CommandService.scala index 494a17c7efa1..77b688af42e4 100644 --- a/ledger-service/http-json/src/main/scala/com/digitalasset/http/CommandService.scala +++ b/ledger-service/http-json/src/main/scala/com/digitalasset/http/CommandService.scala @@ -313,7 +313,7 @@ object CommandService { id: Grpc.Category.PermissionDenied \/ Grpc.Category.InvalidArgument, message: String, ) extends Error - final case class GrpcError(status: io.grpc.Status) extends Error + final case class GrpcError(status: com.google.rpc.Status) extends Error final case class InternalError(id: Option[Symbol], error: Throwable) extends Error object InternalError { def apply(id: Option[Symbol], message: String): InternalError = diff --git a/ledger-service/http-json/src/main/scala/com/digitalasset/http/Endpoints.scala b/ledger-service/http-json/src/main/scala/com/digitalasset/http/Endpoints.scala index 34706568e5c9..3938a016a493 100644 --- a/ledger-service/http-json/src/main/scala/com/digitalasset/http/Endpoints.scala +++ b/ledger-service/http-json/src/main/scala/com/digitalasset/http/Endpoints.scala @@ -522,7 +522,7 @@ object Endpoints { ) ) case CommandService.GrpcError(status) => - ParticipantServerError(status.getCode, Option(status.getDescription)) + ParticipantServerError(status) case CommandService.ClientError(-\/(Category.PermissionDenied), message) => Unauthorized(message) case CommandService.ClientError(\/-(Category.InvalidArgument), message) => diff --git a/ledger-service/http-json/src/main/scala/com/digitalasset/http/EndpointsCompanion.scala b/ledger-service/http-json/src/main/scala/com/digitalasset/http/EndpointsCompanion.scala index 62cfe82e5cb0..91f5446314ef 100644 --- a/ledger-service/http-json/src/main/scala/com/digitalasset/http/EndpointsCompanion.scala +++ b/ledger-service/http-json/src/main/scala/com/digitalasset/http/EndpointsCompanion.scala @@ -7,7 +7,7 @@ import akka.http.scaladsl.model._ import akka.http.scaladsl.server.RouteResult.Complete import akka.http.scaladsl.server.{RequestContext, Route} import akka.util.ByteString -import com.daml.http.domain.{JwtPayload, JwtPayloadLedgerIdOnly, JwtWritePayload} +import com.daml.http.domain.{JwtPayload, JwtPayloadLedgerIdOnly, JwtWritePayload, LedgerApiError} import com.daml.http.json.SprayJson import com.daml.http.util.Logging.{InstanceUUID, RequestID, extendWithRequestIdLogCtx} import util.GrpcHttpErrorCodes._ @@ -20,12 +20,15 @@ import com.daml.ledger.api.auth.{ } import com.daml.ledger.api.domain.UserRight import UserRight.{CanActAs, CanReadAs} +import com.daml.error.utils.ErrorDetails +import com.daml.error.utils.ErrorDetails.ErrorDetail import com.daml.ledger.api.refinements.{ApiTypes => lar} import com.daml.ledger.client.services.admin.UserManagementClient import com.daml.ledger.client.services.identity.LedgerIdentityClient import com.daml.lf.data.Ref.UserId import com.daml.logging.{ContextualizedLogger, LoggingContextOf} -import io.grpc.Status.{Code => GrpcCode} +import com.google.rpc.{Code => GrpcCode} +import com.google.rpc.Status import scalaz.syntax.std.option._ import scalaz.{-\/, EitherT, Monad, NonEmptyList, Show, \/, \/-} import spray.json.JsValue @@ -45,8 +48,20 @@ object EndpointsCompanion { final case class ServerError(message: Throwable) extends Error - final case class ParticipantServerError(grpcStatus: GrpcCode, description: Option[String]) - extends Error + final case class ParticipantServerError( + grpcStatus: GrpcCode, + description: String, + details: Seq[ErrorDetail], + ) extends Error + + object ParticipantServerError { + def apply(status: Status): ParticipantServerError = + ParticipantServerError( + com.google.rpc.Code.forNumber(status.getCode), + status.getMessage, + ErrorDetails.from(status), + ) + } final case class NotFound(message: String) extends Error @@ -58,16 +73,15 @@ object EndpointsCompanion { object Error { implicit val ShowInstance: Show[Error] = Show shows { case InvalidUserInput(e) => s"Endpoints.InvalidUserInput: ${e: String}" - case ParticipantServerError(s, d) => - s"Endpoints.ParticipantServerError: ${s: GrpcCode}${d.cata((": " + _), "")}" + case ParticipantServerError(grpcStatus, description, _) => + s"Endpoints.ParticipantServerError: $grpcStatus: $description" case ServerError(e) => s"Endpoints.ServerError: ${e.getMessage: String}" case Unauthorized(e) => s"Endpoints.Unauthorized: ${e: String}" case NotFound(e) => s"Endpoints.NotFound: ${e: String}" } def fromThrowable: Throwable PartialFunction Error = { - case LedgerClientJwt.Grpc.StatusEnvelope(status) => - ParticipantServerError(status.getCode, Option(status.getDescription)) + case LedgerClientJwt.Grpc.StatusEnvelope(status) => ParticipantServerError(status) case NonFatal(t) => ServerError(t) } } @@ -245,17 +259,37 @@ object EndpointsCompanion { private[http] def errorResponse( error: Error )(implicit lc: LoggingContextOf[InstanceUUID with RequestID]): domain.ErrorResponse = { - val (status, errorMsg): (StatusCode, String) = error match { - case InvalidUserInput(e) => StatusCodes.BadRequest -> e - case ParticipantServerError(grpcStatus, d) => - grpcStatus.asAkkaHttpForJsonApi -> s"$grpcStatus${d.cata((": " + _), "")}" + def mkErrorResponse( + status: StatusCode, + error: String, + ledgerApiError: Option[LedgerApiError] = None, + ) = + domain.ErrorResponse( + errors = List(error), + warnings = None, + status = status, + ledgerApiError = ledgerApiError, + ) + error match { + case InvalidUserInput(e) => mkErrorResponse(StatusCodes.BadRequest, e) + case ParticipantServerError(grpcStatus, description, details) => + val ledgerApiError = + domain.LedgerApiError( + code = grpcStatus.getNumber, + message = description, + details = details, + ) + mkErrorResponse( + grpcStatus.asAkkaHttpForJsonApi, + s"$grpcStatus: $description", + Some(ledgerApiError), + ) case ServerError(reason) => logger.error(s"Internal server error occured", reason) - StatusCodes.InternalServerError -> "HTTP JSON API Server Error" - case Unauthorized(e) => StatusCodes.Unauthorized -> e - case NotFound(e) => StatusCodes.NotFound -> e + mkErrorResponse(StatusCodes.InternalServerError, "HTTP JSON API Server Error") + case Unauthorized(e) => mkErrorResponse(StatusCodes.Unauthorized, e) + case NotFound(e) => mkErrorResponse(StatusCodes.NotFound, e) } - domain.ErrorResponse(errors = List(errorMsg), warnings = None, status = status) } private[http] def httpResponse(status: StatusCode, data: JsValue): HttpResponse = { diff --git a/ledger-service/http-json/src/main/scala/com/digitalasset/http/LedgerClientJwt.scala b/ledger-service/http-json/src/main/scala/com/digitalasset/http/LedgerClientJwt.scala index f648fb6e6278..f99e3ab8d292 100644 --- a/ledger-service/http-json/src/main/scala/com/digitalasset/http/LedgerClientJwt.scala +++ b/ledger-service/http-json/src/main/scala/com/digitalasset/http/LedgerClientJwt.scala @@ -22,17 +22,18 @@ import com.daml.ledger.client.withoutledgerid.{LedgerClient => DamlLedgerClient} import com.daml.lf.data.Ref import com.daml.logging.{ContextualizedLogger, LoggingContextOf} import com.google.protobuf -import io.grpc.Status, Status.Code, Code.{values => _, _} -import scalaz.{OneAnd, \/, -\/} +import scalaz.{-\/, OneAnd, \/} import scalaz.syntax.std.boolean._ -import scala.concurrent.{ExecutionContext => EC, Future} +import scala.concurrent.{Future, ExecutionContext => EC} import scala.util.control.NonFatal import com.daml.ledger.api.{domain => LedgerApiDomain} import com.daml.ledger.api.v1.admin.metering_report_service.{ GetMeteringReportRequest, GetMeteringReportResponse, } +import com.google.rpc.{Code, Status} +import io.grpc.protobuf.StatusProto object LedgerClientJwt { import Grpc.EFuture, Grpc.Category._ @@ -199,13 +200,13 @@ object LedgerClientJwt { def listKnownParties(client: DamlLedgerClient)(implicit ec: EC): ListKnownParties = jwt => client.partyManagementClient.listKnownParties(bearer(jwt)).requireHandling { - case PERMISSION_DENIED => PermissionDenied + case Code.PERMISSION_DENIED => PermissionDenied } def getParties(client: DamlLedgerClient)(implicit ec: EC): GetParties = (jwt, partyIds) => client.partyManagementClient.getParties(partyIds, bearer(jwt)).requireHandling { - case PERMISSION_DENIED => PermissionDenied + case Code.PERMISSION_DENIED => PermissionDenied } def allocateParty(client: DamlLedgerClient): AllocateParty = @@ -253,9 +254,14 @@ object LedgerClientJwt { private[http] object StatusEnvelope { def unapply(t: Throwable): Option[Status] = t match { case NonFatal(t) => - val s = Status fromThrowable t - // fromThrowable uses UNKNOWN if it didn't find one - (s.getCode != UNKNOWN) option s + val status = StatusProto fromThrowable t + if (status == null) None + else { + // fromThrowable uses UNKNOWN if it didn't find one + val code = com.google.rpc.Code.forNumber(status.getCode) + if (code == null) None + else (code != com.google.rpc.Code.UNKNOWN) option status + } case _ => None } } @@ -276,8 +282,8 @@ object LedgerClientJwt { // think of it more like a Venn diagram private[LedgerClientJwt] val submitErrors: Code PartialFunction SubmitError = { - case PERMISSION_DENIED => PermissionDenied - case INVALID_ARGUMENT => InvalidArgument + case Code.PERMISSION_DENIED => PermissionDenied + case Code.INVALID_ARGUMENT => InvalidArgument } private[LedgerClientJwt] implicit final class `Future Status Category ops`[A]( @@ -286,7 +292,7 @@ object LedgerClientJwt { def requireHandling[E](c: Code PartialFunction E)(implicit ec: EC): EFuture[E, A] = fa map \/.right[Error[E], A] recover Function.unlift { case StatusEnvelope(status) => - c.lift(status.getCode) map (e => -\/(Error(e, status.asRuntimeException.getMessage))) + c.lift(Code.forNumber(status.getCode)) map (e => -\/(Error(e, status.getMessage))) case _ => None } } diff --git a/ledger-service/http-json/src/main/scala/com/digitalasset/http/domain.scala b/ledger-service/http-json/src/main/scala/com/digitalasset/http/domain.scala index 1fe166e6db4f..937279e8514d 100644 --- a/ledger-service/http-json/src/main/scala/com/digitalasset/http/domain.scala +++ b/ledger-service/http-json/src/main/scala/com/digitalasset/http/domain.scala @@ -49,6 +49,7 @@ package object domain extends com.daml.fetchcontracts.domain.Aliases { package domain { + import com.daml.error.utils.ErrorDetails.ErrorDetail import com.daml.fetchcontracts.domain.`fc domain ErrorOps` trait JwtPayloadTag @@ -562,10 +563,17 @@ package domain { status: StatusCode = StatusCodes.OK, ) extends SyncResponse[R] + final case class LedgerApiError( + code: Int, + message: String, + details: Seq[ErrorDetail], + ) + final case class ErrorResponse( errors: List[String], warnings: Option[ServiceWarning], status: StatusCode, + ledgerApiError: Option[LedgerApiError] = None, ) extends SyncResponse[Nothing] object OkResponse { diff --git a/ledger-service/http-json/src/main/scala/com/digitalasset/http/json/JsonProtocol.scala b/ledger-service/http-json/src/main/scala/com/digitalasset/http/json/JsonProtocol.scala index 6e1931849c33..015716211388 100644 --- a/ledger-service/http-json/src/main/scala/com/digitalasset/http/json/JsonProtocol.scala +++ b/ledger-service/http-json/src/main/scala/com/digitalasset/http/json/JsonProtocol.scala @@ -4,6 +4,13 @@ package com.daml.http.json import akka.http.scaladsl.model.StatusCode +import com.daml.error.utils.ErrorDetails.{ + ErrorDetail, + ErrorInfoDetail, + RequestInfoDetail, + ResourceInfoDetail, + RetryInfoDetail, +} import com.daml.http.domain import com.daml.http.domain.TemplateId import com.daml.ledger.api.refinements.{ApiTypes => lar} @@ -451,8 +458,72 @@ object JsonProtocol extends JsonProtocolLow { implicit def OkResponseFormat[R: JsonFormat]: RootJsonFormat[domain.OkResponse[R]] = jsonFormat3(domain.OkResponse[R]) + implicit val ResourceInfoDetailFormat: RootJsonFormat[ResourceInfoDetail] = jsonFormat2( + ResourceInfoDetail + ) + implicit val ErrorInfoDetailFormat: RootJsonFormat[ErrorInfoDetail] = jsonFormat2( + ErrorInfoDetail + ) + + implicit val RetryInfoDetailFormat: RootJsonFormat[RetryInfoDetail] = + new RootJsonFormat[RetryInfoDetail] { + override def write(obj: RetryInfoDetail): JsValue = JsObject( + "duration" -> JsNumber(obj.duration.toNanos) + ) + override def read(json: JsValue): RetryInfoDetail = json match { + case JsObject(fields) => + fields + .get("duration") + .collect { case JsNumber(nanos) => + val duration = scala.concurrent.duration.Duration.fromNanos(nanos.toLongExact) + RetryInfoDetail(duration) + } + .getOrElse(deserializationError("Expected field duration of type number")) + case _ => deserializationError("Expected an object with field duration of type number") + } + } + + implicit val RequestInfoDetailFormat: RootJsonFormat[RequestInfoDetail] = jsonFormat1( + RequestInfoDetail + ) + + implicit val ErrorDetailsFormat: RootJsonFormat[ErrorDetail] = + new RootJsonFormat[ErrorDetail] { + override def write(obj: ErrorDetail): JsValue = { + val (jsValue, name) = obj match { + case a: ResourceInfoDetail => ResourceInfoDetailFormat.write(a) -> "ResourceInfoDetail" + case a: ErrorInfoDetail => ErrorInfoDetailFormat.write(a) -> "ErrorInfoDetail" + case a: RetryInfoDetail => RetryInfoDetailFormat.write(a) -> "RetryInfoDetail" + case a: RequestInfoDetail => RequestInfoDetailFormat.write(a) -> "RequestInfoDetail" + } + val fields = jsValue.asJsObject.fields ++ Map("@type" -> JsString(name)) + JsObject(fields.toList: _*) + } + override def read(json: JsValue): ErrorDetail = { + val obj = json.asJsObject + obj.fields + .get("@type") + .map { + case JsString(value) => value + case value => deserializationError(s"Expected string but got $value") + } + .map { + case "ResourceInfoDetail" => ResourceInfoDetailFormat.read(obj) + case "ErrorInfoDetail" => ErrorInfoDetailFormat.read(obj) + case "RetryInfoDetail" => RetryInfoDetailFormat.read(obj) + case "RequestInfoDetail" => + RequestInfoDetailFormat.read(obj) + case name => + deserializationError(s"Unknown value for @type field: $name") + } + }.getOrElse(deserializationError("Field @type is required for decoding ErrorDetail")) + } + + implicit val LedgerApiErrorFormat: RootJsonFormat[domain.LedgerApiError] = + jsonFormat3(domain.LedgerApiError) + implicit val ErrorResponseFormat: RootJsonFormat[domain.ErrorResponse] = - jsonFormat3(domain.ErrorResponse) + jsonFormat4(domain.ErrorResponse) implicit def SyncResponseFormat[R: JsonFormat]: RootJsonFormat[domain.SyncResponse[R]] = new RootJsonFormat[domain.SyncResponse[R]] { diff --git a/ledger-service/http-json/src/main/scala/com/digitalasset/http/util/GrpcHttpErrorCodes.scala b/ledger-service/http-json/src/main/scala/com/digitalasset/http/util/GrpcHttpErrorCodes.scala index 63ef984b4624..a30f6bab24be 100644 --- a/ledger-service/http-json/src/main/scala/com/digitalasset/http/util/GrpcHttpErrorCodes.scala +++ b/ledger-service/http-json/src/main/scala/com/digitalasset/http/util/GrpcHttpErrorCodes.scala @@ -5,7 +5,7 @@ package com.daml.http package util private[http] object GrpcHttpErrorCodes { - import io.grpc.Status.{Code => G} + import com.google.rpc.{Code => G} import akka.http.scaladsl.model.{StatusCode, StatusCodes => A} implicit final class `gRPC status as akka http`(private val self: G) extends AnyVal { @@ -19,7 +19,7 @@ private[http] object GrpcHttpErrorCodes { case G.ABORTED | G.ALREADY_EXISTS => A.Conflict case G.RESOURCE_EXHAUSTED => A.TooManyRequests case G.CANCELLED => ClientClosedRequest - case G.DATA_LOSS | G.UNKNOWN | G.INTERNAL => A.InternalServerError + case G.DATA_LOSS | G.UNKNOWN | G.UNRECOGNIZED | G.INTERNAL => A.InternalServerError case G.UNIMPLEMENTED => A.NotImplemented case G.UNAVAILABLE => A.ServiceUnavailable case G.DEADLINE_EXCEEDED => A.GatewayTimeout diff --git a/ledger-service/http-json/src/test/scala/com/digitalasset/http/json/JsonProtocolTest.scala b/ledger-service/http-json/src/test/scala/com/digitalasset/http/json/JsonProtocolTest.scala index 6614eca1943f..8800e4955678 100644 --- a/ledger-service/http-json/src/test/scala/com/digitalasset/http/json/JsonProtocolTest.scala +++ b/ledger-service/http-json/src/test/scala/com/digitalasset/http/json/JsonProtocolTest.scala @@ -4,6 +4,13 @@ package com.daml.http.json import akka.http.scaladsl.model.StatusCodes +import com.daml.error.utils.ErrorDetails.{ + ErrorDetail, + ErrorInfoDetail, + RequestInfoDetail, + ResourceInfoDetail, + RetryInfoDetail, +} import com.daml.http.Generators.{ OptionalPackageIdGen, contractGen, @@ -177,6 +184,29 @@ class JsonProtocolTest } } + "ErrorDetail" - { + "Encoding and decoding ResourceInfoDetail should result in the same object" in { + val resourceInfoDetail: ErrorDetail = ResourceInfoDetail("test", "test") + resourceInfoDetail shouldBe resourceInfoDetail.toJson.convertTo[ErrorDetail] + } + + "Encoding and decoding RetryInfoDetail should result in the same object" in { + val retryInfoDetail: ErrorDetail = RetryInfoDetail(scala.concurrent.duration.Duration.Zero) + retryInfoDetail shouldBe retryInfoDetail.toJson.convertTo[ErrorDetail] + } + + "Encoding and decoding RequestInfoDetail should result in the same object" in { + val requestInfoDetail: ErrorDetail = RequestInfoDetail("test") + requestInfoDetail shouldBe requestInfoDetail.toJson.convertTo[ErrorDetail] + } + + "Encoding and decoding ErrorInfoDetail should result in the same object" in { + val errorInfoDetail: ErrorDetail = + ErrorInfoDetail("test", Map("test" -> "test1", "test2" -> "test3")) + errorInfoDetail shouldBe errorInfoDetail.toJson.convertTo[ErrorDetail] + } + } + "domain.ExerciseCommand" - { "should serialize to a JSON object with flattened reference fields" in forAll(exerciseCmdGen) { cmd => diff --git a/security-evidence.md b/security-evidence.md index 89631aac5854..cb6f47643ff8 100644 --- a/security-evidence.md +++ b/security-evidence.md @@ -5,7 +5,7 @@ - Updating the package service succeeds with sufficient authorization: [AuthorizationTest.scala](ledger-service/http-json/src/it/scala/http/AuthorizationTest.scala#L89) - accept user tokens: [TestMiddleware.scala](triggers/service/auth/src/test/scala/com/daml/auth/middleware/oauth2/TestMiddleware.scala#L152) - badly-authorized create is rejected: [AuthorizationSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/AuthorizationSpec.scala#L60) -- badly-authorized create is rejected: [AbstractHttpServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala#L1044) +- badly-authorized create is rejected: [AbstractHttpServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala#L1069) - badly-authorized exercise is rejected: [AuthorizationSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/AuthorizationSpec.scala#L158) - badly-authorized exercise/create (create is unauthorized) is rejected: [AuthPropagationSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/AuthPropagationSpec.scala#L274) - badly-authorized exercise/create (exercise is unauthorized) is rejected: [AuthPropagationSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/AuthPropagationSpec.scala#L242) @@ -18,7 +18,7 @@ - create with no signatories is rejected: [AuthorizationSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/AuthorizationSpec.scala#L50) - create with non-signatory maintainers is rejected: [AuthorizationSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/AuthorizationSpec.scala#L72) - exercise with no controllers is rejected: [AuthorizationSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/AuthorizationSpec.scala#L148) -- fetch fails when readAs not authed, even if prior fetch succeeded: [AbstractHttpServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala#L1102) +- fetch fails when readAs not authed, even if prior fetch succeeded: [AbstractHttpServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala#L1127) - forbid a non-authorized party to check the status of a trigger: [TriggerServiceTest.scala](triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceTest.scala#L643) - forbid a non-authorized party to list triggers: [TriggerServiceTest.scala](triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceTest.scala#L633) - forbid a non-authorized party to start a trigger: [TriggerServiceTest.scala](triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceTest.scala#L622) @@ -26,7 +26,7 @@ - forbid a non-authorized user to upload a DAR: [TriggerServiceTest.scala](triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceTest.scala#L675) - multiple websocket requests over the same WebSocket connection are NOT allowed: [AbstractWebsocketServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractWebsocketServiceIntegrationTest.scala#L118) - refresh a token after expiry on the server side: [TriggerServiceTest.scala](triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceTest.scala#L700) -- reject requests with missing auth header: [AbstractHttpServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala#L538) +- reject requests with missing auth header: [AbstractHttpServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala#L541) - request a fresh token after expiry on user request: [TriggerServiceTest.scala](triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceTest.scala#L685) - return the token from a cookie: [TestMiddleware.scala](triggers/service/auth/src/test/scala/com/daml/auth/middleware/oauth2/TestMiddleware.scala#L96) - return unauthorized on an expired token: [TestMiddleware.scala](triggers/service/auth/src/test/scala/com/daml/auth/middleware/oauth2/TestMiddleware.scala#L139) @@ -202,7 +202,7 @@ ## Performance: - Tail call optimization: Tail recursion does not blow the scala JVM stack.: [TailCallTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/TailCallTest.scala#L16) -- archiving a large number of contracts should succeed: [AbstractHttpServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala#L1394) +- archiving a large number of contracts should succeed: [AbstractHttpServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala#L1419) - creating and listing 20K users should be possible: [HttpServiceIntegrationTestUserManagement.scala](ledger-service/http-json/src/it/scala/http/HttpServiceIntegrationTestUserManagement.scala#L562) ## Input Validation: