From fb1086b4f0c042647f60644fd4c3e1df4821dfb1 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Mon, 28 Sep 2026 18:28:08 -0700 Subject: [PATCH 1/8] API key auth optional feature flag --- .../onprc_ehr/ONPRC_SsrsSessionKeyTest.java | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java b/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java index 5a6df3ab2..e9d50fb6a 100644 --- a/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java +++ b/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java @@ -24,6 +24,7 @@ import org.labkey.test.TestTimeoutException; import org.labkey.test.WebTestHelper; import org.labkey.test.categories.ONPRC; +import org.labkey.test.util.OptionalFeatureHelper; import org.labkey.test.util.PasswordUtil; import org.labkey.test.util.SimpleHttpRequest; import org.labkey.test.util.SimpleHttpResponse; @@ -55,7 +56,7 @@ * onprc_ehr-getSessionId.api, which returns a session key (see ONPRC_EHRController.GetSessionIdAction). * 2. Clicking a report builds a URL to the SSRS server carrying that key as the "SessionId" parameter. * 3. SSRS, running on a separate host with no LabKey cookie, calls back to a selectRows URL, passing the - * key as the "LabKeyTransformSessionId" query parameter. SecurityManager.getApiKey() reads it from the + * key as the "apikey" query parameter. SecurityManager.getApiKey() reads it from the * query string and SessionApiKeyManager resolves it back to the user's session. *

