Skip to content

Commit

Permalink
[JSON-API] Add information from the jwtpayload to the logging context (
Browse files Browse the repository at this point in the history
…#9995)

* Add information from the jwtpayload to the logging context

changelog_begin

- [JSON API] For applicable requests actAs, readAs, applicationId & ledgerId are included in the log context

changelog_end

* Update ledger-service/http-json/src/main/scala/com/digitalasset/http/Endpoints.scala

Co-authored-by: Stephen Compall <[email protected]>

* Update ledger-service/http-json/src/main/scala/com/digitalasset/http/Endpoints.scala

Co-authored-by: Stephen Compall <[email protected]>

* Revert changes to make the function generic

* Create JwtPayloadG trait from which both payload variants inherit

* Reduce code duplication in Endpoints.scala

* Apply review suggestion

* Update test name to reflect field name changes

* Update ledger-service/http-json/src/main/scala/com/digitalasset/http/Endpoints.scala

Co-authored-by: Moritz Kiefer <[email protected]>

Co-authored-by: Stephen Compall <[email protected]>
Co-authored-by: Moritz Kiefer <[email protected]>
  • Loading branch information
3 people authored Jun 16, 2021
1 parent fe63825 commit cfee5a2
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ class CommandService(
jwtPayload.ledgerId,
jwtPayload.applicationId,
commandId,
jwtPayload.actAs,
jwtPayload.submitter,
jwtPayload.readAs,
command,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,21 @@ import com.daml.lf
import com.daml.http.ContractsService.SearchResult
import com.daml.http.EndpointsCompanion._
import com.daml.scalautil.Statement.discard
import com.daml.http.domain.{JwtPayload, JwtWritePayload, TemplateId}
import com.daml.http.domain.{JwtPayload, JwtPayloadG, JwtPayloadTag, JwtWritePayload, TemplateId}
import com.daml.http.json._
import com.daml.http.util.Collections.toNonEmptySet
import com.daml.http.util.FutureUtil.{either, eitherT}
import com.daml.http.util.Logging.{InstanceUUID, RequestID, extendWithRequestIdLogCtx}
import com.daml.http.util.ProtobufByteStrings
import com.daml.jwt.domain.Jwt
import com.daml.ledger.api.{v1 => lav1}
import com.daml.logging.LoggingContextOf.withEnrichedLoggingContext
import com.daml.util.ExceptionOps._
import scalaz.std.scalaFuture._
import scalaz.syntax.std.option._
import scalaz.syntax.show._
import scalaz.syntax.traverse._
import scalaz.{-\/, EitherT, NonEmptyList, Show, \/, \/-}
import scalaz.{-\/, EitherT, NonEmptyList, Show, Traverse, \/, \/-}
import spray.json._

import scala.concurrent.duration.FiniteDuration
Expand Down Expand Up @@ -122,77 +123,95 @@ class Endpoints(
}
}

def withJwtPayloadLoggingContext[A](jwtPayload: JwtPayloadG)(
fn: LoggingContextOf[JwtPayloadTag with InstanceUUID with RequestID] => A
)(implicit lc: LoggingContextOf[InstanceUUID with RequestID]): A =
withEnrichedLoggingContext(
LoggingContextOf.label[JwtPayloadTag],
Map(
"ledger_id" -> jwtPayload.ledgerId.toString,
"act_as" -> jwtPayload.actAs.toString,
"application_id" -> jwtPayload.applicationId.toString,
"read_as" -> jwtPayload.readAs.toString,
),
).run(fn)

def handleCommand[T[_]](req: HttpRequest)(
fn: (
Jwt,
JwtWritePayload,
JsValue,
) => LoggingContextOf[JwtPayloadTag with InstanceUUID with RequestID] => ET[
T[ApiValue]
]
)(implicit
lc: LoggingContextOf[InstanceUUID with RequestID],
ev1: JsonWriter[T[JsValue]],
ev2: Traverse[T],
): ET[domain.SyncResponse[JsValue]] = for {
t3 <- inputJsValAndJwtPayload(req): ET[(Jwt, JwtWritePayload, JsValue)]
(jwt, jwtPayload, reqBody) = t3
resp <- withJwtPayloadLoggingContext(jwtPayload)(fn(jwt, jwtPayload, reqBody))
jsVal <- either(SprayJson.encode1(resp).liftErr(ServerError)): ET[JsValue]
} yield domain.OkResponse(jsVal)

def create(req: HttpRequest)(implicit
lc: LoggingContextOf[InstanceUUID with RequestID]
): ET[domain.SyncResponse[JsValue]] =
for {
t3 <- inputJsValAndJwtPayload(req): ET[(Jwt, JwtWritePayload, JsValue)]

(jwt, jwtPayload, reqBody) = t3

cmd <- either(
decoder.decodeCreateCommand(reqBody).liftErr(InvalidUserInput)
): ET[domain.CreateCommand[ApiRecord, TemplateId.RequiredPkg]]

ac <- eitherT(
handleFutureEitherFailure(commandService.create(jwt, jwtPayload, cmd))
): ET[domain.ActiveContract[ApiValue]]

jsVal <- either(SprayJson.encode1(ac).liftErr(ServerError)): ET[JsValue]

} yield domain.OkResponse(jsVal)
handleCommand(req) { (jwt, jwtPayload, reqBody) => implicit lc =>
for {
cmd <- either(
decoder.decodeCreateCommand(reqBody).liftErr(InvalidUserInput)
): ET[domain.CreateCommand[ApiRecord, TemplateId.RequiredPkg]]

ac <- eitherT(
handleFutureEitherFailure(commandService.create(jwt, jwtPayload, cmd))
): ET[domain.ActiveContract[ApiValue]]
} yield ac
}

def exercise(req: HttpRequest)(implicit
lc: LoggingContextOf[InstanceUUID with RequestID]
): ET[domain.SyncResponse[JsValue]] =
for {
t3 <- inputJsValAndJwtPayload(req): ET[(Jwt, JwtWritePayload, JsValue)]

(jwt, jwtPayload, reqBody) = t3

cmd <- either(
decoder.decodeExerciseCommand(reqBody).liftErr(InvalidUserInput)
): ET[domain.ExerciseCommand[LfValue, domain.ContractLocator[LfValue]]]
handleCommand(req) { (jwt, jwtPayload, reqBody) => implicit lc =>
for {
cmd <- either(
decoder.decodeExerciseCommand(reqBody).liftErr(InvalidUserInput)
): ET[domain.ExerciseCommand[LfValue, domain.ContractLocator[LfValue]]]

resolvedRef <- eitherT(
resolveReference(jwt, jwtPayload, cmd.reference)
): ET[domain.ResolvedContractRef[ApiValue]]
resolvedRef <- eitherT(
resolveReference(jwt, jwtPayload, cmd.reference)
): ET[domain.ResolvedContractRef[ApiValue]]

apiArg <- either(lfValueToApiValue(cmd.argument)): ET[ApiValue]
apiArg <- either(lfValueToApiValue(cmd.argument)): ET[ApiValue]

resolvedCmd = cmd.copy(argument = apiArg, reference = resolvedRef)
resolvedCmd = cmd.copy(argument = apiArg, reference = resolvedRef)

resp <- eitherT(
handleFutureEitherFailure(
commandService.exercise(jwt, jwtPayload, resolvedCmd)
)
): ET[domain.ExerciseResponse[ApiValue]]

jsVal <- either(SprayJson.encode1(resp).liftErr(ServerError)): ET[JsValue]
resp <- eitherT(
handleFutureEitherFailure(
commandService.exercise(jwt, jwtPayload, resolvedCmd)
)
): ET[domain.ExerciseResponse[ApiValue]]

} yield domain.OkResponse(jsVal)
} yield resp
}

def createAndExercise(req: HttpRequest)(implicit
lc: LoggingContextOf[InstanceUUID with RequestID]
): ET[domain.SyncResponse[JsValue]] =
for {
t3 <- inputJsValAndJwtPayload(req): ET[(Jwt, JwtWritePayload, JsValue)]

(jwt, jwtPayload, reqBody) = t3

cmd <- either(
decoder.decodeCreateAndExerciseCommand(reqBody).liftErr(InvalidUserInput)
): ET[domain.CreateAndExerciseCommand[ApiRecord, ApiValue, TemplateId.RequiredPkg]]

resp <- eitherT(
handleFutureEitherFailure(
commandService.createAndExercise(jwt, jwtPayload, cmd)
)
): ET[domain.ExerciseResponse[ApiValue]]

jsVal <- either(SprayJson.encode1(resp).liftErr(ServerError)): ET[JsValue]

} yield domain.OkResponse(jsVal)
handleCommand(req) { (jwt, jwtPayload, reqBody) => implicit lc =>
for {
cmd <- either(
decoder.decodeCreateAndExerciseCommand(reqBody).liftErr(InvalidUserInput)
): ET[domain.CreateAndExerciseCommand[ApiRecord, ApiValue, TemplateId.RequiredPkg]]

resp <- eitherT(
handleFutureEitherFailure(
commandService.createAndExercise(jwt, jwtPayload, cmd)
)
): ET[domain.ExerciseResponse[ApiValue]]
} yield resp
}

def fetch(req: HttpRequest)(implicit
lc: LoggingContextOf[InstanceUUID with RequestID]
Expand All @@ -202,21 +221,25 @@ class Endpoints(

(jwt, jwtPayload, reqBody) = input

_ = logger.debug(s"/v1/fetch reqBody: $reqBody")
jsVal <- withJwtPayloadLoggingContext(jwtPayload) { implicit lc =>
logger.debug(s"/v1/fetch reqBody: $reqBody")
for {

cl <- either(
decoder.decodeContractLocator(reqBody).liftErr(InvalidUserInput)
): ET[domain.ContractLocator[LfValue]]
cl <- either(
decoder.decodeContractLocator(reqBody).liftErr(InvalidUserInput)
): ET[domain.ContractLocator[LfValue]]

_ = logger.debug(s"/v1/fetch cl: $cl")
_ = logger.debug(s"/v1/fetch cl: $cl")

ac <- eitherT(
handleFutureFailure(contractsService.lookup(jwt, jwtPayload, cl))
): ET[Option[domain.ActiveContract[JsValue]]]
ac <- eitherT(
handleFutureFailure(contractsService.lookup(jwt, jwtPayload, cl))
): ET[Option[domain.ActiveContract[JsValue]]]

jsVal <- either(
ac.cata(x => toJsValue(x), \/-(JsNull))
): ET[JsValue]
jsVal <- either(
ac.cata(x => toJsValue(x), \/-(JsNull))
): ET[JsValue]
} yield jsVal
}

} yield domain.OkResponse(jsVal)

Expand All @@ -225,13 +248,15 @@ class Endpoints(
): Future[Error \/ SearchResult[Error \/ JsValue]] =
inputAndJwtPayload[JwtPayload](req).map {
_.map { case (jwt, jwtPayload, _) =>
val result: SearchResult[ContractsService.Error \/ domain.ActiveContract[LfValue]] =
contractsService.retrieveAll(jwt, jwtPayload)

domain.SyncResponse.covariant.map(result) { source =>
source
.via(handleSourceFailure)
.map(_.flatMap(lfAcToJsValue)): Source[Error \/ JsValue, NotUsed]
withJwtPayloadLoggingContext(jwtPayload) { implicit lc =>
val result: SearchResult[ContractsService.Error \/ domain.ActiveContract[LfValue]] =
contractsService.retrieveAll(jwt, jwtPayload)

domain.SyncResponse.covariant.map(result) { source =>
source
.via(handleSourceFailure)
.map(_.flatMap(lfAcToJsValue)): Source[Error \/ JsValue, NotUsed]
}
}
}
}
Expand All @@ -241,19 +266,21 @@ class Endpoints(
): Future[Error \/ SearchResult[Error \/ JsValue]] =
inputAndJwtPayload[JwtPayload](req).map {
_.flatMap { case (jwt, jwtPayload, reqBody) =>
SprayJson
.decode[domain.GetActiveContractsRequest](reqBody)
.liftErr[Error](InvalidUserInput)
.map { cmd =>
val result: SearchResult[ContractsService.Error \/ domain.ActiveContract[JsValue]] =
contractsService.search(jwt, jwtPayload, cmd)

domain.SyncResponse.covariant.map(result) { source =>
source
.via(handleSourceFailure)
.map(_.flatMap(toJsValue[domain.ActiveContract[JsValue]](_)))
withJwtPayloadLoggingContext(jwtPayload) { implicit lc =>
SprayJson
.decode[domain.GetActiveContractsRequest](reqBody)
.liftErr[Error](InvalidUserInput)
.map { cmd =>
val result: SearchResult[ContractsService.Error \/ domain.ActiveContract[JsValue]] =
contractsService.search(jwt, jwtPayload, cmd)

domain.SyncResponse.covariant.map(result) { source =>
source
.via(handleSourceFailure)
.map(_.flatMap(toJsValue[domain.ActiveContract[JsValue]](_)))
}
}
}
}
}
}

Expand Down Expand Up @@ -485,7 +512,7 @@ class Endpoints(
jwtPayload: JwtWritePayload,
reference: domain.ContractLocator[LfValue],
)(implicit
lc: LoggingContextOf[InstanceUUID with RequestID]
lc: LoggingContextOf[JwtPayloadTag with InstanceUUID with RequestID]
): Future[Error \/ domain.ResolvedContractRef[ApiValue]] =
contractsService
.resolveContractReference(jwt, jwtPayload.parties, reference)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,27 @@ object domain {
private def oneAndSet[A](p: A, sp: Set[A]) =
OneAnd(p, sp - p)

trait JwtPayloadTag

trait JwtPayloadG {
val ledgerId: LedgerId
val applicationId: ApplicationId
val readAs: List[Party]
val actAs: List[Party]
val parties: OneAnd[Set, Party]
}

// Until we get multi-party submissions, write endpoints require a single party in actAs but we
// can have multiple parties in readAs.
case class JwtWritePayload(
final case class JwtWritePayload(
ledgerId: LedgerId,
applicationId: ApplicationId,
actAs: NonEmptyList[Party],
submitter: NonEmptyList[Party],
readAs: List[Party],
) {
val parties: OneAnd[Set, Party] = oneAndSet(actAs.head, actAs.tail.toSet union readAs.toSet)
) extends JwtPayloadG {
override val actAs: List[Party] = submitter.toList
override val parties: OneAnd[Set, Party] =
oneAndSet(actAs.head, actAs.tail.toSet union readAs.toSet)
}

// JWT payload that preserves readAs and actAs and supports multiple parties. This is currently only used for
Expand All @@ -72,7 +84,7 @@ object domain {
readAs: List[Party],
actAs: List[Party],
parties: OneAnd[Set, Party],
) {}
) extends JwtPayloadG {}

object JwtPayload {
def apply(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ final class DomainSpec extends AnyFreeSpec with Matchers {
private val alice = Party("Alice")
private val bob = Party("Bob")
"JwtWritePayload" - {
"parties deduplicates between actAs and readAs" in {
"parties deduplicates between actAs/submitter and readAs" in {
val payload =
JwtWritePayload(ledgerId, appId, actAs = NonEmptyList(alice), readAs = List(alice, bob))
JwtWritePayload(ledgerId, appId, submitter = NonEmptyList(alice), readAs = List(alice, bob))
payload.parties shouldBe OneAnd(alice, Set(bob))
}
}
Expand Down

0 comments on commit cfee5a2

Please sign in to comment.