Skip to content

Commit 50e25f1

Browse files
fix: add validation for cron in page source during import to prevent malformed cron expressions
1 parent 3d1b125 commit 50e25f1

File tree

5 files changed

+236
-1
lines changed

5 files changed

+236
-1
lines changed

gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/ApiDuplicatorServiceImpl.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import com.fasterxml.jackson.core.JsonProcessingException;
2727
import com.fasterxml.jackson.databind.DeserializationFeature;
28+
import com.fasterxml.jackson.databind.JsonNode;
2829
import com.fasterxml.jackson.databind.ObjectMapper;
2930
import io.gravitee.common.http.HttpMethod;
3031
import io.gravitee.common.utils.IdGenerator;
@@ -102,6 +103,7 @@
102103
import org.slf4j.Logger;
103104
import org.slf4j.LoggerFactory;
104105
import org.springframework.context.annotation.Lazy;
106+
import org.springframework.scheduling.support.CronExpression;
105107
import org.springframework.stereotype.Component;
106108

107109
/**
@@ -1092,6 +1094,23 @@ private void checkPagesConsistency(ImportApiJsonNode apiJsonNode) {
10921094
if (systemFoldersCount > 1) {
10931095
throw new ApiImportException("Only one system folder is allowed in the API pages definition");
10941096
}
1097+
for (JsonNode pageNode : apiJsonNode.getPagesArray()) {
1098+
JsonNode sourceNode = pageNode.path("source");
1099+
JsonNode configNode = sourceNode.path("configuration");
1100+
1101+
if (!configNode.isMissingNode() && configNode.has("fetchCron")) {
1102+
String cron = configNode.path("fetchCron").asText(null);
1103+
1104+
if (cron != null && !cron.isEmpty()) {
1105+
try {
1106+
CronExpression.parse(cron); // Validate cron
1107+
} catch (IllegalArgumentException e) {
1108+
String pageName = pageNode.path("name").asText("Unnamed Page");
1109+
throw new ApiImportException("Invalid fetchCron expression in page '" + pageName + "': " + e.getMessage());
1110+
}
1111+
}
1112+
}
1113+
}
10951114
}
10961115
}
10971116

gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/PageServiceImpl.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,27 @@ private static PageSource convert(PageSourceEntity pageSourceEntity) {
330330
return source;
331331
}
332332

333+
public static void validateFetchConfig(String jsonConfig) throws TechnicalException {
334+
ObjectMapper mapper = new ObjectMapper();
335+
try {
336+
JsonNode root = mapper.readTree(jsonConfig);
337+
boolean autoFetch = root.path("autoFetch").asBoolean(false);
338+
String fetchCron = root.path("fetchCron").asText();
339+
if (autoFetch) {
340+
if (fetchCron == null || fetchCron.isEmpty()) {
341+
throw new TechnicalException("fetchCron is required when autoFetch is true.");
342+
}
343+
}
344+
if (fetchCron != null && !fetchCron.isEmpty() && !"null".equals(fetchCron)) {
345+
CronExpression.parse(fetchCron);
346+
}
347+
} catch (IllegalArgumentException e) {
348+
throw new TechnicalException("Invalid cron expression: " + e.getMessage(), e);
349+
} catch (Exception e) {
350+
throw new TechnicalException("Failed to parse configuration JSON: " + e.getMessage(), e);
351+
}
352+
}
353+
333354
@SuppressWarnings("squid:S1166")
334355
private static boolean isJson(String content) {
335356
try {
@@ -895,7 +916,9 @@ public PageEntity createPage(final ExecutionContext executionContext, String api
895916
if (newPageEntity.getContent() == null && newPageEntity.getSource() != null && newPageType != ROOT) {
896917
fetchPage(newPageEntity);
897918
}
898-
919+
if (newPageEntity.getSource() != null && newPageEntity.getSource().getConfiguration() != null) {
920+
validateFetchConfig(newPageEntity.getSource().getConfiguration());
921+
}
899922
Page page = convert(newPageEntity);
900923

901924
page.setId(id);
@@ -1226,6 +1249,9 @@ public PageEntity update(final ExecutionContext executionContext, String pageId,
12261249
if (partial) {
12271250
page = merge(updatePageEntity, pageToUpdate);
12281251
} else {
1252+
if (updatePageEntity.getSource() != null && updatePageEntity.getSource().getConfiguration() != null) {
1253+
validateFetchConfig(updatePageEntity.getSource().getConfiguration());
1254+
}
12291255
page = convert(updatePageEntity);
12301256
}
12311257

gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/impl/ApiDuplicatorServiceImplTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static org.junit.Assert.assertEquals;
2020
import static org.junit.Assert.assertFalse;
2121
import static org.junit.Assert.assertNotNull;
22+
import static org.junit.Assert.assertThrows;
2223
import static org.junit.Assert.assertTrue;
2324
import static org.mockito.ArgumentMatchers.any;
2425
import static org.mockito.Mockito.*;
@@ -58,6 +59,7 @@
5859
import io.gravitee.rest.api.service.UserService;
5960
import io.gravitee.rest.api.service.common.GraviteeContext;
6061
import io.gravitee.rest.api.service.converter.CategoryMapper;
62+
import io.gravitee.rest.api.service.exceptions.ApiImportException;
6163
import io.gravitee.rest.api.service.exceptions.TechnicalManagementException;
6264
import io.gravitee.rest.api.service.imports.ImportApiJsonNode;
6365
import io.gravitee.rest.api.service.spring.ServiceConfiguration;
@@ -532,4 +534,16 @@ public void shouldPreserveValidAccessControlGroups() throws IOException {
532534
eq(API_ID)
533535
);
534536
}
537+
538+
@Test
539+
public void shouldThrowExceptionForInvalidFetchCronExpression() throws IOException {
540+
ImportApiJsonNode node = loadTestNode(IMPORT_FILES_FOLDER + "import-api.invalid.cron.json");
541+
ApiImportException exception = assertThrows(
542+
ApiImportException.class,
543+
() -> ReflectionTestUtils.invokeMethod(apiDuplicatorService, "checkPagesConsistency", node)
544+
);
545+
546+
assertTrue(exception.getMessage().startsWith("Invalid fetchCron expression in page"));
547+
assertTrue(exception.getMessage().contains("Cron expression must consist of 6 fields"));
548+
}
535549
}

gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/impl/PageService_CreateTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static org.springframework.test.util.ReflectionTestUtils.setField;
2424

2525
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
26+
import com.fasterxml.jackson.databind.node.ObjectNode;
2627
import com.google.common.base.Charsets;
2728
import com.google.common.io.Resources;
2829
import freemarker.template.TemplateException;
@@ -587,4 +588,14 @@ public void shouldNotCreateBecausePageContentTemplatingException() throws Techni
587588

588589
verify(pageRepository).create(any());
589590
}
591+
592+
@Test(expected = TechnicalManagementException.class)
593+
public void shouldFailToCreatePageWithInvalidFetchCron() {
594+
PageSourceEntity source = new PageSourceEntity();
595+
source.setType("github-fetcher");
596+
source.setConfiguration(JsonNodeFactory.instance.objectNode().put("autoFetch", true).put("fetchCron", "15 8,13 * * MON-FRI")); // Invalid cron
597+
when(newPage.getSource()).thenReturn(source);
598+
when(newPage.getVisibility()).thenReturn(Visibility.PUBLIC);
599+
pageService.createPage(GraviteeContext.getExecutionContext(), API_ID, newPage);
600+
}
590601
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
{
2+
"name" : "Test",
3+
"crossId" : "3bb7f9b3-a75b-47cd-b7f9-b3a75bd7cd66",
4+
"version" : "1",
5+
"execution_mode" : "v4-emulation-engine",
6+
"description" : "Test",
7+
"visibility" : "PRIVATE",
8+
"flows" : [ ],
9+
"gravitee" : "2.0.0",
10+
"flow_mode" : "DEFAULT",
11+
"resources" : [ ],
12+
"properties" : [ ],
13+
"members" : [ {
14+
"source" : "memory",
15+
"sourceId" : "admin",
16+
"roles" : [ "0716e6d1-dccb-4c16-96e6-d1dccbac1674" ]
17+
} ],
18+
"pages" : [ {
19+
"id" : "8812c075-92ce-41a6-92c0-7592cea1a684",
20+
"crossId" : "21c3673a-e29c-4470-8367-3ae29c147099",
21+
"name" : "Aside",
22+
"type" : "SYSTEM_FOLDER",
23+
"order" : 0,
24+
"published" : true,
25+
"visibility" : "PUBLIC",
26+
"lastModificationDate" : 1745480801827,
27+
"contentType" : "application/json",
28+
"homepage" : false,
29+
"parentPath" : "",
30+
"excludedAccessControls" : false,
31+
"accessControls" : [ ],
32+
"api" : "c57c319b-62b6-427c-bc31-9b62b6527c37",
33+
"attached_media" : [ ]
34+
}, {
35+
"id" : "0efebe71-6cb1-43e5-bebe-716cb163e539",
36+
"crossId" : "fc3d8a92-b18c-4ba4-bd8a-92b18cfba4ce",
37+
"name" : "Test",
38+
"type" : "SWAGGER",
39+
"content" : "{}",
40+
"order" : 1,
41+
"lastContributor" : "4407b94b-a747-41d6-87b9-4ba747f1d67c",
42+
"published" : false,
43+
"visibility" : "PUBLIC",
44+
"lastModificationDate" : 1745480801842,
45+
"contentType" : "application/json",
46+
"source" : {
47+
"type" : "gitlab-fetcher",
48+
"configuration" : {"gitlabUrl":"https://gitlab.com/api/v4","useSystemProxy":false,"namespace":"t3551","project":"test","branchOrTag":"main","filepath":"/PetStore.json","privateToken":"********","apiVersion":"V4","editLink":null,"fetchCron":"15 8,13 * * MON-FRI","autoFetch":false}
49+
},
50+
"configuration" : {
51+
"viewer" : "Swagger"
52+
},
53+
"homepage" : false,
54+
"parentPath" : "",
55+
"excludedAccessControls" : false,
56+
"accessControls" : [ ],
57+
"api" : "c57c319b-62b6-427c-bc31-9b62b6527c37",
58+
"attached_media" : [ ]
59+
} ],
60+
"plans" : [ {
61+
"id" : "ad8e4d1d-7c2c-4036-8e4d-1d7c2c203637",
62+
"definitionVersion" : "2.0.0",
63+
"crossId" : "5514b581-1083-4ca4-94b5-8110839ca4b1",
64+
"name" : "KeylessPlan",
65+
"description" : "KeylessPlan",
66+
"validation" : "AUTO",
67+
"security" : "KEY_LESS",
68+
"type" : "API",
69+
"status" : "PUBLISHED",
70+
"api" : "c57c319b-62b6-427c-bc31-9b62b6527c37",
71+
"order" : 0,
72+
"characteristics" : [ ],
73+
"tags" : [ ],
74+
"created_at" : 1742314598986,
75+
"updated_at" : 1745480801740,
76+
"published_at" : 1742314598996,
77+
"paths" : { },
78+
"comment_required" : false,
79+
"flows" : [ {
80+
"id" : "29fef3df-5a55-4e45-bef3-df5a559e4526",
81+
"path-operator" : {
82+
"path" : "/",
83+
"operator" : "STARTS_WITH"
84+
},
85+
"condition" : "",
86+
"consumers" : [ ],
87+
"methods" : [ ],
88+
"pre" : [ ],
89+
"post" : [ ],
90+
"enabled" : true
91+
} ]
92+
} ],
93+
"metadata" : [ {
94+
"key" : "email-support",
95+
"name" : "email-support",
96+
"format" : "MAIL",
97+
"value" : "${(api.primaryOwner.email)!''}",
98+
"defaultValue" : "[email protected]",
99+
"apiId" : "c57c319b-62b6-427c-bc31-9b62b6527c37"
100+
} ],
101+
"id" : "c57c319b-62b6-427c-bc31-9b62b6527c37",
102+
"path_mappings" : [ ],
103+
"proxy" : {
104+
"virtual_hosts" : [ {
105+
"path" : "/test/"
106+
} ],
107+
"strip_context_path" : false,
108+
"preserve_host" : false,
109+
"logging" : {
110+
"mode" : "CLIENT_PROXY",
111+
"content" : "HEADERS_PAYLOADS",
112+
"scope" : "REQUEST_RESPONSE",
113+
"condition" : "{#request.timestamp <= 1688987810835l}"
114+
},
115+
"groups" : [ {
116+
"name" : "default-group",
117+
"endpoints" : [ {
118+
"name" : "default",
119+
"target" : "https://webhook.site/356688cd-27d5-4365-ab40-963a7e945e35",
120+
"weight" : 1,
121+
"backup" : false,
122+
"status" : "UP",
123+
"type" : "http",
124+
"inherit" : true,
125+
"proxy" : null,
126+
"http" : null,
127+
"ssl" : null,
128+
"healthcheck" : {
129+
"enabled" : true,
130+
"inherit" : true
131+
}
132+
} ],
133+
"load_balancing" : {
134+
"type" : "ROUND_ROBIN"
135+
},
136+
"services" : {
137+
"discovery" : {
138+
"enabled" : false
139+
}
140+
},
141+
"http" : {
142+
"connectTimeout" : 5000,
143+
"idleTimeout" : 60000,
144+
"keepAliveTimeout" : 300000,
145+
"keepAlive" : false,
146+
"readTimeout" : 300000,
147+
"pipelining" : false,
148+
"maxConcurrentConnections" : 100,
149+
"useCompression" : true,
150+
"followRedirects" : false
151+
},
152+
"ssl" : {
153+
"trustAll" : false,
154+
"hostnameVerifier" : false
155+
}
156+
} ]
157+
},
158+
"response_templates" : { },
159+
"primaryOwner" : {
160+
"id" : "4407b94b-a747-41d6-87b9-4ba747f1d67c",
161+
"displayName" : "admin",
162+
"type" : "USER"
163+
},
164+
"disable_membership_notifications" : false
165+
}

0 commit comments

Comments
 (0)