* The only thing we fake is SSRS: the SSRSServerURL module property points back at this LabKey instance (the @@ -65,6 +66,8 @@ @Category({ONPRC.class}) public class ONPRC_SsrsSessionKeyTest extends BaseWebDriverTest { + private final static String API_KEY_OPTIONAL_FEATURE_FLAG = "AllowApiKeyParameter"; + @Override protected String getProjectName() { @@ -96,6 +99,7 @@ private void doSetup() new ModulePropertyValue("ONPRC_EHR", "/" + getProjectName(), "SSRSReportFolder", "DummySSRSFolder") )); } + @Override protected void checkQueries() { @@ -110,8 +114,8 @@ public void testSsrsSessionKeyAuthentication() throws IOException // 2) Harvest the token exactly as the SSRS link would receive it String sessionKey = waitFor( - () -> (String) executeScript("return (window.ONPRC && ONPRC.Utils) ? ONPRC.Utils.sessionId : null;"), - "ONPRC.Utils.preloadSession() never populated a session key", WAIT_FOR_JAVASCRIPT); + () -> (String) executeScript("return (window.ONPRC && ONPRC.Utils) ? ONPRC.Utils.sessionId : null;"), + "ONPRC.Utils.preloadSession() never populated a session key", WAIT_FOR_JAVASCRIPT); assertNotNull("Session key was not preloaded", sessionKey); // The key must be a session key, NOT the raw JSESSIONID -- that is the whole point of the change. @@ -120,18 +124,33 @@ public void testSsrsSessionKeyAuthentication() throws IOException String expectedEmail = PasswordUtil.getUsername(); + // Attempt to authenticate with the optional feature flag off + OptionalFeatureHelper.disableOptionalFeature(createDefaultConnection(), API_KEY_OPTIONAL_FEATURE_FLAG); + + JSONObject featureOff = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", + Map.of("apikey", sessionKey))); + assertEquals("With optional feature off, apikey parameter should have been ignored, resulting in guest", "guest", featureOff.getString("email")); + + // Turn on the optional feature flag + OptionalFeatureHelper.enableOptionalFeature(createDefaultConnection(), API_KEY_OPTIONAL_FEATURE_FLAG); + + // Attempt to authentication using the old parameter name + JSONObject oldParameter = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", + Map.of("LabKeyTransformSessionId", sessionKey))); + assertEquals("LabKeyTransformSessionId parameter should have been ignored, resulting in guest", "guest", oldParameter.getString("email")); + // 3) Simulate the SSRS callback: cookieless, no Basic auth, ONLY the token on the URL. // 3a) Identity check via whoami -- proves the callback authenticates as the right user. JSONObject whoAmI = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", - Map.of("LabKeyTransformSessionId", sessionKey))); + Map.of("apikey", sessionKey))); assertEquals("Token-authenticated callback resolved to the wrong user", expectedEmail, whoAmI.getString("email")); // 3b) Closest-to-real: the actual selectRows callback shape SSRS uses to fetch data. SSRS's XML data // source extension requests the XML response format, so do the same and validate that the payload is // well-formed XML containing the expected data row (the current user, filtered by email). SimpleHttpResponse selectRows = cookielessGet(WebTestHelper.buildURL("query", getProjectName(), "selectRows", - Map.of("schemaName", "core", "query.queryName", "Users", "query.columns", "Email", - "query.Email~eq", expectedEmail, "respFormat", "xml", "LabKeyTransformSessionId", sessionKey))); + Map.of("schemaName", "core", "query.queryName", "Users", "query.columns", "Email", + "query.Email~eq", expectedEmail, "respFormat", "xml", "apikey", sessionKey))); assertEquals("selectRows callback with a valid token should succeed", 200, selectRows.getResponseCode()); Document doc = parseXml(selectRows.getResponseBody()); @@ -152,13 +171,13 @@ public void testSsrsSessionKeyAuthentication() throws IOException // 4b) Bogus token -> guest JSONObject bogus = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", - Map.of("LabKeyTransformSessionId", "not-a-real-session-key"))); + Map.of("apikey", "not-a-real-session-key"))); assertFalse("A cookieless callback with a bogus token should be guest", bogus.getBoolean("success")); // 5) Lifecycle: after the user logs out, the session key must stop working (auto-invalidated with the session). signOut(); JSONObject afterLogout = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", - Map.of("LabKeyTransformSessionId", sessionKey))); + Map.of("apikey", sessionKey))); assertFalse("A cookieless callback with a bogus token should be guest", afterLogout.getBoolean("success")); } @@ -199,5 +218,9 @@ private Document parseXml(String responseBody) protected void doCleanup(boolean afterTest) throws TestTimeoutException { _containerHelper.deleteProject(getProjectName(), afterTest); + if (afterTest) + { + OptionalFeatureHelper.resetOptionalFeature(createDefaultConnection(), API_KEY_OPTIONAL_FEATURE_FLAG); + } } } \ No newline at end of file From 163bd0cd36902cb8be882d56fee72b85af87a530 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 29 Sep 2026 09:06:36 -0700 Subject: [PATCH 2/8] Update comments --- .../labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java b/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java index e9d50fb6a..93d8e06ec 100644 --- a/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java +++ b/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java @@ -124,7 +124,7 @@ public void testSsrsSessionKeyAuthentication() throws IOException String expectedEmail = PasswordUtil.getUsername(); - // Attempt to authenticate with the optional feature flag off + // Attempt authentication with the optional feature flag off OptionalFeatureHelper.disableOptionalFeature(createDefaultConnection(), API_KEY_OPTIONAL_FEATURE_FLAG); JSONObject featureOff = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", @@ -134,7 +134,7 @@ public void testSsrsSessionKeyAuthentication() throws IOException // Turn on the optional feature flag OptionalFeatureHelper.enableOptionalFeature(createDefaultConnection(), API_KEY_OPTIONAL_FEATURE_FLAG); - // Attempt to authentication using the old parameter name + // Attempt authentication using the old, unsupported parameter name JSONObject oldParameter = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", Map.of("LabKeyTransformSessionId", sessionKey))); assertEquals("LabKeyTransformSessionId parameter should have been ignored, resulting in guest", "guest", oldParameter.getString("email")); From a6b019694bce983897a7b0e48a02a02f028780d6 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 29 Sep 2026 11:43:37 -0700 Subject: [PATCH 3/8] Ensure optional feature flag gets reset --- .../onprc_ehr/ONPRC_SsrsSessionKeyTest.java | 121 +++++++++--------- 1 file changed, 63 insertions(+), 58 deletions(-) diff --git a/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java b/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java index 93d8e06ec..a8ec27063 100644 --- a/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java +++ b/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java @@ -19,6 +19,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; +import org.labkey.remoteapi.Connection; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.ModulePropertyValue; import org.labkey.test.TestTimeoutException; @@ -125,60 +126,68 @@ public void testSsrsSessionKeyAuthentication() throws IOException String expectedEmail = PasswordUtil.getUsername(); // Attempt authentication with the optional feature flag off - OptionalFeatureHelper.disableOptionalFeature(createDefaultConnection(), API_KEY_OPTIONAL_FEATURE_FLAG); - - JSONObject featureOff = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", - Map.of("apikey", sessionKey))); - assertEquals("With optional feature off, apikey parameter should have been ignored, resulting in guest", "guest", featureOff.getString("email")); - - // Turn on the optional feature flag - OptionalFeatureHelper.enableOptionalFeature(createDefaultConnection(), API_KEY_OPTIONAL_FEATURE_FLAG); - - // Attempt authentication using the old, unsupported parameter name - JSONObject oldParameter = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", - Map.of("LabKeyTransformSessionId", sessionKey))); - assertEquals("LabKeyTransformSessionId parameter should have been ignored, resulting in guest", "guest", oldParameter.getString("email")); - - // 3) Simulate the SSRS callback: cookieless, no Basic auth, ONLY the token on the URL. - // 3a) Identity check via whoami -- proves the callback authenticates as the right user. - JSONObject whoAmI = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", - Map.of("apikey", sessionKey))); - assertEquals("Token-authenticated callback resolved to the wrong user", expectedEmail, whoAmI.getString("email")); - - // 3b) Closest-to-real: the actual selectRows callback shape SSRS uses to fetch data. SSRS's XML data - // source extension requests the XML response format, so do the same and validate that the payload is - // well-formed XML containing the expected data row (the current user, filtered by email). - SimpleHttpResponse selectRows = cookielessGet(WebTestHelper.buildURL("query", getProjectName(), "selectRows", - Map.of("schemaName", "core", "query.queryName", "Users", "query.columns", "Email", - "query.Email~eq", expectedEmail, "respFormat", "xml", "apikey", sessionKey))); - assertEquals("selectRows callback with a valid token should succeed", 200, selectRows.getResponseCode()); - - Document doc = parseXml(selectRows.getResponseBody()); - Element root = doc.getDocumentElement(); - assertEquals("Unexpected root element in selectRows XML response", "response", root.getTagName()); - Element rowsElement = (Element) root.getElementsByTagName("rows").item(0); - assertNotNull("selectRows XML response is missing the element", rowsElement); - NodeList rows = rowsElement.getElementsByTagName("element"); - assertTrue("selectRows XML response should contain at least one data row", rows.getLength() >= 1); - Node email = ((Element) rows.item(0)).getElementsByTagName("Email").item(0); - assertNotNull("Data row in selectRows XML response is missing the Email column", email); - assertEquals("Data row in selectRows XML response should be for the current user", expectedEmail, email.getTextContent()); - - // 4) Negative controls -- prove it is the token doing the work. - // 4a) No token -> guest (empty email) - JSONObject noToken = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami")); - assertEquals("A cookieless callback with no token should be guest", "guest", noToken.getString("email")); - - // 4b) Bogus token -> guest - JSONObject bogus = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", - Map.of("apikey", "not-a-real-session-key"))); - assertFalse("A cookieless callback with a bogus token should be guest", bogus.getBoolean("success")); - - // 5) Lifecycle: after the user logs out, the session key must stop working (auto-invalidated with the session). - signOut(); - JSONObject afterLogout = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", - Map.of("apikey", sessionKey))); - assertFalse("A cookieless callback with a bogus token should be guest", afterLogout.getBoolean("success")); + Connection cn = createDefaultConnection(); + OptionalFeatureHelper.disableOptionalFeature(cn, API_KEY_OPTIONAL_FEATURE_FLAG); + + try + { + JSONObject featureOff = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", + Map.of("apikey", sessionKey))); + assertEquals("With optional feature off, apikey parameter should have been ignored, resulting in guest", "guest", featureOff.getString("email")); + + // Turn on the optional feature flag + OptionalFeatureHelper.enableOptionalFeature(createDefaultConnection(), API_KEY_OPTIONAL_FEATURE_FLAG); + + // Attempt authentication using the old, unsupported parameter name + JSONObject oldParameter = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", + Map.of("LabKeyTransformSessionId", sessionKey))); + assertEquals("LabKeyTransformSessionId parameter should have been ignored, resulting in guest", "guest", oldParameter.getString("email")); + + // 3) Simulate the SSRS callback: cookieless, no Basic auth, ONLY the token on the URL. + // 3a) Identity check via whoami -- proves the callback authenticates as the right user. + JSONObject whoAmI = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", + Map.of("apikey", sessionKey))); + assertEquals("Token-authenticated callback resolved to the wrong user", expectedEmail, whoAmI.getString("email")); + + // 3b) Closest-to-real: the actual selectRows callback shape SSRS uses to fetch data. SSRS's XML data + // source extension requests the XML response format, so do the same and validate that the payload is + // well-formed XML containing the expected data row (the current user, filtered by email). + SimpleHttpResponse selectRows = cookielessGet(WebTestHelper.buildURL("query", getProjectName(), "selectRows", + Map.of("schemaName", "core", "query.queryName", "Users", "query.columns", "Email", + "query.Email~eq", expectedEmail, "respFormat", "xml", "apikey", sessionKey))); + assertEquals("selectRows callback with a valid token should succeed", 200, selectRows.getResponseCode()); + + Document doc = parseXml(selectRows.getResponseBody()); + Element root = doc.getDocumentElement(); + assertEquals("Unexpected root element in selectRows XML response", "response", root.getTagName()); + Element rowsElement = (Element) root.getElementsByTagName("rows").item(0); + assertNotNull("selectRows XML response is missing the element", rowsElement); + NodeList rows = rowsElement.getElementsByTagName("element"); + assertTrue("selectRows XML response should contain at least one data row", rows.getLength() >= 1); + Node email = ((Element) rows.item(0)).getElementsByTagName("Email").item(0); + assertNotNull("Data row in selectRows XML response is missing the Email column", email); + assertEquals("Data row in selectRows XML response should be for the current user", expectedEmail, email.getTextContent()); + + // 4) Negative controls -- prove it is the token doing the work. + // 4a) No token -> guest (empty email) + JSONObject noToken = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami")); + assertEquals("A cookieless callback with no token should be guest", "guest", noToken.getString("email")); + + // 4b) Bogus token -> guest + JSONObject bogus = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", + Map.of("apikey", "not-a-real-session-key"))); + assertFalse("A cookieless callback with a bogus token should be guest", bogus.getBoolean("success")); + + // 5) Lifecycle: after the user logs out, the session key must stop working (auto-invalidated with the session). + signOut(); + JSONObject afterLogout = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", + Map.of("apikey", sessionKey))); + assertFalse("A cookieless callback with a bogus token should be guest", afterLogout.getBoolean("success")); + } + finally + { + OptionalFeatureHelper.resetOptionalFeature(cn, API_KEY_OPTIONAL_FEATURE_FLAG); + } } /** @@ -218,9 +227,5 @@ private Document parseXml(String responseBody) protected void doCleanup(boolean afterTest) throws TestTimeoutException { _containerHelper.deleteProject(getProjectName(), afterTest); - if (afterTest) - { - OptionalFeatureHelper.resetOptionalFeature(createDefaultConnection(), API_KEY_OPTIONAL_FEATURE_FLAG); - } } } \ No newline at end of file From f040914c1f8b163230ecc99230619a56a22e0aad Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 29 Sep 2026 14:12:41 -0700 Subject: [PATCH 4/8] Adjust error handling --- .../tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java b/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java index a8ec27063..b76635aad 100644 --- a/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java +++ b/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java @@ -124,24 +124,23 @@ public void testSsrsSessionKeyAuthentication() throws IOException assertNotEquals("getSessionId returned the raw JSESSIONID instead of a session key", jsessionId, sessionKey); String expectedEmail = PasswordUtil.getUsername(); - - // Attempt authentication with the optional feature flag off Connection cn = createDefaultConnection(); - OptionalFeatureHelper.disableOptionalFeature(cn, API_KEY_OPTIONAL_FEATURE_FLAG); try { + // Attempt authentication with the optional feature flag off + OptionalFeatureHelper.disableOptionalFeature(cn, API_KEY_OPTIONAL_FEATURE_FLAG); JSONObject featureOff = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", Map.of("apikey", sessionKey))); assertEquals("With optional feature off, apikey parameter should have been ignored, resulting in guest", "guest", featureOff.getString("email")); - // Turn on the optional feature flag - OptionalFeatureHelper.enableOptionalFeature(createDefaultConnection(), API_KEY_OPTIONAL_FEATURE_FLAG); + // Turn on the optional feature flag for the rest of the test + OptionalFeatureHelper.enableOptionalFeature(cn, API_KEY_OPTIONAL_FEATURE_FLAG); // Attempt authentication using the old, unsupported parameter name - JSONObject oldParameter = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", + SimpleHttpResponse oldParameter = cookielessGet(WebTestHelper.buildURL("login", getProjectName(), "whoami", Map.of("LabKeyTransformSessionId", sessionKey))); - assertEquals("LabKeyTransformSessionId parameter should have been ignored, resulting in guest", "guest", oldParameter.getString("email")); + assertEquals("LabKeyTransformSessionId parameter should have been rejected", 400, oldParameter.getResponseCode()); // 3) Simulate the SSRS callback: cookieless, no Basic auth, ONLY the token on the URL. // 3a) Identity check via whoami -- proves the callback authenticates as the right user. From 207598e5705b88be2fb2a785752b57bbe4df510e Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 29 Sep 2026 14:53:50 -0700 Subject: [PATCH 5/8] Don't set the session cookie when using "apikey" parameter --- .../onprc_ehr/ONPRC_SsrsSessionKeyTest.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java b/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java index b76635aad..dbd83ff30 100644 --- a/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java +++ b/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java @@ -144,9 +144,11 @@ public void testSsrsSessionKeyAuthentication() throws IOException // 3) Simulate the SSRS callback: cookieless, no Basic auth, ONLY the token on the URL. // 3a) Identity check via whoami -- proves the callback authenticates as the right user. - JSONObject whoAmI = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", + SimpleHttpResponse whoAmIResponse = cookielessGet(WebTestHelper.buildURL("login", getProjectName(), "whoami", Map.of("apikey", sessionKey))); + JSONObject whoAmI = new JSONObject(whoAmIResponse.getResponseBody()); assertEquals("Token-authenticated callback resolved to the wrong user", expectedEmail, whoAmI.getString("email")); + assertNoSessionCookie(whoAmIResponse); // 3b) Closest-to-real: the actual selectRows callback shape SSRS uses to fetch data. SSRS's XML data // source extension requests the XML response format, so do the same and validate that the payload is @@ -155,6 +157,7 @@ public void testSsrsSessionKeyAuthentication() throws IOException Map.of("schemaName", "core", "query.queryName", "Users", "query.columns", "Email", "query.Email~eq", expectedEmail, "respFormat", "xml", "apikey", sessionKey))); assertEquals("selectRows callback with a valid token should succeed", 200, selectRows.getResponseCode()); + assertNoSessionCookie(selectRows); Document doc = parseXml(selectRows.getResponseBody()); Element root = doc.getDocumentElement(); @@ -205,6 +208,17 @@ private JSONObject cookielessGetJson(String url) throws IOException return new JSONObject(cookielessGet(url).getResponseBody()); } + // An apikey URL parameter must not plant a session cookie, since the URL could have come from an attacker + private void assertNoSessionCookie(SimpleHttpResponse response) + { + List sessionCookies = response.getResponseHeaderFields().entrySet().stream() + .filter(e -> "Set-Cookie".equalsIgnoreCase(e.getKey())) + .flatMap(e -> e.getValue().stream()) + .filter(cookie -> cookie.startsWith("JSESSIONID=")) + .toList(); + assertTrue("apikey URL parameter should not set a JSESSIONID cookie: " + sessionCookies, sessionCookies.isEmpty()); + } + /** * Parse a response body, failing the test if it is not well-formed XML. */ From 101d71b860741c223b572358d9b84feea20763dc Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 29 Sep 2026 15:19:11 -0700 Subject: [PATCH 6/8] Enable optional feature flag by default --- onprc_ehr/src/org/labkey/onprc_ehr/ONPRC_EHRModule.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/onprc_ehr/src/org/labkey/onprc_ehr/ONPRC_EHRModule.java b/onprc_ehr/src/org/labkey/onprc_ehr/ONPRC_EHRModule.java index aeeeb29b8..ffaa2d010 100644 --- a/onprc_ehr/src/org/labkey/onprc_ehr/ONPRC_EHRModule.java +++ b/onprc_ehr/src/org/labkey/onprc_ehr/ONPRC_EHRModule.java @@ -42,9 +42,12 @@ import org.labkey.api.query.DetailsURL; import org.labkey.api.query.QuerySchema; import org.labkey.api.resource.Resource; +import org.labkey.api.security.SecurityManager; +import org.labkey.api.security.User; import org.labkey.api.security.permissions.AdminPermission; import org.labkey.api.security.roles.RoleManager; import org.labkey.api.settings.AppProps; +import org.labkey.api.settings.OptionalFeatureService; import org.labkey.api.util.URLHelper; import org.labkey.api.util.UnexpectedException; import org.labkey.api.view.ActionURL; @@ -157,6 +160,11 @@ protected void init() @Override protected void doStartupAfterSpringConfig(ModuleContext moduleContext) { + // SSRS reports authenticate via the apikey URL parameter. TODO: Remove this once ONPRC has upgraded to 26.11 or later. + OptionalFeatureService ofs = OptionalFeatureService.get(); + if (!ofs.isFeatureEnabled(SecurityManager.FEATURE_FLAG_ALLOW_APIKEY_PARAMETER)) + ofs.setFeatureEnabled(SecurityManager.FEATURE_FLAG_ALLOW_APIKEY_PARAMETER, true, User.getAdminServiceUser()); + registerEHRResources(); NotificationService ns = NotificationService.get(); From 716b17f065bf3a49387c418f6b14d4ac0e4b3b50 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 29 Sep 2026 18:08:08 -0700 Subject: [PATCH 7/8] Use constants --- .../onprc_ehr/ONPRC_SsrsSessionKeyTest.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java b/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java index dbd83ff30..8ddb96274 100644 --- a/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java +++ b/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java @@ -68,6 +68,8 @@ public class ONPRC_SsrsSessionKeyTest extends BaseWebDriverTest { private final static String API_KEY_OPTIONAL_FEATURE_FLAG = "AllowApiKeyParameter"; + private final static String API_KEY_PARAMETER_NAME = "apikey"; + private final static String OLD_PARAMETER_NAME = "LabKeyTransformSessionId"; @Override protected String getProjectName() @@ -131,21 +133,21 @@ public void testSsrsSessionKeyAuthentication() throws IOException // Attempt authentication with the optional feature flag off OptionalFeatureHelper.disableOptionalFeature(cn, API_KEY_OPTIONAL_FEATURE_FLAG); JSONObject featureOff = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", - Map.of("apikey", sessionKey))); - assertEquals("With optional feature off, apikey parameter should have been ignored, resulting in guest", "guest", featureOff.getString("email")); + Map.of(API_KEY_PARAMETER_NAME, sessionKey))); + assertEquals("With optional feature off, " + API_KEY_PARAMETER_NAME + " parameter should have been ignored, resulting in guest", "guest", featureOff.getString("email")); // Turn on the optional feature flag for the rest of the test OptionalFeatureHelper.enableOptionalFeature(cn, API_KEY_OPTIONAL_FEATURE_FLAG); // Attempt authentication using the old, unsupported parameter name SimpleHttpResponse oldParameter = cookielessGet(WebTestHelper.buildURL("login", getProjectName(), "whoami", - Map.of("LabKeyTransformSessionId", sessionKey))); - assertEquals("LabKeyTransformSessionId parameter should have been rejected", 400, oldParameter.getResponseCode()); + Map.of(OLD_PARAMETER_NAME, sessionKey))); + assertEquals(OLD_PARAMETER_NAME + " parameter should have been rejected", 400, oldParameter.getResponseCode()); // 3) Simulate the SSRS callback: cookieless, no Basic auth, ONLY the token on the URL. // 3a) Identity check via whoami -- proves the callback authenticates as the right user. SimpleHttpResponse whoAmIResponse = cookielessGet(WebTestHelper.buildURL("login", getProjectName(), "whoami", - Map.of("apikey", sessionKey))); + Map.of(API_KEY_PARAMETER_NAME, sessionKey))); JSONObject whoAmI = new JSONObject(whoAmIResponse.getResponseBody()); assertEquals("Token-authenticated callback resolved to the wrong user", expectedEmail, whoAmI.getString("email")); assertNoSessionCookie(whoAmIResponse); @@ -155,7 +157,7 @@ public void testSsrsSessionKeyAuthentication() throws IOException // well-formed XML containing the expected data row (the current user, filtered by email). SimpleHttpResponse selectRows = cookielessGet(WebTestHelper.buildURL("query", getProjectName(), "selectRows", Map.of("schemaName", "core", "query.queryName", "Users", "query.columns", "Email", - "query.Email~eq", expectedEmail, "respFormat", "xml", "apikey", sessionKey))); + "query.Email~eq", expectedEmail, "respFormat", "xml", API_KEY_PARAMETER_NAME, sessionKey))); assertEquals("selectRows callback with a valid token should succeed", 200, selectRows.getResponseCode()); assertNoSessionCookie(selectRows); @@ -177,13 +179,13 @@ public void testSsrsSessionKeyAuthentication() throws IOException // 4b) Bogus token -> guest JSONObject bogus = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", - Map.of("apikey", "not-a-real-session-key"))); + Map.of(API_KEY_PARAMETER_NAME, "not-a-real-session-key"))); assertFalse("A cookieless callback with a bogus token should be guest", bogus.getBoolean("success")); // 5) Lifecycle: after the user logs out, the session key must stop working (auto-invalidated with the session). signOut(); JSONObject afterLogout = cookielessGetJson(WebTestHelper.buildURL("login", getProjectName(), "whoami", - Map.of("apikey", sessionKey))); + Map.of(API_KEY_PARAMETER_NAME, sessionKey))); assertFalse("A cookieless callback with a bogus token should be guest", afterLogout.getBoolean("success")); } finally @@ -216,7 +218,7 @@ private void assertNoSessionCookie(SimpleHttpResponse response) .flatMap(e -> e.getValue().stream()) .filter(cookie -> cookie.startsWith("JSESSIONID=")) .toList(); - assertTrue("apikey URL parameter should not set a JSESSIONID cookie: " + sessionCookies, sessionCookies.isEmpty()); + assertTrue(API_KEY_PARAMETER_NAME + " URL parameter should not set a JSESSIONID cookie: " + sessionCookies, sessionCookies.isEmpty()); } /** From d39f0c2de58aeb2ae09f7546ad9afacf4d0ebb11 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 29 Sep 2026 18:25:13 -0700 Subject: [PATCH 8/8] Use a connection that's not tied to the primary test session to set/reset optional feature. This way, it's not affected by signOut in the test. --- .../test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java b/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java index 8ddb96274..eef0ea849 100644 --- a/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java +++ b/onprc_ehr/test/src/org/labkey/test/tests/onprc_ehr/ONPRC_SsrsSessionKeyTest.java @@ -98,8 +98,8 @@ private void doSetup() // Treat this LabKey instance as the fake SSRS target (mirrors AbstractGenericONPRC_EHRTest). preloadSession() // does not actually need these, but setting them keeps the printable reports page behaving as in production. setModuleProperties(Arrays.asList( - new ModulePropertyValue("ONPRC_EHR", "/" + getProjectName(), "SSRSServerURL", WebTestHelper.getBaseURL()), - new ModulePropertyValue("ONPRC_EHR", "/" + getProjectName(), "SSRSReportFolder", "DummySSRSFolder") + new ModulePropertyValue("ONPRC_EHR", "/" + getProjectName(), "SSRSServerURL", WebTestHelper.getBaseURL()), + new ModulePropertyValue("ONPRC_EHR", "/" + getProjectName(), "SSRSReportFolder", "DummySSRSFolder") )); } @@ -126,7 +126,7 @@ public void testSsrsSessionKeyAuthentication() throws IOException assertNotEquals("getSessionId returned the raw JSESSIONID instead of a session key", jsessionId, sessionKey); String expectedEmail = PasswordUtil.getUsername(); - Connection cn = createDefaultConnection(); + Connection cn = WebTestHelper.getRemoteApiConnection(); // Not tied to test user session try {