|
| 1 | +/* |
| 2 | + * The contents of this file are subject to the terms of the Common Development and |
| 3 | + * Distribution License (the License). You may not use this file except in compliance with the |
| 4 | + * License. |
| 5 | + * |
| 6 | + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the |
| 7 | + * specific language governing permission and limitations under the License. |
| 8 | + * |
| 9 | + * When distributing Covered Software, include this CDDL Header Notice in each file and include |
| 10 | + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL |
| 11 | + * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| 12 | + * information: "Portions copyright [year] [name of copyright owner]". |
| 13 | + * |
| 14 | + * Copyright 2026 3A Systems, LLC. |
| 15 | + */ |
| 16 | +package org.forgerock.openidm.quartz.impl; |
| 17 | + |
| 18 | +import static org.assertj.core.api.Assertions.assertThat; |
| 19 | +import static org.assertj.core.api.Assertions.assertThatThrownBy; |
| 20 | + |
| 21 | +import java.io.File; |
| 22 | +import java.io.InvalidClassException; |
| 23 | +import java.io.Serializable; |
| 24 | +import java.util.Calendar; |
| 25 | +import java.util.Date; |
| 26 | +import java.util.GregorianCalendar; |
| 27 | +import java.util.TimeZone; |
| 28 | + |
| 29 | +import org.quartz.JobDataMap; |
| 30 | +import org.quartz.JobDetail; |
| 31 | +import org.quartz.JobPersistenceException; |
| 32 | +import org.quartz.SimpleTrigger; |
| 33 | +import org.quartz.impl.calendar.AnnualCalendar; |
| 34 | +import org.testng.annotations.Test; |
| 35 | + |
| 36 | +/** |
| 37 | + * Tests the serialize/deserialize round trip in {@link RepoJobStoreUtils}, |
| 38 | + * ensuring the deserialization filter (GHSA-xjw4-4w2v-rp6g, CWE-502) accepts |
| 39 | + * everything the scheduler legitimately persists — including the OpenIDM job |
| 40 | + * implementation class carried by {@link JobDetail} — while rejecting foreign |
| 41 | + * classes and oversized payloads. |
| 42 | + */ |
| 43 | +public class RepoJobStoreUtilsTest { |
| 44 | + |
| 45 | + @Test |
| 46 | + public void shouldRoundTripJobDetailWithSchedulerServiceJobClass() throws Exception { |
| 47 | + JobDetail job = new JobDetail("job", "group", SchedulerServiceJob.class); |
| 48 | + JobDataMap data = job.getJobDataMap(); |
| 49 | + data.put("invokeService", "org.forgerock.openidm.script"); |
| 50 | + data.put("count", 42L); |
| 51 | + data.put("enabled", Boolean.TRUE); |
| 52 | + data.put("createdAt", new Date(1234567890000L)); |
| 53 | + |
| 54 | + JobDetail restored = (JobDetail) RepoJobStoreUtils.deserialize(RepoJobStoreUtils.serialize(job)); |
| 55 | + |
| 56 | + assertThat(restored.getName()).isEqualTo("job"); |
| 57 | + assertThat(restored.getGroup()).isEqualTo("group"); |
| 58 | + assertThat(restored.getJobClass()).isEqualTo(SchedulerServiceJob.class); |
| 59 | + assertThat(restored.getJobDataMap().getString("invokeService")).isEqualTo("org.forgerock.openidm.script"); |
| 60 | + assertThat(restored.getJobDataMap().getLong("count")).isEqualTo(42L); |
| 61 | + assertThat(restored.getJobDataMap().getBoolean("enabled")).isTrue(); |
| 62 | + assertThat(restored.getJobDataMap().get("createdAt")).isEqualTo(new Date(1234567890000L)); |
| 63 | + } |
| 64 | + |
| 65 | + @Test |
| 66 | + public void shouldRoundTripJobDetailWithStatefulSchedulerServiceJobClass() throws Exception { |
| 67 | + JobDetail job = new JobDetail("job", "group", StatefulSchedulerServiceJob.class); |
| 68 | + |
| 69 | + JobDetail restored = (JobDetail) RepoJobStoreUtils.deserialize(RepoJobStoreUtils.serialize(job)); |
| 70 | + |
| 71 | + assertThat(restored.getJobClass()).isEqualTo(StatefulSchedulerServiceJob.class); |
| 72 | + } |
| 73 | + |
| 74 | + @Test |
| 75 | + public void shouldRoundTripSimpleTrigger() throws Exception { |
| 76 | + Date startTime = new Date(1234567890000L); |
| 77 | + Date endTime = new Date(1234567990000L); |
| 78 | + SimpleTrigger trigger = new SimpleTrigger("trigger", "group", startTime, endTime, 5, 1000L); |
| 79 | + |
| 80 | + SimpleTrigger restored = (SimpleTrigger) RepoJobStoreUtils.deserialize(RepoJobStoreUtils.serialize(trigger)); |
| 81 | + |
| 82 | + assertThat(restored.getName()).isEqualTo("trigger"); |
| 83 | + assertThat(restored.getGroup()).isEqualTo("group"); |
| 84 | + assertThat(restored.getStartTime()).isEqualTo(startTime); |
| 85 | + assertThat(restored.getEndTime()).isEqualTo(endTime); |
| 86 | + assertThat(restored.getRepeatCount()).isEqualTo(5); |
| 87 | + assertThat(restored.getRepeatInterval()).isEqualTo(1000L); |
| 88 | + } |
| 89 | + |
| 90 | + @Test |
| 91 | + public void shouldRoundTripAnnualCalendarWithTimeZone() throws Exception { |
| 92 | + AnnualCalendar calendar = new AnnualCalendar(); |
| 93 | + calendar.setTimeZone(TimeZone.getTimeZone("America/New_York")); |
| 94 | + Calendar excluded = new GregorianCalendar(2026, Calendar.JANUARY, 1); |
| 95 | + calendar.setDayExcluded(excluded, true); |
| 96 | + |
| 97 | + AnnualCalendar restored = (AnnualCalendar) RepoJobStoreUtils.deserialize(RepoJobStoreUtils.serialize(calendar)); |
| 98 | + |
| 99 | + assertThat(restored.getTimeZone().getID()).isEqualTo("America/New_York"); |
| 100 | + assertThat(restored.isDayExcluded(excluded)).isTrue(); |
| 101 | + } |
| 102 | + |
| 103 | + @Test |
| 104 | + public void shouldRejectForeignSerializable() throws Exception { |
| 105 | + String serialized = RepoJobStoreUtils.serialize(new File("foreign")); |
| 106 | + |
| 107 | + assertThatThrownBy(() -> RepoJobStoreUtils.deserialize(serialized)) |
| 108 | + .isInstanceOf(JobPersistenceException.class) |
| 109 | + .hasCauseInstanceOf(InvalidClassException.class); |
| 110 | + } |
| 111 | + |
| 112 | + @Test |
| 113 | + public void shouldRejectSerializedLambda() throws Exception { |
| 114 | + Runnable lambda = (Runnable & Serializable) () -> { }; |
| 115 | + String serialized = RepoJobStoreUtils.serialize((Serializable) lambda); |
| 116 | + |
| 117 | + assertThatThrownBy(() -> RepoJobStoreUtils.deserialize(serialized)) |
| 118 | + .isInstanceOf(JobPersistenceException.class) |
| 119 | + .hasCauseInstanceOf(InvalidClassException.class); |
| 120 | + } |
| 121 | + |
| 122 | + @Test |
| 123 | + public void shouldRejectQuartzNativeJob() throws Exception { |
| 124 | + JobDetail job = new JobDetail("job", "group", org.quartz.jobs.NativeJob.class); |
| 125 | + String serialized = RepoJobStoreUtils.serialize(job); |
| 126 | + |
| 127 | + assertThatThrownBy(() -> RepoJobStoreUtils.deserialize(serialized)) |
| 128 | + .isInstanceOf(JobPersistenceException.class) |
| 129 | + .hasCauseInstanceOf(InvalidClassException.class); |
| 130 | + } |
| 131 | + |
| 132 | + @Test |
| 133 | + public void shouldRejectOversizedArray() throws Exception { |
| 134 | + String serialized = RepoJobStoreUtils.serialize(new byte[2_000_000]); |
| 135 | + |
| 136 | + assertThatThrownBy(() -> RepoJobStoreUtils.deserialize(serialized)) |
| 137 | + .isInstanceOf(JobPersistenceException.class) |
| 138 | + .hasCauseInstanceOf(InvalidClassException.class); |
| 139 | + } |
| 140 | + |
| 141 | +} |
0 commit comments