diff --git a/ledger-service/db-backend/src/main/scala/com/digitalasset/http/dbbackend/Queries.scala b/ledger-service/db-backend/src/main/scala/com/digitalasset/http/dbbackend/Queries.scala index ec0975f3163..b0f47a6e05c 100644 --- a/ledger-service/db-backend/src/main/scala/com/digitalasset/http/dbbackend/Queries.scala +++ b/ledger-service/db-backend/src/main/scala/com/digitalasset/http/dbbackend/Queries.scala @@ -987,7 +987,7 @@ private final class OracleQueries( private[http] override def containsAtContractPath(path: JsonPath, literal: JsValue) = { def ensureNotNull = { // we are only trying to reject None for an Optional record/variant/list - val pred: Cord = ('$' -: pathSteps(path)) ++ "?(@ != null)" + val pred: Cord = ('$' -: pathSteps(path)) ++ "?(!(@ == null))" sql"JSON_EXISTS($contractColumnName, ${oracleShortPathEscape(pred)})" } literal match { diff --git a/ledger-service/http-json/BUILD.bazel b/ledger-service/http-json/BUILD.bazel index 92f1603eb1a..7117582ea73 100644 --- a/ledger-service/http-json/BUILD.bazel +++ b/ledger-service/http-json/BUILD.bazel @@ -327,6 +327,7 @@ alias( "//ledger-service/http-json-testing:{}".format(edition), "//ledger-service/db-backend", "//ledger-service/jwt", + "//ledger-service/lf-value-json", "//ledger-service/utils", "//ledger/ledger-api-auth", "//ledger/ledger-api-common", 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 f29ef7b9e01..dc3a8b567f0 100644 --- a/ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala +++ b/ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala @@ -526,6 +526,80 @@ abstract class AbstractHttpServiceIntegrationTestTokenIndependent }): Future[Assertion] } } + + "nested comparison filters" onlyIfLargeQueries_- { + import shapeless.Coproduct, shapeless.syntax.singleton._ + val irrelevant = Ref.Identifier assertFromString "none:Discarded:Identifier" + val (_, bazRecordVA) = VA.record(irrelevant, ShRecord(baz = VA.text)) + val (_, fooVA) = + VA.variant(irrelevant, ShRecord(Bar = VA.int64, Baz = bazRecordVA, Qux = VA.unit)) + val fooVariant = Coproduct[fooVA.Inj] + val (_, kbvarVA) = VA.record( + irrelevant, + ShRecord( + name = VA.text, + party = VAx.partyDomain, + age = VA.int64, + fooVariant = fooVA, + bazRecord = bazRecordVA, + ), + ) + + def withBazRecord(bazRecord: VA.text.Inj)(p: domain.Party): kbvarVA.Inj = + ShRecord( + name = "ABC DEF", + party = p, + age = 123L, + fooVariant = fooVariant(Symbol("Bar") ->> 42L), + bazRecord = ShRecord(baz = bazRecord), + ) + + def withFooVariant(v: VA.int64.Inj)(p: domain.Party): kbvarVA.Inj = + ShRecord( + name = "ABC DEF", + party = p, + age = 123L, + fooVariant = fooVariant(Symbol("Bar") ->> v), + bazRecord = ShRecord(baz = "another baz value"), + ) + + val kbvarId = TpId.Account.KeyedByVariantAndRecord + import FilterDiscriminatorScenario.Scenario + Seq( + Scenario( + "gt string", + kbvarId, + kbvarVA, + Map("bazRecord" -> Map("baz" -> Map("%gt" -> "b")).toJson), + )( + withBazRecord("c"), + withBazRecord("a"), + ), + Scenario( + "gt int", + kbvarId, + kbvarVA, + Map("fooVariant" -> Map("tag" -> "Bar".toJson, "value" -> Map("%gt" -> 2).toJson).toJson), + )(withFooVariant(3), withFooVariant(1)), + ).zipWithIndex.foreach { case (scenario, ix) => + import scenario._ + s"$label (scenario $ix)" in withHttpService { fixture => + for { + (alice, headers) <- fixture.getUniquePartyAndAuthHeaders("Alice") + contracts <- searchExpectOk( + List(matches, doesNotMatch).map { payload => + domain.CreateCommand(ctId, argToApi(va)(payload(alice)), None) + }, + JsObject(Map("templateIds" -> Seq(ctId).toJson, "query" -> query.toJson)), + fixture, + headers, + ) + } yield contracts.map(_.payload) should contain theSameElementsAs Seq( + LfValueCodec.apiValueToJsValue(va.inj(matches(alice))) + ) + } + } + } } "query with invalid JSON query should return error" in withHttpService { fixture => diff --git a/ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTestFuns.scala b/ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTestFuns.scala index d2d1bccaf3e..574e1e8b658 100644 --- a/ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTestFuns.scala +++ b/ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTestFuns.scala @@ -414,6 +414,7 @@ trait AbstractHttpServiceIntegrationTestFuns } object Account { val Account: TId = CtId.Template(None, "Account", "Account") + val KeyedByVariantAndRecord: TId = CtId.Template(None, "Account", "KeyedByVariantAndRecord") } object User { val User: Id = domain.TemplateId(None, "User", "User") diff --git a/ledger-service/http-json/src/itlib/scala/http/FilterDiscriminatorScenario.scala b/ledger-service/http-json/src/itlib/scala/http/FilterDiscriminatorScenario.scala new file mode 100644 index 00000000000..1c0a6a9e188 --- /dev/null +++ b/ledger-service/http-json/src/itlib/scala/http/FilterDiscriminatorScenario.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.daml.http + +import com.daml.lf.value.test.TypedValueGenerators.{ValueAddend => VA} +import spray.json.JsValue + +/** A query, a value that matches the query, and a value that doesn't match. + */ +class FilterDiscriminatorScenario[Inj]( + val label: String, + val ctId: domain.ContractTypeId.OptionalPkg, + val va: VA.Aux[Inj], + val query: Map[String, JsValue], + val matches: domain.Party => Inj, + val doesNotMatch: domain.Party => Inj, +) + +object FilterDiscriminatorScenario { + def Scenario( + label: String, + ctId: domain.ContractTypeId.OptionalPkg, + va: VA, + query: Map[String, JsValue], + )( + matches: domain.Party => va.Inj, + doesNotMatch: domain.Party => va.Inj, + ): FilterDiscriminatorScenario[va.Inj] = + new FilterDiscriminatorScenario(label, ctId, va, query, matches, doesNotMatch) +} diff --git a/ledger-service/http-json/src/test/scala/com/digitalasset/http/query/ValuePredicateTest.scala b/ledger-service/http-json/src/test/scala/com/digitalasset/http/query/ValuePredicateTest.scala index bbae1fe90e5..750b706afa2 100644 --- a/ledger-service/http-json/src/test/scala/com/digitalasset/http/query/ValuePredicateTest.scala +++ b/ledger-service/http-json/src/test/scala/com/digitalasset/http/query/ValuePredicateTest.scala @@ -257,7 +257,7 @@ class ValuePredicateTest "{}", tuple3VA, sql"payload @> ${"""{"foo":{}}""".parseJson}::jsonb", - sql"""JSON_EXISTS(payload, '$$."foo"?(@ != null)')""", + sql"""JSON_EXISTS(payload, '$$."foo"?(!(@ == null))')""", ), ( """{"%lte": 42}""", diff --git a/security-evidence.md b/security-evidence.md index 83f1055d2e0..351f880b3a5 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#L85) - 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#L1136) +- badly-authorized create is rejected: [AbstractHttpServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala#L1210) - 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#L273) - badly-authorized exercise/create (exercise is unauthorized) is rejected: [AuthPropagationSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/AuthPropagationSpec.scala#L241) @@ -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#L1204) +- fetch fails when readAs not authed, even if prior fetch succeeded: [AbstractHttpServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala#L1278) - 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#L680) - forbid a non-authorized party to list triggers: [TriggerServiceTest.scala](triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceTest.scala#L670) - forbid a non-authorized party to start a trigger: [TriggerServiceTest.scala](triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceTest.scala#L659) @@ -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#L712) - multiple websocket requests over the same WebSocket connection are NOT allowed: [AbstractWebsocketServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractWebsocketServiceIntegrationTest.scala#L130) - refresh a token after expiry on the server side: [TriggerServiceTest.scala](triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceTest.scala#L737) -- reject requests with missing auth header: [AbstractHttpServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala#L573) +- reject requests with missing auth header: [AbstractHttpServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala#L647) - request a fresh token after expiry on user request: [TriggerServiceTest.scala](triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceTest.scala#L722) - 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) @@ -208,7 +208,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#L1410) +- archiving a large number of contracts should succeed: [AbstractHttpServiceIntegrationTest.scala](ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala#L1484) - creating and listing 20K users should be possible: [HttpServiceIntegrationTestUserManagement.scala](ledger-service/http-json/src/it/scala/http/HttpServiceIntegrationTestUserManagement.scala#L558) ## Input Validation: