Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[JENKINS-74011] Extract JavaScript block in PageStatePreloadDecorator/header.jelly #2588

Merged
merged 3 commits into from
Nov 23, 2024

Conversation

basil
Copy link
Member

@basil basil commented Nov 19, 2024

Context

See JENKINS-74011.

Problem

<script>//&lt;![CDATA[
// construct the state object parent path inside window.$blueocean.
var stateRoot = window.$blueocean = (window.$blueocean || {});
(function () {
function setState(statePropertyPath, state) {
var pathTokens = statePropertyPath.split('.');
var contextObj = stateRoot;
// Basically an Array shift
function nextToken() {
var nextToken = pathTokens[0];
pathTokens = pathTokens.slice(1);
return nextToken;
}
var pathToken = nextToken();
// Construct up to, but not including, the last point in the graph.
while (pathTokens.length !== 0) {
if (!contextObj[pathToken]) {
contextObj[pathToken] = {};
}
contextObj = contextObj[pathToken];
pathToken = nextToken();
}
// And set the state on the last object on the graph.
contextObj[pathToken] = state;
}
<j:forEach var="preloader" items="${it.pageStatePreloaders}">
// State Preloader: ${preloader.class.name}
<j:set var="stateJson" value="${preloader.stateJson}"/>
<j:if test="${stateJson != null}">
setState('${preloader.statePropertyPath}', ${stateJson});
</j:if>
</j:forEach>
})();
//]]&gt;
</script>

Solution

https://www.jenkins.io/doc/developer/security/csp/#inline-javascript-blocks

Implementation

Follow the general strategy used in JUnit and Theme Manager plugins. Since this data is complex JSON, do not use data attributes in HTML, but rather put the JSON in a <script> of type application/json. Then parse that JSON (without using eval!) to get the data into memory in a CSP-compliant fashion.

Testing done

Tested together with #2587 and

diff --git a/blueocean-web/src/main/resources/io/jenkins/blueocean/BlueOceanUI/index.jelly b/blueocean-web/src/main/resources/io/jenkins/blueocean/BlueOceanUI/index.jelly
index dc61f3c0d..3d75385e7 100644
--- a/blueocean-web/src/main/resources/io/jenkins/blueocean/BlueOceanUI/index.jelly
+++ b/blueocean-web/src/main/resources/io/jenkins/blueocean/BlueOceanUI/index.jelly
@@ -1,6 +1,7 @@
 <?jelly escape-by-default='true'?>
 <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:x="jelly:xml">
     <st:contentType value="text/html;charset=UTF-8"/>
+    <st:header name="Content-Security-Policy" value="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: ; script-src 'self' 'unsafe-eval' 'report-sample' usage.jenkins.io" />
 
     <!-- Add HTTP headers from extensions. See BluePageDecorator.java -->
     <j:forEach var="pd" items="${it.pageDecorators}">

Note that the above contains unsafe-eval to avoid exposing 74883.

Confirmed that after the header was added but before this PR and #2587, Blue Ocean blew up with a CSP violation. Confirmed that after the header was added and after this PR and #2587, Blue Ocean loaded correctly.

Stepped through the setState function calls in the browser's debugger after this PR to ensure they were being called with the same arguments as before from the parsed JSON.

Submitter checklist

  • Link to JIRA ticket in description, if appropriate.
  • Change is code complete and matches issue description
  • Appropriate unit or acceptance tests or explanation to why this change has no tests
  • Reviewer's manual test instructions provided in PR description. See Reviewer's first task below.

Reviewer checklist

  • Run the changes and verified the change matches the issue description
  • Reviewed the code
  • Verified that the appropriate tests have been written or valid explanation given

Comment on lines -4 to +10
// construct the state object parent path inside window.$blueocean.
var stateRoot = window.$blueocean = (window.$blueocean || {});
(function () {
function setState(statePropertyPath, state) {
var pathTokens = statePropertyPath.split('.');
var contextObj = stateRoot;

// Basically an Array shift
function nextToken() {
var nextToken = pathTokens[0];
pathTokens = pathTokens.slice(1);
return nextToken;
}

var pathToken = nextToken();

// Construct up to, but not including, the last point in the graph.
while (pathTokens.length !== 0) {
if (!contextObj[pathToken]) {
contextObj[pathToken] = {};
}
contextObj = contextObj[pathToken];
pathToken = nextToken();
}
// And set the state on the last object on the graph.
contextObj[pathToken] = state;
}

<j:forEach var="preloader" items="${it.pageStatePreloaders}">
// State Preloader: ${preloader.class.name}
<j:set var="stateJson" value="${preloader.stateJson}"/>
<j:if test="${stateJson != null}">
setState('${preloader.statePropertyPath}', ${stateJson});
</j:if>
</j:forEach>
})();
//]]&gt;
<script id="blueocean-page-state-preload-decorator-data" type="application/json">
{<j:forEach var="preloader" items="${it.pageStatePreloaders}" varStatus="st">
<!-- State Preloader: ${preloader.class.name} -->
<j:set var="stateJson" value="${preloader.stateJson}"/>
<j:if test="${stateJson != null}">
"${preloader.statePropertyPath}": ${stateJson}<j:if test="${!st.last}">,</j:if>
</j:if>
</j:forEach>}
Copy link
Member Author

Choose a reason for hiding this comment

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

Diff:

diff --git b/blueocean-web/src/main/resources/io/jenkins/blueocean/PageStatePreloadDecorator/preloader.js a/blueocean-web/src/main/resources/io/jenkins/blueocean/PageStatePreloadDecorator/preloader.js
index a9658b610..4757a1e9d 100644
--- b/blueocean-web/src/main/resources/io/jenkins/blueocean/PageStatePreloadDecorator/preloader.js
+++ a/blueocean-web/src/main/resources/io/jenkins/blueocean/PageStatePreloadDecorator/preloader.js
@@ -25,11 +25,8 @@ var stateRoot = window.$blueocean = (window.$blueocean || {});
     // And set the state on the last object on the graph.
     contextObj[pathToken] = state;
   }
-  <j:forEach var="preloader" items="${it.pageStatePreloaders}">
-    // State Preloader: ${preloader.class.name}
-    <j:set var="stateJson" value="${preloader.stateJson}"/>
-    <j:if test="${stateJson != null}">
-    setState('${preloader.statePropertyPath}', ${stateJson});
-    </j:if>
-  </j:forEach>
+  const data = JSON.parse(document.getElementById('blueocean-page-state-preload-decorator-data').textContent);
+  for (const [key, value] of Object.entries(data)) {
+    setState(key, value);
+  }
 })();

@basil basil marked this pull request as ready for review November 20, 2024 01:07
@basil basil requested a review from a team as a code owner November 20, 2024 01:07
@basil basil requested a review from olamy November 20, 2024 11:42
@olamy olamy merged commit 2b1ec08 into jenkinsci:master Nov 23, 2024
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants