mirror of https://github.com/jenkinsci/jenkins.git
Compare commits
51 Commits
jenkins-2.
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
01fa17680b | |
|
|
46b6e75cf0 | |
|
|
53a905f0ca | |
|
|
a9c6135e43 | |
|
|
b0ea4eba9b | |
|
|
41166d1fc7 | |
|
|
1c60311df2 | |
|
|
2ba827b6c5 | |
|
|
2fa902c78c | |
|
|
f639f2bf12 | |
|
|
5789789340 | |
|
|
110014cf69 | |
|
|
bdf22a2dfd | |
|
|
503b9ac80f | |
|
|
9fb6bf096a | |
|
|
be73a8dd79 | |
|
|
3fb56d4100 | |
|
|
101f904484 | |
|
|
462d748f4b | |
|
|
9a0ae657ca | |
|
|
422e3b8990 | |
|
|
fc21d6f6e6 | |
|
|
91c0643368 | |
|
|
3f15d106aa | |
|
|
d5ecc45833 | |
|
|
5e84413d07 | |
|
|
44513735d6 | |
|
|
74ff843a80 | |
|
|
bdb06bfc55 | |
|
|
86c28550d5 | |
|
|
f18f78d870 | |
|
|
e658af93c5 | |
|
|
28f6320dbe | |
|
|
64dd84718a | |
|
|
cca038f922 | |
|
|
23ff4bcb63 | |
|
|
2a6f83dccb | |
|
|
04bd568cb2 | |
|
|
ec8ec623b2 | |
|
|
8e9147a96d | |
|
|
6e2067e616 | |
|
|
30f301b517 | |
|
|
1c38fe64ee | |
|
|
ab45729395 | |
|
|
90416179c0 | |
|
|
a86e4e2264 | |
|
|
320a149f76 | |
|
|
d8f229853a | |
|
|
a6e8edc1d4 | |
|
|
205849e0f6 | |
|
|
8f1bf5be69 |
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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/**",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
2
ath.sh
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -460,7 +460,7 @@ public class OldDataMonitor extends AdministrativeMonitor {
|
|||
|
||||
@Override
|
||||
public String getUrlName() {
|
||||
return "administrativeMonitor/OldData/";
|
||||
return "administrativeMonitor/OldData/manage";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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>");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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> {
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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])));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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/");
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}"/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
blurb = Recommend setting up Content Security Policy for the Jenkins UI.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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?
|
||||
|
|
@ -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>
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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.
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
Loading…
Reference in New Issue