From db632af9a593cec36de84c4e13596050bf8602d8 Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Sat, 4 Apr 2026 16:06:11 +0530 Subject: [PATCH 01/14] Add secure POST endpoint to apply JCasC YAML via HTTP body --- docs/features/configurationReload.md | 12 + .../plugins/casc/ConfigurationAsCode.java | 47 +++ .../casc/ConfigurationAsCodeApiTest.java | 362 ++++++++++++++++++ 3 files changed, 421 insertions(+) create mode 100644 plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java diff --git a/docs/features/configurationReload.md b/docs/features/configurationReload.md index 19809a4c25..6647659a03 100644 --- a/docs/features/configurationReload.md +++ b/docs/features/configurationReload.md @@ -24,6 +24,18 @@ $ curl -X POST -G -d @/path/to/secret/file "JENKINS_URL/reload-configuration-as- permissions. Since Jenkins 2.96 CRUMB is not needed for API tokens. - via [Jenkins CLI](https://www.jenkins.io/doc/book/managing/cli/): with the Jenkins CLI (either with SSH or JAR), the command `java -jar jenkins-cli.jar -s ${JENKINS_URL} reload-jcasc-configuration` triggers a configuration reload. This Jenkins CLI command is only present when the plugin `configuration-as-code` is installed, and reported in the help message: + +- via http POST to `JENKINS_URL/configuration-as-code/configure` + This endpoint allows you to send raw YAML directly in the HTTP POST body (useful for Git webhooks). + Because sending raw YAML containing secrets over unencrypted HTTP is dangerous, this endpoint is disabled by default. + + To use this endpoint, you must: + 1. Start Jenkins with the system property `-Dcasc.allow.http.post.config=true`. + 2. Authenticate the request using the username and API token of a user with `Administer` permissions. + + **Example Usage:** + ```sh + $ curl -X POST -u admin:YOUR_API_TOKEN --data-binary @jenkins.yaml "JENKINS_URL/configuration-as-code/configure" ```shell $ java -jar jenkins-cli.jar -s ${JENKINS_URL} help diff --git a/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java b/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java index 5578ef4d94..beacdb9d0c 100644 --- a/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java +++ b/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java @@ -108,6 +108,7 @@ public class ConfigurationAsCode extends ManagementLink { public static final String CASC_JENKINS_CONFIG_PROPERTY = "casc.jenkins.config"; public static final String CASC_JENKINS_CONFIG_ENV = "CASC_JENKINS_CONFIG"; + public static final String CASC_ALLOW_HTTP_POST_CONFIG = "casc.allow.http.post.config"; public static final String DEFAULT_JENKINS_YAML_PATH = "jenkins.yaml"; public static final String YAML_FILES_PATTERN = "glob:**.{yml,yaml,YAML,YML}"; @@ -985,4 +986,50 @@ public static String printThrowable(@NonNull Throwable t) { .replaceAll("\t", " "); return s.substring(0, s.lastIndexOf(")") + 1); } + + @RequirePOST + @Restricted(NoExternalUse.class) + @SuppressWarnings("unused") + public void doConfigure(StaplerRequest2 req, StaplerResponse2 res) throws Exception { + + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + res.sendError(HttpServletResponse.SC_FORBIDDEN, "Requires ADMINISTER permission"); + return; + } + + boolean isAllowed = Boolean.parseBoolean(System.getProperty(CASC_ALLOW_HTTP_POST_CONFIG, "false")); + if (!isAllowed) { + res.sendError( + HttpServletResponse.SC_FORBIDDEN, + "Raw YAML POST configuration is disabled for security. Enable with -D" + CASC_ALLOW_HTTP_POST_CONFIG + + "=true"); + return; + } + + if (req.getContentLength() <= 0) { + res.setStatus(HttpServletResponse.SC_BAD_REQUEST); + res.setContentType("application/json; charset=utf-8"); + JSONArray errors = new JSONArray(); + errors.add(new JSONObject().accumulate("line", -1).accumulate("message", "Request body cannot be empty.")); + res.getWriter().print(errors); + return; + } + + try { + configureWith(YamlSource.of(req)); + res.setStatus(HttpServletResponse.SC_OK); + res.setContentType("text/plain; charset=utf-8"); + res.getWriter().print("Configuration successfully applied."); + + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to apply configuration from POST payload", e); + res.setStatus(HttpServletResponse.SC_BAD_REQUEST); + res.setContentType("application/json; charset=utf-8"); + + JSONArray errors = new JSONArray(); + errors.add(new JSONObject().accumulate("line", -1).accumulate("message", e.getMessage())); + + res.getWriter().print(errors); + } + } } diff --git a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java new file mode 100644 index 0000000000..32556f9fca --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java @@ -0,0 +1,362 @@ +package io.jenkins.plugins.casc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import jakarta.servlet.ServletRequest; +import java.net.URL; +import jenkins.model.Jenkins; +import org.htmlunit.HttpMethod; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.JenkinsRule.WebClient; +import org.jvnet.hudson.test.MockAuthorizationStrategy; + +public class ConfigurationAsCodeApiTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + public void testDoConfigure_DisabledByDefault() throws Exception { + System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); + + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(jenkins.model.Jenkins.ADMINISTER) + .everywhere() + .to("admin")); + + WebClient wc = j.createWebClient(); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + wc.login("admin", "admin"); + wc.setThrowExceptionOnFailingStatusCode(false); + + WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); + request.setAdditionalHeader("Content-Type", "application/yaml"); + request.setRequestBody("jenkins:\n systemMessage: 'Testing'"); + + WebResponse response = wc.getPage(request).getWebResponse(); + + assertEquals(403, response.getStatusCode()); + } + + @Test + public void testDoConfigure_RequiresPost() throws Exception { + System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); + + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(jenkins.model.Jenkins.ADMINISTER) + .everywhere() + .to("admin")); + + WebClient wc = j.createWebClient(); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + wc.login("admin", "admin"); + wc.setThrowExceptionOnFailingStatusCode(false); + + WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.GET); + WebResponse response = wc.getPage(request).getWebResponse(); + + assertTrue(response.getStatusCode() == 404 || response.getStatusCode() == 405); + + System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); + } + + @Test + public void testDoConfigure_Success() throws Exception { + System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); + + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(jenkins.model.Jenkins.ADMINISTER) + .everywhere() + .to("admin")); + + WebClient wc = j.createWebClient(); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + wc.login("admin", "admin"); + wc.setThrowExceptionOnFailingStatusCode(false); + + WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); + request.setAdditionalHeader("Content-Type", "application/yaml"); + request.setRequestBody("jenkins:\n systemMessage: 'Webhook Success'"); + + var crumbIssuer = j.jenkins.getCrumbIssuer(); + + if (crumbIssuer != null) { + request.setAdditionalHeader( + crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); + } + + WebResponse response = wc.getPage(request).getWebResponse(); + + assertEquals(200, response.getStatusCode()); + assertEquals("Webhook Success", j.jenkins.getSystemMessage()); + + System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); + } + + @Test + public void testDoConfigure_InvalidYaml() throws Exception { + System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); + + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(jenkins.model.Jenkins.ADMINISTER) + .everywhere() + .to("admin")); + + WebClient wc = j.createWebClient(); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + wc.login("admin", "admin"); + wc.setThrowExceptionOnFailingStatusCode(false); + + WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); + + request.setRequestBody("jenkins:\n systemMessage: [invalid"); + + var crumbIssuer = j.jenkins.getCrumbIssuer(); + + if (crumbIssuer != null) { + request.setAdditionalHeader( + crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); + } + + WebResponse response = wc.getPage(request).getWebResponse(); + + assertEquals(400, response.getStatusCode()); + assertTrue(response.getContentAsString().contains("message")); + + System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); + } + + @Test + public void testDoConfigure_NonAdminForbidden() throws Exception { + System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); + + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.READ) + .everywhere() + .to("user") + .grant(jenkins.model.Jenkins.ADMINISTER) + .everywhere() + .to("admin")); + + WebClient wc = j.createWebClient(); + wc.login("user", "user"); + wc.setThrowExceptionOnFailingStatusCode(false); + + WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); + request.setRequestBody("jenkins:\n systemMessage: 'fail'"); + + var crumbIssuer = j.jenkins.getCrumbIssuer(); + + if (crumbIssuer != null) { + request.setAdditionalHeader( + crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); + } + + WebResponse response = wc.getPage(request).getWebResponse(); + + assertEquals(403, response.getStatusCode()); + + System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); + } + + @Test + public void testDoConfigure_Unauthenticated() throws Exception { + System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); + + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + j.jenkins.setAuthorizationStrategy(new org.jvnet.hudson.test.MockAuthorizationStrategy() + .grant(Jenkins.READ) + .everywhere() + .toEveryone()); + + WebClient wc = j.createWebClient(); + wc.setThrowExceptionOnFailingStatusCode(false); + + WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); + + request.setRequestBody("jenkins:\n systemMessage: 'anonymous bypass attempt'"); + + WebResponse response = wc.getPage(request).getWebResponse(); + + assertEquals(403, response.getStatusCode()); + + System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); + } + + @Test + public void testDoConfigure_MissingCrumb() throws Exception { + System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); + + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.ADMINISTER) + .everywhere() + .to("admin")); + + WebClient wc = j.createWebClient(); + wc.login("admin", "admin"); + wc.setThrowExceptionOnFailingStatusCode(false); + + WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); + + request.setRequestBody("jenkins:\n systemMessage: 'no crumb'"); + + WebResponse response = wc.getPage(request).getWebResponse(); + + assertEquals(403, response.getStatusCode()); + assertTrue(response.getContentAsString().contains("No valid crumb")); + + System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); + } + + @Test + public void testDoConfigure_EmptyBody() throws Exception { + System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); + + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.ADMINISTER) + .everywhere() + .to("admin")); + + WebClient wc = j.createWebClient(); + wc.login("admin", "admin"); + + wc.setThrowExceptionOnFailingStatusCode(false); + + WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); + request.setAdditionalHeader("Content-Type", "application/yaml"); + + var crumbIssuer = j.jenkins.getCrumbIssuer(); + + if (crumbIssuer != null) { + request.setAdditionalHeader( + crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); + } + + WebResponse response = wc.getPage(request).getWebResponse(); + + assertEquals(400, response.getStatusCode()); + assertTrue(response.getContentAsString().contains("message")); + } + + @After + public void tearDown() { + System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); + } + + @Test + public void testDoConfigure_MalformedStructure() throws Exception { + System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(jenkins.model.Jenkins.ADMINISTER) + .everywhere() + .to("admin")); + + WebClient wc = j.createWebClient(); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + wc.login("admin", "admin"); + wc.setThrowExceptionOnFailingStatusCode(false); + + WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); + request.setAdditionalHeader("Content-Type", "application/yaml"); + request.setRequestBody("jenkins:\n invalidRoot:\n foo: bar"); + + var crumbIssuer = j.jenkins.getCrumbIssuer(); + + if (crumbIssuer != null) { + request.setAdditionalHeader( + crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); + } + + WebResponse response = wc.getPage(request).getWebResponse(); + + assertEquals(400, response.getStatusCode()); + assertTrue(response.getContentAsString().contains("message")); + } + + @Test + public void testDoConfigure_ValidYaml_NoChanges() throws Exception { + System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); + + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.ADMINISTER) + .everywhere() + .to("admin")); + + WebClient wc = j.createWebClient(); + wc.login("admin", "admin"); + wc.setThrowExceptionOnFailingStatusCode(false); + + WebRequest request1 = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); + request1.setAdditionalHeader("Content-Type", "application/yaml"); + request1.setRequestBody("jenkins:\n systemMessage: 'Idempotency Test'"); + var crumbIssuer = j.jenkins.getCrumbIssuer(); + if (crumbIssuer != null) { + request1.setAdditionalHeader( + crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); + } + + WebResponse response1 = wc.getPage(request1).getWebResponse(); + assertEquals(200, response1.getStatusCode()); + assertEquals("Idempotency Test", j.jenkins.getSystemMessage()); + + WebRequest request2 = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); + request2.setAdditionalHeader("Content-Type", "application/yaml"); + request2.setRequestBody("jenkins:\n systemMessage: 'Idempotency Test'"); + if (crumbIssuer != null) { + request2.setAdditionalHeader( + crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); + } + + WebResponse response2 = wc.getPage(request2).getWebResponse(); + + assertEquals(200, response2.getStatusCode()); + assertEquals("Idempotency Test", j.jenkins.getSystemMessage()); + } + + @Test + public void testDoConfigure_WithApiToken_NoCrumb() throws Exception { + System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); + + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.ADMINISTER) + .everywhere() + .to("admin")); + + WebClient wc = j.createWebClient(); + + wc.withBasicApiToken("admin"); + wc.setThrowExceptionOnFailingStatusCode(false); + + WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); + request.setAdditionalHeader("Content-Type", "application/yaml"); + request.setRequestBody("jenkins:\n systemMessage: 'API Token Success'"); + + WebResponse response = wc.getPage(request).getWebResponse(); + + assertEquals(200, response.getStatusCode()); + assertEquals("API Token Success", j.jenkins.getSystemMessage()); + + System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); + } +} From d48bdc96563ec1623c5aff36c5050d1a76a9f213 Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Sat, 4 Apr 2026 16:46:08 +0530 Subject: [PATCH 02/14] Fix formatting error --- .../io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java index 32556f9fca..16067f6d45 100644 --- a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java @@ -339,9 +339,9 @@ public void testDoConfigure_WithApiToken_NoCrumb() throws Exception { j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() - .grant(Jenkins.ADMINISTER) - .everywhere() - .to("admin")); + .grant(Jenkins.ADMINISTER) + .everywhere() + .to("admin")); WebClient wc = j.createWebClient(); From 35cb7bb5847b05c77f9b7c6435d13a0f5befc21b Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Sat, 4 Apr 2026 17:26:44 +0530 Subject: [PATCH 03/14] Fix CSRF blocked coverage in tests --- .../jenkins/plugins/casc/ConfigurationAsCodeApiTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java index 16067f6d45..61fcea6d17 100644 --- a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java @@ -41,9 +41,16 @@ public void testDoConfigure_DisabledByDefault() throws Exception { request.setAdditionalHeader("Content-Type", "application/yaml"); request.setRequestBody("jenkins:\n systemMessage: 'Testing'"); + var crumbIssuer = j.jenkins.getCrumbIssuer(); + if (crumbIssuer != null) { + request.setAdditionalHeader( + crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); + } + WebResponse response = wc.getPage(request).getWebResponse(); assertEquals(403, response.getStatusCode()); + assertTrue(response.getContentAsString().contains("disabled for security")); } @Test From 3c5201d389f08b27dbd16d464d1ee9649687f770 Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Sat, 4 Apr 2026 19:56:27 +0530 Subject: [PATCH 04/14] Increasing readability and correctness --- .../java/io/jenkins/plugins/casc/ConfigurationAsCode.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java b/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java index beacdb9d0c..8057c7f8e4 100644 --- a/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java +++ b/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java @@ -1027,7 +1027,10 @@ public void doConfigure(StaplerRequest2 req, StaplerResponse2 res) throws Except res.setContentType("application/json; charset=utf-8"); JSONArray errors = new JSONArray(); - errors.add(new JSONObject().accumulate("line", -1).accumulate("message", e.getMessage())); + JSONObject error = new JSONObject(); + error.put("line", -1); + error.put("message", e.getMessage()); + errors.add(error); res.getWriter().print(errors); } From b2f18aea18fbee84e1d526d5058ec04b1b07abc2 Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Sat, 4 Apr 2026 20:00:03 +0530 Subject: [PATCH 05/14] Replacing accumulate with put --- .../java/io/jenkins/plugins/casc/ConfigurationAsCode.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java b/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java index 8057c7f8e4..10916a6f0f 100644 --- a/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java +++ b/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java @@ -1010,7 +1010,10 @@ public void doConfigure(StaplerRequest2 req, StaplerResponse2 res) throws Except res.setStatus(HttpServletResponse.SC_BAD_REQUEST); res.setContentType("application/json; charset=utf-8"); JSONArray errors = new JSONArray(); - errors.add(new JSONObject().accumulate("line", -1).accumulate("message", "Request body cannot be empty.")); + JSONObject error = new JSONObject(); + error.put("line", -1); + error.put("message", "Request body cannot be empty."); + errors.add(error); res.getWriter().print(errors); return; } From 141da79be622010c014ce227d174f2b978f36aeb Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Sun, 5 Apr 2026 09:36:52 +0530 Subject: [PATCH 06/14] Remove the opt-in flag --- docs/features/configurationReload.md | 6 +- .../plugins/casc/ConfigurationAsCode.java | 9 --- .../casc/ConfigurationAsCodeApiTest.java | 71 ------------------- 3 files changed, 2 insertions(+), 84 deletions(-) diff --git a/docs/features/configurationReload.md b/docs/features/configurationReload.md index 6647659a03..c4bafeafdc 100644 --- a/docs/features/configurationReload.md +++ b/docs/features/configurationReload.md @@ -26,12 +26,10 @@ $ curl -X POST -G -d @/path/to/secret/file "JENKINS_URL/reload-configuration-as- This Jenkins CLI command is only present when the plugin `configuration-as-code` is installed, and reported in the help message: - via http POST to `JENKINS_URL/configuration-as-code/configure` - This endpoint allows you to send raw YAML directly in the HTTP POST body (useful for Git webhooks). - Because sending raw YAML containing secrets over unencrypted HTTP is dangerous, this endpoint is disabled by default. + This endpoint allows you to send raw YAML directly in the HTTP POST body. To use this endpoint, you must: - 1. Start Jenkins with the system property `-Dcasc.allow.http.post.config=true`. - 2. Authenticate the request using the username and API token of a user with `Administer` permissions. + - Authenticate the request using the username and API token of a user with `Administer` permissions. **Example Usage:** ```sh diff --git a/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java b/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java index 10916a6f0f..e3fe2904d9 100644 --- a/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java +++ b/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java @@ -997,15 +997,6 @@ public void doConfigure(StaplerRequest2 req, StaplerResponse2 res) throws Except return; } - boolean isAllowed = Boolean.parseBoolean(System.getProperty(CASC_ALLOW_HTTP_POST_CONFIG, "false")); - if (!isAllowed) { - res.sendError( - HttpServletResponse.SC_FORBIDDEN, - "Raw YAML POST configuration is disabled for security. Enable with -D" + CASC_ALLOW_HTTP_POST_CONFIG - + "=true"); - return; - } - if (req.getContentLength() <= 0) { res.setStatus(HttpServletResponse.SC_BAD_REQUEST); res.setContentType("application/json; charset=utf-8"); diff --git a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java index 61fcea6d17..2bda53b06b 100644 --- a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java @@ -9,7 +9,6 @@ import org.htmlunit.HttpMethod; import org.htmlunit.WebRequest; import org.htmlunit.WebResponse; -import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -21,42 +20,8 @@ public class ConfigurationAsCodeApiTest { @Rule public JenkinsRule j = new JenkinsRule(); - @Test - public void testDoConfigure_DisabledByDefault() throws Exception { - System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); - - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - - j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() - .grant(jenkins.model.Jenkins.ADMINISTER) - .everywhere() - .to("admin")); - - WebClient wc = j.createWebClient(); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - wc.login("admin", "admin"); - wc.setThrowExceptionOnFailingStatusCode(false); - - WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); - request.setAdditionalHeader("Content-Type", "application/yaml"); - request.setRequestBody("jenkins:\n systemMessage: 'Testing'"); - - var crumbIssuer = j.jenkins.getCrumbIssuer(); - if (crumbIssuer != null) { - request.setAdditionalHeader( - crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); - } - - WebResponse response = wc.getPage(request).getWebResponse(); - - assertEquals(403, response.getStatusCode()); - assertTrue(response.getContentAsString().contains("disabled for security")); - } - @Test public void testDoConfigure_RequiresPost() throws Exception { - System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() @@ -73,14 +38,10 @@ public void testDoConfigure_RequiresPost() throws Exception { WebResponse response = wc.getPage(request).getWebResponse(); assertTrue(response.getStatusCode() == 404 || response.getStatusCode() == 405); - - System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); } @Test public void testDoConfigure_Success() throws Exception { - System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() @@ -108,14 +69,10 @@ public void testDoConfigure_Success() throws Exception { assertEquals(200, response.getStatusCode()); assertEquals("Webhook Success", j.jenkins.getSystemMessage()); - - System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); } @Test public void testDoConfigure_InvalidYaml() throws Exception { - System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() @@ -143,14 +100,10 @@ public void testDoConfigure_InvalidYaml() throws Exception { assertEquals(400, response.getStatusCode()); assertTrue(response.getContentAsString().contains("message")); - - System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); } @Test public void testDoConfigure_NonAdminForbidden() throws Exception { - System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() .grant(Jenkins.READ) @@ -177,14 +130,10 @@ public void testDoConfigure_NonAdminForbidden() throws Exception { WebResponse response = wc.getPage(request).getWebResponse(); assertEquals(403, response.getStatusCode()); - - System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); } @Test public void testDoConfigure_Unauthenticated() throws Exception { - System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new org.jvnet.hudson.test.MockAuthorizationStrategy() .grant(Jenkins.READ) @@ -201,14 +150,10 @@ public void testDoConfigure_Unauthenticated() throws Exception { WebResponse response = wc.getPage(request).getWebResponse(); assertEquals(403, response.getStatusCode()); - - System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); } @Test public void testDoConfigure_MissingCrumb() throws Exception { - System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() .grant(Jenkins.ADMINISTER) @@ -227,14 +172,10 @@ public void testDoConfigure_MissingCrumb() throws Exception { assertEquals(403, response.getStatusCode()); assertTrue(response.getContentAsString().contains("No valid crumb")); - - System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); } @Test public void testDoConfigure_EmptyBody() throws Exception { - System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() .grant(Jenkins.ADMINISTER) @@ -262,14 +203,8 @@ public void testDoConfigure_EmptyBody() throws Exception { assertTrue(response.getContentAsString().contains("message")); } - @After - public void tearDown() { - System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); - } - @Test public void testDoConfigure_MalformedStructure() throws Exception { - System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() @@ -301,8 +236,6 @@ public void testDoConfigure_MalformedStructure() throws Exception { @Test public void testDoConfigure_ValidYaml_NoChanges() throws Exception { - System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() .grant(Jenkins.ADMINISTER) @@ -342,8 +275,6 @@ public void testDoConfigure_ValidYaml_NoChanges() throws Exception { @Test public void testDoConfigure_WithApiToken_NoCrumb() throws Exception { - System.setProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG, "true"); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() .grant(Jenkins.ADMINISTER) @@ -363,7 +294,5 @@ public void testDoConfigure_WithApiToken_NoCrumb() throws Exception { assertEquals(200, response.getStatusCode()); assertEquals("API Token Success", j.jenkins.getSystemMessage()); - - System.clearProperty(ConfigurationAsCode.CASC_ALLOW_HTTP_POST_CONFIG); } } From 91fbda93fb4986dd9676f6b4cef50bd4226ab631 Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Sun, 5 Apr 2026 10:24:47 +0530 Subject: [PATCH 07/14] Remove unused line --- .../main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java b/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java index e3fe2904d9..a6197eee08 100644 --- a/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java +++ b/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java @@ -108,7 +108,6 @@ public class ConfigurationAsCode extends ManagementLink { public static final String CASC_JENKINS_CONFIG_PROPERTY = "casc.jenkins.config"; public static final String CASC_JENKINS_CONFIG_ENV = "CASC_JENKINS_CONFIG"; - public static final String CASC_ALLOW_HTTP_POST_CONFIG = "casc.allow.http.post.config"; public static final String DEFAULT_JENKINS_YAML_PATH = "jenkins.yaml"; public static final String YAML_FILES_PATTERN = "glob:**.{yml,yaml,YAML,YML}"; From 1634d5e0b34849c95a5f8e40bb001dd7a145eb40 Mon Sep 17 00:00:00 2001 From: Tim Jacomb <21194782+timja@users.noreply.github.com> Date: Sun, 5 Apr 2026 08:22:57 +0100 Subject: [PATCH 08/14] Apply suggestion from @timja --- docs/features/configurationReload.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/configurationReload.md b/docs/features/configurationReload.md index c4bafeafdc..285722cf10 100644 --- a/docs/features/configurationReload.md +++ b/docs/features/configurationReload.md @@ -26,7 +26,7 @@ $ curl -X POST -G -d @/path/to/secret/file "JENKINS_URL/reload-configuration-as- This Jenkins CLI command is only present when the plugin `configuration-as-code` is installed, and reported in the help message: - via http POST to `JENKINS_URL/configuration-as-code/configure` - This endpoint allows you to send raw YAML directly in the HTTP POST body. + This endpoint allows you to send your configuration as code directly in the HTTP POST body. To use this endpoint, you must: - Authenticate the request using the username and API token of a user with `Administer` permissions. From f919dbb98a86a791aca873bb5d532cf568cd586f Mon Sep 17 00:00:00 2001 From: Tim Jacomb <21194782+timja@users.noreply.github.com> Date: Sun, 5 Apr 2026 08:23:58 +0100 Subject: [PATCH 09/14] Apply suggestion from @timja --- docs/features/configurationReload.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/features/configurationReload.md b/docs/features/configurationReload.md index 285722cf10..512547e57e 100644 --- a/docs/features/configurationReload.md +++ b/docs/features/configurationReload.md @@ -33,7 +33,9 @@ $ curl -X POST -G -d @/path/to/secret/file "JENKINS_URL/reload-configuration-as- **Example Usage:** ```sh - $ curl -X POST -u admin:YOUR_API_TOKEN --data-binary @jenkins.yaml "JENKINS_URL/configuration-as-code/configure" + $ curl -X POST -u admin:YOUR_API_TOKEN \ + --data-binary @jenkins.yaml \ + "JENKINS_URL/configuration-as-code/configure" ```shell $ java -jar jenkins-cli.jar -s ${JENKINS_URL} help From 13e9c85cfd1836bfc8a7887e7520cd3e74658559 Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Sun, 5 Apr 2026 15:05:02 +0530 Subject: [PATCH 10/14] Remove redundant lines from test class --- .../casc/ConfigurationAsCodeApiTest.java | 139 +++--------------- 1 file changed, 19 insertions(+), 120 deletions(-) diff --git a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java index 2bda53b06b..189981fd9e 100644 --- a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java @@ -3,7 +3,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import jakarta.servlet.ServletRequest; import java.net.URL; import jenkins.model.Jenkins; import org.htmlunit.HttpMethod; @@ -22,16 +21,7 @@ public class ConfigurationAsCodeApiTest { @Test public void testDoConfigure_RequiresPost() throws Exception { - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - - j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() - .grant(jenkins.model.Jenkins.ADMINISTER) - .everywhere() - .to("admin")); - WebClient wc = j.createWebClient(); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - wc.login("admin", "admin"); wc.setThrowExceptionOnFailingStatusCode(false); WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.GET); @@ -42,28 +32,20 @@ public void testDoConfigure_RequiresPost() throws Exception { @Test public void testDoConfigure_Success() throws Exception { - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + WebClient wc = j.createWebClient(); + wc.setThrowExceptionOnFailingStatusCode(false); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() - .grant(jenkins.model.Jenkins.ADMINISTER) + .grant(Jenkins.ADMINISTER) .everywhere() .to("admin")); - WebClient wc = j.createWebClient(); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - wc.login("admin", "admin"); - wc.setThrowExceptionOnFailingStatusCode(false); - WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); request.setAdditionalHeader("Content-Type", "application/yaml"); request.setRequestBody("jenkins:\n systemMessage: 'Webhook Success'"); - var crumbIssuer = j.jenkins.getCrumbIssuer(); - - if (crumbIssuer != null) { - request.setAdditionalHeader( - crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); - } + wc.withBasicApiToken("admin"); WebResponse response = wc.getPage(request).getWebResponse(); @@ -73,28 +55,21 @@ public void testDoConfigure_Success() throws Exception { @Test public void testDoConfigure_InvalidYaml() throws Exception { - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + WebClient wc = j.createWebClient(); + wc.setThrowExceptionOnFailingStatusCode(false); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() - .grant(jenkins.model.Jenkins.ADMINISTER) + .grant(Jenkins.ADMINISTER) .everywhere() .to("admin")); - WebClient wc = j.createWebClient(); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - wc.login("admin", "admin"); - wc.setThrowExceptionOnFailingStatusCode(false); - WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); + request.setAdditionalHeader("Content-Type", "application/yaml"); request.setRequestBody("jenkins:\n systemMessage: [invalid"); - var crumbIssuer = j.jenkins.getCrumbIssuer(); - - if (crumbIssuer != null) { - request.setAdditionalHeader( - crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); - } + wc.withBasicApiToken("admin"); WebResponse response = wc.getPage(request).getWebResponse(); @@ -114,18 +89,12 @@ public void testDoConfigure_NonAdminForbidden() throws Exception { .to("admin")); WebClient wc = j.createWebClient(); - wc.login("user", "user"); wc.setThrowExceptionOnFailingStatusCode(false); WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); request.setRequestBody("jenkins:\n systemMessage: 'fail'"); - var crumbIssuer = j.jenkins.getCrumbIssuer(); - - if (crumbIssuer != null) { - request.setAdditionalHeader( - crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); - } + wc.withBasicApiToken("user"); WebResponse response = wc.getPage(request).getWebResponse(); @@ -152,28 +121,6 @@ public void testDoConfigure_Unauthenticated() throws Exception { assertEquals(403, response.getStatusCode()); } - @Test - public void testDoConfigure_MissingCrumb() throws Exception { - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() - .grant(Jenkins.ADMINISTER) - .everywhere() - .to("admin")); - - WebClient wc = j.createWebClient(); - wc.login("admin", "admin"); - wc.setThrowExceptionOnFailingStatusCode(false); - - WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); - - request.setRequestBody("jenkins:\n systemMessage: 'no crumb'"); - - WebResponse response = wc.getPage(request).getWebResponse(); - - assertEquals(403, response.getStatusCode()); - assertTrue(response.getContentAsString().contains("No valid crumb")); - } - @Test public void testDoConfigure_EmptyBody() throws Exception { j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); @@ -183,19 +130,13 @@ public void testDoConfigure_EmptyBody() throws Exception { .to("admin")); WebClient wc = j.createWebClient(); - wc.login("admin", "admin"); wc.setThrowExceptionOnFailingStatusCode(false); WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); request.setAdditionalHeader("Content-Type", "application/yaml"); - var crumbIssuer = j.jenkins.getCrumbIssuer(); - - if (crumbIssuer != null) { - request.setAdditionalHeader( - crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); - } + wc.withBasicApiToken("admin"); WebResponse response = wc.getPage(request).getWebResponse(); @@ -205,28 +146,20 @@ public void testDoConfigure_EmptyBody() throws Exception { @Test public void testDoConfigure_MalformedStructure() throws Exception { - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + WebClient wc = j.createWebClient(); + wc.setThrowExceptionOnFailingStatusCode(false); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() - .grant(jenkins.model.Jenkins.ADMINISTER) + .grant(Jenkins.ADMINISTER) .everywhere() .to("admin")); - WebClient wc = j.createWebClient(); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - wc.login("admin", "admin"); - wc.setThrowExceptionOnFailingStatusCode(false); - WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); request.setAdditionalHeader("Content-Type", "application/yaml"); request.setRequestBody("jenkins:\n invalidRoot:\n foo: bar"); - var crumbIssuer = j.jenkins.getCrumbIssuer(); - - if (crumbIssuer != null) { - request.setAdditionalHeader( - crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); - } + wc.withBasicApiToken("admin"); WebResponse response = wc.getPage(request).getWebResponse(); @@ -241,19 +174,13 @@ public void testDoConfigure_ValidYaml_NoChanges() throws Exception { .grant(Jenkins.ADMINISTER) .everywhere() .to("admin")); - WebClient wc = j.createWebClient(); - wc.login("admin", "admin"); + wc.withBasicApiToken("admin"); wc.setThrowExceptionOnFailingStatusCode(false); WebRequest request1 = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); request1.setAdditionalHeader("Content-Type", "application/yaml"); request1.setRequestBody("jenkins:\n systemMessage: 'Idempotency Test'"); - var crumbIssuer = j.jenkins.getCrumbIssuer(); - if (crumbIssuer != null) { - request1.setAdditionalHeader( - crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); - } WebResponse response1 = wc.getPage(request1).getWebResponse(); assertEquals(200, response1.getStatusCode()); @@ -262,37 +189,9 @@ public void testDoConfigure_ValidYaml_NoChanges() throws Exception { WebRequest request2 = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); request2.setAdditionalHeader("Content-Type", "application/yaml"); request2.setRequestBody("jenkins:\n systemMessage: 'Idempotency Test'"); - if (crumbIssuer != null) { - request2.setAdditionalHeader( - crumbIssuer.getCrumbRequestField(), crumbIssuer.getCrumb((ServletRequest) null)); - } WebResponse response2 = wc.getPage(request2).getWebResponse(); - assertEquals(200, response2.getStatusCode()); assertEquals("Idempotency Test", j.jenkins.getSystemMessage()); } - - @Test - public void testDoConfigure_WithApiToken_NoCrumb() throws Exception { - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() - .grant(Jenkins.ADMINISTER) - .everywhere() - .to("admin")); - - WebClient wc = j.createWebClient(); - - wc.withBasicApiToken("admin"); - wc.setThrowExceptionOnFailingStatusCode(false); - - WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); - request.setAdditionalHeader("Content-Type", "application/yaml"); - request.setRequestBody("jenkins:\n systemMessage: 'API Token Success'"); - - WebResponse response = wc.getPage(request).getWebResponse(); - - assertEquals(200, response.getStatusCode()); - assertEquals("API Token Success", j.jenkins.getSystemMessage()); - } } From e22033624dc56b17c7a5f8aa8ef5f4f02d11ac0c Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Sun, 5 Apr 2026 15:10:28 +0530 Subject: [PATCH 11/14] Remove redundant lines --- .../plugins/casc/ConfigurationAsCodeApiTest.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java index 189981fd9e..0794effe3f 100644 --- a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java @@ -79,15 +79,6 @@ public void testDoConfigure_InvalidYaml() throws Exception { @Test public void testDoConfigure_NonAdminForbidden() throws Exception { - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() - .grant(Jenkins.READ) - .everywhere() - .to("user") - .grant(jenkins.model.Jenkins.ADMINISTER) - .everywhere() - .to("admin")); - WebClient wc = j.createWebClient(); wc.setThrowExceptionOnFailingStatusCode(false); @@ -104,7 +95,7 @@ public void testDoConfigure_NonAdminForbidden() throws Exception { @Test public void testDoConfigure_Unauthenticated() throws Exception { j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - j.jenkins.setAuthorizationStrategy(new org.jvnet.hudson.test.MockAuthorizationStrategy() + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() .grant(Jenkins.READ) .everywhere() .toEveryone()); From 9afed0e7b361b24d2304b8273ada966481ab5c5d Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Sun, 5 Apr 2026 15:10:57 +0530 Subject: [PATCH 12/14] Fix formatting --- .../io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java index 0794effe3f..1ceb2fb2f0 100644 --- a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java @@ -95,10 +95,8 @@ public void testDoConfigure_NonAdminForbidden() throws Exception { @Test public void testDoConfigure_Unauthenticated() throws Exception { j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() - .grant(Jenkins.READ) - .everywhere() - .toEveryone()); + j.jenkins.setAuthorizationStrategy( + new MockAuthorizationStrategy().grant(Jenkins.READ).everywhere().toEveryone()); WebClient wc = j.createWebClient(); wc.setThrowExceptionOnFailingStatusCode(false); From 54ab408508e93db86d331a0f292f6b5470bac0a6 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Sun, 5 Apr 2026 21:51:53 +0100 Subject: [PATCH 13/14] Cleanup test --- .../casc/ConfigurationAsCodeApiTest.java | 204 ++++++++---------- 1 file changed, 91 insertions(+), 113 deletions(-) diff --git a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java index 1ceb2fb2f0..ec029fe180 100644 --- a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java @@ -1,7 +1,8 @@ package io.jenkins.plugins.casc; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; import java.net.URL; import jenkins.model.Jenkins; @@ -11,7 +12,6 @@ import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.JenkinsRule.WebClient; import org.jvnet.hudson.test.MockAuthorizationStrategy; public class ConfigurationAsCodeApiTest { @@ -19,77 +19,82 @@ public class ConfigurationAsCodeApiTest { @Rule public JenkinsRule j = new JenkinsRule(); - @Test - public void testDoConfigure_RequiresPost() throws Exception { - WebClient wc = j.createWebClient(); - wc.setThrowExceptionOnFailingStatusCode(false); - - WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.GET); - WebResponse response = wc.getPage(request).getWebResponse(); + private static final String ENDPOINT = "configuration-as-code/configure"; + private static final String YAML_CONTENT_TYPE = "application/yaml"; + private static final String ADMIN = "admin"; - assertTrue(response.getStatusCode() == 404 || response.getStatusCode() == 405); + private WebRequest webRequest(HttpMethod method) throws Exception { + return new WebRequest(new URL(j.getURL(), ENDPOINT), method); } - @Test - public void testDoConfigure_Success() throws Exception { - WebClient wc = j.createWebClient(); - wc.setThrowExceptionOnFailingStatusCode(false); + private WebRequest yamlPost(String requestBody) throws Exception { + WebRequest request = webRequest(HttpMethod.POST); + request.setAdditionalHeader("Content-Type", YAML_CONTENT_TYPE); + if (requestBody != null) { + request.setRequestBody(requestBody); + } + return request; + } + private void configureAdminSecurity() { j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() .grant(Jenkins.ADMINISTER) .everywhere() - .to("admin")); - - WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); - request.setAdditionalHeader("Content-Type", "application/yaml"); - request.setRequestBody("jenkins:\n systemMessage: 'Webhook Success'"); - - wc.withBasicApiToken("admin"); + .to(ADMIN)); + } - WebResponse response = wc.getPage(request).getWebResponse(); + @Test + public void testDoConfigure_RequiresPost() throws Exception { + try (JenkinsRule.WebClient wc = j.createWebClient()) { + wc.setThrowExceptionOnFailingStatusCode(false); - assertEquals(200, response.getStatusCode()); - assertEquals("Webhook Success", j.jenkins.getSystemMessage()); + WebResponse response = wc.getPage(webRequest(HttpMethod.GET)).getWebResponse(); + assertThat(response.getStatusCode(), is(405)); + } } @Test - public void testDoConfigure_InvalidYaml() throws Exception { - WebClient wc = j.createWebClient(); - wc.setThrowExceptionOnFailingStatusCode(false); + public void testDoConfigure_Success() throws Exception { + configureAdminSecurity(); - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() - .grant(Jenkins.ADMINISTER) - .everywhere() - .to("admin")); + try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) { + wc.setThrowExceptionOnFailingStatusCode(false); - WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); + WebResponse response = wc.getPage(yamlPost("jenkins:\n systemMessage: 'Webhook Success'")) + .getWebResponse(); - request.setAdditionalHeader("Content-Type", "application/yaml"); - request.setRequestBody("jenkins:\n systemMessage: [invalid"); + assertThat(response.getStatusCode(), is(200)); + assertThat(j.jenkins.getSystemMessage(), is("Webhook Success")); + } + } - wc.withBasicApiToken("admin"); + @Test + public void testDoConfigure_InvalidYaml() throws Exception { + configureAdminSecurity(); + + try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) { + wc.setThrowExceptionOnFailingStatusCode(false); - WebResponse response = wc.getPage(request).getWebResponse(); + WebResponse response = + wc.getPage(yamlPost("jenkins:\n systemMessage: [invalid")).getWebResponse(); - assertEquals(400, response.getStatusCode()); - assertTrue(response.getContentAsString().contains("message")); + assertThat(response.getStatusCode(), is(400)); + assertThat(response.getContentAsString(), containsString("message")); + } } @Test public void testDoConfigure_NonAdminForbidden() throws Exception { - WebClient wc = j.createWebClient(); - wc.setThrowExceptionOnFailingStatusCode(false); - - WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); - request.setRequestBody("jenkins:\n systemMessage: 'fail'"); - - wc.withBasicApiToken("user"); + try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken("user")) { + wc.setThrowExceptionOnFailingStatusCode(false); - WebResponse response = wc.getPage(request).getWebResponse(); + WebRequest request = webRequest(HttpMethod.POST); + request.setRequestBody("jenkins:\n systemMessage: 'fail'"); - assertEquals(403, response.getStatusCode()); + WebResponse response = wc.getPage(request).getWebResponse(); + assertThat(response.getStatusCode(), is(403)); + } } @Test @@ -98,89 +103,62 @@ public void testDoConfigure_Unauthenticated() throws Exception { j.jenkins.setAuthorizationStrategy( new MockAuthorizationStrategy().grant(Jenkins.READ).everywhere().toEveryone()); - WebClient wc = j.createWebClient(); - wc.setThrowExceptionOnFailingStatusCode(false); + try (JenkinsRule.WebClient wc = j.createWebClient()) { + wc.setThrowExceptionOnFailingStatusCode(false); - WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); + WebRequest request = webRequest(HttpMethod.POST); + request.setRequestBody("jenkins:\n systemMessage: 'anonymous bypass attempt'"); - request.setRequestBody("jenkins:\n systemMessage: 'anonymous bypass attempt'"); - - WebResponse response = wc.getPage(request).getWebResponse(); - - assertEquals(403, response.getStatusCode()); + WebResponse response = wc.getPage(request).getWebResponse(); + assertThat(response.getStatusCode(), is(403)); + } } @Test public void testDoConfigure_EmptyBody() throws Exception { - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() - .grant(Jenkins.ADMINISTER) - .everywhere() - .to("admin")); + configureAdminSecurity(); - WebClient wc = j.createWebClient(); + try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) { + wc.setThrowExceptionOnFailingStatusCode(false); - wc.setThrowExceptionOnFailingStatusCode(false); + WebResponse response = wc.getPage(yamlPost(null)).getWebResponse(); - WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); - request.setAdditionalHeader("Content-Type", "application/yaml"); - - wc.withBasicApiToken("admin"); - - WebResponse response = wc.getPage(request).getWebResponse(); - - assertEquals(400, response.getStatusCode()); - assertTrue(response.getContentAsString().contains("message")); + assertThat(response.getStatusCode(), is(400)); + assertThat(response.getContentAsString(), containsString("message")); + } } @Test public void testDoConfigure_MalformedStructure() throws Exception { - WebClient wc = j.createWebClient(); - wc.setThrowExceptionOnFailingStatusCode(false); - - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() - .grant(Jenkins.ADMINISTER) - .everywhere() - .to("admin")); + configureAdminSecurity(); - WebRequest request = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); - request.setAdditionalHeader("Content-Type", "application/yaml"); - request.setRequestBody("jenkins:\n invalidRoot:\n foo: bar"); + try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) { + wc.setThrowExceptionOnFailingStatusCode(false); - wc.withBasicApiToken("admin"); + WebResponse response = wc.getPage(yamlPost("jenkins:\n invalidRoot:\n foo: bar")) + .getWebResponse(); - WebResponse response = wc.getPage(request).getWebResponse(); - - assertEquals(400, response.getStatusCode()); - assertTrue(response.getContentAsString().contains("message")); + assertThat(response.getStatusCode(), is(400)); + assertThat(response.getContentAsString(), containsString("message")); + } } @Test public void testDoConfigure_ValidYaml_NoChanges() throws Exception { - j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() - .grant(Jenkins.ADMINISTER) - .everywhere() - .to("admin")); - WebClient wc = j.createWebClient(); - wc.withBasicApiToken("admin"); - wc.setThrowExceptionOnFailingStatusCode(false); - - WebRequest request1 = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); - request1.setAdditionalHeader("Content-Type", "application/yaml"); - request1.setRequestBody("jenkins:\n systemMessage: 'Idempotency Test'"); - - WebResponse response1 = wc.getPage(request1).getWebResponse(); - assertEquals(200, response1.getStatusCode()); - assertEquals("Idempotency Test", j.jenkins.getSystemMessage()); - - WebRequest request2 = new WebRequest(new URL(j.getURL(), "configuration-as-code/configure"), HttpMethod.POST); - request2.setAdditionalHeader("Content-Type", "application/yaml"); - request2.setRequestBody("jenkins:\n systemMessage: 'Idempotency Test'"); - - WebResponse response2 = wc.getPage(request2).getWebResponse(); - assertEquals(200, response2.getStatusCode()); - assertEquals("Idempotency Test", j.jenkins.getSystemMessage()); + configureAdminSecurity(); + + try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) { + wc.setThrowExceptionOnFailingStatusCode(false); + + WebResponse response1 = wc.getPage(yamlPost("jenkins:\n systemMessage: 'Idempotency Test'")) + .getWebResponse(); + assertThat(response1.getStatusCode(), is(200)); + assertThat(j.jenkins.getSystemMessage(), is("Idempotency Test")); + + WebResponse response2 = wc.getPage(yamlPost("jenkins:\n systemMessage: 'Idempotency Test'")) + .getWebResponse(); + assertThat(response2.getStatusCode(), is(200)); + assertThat(j.jenkins.getSystemMessage(), is("Idempotency Test")); + } } } From ebcfef575278e650e0ba96c059d68af06493404c Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Sun, 5 Apr 2026 21:53:41 +0100 Subject: [PATCH 14/14] Bit more --- .../io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java index ec029fe180..b69e8774e0 100644 --- a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java @@ -89,8 +89,7 @@ public void testDoConfigure_NonAdminForbidden() throws Exception { try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken("user")) { wc.setThrowExceptionOnFailingStatusCode(false); - WebRequest request = webRequest(HttpMethod.POST); - request.setRequestBody("jenkins:\n systemMessage: 'fail'"); + WebRequest request = yamlPost("jenkins:\n systemMessage: 'fail'"); WebResponse response = wc.getPage(request).getWebResponse(); assertThat(response.getStatusCode(), is(403)); @@ -106,8 +105,7 @@ public void testDoConfigure_Unauthenticated() throws Exception { try (JenkinsRule.WebClient wc = j.createWebClient()) { wc.setThrowExceptionOnFailingStatusCode(false); - WebRequest request = webRequest(HttpMethod.POST); - request.setRequestBody("jenkins:\n systemMessage: 'anonymous bypass attempt'"); + WebRequest request = yamlPost("jenkins:\n systemMessage: 'anonymous bypass attempt'"); WebResponse response = wc.getPage(request).getWebResponse(); assertThat(response.getStatusCode(), is(403));