Skip to content

Commit 7daf683

Browse files
committed
Validate test/schema import format: Fixes Hyperfoil#1685
- Inject cached custom mapper for Schema and Test import
1 parent 653c086 commit 7daf683

File tree

6 files changed

+264
-7
lines changed

6 files changed

+264
-7
lines changed

horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/SchemaServiceImpl.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package io.hyperfoil.tools.horreum.svc;
22

3+
import com.fasterxml.jackson.core.JsonFactory;
4+
import com.fasterxml.jackson.core.JsonGenerator;
5+
import com.fasterxml.jackson.core.JsonParser;
6+
import com.fasterxml.jackson.databind.DeserializationConfig;
7+
import com.fasterxml.jackson.databind.DeserializationFeature;
38
import com.fasterxml.jackson.databind.JsonNode;
49
import com.fasterxml.jackson.databind.ObjectMapper;
510
import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -116,6 +121,7 @@ SELECT substring(jsonb_path_query(schema, '$.**.\"$ref\" ? (! (@ starts with \"#
116121
Session session;
117122

118123
@Inject
124+
@Util.FailUnknownProperties
119125
ObjectMapper mapper;
120126

121127
@WithToken
@@ -849,7 +855,14 @@ public SchemaExport exportSchema(int id) {
849855
@Transactional
850856
@Override
851857
public void importSchema(ObjectNode node) {
852-
SchemaExport importSchema = Util.OBJECT_MAPPER.convertValue(node, SchemaExport.class);
858+
SchemaExport importSchema;
859+
860+
try {
861+
importSchema = mapper.convertValue(node, SchemaExport.class);
862+
} catch (IllegalArgumentException e){
863+
throw ServiceException.badRequest("Failed to parse Schema definition: "+e.getMessage());
864+
}
865+
853866
boolean newSchema = true;
854867
SchemaDAO schema = null;
855868
if (importSchema.id != null) {

horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/TestServiceImpl.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ SELECT DISTINCT COALESCE(jsonb_object_agg(label.name, lv.value) FILTER (WHERE la
100100

101101
//@formatter:on
102102
@Inject
103+
@Util.FailUnknownProperties
103104
ObjectMapper mapper;
104105

105106
@Inject
@@ -818,7 +819,15 @@ public TestExport export(int testId) {
818819
@Transactional
819820
@Override
820821
public void importTest(ObjectNode node) {
821-
TestExport newTest = Util.OBJECT_MAPPER.convertValue(node, TestExport.class);
822+
823+
TestExport newTest;
824+
825+
try {
826+
newTest = mapper.convertValue(node, TestExport.class);
827+
} catch (IllegalArgumentException e){
828+
throw ServiceException.badRequest("Failed to parse Test definition: "+e.getMessage());
829+
}
830+
822831
//need to add logic for datastore
823832
if(newTest.datastore != null) {
824833
//first check if datastore already exists

horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/Util.java

+35
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import java.io.IOException;
55
import java.io.OutputStream;
66
import java.lang.annotation.Annotation;
7+
import java.lang.annotation.Retention;
8+
import java.lang.annotation.Target;
79
import java.lang.reflect.Method;
810
import java.net.URLDecoder;
911
import java.nio.charset.StandardCharsets;
@@ -14,7 +16,15 @@
1416
import java.util.*;
1517
import java.util.function.*;
1618

19+
import com.fasterxml.jackson.databind.DeserializationFeature;
1720
import com.fasterxml.jackson.databind.node.*;
21+
import jakarta.enterprise.context.ApplicationScoped;
22+
import jakarta.enterprise.inject.Default;
23+
import jakarta.enterprise.inject.Produces;
24+
import jakarta.enterprise.inject.spi.Bean;
25+
import jakarta.enterprise.inject.spi.BeanManager;
26+
import jakarta.enterprise.inject.spi.CDI;
27+
import jakarta.inject.Qualifier;
1828
import jakarta.persistence.EntityManager;
1929
import jakarta.persistence.NoResultException;
2030
import jakarta.persistence.OptimisticLockException;
@@ -49,6 +59,9 @@
4959
import io.vertx.core.Vertx;
5060
import io.vertx.core.eventbus.EventBus;
5161

62+
import static java.lang.annotation.ElementType.*;
63+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
64+
5265
public class Util {
5366
private static final Logger log = Logger.getLogger(Util.class);
5467
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@@ -65,6 +78,28 @@ public class Util {
6578
OBJECT_MAPPER.registerModule(new JavaTimeModule());
6679
}
6780

81+
@Qualifier
82+
@Retention(RUNTIME)
83+
@Target({METHOD, FIELD, PARAMETER, TYPE})
84+
public @interface FailUnknownProperties {}
85+
86+
87+
@Produces
88+
@FailUnknownProperties
89+
@ApplicationScoped
90+
public ObjectMapper producerObjectMapper() {
91+
BeanManager beanManager = CDI.current().getBeanManager();
92+
93+
Bean<ObjectMapper> bean = (Bean<ObjectMapper>) beanManager.resolve(beanManager.getBeans(ObjectMapper.class));
94+
ObjectMapper objectMapper = beanManager.getContext(bean.getScope()).get(bean, beanManager.createCreationalContext(bean));
95+
96+
ObjectMapper customMapper = objectMapper.copy();
97+
customMapper.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
98+
99+
return customMapper;
100+
}
101+
102+
68103
static String destringify(String str) {
69104
if (str == null || str.isEmpty()) {
70105
return str;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
3+
<!-- This file is here only to let Quarkus hot-reload the dependency during dev mode -->
4+
</beans>

horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/SchemaServiceNoRestTest.java

+84-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package io.hyperfoil.tools.horreum.svc;
22

3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.fasterxml.jackson.databind.node.ObjectNode;
36
import io.hyperfoil.tools.horreum.api.SortDirection;
4-
import io.hyperfoil.tools.horreum.api.data.Access;
5-
import io.hyperfoil.tools.horreum.api.data.Extractor;
6-
import io.hyperfoil.tools.horreum.api.data.Label;
7-
import io.hyperfoil.tools.horreum.api.data.Schema;
8-
import io.hyperfoil.tools.horreum.api.data.Transformer;
7+
import io.hyperfoil.tools.horreum.api.data.*;
98
import io.hyperfoil.tools.horreum.api.services.SchemaService;
109
import io.hyperfoil.tools.horreum.entity.data.LabelDAO;
1110
import io.hyperfoil.tools.horreum.entity.data.SchemaDAO;
@@ -35,6 +34,7 @@
3534
import static org.junit.jupiter.api.Assertions.assertNull;
3635
import static org.junit.jupiter.api.Assertions.assertThrows;
3736
import static org.junit.jupiter.api.Assertions.assertTrue;
37+
import static org.keycloak.util.JsonSerialization.mapper;
3838

3939
@QuarkusTest
4040
@QuarkusTestResource(PostgresResource.class)
@@ -46,6 +46,9 @@ class SchemaServiceNoRestTest extends BaseServiceNoRestTest {
4646
@Inject
4747
SchemaService schemaService;
4848

49+
@Inject
50+
ObjectMapper objectMapper;
51+
4952
@org.junit.jupiter.api.Test
5053
void testCreateSchema() {
5154
String schemaUri = "urn:dummy:schema";
@@ -649,6 +652,82 @@ void testDeleteSchemaLabelWithFailures() {
649652
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), thrown.getResponse().getStatus());
650653
}
651654

655+
@org.junit.jupiter.api.Test
656+
void testImportSchemaWithValidStructure() throws JsonProcessingException {
657+
String schemaImport = """
658+
{
659+
"labels" : [ {
660+
"name" : "kb_report_results_podLatencyQuantilesMeasurement_quantiles_Ready_P99",
661+
"filtering" : true,
662+
"metrics" : true,
663+
"schemaId" : "221",
664+
"access" : "PUBLIC",
665+
"owner" : "TEAM_NAME",
666+
"extractors" : [ {
667+
"name" : "P99",
668+
"jsonpath" : "$.results.podLatencyQuantilesMeasurement.quantiles.Ready.P99",
669+
"isarray" : false
670+
} ]
671+
} ],
672+
"transformers": [],
673+
"id": 221,
674+
"uri": "urn:kube-burner-report:0.1",
675+
"name": "kube-burner-report",
676+
"description": "Kube Burner test for the report variant of results",
677+
"schema": {
678+
"$id": "urn:kube-burner-report:0.2",
679+
"type": "object",
680+
"$schema": "http://json-schema.org/draft-07/schema#"
681+
},
682+
"access": "PUBLIC",
683+
"owner": "TEAM_NAME"
684+
} """;
685+
686+
ObjectNode schemaJson = (ObjectNode) objectMapper.readTree(schemaImport.replaceAll("TEAM_NAME", FOO_TEAM));
687+
schemaService.importSchema(schemaJson);
688+
689+
}
690+
691+
@org.junit.jupiter.api.Test
692+
void testImportSchemaWithInvalidStructure() throws JsonProcessingException {
693+
String schemaImport = """
694+
{
695+
"labels" : [ {
696+
"name" : "kb_report_results_podLatencyQuantilesMeasurement_quantiles_Ready_P99",
697+
"filtering" : true,
698+
"metrics" : true,
699+
"schemaId" : "221",
700+
"acccess" : "PUBLIC",
701+
"owner" : "TEAM_NAME",
702+
"extractors" : [ {
703+
"name" : "P99",
704+
"path" : "$.results.podLatencyQuantilesMeasurement.quantiles.Ready.P99",
705+
"isarray" : false
706+
} ]
707+
} ],
708+
"transformers": [],
709+
"id": 221,
710+
"uri": "urn:kube-burner-report:0.2",
711+
"name": "kube-burner-report",
712+
"description": "Kube Burner test for the report variant of results",
713+
"schema": {
714+
"$id": "urn:kube-burner-report:0.2",
715+
"type": "object",
716+
"$schema": "http://json-schema.org/draft-07/schema#"
717+
},
718+
"acccess": "PUBLIC",
719+
"owner": "TEAM_NAME"
720+
}
721+
""";
722+
723+
ObjectNode schemaJson = (ObjectNode) objectMapper.readTree(schemaImport.replaceAll("TEAM_NAME", FOO_TEAM));
724+
725+
726+
ServiceException thrown = assertThrows(ServiceException.class, () -> schemaService.importSchema(schemaJson));
727+
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), thrown.getResponse().getStatus());
728+
729+
}
730+
652731
// utility to create a schema in the db, tested with testCreateSchema
653732
private Schema createSchema(String name, String uri) {
654733
return createSchema(name, uri, null);

horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/TestServiceNoRestTest.java

+117
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package io.hyperfoil.tools.horreum.svc;
22

3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.fasterxml.jackson.databind.node.ObjectNode;
36
import io.hyperfoil.tools.horreum.api.data.Access;
47
import io.hyperfoil.tools.horreum.api.data.Test;
58
import io.hyperfoil.tools.horreum.api.data.TestToken;
@@ -41,6 +44,9 @@ class TestServiceNoRestTest extends BaseServiceNoRestTest {
4144
@Inject
4245
TestService testService;
4346

47+
@Inject
48+
ObjectMapper objectMapper;
49+
4450
@org.junit.jupiter.api.Test
4551
void testCreateTest() {
4652
Test t1 = createSampleTest("test", null, null, null);
@@ -524,6 +530,117 @@ void testUpdateTestFolderWithWildcardFailure() {
524530
assertNull(test.folder);
525531
}
526532

533+
@org.junit.jupiter.api.Test
534+
void testImportTestWithValidStructure() throws JsonProcessingException {
535+
String testImport = """
536+
{
537+
"access": "PUBLIC",
538+
"owner": "TEAM_NAME",
539+
"name": "Quarkus - config-quickstart - JVM",
540+
"folder": "quarkus",
541+
"description": "",
542+
"datastoreId": null,
543+
"tokens": null,
544+
"timelineLabels": [],
545+
"timelineFunction": null,
546+
"fingerprintLabels": [
547+
"buildType"
548+
],
549+
"fingerprintFilter": null,
550+
"compareUrl": null,
551+
"transformers": [],
552+
"notificationsEnabled": true,
553+
"variables": [],
554+
"missingDataRules": [],
555+
"experiments": [],
556+
"actions": [],
557+
"subscriptions": null,
558+
"datastore": null
559+
}
560+
""";
561+
562+
ObjectNode testJson = (ObjectNode) objectMapper.readTree(testImport .replaceAll("TEAM_NAME", FOO_TEAM));
563+
testService.importTest(testJson);
564+
565+
}
566+
567+
@org.junit.jupiter.api.Test
568+
void testImportTestWithIncorrectTeam() throws JsonProcessingException {
569+
String testImport = """
570+
{
571+
"access": "PUBLIC",
572+
"owner": "perf-team",
573+
"id": 14,
574+
"name": "Quarkus - config-quickstart - JVM",
575+
"folder": "quarkus",
576+
"description": "",
577+
"datastoreId": null,
578+
"tokens": null,
579+
"timelineLabels": [],
580+
"timelineFunction": null,
581+
"fingerprintLabels": [
582+
"buildType"
583+
],
584+
"fingerprintFilter": null,
585+
"compareUrl": null,
586+
"transformers": [],
587+
"notificationsEnabled": true,
588+
"variables": [],
589+
"missingDataRules": [],
590+
"experiments": [],
591+
"actions": [],
592+
"subscriptions": {},
593+
"datastore": null
594+
}
595+
""";
596+
597+
ObjectNode testJson = (ObjectNode) objectMapper.readTree(testImport);
598+
599+
600+
ServiceException thrown = assertThrows(ServiceException.class, () -> testService.importTest(testJson));
601+
assertEquals(Response.Status.FORBIDDEN.getStatusCode(), thrown.getResponse().getStatus());
602+
assertEquals("This user does not have the perf-team role!", thrown.getMessage());
603+
604+
}
605+
606+
607+
@org.junit.jupiter.api.Test
608+
void testImporttestWithInvalidStructure() throws JsonProcessingException {
609+
String testImport = """
610+
{
611+
"accccess": "PUBLIC",
612+
"ownerrr": "TEAM_NAME",
613+
"name": "Quarkus - config-quickstart - JVM",
614+
"folder": "quarkus",
615+
"description": "",
616+
"datastoreId": null,
617+
"tokens": null,
618+
"timelineLabels": [],
619+
"timelineFunction": null,
620+
"fingerprintLabels": [
621+
"buildType"
622+
],
623+
"fingerprintFilter": null,
624+
"compareUrl": null,
625+
"transformers": [],
626+
"notificationsEnabled": true,
627+
"variables": [],
628+
"missingDataRules": [],
629+
"experiments": [],
630+
"actions": [],
631+
"subscriptions": null,
632+
"datastore": null
633+
}
634+
""";
635+
636+
ObjectNode testJson = (ObjectNode) objectMapper.readTree(testImport.replaceAll("TEAM_NAME", FOO_TEAM));
637+
638+
639+
ServiceException thrown = assertThrows(ServiceException.class, () -> testService.importTest(testJson));
640+
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), thrown.getResponse().getStatus());
641+
642+
}
643+
527644
// utility to create a sample test and add to Horreum
528645
private Test addTest(String name, String owner, String folder, Integer datastoreId) {
529646
Test test = createSampleTest(name, owner, folder, datastoreId);

0 commit comments

Comments
 (0)