Repository navigation
Fix note-level CodeQL findings across core, oauth2 and doc - #178
Conversation
maximthomas
left a comment
There was a problem hiding this comment.
praise: The NumberFormatException paths now fail with errors that say what is wrong and reach the right handler.
ICAPFilter.Heaplet.timeout(name, default)reports the bad field's JSON pointer, andICAPFilterTest's non-numeric data-provider case pins it.- A non-numeric
expires_innow raisesOAuth2ErrorExceptionfromOAuth2Session.stateAuthorized, which its only caller routes tohandleOAuth2ErrorException(OAuth2ClientFilter.java:541-575).
issue (non-blocking): PrincipalTest pins only ttlMillis(); nothing fails if authorize() goes back to parsing the property on every call.
openig-core/src/main/java/org/openidentityplatform/openig/websocket/Principal.java:51, :65-66, openig-core/src/test/java/org/openidentityplatform/openig/websocket/PrincipalTest.java:26-37
Both tests call Principal.ttlMillis(String) with literals. authorize() is reached only from websocket/Filter.java:77 and ServerEndPoint.java:51, and no test calls either. So replacing final long ttl=TTL_MILLIS; at :66 with 0, or with the base's Long.parseLong(System.getProperty(...,"180"))*1000 (the NumberFormatException of alert #50), keeps both tests green. PrincipalTest is the only test for #50. authorize() gets its handler from the static HttpFrameworkServlet.getGatewayHttpFrameworkServlet(), so the cheap seam is the interval check. The test below also pins the read-once behaviour that the description calls out.
// Principal.java — both checks in authorize() become expired(System.currentTimeMillis())
boolean expired(long now) {
return res==null || now-lastAuthorize>=TTL_MILLIS;
}
// PrincipalTest.java (import org.forgerock.http.protocol.Status)
@Test
public void shouldReauthorizeWithTtlReadAtClassLoad() {
Principal principal = new Principal(null, null);
principal.res = Status.OK;
principal.lastAuthorize = 1_000L;
long ttl = Principal.ttlMillis(System.getProperty(Principal.TTL_PROPERTY));
System.setProperty(Principal.TTL_PROPERTY, "0");
try {
assertThat(principal.expired(1_000L + ttl - 1)).isFalse();
assertThat(principal.expired(1_000L + ttl)).isTrue();
} finally {
System.clearProperty(Principal.TTL_PROPERTY);
}
}Pin: this kills ttl = 0 and the per-call read at :66. The ttlMillis(null) mutant at :51 would need a forked JVM with the property set.
question (non-blocking): Should an Error thrown by the target constructor still propagate from JsonValues.asNewInstance as before, rather than arrive as the cause of a JsonValueException?
openig-core/src/main/java/org/forgerock/openig/util/JsonValues.java:155-161
Class.newInstance() rethrew the constructor's own throwable. getDeclaredConstructor().newInstance() wraps it in InvocationTargetException, and :158 makes the target the cause of a JsonValueException. A constructor that throws IllegalStateException, NoClassDefFoundError or OutOfMemoryError now reaches the caller as a JsonValueException. Nothing in the repo calls asNewInstance, so only code outside it can notice. Wrapping exceptions adds the JSON pointer, which helps. Wrapping an Error turns a JVM failure into a configuration error. Minor either way. If wrapping Errors is not intended:
} catch (InvocationTargetException e) {
final Throwable target = e.getTargetException();
if (target instanceof Error) {
throw (Error) target;
}
throw new JsonValueException(value, target);
}suggestion (non-blocking): shouldStateAuthorizedWithNonNumericExpiresInFailWithOAuth2Error does not assert the server_error code.
openig-oauth2/src/test/java/org/forgerock/openig/filter/oauth2/client/OAuth2SessionTest.java:182-192
The test is red at the base (a raw NumberFormatException), so the fix is pinned. But "expires_in" matches the description, not the error code. Passing E_INVALID_REQUEST instead of E_SERVER_ERROR keeps the test green, and the description gives consistency with the sibling server_error branch as the reason for the change.
assertThatThrownBy(() -> OAuth2Session.stateNew(time).stateAuthorized(accessTokenResponse))
.isInstanceOfSatisfying(OAuth2ErrorException.class, e ->
assertThat(e.getOAuth2Error().getError()).isEqualTo(OAuth2Error.E_SERVER_ERROR))
.hasMessageContaining("expires_in");suggestion (non-blocking): No test pins the ICAPFilter timeout defaults (5000 / 15000 ms).
openig-core/src/main/java/org/openidentityplatform/openig/filter/ICAPFilter.java:77-78, openig-core/src/test/java/org/openidentityplatform/openig/filter/ICAPFilterTest.java:35-45
shouldConfigureTimeouts sets both fields, and it passes at the base too. The non-numeric case checks only the exception. Swapping the defaults in timeout("connect_timeout", "5000") / timeout("read_timeout", "15000") keeps every test green.
@Test
public void shouldApplyDefaultTimeouts() throws Exception {
ICAPFilter filter = (ICAPFilter) new ICAPFilter.Heaplet().create(
Name.of("icap"),
json(object(field("server", "icap://localhost:1344"))),
HeapUtilsTest.buildDefaultHeap());
assertThat(filter.icap.getConnectTimeout()).isEqualTo(5000);
assertThat(filter.icap.getReadTimeout()).isEqualTo(15000);
}suggestion (non-blocking): The latitude/longitude branches of MockDataGenerator have no test.
openig-core/src/main/java/org/forgerock/openig/handler/MockDataGenerator.java:294-297
No test or test resource in any module uses a latitude or longitude field name. A mutant that uses randomDouble(6, -180, 180) for latitude, or returns a String, passes every test.
@Test
public void generate_returnsCoordinatesInRange() {
Schema<Object> schema = new Schema<>();
schema.setType("number");
for (int i = 0; i < 100; i++) {
final Object latitude = MockDataGenerator.generate("latitude", schema);
final Object longitude = MockDataGenerator.generate("longitude", schema);
assertThat(latitude).isInstanceOf(Number.class);
assertThat(((Number) latitude).doubleValue()).isBetween(-90.0, 90.0);
assertThat(((Number) longitude).doubleValue()).isBetween(-180.0, 180.0);
}
}suggestion (non-blocking): No test exercises the default response fallback of bestResponseSchema.
openig-core/src/main/java/org/forgerock/openig/handler/OpenApiMockResponseHandler.java:268-269
responses.get(ApiResponses.DEFAULT) does the same lookup as getDefault(). The bestResponseSchema tests build only 2xx responses, though, so return null; at :269 passes them all.
@Test
public void bestResponseSchema_fallsBackToDefaultResponse() {
final Operation op = buildOperation("default");
assertThat(OpenApiMockResponseHandler.bestResponseSchema(op).getDescription())
.isEqualTo("schema-default");
}7841537 to
c700163
Compare
|
Addressed in c700163. The branch is rebased onto current issue — question — suggestions: all four added as proposed:
Each new check was run against its mutant (per-call TTL read, |
maximthomas
left a comment
There was a problem hiding this comment.
praise: the round-1 points are addressed where the behaviour lives, and the new error paths name what failed.
ICAPFilter.timeoutbuilds theJsonValueExceptionfromconfig.get(name).defaultTo(...), the JSON value before evaluation, so the message carries/connect_timeoutor/read_timeout.ICAPFilterTestcan only match the field name through that pointer.JsonValues.asNewInstancenow rethrows anErrorfrom the constructor and wraps exceptions, as its Javadoc describes ("as if by anewexpression").responses.get(ApiResponses.DEFAULT)is exactly whatgetDefault()did in swagger-models 2.2.37 and 2.2.45 (get("default")).
issue (non-blocking): PrincipalTest still never calls authorize(), so it does not pin that authorize() uses the interval read at class load.
openig-core/src/test/java/org/openidentityplatform/openig/websocket/PrincipalTest.java:41-56, openig-core/src/main/java/org/openidentityplatform/openig/websocket/Principal.java:66-80
shouldReauthorizeWithTtlReadAtClassLoad only asserts expired(...), and no test in the repo calls authorize(). I reverted authorize() to the BASE per-call Long.parseLong(System.getProperty(TTL_PROPERTY, "180"))*1000 and left expired()/TTL_MILLIS untouched. With that mutant all 3 PrincipalTest methods still pass (JDK 26). The mutant also brings back alert #50's per-message NumberFormatException for an invalid property value. Dropping res==null || from expired() survives too, because the only expired() test sets res = Status.OK first.
@Test
public void shouldAuthorizeFromCacheWithTtlReadAtClassLoad() throws Exception {
final Principal principal = new Principal(null, null);
principal.res = Status.OK;
principal.lastAuthorize = System.currentTimeMillis();
// A per-call read of "0" would re-authorize through the gateway servlet instead of the cache
final String previous = System.setProperty(Principal.TTL_PROPERTY, "0");
try {
assertThat(principal.authorize()).isSameAs(Status.OK);
} finally {
if (previous == null) {
System.clearProperty(Principal.TTL_PROPERTY);
} else {
System.setProperty(Principal.TTL_PROPERTY, previous);
}
}
}
@Test
public void shouldExpireWithoutCachedResult() {
assertThat(new Principal(null, null).expired(1_000L)).isTrue();
}Pin: the first test fails under the per-call mutant, which leaves the cache and goes to HttpFrameworkServlet.getGatewayHttpFrameworkServlet(). The second test kills the res==null drop. Neither test covers a mutant that ignores the TTL entirely, because that needs a gateway-servlet stand-in. This review did not compile or run the snippet.
issue (non-blocking): with -Djavax.net.ssl.keyStoreProvider naming a missing provider, SampleApplication now starts the HTTPS listener with no server key, where it used to skip HTTPS.
openig-doc/src/main/java/org/forgerock/openig/doc/SampleApplication.java:235, :248-256
createSSLContext(true) only logs a key-store NoSuchProviderException, in both grizzly-framework 3.0.1 and 4.0.2. The key manager stays null and SSLContext.init(null, null, null) succeeds. The removed validateConfiguration(true) returned false for this configuration, so BASE logged "Failed to build a valid HTTPS configuration." and added no listener. A probe against both jars confirmed this; the TLS handshake failure that follows was not run. The description says GenericStoreException is "what Grizzly 4.0.2 throws", but openig-doc resolves grizzly-framework 3.0.1, and neither version throws for this input. validateConfiguration(boolean) is deprecated in both versions, so putting it back would bring the alert back.
SSLContextConfigurator sslContextConfigurator = new SSLContextConfigurator(false);Or: keep the head and correct the "Grizzly 4.0.2" sentence. SSLContextConfigurator(boolean) is public and not deprecated in 3.0.1 or 4.0.2. It reads no javax.net.ssl.* system property, so a bogus provider never gets in. Whether the sample relies on any other javax.net.ssl.* property was not checked.
suggestion (non-blocking): no test pins that an ExceptionInInitializerError raised inside the constructor now escapes asNewInstance raw.
openig-core/src/main/java/org/forgerock/openig/util/JsonValues.java:157-163
At BASE, Class.newInstance() rethrew such an error and catch (ExceptionInInitializerError | ...) wrapped it. The head rethrows it, in line with the "Error propagates" rule and with Class.forName(name, true, ...) at :102, which already lets a target-class static-init error out raw. The only Error test throws NoClassDefFoundError, which BASE also let out raw, so wrapping this error again would fail no test. A probe on JDK 8 through 26 showed BASE wrapped and head raw.
@Test(expectedExceptions = ExceptionInInitializerError.class)
public void shouldAsNewInstancePropagateInitializerErrorRaisedByConstructor() throws Exception {
asNewInstance(json(ConstructorTouchingBrokenClass.class.getName()), Object.class);
}
public static class ConstructorTouchingBrokenClass {
public ConstructorTouchingBrokenClass() {
BrokenStaticInitializer.touch();
}
}
static class BrokenStaticInitializer {
static final int VALUE = Integer.parseInt("boom");
static int touch() {
return VALUE;
}
}Pin: the test fails if the Error branch wraps ExceptionInInitializerError again. This review did not compile or run the snippet.
nitpick (non-blocking): ICAPFilter.timeout reports "Expecting a number of milliseconds" for a number that does not fit in an int.
openig-core/src/main/java/org/openidentityplatform/openig/filter/ICAPFilter.java:87-96
connect_timeout: "99999999999" fails with "/connect_timeout: Expecting a number of milliseconds, got: 99999999999". The Javadoc also says "when the value is not a number".
/** Reads a timeout (in milliseconds) from the config, naming the field when the value is not an int. */
...
throw new JsonValueException(value, "Expecting a number of milliseconds that fits in an int, got: "+evaluated, e);- OAuth2Session: fix the missing space in the "ID token could not be decoded" message, report a non-numeric expires_in as an OAuth2 server error instead of a raw NumberFormatException, and stop shadowing the accessTokenResponse field - ICAPFilter: name the offending field when connect_timeout or read_timeout is not a number - Principal: parse the websocket TTL system property once, falling back to the default with a warning when it is not a number - LdapClient: drop finalize(), which never runs on the singleton - Heaplets, JsonValues: replace the deprecated Class.newInstance() - OpenApiMockResponseHandler: replace the deprecated ApiResponses.getDefault() - MockDataGenerator: generate coordinates as numbers instead of parsing locale-formatted strings - SampleApplication: use SSLContextConfigurator.createSSLContext(true) and print the usage on a non-numeric port argument
- Principal: check the re-authorization interval through expired(now) and pin in a test that the TTL is read once at class load - JsonValues.asNewInstance, Heaplets.getHeaplet: let an Error thrown by the constructor propagate, as Class.newInstance() did, instead of wrapping it or logging it away - Tests: assert the server_error code for a non-numeric expires_in, the ICAP default timeouts, the coordinate ranges in MockDataGenerator and the default-response fallback of bestResponseSchema
- PrincipalTest: pin that authorize() serves the cached result with the TTL read at class load, and that a missing result is always refreshed - SampleApplication: build the SSL context without the javax.net.ssl.* system properties, so a missing key store provider cannot leave the HTTPS listener without a key - JsonValuesTest: pin that an ExceptionInInitializerError raised by the constructor propagates from asNewInstance - ICAPFilter: say that the timeout must fit in an int
c700163 to
d5133ba
Compare
|
Addressed in d5133ba. The branch is rebased onto current issue — issue — bogus suggestion — nitpick —
|
maximthomas
left a comment
There was a problem hiding this comment.
praise: The round-3 tests each fail when their fix is reverted.
PrincipalTest.shouldAuthorizeFromCacheWithTtlReadAtClassLoadturns red whenPrincipal.authorize()goes back to readingorg.openidentityplatform.openig.websocket.ttlon every call, andshouldExpireWithoutCachedResultturns red withoutres==nullinexpired(long). Both are green at the head.JsonValuesTest.shouldAsNewInstancePropagateInitializerErrorRaisedByConstructorturns red whenasNewInstancewraps theExceptionInInitializerErrorin aJsonValueExceptionagain. It is green at the head.SampleApplication.createSslConfigurationnow buildsnew SSLContextConfigurator(false). A deployer'sjavax.net.ssl.*properties can no longer start the sample's HTTPS listener without a key.
Closes the 16
note-level code scanning alerts that are worth fixing (#10–#15, #32, #46–#53, #58). The remaining notes are either pure@Override/staticcosmetics (done separately in #180) or by-design findings to be dismissed.openig-oauth2—OAuth2Sessionmissing-space-in-concatenation"…ID token" + "could not be decoded"producedID tokencould not be decodedin theserver_errordescription.uncaught-number-format-exceptionexpires_inthat is not a number now raisesOAuth2ErrorException(E_SERVER_ERROR, "'expires_in' field value is not a number: …"), consistent with the sibling "neither a Number nor a String" branch, instead of a rawNumberFormatException.local-shadows-fieldmergedResponse; its uses (extractIdToken, the constructor) are pointed at it. No behavior change —testStateRefreshedcovers the merge semantics.openig-coreuncaught-number-format-exceptionICAPFilter: aconnect_timeout/read_timeoutthat is not anintnow fails with aJsonValueExceptionthat points at the field (/connect_timeout: Expecting a number of milliseconds that fits in an int, got: soon) instead ofNumberFormatException: For input string: "soon".deprecated-call(Object.finalize)LdapClient.finalize()removed: the class is a singleton (INSTANCE), so the finalizer never ran.deprecated-call(Class.newInstance)Heaplets,JsonValues:getDeclaredConstructor().newInstance(). TheInvocationTargetExceptionit adds is unwrapped so that anErrorthrown by the constructor still propagates as it did withClass.newInstance(). OtherwiseJsonValues.asNewInstancemakes the constructor's own exception the cause of aJsonValueException(other reflective failures are caught asReflectiveOperationException), andHeaplets.getHeapletlogs and ignores it as before. One difference: anExceptionInInitializerErrorraised inside the constructor (by touching a class whose static initializer fails) used to be wrapped byasNewInstance'scatch (ExceptionInInitializerError …); it now propagates like any otherError, as it already did fromClass.forName(name, true, …)for the target class itself and fromHeaplets.getHeaplet.deprecated-callOpenApiMockResponseHandler:responses.get(ApiResponses.DEFAULT).uncaught-number-format-exceptionPrincipal: theorg.openidentityplatform.openig.websocket.ttlproperty is parsed once at class load byttlMillis(String); an invalid value logs a warning and falls back to 180 s instead of throwing on every websocket message.authorize()checks the interval throughexpired(now). Semantic change: the property used to be re-read on everyauthorize()call, i.e. it could be changed at runtime withSystem.setProperty. I assumed that is not a supported use — say so if it is.uncaught-number-format-exceptionMockDataGenerator: latitude/longitude generated withFAKER.number().randomDouble(6, …)instead of parsing a locale-formatted string.openig-doc—SampleApplicationdeprecated-callvalidateConfiguration(true)+createSSLContext()→createSSLContext(true), catchingSSLContextConfigurator.GenericStoreException, which grizzly-framework 3.0.1 (the versionopenig-docresolves, throughopendj-grizzly) throws when the bundled key store cannot be loaded. The configurator is created withSSLContextConfigurator(false), so it no longer reads thejavax.net.ssl.*system properties: the sample sets its own key store and passwords, andcreateSSLContext(true)only logs a missing key store provider, which would have left the HTTPS listener without a key. Exercised by theopenig-docintegration tests, which start the sample with HTTPS.uncaught-number-format-exceptionPort numbers must be integers: …plus the usage and exits, instead of a stack trace.Tests
OAuth2SessionTest:shouldStateAuthorizedWithNonNumericExpiresInFailWithOAuth2Error(failed before withNumberFormatException: For input string: "soon"; asserts theserver_errorcode) andshouldStateAuthorizedWithUndecodableIdTokenFailWithReadableMessage(failed before withID tokencould).ICAPFilterTest(new): configured and default (5000 / 15000 ms) timeouts are applied; a non-numericconnect_timeout/read_timeout(data provider) fails with aJsonValueExceptionnaming the field — failed before with a bareNumberFormatException.PrincipalTest(new):ttlMillis— seconds to millis, trimming, and the fallback fornull/ empty / non-numeric values;shouldReauthorizeWithTtlReadAtClassLoadandshouldAuthorizeFromCacheWithTtlReadAtClassLoadpin that the interval is read once at class load, inexpired()and throughauthorize();shouldExpireWithoutCachedResultpins that a missing result is always refreshed.JsonValuesTest,HeapletsTest: anErrorthrown by the constructor propagates, including anExceptionInInitializerErrorraised in it (asNewInstance); an exception is wrapped in aJsonValueException(asNewInstance) or ignored (getHeaplet).MockDataGeneratorTest.generate_returnsCoordinatesInRange,OpenApiMockResponseHandlerTest.bestResponseSchema_fallsBackToDefaultResponse.Each test added in the review rounds was run against the mutant it targets (per-call TTL read in
expired()and inauthorize(),res==nulldropped fromexpired(),Errorwrapped or swallowed,ExceptionInInitializerErrorwrapped again,E_INVALID_REQUEST, swapped ICAP defaults, latitude in ±180,return nullfordefault): all ten fail.Verified, rebased on current
master:openig-core1029 tests,openig-oauth2160 andopenig-doc11 (integration tests,testGetLoginPageHttpsincluded) undermvn verify, javadoc withfailOnWarningsincluded.#173, which also touches
Heaplets.java(thewarnline, left untouched here) andHeapletsTest.java, has been merged; the branch is rebased onto it, andHeapletsTestkeeps the tests of both PRs.