Compare commits

...

51 Commits

Author SHA1 Message Date
renovate[bot] 01fa17680b
Update actions/setup-java digest to 4e7e684 (#23857)
Changelog Drafter / update_draft_release (push) Waiting to run Details
Changelog Drafter / jenkins_io_draft (push) Waiting to run Details
Label conflicting PRs / main (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-26 22:42:31 +01:00
renovate[bot] 46b6e75cf0
Update dependency stylelint to v16.26.0 (#19540)
Changelog Drafter / update_draft_release (push) Waiting to run Details
Changelog Drafter / jenkins_io_draft (push) Waiting to run Details
Label conflicting PRs / main (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 17:49:44 +00:00
Jenkins Release Bot 53a905f0ca Merge remote-tracking branch 'origin/master'
Changelog Drafter / update_draft_release (push) Waiting to run Details
Changelog Drafter / jenkins_io_draft (push) Waiting to run Details
Label conflicting PRs / main (push) Waiting to run Details
2025-11-25 15:01:17 +00:00
Jenkins Release Bot a9c6135e43 [maven-release-plugin] prepare for next development iteration 2025-11-25 15:01:14 +00:00
Jenkins Release Bot b0ea4eba9b [maven-release-plugin] prepare release jenkins-2.539 2025-11-25 15:01:04 +00:00
Tim Jacomb 41166d1fc7
Adapt docs for Jira migration (#11291)
Co-authored-by: Hervé Le Meur <91831478+lemeurherve@users.noreply.github.com>
Co-authored-by: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
2025-11-25 14:32:38 +00:00
Tim Jacomb 1c60311df2
Reduce testing branches (#23852) 2025-11-25 14:22:52 +00:00
Daniel Beck 2ba827b6c5
[JENKINS-76263] Add `Content-Security-Policy` header (#11269) 2025-11-25 09:48:48 +01:00
Markus Winter 2fa902c78c
Fix api token rendering (#11321)
Changelog Drafter / update_draft_release (push) Waiting to run Details
Changelog Drafter / jenkins_io_draft (push) Waiting to run Details
Label conflicting PRs / main (push) Waiting to run Details
* Fix api token css

Change #11141 added display: flex with direction column to jenkins-card.
This broke the styling of the api token display
This makes the api-tokens more independent from jenkins-card

* prettier
2025-11-24 14:39:55 -07:00
Yen Cheng Lin f639f2bf12
[JENKINS-76299] Jenkins 2.536 and later overlap text when viewing builds for jobs with long names (#11334)
* Fix overlap text with long names

* Update _breadcrumbs.scss

* Update _breadcrumbs.scss
2025-11-24 14:39:33 -07:00
renovate[bot] 5789789340
Update dependency io.jenkins.plugins:jakarta-mail-api to v2.1.5-1 (#11329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 14:39:10 -07:00
Markus Winter 110014cf69
Add rest api to create agent from xml (#11229)
Changelog Drafter / update_draft_release (push) Has been cancelled Details
Changelog Drafter / jenkins_io_draft (push) Has been cancelled Details
Label conflicting PRs / main (push) Has been cancelled Details
* Create agent from xml via rest api

One can create jobs from xml via the rest api or from cli. One can also
create agents from xml via the cli, but creating an agent via rest from
xml is not possible so far.

* add tests

* fix whitespace

---------

Co-authored-by: Mark Waite <mark.earl.waite@gmail.com>
2025-11-23 04:37:49 -07:00
Jesse Glick bdf22a2dfd
Optimizations & simplifications pertaining to lazy-loading (#11320)
* Avoid `TransientActionFactory` from `Job.getPermalinks`

* Optimizing `PeepholePermalink.RunListener` to avoid resolving old targets

* Simplified `RunMixIn` handling of next/previous build
2025-11-23 04:36:09 -07:00
renovate[bot] 503b9ac80f
Update actions/create-github-app-token action to v2.2.0 (#11327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-23 04:35:52 -07:00
renovate[bot] 9fb6bf096a
Update dependency sass to v1.94.2 (#11331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-23 04:35:31 -07:00
renovate[bot] be73a8dd79
Update peter-evans/create-pull-request action to v7.0.9 (#11330)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-23 04:34:08 -07:00
renovate[bot] 3fb56d4100
Update dependency io.jenkins.plugins:jakarta-xml-bind-api to v4.0.6-10.v9b_7e1d1fc40b_ (#11326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-23 04:32:57 -07:00
renovate[bot] 101f904484
Update dependency io.jenkins.plugins:jakarta-activation-api to v2.1.4-1 (#11325)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-23 04:32:22 -07:00
renovate[bot] 462d748f4b
Update dependency sass to v1.94.1 (#11323)
Changelog Drafter / update_draft_release (push) Waiting to run Details
Changelog Drafter / jenkins_io_draft (push) Waiting to run Details
Label conflicting PRs / main (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 13:47:19 +00:00
renovate[bot] 9a0ae657ca
Update dependency webpack to v5.103.0 (#11324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 13:47:09 +00:00
renovate[bot] 422e3b8990
Update actions/checkout action to v6 (#11319)
Changelog Drafter / update_draft_release (push) Waiting to run Details
Changelog Drafter / jenkins_io_draft (push) Waiting to run Details
Label conflicting PRs / main (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-21 10:46:54 +01:00
renovate[bot] fc21d6f6e6
Update dependency org.springframework:spring-framework-bom to v6.2.14 (#11318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-21 10:46:43 +01:00
renovate[bot] 91c0643368
Update actions/setup-java digest to 46c56d6 (#11307)
Changelog Drafter / update_draft_release (push) Waiting to run Details
Changelog Drafter / jenkins_io_draft (push) Waiting to run Details
Label conflicting PRs / main (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 10:18:53 +00:00
renovate[bot] 3f15d106aa
Update dependency org.jenkins-ci.main:remoting to v3352 (#11315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 10:18:41 +00:00
renovate[bot] d5ecc45833
Migrate Renovate config (#11309) 2025-11-19 09:50:01 +01:00
Jenkins Release Bot 5e84413d07 [maven-release-plugin] prepare for next development iteration 2025-11-19 06:10:38 +00:00
Jenkins Release Bot 44513735d6 [maven-release-plugin] prepare release jenkins-2.538 2025-11-19 06:10:29 +00:00
Mark Waite 74ff843a80
[JENKINS-76295] Allow SSH build agents to connect (#11310)
[JENKINS-76295] Revert "Update dependency commons-io:commons-io to v2.21.0 (#11276)"

https://issues.jenkins.io/browse/JENKINS-76295 reports that SSH build
agents fail to connect after this change was included.

Testing done:

* Confirmed that Jenkins 2.537 fails to start an SSH build agent on
  Debian 13
* Confirmed that with this change reverted, the SSH build agent starts
  as expected on Debian 13

This reverts commit ab45729395.
2025-11-18 21:05:53 -07:00
Alexander Brandes bdb06bfc55
Enable renovate dashboard (#11306)
Changelog Drafter / update_draft_release (push) Has been cancelled Details
Changelog Drafter / jenkins_io_draft (push) Has been cancelled Details
Label conflicting PRs / main (push) Has been cancelled Details
Signed-off-by: Alexander Brandes <mc.cache@web.de>
2025-11-18 18:22:15 +00:00
Jenkins Release Bot 86c28550d5 [maven-release-plugin] prepare for next development iteration
Changelog Drafter / update_draft_release (push) Waiting to run Details
Changelog Drafter / jenkins_io_draft (push) Waiting to run Details
Label conflicting PRs / main (push) Waiting to run Details
2025-11-18 12:31:36 +00:00
Jenkins Release Bot f18f78d870 [maven-release-plugin] prepare release jenkins-2.537 2025-11-18 12:31:25 +00:00
Damien Duportal e658af93c5
chore(GHA/publish-release-artifacts) merge into a single package for RH/SuSE (#11299)
Changelog Drafter / update_draft_release (push) Waiting to run Details
Changelog Drafter / jenkins_io_draft (push) Waiting to run Details
Label conflicting PRs / main (push) Waiting to run Details
* chore(GHA/publish-release-artifacts) merge into a single package for RH/SuSE

Ref. https://github.com/jenkinsci/packaging/pull/430 for details

this change updates the artifacts upload GHA workflow to merge the Redhat and OpenSuse packages into a single unified RPM package

* Update .github/workflows/publish-release-artifact.yml
2025-11-18 08:26:04 +00:00
renovate[bot] 28f6320dbe
Update actions/setup-java digest to 66b9457 (#11302)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 08:37:29 +01:00
renovate[bot] 64dd84718a
Update actions/checkout action to v5.0.1 (#11300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 08:37:07 +01:00
renovate[bot] cca038f922
Update dependency org.springframework.security:spring-security-bom to v6.5.7 (#11303)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 08:36:54 +01:00
Jan Faracik 23ff4bcb63
Add experimental Manage Jenkins layout (#11222)
Changelog Drafter / update_draft_release (push) Waiting to run Details
Changelog Drafter / jenkins_io_draft (push) Waiting to run Details
Label conflicting PRs / main (push) Waiting to run Details
Co-authored-by: Tim Jacomb <21194782+timja@users.noreply.github.com>
2025-11-17 16:26:44 +00:00
renovate[bot] 2a6f83dccb
Update jenkins/ath Docker tag to v6464 (#11296)
Changelog Drafter / update_draft_release (push) Has been cancelled Details
Changelog Drafter / jenkins_io_draft (push) Has been cancelled Details
Label conflicting PRs / main (push) Has been cancelled Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-16 10:50:46 +01:00
renovate[bot] 04bd568cb2
Update dependency sass to v1.94.0 (#11295)
Changelog Drafter / update_draft_release (push) Waiting to run Details
Changelog Drafter / jenkins_io_draft (push) Waiting to run Details
Label conflicting PRs / main (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 13:38:20 -07:00
renovate[bot] ec8ec623b2
Update dependency org.jenkins-ci.plugins:structs to v362 (#11297)
Changelog Drafter / update_draft_release (push) Waiting to run Details
Changelog Drafter / jenkins_io_draft (push) Waiting to run Details
Label conflicting PRs / main (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-14 20:09:20 -07:00
renovate[bot] 8e9147a96d
Update actions/setup-java digest to 6ba5449 (#11294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-14 20:03:51 -07:00
Jesse Glick 6e2067e616
Optimize `MappingWorksheet` to avoid calling `Task.getEstimatedDuration` (#11293) 2025-11-14 20:03:35 -07:00
Jesse Glick 30f301b517
Delete `LegacyInstancesAreScopedToHudson` and associated machinery (#11225)
* Deleting `LegacyInstancesAreScopedToHudson` and associated machinery

* In fact we can deprecate external use of the `ExtensionList.<init>(Jenkins, Class, CopyOnWriteArrayList)` constructor https://github.com/jenkinsci/jenkins/pull/11225#discussion_r2462205845

* Adjusting Javadoc to guide developers to simpler APIs like `ExtensionList.lookup`
2025-11-14 20:03:13 -07:00
renovate[bot] 1c38fe64ee
Update Winstone and Jetty (#11289)
Changelog Drafter / update_draft_release (push) Has been cancelled Details
Changelog Drafter / jenkins_io_draft (push) Has been cancelled Details
Label conflicting PRs / main (push) Has been cancelled Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 20:09:18 +01:00
renovate[bot] ab45729395
Update dependency commons-io:commons-io to v2.21.0 (#11276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 20:08:48 +01:00
renovate[bot] 90416179c0
Update dependency org.glassfish.tyrus.bundles:tyrus-standalone-client-jdk to v2.2.1 (#11250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 20:08:39 +01:00
renovate[bot] a86e4e2264
Update dependency org.springframework:spring-framework-bom to v6.2.13 (#11290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 20:08:25 +01:00
Jan Faracik 320a149f76
Refine Changes for the experimental Run UI (#11277)
Changelog Drafter / update_draft_release (push) Has been cancelled Details
Changelog Drafter / jenkins_io_draft (push) Has been cancelled Details
Label conflicting PRs / main (push) Has been cancelled Details
2025-11-12 15:27:01 +00:00
renovate[bot] d8f229853a
Update Node.js to v24.11.1 (#11287)
Changelog Drafter / update_draft_release (push) Waiting to run Details
Changelog Drafter / jenkins_io_draft (push) Waiting to run Details
Label conflicting PRs / main (push) Waiting to run Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 19:51:24 -07:00
renovate[bot] a6e8edc1d4
Update dependency com.puppycrawl.tools:checkstyle to v12.1.2 (#11286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 19:51:06 -07:00
Mark Waite 205849e0f6
Skip spotless in release:prepare (#11285)
The weekly release of Jenkins 2.536 failed its initial build because Maven
release plugin 3.2.0 behaves differently than Maven release plugin 3.1.1.
For 2.535, we were using Maven release plugin 3.1.1 and it did not require
that we skip spotless verification during the `release:prepare` phase.
Maven release plugin 3.2.0 requires it.

Will allow the temporary change to be reverted from pull request:

* https://github.com/jenkins-infra/release/pull/782

Special thanks to @jglick for noting the better way to resolve the issue.

Testing done:

* Confirmed that builds fail without this change when running

  `mvn -B -V -ntp release:prepare`

* Confirmed that builds pass with this change when running

  `mvn -B -V -ntp release:prepare`
2025-11-11 16:48:50 -07:00
Jenkins Release Bot 8f1bf5be69 [maven-release-plugin] prepare for next development iteration 2025-11-11 16:33:16 +00:00
137 changed files with 5700 additions and 1172 deletions

View File

@ -1,17 +1,15 @@
<!-- Comment:
A great PR typically begins with the line below.
Replace XXXXX with the numeric part of the issue ID you created in Jira.
Note that if you want your changes backported into LTS, you need to create a Jira issue. See https://www.jenkins.io/download/lts/#backporting-process for more information.
Replace <issue-number> with the issue number.
-->
See [JENKINS-XXXXX](https://issues.jenkins.io/browse/JENKINS-XXXXX).
Fixes #<issue-number>
<!-- Comment:
If the issue is not fully described in Jira, add more information here (justification, pull request links, etc.).
If the issue is not fully described in the issue tracker, add more information here (justification, pull request links, etc.).
* We do not require Jira issues for minor improvements.
* Bug fixes should have a Jira issue to facilitate the backporting process.
* Major new features should have a Jira issue.
* We do not require an issue for minor improvements.
* Major new features should have an issue created.
-->
### Testing done
@ -34,8 +32,8 @@ For refactoring and code cleanup changes, exercise the code before and after the
The changelog entry should be in the imperative mood; e.g., write "do this"/"return that" rather than "does this"/"returns that".
For examples, see: https://www.jenkins.io/changelog/
Do not include the Jira issue in the changelog entry.
Include the Jira issue in the description of the pull request so that the changelog generator can find it and include it in the generated changelog.
Do not include the issue in the changelog entry.
Include the issue in the description of the pull request so that the changelog generator can find it and include it in the generated changelog.
You may add multiple changelog entries if applicable by adding a new entry to the list, e.g.
- First changelog entry
@ -82,7 +80,7 @@ The changelog generator relies on the presence of the upgrade guidelines section
### Submitter checklist
- [ ] The Jira issue, if it exists, is well-described.
- [ ] The issue, if it exists, is well-described.
- [ ] The changelog entries and upgrade guidelines are appropriate for the audience affected by the change (users or developers, depending on the change) and are in the imperative mood (see [examples](https://github.com/jenkins-infra/jenkins.io/blob/master/content/_data/changelogs/weekly.yml)). Fill in the **Proposed upgrade guidelines** section only if there are breaking changes or changes that may require extra steps from users during upgrade.
- [ ] There is automated testing or an explanation as to why this change has no tests.
- [ ] New public classes, fields, and methods are annotated with `@Restricted` or have `@since TODO` Javadocs, as appropriate.
@ -108,4 +106,4 @@ Before the changes are marked as `ready-for-merge`:
- [ ] Changelog entries in the pull request title and/or **Proposed changelog entries** are accurate, human-readable, and in the imperative mood.
- [ ] Proper changelog labels are set so that the changelog can be generated automatically.
- [ ] If the change needs additional upgrade steps from users, the `upgrade-guide-needed` label is set and there is a **Proposed upgrade guidelines** section in the pull request title (see [example](https://github.com/jenkinsci/jenkins/pull/4387)).
- [ ] If it would make sense to backport the change to LTS, a Jira issue must exist, be a _Bug_ or _Improvement_, and be labeled as `lts-candidate` to be considered (see [query](https://issues.jenkins.io/issues/?filter=12146)).
- [ ] If it would make sense to backport the change to LTS, be a _Bug_ or _Improvement_, and either the issue or pull request must be labeled as `lts-candidate` to be considered.

133
.github/renovate.json vendored
View File

@ -1,33 +1,19 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":disableDependencyDashboard",
":semanticCommitsDisabled"
],
"extends": ["config:recommended", ":semanticCommitsDisabled"],
"prHourlyLimit": 0,
"prConcurrentLimit": 0,
"postUpdateOptions": [
"yarnDedupeHighest"
],
"postUpdateOptions": ["yarnDedupeHighest"],
"packageRules": [
{
"matchDatasources": [
"npm"
],
"addLabels": [
"javascript"
],
"matchDatasources": ["npm"],
"addLabels": ["javascript"],
"minimumReleaseAge": "3 days",
"reviewers": [
"team:sig-ux"
]
"reviewers": ["team:sig-ux"]
},
{
"description": "Should be upgraded in lockstep in order to keep their corresponding Jetty versions aligned",
"matchManagers": [
"maven"
],
"matchManagers": ["maven"],
"groupName": "Winstone and Jetty",
"matchPackageNames": [
"org.eclipse.jetty.ee9:jetty-ee9-maven-plugin",
@ -36,9 +22,7 @@
},
{
"description": "Provided by Jetty and should be aligned with the version provided by the version of Jetty we deliver. See: https://github.com/jenkinsci/jenkins/pull/5211",
"matchManagers": [
"maven"
],
"matchManagers": ["maven"],
"enabled": false,
"matchPackageNames": [
"jakarta.servlet:jakarta.servlet-api",
@ -47,63 +31,37 @@
},
{
"description": "Needs significant testing. See: https://github.com/jenkinsci/jenkins/pull/5112#issuecomment-744429487 and https://github.com/jenkinsci/jenkins/pull/5116#issuecomment-744526638",
"matchManagers": [
"maven"
],
"matchManagers": ["maven"],
"allowedVersions": "<2.5.0",
"matchPackageNames": [
"org.codehaus.groovy:groovy-all"
]
"matchPackageNames": ["org.codehaus.groovy:groovy-all"]
},
{
"description": "Consumed by Groovy and should be updated in lockstep with Groovy. See: https://github.com/jenkinsci/jenkins/pull/5184",
"matchManagers": [
"maven"
],
"matchManagers": ["maven"],
"enabled": false,
"matchPackageNames": [
"org.fusesource.jansi:jansi"
]
"matchPackageNames": ["org.fusesource.jansi:jansi"]
},
{
"description": "Contains incompatible API changes and needs compatibility work. See: https://github.com/jenkinsci/jenkins/pull/4224",
"matchManagers": [
"maven"
],
"matchManagers": ["maven"],
"enabled": false,
"matchPackageNames": [
"org.jfree:jfreechart"
]
"matchPackageNames": ["org.jfree:jfreechart"]
},
{
"description": "Starting with 7.x, Guice switches from javax.* to jakarta.* bindings. See https://github.com/google/guice/wiki/Guice700",
"matchManagers": [
"maven"
],
"matchManagers": ["maven"],
"allowedVersions": "<7.0.0",
"matchPackageNames": [
"com.google.inject:guice-bom"
]
"matchPackageNames": ["com.google.inject:guice-bom"]
},
{
"matchFileNames": [
"core/pom.xml",
"test/pom.xml",
"war/pom.xml"
],
"matchPackageNames": [
"org.jenkins-ci.main:remoting"
],
"matchFileNames": ["core/pom.xml", "test/pom.xml", "war/pom.xml"],
"matchPackageNames": ["org.jenkins-ci.main:remoting"],
"description": "Avoid updating the remoting.minimum.supported.version property but still update latest one by not placing this property in the parent pom.xml",
"enabled": false
},
{
"matchPackageNames": [
"net.jcip:jcip-annotations"
],
"matchDatasources": [
"maven"
],
"matchPackageNames": ["net.jcip:jcip-annotations"],
"matchDatasources": ["maven"],
"enabled": false,
"description": "maven-metadata.xml is missing for this really old package which is required by renovate"
}
@ -111,76 +69,49 @@
"customManagers": [
{
"customType": "regex",
"fileMatch": [
"pom.xml"
],
"matchStrings": [
"<node.version>(?<currentValue>.*?)</node.version>"
],
"managerFilePatterns": ["/pom.xml/"],
"matchStrings": ["<node.version>(?<currentValue>.*?)</node.version>"],
"depNameTemplate": "node",
"datasourceTemplate": "node-version"
},
{
"customType": "regex",
"fileMatch": [
"ath.sh"
],
"matchStrings": [
"export ATH_VERSION=(?<currentValue>.*?)\n"
],
"managerFilePatterns": ["/ath.sh/"],
"matchStrings": ["export ATH_VERSION=(?<currentValue>.*?)\n"],
"depNameTemplate": "jenkins/ath",
"datasourceTemplate": "docker",
"versioningTemplate": "loose"
},
{
"customType": "regex",
"fileMatch": [
".gitpod/Dockerfile"
],
"matchStrings": [
"ARG MAVEN_VERSION=(?<currentValue>.*?)\n"
],
"managerFilePatterns": ["/.gitpod/Dockerfile/"],
"matchStrings": ["ARG MAVEN_VERSION=(?<currentValue>.*?)\n"],
"depNameTemplate": "org.apache.maven:maven-core",
"datasourceTemplate": "maven"
},
{
"customType": "regex",
"fileMatch": [
"core/src/site/site.xml"
],
"matchStrings": [
"lit@(?<currentValue>.*?)/"
],
"managerFilePatterns": ["/core/src/site/site.xml/"],
"matchStrings": ["lit@(?<currentValue>.*?)/"],
"depNameTemplate": "lit",
"datasourceTemplate": "npm"
},
{
"customType": "regex",
"fileMatch": [
"core/src/site/site.xml"
],
"matchStrings": [
"webcomponentsjs@(?<currentValue>.*?)/"
],
"managerFilePatterns": ["/core/src/site/site.xml/"],
"matchStrings": ["webcomponentsjs@(?<currentValue>.*?)/"],
"depNameTemplate": "@webcomponents/webcomponentsjs",
"datasourceTemplate": "npm"
},
{
"customType": "regex",
"fileMatch": [
"core/src/site/site.xml"
],
"matchStrings": [
"<version>(?<currentValue>.*?)</version>"
],
"managerFilePatterns": ["/core/src/site/site.xml/"],
"matchStrings": ["<version>(?<currentValue>.*?)</version>"],
"depNameTemplate": "org.apache.maven.skins:maven-fluido-skin",
"datasourceTemplate": "maven"
}
],
"labels": [
"dependencies",
"skip-changelog"
],
"labels": ["dependencies", "skip-changelog"],
"rebaseWhen": "conflicted",
"ignorePaths": [
"**/node_modules/**",

View File

@ -44,7 +44,7 @@ jobs:
runs-on: ubuntu-latest
if: github.repository_owner == 'jenkinsci'
steps:
- uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
- uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
id: generate-token
with:
app-id: ${{ secrets.JENKINS_CHANGELOG_UPDATER_APP_ID }}
@ -52,7 +52,7 @@ jobs:
owner: jenkins-infra
repositories: jenkins.io
- name: Check out
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
- name: Publish jenkins.io changelog draft

View File

@ -16,9 +16,9 @@ jobs:
is-lts: ${{ steps.set-version.outputs.is-lts }}
is-rc: ${{ steps.set-version.outputs.is-rc }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up JDK 21
uses: actions/setup-java@de5a937a1dc73fbc1a67d7d1aa4bebc1082f3190 #v 5.0.0
uses: actions/setup-java@4e7e684fbb6e33f88ecb2cf1e6b3797739cf499b #v 5.0.0
with:
distribution: "temurin"
java-version: 21
@ -114,7 +114,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: ${{ steps.fetch-deb.outputs.file-name }}
redhat:
rpm:
permissions:
contents: write # to upload release asset (softprops/action-gh-release)
@ -128,15 +128,16 @@ jobs:
run: |
PROJECT_VERSION=${{ needs.determine-version.outputs.project-version }}
echo $PROJECT_VERSION
FILE_NAME=jenkins-${PROJECT_VERSION}-1.1.noarch.rpm
FILE_NAME=jenkins-${PROJECT_VERSION}-1.noarch.rpm
IS_LTS=${{ needs.determine-version.outputs.is-lts }}
echo "file-name=$FILE_NAME" >> $GITHUB_OUTPUT
REPO=redhat
REPO=rpm
if [ ${IS_LTS} = 'true' ]
then
REPO=redhat-stable
FILE_NAME=jenkins-${PROJECT_VERSION}-1.1.noarch.rpm
fi
echo $REPO
@ -186,39 +187,3 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: ${{ steps.fetch-msi.outputs.file-name }}
suse:
permissions:
contents: write # to upload release asset (softprops/action-gh-release)
runs-on: ubuntu-latest
needs: determine-version
if: ${{ needs.determine-version.outputs.is-rc == 'false' }}
steps:
- name: Fetch suse rpm
id: fetch-suse-rpm
if: always()
run: |
PROJECT_VERSION=${{ needs.determine-version.outputs.project-version }}
echo $PROJECT_VERSION
# we need opensuse to the file
OUTPUT_FILE_NAME=jenkins-${PROJECT_VERSION}-1.2-opensuse.noarch.rpm
IS_LTS=${{ needs.determine-version.outputs.is-lts }}
echo "file-name=$OUTPUT_FILE_NAME" >> $GITHUB_OUTPUT
REPO=opensuse
if [ ${IS_LTS} = 'true' ]
then
REPO=opensuse-stable
fi
echo $REPO
wget -q https://get.jenkins.io/${REPO}/jenkins-${PROJECT_VERSION}-1.2.noarch.rpm -O ${OUTPUT_FILE_NAME}
- name: Upload Release Asset
id: upload-suse-rpm
if: always()
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: ${{ steps.fetch-suse-rpm.outputs.file-name }}

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'jenkinsci' }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
- name: Run update-since-todo.py
@ -29,7 +29,7 @@ jobs:
id: run_script
shell: bash
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Fill in since annotations

View File

@ -18,8 +18,8 @@ This page provides information about contributing code to the Jenkins core codeb
If you want to contribute to Jenkins, or just learn about the project,
you can start by fixing some easier issues.
In the Jenkins issue tracker we mark such issues as `newbie-friendly`.
You can find them by using this query (check the link) for [newbie friendly issues](<https://issues.jenkins.io/issues/?jql=project%20%3D%20JENKINS%20AND%20status%20in%20(Open%2C%20%22In%20Progress%22%2C%20Reopened)%20AND%20component%20%3D%20core%20AND%20labels%20in%20(newbie-friendly)>).
In the Jenkins issue tracker we mark such issues as `good first issue`.
You can find them by using this query (check the link) for [good first issues](https://github.com/jenkinsci/jenkins/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22).
## Building and Debugging
@ -261,4 +261,4 @@ just submit a pull request.
- [Jenkins Contribution Landing Page](https://www.jenkins.io/participate/)
- [Jenkins Chat Channels](https://www.jenkins.io/chat/)
- [Beginners Guide To Contributing](https://www.jenkins.io/participate/)
- [List of newbie-friendly issues in the core](<https://issues.jenkins.io/issues/?jql=project%20%3D%20JENKINS%20AND%20status%20in%20(Open%2C%20%22In%20Progress%22%2C%20Reopened)%20AND%20component%20%3D%20core%20AND%20labels%20in%20(newbie-friendly)>)
- [List of good first issues in core](<https://github.com/jenkinsci/jenkins/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22)

4
Jenkinsfile vendored
View File

@ -14,12 +14,12 @@ properties([
def axes = [
platforms: ['linux', 'windows'],
jdks: [17, 21, 25],
jdks: [21, 25],
]
stage('Record build') {
retry(conditions: [kubernetesAgent(handleNonKubernetes: true), nonresumable()], count: 2) {
node('maven-17') {
node('maven-21') {
infra.checkoutSCM()
/*

2
ath.sh
View File

@ -6,7 +6,7 @@ set -o xtrace
cd "$(dirname "$0")"
# https://github.com/jenkinsci/acceptance-test-harness/releases
export ATH_VERSION=6446.v64eb_f0dfb_26d
export ATH_VERSION=6464.vf87c7908f638
if [[ $# -eq 0 ]]; then
export JDK=21

View File

@ -28,7 +28,7 @@ THE SOFTWARE.
<parent>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>jenkins-parent</artifactId>
<version>2.536</version>
<version>${revision}${changelist}</version>
</parent>
<artifactId>jenkins-bom</artifactId>
@ -63,7 +63,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>6.2.12</version>
<version>6.2.14</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@ -71,7 +71,7 @@ THE SOFTWARE.
<!-- https://docs.spring.io/spring-security/reference/6.3/getting-spring-security.html#getting-maven-no-boot -->
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>6.5.6</version>
<version>6.5.7</version>
<type>pom</type>
<scope>import</scope>
</dependency>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>jenkins-parent</artifactId>
<version>2.536</version>
<version>${revision}${changelist}</version>
</parent>
<artifactId>cli</artifactId>
@ -59,7 +59,7 @@
<dependency>
<groupId>org.glassfish.tyrus.bundles</groupId>
<artifactId>tyrus-standalone-client-jdk</artifactId>
<version>2.2.0</version>
<version>2.2.1</version>
<optional>true</optional>
</dependency>
<dependency>

View File

@ -29,7 +29,7 @@ THE SOFTWARE.
<parent>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>jenkins-parent</artifactId>
<version>2.536</version>
<version>${revision}${changelist}</version>
</parent>
<artifactId>jenkins-core</artifactId>

View File

@ -34,16 +34,10 @@ import hudson.model.Hudson;
import hudson.model.ViewDescriptor;
import hudson.slaves.NodeDescriptor;
import hudson.tasks.Publisher;
import hudson.util.AdaptedIterator;
import hudson.util.Iterators.FlattenIterator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.ExtensionComponentSet;
@ -106,7 +100,7 @@ public class DescriptorExtensionList<T extends Describable<T>, D extends Descrip
}
protected DescriptorExtensionList(Jenkins jenkins, Class<T> describableType) {
super(jenkins, (Class) Descriptor.class, (CopyOnWriteArrayList) getLegacyDescriptors(describableType));
super(jenkins, (Class) Descriptor.class);
this.describableType = describableType;
}
@ -238,46 +232,19 @@ public class DescriptorExtensionList<T extends Describable<T>, D extends Descrip
}
/**
* Stores manually registered Descriptor instances. Keyed by the {@link Describable} type.
*/
@SuppressWarnings("rawtypes")
private static final Map<Class, CopyOnWriteArrayList<ExtensionComponent<Descriptor>>> legacyDescriptors = new ConcurrentHashMap<>();
@SuppressWarnings({"unchecked", "rawtypes"})
private static <T extends Describable<T>> CopyOnWriteArrayList<ExtensionComponent<Descriptor>> getLegacyDescriptors(Class<T> type) {
return legacyDescriptors.computeIfAbsent(type, key -> new CopyOnWriteArrayList());
}
/**
* List up all the legacy instances currently in use.
* @deprecated Now always empty.
*/
@Deprecated
public static Iterable<Descriptor> listLegacyInstances() {
return new Iterable<>() {
@Override
public Iterator<Descriptor> iterator() {
return new AdaptedIterator<ExtensionComponent<Descriptor>, Descriptor>(
new FlattenIterator<ExtensionComponent<Descriptor>, CopyOnWriteArrayList<ExtensionComponent<Descriptor>>>(legacyDescriptors.values()) {
@Override
protected Iterator<ExtensionComponent<Descriptor>> expand(CopyOnWriteArrayList<ExtensionComponent<Descriptor>> v) {
return v.iterator();
}
}) {
@Override
protected Descriptor adapt(ExtensionComponent<Descriptor> item) {
return item.getInstance();
}
};
}
};
return List.of();
}
/**
* Exposed just for the test harness. Clear legacy instances.
* @deprecated No longer does anything.
*/
@SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "TODO needs triage")
@SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "irrelevant now")
@Deprecated
public static void clearLegacyInstances() {
legacyDescriptors.clear();
}
private static final Logger LOGGER = Logger.getLogger(DescriptorExtensionList.class.getName());

View File

@ -26,11 +26,9 @@ package hudson;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson;
import hudson.init.InitMilestone;
import hudson.model.Hudson;
import hudson.util.AdaptedIterator;
import hudson.util.DescriptorList;
import hudson.util.Iterators;
import java.util.AbstractList;
import java.util.ArrayList;
@ -39,11 +37,8 @@ import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -57,22 +52,14 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
* Retains the known extension instances for the given type 'T'.
*
* <p>
* Extensions are loaded lazily on demand and automatically by using {@link ExtensionFinder}, but this
* class also provides a mechanism to provide compatibility with the older {@link DescriptorList}-based
* manual registration,
*
* <p>
* All {@link ExtensionList} instances should be owned by {@link jenkins.model.Jenkins}, even though
* extension points can be defined by anyone on any type. Use {@link jenkins.model.Jenkins#getExtensionList(Class)}
* and {@link jenkins.model.Jenkins#getDescriptorList(Class)} to obtain the instances.
* Use {@link Extension} to register extensions.
* Use {@link #lookup}, {@link #lookupSingleton}, or {@link #lookupFirst} to find them.
*
* @param <T>
* Type of the extension point. This class holds instances of the subtypes of 'T'.
*
* @author Kohsuke Kawaguchi
* @since 1.286
* @see jenkins.model.Jenkins#getExtensionList(Class)
* @see jenkins.model.Jenkins#getDescriptorList(Class)
*/
public class ExtensionList<T> extends AbstractList<T> implements OnMaster {
/**
@ -93,7 +80,7 @@ public class ExtensionList<T> extends AbstractList<T> implements OnMaster {
private final List<ExtensionListListener> listeners = new CopyOnWriteArrayList<>();
/**
* Place to store manually registered instances with the per-Hudson scope.
* Place to store manually registered instances.
* {@link CopyOnWriteArrayList} is used here to support concurrent iterations and mutation.
*/
private final CopyOnWriteArrayList<ExtensionComponent<T>> legacyInstances;
@ -113,7 +100,7 @@ public class ExtensionList<T> extends AbstractList<T> implements OnMaster {
/**
* @deprecated as of 1.416
* Use {@link #ExtensionList(Jenkins, Class, CopyOnWriteArrayList)}
* Use {@link #ExtensionList(Jenkins, Class)}
*/
@Deprecated
protected ExtensionList(Hudson hudson, Class<T> extensionType, CopyOnWriteArrayList<ExtensionComponent<T>> legacyStore) {
@ -121,12 +108,9 @@ public class ExtensionList<T> extends AbstractList<T> implements OnMaster {
}
/**
*
* @param legacyStore
* Place to store manually registered instances. The version of the constructor that
* omits this uses a new {@link Vector}, making the storage lifespan tied to the life of {@link ExtensionList}.
* If the manually registered instances are scoped to VM level, the caller should pass in a static list.
* @deprecated {@link #ExtensionList(Jenkins, Class)} should suffice
*/
@Deprecated
protected ExtensionList(Jenkins jenkins, Class<T> extensionType, CopyOnWriteArrayList<ExtensionComponent<T>> legacyStore) {
this.hudson = (Hudson) jenkins;
this.jenkins = jenkins;
@ -437,11 +421,7 @@ public class ExtensionList<T> extends AbstractList<T> implements OnMaster {
@SuppressWarnings({"unchecked", "rawtypes"})
public static <T> ExtensionList<T> create(Jenkins jenkins, Class<T> type) {
if (type.getAnnotation(LegacyInstancesAreScopedToHudson.class) != null)
return new ExtensionList<>(jenkins, type);
else {
return new ExtensionList(jenkins, type, staticLegacyInstances.computeIfAbsent(type, key -> new CopyOnWriteArrayList()));
}
return new ExtensionList<>(jenkins, type);
}
/**
@ -506,16 +486,10 @@ public class ExtensionList<T> extends AbstractList<T> implements OnMaster {
}
/**
* Places to store static-scope legacy instances.
*/
@SuppressWarnings("rawtypes")
private static final Map<Class, CopyOnWriteArrayList> staticLegacyInstances = new ConcurrentHashMap<>();
/**
* Exposed for the test harness to clear all legacy extension instances.
* @deprecated No longer does anything.
*/
@Deprecated
public static void clearLegacyInstances() {
staticLegacyInstances.clear();
}
private static final Logger LOGGER = Logger.getLogger(ExtensionList.class.getName());

View File

@ -29,7 +29,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import jenkins.model.Jenkins;
/**
* Marker interface that designates extensible components
@ -47,10 +46,9 @@ import jenkins.model.Jenkins;
*/
public interface ExtensionPoint {
/**
* Used by designers of extension points (direct subtypes of {@link ExtensionPoint}) to indicate that
* the legacy instances are scoped to {@link Jenkins} instance. By default, legacy instances are
* static scope.
* @deprecated No longer has any effect.
*/
@Deprecated
@Target(TYPE)
@Retention(RUNTIME)
@interface LegacyInstancesAreScopedToHudson {}

View File

@ -31,7 +31,6 @@ import hudson.AbortException;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson;
import hudson.Functions;
import hudson.cli.declarative.CLIMethod;
import hudson.cli.declarative.OptionHandlerExtension;
@ -103,7 +102,6 @@ import org.springframework.security.core.context.SecurityContextHolder;
* @since 1.302
* @see CLIMethod
*/
@LegacyInstancesAreScopedToHudson
public abstract class CLICommand implements ExtensionPoint, Cloneable {
/**

View File

@ -460,7 +460,7 @@ public class OldDataMonitor extends AdministrativeMonitor {
@Override
public String getUrlName() {
return "administrativeMonitor/OldData/";
return "administrativeMonitor/OldData/manage";
}
@Override

View File

@ -35,11 +35,8 @@ import java.io.StringWriter;
import java.io.Writer;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jenkins.util.SystemProperties;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@ -133,7 +130,7 @@ public abstract class MarkupFormatter implements Describable<MarkupFormatter>, E
translate(text, w);
Map<String, String> extraHeaders = Collections.emptyMap();
if (PREVIEWS_SET_CSP) {
extraHeaders = Stream.of("Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy").collect(Collectors.toMap(Function.identity(), v -> "default-src 'none';"));
extraHeaders = Map.of("Content-Security-Policy", "default-src 'none';");
}
return html(200, w.toString(), extraHeaders);
}

View File

@ -27,7 +27,6 @@ package hudson.model;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson;
import hudson.security.Permission;
import hudson.triggers.SCMTrigger;
import hudson.triggers.TimerTrigger;
@ -88,7 +87,6 @@ import org.kohsuke.stapler.interceptor.RequirePOST;
* @since 1.273
* @see Jenkins#administrativeMonitors
*/
@LegacyInstancesAreScopedToHudson
public abstract class AdministrativeMonitor extends AbstractModelObject implements ExtensionPoint, StaplerProxy {
/**
* Human-readable ID of this monitor, which needs to be unique within the system.

View File

@ -44,6 +44,7 @@ import hudson.util.DescribableList;
import hudson.util.FormApply;
import hudson.util.FormValidation;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
@ -281,6 +282,12 @@ public final class ComputerSet extends AbstractModelObject implements Describabl
final Jenkins app = Jenkins.get();
app.checkPermission(Computer.CREATE);
String requestContentType = req.getContentType();
boolean isXmlSubmission = requestContentType != null
&& (requestContentType.startsWith("application/xml")
|| requestContentType.startsWith("text/xml"));
if (mode != null && mode.equals("copy")) {
name = checkName(name);
@ -319,6 +326,22 @@ public final class ComputerSet extends AbstractModelObject implements Describabl
// send the browser to the config page
rsp.sendRedirect2(result.getNodeName() + "/configure");
} else {
if (isXmlSubmission) {
final Node newNode = (Node) Jenkins.XSTREAM2.fromXML(req.getInputStream());
name = Util.fixEmptyAndTrim(name);
if (name != null) {
newNode.setNodeName(name);
}
if (app.getNode(newNode.getNodeName()) != null) {
throw new Failure("Node '" + newNode.getNodeName() + "' already exists");
}
app.addNode(newNode);
rsp.setStatus(HttpServletResponse.SC_OK);
return;
}
// proceed to step 2
if (mode == null) {
throw new Failure("No mode given");

View File

@ -64,6 +64,7 @@ import jenkins.model.Jenkins;
import jenkins.security.MasterToSlaveCallable;
import jenkins.security.ResourceDomainConfiguration;
import jenkins.security.ResourceDomainRootAction;
import jenkins.security.csp.CspHeader;
import jenkins.util.SystemProperties;
import jenkins.util.VirtualFile;
import org.apache.commons.io.IOUtils;
@ -398,13 +399,14 @@ public final class DirectoryBrowserSupport implements HttpResponse {
rsp.sendRedirect(302, ResourceDomainRootAction.get().getRedirectUrl(resourceToken, req.getRestOfPath()));
} else {
if (!ResourceDomainConfiguration.isResourceRequest(req)) {
// if we're serving this from the main domain, set CSP headers
// If we're serving this from the main domain, set CSP headers. These override the default CSP headers.
String csp = SystemProperties.getString(CSP_PROPERTY_NAME, DEFAULT_CSP_VALUE);
if (!csp.trim().isEmpty()) {
// allow users to prevent sending this header by setting empty system property
for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {
rsp.setHeader(header, csp);
}
rsp.setHeader(CspHeader.ContentSecurityPolicy.getHeaderName(), csp);
} else {
// Clear the header value if configured by the user.
rsp.setHeader(CspHeader.ContentSecurityPolicy.getHeaderName(), null);
}
}
InputStream in;

View File

@ -1119,12 +1119,15 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
*
* @return never null
*/
@SuppressWarnings("deprecation")
public PermalinkList getPermalinks() {
PeepholePermalink.initialized();
// TODO: shall we cache this?
PermalinkList permalinks = new PermalinkList(Permalink.BUILTIN);
for (PermalinkProjectAction ppa : getActions(PermalinkProjectAction.class)) {
permalinks.addAll(ppa.getPermalinks());
for (var action : getActions()) {
if (action instanceof PermalinkProjectAction ppa) {
permalinks.addAll(ppa.getPermalinks());
}
}
return permalinks;
}

View File

@ -28,16 +28,19 @@ import hudson.Extension;
import hudson.Util;
import hudson.util.HudsonIsLoading;
import hudson.util.HudsonIsRestarting;
import jakarta.servlet.ServletException;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.management.Badge;
import jenkins.model.Jenkins;
import jenkins.model.ModelObjectWithContextMenu;
import jenkins.model.experimentalflags.NewManageJenkinsUserExperimentalFlag;
import org.apache.commons.jelly.JellyException;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpRedirect;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerFallback;
import org.kohsuke.stapler.StaplerRequest2;
@ -76,6 +79,21 @@ public class ManageJenkinsAction implements RootAction, StaplerFallback, ModelOb
return true;
}
public HttpRedirect doIndex(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException {
try {
var newUiEnabled = new NewManageJenkinsUserExperimentalFlag().getFlagValue();
if (newUiEnabled) {
return new HttpRedirect("configure");
}
req.getView(this, "index.jelly").forward(req, rsp);
} catch (ServletException e) {
throw new RuntimeException(e);
}
return null;
}
@Override
public Object getStaplerFallback() {
return Jenkins.get();

View File

@ -191,23 +191,6 @@ public abstract class Run<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
*/
private long queueId = Run.QUEUE_ID_UNKNOWN;
/**
* Previous build. Can be null.
* TODO JENKINS-22052 this is not actually implemented any more
*
* External code should use {@link #getPreviousBuild()}
*/
@Restricted(NoExternalUse.class)
protected transient volatile RunT previousBuild;
/**
* Next build. Can be null.
*
* External code should use {@link #getNextBuild()}
*/
@Restricted(NoExternalUse.class)
protected transient volatile RunT nextBuild;
/**
* Pointer to the next younger build in progress. This data structure is lazily updated,
* so it may point to the build that's already completed. This pointer is set to 'this'
@ -801,23 +784,19 @@ public abstract class Run<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
}
/**
* Called by {@link RunMap} to drop bi-directional links in preparation for
* deleting a build.
* Called by {@link RunMap} in preparation for deleting a build.
* @see jenkins.model.lazy.LazyBuildMixIn.RunMixIn#dropLinks
* @since 1.556
*/
protected void dropLinks() {
if (nextBuild != null)
nextBuild.previousBuild = previousBuild;
if (previousBuild != null)
previousBuild.nextBuild = nextBuild;
}
/**
* @see jenkins.model.lazy.LazyBuildMixIn.RunMixIn#getPreviousBuild
*/
public @CheckForNull RunT getPreviousBuild() {
return previousBuild;
// TODO could be implemented for benefit of ExternalRun
return null;
}
/**
@ -960,7 +939,8 @@ public abstract class Run<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
* @see jenkins.model.lazy.LazyBuildMixIn.RunMixIn#getNextBuild
*/
public @CheckForNull RunT getNextBuild() {
return nextBuild;
// TODO could be implemented for benefit of ExternalRun
return null;
}

View File

@ -67,6 +67,8 @@ import jenkins.model.Jenkins;
import jenkins.security.FIPS140;
import jenkins.util.SystemProperties;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.StaplerRequest2;
/**
@ -103,8 +105,7 @@ public class UsageStatistics extends PageDecorator implements PersistentDescript
* Returns true if it's time for us to check for new version.
*/
public boolean isDue() {
// user opted out (explicitly or FIPS is requested). no data collection
if (!Jenkins.get().isUsageStatisticsCollected() || DISABLED || FIPS140.useCompliantAlgorithms()) {
if (!isEnabled()) {
return false;
}
@ -116,6 +117,19 @@ public class UsageStatistics extends PageDecorator implements PersistentDescript
return false;
}
/**
* Returns whether between UI configuration, system property, and environment,
* usage statistics should be submitted.
*
* @return true if and only if usage stats should be submitted
* @since TODO
*/
@Restricted(NoExternalUse.class)
public static boolean isEnabled() {
// user opted out (explicitly or FIPS is requested). no data collection
return Jenkins.get().isUsageStatisticsCollected() && !DISABLED && !FIPS140.useCompliantAlgorithms();
}
private RSAPublicKey getKey() {
try {
if (key == null) {

View File

@ -40,6 +40,8 @@ import hudson.model.Queue.JobOffer;
import hudson.model.Queue.Task;
import hudson.model.labels.LabelAssignmentAction;
import hudson.security.ACL;
import hudson.slaves.DumbSlave;
import java.time.Duration;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
@ -330,15 +332,27 @@ public class MappingWorksheet {
this.item = item;
// group executors by their computers
boolean someNonCloud = false;
Map<Computer, List<ExecutorSlot>> j = new HashMap<>();
for (ExecutorSlot o : offers) {
Computer c = o.getExecutor().getOwner();
if (!someNonCloud) {
Node n = c.getNode();
if (n == null || n instanceof DumbSlave) {
someNonCloud = true;
}
}
List<ExecutorSlot> l = j.computeIfAbsent(c, k -> new ArrayList<>());
l.add(o);
}
{ // take load prediction into account and reduce the available executor pool size accordingly
if (j.isEmpty()) {
LOGGER.fine(() -> "nothing to consider for " + item);
} else if (!someNonCloud) {
LOGGER.fine(() -> "only cloud agents to consider for " + item + ": " + offers);
} else { // take load prediction into account and reduce the available executor pool size accordingly
long duration = item.task.getEstimatedDuration();
LOGGER.fine(() -> "for " + item + " taking " + Duration.ofMillis(duration) + " considering " + offers);
if (duration > 0) {
long now = System.currentTimeMillis();
for (Map.Entry<Computer, List<ExecutorSlot>> e : j.entrySet()) {

View File

@ -231,9 +231,7 @@ public abstract class FormFieldValidator {
} else {
response.setContentType("text/html;charset=UTF-8");
if (APPLY_CONTENT_SECURITY_POLICY_HEADERS) {
for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {
response.setHeader(header, "sandbox; default-src 'none';");
}
response.setHeader("Content-Security-Policy", "sandbox; default-src 'none';");
}
response.getWriter().print("<div class=" + cssClass + ">" +
message + "</div>");

View File

@ -612,9 +612,7 @@ public abstract class FormValidation extends IOException implements HttpResponse
protected void respond(StaplerResponse2 rsp, String html) throws IOException, ServletException {
rsp.setContentType("text/html;charset=UTF-8");
if (APPLY_CONTENT_SECURITY_POLICY_HEADERS) {
for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {
rsp.setHeader(header, "sandbox; default-src 'none';");
}
rsp.setHeader("Content-Security-Policy", "sandbox; default-src 'none';");
}
rsp.getWriter().print(html);
}

View File

@ -58,7 +58,6 @@ import hudson.Extension;
import hudson.ExtensionComponent;
import hudson.ExtensionFinder;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.FilePath;
import hudson.Functions;
import hudson.Launcher;
@ -197,7 +196,6 @@ import hudson.util.FormValidation;
import hudson.util.Futures;
import hudson.util.HudsonIsLoading;
import hudson.util.HudsonIsRestarting;
import hudson.util.Iterators;
import hudson.util.JenkinsReloadFailed;
import hudson.util.LogTaskListener;
import hudson.util.MultipartFormDataParser;
@ -1507,8 +1505,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
*/
@SuppressWarnings("rawtypes") // too late to fix
public Descriptor getDescriptor(String id) {
// legacy descriptors that are registered manually doesn't show up in getExtensionList, so check them explicitly.
Iterable<Descriptor> descriptors = Iterators.sequence(getExtensionList(Descriptor.class), DescriptorExtensionList.listLegacyInstances());
Iterable<Descriptor> descriptors = getExtensionList(Descriptor.class);
for (Descriptor d : descriptors) {
if (d.getId().equals(id)) {
return d;
@ -2817,14 +2814,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
}
/**
* Returns {@link ExtensionList} that retains the discovered instances for the given extension type.
*
* @param extensionType
* The base type that represents the extension point. Normally {@link ExtensionPoint} subtype
* but that's not a hard requirement.
* @return
* Can be an empty list but never null.
* @see ExtensionList#lookup
* An obsolete alias for {@link ExtensionList#lookup}.
*/
@SuppressWarnings("unchecked")
public <T> ExtensionList<T> getExtensionList(Class<T> extensionType) {
@ -2848,7 +2838,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
/**
* Returns {@link ExtensionList} that retains the discovered {@link Descriptor} instances for the given
* kind of {@link Describable}.
*
* <p>Assuming an appropriate {@link Descriptor} subtype, for most purposes you can simply use {@link ExtensionList#lookup}.
* @return
* Can be an empty list but never null.
*/

View File

@ -90,7 +90,24 @@ public abstract class PeepholePermalink extends Permalink implements Predicate<R
*/
@Override
public Run<?, ?> resolve(Job<?, ?> job) {
return ExtensionList.lookupFirst(Cache.class).get(job, getId()).resolve(this, job, getId());
return get(job).resolve(this, job, getId());
}
Cache.PermalinkTarget get(Job<?, ?> job) {
return ExtensionList.lookupFirst(Cache.class).get(job, getId());
}
int resolveNumber(Job<?, ?> job) {
// TODO Java 21+ use patterns
var pt = get(job);
if (pt instanceof Cache.Some some) {
return some.number;
} else if (pt instanceof Cache.None) {
return 0;
} else { // Unknown
var b = pt.resolve(this, job, getId());
return b != null ? b.number : 0;
}
}
/**
@ -312,7 +329,7 @@ public abstract class PeepholePermalink extends Permalink implements Predicate<R
public void onDeleted(Run run) {
Job<?, ?> j = run.getParent();
for (PeepholePermalink pp : Util.filter(j.getPermalinks(), PeepholePermalink.class)) {
if (pp.resolve(j) == run) {
if (pp.resolveNumber(j) == run.number) {
Run<?, ?> r = pp.find(run.getPreviousBuild());
LOGGER.fine(() -> "Updating " + pp.getId() + " permalink from deleted " + run + " to " + (r == null ? -1 : r.getNumber()));
pp.updateCache(j, r);
@ -328,8 +345,7 @@ public abstract class PeepholePermalink extends Permalink implements Predicate<R
Job<?, ?> j = run.getParent();
for (PeepholePermalink pp : Util.filter(j.getPermalinks(), PeepholePermalink.class)) {
if (pp.apply(run)) {
Run<?, ?> cur = pp.resolve(j);
if (cur == null || cur.getNumber() < run.getNumber()) {
if (pp.resolveNumber(j) < run.getNumber()) {
LOGGER.fine(() -> "Updating " + pp.getId() + " permalink to completed " + run);
pp.updateCache(j, run);
}

View File

@ -0,0 +1,49 @@
/*
* The MIT License
*
* Copyright (c) 2025, Jan Faracik
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.model.experimentalflags;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.Extension;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@Extension
@Restricted(NoExternalUse.class)
public class NewManageJenkinsUserExperimentalFlag extends BooleanUserExperimentalFlag {
public NewManageJenkinsUserExperimentalFlag() {
super("new-manage-jenkins.flag");
}
@Override
public String getDisplayName() {
return "New Manage Jenkins UI";
}
@Nullable
@Override
public String getShortDescription() {
return "Enables a sidebar for the Manage Jenkins pages for easier navigation.";
}
}

View File

@ -24,7 +24,6 @@
package jenkins.model.lazy;
import static java.util.logging.Level.FINER;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
@ -46,7 +45,6 @@ import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -194,7 +192,6 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT, RunT> & Queue.Task &
return newBuild();
}
builds.put(lastBuild);
lastBuild.getPreviousBuild(); // JENKINS-20662: create connection to previous build
return lastBuild;
} catch (InvocationTargetException e) {
LOGGER.log(Level.WARNING, String.format("A new build could not be created in job %s", asJob().getFullName()), e);
@ -334,32 +331,6 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT, RunT> & Queue.Task &
*/
public abstract static class RunMixIn<JobT extends Job<JobT, RunT> & Queue.Task & LazyBuildMixIn.LazyLoadingJob<JobT, RunT>, RunT extends Run<JobT, RunT> & LazyLoadingRun<JobT, RunT>> {
/**
* Pointers to form bi-directional link between adjacent runs using
* {@link LazyBuildMixIn}.
*
* <p>
* Some {@link Run}s do lazy-loading, so we don't use
* {@link #previousBuildR} and {@link #nextBuildR}, and instead use these
* fields and point to {@link #selfReference} (or {@link #none}) of
* adjacent builds.
*/
private volatile BuildReference<RunT> previousBuildR, nextBuildR;
/**
* Used in {@link #previousBuildR} and {@link #nextBuildR} to indicate
* that we know there is no next/previous build (as opposed to {@code null},
* which is used to indicate we haven't determined if there is a next/previous
* build.)
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private static final BuildReference NONE = new BuildReference("NONE", null);
@SuppressWarnings("unchecked")
private BuildReference<RunT> none() {
return NONE;
}
private BuildReference<RunT> selfReference;
protected RunMixIn() {}
@ -380,19 +351,6 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT, RunT> & Queue.Task &
* To implement {@link Run#dropLinks}.
*/
public final void dropLinks() {
if (nextBuildR != null) {
RunT nb = nextBuildR.get();
if (nb != null) {
nb.getRunMixIn().previousBuildR = previousBuildR;
}
}
if (previousBuildR != null) {
RunT pb = previousBuildR.get();
if (pb != null) {
pb.getRunMixIn().nextBuildR = nextBuildR;
}
}
// make this build object unreachable by other Runs
createReference().clear();
}
@ -401,69 +359,14 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT, RunT> & Queue.Task &
* To implement {@link Run#getPreviousBuild}.
*/
public final RunT getPreviousBuild() {
while (true) {
BuildReference<RunT> r = previousBuildR; // capture the value once
if (r == null) {
// having two neighbors pointing to each other is important to make RunMap.removeValue work
JobT _parent = Objects.requireNonNull(asRun().getParent(), "no parent for " + asRun().number);
RunT pb = _parent.getLazyBuildMixIn()._getRuns().search(asRun().number - 1, AbstractLazyLoadRunMap.Direction.DESC);
if (pb != null) {
pb.getRunMixIn().nextBuildR = createReference(); // establish bi-di link
this.previousBuildR = pb.getRunMixIn().createReference();
LOGGER.log(FINER, "Linked {0}<->{1} in getPreviousBuild()", new Object[]{this, pb});
return pb;
} else {
this.previousBuildR = none();
return null;
}
}
if (r == none()) {
return null;
}
RunT referent = r.get();
if (referent != null) {
return referent;
}
// the reference points to a GC-ed object, drop the reference and do it again
this.previousBuildR = null;
}
return asRun().getParent().getLazyBuildMixIn()._getRuns().search(asRun().number - 1, AbstractLazyLoadRunMap.Direction.DESC);
}
/**
* To implement {@link Run#getNextBuild}.
*/
public final RunT getNextBuild() {
while (true) {
BuildReference<RunT> r = nextBuildR; // capture the value once
if (r == null) {
// having two neighbors pointing to each other is important to make RunMap.removeValue work
RunT nb = asRun().getParent().getLazyBuildMixIn()._getRuns().search(asRun().number + 1, AbstractLazyLoadRunMap.Direction.ASC);
if (nb != null) {
nb.getRunMixIn().previousBuildR = createReference(); // establish bi-di link
this.nextBuildR = nb.getRunMixIn().createReference();
LOGGER.log(FINER, "Linked {0}<->{1} in getNextBuild()", new Object[]{this, nb});
return nb;
} else {
this.nextBuildR = none();
return null;
}
}
if (r == none()) {
return null;
}
RunT referent = r.get();
if (referent != null) {
return referent;
}
// the reference points to a GC-ed object, drop the reference and do it again
this.nextBuildR = null;
}
return asRun().getParent().getLazyBuildMixIn()._getRuns().search(asRun().number + 1, AbstractLazyLoadRunMap.Direction.ASC);
}
}

View File

@ -42,6 +42,9 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
@Extension(ordinal = -1)
public class UserAction implements RootAction {
@Restricted(NoExternalUse.class)
public static final String AVATAR_SIZE = "96x96";
@Override
public String getIconFileName() {
User current = User.current();
@ -50,7 +53,7 @@ public class UserAction implements RootAction {
return null;
}
return getAvatar(current, "96x96");
return getAvatar(current, AVATAR_SIZE);
}
@Override

View File

@ -0,0 +1,61 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp;
import hudson.DescriptorExtensionList;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.model.Describable;
import java.util.Optional;
import jenkins.model.Jenkins;
import jenkins.security.csp.impl.CspConfiguration;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
/**
* Add more advanced options to the {@link jenkins.security.csp.impl.CspConfiguration} UI.
*
* @since TODO
*/
@Restricted(Beta.class)
public abstract class AdvancedConfiguration implements Describable<AdvancedConfiguration>, ExtensionPoint {
public static DescriptorExtensionList<AdvancedConfiguration, AdvancedConfigurationDescriptor> all() {
return Jenkins.get().getDescriptorList(AdvancedConfiguration.class);
}
/**
* Return the currently configured {@link jenkins.security.csp.AdvancedConfiguration}, if any.
*
* @param clazz the {@link jenkins.security.csp.AdvancedConfiguration} type to look up
* @param <T> the {@link jenkins.security.csp.AdvancedConfiguration} type to look up
* @return the configured instance, if any
*/
public static <T extends AdvancedConfiguration> Optional<T> getCurrent(Class<T> clazz) {
return ExtensionList.lookupSingleton(CspConfiguration.class).getAdvanced().stream()
.filter(a -> a.getClass() == clazz)
.map(clazz::cast)
.findFirst();
}
}

View File

@ -0,0 +1,38 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp;
import hudson.model.Descriptor;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
/**
* Descriptor for {@link jenkins.security.csp.AdvancedConfiguration}.
*
* @since TODO
*/
@Restricted(Beta.class)
public abstract class AdvancedConfigurationDescriptor extends Descriptor<AdvancedConfiguration> {
}

View File

@ -0,0 +1,129 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.Extension;
import hudson.ExtensionList;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
/**
* This is a general extension for use by implementations of {@link hudson.tasks.UserAvatarResolver}
* and {@code AvatarMetadataAction} from {@code scm-api} plugin, or other "avatar-like" use cases.
* It simplifies allowlisting safe sources of avatars by offering simple APIs that take a complete URL.
*/
@Restricted(Beta.class)
@Extension
public class AvatarContributor implements Contributor {
private static final Logger LOGGER = Logger.getLogger(AvatarContributor.class.getName());
private final Set<String> domains = ConcurrentHashMap.newKeySet();
@Override
public void apply(CspBuilder cspBuilder) {
domains.forEach(d -> cspBuilder.add("img-src", d));
}
/**
* Request addition of the domain of the specified URL to the allowed set of avatar image domains.
* <p>
* This is a utility method intended to accept any avatar URL from an undetermined, but trusted (for images) domain.
* If the specified URL is not {@code null}, has a host, and {@code http} or {@code https} scheme, its domain will
* be added to the set of allowed domains.
* </p>
* <p>
* <strong>Important:</strong> Only implementations restricting specification of avatar URLs to at least somewhat
* privileged users to should invoke this method, for example users with at least {@link hudson.model.Item#CONFIGURE}
* permission. Note that this guidance may change over time and require implementation changes.
* </p>
*
* @param url The avatar image URL whose domain should be added to the list of allowed domains
*/
public static void allow(@CheckForNull String url) {
String domain = extractDomainFromUrl(url);
if (domain == null) {
LOGGER.log(Level.FINE, "Skipping null domain in avatar URL: " + url);
return;
}
if (ExtensionList.lookupSingleton(AvatarContributor.class).domains.add(domain)) {
LOGGER.log(Level.CONFIG, "Adding domain '" + domain + "' from avatar URL: " + url);
} else {
LOGGER.log(Level.FINEST, "Skipped adding duplicate domain '" + domain + "' from avatar URL: " + url);
}
}
/**
* Utility method extracting the domain specification for CSP fetch directives from a specified URL.
* If the specified URL is not {@code null}, has a host, and {@code http} or {@code https} scheme, this method
* will return its domain.
* This can be used by implementations of {@link jenkins.security.csp.Contributor} for which {@link #allow(String)}
* is not flexible enough (e.g., requesting administrator approval for a domain).
*
* @param url the URL
* @return the domain from the specified URL, or {@code null} if the URL does not satisfy the stated conditions
*/
@CheckForNull
public static String extractDomainFromUrl(@CheckForNull String url) {
if (url == null) {
return null;
}
try {
final URI uri = new URI(url);
final String host = uri.getHost();
if (host == null) {
// If there's no host, assume a local path
LOGGER.log(Level.FINER, "Ignoring URI without host: " + url);
return null;
}
String domain = host;
final String scheme = uri.getScheme();
if (scheme != null) {
if (scheme.equals("http") || scheme.equals("https")) {
domain = scheme + "://" + domain;
} else {
LOGGER.log(Level.FINER, "Ignoring URI with unsupported scheme: " + url);
return null;
}
}
final int port = uri.getPort();
if (port != -1) {
domain = domain + ":" + port;
}
return domain;
} catch (URISyntaxException e) {
LOGGER.log(Level.FINE, "Failed to parse avatar URI: " + url, e);
return null;
}
}
}

View File

@ -0,0 +1,52 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
/**
* Contribute to the Content-Security-Policy rules.
*
* @since TODO
*/
@Restricted(Beta.class)
public interface Contributor extends ExtensionPoint {
/**
* Contribute to the builder's rules by adding to or
* removing from the provided {@link jenkins.security.csp.CspBuilder}.
*
* @param cspBuilder the builder
*/
default void apply(CspBuilder cspBuilder) {
}
static ExtensionList<Contributor> all() {
return ExtensionList.lookup(Contributor.class);
}
}

View File

@ -0,0 +1,254 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Builder for a CSP rule set.
*
* @see jenkins.security.csp.Contributor
* @since TODO
*/
@Restricted(Beta.class)
public class CspBuilder {
private static final Logger LOGGER = Logger.getLogger(CspBuilder.class.getName());
/**
* This list contains directives that accept 'none' as a value and are not fetch directives.
*/
private static final List<String> NONE_DIRECTIVES = List.of(Directive.BASE_URI, Directive.FRAME_ANCESTORS, Directive.FORM_ACTION);
private final Map<String, Set<String>> directives = new HashMap<>();
private final EnumSet<FetchDirective> initializedFDs = EnumSet.noneOf(FetchDirective.class);
/**
* These keys cannot be set explicitly, as they're set by Jenkins.
*/
@Restricted(NoExternalUse.class)
static final Set<String> PROHIBITED_KEYS = Set.of(Directive.REPORT_URI, Directive.REPORT_TO);
public CspBuilder withDefaultContributions() {
Contributor.all().forEach(c -> {
try {
c.apply(this);
} catch (RuntimeException ex) {
LOGGER.log(Level.WARNING, "Failed to apply CSP contributions from " + c, ex);
}
});
return this;
}
/**
* Add the given directive and values. If the directive is already present, merge the values.
* If this is a fetch directive, {@code #add} does not disable inheritance from fallback directives.
* To disable inheritance for fetch directives, call {@link #initialize(FetchDirective, String...)} instead.
* <p>
* The directives {@link jenkins.security.csp.Directive#REPORT_URI} and
* {@link jenkins.security.csp.Directive#REPORT_TO} cannot be set manually, so will be skipped.
* </p>
* <p>
* Similarly, the value {@link jenkins.security.csp.Directive#NONE} cannot be set and will be skipped.
* Instead, call {@link #remove(String, String...)} with a single argument to reset the directive, then
* call {@link #initialize(FetchDirective, String...)} with just the {@link jenkins.security.csp.FetchDirective}
* argument to disable inheritance.
* </p>
*
* @param directive the directive to add
* @param values the values to add to the directive. {@code null} values are ignored. If only {@code null} values
* are passed, the directive will not be added. This is different from calling this with only the
* {@code directive} argument (i.e., an empty list of values), which will add the directive with no
* additional values, potentially resulting in an effective {@link jenkins.security.csp.Directive#NONE}
* value.
* @return this builder
*/
public CspBuilder add(String directive, String... values) {
if (PROHIBITED_KEYS.contains(directive)) {
LOGGER.config("Directive " + directive + " cannot be set manually");
return this;
}
directives.compute(directive, (k, current) -> {
final List<String> additions = new ArrayList<>(Arrays.stream(values).toList());
if (additions.contains(Directive.NONE)) {
LOGGER.config("Cannot explicitly add 'none'. See " + Directive.class.getName() + "#NONE Javadoc.");
additions.remove(Directive.NONE);
}
Set<String> nonNullAdditions = additions.stream().filter(Objects::nonNull).collect(Collectors.toSet());
if (nonNullAdditions.isEmpty() != additions.isEmpty()) {
return current;
}
if (current == null) {
return new HashSet<>(nonNullAdditions);
} else {
nonNullAdditions.addAll(current);
return nonNullAdditions;
}
});
return this;
}
/**
* Remove the given values from the directive, if present. If the directive does not exist, do nothing.
* If no values are provided, removes the entire directive.
*
* @param directive the directive to remove
* @param values the values to remove from the directive, or none if the entire directive should be removed.
* @return this builder
*/
public CspBuilder remove(String directive, String... values) {
if (values.length == 0) {
if (FetchDirective.isFetchDirective(directive)) {
initializedFDs.remove(FetchDirective.fromKey(directive));
}
directives.remove(directive);
} else {
directives.compute(directive, (k, v) -> {
if (v == null) {
return null;
} else {
Arrays.asList(values).forEach(v::remove);
return v;
}
});
}
return this;
}
/**
* Adds an <em>initial value</em> for the specified {@code *-src} directive.
* Unlike calls to {@link #add(String, String...)}, this disables inheriting from (fetch directive) fallbacks.
* This can be invoked multiple times, and the merged set of values will be used.
*
* @param fetchDirective the directive
* @param values Its initial values. If this is an empty list, will initialize as {@link jenkins.security.csp.Directive#NONE}.
* {@code null} values in the list are ignored. If this is a non-empty list with only {@code null}
* values, this invocation has no effect.
* @return this builder
*/
public CspBuilder initialize(FetchDirective fetchDirective, String... values) {
add(fetchDirective.toKey(), values);
if (directives.containsKey(fetchDirective.toKey())) {
initializedFDs.add(fetchDirective);
} else {
// Handle the special case of values being a non-empty array with only null values
LOGGER.log(Level.CONFIG, "Ignoring initialization call with no-op null values list for " + fetchDirective.toKey());
}
return this;
}
/**
* Determine the current effective directives.
* This can be used to inform potential callers of {@link #remove(String, String...)} what to remove.
*
* @return the current effective directives
*/
public List<Directive> getMergedDirectives() {
List<Directive> result = new ArrayList<>();
for (Map.Entry<String, Set<String>> entry : directives.entrySet()) {
Set<String> effectiveValues = new HashSet<>();
final String name = entry.getKey();
// Calculate inherited values from fallback chain
if (FetchDirective.isFetchDirective(name)) {
FetchDirective current = FetchDirective.fromKey(name);
// Check if this directive was initialized
boolean wasInitialized = initializedFDs.contains(current);
// If NOT initialized, traverse fallback chain
if (!wasInitialized) {
FetchDirective fallback = current.getFallback();
while (fallback != null && !initializedFDs.contains(fallback)) {
fallback = fallback.getFallback();
}
if (fallback == null) {
// This could happen if nothing was initialized, in that case, fallback is default-src
fallback = FetchDirective.DEFAULT_SRC;
}
// If we found an initialized fallback, inherit its values
if (directives.containsKey(fallback.toKey())) {
effectiveValues.addAll(directives.get(fallback.toKey()));
}
}
effectiveValues.addAll(entry.getValue());
result.add(new Directive(name, !wasInitialized, List.copyOf(effectiveValues)));
} else {
// Non-fetch directives don't inherit
effectiveValues.addAll(entry.getValue());
result.add(new Directive(name, null, List.copyOf(effectiveValues)));
}
}
return List.copyOf(result);
}
/**
* Build the final CSP string. Any directives with no values left will have the 'none' value set.
*
* @return the CSP string
*/
public String build() {
return buildDirectives().entrySet().stream().map(e -> {
if (e.getValue().isEmpty()) {
return e.getKey() + ";";
}
return e.getKey() + " " + e.getValue() + ";";
}).collect(Collectors.joining(" "));
}
/**
* Compiles the directives into a map from key (e.g., {@code default-src}) to values (e.g., {@code 'self' 'unsafe-inline'}).
*
* @return a map from directive name to its value for all specified directives.
*/
public Map<String, String> buildDirectives() {
return getMergedDirectives().stream().sorted(Comparator.comparing(Directive::name)).map(directive -> {
String name = directive.name();
List<String> values = directive.values().stream().sorted(String::compareTo).toList();
if (values.isEmpty() && (FetchDirective.isFetchDirective(name) || NONE_DIRECTIVES.contains(name))) {
values = List.of(Directive.NONE);
}
return Map.entry(name, String.join(" ", values));
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, TreeMap::new));
}
}

View File

@ -0,0 +1,49 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
/**
* The possible CSP headers.
*
* @since TODO
*/
@Restricted(Beta.class)
public enum CspHeader {
ContentSecurityPolicy("Content-Security-Policy"),
ContentSecurityPolicyReportOnly("Content-Security-Policy-Report-Only");
private final String headerName;
CspHeader(String headerName) {
this.headerName = headerName;
}
public String getHeaderName() {
return headerName;
}
}

View File

@ -0,0 +1,62 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import java.util.Optional;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
/**
* Extension point to decide which {@link jenkins.security.csp.CspHeader} will be set.
* The highest priority implementation returning a value will be chosen both to
* show the configuration UI for {@link jenkins.security.csp.impl.CspConfiguration},
* if any, and to select the header during request processing.
* As a result, implementations must have fairly consistent behavior and not, e.g.,
* inspect the current HTTP request to decide between providing a value (any value)
* or not (inspecting the request and deciding which header to choose is fine,
* as long as an implementation always returns a header).
*
* @since TODO
*/
@Restricted(Beta.class)
public interface CspHeaderDecider extends ExtensionPoint {
Optional<CspHeader> decide();
static ExtensionList<CspHeaderDecider> all() {
return ExtensionList.lookup(CspHeaderDecider.class);
}
static Optional<CspHeaderDecider> getCurrentDecider() {
for (CspHeaderDecider decider : all()) {
final Optional<CspHeader> decision = decider.decide();
if (decision.isPresent()) {
return Optional.of(decider);
}
}
return Optional.empty();
}
}

View File

@ -0,0 +1,46 @@
/*
* The MIT License
*
* Copyright (c) 2021-2025 Daniel Beck, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.ExtensionPoint;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
/**
* Extension point for receivers of Content Security Policy reports.
*
* @since TODO
*/
@Restricted(Beta.class)
public interface CspReceiver extends ExtensionPoint {
void report(@NonNull ViewContext viewContext, @CheckForNull String userId, @NonNull JSONObject report);
record ViewContext(String className, String viewName) {
}
}

View File

@ -0,0 +1,126 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp;
import java.util.List;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Represents a defined Content Security Policy directive
* @param name {@code default-src}, {@code frame-ancestors}, etc.
* @param inheriting whether the directive is inheriting or not. Only applies to
* directives based on {@link jenkins.security.csp.FetchDirective}.
* @param values {@code 'self'}, {@code data:}, {@code jenkins.io}, etc.
*
* @since TODO
*/
@Restricted(Beta.class)
public record Directive(String name, Boolean inheriting, List<String> values) {
/* Fetch directives */
public static final String DEFAULT_SRC = "default-src";
public static final String CHILD_SRC = "child-src";
public static final String CONNECT_SRC = "connect-src";
public static final String FONT_SRC = "font-src";
public static final String FRAME_SRC = "frame-src";
public static final String IMG_SRC = "img-src";
public static final String MANIFEST_SRC = "manifest-src";
public static final String MEDIA_SRC = "media-src";
public static final String OBJECT_SRC = "object-src";
public static final String PREFETCH_SRC = "prefetch-src";
public static final String SCRIPT_SRC = "script-src";
public static final String SCRIPT_SRC_ELEM = "script-src-elem";
public static final String SCRIPT_SRC_ATTR = "script-src-attr";
public static final String STYLE_SRC = "style-src";
public static final String STYLE_SRC_ELEM = "style-src-elem";
public static final String STYLE_SRC_ATTR = "style-src-attr";
public static final String WORKER_SRC = "worker-src";
/* Fetch directives end */
/* Other directives */
public static final String BASE_URI = "base-uri";
/**
* Deprecated directive.
*
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/block-all-mixed-content">MDN</a>
* @deprecated by CSP spec
*/
@Deprecated
public static final String BLOCK_ALL_MIXED_CONTENT = "block-all-mixed-content";
public static final String FORM_ACTION = "form-action";
public static final String FRAME_ANCESTORS = "frame-ancestors";
/**
* Unsupported for use in plugins.
*
* @see CspBuilder#PROHIBITED_KEYS
*/
@Restricted(NoExternalUse.class)
public static final String REPORT_TO = "report-to";
/**
* Deprecated directive. Intended to be replaced by {@link #REPORT_TO}. Unsupported for use in plugins.
*
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/report-uri">MDN</a>
* @see CspBuilder#PROHIBITED_KEYS
* @deprecated by CSP spec
*/
@Restricted(NoExternalUse.class)
@Deprecated
public static final String REPORT_URI = "report-uri";
public static final String REQUIRE_TRUSTED_TYPES_FOR = "require-trusted-types-for";
public static final String SANDBOX = "sandbox";
public static final String TRUSTED_TYPES = "trusted-types";
public static final String UPGRADE_INSECURE_REQUESTS = "upgrade-insecure-requests";
/* Other directives end */
/* Values */
public static final String SELF = "'self'";
/**
* Disallow all.
* Note that this is not a valid argument for {@link CspBuilder#add(String, String...)}.
* To initialize a previously undefined fetch directive, call {@link CspBuilder#initialize(FetchDirective, String...)} and pass no values.
* To remove all other values, call {@link CspBuilder#remove(String, String...)}.
*/
public static final String NONE = "'none'";
public static final String UNSAFE_INLINE = "'unsafe-inline'";
/**
* Probably should not be used.
*
* @deprecated This should not be used.
*/
@Deprecated // Indicator for discouraged use.
public static final String UNSAFE_EVAL = "'unsafe-eval'";
public static final String DATA = "data:";
public static final String BLOB = "blob:";
public static final String REPORT_SAMPLE = "'report-sample'";
/* Values end */
}

View File

@ -0,0 +1,135 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp;
import java.util.Optional;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
/**
* The fetch directives and their inheritance rules (in {@link #getFallback()}).
*
* @see <a href="https://developer.mozilla.org/en-US/docs/Glossary/Fetch_directive">MDN</a>
* @since TODO
*/
@Restricted(Beta.class)
public enum FetchDirective {
DEFAULT_SRC(Directive.DEFAULT_SRC),
CHILD_SRC(Directive.CHILD_SRC),
CONNECT_SRC(Directive.CONNECT_SRC),
FONT_SRC(Directive.FONT_SRC),
FRAME_SRC(Directive.FRAME_SRC),
IMG_SRC(Directive.IMG_SRC),
MANIFEST_SRC(Directive.MANIFEST_SRC),
MEDIA_SRC(Directive.MEDIA_SRC),
OBJECT_SRC(Directive.OBJECT_SRC),
PREFETCH_SRC(Directive.PREFETCH_SRC),
SCRIPT_SRC(Directive.SCRIPT_SRC),
SCRIPT_SRC_ELEM(Directive.SCRIPT_SRC_ELEM),
SCRIPT_SRC_ATTR(Directive.SCRIPT_SRC_ATTR),
STYLE_SRC(Directive.STYLE_SRC),
STYLE_SRC_ELEM(Directive.STYLE_SRC_ELEM),
STYLE_SRC_ATTR(Directive.STYLE_SRC_ATTR),
WORKER_SRC(Directive.WORKER_SRC);
private final String key;
FetchDirective(String s) {
this.key = s;
}
public String toKey() {
return key;
}
/**
* Returns the {@link jenkins.security.csp.FetchDirective} corresponding to
* the specified key. For example, the parameter {@code default-src} will
* return {@link #DEFAULT_SRC}.
*
* @param s the key for the directive
* @return the {@link jenkins.security.csp.FetchDirective} corresponding to the key
*/
public static FetchDirective fromKey(String s) {
for (FetchDirective e : FetchDirective.values()) {
if (e.key.equals(s)) {
return e;
}
}
throw new IllegalArgumentException("Key not found: " + s);
}
/**
* Returns true if and only if the specified key is a {@link jenkins.security.csp.FetchDirective}.
* Returns {@code true} for {@code script-src}, {@code false} for {@code sandbox}.
*/
public static boolean isFetchDirective(String key) {
return toFetchDirective(key).isPresent();
}
/**
* Similar to {@link #fromKey(String)}, this returns the corresponding
* {@link jenkins.security.csp.FetchDirective} wrapped in {@link java.util.Optional}.
* If the specified key does not correspond to a fetch directive, instead leaves the Optional empty.
*
* @param key the key for the directive
* @return an {@link java.util.Optional} containing the corresponding {@link jenkins.security.csp.FetchDirective}, or left empty if there is none.
*/
public static Optional<FetchDirective> toFetchDirective(String key) {
try {
return Optional.of(fromKey(key));
} catch (IllegalArgumentException e) {
return Optional.empty();
}
}
/**
* Which element is used as fallback if one is undefined.
* For {@code *-src-elem} and {@code *-src-attr} this is the corresponding
* {@code *-src}, for {@code frame-src} and {@code worker-src} this is
* {@code child-src}, for everything else, except {@code default-src}, it's
* {@code default-src}.
*
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy#fallbacks">MDN</a>
*
* @return The fallback directive if this one is unspecified, or {@code null}
* if there is no fallback.
*/
public FetchDirective getFallback() {
if (this == SCRIPT_SRC_ATTR || this == SCRIPT_SRC_ELEM) {
return SCRIPT_SRC;
}
if (this == STYLE_SRC_ATTR || this == STYLE_SRC_ELEM) {
return STYLE_SRC;
}
if (this == FRAME_SRC || this == WORKER_SRC) {
return CHILD_SRC;
}
if (this != DEFAULT_SRC) {
return DEFAULT_SRC;
}
return null;
}
}

View File

@ -0,0 +1,102 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Util;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import jenkins.security.HMACConfidentialKey;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
import org.springframework.security.core.Authentication;
/**
* The context (user and page) from which a CSP report is received.
*
* @since TODO
*/
@Restricted(Beta.class)
public class ReportingContext {
private static final HMACConfidentialKey KEY = new HMACConfidentialKey(ReportingContext.class, "key");
private ReportingContext() {}
private static String toBase64(String utf8) {
return Base64.getUrlEncoder().encodeToString(utf8.getBytes(StandardCharsets.UTF_8));
}
private static String fromBase64(String b64) {
return new String(Base64.getUrlDecoder().decode(b64), StandardCharsets.UTF_8);
}
public static String encodeContext(
@CheckForNull final Class<?> ancestorClass,
@CheckForNull final Authentication authentication,
@NonNull final String restOfPath) {
final String userId = authentication == null ? "" : authentication.getName();
final String encodedContext =
toBase64(userId) + ":" + toBase64(ancestorClass == null ? "" : ancestorClass.getName()) + ":" + toBase64(restOfPath);
final String mac =
Base64.getUrlEncoder().encodeToString(KEY.mac(encodedContext.getBytes(StandardCharsets.UTF_8)));
return mac + ":" + encodedContext;
}
public static DecodedContext decodeContext(final String rawContext) {
String[] macAndContext = rawContext.split(":", 2);
if (macAndContext.length != 2) {
throw new IllegalArgumentException(
"Unexpected number of split entries, expected 2, got " + macAndContext.length);
}
String mac = macAndContext[0];
String encodedContext = macAndContext[1];
if (!KEY.checkMac(
encodedContext.getBytes(StandardCharsets.UTF_8),
Base64.getUrlDecoder().decode(mac))) {
throw new IllegalArgumentException("Mac check failed for " + encodedContext);
}
String[] encodedContextParts = encodedContext.split(":", 3);
if (encodedContextParts.length != 3) {
throw new IllegalArgumentException(
"Unexpected number of split entries, expected 3, got " + encodedContextParts.length);
}
return new DecodedContext(
fromBase64(encodedContextParts[0]),
fromBase64(encodedContextParts[1]),
fromBase64(encodedContextParts[2]));
}
public record DecodedContext(String userId, String contextClassName, String restOfPath) {
public DecodedContext(
@CheckForNull String userId, @NonNull String contextClassName, @NonNull String restOfPath) {
this.userId = Util.fixEmpty(userId);
this.contextClassName = contextClassName;
this.restOfPath = restOfPath;
}
}
}

View File

@ -0,0 +1,51 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp;
import java.util.ArrayList;
import java.util.List;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
/**
* Convenient base class for CSP contributors only adding individual domains to a fetch directive.
* Plugins may need to do this, likely for {@code img-src}, to allow loading avatars and similar resources.
*
* @since TODO
*/
@Restricted(Beta.class)
public abstract class SimpleContributor implements Contributor {
private final List<Directive> allowlist = new ArrayList<>();
protected void allow(FetchDirective directive, String... domain) {
// Inheritance is unused, so doesn't matter despite being a FetchDirective
this.allowlist.add(new Directive(directive.toKey(), null, List.of(domain)));
}
@Override
public final void apply(CspBuilder cspBuilder) {
allowlist.forEach(entry -> cspBuilder.add(entry.name(), entry.values().toArray(new String[0])));
}
}

View File

@ -0,0 +1,54 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp.impl;
import static jenkins.security.csp.Directive.REPORT_SAMPLE;
import static jenkins.security.csp.Directive.SELF;
import hudson.Extension;
import jenkins.security.csp.Contributor;
import jenkins.security.csp.CspBuilder;
import jenkins.security.csp.Directive;
import jenkins.security.csp.FetchDirective;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Add a minimal set of CSP directives not expected to be removed later.
*/
@Extension(ordinal = Integer.MAX_VALUE)
@Restricted(NoExternalUse.class)
public final class BaseContributor implements Contributor {
@Override
public void apply(CspBuilder cspBuilder) {
cspBuilder
.initialize(FetchDirective.DEFAULT_SRC, SELF)
.add(Directive.STYLE_SRC, REPORT_SAMPLE)
.add(Directive.SCRIPT_SRC, REPORT_SAMPLE)
.add(Directive.FORM_ACTION, SELF)
.add(Directive.BASE_URI) // 'none'
.add(Directive.FRAME_ANCESTORS, SELF);
}
}

View File

@ -0,0 +1,60 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp.impl;
import static jenkins.security.csp.Directive.DATA;
import static jenkins.security.csp.Directive.IMG_SRC;
import static jenkins.security.csp.Directive.SCRIPT_SRC;
import static jenkins.security.csp.Directive.STYLE_SRC;
import static jenkins.security.csp.Directive.UNSAFE_INLINE;
import hudson.Extension;
import hudson.model.UsageStatistics;
import jenkins.security.csp.Contributor;
import jenkins.security.csp.CspBuilder;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Add the CSP directives currently required for Jenkins to work properly.
* The long-term goal is for these directives to become unnecessary.
* Any for which we determine with high confidence they can never be fixed
* should be moved into {@link BaseContributor} instead.
*/
@Extension(ordinal = Integer.MAX_VALUE / 2.0)
@Restricted(NoExternalUse.class)
public final class CompatibleContributor implements Contributor {
@Override
public void apply(CspBuilder cspBuilder) {
cspBuilder
.add(STYLE_SRC, UNSAFE_INLINE) // Infeasible for now given inline styles in SVGs
.add(IMG_SRC, DATA); // TODO https://github.com/jenkinsci/csp-plugin/issues/60
if (UsageStatistics.isEnabled()) {
// For transparency, always do this while usage stats submission is enabled, rather than checking #isDue.
// TODO https://issues.jenkins.io/browse/JENKINS-76268
cspBuilder.add(SCRIPT_SRC, "usage.jenkins.io");
}
}
}

View File

@ -0,0 +1,159 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp.impl;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.DescriptorExtensionList;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.XmlFile;
import hudson.model.PersistentDescriptor;
import hudson.util.DescribableList;
import hudson.util.FormValidation;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import jenkins.model.GlobalConfiguration;
import jenkins.model.GlobalConfigurationCategory;
import jenkins.model.Jenkins;
import jenkins.security.ResourceDomainConfiguration;
import jenkins.security.csp.AdvancedConfiguration;
import jenkins.security.csp.AdvancedConfigurationDescriptor;
import jenkins.security.csp.CspHeader;
import jenkins.security.csp.CspHeaderDecider;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.verb.POST;
@Extension
@Symbol("contentSecurityPolicy")
@Restricted(NoExternalUse.class)
public class CspConfiguration extends GlobalConfiguration implements PersistentDescriptor {
/**
* Package-private to allow setting by {@link CspRecommendation} without saving.
* Also exposes the "unconfigured" state as {@code null}, unlike the public getter used by Jelly.
*/
protected Boolean enforce;
private final DescribableList<AdvancedConfiguration, AdvancedConfigurationDescriptor> advanced = new DescribableList<>(this);
@NonNull
@Override
public GlobalConfigurationCategory getCategory() {
return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class);
}
public boolean isEnforce() {
return enforce != null && enforce;
}
@DataBoundSetter
public void setEnforce(boolean enforce) {
this.enforce = enforce;
save();
}
// Make available to test in same package
@Override
protected XmlFile getConfigFile() {
return super.getConfigFile();
}
@Restricted(DoNotUse.class) // Jelly
public boolean isShowHeaderConfiguration() {
final Optional<CspHeaderDecider> currentDecider = CspHeaderDecider.getCurrentDecider();
return currentDecider.filter(cspHeaderDecider -> cspHeaderDecider instanceof ConfigurationHeaderDecider).isPresent();
}
@Restricted(DoNotUse.class) // Jelly
public CspHeaderDecider getCurrentDecider() {
return CspHeaderDecider.getCurrentDecider().orElse(null);
}
@POST
public FormValidation doCheckEnforce(@QueryParameter boolean enforce) {
if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
return FormValidation.ok();
}
if (!getConfigFile().exists()) {
// The configuration has not been saved yet
if (enforce) {
if (ResourceDomainConfiguration.isResourceDomainConfigured()) {
return FormValidation.ok(Messages.CspConfiguration_UndefinedToTrueWithResourceDomain());
}
return FormValidation.okWithMarkup(Messages.CspConfiguration_UndefinedToTrueWithoutResourceDomain(Jenkins.get().getRootUrlFromRequest()));
}
// Warning because we're here just after the admin confirmed they wanted to set it up.
return FormValidation.warning(Messages.CspConfiguration_UndefinedToFalse());
}
if (enforce && !ResourceDomainConfiguration.isResourceDomainConfigured()) {
if (ExtensionList.lookupSingleton(CspConfiguration.class).isEnforce()) {
return FormValidation.warningWithMarkup(Messages.CspConfiguration_TrueToTrueWithoutResourceDomain(Jenkins.get().getRootUrlFromRequest()));
}
return FormValidation.okWithMarkup(Messages.CspConfiguration_FalseToTrueWithoutResourceDomain(Jenkins.get().getRootUrlFromRequest()));
}
return FormValidation.ok();
}
public List<? extends AdvancedConfiguration> getAdvanced() {
return advanced;
}
@DataBoundSetter
public void setAdvanced(List<? extends AdvancedConfiguration> advanced) throws IOException {
this.advanced.replaceBy(advanced);
save();
}
/**
* For Jelly
* */
public DescriptorExtensionList<AdvancedConfiguration, AdvancedConfigurationDescriptor> getAdvancedDescriptors() {
return AdvancedConfiguration.all();
}
@Extension
public static class ConfigurationHeaderDecider implements CspHeaderDecider {
@Override
public Optional<CspHeader> decide() {
Boolean enforce = ExtensionList.lookupSingleton(CspConfiguration.class).enforce;
if (enforce == null) {
// If no configuration is present, use FallbackDecider to show initial UI
return Optional.empty();
}
if (enforce) {
return Optional.of(CspHeader.ContentSecurityPolicy);
}
return Optional.of(CspHeader.ContentSecurityPolicyReportOnly);
}
}
}

View File

@ -0,0 +1,127 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp.impl;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.Extension;
import hudson.model.PageDecorator;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import jenkins.security.csp.CspBuilder;
import jenkins.security.csp.CspHeader;
import jenkins.security.csp.CspHeaderDecider;
import jenkins.security.csp.ReportingContext;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest2;
@Restricted(NoExternalUse.class)
@Extension
public class CspDecorator extends PageDecorator {
private static final String REPORTING_ENDPOINT_NAME = "content-security-policy";
private static final Logger LOGGER = Logger.getLogger(CspDecorator.class.getName());
public String getContentSecurityPolicyHeaderValue(HttpServletRequest req) {
String cspDirectives = new CspBuilder().withDefaultContributions().build();
final String reportingEndpoint = getReportingEndpoint(req);
if (reportingEndpoint != null) {
cspDirectives += " report-to " + REPORTING_ENDPOINT_NAME + "; report-uri " + reportingEndpoint;
}
return cspDirectives;
}
// Also used in httpHeaders.jelly
@CheckForNull
public String getReportingEndpointsHeaderValue(HttpServletRequest req) {
final String reportingEndpoint = getReportingEndpoint(req);
if (reportingEndpoint == null) {
return null;
}
return REPORTING_ENDPOINT_NAME + ": " + reportingEndpoint;
}
@CheckForNull
/* package */ static String getReportingEndpoint(HttpServletRequest req) {
Class<?> modelObjectClass = null;
String restOfPath = StringUtils.removeStart(req.getRequestURI(), req.getContextPath());
final StaplerRequest2 staplerRequest2 = Stapler.getCurrentRequest2();
if (staplerRequest2 != null) {
final List<Ancestor> ancestors = staplerRequest2.getAncestors();
if (!ancestors.isEmpty()) {
final Ancestor nearest = ancestors.get(ancestors.size() - 1);
restOfPath = nearest.getRestOfUrl();
modelObjectClass = nearest.getObject().getClass();
}
}
String rootUrl;
try {
rootUrl = Jenkins.get().getRootUrlFromRequest();
} catch (IllegalStateException e) {
LOGGER.log(Level.FINEST, "Cannot get root URL from request", e);
// Outside a Stapler request, probably in the CspFilter, so get configured root URL
try {
rootUrl = Jenkins.get().getRootUrl();
} catch (IllegalStateException ise) {
LOGGER.log(Level.FINEST, "Cannot get root URL from configuration", ise);
return null;
}
}
if (rootUrl == null) {
return null;
}
return rootUrl + ReportingAction.URL + "/" + ReportingContext.encodeContext(modelObjectClass, Jenkins.getAuthentication2(), restOfPath);
}
/**
* Determines the name of the HTTP header to set.
*
* @return the name of the HTTP header to set.
*/
public String getContentSecurityPolicyHeaderName() {
final Optional<CspHeaderDecider> decider = CspHeaderDecider.getCurrentDecider();
if (decider.isPresent()) {
final CspHeaderDecider presentDecider = decider.get();
LOGGER.log(Level.FINEST, "Choosing header from decider " + presentDecider.getClass().getName());
final Optional<CspHeader> decision = presentDecider.decide();
if (decision.isPresent()) {
return decision.get().getHeaderName();
}
LOGGER.log(Level.FINE, "Decider changed its mind after selection: " + presentDecider.getClass().getName());
}
LOGGER.log(Level.WARNING, "Failed to find a CspHeaderDecider, falling back to default");
return CspHeader.ContentSecurityPolicyReportOnly.getHeaderName();
}
}

View File

@ -0,0 +1,104 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp.impl;
import hudson.ExtensionList;
import hudson.Functions;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.security.ResourceDomainConfiguration;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@Restricted(NoExternalUse.class)
public class CspFilter implements Filter {
private static final Logger LOGGER = Logger.getLogger(CspFilter.class.getName());
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!(request instanceof HttpServletRequest req) || !(response instanceof HttpServletResponse rsp)) {
chain.doFilter(request, response);
return;
}
if (!Functions.isExtensionsAvailable()) {
// TODO Implement CSP protection while extensions are not available
LOGGER.log(Level.FINER, "Extensions are not available, so skipping CSP enforcement for: " + req.getRequestURI());
chain.doFilter(request, response);
return;
}
CspDecorator cspDecorator = ExtensionList.lookupSingleton(CspDecorator.class);
final String headerName = cspDecorator.getContentSecurityPolicyHeaderName();
// This is the preliminary value outside Stapler request handling (and providing a context object)
final String headerValue = cspDecorator.getContentSecurityPolicyHeaderValue(req);
final boolean isResourceRequest = ResourceDomainConfiguration.isResourceRequest(req);
if (!isResourceRequest) {
// The Filter/Decorator approach needs us to "set" headers rather than "add", so no additional endpoints are supported at the moment.
final String reportingEndpoints = cspDecorator.getReportingEndpointsHeaderValue(req);
if (reportingEndpoints != null) {
rsp.setHeader("Reporting-Endpoints", reportingEndpoints);
}
rsp.setHeader(headerName, headerValue);
}
try {
chain.doFilter(req, rsp);
} finally {
try {
final String actualHeader = rsp.getHeader(headerName);
if (!isResourceRequest && hasUnexpectedDifference(headerValue, actualHeader)) {
LOGGER.log(Level.FINE, "CSP header has unexpected differences: Expected '" + headerValue + "' but got '" + actualHeader + "'");
}
} catch (RuntimeException e) {
// Be defensive just in case
LOGGER.log(Level.FINER, "Error checking CSP header after request processing", e);
}
}
}
private static boolean hasUnexpectedDifference(String headerByFilter, String actualHeader) {
if (actualHeader == null) {
return true;
}
String expectedPrefix = headerByFilter.substring(0, headerByFilter.indexOf(" report-uri ")); // cf. CspDecorator
if (!actualHeader.contains(" report-uri ")) {
return true;
}
String actualPrefix = actualHeader.substring(0, actualHeader.indexOf(" report-uri "));
return !expectedPrefix.equals(actualPrefix);
}
}

View File

@ -0,0 +1,84 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp.impl;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.model.AdministrativeMonitor;
import java.io.IOException;
import jenkins.security.csp.CspHeaderDecider;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.verb.POST;
/**
* This administrative monitor recommends that admins set up CSP.
* It is shown when no higher priority {@link jenkins.security.csp.CspHeaderDecider}
* determines the header, but {@link CspConfiguration#enforce}
* is {@code null}, indicating admins didn't configure it yet.
*/
@Restricted(NoExternalUse.class)
@Extension
public class CspRecommendation extends AdministrativeMonitor {
@Override
public String getDisplayName() {
return Messages.CspRecommendation_DisplayName();
}
@Override
public boolean isActivated() {
if (ExtensionList.lookupSingleton(CspConfiguration.class).enforce != null) {
// already being configured
return false;
}
// If the current decider is the fallback one that advertises setting up configuration, this should show
return CspHeaderDecider.getCurrentDecider().filter(d -> d instanceof FallbackDecider).isPresent();
}
@Override
public boolean isSecurity() {
return true;
}
@POST
public void doAct(@QueryParameter String setup, @QueryParameter String more, @QueryParameter String dismiss, @QueryParameter String defer) throws IOException {
if (more != null) {
throw HttpResponses.redirectViaContextPath("manage/administrativeMonitor/jenkins.security.csp.impl.CspRecommendation");
}
if (setup != null) {
// Go through field to not call #save in case the admin abandons configuration
ExtensionList.lookupSingleton(CspConfiguration.class).enforce = false;
throw HttpResponses.redirectViaContextPath("manage/configureSecurity/#contentSecurityPolicy");
}
if (dismiss != null) {
disable(true);
}
// Since we no longer show admins monitors just everywhere, we can explicitly navigate here for the monitor and take care of the index view at the same time
throw HttpResponses.redirectViaContextPath("manage/");
}
}

View File

@ -0,0 +1,60 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp.impl;
import hudson.Extension;
import hudson.Main;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.security.csp.CspHeader;
import jenkins.security.csp.CspHeaderDecider;
import jenkins.util.SystemProperties;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* During development and tests, use {@link jenkins.security.csp.CspHeader#ContentSecurityPolicy}.
*/
@Restricted(NoExternalUse.class)
@Extension(ordinal = Double.MAX_VALUE / 2) // High precedence
public class DevelopmentHeaderDecider implements CspHeaderDecider {
private static final Logger LOGGER = Logger.getLogger(DevelopmentHeaderDecider.class.getName());
static /* non-final for script console */ boolean DISABLED = SystemProperties.getBoolean(DevelopmentHeaderDecider.class.getName() + ".DISABLED");
@Override
public Optional<CspHeader> decide() {
if (DISABLED) {
LOGGER.log(Level.FINEST, "DevelopmentHeaderDecider disabled by system property");
return Optional.empty();
}
LOGGER.log(Level.FINEST, "Main.isUnitTest: {0}, Main.isDevelopmentMode: {1}", new Object[] { Main.isUnitTest, Main.isDevelopmentMode });
if (Main.isUnitTest || Main.isDevelopmentMode) {
return Optional.of(CspHeader.ContentSecurityPolicy);
}
return Optional.empty();
}
}

View File

@ -0,0 +1,45 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp.impl;
import hudson.Extension;
import java.util.Optional;
import jenkins.security.csp.CspHeader;
import jenkins.security.csp.CspHeaderDecider;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Fallback implementation: Choose {@link jenkins.security.csp.CspHeader#ContentSecurityPolicyReportOnly}.
* The UI for this lets the user enable {@link CspConfiguration} via {@link CspRecommendation}.
*/
@Restricted(NoExternalUse.class)
@Extension(ordinal = -100000)
public class FallbackDecider implements CspHeaderDecider {
@Override
public Optional<CspHeader> decide() {
return Optional.of(CspHeader.ContentSecurityPolicyReportOnly);
}
}

View File

@ -0,0 +1,52 @@
/*
* The MIT License
*
* Copyright (c) 2025 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp.impl;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.security.csp.CspReceiver;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Basic {@link jenkins.security.csp.CspReceiver} that just logs received reports.
*/
@Restricted(NoExternalUse.class)
@Extension
public class LoggingReceiver implements CspReceiver {
private static final Logger LOGGER = Logger.getLogger(jenkins.security.csp.impl.LoggingReceiver.class.getName());
@Override
public void report(@NonNull ViewContext viewContext, String userId, @NonNull JSONObject report) {
if (userId == null) {
LOGGER.log(Level.FINEST, "Received anonymous report for context {0}: {1}", new Object[]{viewContext, report});
} else {
LOGGER.log(Level.FINE, "Received report from {0} for context {1}: {2}", new Object[]{userId, viewContext, report});
}
}
}

View File

@ -0,0 +1,135 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp.impl;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.model.InvisibleAction;
import hudson.model.UnprotectedRootAction;
import hudson.model.User;
import hudson.security.csrf.CrumbExclusion;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.security.csp.CspReceiver;
import jenkins.security.csp.ReportingContext;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.BoundedInputStream;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.StaplerRequest2;
import org.kohsuke.stapler.verb.POST;
/**
* This action receives reports of Content-Security-Policy violations.
* It needs to be an {@link hudson.model.UnprotectedRootAction} because these requests do not have cookies.
* If we wanted to restrict submissions by unprivileged users, we'd not generate the Content-Security-Policy header
* for them, or removed the report-uri / report-to directives.
*/
@Restricted(NoExternalUse.class)
@Extension
public class ReportingAction extends InvisibleAction implements UnprotectedRootAction {
public static final String URL = "content-security-policy-reporting-endpoint";
private static final Logger LOGGER = Logger.getLogger(ReportingAction.class.getName());
// In limited testing, reports seem to be a few hundred bytes (mostly the actual policy), so this seems plenty.
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP#violation_reporting for an example.
private static /* non-final for script console */ int MAX_REPORT_LENGTH = 20 * 1024;
@Override
public String getUrlName() {
return URL;
}
@POST
public HttpResponse doDynamic(StaplerRequest2 req) {
final String requestRestOfPath = req.getRestOfPath();
String restOfPath = requestRestOfPath.startsWith("/") ? requestRestOfPath.substring(1) : requestRestOfPath;
try {
final ReportingContext.DecodedContext context = ReportingContext.decodeContext(restOfPath);
CspReceiver.ViewContext viewContext =
new CspReceiver.ViewContext(context.contextClassName(), context.restOfPath());
final boolean[] maxReached = new boolean[1];
try (InputStream is = req.getInputStream(); BoundedInputStream bis = BoundedInputStream.builder().setMaxCount(MAX_REPORT_LENGTH).setOnMaxCount((x, y) -> maxReached[0] = true).setInputStream(is).get()) {
String report = IOUtils.toString(bis, req.getCharacterEncoding());
if (maxReached[0]) {
LOGGER.log(Level.FINE, () -> "Report for " + viewContext + " exceeded max length of " + MAX_REPORT_LENGTH);
return HttpResponses.ok();
}
LOGGER.log(Level.FINEST, () -> "Report for " + viewContext + " length: " + report.length());
LOGGER.log(Level.FINER, () -> viewContext + " " + report);
JSONObject jsonObject;
try {
jsonObject = JSONObject.fromObject(report);
} catch (JSONException ex) {
LOGGER.log(Level.FINE, ex, () -> "Failed to parse JSON report for " + viewContext + ": " + report);
return HttpResponses.ok();
}
User user = context.userId() != null ? User.getById(context.userId(), false) : null;
for (CspReceiver receiver :
ExtensionList.lookup(CspReceiver.class)) {
try {
receiver.report(viewContext, user == null ? null : context.userId(), jsonObject);
} catch (Exception ex) {
LOGGER.log(Level.WARNING, ex, () -> "Error reporting CSP for " + viewContext + " to " + receiver);
}
}
} catch (IOException e) {
LOGGER.log(Level.FINE, e, () -> "Failed to read request body for " + viewContext);
}
return HttpResponses.ok();
} catch (RuntimeException ex) {
LOGGER.log(Level.FINE, ex, () -> "Unexpected rest of path failed to decode: " + restOfPath);
return HttpResponses.ok();
}
}
@Extension
public static class CrumbExclusionImpl extends CrumbExclusion {
@Override
public boolean process(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String pathInfo = request.getPathInfo();
if (pathInfo != null && pathInfo.startsWith("/" + URL + "/")) {
chain.doFilter(request, response);
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,63 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp.impl;
import hudson.Extension;
import java.util.Arrays;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.security.csp.CspHeader;
import jenkins.security.csp.CspHeaderDecider;
import jenkins.util.SystemProperties;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Specify the {@link jenkins.security.csp.CspHeader} for {@link CspDecorator} via system property.
*/
@Restricted(NoExternalUse.class)
@Extension(ordinal = Double.MAX_VALUE) // Highest precedence
public class SystemPropertyHeaderDecider implements CspHeaderDecider {
private static final Logger LOGGER = Logger.getLogger(SystemPropertyHeaderDecider.class.getName());
static final String SYSTEM_PROPERTY_NAME = CspHeader.class.getName() + ".headerName";
@Override
public Optional<CspHeader> decide() {
final String systemProperty = SystemProperties.getString(SYSTEM_PROPERTY_NAME);
if (systemProperty != null) {
LOGGER.log(Level.FINEST, "Using system property: {0}", new Object[]{ systemProperty });
return Arrays.stream(CspHeader.values()).filter(h -> h.getHeaderName().equals(systemProperty)).findFirst();
}
return Optional.empty();
}
// Jelly
public String getHeaderName() {
final Optional<CspHeader> decision = decide();
return decision.map(CspHeader::getHeaderName).orElse(null);
}
}

View File

@ -0,0 +1,65 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.csp.impl;
import hudson.Extension;
import hudson.model.User;
import hudson.tasks.UserAvatarResolver;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.navigation.UserAction;
import jenkins.security.csp.AvatarContributor;
import jenkins.security.csp.Contributor;
import jenkins.security.csp.CspBuilder;
import jenkins.security.csp.Directive;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* This extension automatically allows loading images from the domain hosting the current user's avatar
* as determined via {@link hudson.tasks.UserAvatarResolver}.
* Note that this will not make avatars of other users work, if they're from different domains.
*/
@Restricted(NoExternalUse.class)
@Extension
public class UserAvatarContributor implements Contributor {
private static final Logger LOGGER = Logger.getLogger(UserAvatarContributor.class.getName());
@Override
public void apply(CspBuilder cspBuilder) {
User user = User.current();
if (user == null) {
return;
}
final String url = UserAvatarResolver.resolveOrNull(user, UserAction.AVATAR_SIZE);
if (url == null) {
LOGGER.log(Level.FINE, "No avatar image found for user " + user.getId());
return;
}
cspBuilder.add(Directive.IMG_SRC, AvatarContributor.extractDomainFromUrl(url));
}
}

View File

@ -73,6 +73,11 @@ THE SOFTWARE.
<filter-class>hudson.security.HudsonFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter>
<filter-name>csp-filter</filter-name>
<filter-class>jenkins.security.csp.impl.CspFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter>
<filter-name>csrf-filter</filter-name>
<filter-class>hudson.security.csrf.CrumbFilter</filter-class>
@ -154,6 +159,10 @@ THE SOFTWARE.
<filter-name>authentication-filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>csp-filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>csrf-filter</filter-name>
<url-pattern>/*</url-pattern>

View File

@ -25,18 +25,9 @@ THE SOFTWARE.
<!-- About Jenkins page -->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:t="/lib/hudson">
<l:layout type="one-column" permissions="${app.MANAGE_AND_SYSTEM_READ}" title="${%about(app.VERSION)}">
<l:header>
<script src="${resURL}/jsbundles/section-to-tabs.js" type="text/javascript" defer="true" />
</l:header>
<l:main-panel>
<div class="app-about-branding">
<div class="app-about-branding__aurora"></div>
<img src="${imagesURL}/svgs/logo.svg" alt="${%logo}" />
</div>
<div class="jenkins-app-bar">
<j:set var="header">
<l:view>
<div class="jenkins-app-bar jenkins-!-margin-top-2 jenkins-!-margin-bottom-4">
<div class="jenkins-app-bar__content">
<h1 class="app-about-heading">Jenkins</h1>
<p class="app-about-version">
@ -50,93 +41,104 @@ THE SOFTWARE.
</a>
</div>
</div>
</l:view>
</j:set>
<p class="app-about-paragraph">${%blurb}</p>
<l:settings-subpage header="${header}"
placeholder="${null}"
permissions="${app.MANAGE_AND_SYSTEM_READ}">
<script src="${resURL}/jsbundles/section-to-tabs.js" type="text/javascript" defer="true" />
<div class="jenkins-tab-pane">
<h2 class="jenkins-tab-pane__title">${%maven.dependencies}</h2>
<j:set var="uri" value="${it.licensesURL}"/>
<j:choose>
<j:when test="${uri != null}">
<t:thirdPartyLicenses>
<j:include uri="${uri}"/>
</t:thirdPartyLicenses>
</j:when>
<j:otherwise>
<p>${%No information recorded}</p>
</j:otherwise>
</j:choose>
</div>
<div class="app-about-branding">
<div class="app-about-branding__aurora"></div>
<img src="${imagesURL}/svgs/logo.svg" alt="${%logo}" />
</div>
<div class="jenkins-tab-pane">
<h2 class="jenkins-tab-pane__title">${%static.dependencies}</h2>
<table class="jenkins-table sortable">
<thead>
<tr>
<th>${%Name}</th>
<th>${%Author}</th>
<th>${%Licence}</th>
</tr>
</thead>
<tbody>
<p class="jenkins-page-description jenkins-!-text-color">${%blurb}</p>
<div class="jenkins-tab-pane">
<h2 class="jenkins-tab-pane__title">${%maven.dependencies}</h2>
<j:set var="uri" value="${it.licensesURL}"/>
<j:choose>
<j:when test="${uri != null}">
<t:thirdPartyLicenses>
<j:include uri="${uri}"/>
</t:thirdPartyLicenses>
</j:when>
<j:otherwise>
<p>${%No information recorded}</p>
</j:otherwise>
</j:choose>
</div>
<div class="jenkins-tab-pane">
<h2 class="jenkins-tab-pane__title">${%static.dependencies}</h2>
<table class="jenkins-table sortable">
<thead>
<tr>
<th>${%Name}</th>
<th>${%Author}</th>
<th>${%Licence}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<a class="jenkins-table__link" href="https://github.com/jenkins-contrib-themes/jenkins-core-theme">
Jenkins Contrib Themes
</a>
</td>
<td>
<a class="jenkins-table__link" href="https://github.com/afonsof">Afonso Franca</a>
</td>
<td>
<a class="jenkins-table__link" href="https://opensource.org/licenses/MIT">MIT License</a>
</td>
</tr>
<tr>
<td>
<a class="jenkins-table__link" href="https://ionic.io/ionicons">Ionicons</a>
</td>
<td>
<a class="jenkins-table__link" href="https://github.com/ionic-team">Ionic</a>
</td>
<td>
<a class="jenkins-table__link" href="https://github.com/ionic-team/ionicons/blob/master/LICENSE">MIT
License
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="jenkins-tab-pane">
<h2 class="jenkins-tab-pane__title">${%plugin.dependencies}</h2>
<table class="jenkins-table sortable">
<thead>
<tr>
<th>${%Name}</th>
</tr>
</thead>
<tbody>
<j:forEach var="p" items="${app.pluginManager.plugins}">
<tr>
<td>
<a class="jenkins-table__link" href="https://github.com/jenkins-contrib-themes/jenkins-core-theme">
Jenkins Contrib Themes
</a>
</td>
<td>
<a class="jenkins-table__link" href="https://github.com/afonsof">Afonso Franca</a>
</td>
<td>
<a class="jenkins-table__link" href="https://opensource.org/licenses/MIT">MIT License</a>
</td>
</tr>
<tr>
<td>
<a class="jenkins-table__link" href="https://ionic.io/ionicons">Ionicons</a>
</td>
<td>
<a class="jenkins-table__link" href="https://github.com/ionic-team">Ionic</a>
</td>
<td>
<a class="jenkins-table__link" href="https://github.com/ionic-team/ionicons/blob/master/LICENSE">MIT
License
<a class="jenkins-table__link" href="${rootURL}/plugin/${p.shortName}/wrapper/thirdPartyLicenses">
<j:choose>
<j:when test="${p.active}">
${p.displayName}
</j:when>
<j:otherwise>
<strike>${p.displayName}</strike>
</j:otherwise>
</j:choose>
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="jenkins-tab-pane">
<h2 class="jenkins-tab-pane__title">${%plugin.dependencies}</h2>
<table class="jenkins-table sortable">
<thead>
<tr>
<th>${%Name}</th>
</tr>
</thead>
<tbody>
<j:forEach var="p" items="${app.pluginManager.plugins}">
<tr>
<td>
<a class="jenkins-table__link" href="${rootURL}/plugin/${p.shortName}/wrapper/thirdPartyLicenses">
<j:choose>
<j:when test="${p.active}">
${p.displayName}
</j:when>
<j:otherwise>
<strike>${p.displayName}</strike>
</j:otherwise>
</j:choose>
</a>
</td>
</tr>
</j:forEach>
</tbody>
</table>
</div>
</l:main-panel>
</l:layout>
</j:forEach>
</tbody>
</table>
</div>
</l:settings-subpage>
</j:jelly>

View File

@ -24,20 +24,20 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<j:new var="managementLink" className="jenkins.management.CliLink" />
<l:layout title="${managementLink.displayName}" permission="${app.READ}" type="one-column">
<!-- no need for additional breadcrumb here as we're on an index page already including breadcrumb -->
<l:main-panel>
<l:app-bar title="${managementLink.displayName}">
<j:set var="header">
<l:view>
<l:app-bar title="${it.displayName}">
<t:help href="https://www.jenkins.io/redirect/cli" />
</l:app-bar>
<div class="jenkins-page-description">
${%description}
</div>
</l:view>
</j:set>
<l:settings-subpage header="${header}" permission="${app.READ}">
<!-- no need for additional breadcrumb here as we're on an index page already including breadcrumb -->
<f:section title="${%Getting started}">
<ol class="jenkins-instructions">
<li>
@ -79,6 +79,5 @@ THE SOFTWARE.
</tbody>
</table>
</f:section>
</l:main-panel>
</l:layout>
</l:settings-subpage>
</j:jelly>

View File

@ -24,11 +24,13 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:layout title="${%Manage Old Data}" type="one-column">
<!-- no need for additional breadcrumb here as we're on an "index" page already including breadcrumb -->
<l:main-panel>
<h1>${%Manage Old Data}</h1>
<p>${%blurb.1}</p>
<j:set var="previousIt" value="${it}" />
<j:new var="it" className="hudson.diagnosis.OldDataMonitor$ManagementLinkImpl" />
<l:settings-subpage>
<j:set var="it" value="${previousIt}" />
<p class="jenkins-!-margin-top-0">${%blurb.1}</p>
<p>${%blurb.2}</p>
<!-- First set a flag to check if we'll have any valid data -->
@ -132,6 +134,5 @@ THE SOFTWARE.
<f:submit value="${%Discard Unreadable Data}"/>
</form>
</j:if>
</l:main-panel>
</l:layout>
</l:settings-subpage>
</j:jelly>

View File

@ -27,22 +27,30 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:layout title="${%Log Recorders}" type="one-column">
<!-- no need for additional breadcrumb here as we're on an index page already including breadcrumb -->
<l:main-panel xmlns:local="local">
<l:app-bar title="${%Log Recorders}">
<l:isAdmin>
<a href="new" class="jenkins-button jenkins-button--primary">
<l:icon src="symbol-add" />
${%Add recorder}
</a>
</l:isAdmin>
<a href="levels" class="jenkins-button">
${%Log levels}
</a>
<t:help tooltip="${%Additional information on log recorders}" href="https://www.jenkins.io/redirect/log-recorders" />
</l:app-bar>
<j:set var="header">
<j:new var="it" className="jenkins.management.SystemLogLink" />
<l:view>
<l:app-bar title="${it.displayName}">
<l:isAdmin>
<a href="new" class="jenkins-button jenkins-button--primary">
<l:icon src="symbol-add" />
${%Add recorder}
</a>
</l:isAdmin>
<a href="levels" class="jenkins-button">
${%Log levels}
</a>
<t:help tooltip="${%Additional information on log recorders}" href="https://www.jenkins.io/redirect/log-recorders" />
</l:app-bar>
<div class="jenkins-page-description">
<j:out value="${it.description}" />
</div>
</l:view>
</j:set>
<l:settings-subpage header="${header}" xmlns:local="local">
<d:taglib uri="local">
<d:tag name="row">
<tr>
@ -76,6 +84,5 @@ THE SOFTWARE.
</j:forEach>
</tbody>
</table>
</l:main-panel>
</l:layout>
</l:settings-subpage>
</j:jelly>

View File

@ -22,9 +22,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<!--
Displays the console output
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout">
<j:new className="jenkins.run.ChangesTab" var="it">
@ -32,19 +29,26 @@ THE SOFTWARE.
</j:new>
<l:run-subpage>
<l:app-bar title="${it.displayName}" headingLevel="h2" />
<l:app-bar title="${it.displayName}">
<l:details-bar it="${it.object}" />
</l:app-bar>
<j:set var="it" value="${it.object}" />
<j:set var="changeSets" value="${it.object.changeSets}" />
<j:choose>
<j:when test="${it.hasChangeSetComputed()}">
<st:include page="index.jelly" it="${it.changeSet}" />
</j:when>
<j:when test="${it.building}">
${%Not yet determined}
<j:when test="${!empty(changeSets)}">
<div class="jenkins-card">
<div class="jenkins-card__content">
<j:forEach var="changeSet" items="${changeSets}">
<st:include page="index.jelly" it="${changeSet}" />
</j:forEach>
</div>
</div>
</j:when>
<j:otherwise>
${%Failed to determine} (<a href="${h.getConsoleUrl(it)}">${%log}</a>)
<l:notice icon="${it.iconFileName}" title="${%Failed to determine}">
<a href="${h.getConsoleUrl(it.object)}">${%log}</a>
</l:notice>
</j:otherwise>
</j:choose>
</l:run-subpage>

View File

@ -29,7 +29,6 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:p="/lib/hudson/project">
<j:choose>
<j:when test="${newBuildPage}">
<l:task href="${buildUrl.baseUrl}/changes" icon="symbol-changes" title="${%Changes}"/>
<j:choose>
<j:when test="${h.hasPermission(it,it.UPDATE)}">
<l:task icon="symbol-edit-note" href="${buildUrl.baseUrl}/configure" title="${%Edit Build Information}"/>

View File

@ -0,0 +1,12 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
<h2>Create Node</h2>
<p>
To create a new node, post <code>config.xml</code> to <a href="../createItem">this URL</a>. Optionally include the query
parameter <code>name=<i>NODENAME</i></code> if you want to overwrite the node name set in the xml. You need to send
a <code>Content-Type: application/xml header</code>. You will get a 200 status code if the creation
is successful, or 4xx/5xx code if it fails. <code>config.xml</code> is the format Jenkins uses to store the node in the file
system, so you can see examples of them in the Jenkins home directory, or by retrieving the XML configuration of
existing nodes from <code>/computer/<i>NODENAME</i>/config.xml</code>.
</p>
</j:jelly>

View File

@ -27,32 +27,40 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:s="/lib/form">
<l:layout title="${%Nodes}" type="one-column">
<!-- no need for additional breadcrumb here as we're on an index page already including breadcrumb -->
<l:main-panel>
<l:app-bar title="${%Nodes}">
<j:getStatic var="createPermission" className="hudson.model.Computer" field="CREATE"/>
<j:if test="${h.hasPermission(createPermission)}">
<a class="jenkins-button jenkins-button--primary" href="new">
<l:icon src="symbol-add" />
${%New Node}
</a>
</j:if>
<j:set var="hasPermission" value="${h.hasAnyPermission(it, app.MANAGE_AND_SYSTEM_READ)}" />
<j:if test="${hasPermission}">
<a class="jenkins-button" href="configure" tooltip="${%Configure Node Monitors}">
${%Configure Monitors}
</a>
</j:if>
<l:hasAdministerOrManage>
<form method="post" action="updateNow" class="jenkins-!-display-contents">
<button tooltip="${%Refresh status}" class="jenkins-button">
<l:icon src="symbol-refresh" />
</button>
</form>
</l:hasAdministerOrManage>
</l:app-bar>
<j:set var="header">
<l:view>
<j:new var="it" className="jenkins.management.NodesLink" />
<l:app-bar title="${it.displayName}">
<j:getStatic var="createPermission" className="hudson.model.Computer" field="CREATE"/>
<j:if test="${h.hasPermission(createPermission)}">
<a class="jenkins-button jenkins-button--primary" href="new">
<l:icon src="symbol-add" />
${%New Node}
</a>
</j:if>
<j:set var="hasPermission" value="${h.hasAnyPermission(it, app.MANAGE_AND_SYSTEM_READ)}" />
<j:if test="${hasPermission}">
<a class="jenkins-button" href="configure" tooltip="${%Configure Node Monitors}">
${%Configure Monitors}
</a>
</j:if>
<l:hasAdministerOrManage>
<form method="post" action="updateNow" class="jenkins-!-display-contents">
<button tooltip="${%Refresh status}" class="jenkins-button">
<l:icon src="symbol-refresh" />
</button>
</form>
</l:hasAdministerOrManage>
</l:app-bar>
<div class="jenkins-page-description">
${it.description}
</div>
</l:view>
</j:set>
<l:settings-subpage header="${header}">
<j:set var="monitors" value="${it._monitors}"/>
<j:set var="tableWidth" value="${3}"/>
<t:setIconSize/>
@ -133,6 +141,5 @@ THE SOFTWARE.
<st:include page="_legend.jelly"/>
</template>
<script src="${resURL}/jsbundles/pages/computer-set.js" type="text/javascript" defer="true" />
</l:main-panel>
</l:layout>
</l:settings-subpage>
</j:jelly>

View File

@ -24,11 +24,12 @@ THE SOFTWARE.
<!-- renders an HTML fragment that shows trend graph -->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form">
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form" xmlns:l="/lib/layout">
<st:adjunct includes="hudson.model.LoadStatistics.resources"/>
<h1>
${%title(it.displayName)}
</h1>
<j:if test="${it != app}">
<l:app-bar title="${%title(it.displayName)}" />
</j:if>
<f:entry title="${%Timespan}" class="jenkins-form-item--small">
<div class="jenkins-select">

View File

@ -10,64 +10,60 @@ def f=namespace(lib.FormTagLib)
def l=namespace(lib.LayoutTagLib)
def st=namespace("jelly:stapler")
l.layout(permission:app.SYSTEM_READ, title:my.displayName, cssclass:request2.getParameter('decorate'), type:"one-column") {
l.main_panel {
l.app_bar(title: my.displayName)
l.'settings-subpage'(permission: app.SYSTEM_READ) {
set("readOnlyMode", !app.hasPermission(app.ADMINISTER))
set("readOnlyMode", !app.hasPermission(app.ADMINISTER))
l.skeleton()
f.form(method:"post", name:"config", action:"configure", class: "jenkins-form") {
set("instance", my)
set("descriptor", my.descriptor)
l.skeleton()
f.form(method:"post", name:"config", action:"configure", class: "jenkins-form") {
set("instance", my)
set("descriptor", my.descriptor)
f.section(title:_("Authentication")) {
f.entry(help: '/descriptor/hudson.security.GlobalSecurityConfiguration/help/disableRememberMe') {
f.checkbox(title:_("Disable remember me"), field: "disableRememberMe")
}
f.dropdownDescriptorSelector(title: _("Security Realm"), field: 'securityRealm', descriptors: h.filterDescriptors(app, SecurityRealm.all()))
f.dropdownDescriptorSelector(title: _("Authorization"), field: 'authorizationStrategy', descriptors: h.filterDescriptors(app, AuthorizationStrategy.all()))
f.section(title:_("Authentication")) {
f.entry(help: '/descriptor/hudson.security.GlobalSecurityConfiguration/help/disableRememberMe') {
f.checkbox(title:_("Disable remember me"), field: "disableRememberMe")
}
f.dropdownDescriptorSelector(title: _("Security Realm"), field: 'securityRealm', descriptors: h.filterDescriptors(app, SecurityRealm.all()))
f.dropdownDescriptorSelector(title: _("Authorization"), field: 'authorizationStrategy', descriptors: h.filterDescriptors(app, AuthorizationStrategy.all()))
}
f.section(title: _("Markup Formatter")) {
f.dropdownDescriptorSelector(title:_("Markup Formatter"), descriptors: MarkupFormatterDescriptor.all(), field: 'markupFormatter')
}
f.section(title: _("Markup Formatter")) {
f.dropdownDescriptorSelector(title:_("Markup Formatter"), descriptors: MarkupFormatterDescriptor.all(), field: 'markupFormatter')
}
f.section(title: _("Agents")) {
f.entry(title: _("TCP port for inbound agents"), field: "slaveAgentPort") {
if (my.slaveAgentPortEnforced) {
if (my.slaveAgentPort == -1) {
text(_("slaveAgentPortEnforcedDisabled"))
} else if (my.slaveAgentPort == 0) {
text(_("slaveAgentPortEnforcedRandom"))
} else {
text(_("slaveAgentPortEnforced", my.slaveAgentPort))
}
f.section(title: _("Agents")) {
f.entry(title: _("TCP port for inbound agents"), field: "slaveAgentPort") {
if (my.slaveAgentPortEnforced) {
if (my.slaveAgentPort == -1) {
text(_("slaveAgentPortEnforcedDisabled"))
} else if (my.slaveAgentPort == 0) {
text(_("slaveAgentPortEnforcedRandom"))
} else {
f.serverTcpPort()
text(_("slaveAgentPortEnforced", my.slaveAgentPort))
}
} else {
f.serverTcpPort()
}
}
}
Functions.getSortedDescriptorsForGlobalConfigByDescriptor(my.FILTER).each { Descriptor descriptor ->
set("descriptor", descriptor)
set("instance", descriptor)
f.rowSet(name:descriptor.jsonSafeClassName) {
st.include(from:descriptor, page:descriptor.globalConfigPage)
}
}
l.isAdmin() {
f.bottomButtonBar {
f.submit(value: _("Save"))
f.apply()
}
Functions.getSortedDescriptorsForGlobalConfigByDescriptor(my.FILTER).each { Descriptor descriptor ->
set("descriptor", descriptor)
set("instance", descriptor)
f.rowSet(name:descriptor.jsonSafeClassName) {
st.include(from:descriptor, page:descriptor.globalConfigPage)
}
}
l.isAdmin() {
st.adjunct(includes: "lib.form.confirm")
f.bottomButtonBar {
f.submit(value: _("Save"))
f.apply()
}
}
}
l.isAdmin() {
st.adjunct(includes: "lib.form.confirm")
}
}

View File

@ -23,9 +23,9 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout">
<l:layout permission="${app.ADMINISTER}" title="${%Users}" type="one-column">
<l:main-panel>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout">
<j:set var="header">
<l:view>
<l:app-bar title="${%Users}" subtitle="${it.allUsers.size()}">
<l:isAdmin>
<a href="addUser" class="jenkins-button jenkins-button--primary">
@ -35,53 +35,55 @@ THE SOFTWARE.
</l:isAdmin>
</l:app-bar>
<p class="jenkins-description">${%blurb}</p>
<p class="jenkins-page-description">${%blurb}</p>
</l:view>
</j:set>
<table class="jenkins-table sortable" id="people">
<thead>
<l:settings-subpage header="${header}" permission="${app.ADMINISTER}">
<table class="jenkins-table sortable" id="people">
<thead>
<tr>
<th class="jenkins-table__cell--tight" data-sort-disable="true" />
<th initialSortDir="down">${%User ID}</th>
<th>${%Name}</th>
<th class="jenkins-table__cell--tight" data-sort-disable="true" />
<th class="jenkins-table__cell--tight" data-sort-disable="true" />
</tr>
</thead>
<tbody>
<j:forEach var="user" items="${it.allUsers}">
<tr>
<th class="jenkins-table__cell--tight" data-sort-disable="true" />
<th initialSortDir="down">${%User ID}</th>
<th>${%Name}</th>
<th class="jenkins-table__cell--tight" data-sort-disable="true" />
<th class="jenkins-table__cell--tight" data-sort-disable="true" />
<td class="jenkins-table__cell--tight jenkins-table__icon">
<div class="jenkins-table__cell__button-wrapper">
<l:icon src="${h.getUserAvatar(user, '64x64')}" class="jenkins-avatar" />
</div>
</td>
<td>
<a href="${user.url}/" class="jenkins-table__link model-link inside">${user.id}</a>
</td>
<td>
${user.fullName}
</td>
<td>
<div class="jenkins-table__cell__button-wrapper">
<a href="${user.url}/" class="jenkins-button jenkins-button--tertiary">
<l:icon src="symbol-settings" />
</a>
</div>
</td>
<td>
<j:if test="${user.canDelete()}">
<div class="jenkins-table__cell__button-wrapper">
<l:confirmationLink href="${user.url}/doDelete" class="jenkins-button jenkins-button--tertiary jenkins-!-destructive-color"
post="true" message="${%delete.user(user.displayName)}">
<l:icon src="symbol-trash" />
</l:confirmationLink>
</div>
</j:if>
</td>
</tr>
</thead>
<tbody>
<j:forEach var="user" items="${it.allUsers}">
<tr>
<td class="jenkins-table__cell--tight jenkins-table__icon">
<div class="jenkins-table__cell__button-wrapper">
<l:icon src="${h.getUserAvatar(user, '64x64')}" class="jenkins-avatar" />
</div>
</td>
<td>
<a href="${user.url}/" class="jenkins-table__link model-link inside">${user.id}</a>
</td>
<td>
${user.fullName}
</td>
<td>
<div class="jenkins-table__cell__button-wrapper">
<a href="${user.url}/" class="jenkins-button jenkins-button--tertiary">
<l:icon src="symbol-settings" />
</a>
</div>
</td>
<td>
<j:if test="${user.canDelete()}">
<div class="jenkins-table__cell__button-wrapper">
<l:confirmationLink href="${user.url}/doDelete" class="jenkins-button jenkins-button--tertiary jenkins-!-destructive-color"
post="true" message="${%delete.user(user.displayName)}">
<l:icon src="symbol-trash" />
</l:confirmationLink>
</div>
</j:if>
</td>
</tr>
</j:forEach>
</tbody>
</table>
</l:main-panel>
</l:layout>
</j:forEach>
</tbody>
</table>
</l:settings-subpage>
</j:jelly>

View File

@ -27,124 +27,126 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:f="/lib/form">
<l:layout title="${it.displayName}" type="one-column">
<l:isAdmin>
<l:header>
<script src="${resURL}/jsbundles/pages/cloud-set.js" type="text/javascript" />
<link rel="stylesheet" href="${resURL}/jsbundles/pages/cloud-set.css" type="text/css" />
</l:header>
</l:isAdmin>
<l:main-panel>
<j:set var="readOnlyMode" value="${!app.hasPermission(app.ADMINISTER)}"/>
<j:choose>
<j:when test="${it.cloudAvailable and it.hasClouds()}">
<l:app-bar title="${%Clouds}">
<l:isAdmin>
<a class="jenkins-button jenkins-button--primary" href="new">
<l:icon src="symbol-add"/>
${%newCloud}
</a>
</l:isAdmin>
</l:app-bar>
<p class="description">${%description}</p>
<f:form method="post" name="config" action="reorder">
<table id="clouds" class="jenkins-table">
<thead>
<tr>
<th tooltip="${%Drag and drop cells to reorder}">${%Order}</th>
<th>${%Name}</th>
<th/>
</tr>
</thead>
<tbody class="with-drag-drop">
<j:forEach var="cloud" items="${it.clouds}">
<tr class="repeated-chunk" id="cloud_${cloud.name}">
<td data="${cloud.icon}" class="jenkins-table__cell--tight jenkins-table__icon">
<div class="jenkins-table__cell__button-wrapper dd-handle">
<l:icon src="${cloud.iconClassName}" tooltip="${cloud.iconAltText}"/>
</div>
</td>
<td>
<input type="hidden" name="name" value="${cloud.name}" />
<a href="${it.getCloudUrl(request2,app,cloud)}" class="jenkins-table__link model-link inside">${cloud.name}</a>
</td>
<td class="jenkins-table__cell--tight">
<div class="jenkins-table__cell__button-wrapper">
<a href="${it.getCloudUrl(request2,app,cloud)}configure" class="jenkins-button">
<l:icon src="symbol-settings"/>
</a>
</div>
</td>
</tr>
</j:forEach>
</tbody>
</table>
<l:isAdmin>
<f:bottomButtonBar>
<f:submit id="saveButton" value="${%Save}" clazz="jenkins-hidden" />
</f:bottomButtonBar>
</l:isAdmin>
</f:form>
<st:adjunct includes="lib.form.confirm" />
</j:when>
<j:otherwise>
<div class="empty-state-block">
<l:app-bar title="${%Clouds}"/>
<j:set var="previousIt" value="${it}" />
<j:new var="it" className="jenkins.agents.CloudsLink" />
<section class="empty-state-section">
<j:choose>
<j:when test="${it.cloudAvailable}">
<j:choose>
<j:when test="${h.hasPermission(app.ADMINISTER)}">
<p>${%noCloudAvailableCTA}</p>
</j:when>
<j:otherwise>
<p>${%noCloudAvailable}</p>
</j:otherwise>
</j:choose>
</j:when>
<j:otherwise>
<p>${%noCloudPlugin}</p>
</j:otherwise>
</j:choose>
<ul class="empty-state-section-list">
<l:isAdmin>
<j:if test="${it.cloudAvailable}">
<li class="content-block">
<a href="new"
class="content-block__link">
<span>${%newCloud}</span>
<span class="trailing-icon">
<l:icon src="symbol-add"/>
</span>
<l:settings-subpage header="${null}">
<j:set var="it" value="${previousIt}" />
<l:isAdmin>
<script src="${resURL}/jsbundles/pages/cloud-set.js" type="text/javascript" />
<link rel="stylesheet" href="${resURL}/jsbundles/pages/cloud-set.css" type="text/css" />
</l:isAdmin>
<j:set var="readOnlyMode" value="${!app.hasPermission(app.ADMINISTER)}"/>
<j:choose>
<j:when test="${it.cloudAvailable and it.hasClouds()}">
<l:app-bar title="${%Clouds}">
<l:isAdmin>
<a class="jenkins-button jenkins-button--primary" href="new">
<l:icon src="symbol-add"/>
${%newCloud}
</a>
</l:isAdmin>
</l:app-bar>
<p class="description">${%description}</p>
<f:form method="post" name="config" action="reorder">
<table id="clouds" class="jenkins-table">
<thead>
<tr>
<th tooltip="${%Drag and drop cells to reorder}">${%Order}</th>
<th>${%Name}</th>
<th/>
</tr>
</thead>
<tbody class="with-drag-drop">
<j:forEach var="cloud" items="${it.clouds}">
<tr class="repeated-chunk" id="cloud_${cloud.name}">
<td data="${cloud.icon}" class="jenkins-table__cell--tight jenkins-table__icon">
<div class="jenkins-table__cell__button-wrapper dd-handle">
<l:icon src="${cloud.iconClassName}" tooltip="${cloud.iconAltText}"/>
</div>
</td>
<td>
<input type="hidden" name="name" value="${cloud.name}" />
<a href="${it.getCloudUrl(request2,app,cloud)}" class="jenkins-table__link model-link inside">${cloud.name}</a>
</td>
<td class="jenkins-table__cell--tight">
<div class="jenkins-table__cell__button-wrapper">
<a href="${it.getCloudUrl(request2,app,cloud)}configure" class="jenkins-button">
<l:icon src="symbol-settings"/>
</a>
</li>
</j:if>
</div>
</td>
</tr>
</j:forEach>
</tbody>
</table>
<l:isAdmin>
<f:bottomButtonBar>
<f:submit id="saveButton" value="${%Save}" clazz="jenkins-hidden" />
</f:bottomButtonBar>
</l:isAdmin>
</f:form>
<st:adjunct includes="lib.form.confirm" />
</j:when>
<j:otherwise>
<div class="empty-state-block">
<l:app-bar title="${%Clouds}"/>
<section class="empty-state-section">
<j:choose>
<j:when test="${it.cloudAvailable}">
<j:choose>
<j:when test="${h.hasPermission(app.ADMINISTER)}">
<p>${%noCloudAvailableCTA}</p>
</j:when>
<j:otherwise>
<p>${%noCloudAvailable}</p>
</j:otherwise>
</j:choose>
</j:when>
<j:otherwise>
<p>${%noCloudPlugin}</p>
</j:otherwise>
</j:choose>
<ul class="empty-state-section-list">
<l:isAdmin>
<j:if test="${it.cloudAvailable}">
<li class="content-block">
<a href="${rootURL}/manage/pluginManager/available?filter=${it.cloudUpdateCenterCategoryLabel}"
<a href="new"
class="content-block__link">
<span>${%installCloudPlugin}</span>
<span>${%newCloud}</span>
<span class="trailing-icon">
<l:icon src="symbol-plugins" />
<l:icon src="symbol-add"/>
</span>
</a>
</li>
</l:isAdmin>
</j:if>
<li class="content-block">
<a href="https://www.jenkins.io/redirect/distributed-builds"
class="content-block__link"
target="_blank" rel="noopener noreferrer">
<span>${%learnMoreDistributedBuilds}</span>
<a href="${rootURL}/manage/pluginManager/available?filter=${it.cloudUpdateCenterCategoryLabel}"
class="content-block__link">
<span>${%installCloudPlugin}</span>
<span class="trailing-icon">
<l:icon src="symbol-help-circle" />
<l:icon src="symbol-plugins" />
</span>
</a>
</li>
</ul>
</section>
</div>
</j:otherwise>
</j:choose>
</l:main-panel>
</l:layout>
</l:isAdmin>
<li class="content-block">
<a href="https://www.jenkins.io/redirect/distributed-builds"
class="content-block__link"
target="_blank" rel="noopener noreferrer">
<span>${%learnMoreDistributedBuilds}</span>
<span class="trailing-icon">
<l:icon src="symbol-help-circle" />
</span>
</a>
</li>
</ul>
</section>
</div>
</j:otherwise>
</j:choose>
</l:settings-subpage>
</j:jelly>

View File

@ -24,13 +24,12 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:f="/lib/form">
<l:layout title="${%Appearance}" permissions="${app.MANAGE_AND_SYSTEM_READ}" type="one-column">
<j:set var="readOnlyMode" value="${!app.hasPermission(app.MANAGE)}"/>
<j:set var="pluginsUrl" value="${rootURL}/manage/pluginManager/available?filter=UI Themes" />
<j:set var="hasPlugins" value="${it.hasPlugins()}" />
<j:set var="header">
<l:view>
<j:set var="pluginsUrl" value="${rootURL}/manage/pluginManager/available?filter=UI Themes" />
<j:set var="hasPlugins" value="${it.hasPlugins()}" />
<l:main-panel>
<l:app-bar title="${%Appearance}">
<l:app-bar title="${it.displayName}">
<j:if test="${hasPlugins}">
<a href="${pluginsUrl}" class="jenkins-button">
<l:icon src="symbol-plugins" />
@ -39,36 +38,46 @@ THE SOFTWARE.
</j:if>
</l:app-bar>
<j:choose>
<j:when test="${!hasPlugins}">
<l:notice title="${%noPlugins}" icon="symbol-brush-outline">
<a href="${pluginsUrl}">
${%findPlugins}
</a>
</l:notice>
</j:when>
<j:otherwise>
<l:skeleton />
<div class="jenkins-page-description">
${it.description}
</div>
</l:view>
</j:set>
<f:form method="post" name="config" action="configure">
<j:set var="instance" value="${it}" />
<j:set var="descriptor" value="${instance.descriptor}" />
<l:settings-subpage header="${header}" permissions="${app.MANAGE_AND_SYSTEM_READ}">
<j:set var="readOnlyMode" value="${!app.hasPermission(app.MANAGE)}"/>
<j:set var="pluginsUrl" value="${rootURL}/manage/pluginManager/available?filter=UI Themes" />
<j:set var="hasPlugins" value="${it.hasPlugins()}" />
<j:forEach var="descriptor" items="${h.getSortedDescriptorsForGlobalConfigByDescriptor(it.FILTER)}">
<j:set var="instance" value="${descriptor}" />
<f:rowSet name="${descriptor.jsonSafeClassName}">
<st:include page="${descriptor.globalConfigPage}" from="${descriptor}" />
</f:rowSet>
</j:forEach>
<j:choose>
<j:when test="${!hasPlugins}">
<l:notice title="${%noPlugins}" icon="symbol-brush-outline">
<a href="${pluginsUrl}">
${%findPlugins}
</a>
</l:notice>
</j:when>
<j:otherwise>
<l:skeleton />
<f:saveApplyBar/>
</f:form>
<f:form method="post" name="config" action="configure">
<j:set var="instance" value="${it}" />
<j:set var="descriptor" value="${instance.descriptor}" />
<l:hasAdministerOrManage>
<st:adjunct includes="lib.form.confirm" />
</l:hasAdministerOrManage>
</j:otherwise>
</j:choose>
</l:main-panel>
</l:layout>
<j:forEach var="descriptor" items="${h.getSortedDescriptorsForGlobalConfigByDescriptor(it.FILTER)}">
<j:set var="instance" value="${descriptor}" />
<f:rowSet name="${descriptor.jsonSafeClassName}">
<st:include page="${descriptor.globalConfigPage}" from="${descriptor}" />
</f:rowSet>
</j:forEach>
<f:saveApplyBar/>
</f:form>
<l:hasAdministerOrManage>
<st:adjunct includes="lib.form.confirm" />
</l:hasAdministerOrManage>
</j:otherwise>
</j:choose>
</l:settings-subpage>
</j:jelly>

View File

@ -6,35 +6,25 @@ def f = namespace(lib.FormTagLib)
def l = namespace(lib.LayoutTagLib)
def st = namespace("jelly:stapler")
l.layout(permission: app.MANAGE, title: my.displayName, type: "one-column") {
l.main_panel {
h1 {
text(Messages.ShutdownLink_DisplayName_prepare())
l.'settings-subpage'(permission: app.MANAGE) {
f.form(method: "post", name: "prepareShutdown", action: "prepare") {
f.entry(title: Messages.ShutdownLink_ShutDownReason_title()) {
f.textbox(name: "parameter.shutdownReason", value: app.quietDownReason ?: null)
}
p {
text(my.description)
f.bottomButtonBar {
f.submit(value: _(app.isQuietingDown()
? Messages.ShutdownLink_ShutDownReason_update()
: Messages.ShutdownLink_DisplayName_prepare()),
clazz: "jenkins-!-destructive-color")
}
}
f.form(method: "post", name: "prepareShutdown", action: "prepare") {
f.entry(title: Messages.ShutdownLink_ShutDownReason_title()) {
f.textbox(name: "parameter.shutdownReason", value: app.quietDownReason ?: null)
}
if (app.isQuietingDown()) {
f.form(method: "post", name: "cancelShutdown", action: "cancel") {
f.bottomButtonBar {
f.submit(value: _(app.isQuietingDown()
? Messages.ShutdownLink_ShutDownReason_update()
: Messages.ShutdownLink_DisplayName_prepare()),
clazz: "jenkins-!-destructive-color")
}
}
if (app.isQuietingDown()) {
f.form(method: "post", name: "cancelShutdown", action: "cancel") {
f.bottomButtonBar {
f.submit(value: _(Messages.ShutdownLink_DisplayName_cancel()))
}
f.submit(value: _(Messages.ShutdownLink_DisplayName_cancel()))
}
}
}

View File

@ -26,30 +26,40 @@ THE SOFTWARE.
Config page
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:layout permissions="${app.MANAGE_AND_SYSTEM_READ}" title="${%System}" type="one-column">
<st:include page="sidepanel.jelly" />
<f:breadcrumb-config-outline title="${%System}" />
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:f="/lib/form">
<j:new className="jenkins.management.ConfigureLink" var="it" />
<l:main-panel>
<l:settings-subpage permissions="${app.MANAGE_AND_SYSTEM_READ}" includeBreadcrumb="true">
<j:set var="managementLink" value="${it}" />
<j:set var="it" value="${app}" />
<j:set var="readOnlyMode" value="${!h.hasPermission(app.MANAGE)}"/>
<l:app-bar title="${%System}" />
<l:skeleton />
<l:userExperimentalFlag var="newManageJenkins" flagClassName="jenkins.model.experimentalflags.NewManageJenkinsUserExperimentalFlag" />
<j:if test="${newManageJenkins}">
<section class="manage-messages">
<j:forEach var="am" items="${app.activeAdministrativeMonitors}">
<st:include page="message.jelly" it="${am}" />
</j:forEach>
<st:include page="downgrade.jelly" it="${app}" />
</section>
</j:if>
<f:form method="post" name="config" action="configSubmit" class="jenkins-form">
<j:set var="instance" value="${it}" />
<j:set var="descriptor" value="${instance.descriptor}" />
<f:entry title="${%Home directory}" description="${%By default, Jenkins stores all of its data in this directory on the file system}" help="/help/system-config/homeDirectory.html">
<div class="jenkins-quote jenkins-quote--monospace">
${it.rootDir}
<l:copyButton tooltip="${%Copy home directory}" text="${it.rootDir}" iconOnly="true" />
</div>
</f:entry>
<f:entry title="${%System Message}" description="${%This message will be displayed at the top of the Jenkins main page. This can be useful for posting notifications to your users}">
<f:textarea name="system_message" value="${it.systemMessage}" disabled="${readOnlyMode?'true':null}" readonly="${readOnlyMode?'true':null}"
codemirror-mode="${app.markupFormatter.codeMirrorMode}" codemirror-config="${app.markupFormatter.codeMirrorConfig}" previewEndpoint="/markupFormatter/previewDescription"/>
</f:entry>
<f:section title="General">
<f:entry title="${%Home directory}" description="${%By default, Jenkins stores all of its data in this directory on the file system}" help="/help/system-config/homeDirectory.html">
<div class="jenkins-quote jenkins-quote--monospace">
${it.rootDir}
<l:copyButton tooltip="${%Copy home directory}" text="${it.rootDir}" iconOnly="true" />
</div>
</f:entry>
<f:entry title="${%System Message}" description="${%This message will be displayed at the top of the Jenkins main page. This can be useful for posting notifications to your users}">
<f:textarea name="system_message" value="${it.systemMessage}" disabled="${readOnlyMode?'true':null}" readonly="${readOnlyMode?'true':null}"
codemirror-mode="${app.markupFormatter.codeMirrorMode}" codemirror-config="${app.markupFormatter.codeMirrorConfig}" previewEndpoint="/markupFormatter/previewDescription"/>
</f:entry>
</f:section>
<!-- TODO remove after January 2026 LTS line -->
<div class="jenkins-alert jenkins-alert-info">
@ -69,6 +79,5 @@ THE SOFTWARE.
<j:set var="readOnlyMode" value="${!h.hasPermission(app.MANAGE)}"/>
<f:saveApplyBar/>
</f:form>
</l:main-panel>
</l:layout>
</l:settings-subpage>
</j:jelly>

View File

@ -24,11 +24,12 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:layout title="${it.displayName} ${%Load Statistics}" type="one-column">
<l:breadcrumb title="${%Load Statistics}"/>
<l:main-panel>
<j:set var="prefix" value="overallLoad" />
<st:include page="main.jelly" from="${it.overallLoad}" />
</l:main-panel>
</l:layout>
<j:new className="jenkins.management.StatisticsLink" var="it" />
<l:settings-subpage includeBreadcrumb="true">
<j:set var="it" value="${app}" />
<j:set var="prefix" value="overallLoad" />
<st:include page="main.jelly" from="${it.overallLoad}" />
</l:settings-subpage>
</j:jelly>

View File

@ -27,85 +27,79 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:layout permissions="${app.MANAGE_AND_SYSTEM_READ}" title="${%System Information}" type="one-column">
<l:header>
<script src="${resURL}/jsbundles/section-to-tabs.js" type="text/javascript" defer="true" />
<script src="${resURL}/jsbundles/pages/manage-jenkins/system-information.js" type="text/javascript" defer="true" />
</l:header>
<l:breadcrumb title="${%System Information}"/>
<l:main-panel>
<l:app-bar title="${%System Information}" />
<j:new className="jenkins.management.SystemInfoLink" var="it" />
<l:hasPermission permission="${app.SYSTEM_READ}">
<l:tabPane title="${%System Properties}">
<t:propertyTable items="${h.systemProperties}" sensitive="true" />
</l:tabPane>
</l:hasPermission>
<l:settings-subpage permissions="${app.MANAGE_AND_SYSTEM_READ}" includeBreadcrumb="true">
<script src="${resURL}/jsbundles/section-to-tabs.js" type="text/javascript" defer="true" />
<script src="${resURL}/jsbundles/pages/manage-jenkins/system-information.js" type="text/javascript" defer="true" />
<l:hasPermission permission="${app.SYSTEM_READ}">
<l:tabPane title="${%Environment Variables}">
<t:propertyTable items="${h.envVars}" sensitive="true" />
</l:tabPane>
</l:hasPermission>
<l:hasPermission permission="${app.SYSTEM_READ}">
<l:tabPane title="${%System Properties}">
<t:propertyTable items="${h.systemProperties}" sensitive="true" />
</l:tabPane>
</l:hasPermission>
<l:tabPane title="${%Plugins}">
<l:hasPermission permission="${app.SYSTEM_READ}">
<l:tabPane title="${%Environment Variables}">
<t:propertyTable items="${h.envVars}" sensitive="true" />
</l:tabPane>
</l:hasPermission>
<j:choose>
<j:when test="${empty(app.pluginManager.plugins) and empty(app.pluginManager.failedPlugins)}">
<l:notice icon="symbol-plugins" title="${%No plugins installed}" />
</j:when>
<j:otherwise>
<table class="jenkins-table sortable">
<thead>
<l:tabPane title="${%Plugins}">
<j:choose>
<j:when test="${empty(app.pluginManager.plugins) and empty(app.pluginManager.failedPlugins)}">
<l:notice icon="symbol-plugins" title="${%No plugins installed}" />
</j:when>
<j:otherwise>
<table class="jenkins-table sortable">
<thead>
<tr>
<th initialSortDir="down">${%Name}</th>
<th>${%Version}</th>
<th>${%Enabled}</th>
</tr>
</thead>
<tbody>
<j:forEach var="p" items="${app.pluginManager.plugins}">
<j:set var="state" value="${p.enabled?'true':'false'}"/>
<tr>
<th initialSortDir="down">${%Name}</th>
<th>${%Version}</th>
<th>${%Enabled}</th>
<td>
<a class="jenkins-table__link" href="${p.url}" target="_blank" rel="noopener noreferrer">
<st:out value="${p.displayName}"/>
</a>
</td>
<td>
<st:out value="${p.version}"/>
</td>
<td>
<st:out value="${state}"/>
</td>
</tr>
</thead>
<tbody>
<j:forEach var="p" items="${app.pluginManager.plugins}">
<j:set var="state" value="${p.enabled?'true':'false'}"/>
<tr>
<td>
<a class="jenkins-table__link" href="${p.url}" target="_blank" rel="noopener noreferrer">
<st:out value="${p.displayName}"/>
</a>
</td>
<td>
<st:out value="${p.version}"/>
</td>
<td>
<st:out value="${state}"/>
</td>
</tr>
</j:forEach>
</tbody>
</table>
</j:otherwise>
</j:choose>
</j:forEach>
</tbody>
</table>
</j:otherwise>
</j:choose>
</l:tabPane>
<l:tabPane title="${%Memory Usage}">
<f:entry title="${%Timespan}" class="jenkins-form-item--small">
<div class="jenkins-select">
<select id="timespan-select" class="jenkins-select__input">
<option value="sec10">${%Short}</option>
<option value="min" selected="true">${%Medium}</option>
<option value="hour">${%Long}</option>
</select>
</div>
</f:entry>
<div id="graph-host" />
</l:tabPane>
<l:isAdmin>
<l:tabPane title="${%Thread Dumps}">
<p>${%threadDump_blurb('threadDump')}</p>
</l:tabPane>
<l:tabPane title="${%Memory Usage}">
<f:entry title="${%Timespan}" class="jenkins-form-item--small">
<div class="jenkins-select">
<select id="timespan-select" class="jenkins-select__input">
<option value="sec10">${%Short}</option>
<option value="min" selected="true">${%Medium}</option>
<option value="hour">${%Long}</option>
</select>
</div>
</f:entry>
<div id="graph-host" />
</l:tabPane>
<l:isAdmin>
<l:tabPane title="${%Thread Dumps}">
<p>${%threadDump_blurb('threadDump')}</p>
</l:tabPane>
</l:isAdmin>
</l:main-panel>
</l:layout>
</l:isAdmin>
</l:settings-subpage>
</j:jelly>

View File

@ -24,17 +24,10 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:st="jelly:stapler">
<l:card title="${%Changes}" expandable="changes">
<j:choose>
<j:when test="${it.hasChangeSetComputed()}">
<st:include page="index.jelly" it="${it.changeSet}" />
</j:when>
<j:when test="${it.building}">
${%Not yet determined}
</j:when>
<j:otherwise>
${%Failed to determine} (<a href="${h.getConsoleUrl(it)}">${%log}</a>)
</j:otherwise>
</j:choose>
<l:card title="${it.displayName}" expandable="${it.urlName}">
<j:set var="changeSets" value="${it.object.changeSets}" />
<j:forEach var="changeSet" items="${changeSets}">
<st:include it="${changeSet}" page="digest.jelly" />
</j:forEach>
</l:card>
</j:jelly>

View File

@ -23,20 +23,24 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:t="/lib/hudson">
<j:set var="changeSets" value="${it.changeSets}"/>
<table>
<t:summary icon="symbol-changes">
<j:choose>
<j:when test="${!changeSets.isEmpty()}">
<j:forEach var="changeSet" items="${changeSets}">
<st:include it="${changeSet}" page="digest.jelly"/>
</j:forEach>
</j:when>
<j:otherwise>
<st:include class="hudson.scm.EmptyChangeLogSet" page="digest.jelly" />
</j:otherwise>
</j:choose>
</t:summary>
</table>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:t="/lib/hudson" xmlns:l="/lib/layout">
<l:userExperimentalFlag var="newBuildPage" flagClassName="jenkins.model.experimentalflags.NewBuildPageUserExperimentalFlag" />
<j:if test="${!newBuildPage}">
<j:set var="changeSets" value="${it.changeSets}"/>
<table>
<t:summary icon="symbol-changes">
<j:choose>
<j:when test="${!changeSets.isEmpty()}">
<j:forEach var="changeSet" items="${changeSets}">
<st:include it="${changeSet}" page="digest.jelly"/>
</j:forEach>
</j:when>
<j:otherwise>
<st:include class="hudson.scm.EmptyChangeLogSet" page="digest.jelly" />
</j:otherwise>
</j:choose>
</t:summary>
</table>
</j:if>
</j:jelly>

View File

@ -62,7 +62,7 @@ THE SOFTWARE.
</d:tag>
</d:taglib>
<div class="jenkins-hidden" id="api-token-row-template" xmlns:local="local">
<div class="token-card jenkins-card" id="${token.uuid}">
<div class="token-card" id="${token.uuid}">
<div class="token-card-inner">
<div class="token-card__title">
<span class="token-name">${token.name}</span>
@ -122,7 +122,7 @@ THE SOFTWARE.
<j:set var="legacyClazz" value="legacy-token" />
<j:set var="legacyToolTip" value="${%LegacyToken}" />
</j:if>
<div class="token-card jenkins-card ${legacyClazz}" id="${token.uuid}">
<div class="token-card ${legacyClazz}" id="${token.uuid}">
<div class="token-card-inner">
<div class="token-card__title ${legacyClazz}" tooltip="${legacyToolTip}">
<j:if test="${token.isLegacy}">

View File

@ -26,10 +26,40 @@
max-width: 700px;
}
#api-tokens {
width: 100%;
}
#api-token-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.token-card {
flex-direction: row;
display: flex;
gap: 0.5rem;
border-radius: var(--form-input-border-radius);
background: var(--card-background);
position: relative;
}
.token-card::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
border: var(--card-border-width) solid var(--card-border-color);
z-index: 1;
pointer-events: none;
}
.token-card__title,
.token-stats,
.token-never-used,
.token-list .jenkins-card,
.token-list,
.token-controls {
display: flex;
gap: 0.5rem;

View File

@ -24,6 +24,7 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<a name="ResourceDomainConfiguration"/>
<f:section title="${%Serve resource files from another domain}">
<f:entry title="${%Resource Root URL}" field="url">
<f:textbox checkMethod="post"/>

View File

@ -1 +1,2 @@
blurb = Informs about the resource root URL option if the <code>Content-Security-Policy</code> HTTP header for user-controlled resources served by Jenkins has been set to a custom value.
blurb = Informs about the resource root URL option if the <code>Content-Security-Policy</code> HTTP header for user-controlled resources served by Jenkins has been set to a custom value. \
<a href="https://www.jenkins.io/redirect/resource-root-url">Learn more.</a>

View File

@ -0,0 +1,48 @@
<!--
The MIT License
Copyright (c) 2025 CloudBees, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form">
<f:section title="${%Content Security Policy}">
<a name="contentSecurityPolicy"/>
<j:choose>
<j:when test="${instance.isShowHeaderConfiguration()}">
<f:entry field="enforce">
<f:checkbox title="${%Enforce Content Security Policy}"/>
</f:entry>
<j:if test="${!empty(instance.advancedDescriptors)}">
<f:advanced>
<f:descriptorList descriptors="${instance.advancedDescriptors}"
instances="${instance.advanced}"
field="advanced"
forceRowSet="true" />
</f:advanced>
</j:if>
</j:when>
<j:otherwise>
<st:include page="message.jelly" it="${instance.getCurrentDecider()}" />
</j:otherwise>
</j:choose>
</f:section>
</j:jelly>

View File

@ -0,0 +1,8 @@
<div>
When checked, Jenkins will set the
<code>Content-Security-Policy</code>
header that enforces its configuration. Otherwise, it will set the
<code>Content-Security-Policy-Report-Only</code>
header, which only requests that browsers report violations, but does not
enforce them.
</div>

View File

@ -0,0 +1,28 @@
<!--
The MIT License
Copyright (c) 2025 CloudBees, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
<st:setHeader name="${it.getContentSecurityPolicyHeaderName()}" value="${it.getContentSecurityPolicyHeaderValue(request2)}" />
<st:setHeader name="Reporting-Endpoints" value="${it.getReportingEndpointsHeaderValue(request2)}" />
</j:jelly>

View File

@ -0,0 +1,27 @@
<!--
The MIT License
Copyright (c) 2025 CloudBees, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core">
${%blurb}
</j:jelly>

View File

@ -0,0 +1 @@
blurb = Recommend setting up Content Security Policy for the Jenkins UI.

View File

@ -0,0 +1,48 @@
<!--
The MIT License
Copyright (c) 2025 CloudBees, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:f="/lib/form">
<l:layout title="${%Content Security Policy configuration}" type="one-column">
<l:main-panel>
<l:app-bar title="${%Enabling Content Security Policy configuration}"/>
<p>
${%paragraph1}
</p>
<p>
${%paragraph2}
</p>
<p>
${%paragraph3}
</p>
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<div>
<f:bottomButtonBar>
<f:submit name="setup" value="${%Set up now}"/>
<f:submit primary="false" name="defer" value="${%Cancel}"/>
</f:bottomButtonBar>
</div>
</form>
</l:main-panel>
</l:layout>
</j:jelly>

View File

@ -0,0 +1,7 @@
# TODO Add better explanation
paragraph1 = Content Security Policy is a security mechanism that can reduce or eliminate the impact of web security vulnerabilities like cross-site-scripting (XSS).
paragraph2 = Most popular Jenkins plugins are compatible with Jenkins''s default rule set, but not everything currently installed may be. \
If you choose to enforce Content Security Policy and it causes the Jenkins UI to break, you can start Jenkins with the Java system property \
<code>jenkins.security.csp.CspHeader.headerName</code> set to <code>Content-Security-Policy-Report-Only</code> to disable protections again.
paragraph3 = For resources on determining whether your current setup is compatible with Content Security Policy enforcement, visit <a href="https://www.jenkins.io/redirect/csp-compatibility" target="_blank">the documentation website</a>.
# TODO Is this even useful advice, given they could just delete the configuration file?

View File

@ -0,0 +1,33 @@
<!--
The MIT License
Copyright (c) 2025 CloudBees, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-info">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="more" value="${%Learn more}"/>
<f:submit name="dismiss" primary="false" value="${%Dismiss}"/>
</form>
${%blurb}
</div>
</j:jelly>

View File

@ -0,0 +1,4 @@
blurb = Jenkins can enforce Content Security Policy (CSP). \
CSP tells web browsers what they are allowed to do while rendering a web page. \
This limits or even eliminates the impact of vulnerabilities like cross-site scripting (XSS). \
CSP is disabled by default for backward compatibility, but it is recommended to enable it, if possible.

View File

@ -0,0 +1,27 @@
<!--
The MIT License
Copyright (c) 2025 CloudBees, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:description>${%blurb}</f:description>
</j:jelly>

View File

@ -0,0 +1,2 @@
blurb = The Content Security Policy header is currently set to <code>Content-Security-Policy</code> because Jenkins is running in development mode. \
This behavior can be disabled by setting the Java system property <code>jenkins.security.csp.impl.DevelopmentHeaderDecider.DISABLED</code> to <code>true</code>.

View File

@ -0,0 +1,27 @@
<!--
The MIT License
Copyright (c) 2025 CloudBees, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:description>${%blurb(rootURL)}</f:description>
</j:jelly>

View File

@ -0,0 +1,2 @@
blurb = Content-Security-Policy enforcement is currently disabled. \
<a href="{0}/manage/administrativeMonitor/jenkins.security.csp.impl.CspRecommendation">Learn more.</a>

View File

@ -0,0 +1,11 @@
CspRecommendation.DisplayName = Recommend Content Security Policy
CspConfiguration.UndefinedToTrueWithResourceDomain = Content Security Policy will be enforced once you save this configuration.
CspConfiguration.UndefinedToTrueWithoutResourceDomain = Content Security Policy will be enforced once you save this configuration. \
The <a href="{0}manage/configure#ResourceDomainConfiguration" target="_blank">resource root URL</a> should be configured for better protection. \
<a href="https://www.jenkins.io/redirect/resource-root-url" target="_blank">Learn more.</a>
CspConfiguration.UndefinedToFalse = Saving this configuration as is will leave Content Security Policy enforcement disabled. You can enable it now, or at any later time.
CspConfiguration.FalseToTrueWithoutResourceDomain = Content Security Policy will be enforced once you save this configuration. \
The <a href="{0}manage/configure#ResourceDomainConfiguration" target="_blank">resource root URL</a> should be configured for better protection. \
<a href="https://www.jenkins.io/redirect/resource-root-url" target="_blank">Learn more.</a>
CspConfiguration.TrueToTrueWithoutResourceDomain = Content Security Policy is currently enforced, but the <a href="{0}manage/configure#ResourceDomainConfiguration" target="_blank">resource root URL</a> should be configured for better protection. \
<a href="https://www.jenkins.io/redirect/resource-root-url" target="_blank">Learn more.</a>

View File

@ -0,0 +1,27 @@
<!--
The MIT License
Copyright (c) 2025 CloudBees, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:description>${%blurb(it.headerName)}</f:description>
</j:jelly>

View File

@ -0,0 +1,2 @@
blurb = Content Security Policy is configured to use the HTTP header <code>{0}</code> based on the system property <code>jenkins.security.csp.CspHeader.headerName</code>. \
It cannot be configured through the UI while this system property specifies a header name.

View File

@ -7,32 +7,29 @@ def f=namespace(lib.FormTagLib)
def l=namespace(lib.LayoutTagLib)
def st=namespace("jelly:stapler")
l.layout(permission:app.SYSTEM_READ, title:my.displayName, type:"one-column") {
l.'settings-subpage'(permission: app.SYSTEM_READ) {
set("readOnlyMode", !app.hasPermission(app.ADMINISTER))
l.main_panel {
l.app_bar(title: my.displayName)
l.skeleton()
l.skeleton()
f.form(method:"post",name:"config",action:"configure", class: "jenkins-form") {
Functions.getSortedDescriptorsForGlobalConfigByDescriptor(my.FILTER).each { Descriptor descriptor ->
set("descriptor",descriptor)
set("instance",descriptor)
f.rowSet(name:descriptor.jsonSafeClassName) {
st.include(from:descriptor, page:descriptor.globalConfigPage)
}
}
l.isAdmin() {
f.bottomButtonBar {
f.submit(value: _("Save"))
f.apply(value: _("Apply"))
}
f.form(method:"post",name:"config",action:"configure", class: "jenkins-form") {
Functions.getSortedDescriptorsForGlobalConfigByDescriptor(my.FILTER).each { Descriptor descriptor ->
set("descriptor",descriptor)
set("instance",descriptor)
f.rowSet(name:descriptor.jsonSafeClassName) {
st.include(from:descriptor, page:descriptor.globalConfigPage)
}
}
l.isAdmin() {
st.adjunct(includes: "lib.form.confirm")
f.bottomButtonBar {
f.submit(value: _("Save"))
f.apply(value: _("Apply"))
}
}
}
l.isAdmin() {
st.adjunct(includes: "lib.form.confirm")
}
}

View File

@ -40,23 +40,21 @@ THE SOFTWARE.
Generates a row containing the page title and an optional set of controls
</st:documentation>
<j:if test="${mode=='main-panel' or mode=='side-panel'}">
<div class="jenkins-app-bar ${attrs.icon != null ? 'jenkins-app-bar--with-icon' :''}">
<div class="jenkins-app-bar__content">
<j:if test="${attrs.icon != null}">
<l:icon src="${attrs.icon}" class="jenkins-avatar" />
</j:if>
<div class="jenkins-app-bar ${attrs.icon != null ? 'jenkins-app-bar--with-icon' :''}">
<div class="jenkins-app-bar__content">
<j:if test="${attrs.icon != null}">
<l:icon src="${attrs.icon}" class="jenkins-avatar" />
</j:if>
<x:element name="${headingLevel ?: 'h1'}">
${title}
<j:if test="${attrs.subtitle != null}">
<span class="jenkins-app-bar__subtitle">${attrs.subtitle}</span>
</j:if>
</x:element>
</div>
<div class="jenkins-app-bar__controls">
<d:invokeBody/>
</div>
<x:element name="${headingLevel ?: 'h1'}">
${title}
<j:if test="${attrs.subtitle != null}">
<span class="jenkins-app-bar__subtitle">${attrs.subtitle}</span>
</j:if>
</x:element>
</div>
</j:if>
<div class="jenkins-app-bar__controls">
<d:invokeBody/>
</div>
</div>
</j:jelly>

View File

@ -0,0 +1,33 @@
<!--
The MIT License
Copyright (c) 2025 Jan Faracik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define">
<st:documentation>
A lazy-loaded element.
</st:documentation>
<j:if test="${deferMode == 'children'}">
<d:invokeBody />
</j:if>
</j:jelly>

View File

@ -0,0 +1,43 @@
<!--
The MIT License
Copyright (c) 2025 Jan Faracik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout">
<st:documentation>
A lazy-loaded element.
</st:documentation>
<j:set var="deferMode" value="placeholder" scope="parent" />
<div class="jenkins-!-display-contents">
<d:invokeBody />
</div>
<j:set var="deferMode" value="children" scope="parent" />
<l:renderOnDemand clazz="defer-element" tag="div" capture="it,deferMode,previousIt">
<l:ajax>
<div>
<d:invokeBody />
</div>
</l:ajax>
</l:renderOnDemand>
</j:jelly>

Some files were not shown because too many files have changed in this diff Show More