Skip to content

fix: add validation for cron in page source during import to prevent malformed cron expressions #12324

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 4.5.x
Choose a base branch
from

Conversation

vikrantgravitee
Copy link
Contributor

@vikrantgravitee vikrantgravitee commented Jun 29, 2025

Issue

https://gravitee.atlassian.net/browse/APIM-9446

Description

This commit adds validation logic for the cron expression field in page sources (e.g., GitHub fetcher). Previously, malformed cron expressions (e.g., with only 5 fields) were silently accepted during import or upgrade, leading to inconsistencies in behavior.

Issue:

APIM-9446.mov

Fix:

Fix_APIM-9446.mov

Additional context


📚  View the storybook of this branch here

Copy link

cypress bot commented Jun 29, 2025

APIM UI Tests    Run #5301

Run Properties:  status check passed Passed #5301  •  git commit 358c63b0d8: fix: add validation for cron in page source during import to prevent malformed c...
Project APIM UI Tests
Branch Review APIM-9446-fix-cron-check-api-import
Run status status check passed Passed #5301
Run duration 06m 51s
Commit git commit 358c63b0d8: fix: add validation for cron in page source during import to prevent malformed c...
Committer vikrant
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 8
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 82
View all changes introduced in this branch ↗︎

@vikrantgravitee vikrantgravitee force-pushed the APIM-9446-fix-cron-check-api-import branch 4 times, most recently from fedd86f to 2b061d0 Compare July 2, 2025 13:27
Comment on lines +1097 to +1113
for (JsonNode pageNode : apiJsonNode.getPagesArray()) {
JsonNode sourceNode = pageNode.path("source");
JsonNode configNode = sourceNode.path("configuration");

if (!configNode.isMissingNode() && configNode.has("fetchCron")) {
String cron = configNode.path("fetchCron").asText(null);

if (cron != null && !cron.isEmpty()) {
try {
CronExpression.parse(cron); // Validate cron
} catch (IllegalArgumentException e) {
String pageName = pageNode.path("name").asText("Unnamed Page");
throw new ApiImportException("Invalid fetchCron expression in page '" + pageName + "': " + e.getMessage());
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be run whenever a page is created or modified?
I wonder if there is a more central page validation service to add this to? 🤔

Comment on lines 334 to 352
public static void validateFetchConfig(String jsonConfig) {
ObjectMapper mapper = new ObjectMapper();
try {
JsonNode root = mapper.readTree(jsonConfig);
boolean autoFetch = root.path("autoFetch").asBoolean(false);
if (autoFetch) {
String fetchCron = root.path("fetchCron").asText();
if (fetchCron == null || fetchCron.isEmpty()) {
throw new IllegalArgumentException("fetchCron is required when autoFetch is true.");
}
// Throws IllegalArgumentException if cron expression is invalid
CronExpression.parse(fetchCron);
}
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid cron expression: " + e.getMessage(), e);
} catch (Exception e) {
throw new RuntimeException("Failed to parse configuration JSON: " + e.getMessage(), e);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this could throw a gravitee exception and add the exception to the signature?
I believe we try to avoid throwing RuntimeExceptions 🤔

@vikrantgravitee vikrantgravitee force-pushed the APIM-9446-fix-cron-check-api-import branch from 50e25f1 to b205cee Compare July 4, 2025 04:31
Comment on lines +333 to +353
public static void validateFetchConfig(String jsonConfig) throws TechnicalException {
ObjectMapper mapper = new ObjectMapper();
try {
JsonNode root = mapper.readTree(jsonConfig);
boolean autoFetch = root.path("autoFetch").asBoolean(false);
String fetchCron = root.path("fetchCron").asText();
if (autoFetch) {
if (fetchCron == null || fetchCron.isEmpty()) {
throw new TechnicalException("fetchCron is required when autoFetch is true.");
}
}
if (fetchCron != null && !fetchCron.isEmpty() && !"null".equals(fetchCron)) {
CronExpression.parse(fetchCron);
}
} catch (IllegalArgumentException e) {
throw new TechnicalException("Invalid cron expression: " + e.getMessage(), e);
} catch (Exception e) {
throw new TechnicalException("Failed to parse configuration JSON: " + e.getMessage(), e);
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two things that I suggest improving in this method:

  1. Not to throw an exception, catch it, then rethrow it
  2. Using exceptions in io.gravitee.rest.api.service.exceptions; instead of io.gravitee.repository.exceptions; to have the desired impact when the user calls the resource

By throwing a TechnicalException that is specific to repository errors, the user will receive a 500 error if their CRON is invalid. This should probably be a 400. You can create a new exception PageCronExpressionInvalidException (or however you want to name it) that extends AbstractManagementException and returns HttpStatusCode.BAD_REQUEST_400 for getHttpCode.

A suggested refactoring:

Suggested change
public static void validateFetchConfig(String jsonConfig) throws TechnicalException {
ObjectMapper mapper = new ObjectMapper();
try {
JsonNode root = mapper.readTree(jsonConfig);
boolean autoFetch = root.path("autoFetch").asBoolean(false);
String fetchCron = root.path("fetchCron").asText();
if (autoFetch) {
if (fetchCron == null || fetchCron.isEmpty()) {
throw new TechnicalException("fetchCron is required when autoFetch is true.");
}
}
if (fetchCron != null && !fetchCron.isEmpty() && !"null".equals(fetchCron)) {
CronExpression.parse(fetchCron);
}
} catch (IllegalArgumentException e) {
throw new TechnicalException("Invalid cron expression: " + e.getMessage(), e);
} catch (Exception e) {
throw new TechnicalException("Failed to parse configuration JSON: " + e.getMessage(), e);
}
}
public static void validateFetchConfig(String jsonConfig) throws PageCronExpressionInvalidException {
ObjectMapper mapper = new ObjectMapper();
JsonNode root;
try {
root = mapper.readTree(jsonConfig);
} catch (IOException e) {
throw new PageCronExpressionInvalidException("Invalid JSON format: " + e.getMessage(), e);
}
boolean autoFetch = root.path("autoFetch").asBoolean(false);
String fetchCron = root.path("fetchCron").asText(null); // default to null
if (autoFetch && (fetchCron == null || fetchCron.trim().isEmpty() || "null".equalsIgnoreCase(fetchCron))) {
throw new PageCronExpressionInvalidException("fetchCron is required when autoFetch is true.");
}
if (fetchCron != null && !fetchCron.trim().isEmpty() && !"null".equalsIgnoreCase(fetchCron)) {
try {
CronExpression.parse(fetchCron);
} catch (IllegalArgumentException e) {
throw new PageCronExpressionInvalidException("Invalid cron expression: " + e.getMessage(), e);
}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can probably make a cleaner solution that what I proposed...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants