Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02b8d4cb32 | |||
| e0655bee7c | |||
| 1a90041271 | |||
| 2ee188ee36 | |||
| 5bfd1f59d1 | |||
| 4b10c2daac | |||
| 0e4d29763f | |||
| 63e24a3026 | |||
| 514600721c | |||
| 2563b854ea | |||
| 9027dceb71 | |||
| d61a85f74b | |||
| 8b3d33ed64 | |||
| defe27d53e | |||
| c5b86370ff | |||
| da796e44e5 | |||
| f5b9dc3282 | |||
| 1ebd663253 | |||
| a8c60393f1 | |||
| 09198447af | |||
| 9ebee3e984 | |||
| 60e3f27211 | |||
| f7650b3ec0 | |||
| e1ec77a7a1 | |||
| 16e0d61005 | |||
| befa003f94 | |||
| 74a77d2f72 | |||
| a80fdee533 | |||
| 53b581bb8c | |||
| d686037419 | |||
| 5c8311b7d3 | |||
| d86545dd41 | |||
| ee1031356a | |||
| f74aeaffa1 | |||
| ec1b15ed68 | |||
| 93aa48f863 | |||
| ce995a0178 | |||
| bbc5c5ce84 | |||
| 1b8cf6cc55 | |||
| e5ee45d568 | |||
| 38ae090d3d | |||
| 3b9991fc89 | |||
| 1ecb036fba | |||
| 0e211382ee | |||
| 2477c22674 | |||
| d88f2e5133 | |||
| c35e107744 | |||
| 3f7d9e389d | |||
| 992243fc8b | |||
| d0341bf893 | |||
| 8271258618 | |||
| 761fdd228d | |||
| 1010d253a4 | |||
| c8342fcfae | |||
| c97f710747 | |||
| 2287d67106 | |||
| da4af08362 | |||
| 61df8e493d | |||
| 470e2c5c97 | |||
| ade45771b2 | |||
| 657760af5b | |||
| 8a03482856 | |||
| 71a51db3a1 | |||
| 17873adf1f | |||
| 4bab573562 | |||
| c74f6bdac4 | |||
| a55c5c1d0d | |||
| 8a7ae7a7f9 | |||
| a608352689 | |||
| 0c20b14542 | |||
| c3dbbb2d01 | |||
| d187ddb5c6 | |||
| 9a5319d5a2 | |||
| 1fb00879f6 | |||
| 472c9f8275 | |||
| ef00312991 | |||
| 0af0751cfd | |||
| c2447ec257 | |||
| 39dbd24dcb | |||
| 33ebd5405a | |||
| 76a465a544 | |||
| 5f1b9862cc | |||
| 6a66a5b74f | |||
| 2d2c2d31fa | |||
| 80845d0c9a | |||
| ba575e8564 | |||
| 79801134b6 | |||
| 7162046144 | |||
| ce9f1821b1 | |||
| 153ec5fbde | |||
| b58018dc0f | |||
| 6c9efc2f09 | |||
| 55be9eabd4 | |||
| 1a163dca37 | |||
| 1b0c4d68da | |||
| b38b495630 | |||
| c8a0601e6a | |||
| f226490be3 | |||
| f84c4ea583 | |||
| 2c9dc08e43 | |||
| 5a7f12f1a9 | |||
| 8d6ede27d1 | |||
| 747b806d37 | |||
| 3b355a0278 | |||
| 82e3061120 | |||
| a6d362fa18 | |||
| dcfee957b5 | |||
| 37984c2e11 | |||
| d17a533e86 | |||
| b3a43d6154 | |||
| 712595b3a3 | |||
| a87fc4ea8a | |||
| 85d7e9ab55 | |||
| e7bff4240a | |||
| 9794952153 | |||
| 4b30acd1e0 | |||
| 4e6b8e4d29 | |||
| e550c48180 | |||
| 6d5f87e631 | |||
| 2202632cad | |||
| a3c06d98ca | |||
| 8fe0303bad | |||
| 8f42c86a57 | |||
| a4a4c813d4 | |||
| 5dae6da15a | |||
| 6955c7aaea | |||
| 0ab9ad774d | |||
| e1c4177cd8 | |||
| 7f34ef7951 | |||
| c2e97d7661 | |||
| 7e9f421a77 | |||
| ddd5d2e9cd | |||
| b20d05724f | |||
| 27cd9fa86c | |||
| 94a66f7a26 | |||
| 7f106f0419 | |||
| 5f80468de3 | |||
| 2f762fefe1 | |||
| 2fdd541ea5 | |||
| 45c37c4454 | |||
| 89246998c4 | |||
| e8c93fdc98 | |||
| 008296cce2 | |||
| 238bc9733a | |||
| d80fade8b1 | |||
| 0c70f358d5 | |||
| 748c723bba | |||
| 2bd2aa9a75 | |||
| 7d38686e09 | |||
| 8b9beb0e1f | |||
| dd5bfc0b31 |
@@ -1,23 +0,0 @@
|
||||
name: Clean build artifacts
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 10 * * *' # Once per day at 10am UTC
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
permissions:
|
||||
contents: none
|
||||
steps:
|
||||
- name: Delete artifacts in cron job
|
||||
env:
|
||||
GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: |
|
||||
echo "Running clean build artifacts logic"
|
||||
output=$(curl -X GET -H "Authorization: token $GH_ACTIONS_REPO_TOKEN" https://api.github.com/repos/spring-projects/spring-security/actions/artifacts | grep '"id"' | cut -d : -f2 | sed 's/,*$//g')
|
||||
echo Output is $output
|
||||
for id in $output; do curl -X DELETE -H "Authorization: token $GH_ACTIONS_REPO_TOKEN" https://api.github.com/repos/spring-projects/spring-security/actions/artifacts/$id; done;
|
||||
@@ -1,130 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- "dependabot/**"
|
||||
schedule:
|
||||
- cron: '0 10 * * *' # Once per day at 10am UTC
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
env:
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@v1
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest ]
|
||||
jdk: [ 17 ]
|
||||
with:
|
||||
runs-on: ${{ matrix.os }}
|
||||
java-version: ${{ matrix.jdk }}
|
||||
distribution: temurin
|
||||
secrets: inherit
|
||||
test:
|
||||
name: Test Against Snapshots
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@v1
|
||||
strategy:
|
||||
matrix:
|
||||
java-version: [ 17 ]
|
||||
with:
|
||||
java-version: ${{ matrix.java-version }}
|
||||
test-args: --refresh-dependencies -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PspringFrameworkVersion=6.0.+ -PreactorVersion=2022.0.+ -PspringDataVersion=2022.0.+ --stacktrace
|
||||
secrets: inherit
|
||||
check-samples:
|
||||
name: Check Samples
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'spring-projects' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- name: Check samples project
|
||||
env:
|
||||
LOCAL_REPOSITORY_PATH: ${{ github.workspace }}/build/publications/repos
|
||||
SAMPLES_DIR: ../spring-security-samples
|
||||
run: |
|
||||
# Extract version from gradle.properties
|
||||
version=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}')
|
||||
# Extract samplesBranch from gradle.properties
|
||||
samples_branch=$(cat gradle.properties | grep "samplesBranch=" | awk -F'=' '{print $2}')
|
||||
./gradlew publishMavenJavaPublicationToLocalRepository
|
||||
./gradlew cloneRepository -PrepositoryName="spring-projects/spring-security-samples" -Pref="$samples_branch" -PcloneOutputDirectory="$SAMPLES_DIR"
|
||||
./gradlew --project-dir "$SAMPLES_DIR" --init-script spring-security-ci.gradle -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" -PspringSecurityVersion="$version" :runAllTests
|
||||
check-tangles:
|
||||
name: Check for Package Tangles
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'spring-projects' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- name: Check for package tangles
|
||||
env:
|
||||
STRUCTURE101_LICENSEID: ${{ secrets.STRUCTURE101_LICENSEID }}
|
||||
run: |
|
||||
./gradlew check s101 -Ps101.licenseId="$STRUCTURE101_LICENSEID" --stacktrace
|
||||
deploy-artifacts:
|
||||
name: Deploy Artifacts
|
||||
needs: [ build, test, check-samples, check-tangles ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@v1
|
||||
with:
|
||||
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
secrets: inherit
|
||||
deploy-docs:
|
||||
name: Deploy Docs
|
||||
needs: [ build, test, check-samples, check-tangles ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-docs.yml@v1
|
||||
with:
|
||||
should-deploy-docs: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
secrets: inherit
|
||||
deploy-schema:
|
||||
name: Deploy Schema
|
||||
needs: [ build, test, check-samples, check-tangles ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@v1
|
||||
with:
|
||||
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
secrets: inherit
|
||||
perform-release:
|
||||
name: Perform Release
|
||||
needs: [ deploy-artifacts, deploy-docs, deploy-schema ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@v1
|
||||
with:
|
||||
should-perform-release: ${{ needs.deploy-artifacts.outputs.artifacts-deployed }}
|
||||
project-version: ${{ needs.deploy-artifacts.outputs.project-version }}
|
||||
milestone-repo-url: https://repo.spring.io/artifactory/milestone
|
||||
release-repo-url: https://repo1.maven.org/maven2
|
||||
artifact-path: org/springframework/security/spring-security-core
|
||||
slack-announcing-id: spring-security-announcing
|
||||
secrets: inherit
|
||||
notify_result:
|
||||
name: Check for failures
|
||||
needs: [ perform-release ]
|
||||
if: failure()
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Send Slack message
|
||||
# Workaround while waiting for Gamesight/slack-workflow-status#38 to be fixed
|
||||
# See https://github.com/Gamesight/slack-workflow-status/issues/38
|
||||
uses: sjohnr/slack-workflow-status@v1-beta
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
channel: '#spring-security-ci'
|
||||
name: 'CI Notifier'
|
||||
@@ -1,33 +0,0 @@
|
||||
name: Deploy Docs
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- "gh-pages"
|
||||
- "dependabot/**"
|
||||
tags: '**'
|
||||
repository_dispatch:
|
||||
types: request-build-reference # legacy
|
||||
#schedule:
|
||||
#- cron: '0 10 * * *' # Once per day at 10am UTC
|
||||
workflow_dispatch:
|
||||
permissions: read-all
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: docs-build
|
||||
fetch-depth: 1
|
||||
- name: Dispatch (partial build)
|
||||
if: github.ref_type == 'branch'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }}
|
||||
- name: Dispatch (full build)
|
||||
if: github.ref_type == 'tag'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD)
|
||||
@@ -1,13 +0,0 @@
|
||||
name: Merge Dependabot PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
run-name: Merge Dependabot PR ${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
merge-dependabot-pr:
|
||||
permissions: write-all
|
||||
uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@1e8b0587a1f4f01697f9753fa3339c3e0d30f396
|
||||
with:
|
||||
mergeArguments: '--auto --rebase'
|
||||
@@ -1,40 +0,0 @@
|
||||
name: Check Milestone
|
||||
on:
|
||||
milestone:
|
||||
types: [created, opened, edited]
|
||||
env:
|
||||
DUE_ON: ${{ github.event.milestone.due_on }}
|
||||
TITLE: ${{ github.event.milestone.title }}
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
spring-releasetrain-checks:
|
||||
name: Check DueOn is on a Release Date
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
permissions:
|
||||
contents: none
|
||||
steps:
|
||||
- name: Print Milestone Being Checked
|
||||
run: echo "Validating DueOn '$DUE_ON' for milestone '$TITLE'"
|
||||
- name: Validate DueOn
|
||||
if: env.DUE_ON != ''
|
||||
run: |
|
||||
export TOOL_VERSION=0.1.1
|
||||
wget "https://repo.maven.apache.org/maven2/io/spring/releasetrain/spring-release-train-tools/$TOOL_VERSION/spring-release-train-tools-$TOOL_VERSION.jar"
|
||||
java -cp "spring-release-train-tools-$TOOL_VERSION.jar" io.spring.releasetrain.CheckMilestoneDueOnMain --dueOn "$DUE_ON" --expectedDayOfWeek MONDAY --expectedMondayCount 3
|
||||
notify_result:
|
||||
name: Check for failures
|
||||
needs: [spring-releasetrain-checks]
|
||||
if: failure()
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Send Slack message
|
||||
uses: Gamesight/slack-workflow-status@v1.3.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
channel: '#spring-security-ci'
|
||||
name: 'CI Notifier'
|
||||
@@ -1,26 +0,0 @@
|
||||
name: PR Build
|
||||
|
||||
on: pull_request
|
||||
|
||||
env:
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build --continue --scan
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Release Scheduler
|
||||
on:
|
||||
schedule:
|
||||
- cron: '15 15 * * MON' # Every Monday at 3:15pm UTC
|
||||
workflow_dispatch:
|
||||
permissions: read-all
|
||||
jobs:
|
||||
dispatch_scheduled_releases:
|
||||
name: Dispatch scheduled releases
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
strategy:
|
||||
matrix:
|
||||
# List of active maintenance branches.
|
||||
branch: [ main, 6.0.x, 5.8.x, 5.7.x ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Dispatch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: gh workflow run update-scheduled-release-version.yml -r ${{ matrix.branch }}
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Trigger Dependabot Auto Merge Forward
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*.x'
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
trigger-worflow:
|
||||
name: Trigger Workflow
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.commits[0].author.username == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
- id: trigger
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: gh workflow run dependabot-auto-merge-forward.yml -r main
|
||||
@@ -1,13 +0,0 @@
|
||||
name: Update Scheduled Release Version
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Manual trigger only. Triggered by release-scheduler.yml on main.
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update-scheduled-release-version:
|
||||
name: Update Scheduled Release Version
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@v1
|
||||
secrets: inherit
|
||||
@@ -28,3 +28,5 @@ s101plugin.state
|
||||
|
||||
!.idea/checkstyle-idea.xml
|
||||
!.idea/externalDependencies.xml
|
||||
|
||||
node_modules
|
||||
|
||||
+207
-220
@@ -1,54 +1,236 @@
|
||||
= Release Process
|
||||
|
||||
The release process for Spring Security is partially automated. The following table outlines which steps are automated and which are manual. Follow the links to read about each step.
|
||||
The release process for Spring Security is entirely automated via the https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc[Spring Security Release Plugin] and https://github.com/spring-io/spring-security-release-tools/tree/main/.github/workflows[reusable workflows].
|
||||
The following table outlines the steps that are taken by the automation.
|
||||
|
||||
[cols="1,1"]
|
||||
WARNING: The `5.8.x` branch does not have all of the improvements from the `6.x.x` branches. See "Status (5.8.x)" for which steps are still manual.
|
||||
|
||||
In case of a failure, you can follow the links below to read about each step, which includes instructions for performing the step manually if applicable.
|
||||
See <<frequently-asked-questions,FAQ>> for troubleshooting tips.
|
||||
|
||||
[cols="1,1,1"]
|
||||
|===
|
||||
| Step | Status
|
||||
| Step | Status (5.8.x) | Status (6.0.x+)
|
||||
|
||||
| <<update-dependencies>>
|
||||
| :x: manual
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<check-all-issues-are-closed>>
|
||||
| :white_check_mark: automated (scheduled release will abort if any issues are open)
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<update-release-version>>
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<update-antora-version>>
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<build-locally>>
|
||||
| :x: manual (when updating dependencies)
|
||||
|
||||
| <<push-release-commit>>
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<announce-release-on-slack>>
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<tag-release>>
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<update-to-next-development-version>>
|
||||
| <<push-release-commit>>
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<build-locally>>
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<update-release-notes-on-github>>
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<update-version-on-project-page>>
|
||||
| :x: manual
|
||||
|
||||
| <<update-release-notes-on-github>>
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<close-create-milestone,Close milestone>>
|
||||
| :x: manual (move issues to new milestone before release)
|
||||
| :x: manual
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<announce-release-on-slack>>
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<update-to-next-development-version>>
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<close-create-milestone,Create milestone>>
|
||||
| :white_check_mark: automated (if not already created)
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<announce-release-on-other-channels>>
|
||||
| :x: manual
|
||||
| :x: manual
|
||||
|===
|
||||
|
||||
[#update-dependencies]
|
||||
== Update dependencies
|
||||
|
||||
Dependency versions are managed in the file xref:./gradle/libs.versions.toml[libs.versions.toml] and are automatically updated by xref:./.github/dependabot.yml[dependabot].
|
||||
|
||||
[#check-all-issues-are-closed]
|
||||
== Check all issues are closed
|
||||
|
||||
The first step of a release is to check if there are any open issues remaining in a milestone.
|
||||
|
||||
NOTE: A scheduled release will not proceed if there are any open issues.
|
||||
|
||||
TIP: If you need to prevent a release from occurring automatically, the easiest way to block a release is to add an unresolved issue to the milestone.
|
||||
|
||||
The https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#checkMilestoneHasNoOpenIssues[`checkMilestoneHasOpenIssues`] command will check if there are any open issues for the release.
|
||||
Before running the command manually, replace the following values:
|
||||
|
||||
* `<next-version>` - Replace with the title of the milestone you are releasing now (i.e. 5.5.0-RC1)
|
||||
* `<github-personal-access-token>` - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `public_repo`. This is optional since you are unlikely to reach the rate limit for such a simple check.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew checkMilestoneHasOpenIssues -PnextVersion=<next-version> -PgitHubAccessToken=<github-personal-access-token>
|
||||
----
|
||||
|
||||
Alternatively, you can manually check using the https://github.com/spring-projects/spring-security/milestones[milestones] page.
|
||||
|
||||
[#update-release-version]
|
||||
== Update release version
|
||||
|
||||
If all issues for the release are <<check-all-issues-are-closed,closed>>, the version number is automatically updated using the milestone title.
|
||||
When performing this step manually, update the version number in `gradle.properties` for the release (for example `5.5.0`) and commit the change using the message "Release x.y.z".
|
||||
|
||||
[#tag-release]
|
||||
== Tag release
|
||||
|
||||
The release will automatically be tagged using the milestone title.
|
||||
It is not required to tag manually.
|
||||
However, you can perform this step manually by running the following command:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
git tag 5.5.0
|
||||
----
|
||||
|
||||
[#push-release-commit]
|
||||
== Push release commit
|
||||
|
||||
During a scheduled release, the release commit will automatically be pushed to trigger a build.
|
||||
If performing this step manually, you can push the commit and tag and GitHub actions will build and deploy the artifacts with the following command:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
git push --atomic origin main 5.5.0
|
||||
----
|
||||
|
||||
The build will automatically wait for artifacts to be released to Maven Central.
|
||||
You can get notified manually when uploading is complete by running the following:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./scripts/release/wait-for-done.sh 5.5.0
|
||||
----
|
||||
|
||||
[#build-locally]
|
||||
== Build
|
||||
|
||||
All checks will automatically be performed by the build prior to uploading the artifacts to Maven Central.
|
||||
If something goes wrong, you can run the build locally using:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew check
|
||||
----
|
||||
|
||||
[#update-release-notes-on-github]
|
||||
== Update release notes on GitHub
|
||||
|
||||
Once the release has been uploaded to Maven Central, release notes will automatically be generated and a GitHub release will be created.
|
||||
To do this manually, you can use the https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#generateChangelog[`generateChangelog`] command to generate the release notes by replacing:
|
||||
|
||||
* `<next-version>` - Replace with the milestone you are releasing now (i.e. 5.5.0)
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew generateChangelog -PnextVersion=<next-version>
|
||||
----
|
||||
|
||||
Then copy the release notes to your clipboard (your mileage may vary with the following command):
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
cat build/changelog/release-notes.md | xclip -selection clipboard
|
||||
----
|
||||
|
||||
Finally, create the
|
||||
https://github.com/spring-projects/spring-security/releases[release on
|
||||
GitHub], associate it with the tag, and paste the generated notes.
|
||||
|
||||
Alternatively, you can run the https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#createGitHubRelease[`createGitHubRelease`] command to perform these steps automatically, replacing:
|
||||
|
||||
* `<next-version>` - Replace with the milestone you are releasing now (i.e. 5.5.0)
|
||||
* `<branch>` - The name of the branch to be tagged (if the release commit has not already been tagged)
|
||||
* `<github-personal-access-token>` - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `write:org`
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew createGitHubRelease -PnextVersion=<next-version> -Pbranch=<branch> -PcreateRelease=true -PgitHubAccessToken=<github-personal-access-token>
|
||||
----
|
||||
|
||||
[#update-version-on-project-page]
|
||||
== Update version on project page
|
||||
|
||||
The build will automatically update the project versions on https://spring.io/projects/spring-security#learn.
|
||||
To do this manually, you can use the https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#createSaganRelease[`createSaganRelease`] and https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#deleteSaganRelease[`deleteSaganRelease`] commands using the following parameters:
|
||||
|
||||
* `<next-version>` - Replace with the milestone you are releasing now (i.e. 5.5.0)
|
||||
* `<previous-version>` - Replace with the previous release which will be removed from the listed versions (i.e. 5.5.0-RC1)
|
||||
* `<github-personal-access-token>` - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `read:org` as https://spring.io/restdocs/index.html#authentication[documented for spring.io api]
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew createSaganRelease deleteSaganRelease -PnextVersion=<next-version> -PpreviousVersion=<previous-version> -PgitHubAccessToken=<github-personal-access-token>
|
||||
----
|
||||
|
||||
Alternatively, you can log into Contentful and update the versions manually on the Spring Security project page.
|
||||
|
||||
[#close-create-milestone]
|
||||
== Close / Create milestone
|
||||
|
||||
The release milestone will be automatically closed once the release is complete.
|
||||
To proceed manually, perform the following steps:
|
||||
|
||||
1. Visit https://github.com/spring-projects/spring-security/milestones[GitHub
|
||||
Milestones] and create a new milestone for the next release version
|
||||
2. Move any open issues from the existing milestone you just released to the new milestone
|
||||
3. Close the milestone for the release
|
||||
|
||||
NOTE: Remember that scheduled releases <<check-all-issues-are-closed,will not proceed>> if there are still open issues in the milestone.
|
||||
|
||||
[#announce-release-on-slack]
|
||||
== Announce release on Slack
|
||||
|
||||
The release will automatically be announced on Slack.
|
||||
If proceeding manually, announce the release on Slack in the channel https://pivotal.slack.com/messages/spring-release[#spring-release], including the keyword `+spring-security-announcing+` in the message.
|
||||
Something like:
|
||||
|
||||
....
|
||||
spring-security-announcing `5.5.0` is available now
|
||||
....
|
||||
|
||||
[#update-to-next-development-version]
|
||||
== Update to next development version
|
||||
|
||||
After the release is complete and artifacts have been uploaded to Maven Central, the build will automatically update to the next development version, commit and push.
|
||||
If proceeding manually, update the version in `gradle.properties` to the next `+SNAPSHOT+` version with the commit message "Next development version" and then push.
|
||||
|
||||
[#announce-release-on-other-channels]
|
||||
== Announce release on other channels
|
||||
|
||||
* Create a blog post on Contentful
|
||||
* Tweet from https://twitter.com/springsecurity[@SpringSecurity]
|
||||
|
||||
[[frequently-asked-questions]]
|
||||
== Frequently Asked Questions
|
||||
|
||||
*When should I update dependencies manually?* Dependencies should be updated at the latest the end of the week prior to the release. This is usually the Friday following the 2nd Monday of the month (counting from the first week with a Monday). When in doubt, check the https://github.com/spring-projects/spring-security/milestones[milestones] page for release due dates.
|
||||
|
||||
*When do scheduled releases occur?* Automated releases are scheduled to occur at *3:15 PM UTC* on the *3rd Monday of the month* (counting from the first week with a Monday).
|
||||
@@ -59,9 +241,10 @@ The scheduled release process currently runs every Monday but only releases when
|
||||
The automated release process occurs on the following branches:
|
||||
|
||||
* `main`
|
||||
* `6.0.x`
|
||||
* `6.2.x`
|
||||
* `6.1.x`
|
||||
* `6.0.x` (commercial only)
|
||||
* `5.8.x`
|
||||
* `5.7.x`
|
||||
|
||||
For each of the above branches, the automated process performs the following checks before proceeding with the release:
|
||||
|
||||
@@ -71,7 +254,7 @@ For each of the above branches, the automated process performs the following che
|
||||
[IMPORTANT]
|
||||
You should ensure all issues are closed or moved to another milestone prior to a scheduled release.
|
||||
|
||||
If the above checks pass, the version number is updated (in `gradle.properties` and `antora.yml`) and a commit is pushed to trigger the CI process.
|
||||
If the above checks pass, the version number is updated (in `gradle.properties`) and a commit is pushed to trigger the CI process.
|
||||
|
||||
*How do I trigger a release manually?* You can trigger a release manually in two ways:
|
||||
|
||||
@@ -81,199 +264,3 @@ If the above checks pass, the version number is updated (in `gradle.properties`
|
||||
*When should additional manual steps be performed?* All other automated steps listed above occur during the normal CI process. Additional manual steps can be performed at any time once the builds pass and releases are finished.
|
||||
|
||||
*What if something goes wrong?* If the normal CI process fails, you can retry by re-running the failed jobs with the "Re-run failed jobs" option in GitHub Actions. If changes are required, you should revert the "Release x.y.z" commit, delete the tag, and proceed manually.
|
||||
|
||||
[#update-dependencies]
|
||||
== Update dependencies
|
||||
|
||||
Ensure you have no changes in your local repository.
|
||||
Change to a new branch.
|
||||
For example:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ git checkout -b 5.5.0-RC1-dependencies
|
||||
----
|
||||
|
||||
Review the rules in build.gradle to ensure the rules make sense.
|
||||
For example, we should not allow major version updates in a patch release.
|
||||
Also ensure that all of the exclusions still make sense.
|
||||
|
||||
The following Gradle command will update your dependencies creating a commit for each dependency update.
|
||||
The first invocation of the command will take quite a while (~20 minutes depending on internet speed) to run because it is indexing all the versions of all the dependencies.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./gradlew updateDependencies
|
||||
----
|
||||
|
||||
Review the commits to ensure that the updated dependency versions make sense for this release. For example, we should not perform a major version update for a patch release.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ git log
|
||||
----
|
||||
|
||||
If any of the versions don’t make sense, update `build.gradle` to ensure that the version is excluded.
|
||||
|
||||
Run all the checks:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./gradlew check
|
||||
----
|
||||
|
||||
If they don’t work, you can run a git bisect to discover what broke the build.
|
||||
Fix any commits that broke the build.
|
||||
|
||||
Check out the original brach:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ git checkout -
|
||||
----
|
||||
|
||||
The following command will update the dependencies again but this time creating a ticket for each update and placing `Closes gh-<number>` in the commit. Replacing the following values:
|
||||
|
||||
* <github-personal-access-token> - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `public_repo`
|
||||
* <next-version> - Replace with the title of the milestone you are releasing now (i.e. 5.5.0-RC1)
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./gradlew updateDependencies -PupdateMode=GITHUB_ISSUE -PgitHubAccessToken=<github-personal-access-token> -PnextVersion=<next-version>
|
||||
----
|
||||
|
||||
Apply any fixes from your previous branch that were necessary.
|
||||
|
||||
[#check-all-issues-are-closed]
|
||||
== Check all issues are closed
|
||||
|
||||
The following command will check if there are any open issues for the ticket.
|
||||
Before running the command, replace the following values:
|
||||
|
||||
* <github-personal-access-token> - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `public_repo`. This is optional since you are unlikely to reach the rate limit for such a simple check.
|
||||
* <next-version> - Replace with the title of the milestone you are releasing now (i.e. 5.5.0-RC1)
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./gradlew gitHubCheckMilestoneHasNoOpenIssues -PgitHubAccessToken=<github-personal-access-token> -PnextVersion=<next-version>
|
||||
----
|
||||
|
||||
Alternatively, you can manually check using https://github.com/spring-projects/spring-security/milestones
|
||||
|
||||
[#update-release-version]
|
||||
== Update release version
|
||||
|
||||
Update the version number in `gradle.properties` for the release, for example `5.5.0-M1`, `5.5.0-RC1`, `5.5.0`
|
||||
|
||||
[#update-antora-version]
|
||||
== Update antora version
|
||||
|
||||
You will need to update the antora.yml version.
|
||||
If you are unsure of what the values should be, the following task will instruct you what the expected values are:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew :spring-security-docs:antoraCheckVersion
|
||||
----
|
||||
|
||||
[#build-locally]
|
||||
== Build locally
|
||||
|
||||
Run the build using
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./gradlew check
|
||||
----
|
||||
|
||||
[#push-release-commit]
|
||||
== Push release commit
|
||||
|
||||
Push the commit and GitHub actions will build and deploy the artifacts
|
||||
If you are pushing to Maven Central, then you can get notified when it’s uploaded by running the following:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./scripts/release/wait-for-done.sh 5.5.0
|
||||
----
|
||||
|
||||
[#announce-release-on-slack]
|
||||
== Announce release on Slack
|
||||
|
||||
* Announce via Slack on
|
||||
https://pivotal.slack.com/messages/spring-release[#spring-release],
|
||||
including the keyword `+spring-security-announcing+` in the message.
|
||||
Something like:
|
||||
|
||||
....
|
||||
spring-security-announcing 5.5.0 is available.
|
||||
....
|
||||
|
||||
[#tag-release]
|
||||
== Tag release
|
||||
|
||||
* Tag the release and then push the tag
|
||||
|
||||
....
|
||||
git tag 5.4.0-RC1
|
||||
git push origin 5.4.0-RC1
|
||||
....
|
||||
|
||||
[#update-to-next-development-version]
|
||||
== Update to next development version
|
||||
|
||||
* Update `gradle.properties` version to next `+SNAPSHOT+` version, update antora.yml, and then push
|
||||
|
||||
[#update-version-on-project-page]
|
||||
== Update version on project page
|
||||
|
||||
The following command will update https://spring.io/projects/spring-security#learn with the new release version using the following parameters
|
||||
|
||||
<github-personal-access-token> - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `read:org` as https://spring.io/restdocs/index.html#authentication[documented for spring.io api]
|
||||
<next-version> - Replace with the milestone you are releasing now (i.e. 5.5.0-RC1)
|
||||
<previous-version> - Replace with the previous release which will be removed from the listed versions (i.e. 5.5.0-M3)
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./gradlew saganCreateRelease saganDeleteRelease -PgitHubAccessToken=<github-personal-access-token> -PnextVersion=<next-version> -PpreviousVersion=<previous-version>
|
||||
----
|
||||
|
||||
[#update-release-notes-on-github]
|
||||
== Update release notes on GitHub
|
||||
|
||||
Generate the Release Notes replacing:
|
||||
|
||||
* <next-version> - Replace with the milestone you are releasing now (i.e. 5.5.0-RC1)
|
||||
|
||||
----
|
||||
$ ./gradlew generateChangelog -PnextVersion=<next-version>
|
||||
----
|
||||
|
||||
* Copy the release notes to your clipboard (your mileage may vary with
|
||||
the following command)
|
||||
|
||||
....
|
||||
cat build/changelog/release-notes.md | xclip -selection clipboard
|
||||
....
|
||||
|
||||
* Create the
|
||||
https://github.com/spring-projects/spring-security/releases[release on
|
||||
GitHub], associate it with the tag, and paste the generated notes
|
||||
|
||||
[#close-create-milestone]
|
||||
== Close / Create milestone
|
||||
|
||||
* In
|
||||
https://github.com/spring-projects/spring-security/milestones[GitHub
|
||||
Milestones], create a new milestone for the next release version
|
||||
* Move any open issues from the existing milestone you just released to
|
||||
the new milestone
|
||||
* Close the milestone for the release.
|
||||
|
||||
[#announce-release-on-other-channels]
|
||||
== Announce release on other channels
|
||||
|
||||
* Create a https://spring.io/admin/blog[Blog]
|
||||
* Tweet from [@SpringSecurity](https://twitter.com/springsecurity)
|
||||
|
||||
|
||||
|
||||
+5
-4
@@ -37,7 +37,7 @@ springRelease {
|
||||
weekOfMonth = 3
|
||||
dayOfWeek = 1
|
||||
referenceDocUrl = "https://docs.spring.io/spring-security/reference/{version}/index.html"
|
||||
apiDocUrl = "https://docs.spring.io/spring-security/docs/{version}/api/"
|
||||
apiDocUrl = "https://docs.spring.io/spring-security/site/docs/{version}/api/"
|
||||
replaceSnapshotVersionInReferenceDocUrl = true
|
||||
}
|
||||
|
||||
@@ -86,10 +86,10 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
if (hasProperty('buildScan')) {
|
||||
develocity {
|
||||
buildScan {
|
||||
termsOfServiceUrl = 'https://gradle.com/terms-of-service'
|
||||
termsOfServiceAgree = 'yes'
|
||||
termsOfUseUrl = 'https://gradle.com/help/legal-terms-of-use'
|
||||
termsOfUseAgree = 'yes'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,5 +106,6 @@ tasks.register('cloneRepository', IncludeRepoTask) {
|
||||
}
|
||||
|
||||
s101 {
|
||||
repository = 'https://structure101.com/binaries/latest'
|
||||
configurationDirectory = project.file("etc/s101")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,7 +17,6 @@
|
||||
package s101;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -34,18 +33,11 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarInputStream;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.WebClient;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import com.github.mustachejava.DefaultMustacheFactory;
|
||||
import com.github.mustachejava.Mustache;
|
||||
import com.github.mustachejava.MustacheFactory;
|
||||
@@ -71,6 +63,10 @@ public class S101Configurer {
|
||||
|
||||
private final Path licenseDirectory;
|
||||
|
||||
private final String repository;
|
||||
|
||||
private final String version;
|
||||
|
||||
private final Project project;
|
||||
private final Logger logger;
|
||||
|
||||
@@ -90,6 +86,9 @@ public class S101Configurer {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
this.licenseDirectory = new File(System.getProperty("user.home") + "/.Structure101/java").toPath();
|
||||
S101PluginExtension extension = project.getExtensions().getByType(S101PluginExtension.class);
|
||||
this.repository = extension.getRepository().get();
|
||||
this.version = extension.getVersion().get();
|
||||
}
|
||||
|
||||
public void license(String licenseId) {
|
||||
@@ -129,25 +128,7 @@ public class S101Configurer {
|
||||
|
||||
public void configure(File installationDirectory, File configurationDirectory) {
|
||||
deleteDirectory(configurationDirectory);
|
||||
String version = computeVersionFromInstallation(installationDirectory);
|
||||
configureProject(version, configurationDirectory);
|
||||
}
|
||||
|
||||
private String computeVersionFromInstallation(File installationDirectory) {
|
||||
File buildJar = new File(installationDirectory, "structure101-java-build.jar");
|
||||
try (JarInputStream input = new JarInputStream(new FileInputStream(buildJar))) {
|
||||
JarEntry entry;
|
||||
while ((entry = input.getNextJarEntry()) != null) {
|
||||
if (entry.getName().contains("structure101-build.properties")) {
|
||||
Properties properties = new Properties();
|
||||
properties.load(input);
|
||||
return properties.getProperty("s101-build");
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
throw new IllegalStateException("Unable to determine Structure101 version");
|
||||
configureProject(this.version, configurationDirectory);
|
||||
}
|
||||
|
||||
private boolean deleteDirectory(File directoryToBeDeleted) {
|
||||
@@ -161,24 +142,8 @@ public class S101Configurer {
|
||||
}
|
||||
|
||||
private String installBuildTool(File installationDirectory, File configurationDirectory) {
|
||||
String source = "https://structure101.com/binaries/v6";
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(source);
|
||||
Matcher matcher = null;
|
||||
for (HtmlAnchor anchor : page.getAnchors()) {
|
||||
Matcher candidate = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute());
|
||||
if (candidate.find()) {
|
||||
matcher = candidate;
|
||||
}
|
||||
}
|
||||
if (matcher == null) {
|
||||
return null;
|
||||
}
|
||||
copyZipToFilesystem(source, installationDirectory, matcher.group(1) + matcher.group(2));
|
||||
return matcher.group(2);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
copyZipToFilesystem(this.repository, installationDirectory, "structure101-build-java-all-" + this.version);
|
||||
return this.version;
|
||||
}
|
||||
|
||||
private void copyZipToFilesystem(String source, File destination, String name) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,7 +17,12 @@
|
||||
package s101;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.WebClient;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.Input;
|
||||
@@ -25,6 +30,11 @@ import org.gradle.api.tasks.InputDirectory;
|
||||
|
||||
public class S101PluginExtension {
|
||||
private final Property<String> licenseId;
|
||||
|
||||
private final Property<String> repository;
|
||||
|
||||
private final Property<String> version;
|
||||
|
||||
private final Property<File> installationDirectory;
|
||||
private final Property<File> configurationDirectory;
|
||||
private final Property<String> label;
|
||||
@@ -65,6 +75,24 @@ public class S101PluginExtension {
|
||||
this.label.set(label);
|
||||
}
|
||||
|
||||
@Input
|
||||
public Property<String> getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
public void setRepository(String repository) {
|
||||
this.repository.set(repository);
|
||||
}
|
||||
|
||||
@Input
|
||||
public Property<String> getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version.set(version);
|
||||
}
|
||||
|
||||
public S101PluginExtension(Project project) {
|
||||
this.licenseId = project.getObjects().property(String.class);
|
||||
if (project.hasProperty("s101.licenseId")) {
|
||||
@@ -78,5 +106,31 @@ public class S101PluginExtension {
|
||||
if (project.hasProperty("s101.label")) {
|
||||
setLabel((String) project.findProperty("s101.label"));
|
||||
}
|
||||
this.repository = project.getObjects().property(String.class);
|
||||
if (project.hasProperty("s101.repository")) {
|
||||
setRepository((String) project.findProperty("s101.repository"));
|
||||
} else {
|
||||
setRepository("https://structure101.com/binaries/v6");
|
||||
}
|
||||
this.version = project.getObjects().property(String.class);
|
||||
if (project.hasProperty("s101.version")) {
|
||||
setVersion((String) project.findProperty("s101.version"));
|
||||
} else {
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(getRepository().get());
|
||||
Matcher matcher = null;
|
||||
for (HtmlAnchor anchor : page.getAnchors()) {
|
||||
Matcher candidate = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute());
|
||||
if (candidate.find()) {
|
||||
matcher = candidate;
|
||||
}
|
||||
}
|
||||
if (matcher != null) {
|
||||
setVersion(matcher.group(2));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -31,7 +31,6 @@ import org.springframework.security.authentication.AccountStatusUserDetailsCheck
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.SpringSecurityMessageSource;
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2011-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.cas.authentication;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
/**
|
||||
* In order for the {@link CasAuthenticationProvider} to provide the correct service url
|
||||
* to authenticate the ticket, the returned value of {@link Authentication#getDetails()}
|
||||
* should implement this interface when tickets can be sent to any URL rather than only
|
||||
* {@link ServiceProperties#getService()}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @see org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource
|
||||
*/
|
||||
public interface ServiceAuthenticationDetails extends Serializable {
|
||||
|
||||
/**
|
||||
* Gets the absolute service url (i.e. https://example.com/service/).
|
||||
* @return the service url. Cannot be <code>null</code>.
|
||||
*/
|
||||
String getServiceUrl();
|
||||
|
||||
}
|
||||
+11
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2011-2023 the original author or authors.
|
||||
* Copyright 2011-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,22 +16,24 @@
|
||||
|
||||
package org.springframework.security.cas.web.authentication;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
/**
|
||||
* In order for the {@link CasAuthenticationProvider} to provide the correct service url
|
||||
* to authenticate the ticket, the returned value of {@link Authentication#getDetails()}
|
||||
* should implement this interface when tickets can be sent to any URL rather than only
|
||||
* {@link ServiceProperties#getService()}.
|
||||
* In order for the
|
||||
* {@link org.springframework.security.cas.authentication.CasAuthenticationProvider} to
|
||||
* provide the correct service url to authenticate the ticket, the returned value of
|
||||
* {@link Authentication#getDetails()} should implement this interface when tickets can be
|
||||
* sent to any URL rather than only {@link ServiceProperties#getService()}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @see ServiceAuthenticationDetailsSource
|
||||
* @deprecated Please use
|
||||
* org.springframework.security.cas.authentication.ServiceAuthenticationDetails
|
||||
*/
|
||||
public interface ServiceAuthenticationDetails extends Serializable {
|
||||
@Deprecated
|
||||
public interface ServiceAuthenticationDetails
|
||||
extends org.springframework.security.cas.authentication.ServiceAuthenticationDetails {
|
||||
|
||||
/**
|
||||
* Gets the absolute service url (i.e. https://example.com/service/).
|
||||
|
||||
+35
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,11 +22,14 @@ import java.util.List;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.Aware;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
import org.springframework.core.NativeDetector;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -55,14 +58,13 @@ final class AutowireBeanFactoryObjectPostProcessor
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T postProcess(T object) {
|
||||
if (object == null) {
|
||||
return null;
|
||||
}
|
||||
T result = null;
|
||||
try {
|
||||
result = (T) this.autowireBeanFactory.initializeBean(object, object.toString());
|
||||
result = initializeBeanIfNeeded(object);
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
Class<?> type = object.getClass();
|
||||
@@ -78,6 +80,36 @@ final class AutowireBeanFactoryObjectPostProcessor
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link AutowireCapableBeanFactory#initializeBean(Object, String)} only if
|
||||
* needed, i.e when the application is not a native image or the object is not a CGLIB
|
||||
* proxy.
|
||||
* @param object the object to initialize
|
||||
* @param <T> the type of the object
|
||||
* @return the initialized bean or an existing bean if the object is a CGLIB proxy and
|
||||
* the application is a native image
|
||||
* @see <a href=
|
||||
* "https://github.com/spring-projects/spring-security/issues/14825">Issue
|
||||
* gh-14825</a>
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T initializeBeanIfNeeded(T object) {
|
||||
if (!NativeDetector.inNativeImage() || !AopUtils.isCglibProxy(object)) {
|
||||
return (T) this.autowireBeanFactory.initializeBean(object, object.toString());
|
||||
}
|
||||
ObjectProvider<?> provider = this.autowireBeanFactory.getBeanProvider(object.getClass());
|
||||
Object bean = provider.getIfUnique();
|
||||
if (bean == null) {
|
||||
String msg = """
|
||||
Failed to resolve an unique bean (single or primary) of type [%s] from the BeanFactory.
|
||||
Because the object is a CGLIB Proxy, a raw bean cannot be initialized during runtime in a native image.
|
||||
"""
|
||||
.formatted(object.getClass());
|
||||
throw new IllegalStateException(msg);
|
||||
}
|
||||
return (T) bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
for (SmartInitializingSingleton singleton : this.smartSingletons) {
|
||||
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -335,7 +335,6 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
LogoutFilter result = new LogoutFilter(getLogoutSuccessHandler(), handlers);
|
||||
result.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
result.setLogoutRequestMatcher(getLogoutRequestMatcher(http));
|
||||
result.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
result = postProcess(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
+1
-1
@@ -23,11 +23,11 @@ import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
|
||||
import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver;
|
||||
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.metadata.RequestMatcherMetadataResponseResolver;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -42,7 +42,7 @@ public class JdbcUserServiceBeanDefinitionParser extends AbstractUserDetailsServ
|
||||
@Override
|
||||
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
|
||||
String dataSource = element.getAttribute(ATT_DATA_SOURCE);
|
||||
if (dataSource != null) {
|
||||
if (StringUtils.hasText(dataSource)) {
|
||||
builder.addPropertyReference("dataSource", dataSource);
|
||||
}
|
||||
else {
|
||||
|
||||
+2
-6
@@ -41,8 +41,6 @@ public class PasswordEncoderParser {
|
||||
|
||||
public static final String ATT_HASH = "hash";
|
||||
|
||||
static final String ATT_BASE_64 = "base64";
|
||||
|
||||
static final String OPT_HASH_BCRYPT = "bcrypt";
|
||||
|
||||
private static final Map<String, Class<?>> ENCODER_CLASSES = Collections.singletonMap(OPT_HASH_BCRYPT,
|
||||
@@ -62,19 +60,17 @@ public class PasswordEncoderParser {
|
||||
return;
|
||||
}
|
||||
String hash = element.getAttribute(ATT_HASH);
|
||||
boolean useBase64 = StringUtils.hasText(element.getAttribute(ATT_BASE_64))
|
||||
&& Boolean.parseBoolean(element.getAttribute(ATT_BASE_64));
|
||||
String ref = element.getAttribute(ATT_REF);
|
||||
if (StringUtils.hasText(ref)) {
|
||||
this.passwordEncoder = new RuntimeBeanReference(ref);
|
||||
}
|
||||
else {
|
||||
this.passwordEncoder = createPasswordEncoderBeanDefinition(hash, useBase64);
|
||||
this.passwordEncoder = createPasswordEncoderBeanDefinition(hash);
|
||||
((RootBeanDefinition) this.passwordEncoder).setSource(parserContext.extractSource(element));
|
||||
}
|
||||
}
|
||||
|
||||
public static BeanDefinition createPasswordEncoderBeanDefinition(String hash, boolean useBase64) {
|
||||
public static BeanDefinition createPasswordEncoderBeanDefinition(String hash) {
|
||||
Class<?> beanClass = ENCODER_CLASSES.get(hash);
|
||||
BeanDefinitionBuilder beanBldr = BeanDefinitionBuilder.rootBeanDefinition(beanClass);
|
||||
return beanBldr.getBeanDefinition();
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ public class LdapProviderBeanDefinitionParser implements BeanDefinitionParser {
|
||||
}
|
||||
else if (StringUtils.hasText(hash)) {
|
||||
authenticatorBuilder.addPropertyValue("passwordEncoder",
|
||||
PasswordEncoderParser.createPasswordEncoderBeanDefinition(hash, false));
|
||||
PasswordEncoderParser.createPasswordEncoderBeanDefinition(hash));
|
||||
}
|
||||
}
|
||||
authenticatorBuilder.addConstructorArgValue(contextSource);
|
||||
|
||||
+61
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,9 +16,13 @@
|
||||
|
||||
package org.springframework.security.config.annotation.configuration;
|
||||
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
@@ -31,13 +35,16 @@ import org.springframework.context.EnvironmentAware;
|
||||
import org.springframework.context.MessageSourceAware;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.NativeDetector;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.web.context.ServletContextAware;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatException;
|
||||
import static org.mockito.ArgumentMatchers.isNotNull;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@@ -132,6 +139,59 @@ public class AutowireBeanFactoryObjectPostProcessorTests {
|
||||
assertThat(bean.doStuff()).isEqualTo("null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void postProcessWhenObjectIsCgLibProxyAndInNativeImageThenUseExistingBean() {
|
||||
try (var detector = Mockito.mockStatic(NativeDetector.class)) {
|
||||
given(NativeDetector.inNativeImage()).willReturn(true);
|
||||
|
||||
ProxyFactory proxyFactory = new ProxyFactory(new MyClass());
|
||||
proxyFactory.setProxyTargetClass(!Modifier.isFinal(MyClass.class.getModifiers()));
|
||||
MyClass myClass = (MyClass) proxyFactory.getProxy();
|
||||
|
||||
this.spring.register(Config.class, myClass.getClass()).autowire();
|
||||
this.spring.getContext().getBean(myClass.getClass()).setIdentifier("0000");
|
||||
|
||||
MyClass postProcessed = this.objectObjectPostProcessor.postProcess(myClass);
|
||||
assertThat(postProcessed.getIdentifier()).isEqualTo("0000");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void postProcessWhenObjectIsCgLibProxyAndInNativeImageAndBeanDoesNotExistsThenIllegalStateException() {
|
||||
try (var detector = Mockito.mockStatic(NativeDetector.class)) {
|
||||
given(NativeDetector.inNativeImage()).willReturn(true);
|
||||
|
||||
ProxyFactory proxyFactory = new ProxyFactory(new MyClass());
|
||||
proxyFactory.setProxyTargetClass(!Modifier.isFinal(MyClass.class.getModifiers()));
|
||||
MyClass myClass = (MyClass) proxyFactory.getProxy();
|
||||
|
||||
this.spring.register(Config.class).autowire();
|
||||
|
||||
assertThatException().isThrownBy(() -> this.objectObjectPostProcessor.postProcess(myClass))
|
||||
.havingRootCause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.withMessage(
|
||||
"""
|
||||
Failed to resolve an unique bean (single or primary) of type [class org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessorTests$MyClass$$SpringCGLIB$$0] from the BeanFactory.
|
||||
Because the object is a CGLIB Proxy, a raw bean cannot be initialized during runtime in a native image.
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
static class MyClass {
|
||||
|
||||
private String identifier = "1234";
|
||||
|
||||
String getIdentifier() {
|
||||
return this.identifier;
|
||||
}
|
||||
|
||||
void setIdentifier(String identifier) {
|
||||
this.identifier = identifier;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
|
||||
+37
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,7 +19,10 @@ package org.springframework.security.config.authentication;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.w3c.dom.Element;
|
||||
import org.xml.sax.SAXParseException;
|
||||
|
||||
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
|
||||
import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.CachingUserDetailsService;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
@@ -33,6 +36,7 @@ import org.springframework.security.provisioning.JdbcUserDetailsManager;
|
||||
import org.springframework.security.util.FieldUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@@ -160,6 +164,38 @@ public class JdbcUserServiceBeanDefinitionParserTests {
|
||||
assertThat(AuthorityUtils.authorityListToSet(rod.getAuthorities())).contains("PREFIX_ROLE_SUPERVISOR");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyDataSourceRef() {
|
||||
// @formatter:off
|
||||
String xml = "<authentication-manager>"
|
||||
+ " <authentication-provider>"
|
||||
+ " <jdbc-user-service data-source-ref=''/>"
|
||||
+ " </authentication-provider>"
|
||||
+ "</authentication-manager>";
|
||||
assertThatExceptionOfType(BeanDefinitionParsingException.class)
|
||||
.isThrownBy(() -> setContext(xml))
|
||||
.withFailMessage("Expected exception due to empty data-source-ref")
|
||||
.withMessageContaining("data-source-ref is required for jdbc-user-service");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingDataSourceRef() {
|
||||
// @formatter:off
|
||||
String xml = "<authentication-manager>"
|
||||
+ " <authentication-provider>"
|
||||
+ " <jdbc-user-service/>"
|
||||
+ " </authentication-provider>"
|
||||
+ "</authentication-manager>";
|
||||
assertThatExceptionOfType(XmlBeanDefinitionStoreException.class)
|
||||
.isThrownBy(() -> setContext(xml))
|
||||
.withFailMessage("Expected exception due to missing data-source-ref")
|
||||
.havingRootCause()
|
||||
.isInstanceOf(SAXParseException.class)
|
||||
.withMessageContaining("Attribute 'data-source-ref' must appear on element 'jdbc-user-service'");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private void setContext(String context) {
|
||||
this.appContext = new InMemoryXmlApplicationContext(context);
|
||||
}
|
||||
|
||||
+14
@@ -20,10 +20,13 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
@@ -65,4 +68,15 @@ public class PasswordEncoderParserTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreatePasswordEncoderBeanDefinition() throws Exception {
|
||||
String hash = "bcrypt";
|
||||
Class<?> expectedBeanClass = BCryptPasswordEncoder.class;
|
||||
|
||||
BeanDefinition beanDefinition = PasswordEncoderParser.createPasswordEncoderBeanDefinition(hash);
|
||||
|
||||
Class<?> actualBeanClass = Class.forName(beanDefinition.getBeanClassName());
|
||||
assertThat(actualBeanClass).isEqualTo(expectedBeanClass);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,8 +61,7 @@ public class AuthenticatedVoter implements AccessDecisionVoter<Object> {
|
||||
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
|
||||
|
||||
private boolean isFullyAuthenticated(Authentication authentication) {
|
||||
return (!this.authenticationTrustResolver.isAnonymous(authentication)
|
||||
&& !this.authenticationTrustResolver.isRememberMe(authentication));
|
||||
return this.authenticationTrustResolver.isFullyAuthenticated(authentication);
|
||||
}
|
||||
|
||||
public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
|
||||
|
||||
+2
-4
@@ -21,8 +21,6 @@ import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationConvention;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.security.authorization.method.MethodInvocationResult;
|
||||
|
||||
/**
|
||||
* An {@link ObservationConvention} for translating authorizations into {@link KeyValues}.
|
||||
*
|
||||
@@ -85,10 +83,10 @@ public final class AuthorizationObservationConvention
|
||||
if (context.getObject() instanceof MethodInvocation) {
|
||||
return "method";
|
||||
}
|
||||
if (context.getObject() instanceof MethodInvocationResult) {
|
||||
String className = context.getObject().getClass().getSimpleName();
|
||||
if (className.contains("Method")) {
|
||||
return "method";
|
||||
}
|
||||
String className = context.getObject().getClass().getSimpleName();
|
||||
if (className.contains("Request")) {
|
||||
return "request";
|
||||
}
|
||||
|
||||
+2
-2
@@ -93,7 +93,7 @@ public final class AuthorizationManagerAfterMethodInterceptor
|
||||
PostAuthorizeAuthorizationManager authorizationManager) {
|
||||
AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(
|
||||
AuthorizationMethodPointcuts.forAnnotations(PostAuthorize.class), authorizationManager);
|
||||
interceptor.setOrder(500);
|
||||
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder());
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ public final class AuthorizationManagerAfterMethodInterceptor
|
||||
AuthorizationManager<MethodInvocationResult> authorizationManager) {
|
||||
AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(
|
||||
AuthorizationMethodPointcuts.forAnnotations(PostAuthorize.class), authorizationManager);
|
||||
interceptor.setOrder(500);
|
||||
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder());
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -59,6 +59,7 @@ public class AuthenticatedVoterTests {
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createAnonymous(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createRememberMe(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createFullyAuthenticated(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(null, null, def));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -68,6 +69,7 @@ public class AuthenticatedVoterTests {
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(createAnonymous(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(createRememberMe(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createFullyAuthenticated(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(null, null, def));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -77,6 +79,7 @@ public class AuthenticatedVoterTests {
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(createAnonymous(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createRememberMe(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createFullyAuthenticated(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(null, null, def));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+9
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -129,6 +129,10 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
|
||||
|
||||
private static final String DEFAULT_ID_SUFFIX = "}";
|
||||
|
||||
public static final String NO_PASSWORD_ENCODER_MAPPED = "There is no PasswordEncoder mapped for the id \"%s\"";
|
||||
|
||||
public static final String NO_PASSWORD_ENCODER_PREFIX = "You have entered a password with no PasswordEncoder. If that is your intent, it should be prefixed with `{noop}`.";
|
||||
|
||||
private final String idPrefix;
|
||||
|
||||
private final String idSuffix;
|
||||
@@ -286,7 +290,10 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
|
||||
@Override
|
||||
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
|
||||
String id = extractId(prefixEncodedPassword);
|
||||
throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
|
||||
if (id != null && !id.isEmpty()) {
|
||||
throw new IllegalArgumentException(String.format(NO_PASSWORD_ENCODER_MAPPED, id));
|
||||
}
|
||||
throw new IllegalArgumentException(NO_PASSWORD_ENCODER_PREFIX);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+18
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -43,6 +43,8 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class DelegatingPasswordEncoderTests {
|
||||
|
||||
public static final String NO_PASSWORD_ENCODER = "You have entered a password with no PasswordEncoder. If that is your intent, it should be prefixed with `{noop}`.";
|
||||
|
||||
@Mock
|
||||
private PasswordEncoder bcrypt;
|
||||
|
||||
@@ -201,7 +203,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
public void matchesWhenNoClosingPrefixStringThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{bcrypt" + this.rawPassword))
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -209,7 +211,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
public void matchesWhenNoStartingPrefixStringThenFalse() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "bcrypt}" + this.rawPassword))
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -217,7 +219,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
public void matchesWhenNoIdStringThenFalse() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{}" + this.rawPassword))
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -226,7 +228,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "invalid" + this.bcryptEncodedPassword))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -236,7 +238,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates);
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> passwordEncoder.matches(this.rawPassword, this.rawPassword))
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -289,4 +291,14 @@ public class DelegatingPasswordEncoderTests {
|
||||
verifyNoMoreInteractions(this.bcrypt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesShouldThrowIllegalArgumentExceptionWhenNoPasswordEncoderIsMappedForTheId() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches("rawPassword", "prefixEncodedPassword"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
antora:
|
||||
extensions:
|
||||
- require: '@springio/antora-extensions'
|
||||
root_component_name: 'security'
|
||||
site:
|
||||
title: Spring Security
|
||||
url: https://docs.spring.io/spring-security/reference
|
||||
robots: allow
|
||||
git:
|
||||
ensure_git_suffix: false
|
||||
content:
|
||||
sources:
|
||||
- url: https://github.com/spring-projects/spring-security
|
||||
branches: [main, '5.{{6..9},{1..9}+({0..9})}.x', '6.+({0..9}).x']
|
||||
tags: ['5.{{6..9},{1..9}+({0..9})}.{0..99}?(-RC+({0..9}))', '6.+({0..9}).+({0..9})?(-{RC,M}*)','!(5.6.{0..10}*)', '!(5.7.{0..8}*)', '!(5.8.{0..3}?({-RC,-M}+({0..9})))','!(6.0.{0..3}*)','!(6.1.0*)']
|
||||
start_path: docs
|
||||
asciidoc:
|
||||
attributes:
|
||||
page-stackoverflow-url: https://stackoverflow.com/tags/spring-security
|
||||
page-related-doc-categories: security
|
||||
page-related-doc-projects: framework,graphql
|
||||
hide-uri-scheme: '@'
|
||||
tabs-sync-option: '@'
|
||||
extensions:
|
||||
- '@asciidoctor/tabs'
|
||||
- '@springio/asciidoctor-extensions'
|
||||
urls:
|
||||
latest_version_segment_strategy: redirect:to
|
||||
latest_version_segment: ''
|
||||
redirect_facility: httpd
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.15/ui-bundle.zip
|
||||
snapshot: true
|
||||
runtime:
|
||||
log:
|
||||
failure_level: warn
|
||||
@@ -20,7 +20,7 @@ Java::
|
||||
@Bean
|
||||
SecurityWebFilterChain http(ServerHttpSecurity http) throws Exception {
|
||||
DelegatingServerLogoutHandler logoutHandler = new DelegatingServerLogoutHandler(
|
||||
new WebSessionServerLogoutHandler(), new SecurityContextServerLogoutHandler()
|
||||
new SecurityContextServerLogoutHandler(), new WebSessionServerLogoutHandler()
|
||||
);
|
||||
|
||||
http
|
||||
@@ -38,7 +38,7 @@ Kotlin::
|
||||
@Bean
|
||||
fun http(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
val customLogoutHandler = DelegatingServerLogoutHandler(
|
||||
WebSessionServerLogoutHandler(), SecurityContextServerLogoutHandler()
|
||||
SecurityContextServerLogoutHandler(), WebSessionServerLogoutHandler()
|
||||
)
|
||||
|
||||
return http {
|
||||
|
||||
@@ -105,6 +105,7 @@ Java::
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
|
||||
return new GrantedAuthorityDefaults("MYPREFIX_");
|
||||
}
|
||||
@@ -113,7 +114,8 @@ static GrantedAuthorityDefaults grantedAuthorityDefaults() {
|
||||
|
||||
[TIP]
|
||||
====
|
||||
We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes
|
||||
We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes.
|
||||
Since the `GrantedAuthorityDefaults` bean is part of internal workings of Spring Security, we should also expose it as an infrastructural bean effectively avoiding some warnings related to bean post-processing (see https://github.com/spring-projects/spring-security/issues/14751[gh-14751]).
|
||||
====
|
||||
|
||||
[[jc-reactive-method-security-custom-authorization-manager]]
|
||||
|
||||
@@ -38,7 +38,7 @@ public class MyApplication {
|
||||
|
||||
@Bean
|
||||
ObservationRegistryCustomizer<ObservationRegistry> addTextHandler() {
|
||||
return (registry) -> registry.observationConfig().observationHandler(new ObservationTextHandler());
|
||||
return (registry) -> registry.observationConfig().observationHandler(new ObservationTextPublisher());
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -67,7 +67,7 @@ class MyApplication {
|
||||
@Bean
|
||||
fun addTextHandler(): ObservationRegistryCustomizer<ObservationRegistry> {
|
||||
return registry: ObservationRegistry -> registry.observationConfig()
|
||||
.observationHandler(ObservationTextHandler());
|
||||
.observationHandler(ObservationTextPublisher());
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
@@ -129,7 +129,7 @@ public class MyApplication {
|
||||
@Bean
|
||||
ObservationRegistry<ObservationRegistry> observationRegistry() {
|
||||
ObservationRegistry registry = ObservationRegistry.create();
|
||||
registry.observationConfig().observationHandler(new ObservationTextHandler());
|
||||
registry.observationConfig().observationHandler(new ObservationTextPublisher());
|
||||
return registry;
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ class MyApplication {
|
||||
@Bean
|
||||
fun observationRegistry(): ObservationRegistry<ObservationRegistry> {
|
||||
ObservationRegistry registry = ObservationRegistry.create()
|
||||
registry.observationConfig().observationHandler(ObservationTextHandler())
|
||||
registry.observationConfig().observationHandler(ObservationTextPublisher())
|
||||
return registry
|
||||
}
|
||||
|
||||
|
||||
@@ -449,7 +449,7 @@ In Spring Security this is done by saving the `HttpServletRequest` using a <<req
|
||||
|
||||
The `HttpServletRequest` is saved in the {security-api-url}org/springframework/security/web/savedrequest/RequestCache.html[`RequestCache`].
|
||||
When the user successfully authenticates, the `RequestCache` is used to replay the original request.
|
||||
The <<requestcacheawarefilter,`RequestCacheAwareFilter`>> is what uses the `RequestCache` to save the `HttpServletRequest`.
|
||||
The <<requestcacheawarefilter,`RequestCacheAwareFilter`>> uses the `RequestCache` to get the saved `HttpServletRequest` after the user authenticates, while the `ExceptionTranslationFilter` uses the `RequestCache` to save the `HttpServletRequest` after it detects `AuthenticationException`, before redirecting the user to the login endpoint.
|
||||
|
||||
By default, an `HttpSessionRequestCache` is used.
|
||||
The code below demonstrates how to customize the `RequestCache` implementation that is used to check the `HttpSession` for a saved request if the parameter named `continue` is present.
|
||||
@@ -517,7 +517,7 @@ XML::
|
||||
[[requestcacheawarefilter]]
|
||||
=== RequestCacheAwareFilter
|
||||
|
||||
The {security-api-url}org/springframework/security/web/savedrequest/RequestCacheAwareFilter.html[`RequestCacheAwareFilter`] uses the <<requestcache,`RequestCache`>> to save the `HttpServletRequest`.
|
||||
The {security-api-url}org/springframework/security/web/savedrequest/RequestCacheAwareFilter.html[`RequestCacheAwareFilter`] uses the <<requestcache,`RequestCache`>> to replay the original request.
|
||||
|
||||
[[servlet-logging]]
|
||||
== Logging
|
||||
|
||||
@@ -876,7 +876,7 @@ class SpaCsrfTokenRequestHandler : CsrfTokenRequestAttributeHandler() {
|
||||
delegate.handle(request, response, csrfToken)
|
||||
}
|
||||
|
||||
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String {
|
||||
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
|
||||
/*
|
||||
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
|
||||
* to resolve the CsrfToken. This applies when a single-page application includes
|
||||
@@ -1221,6 +1221,24 @@ public class CsrfTests {
|
||||
.andExpect(header().string(HttpHeaders.LOCATION, "/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginWhenInvalidCsrfTokenThenForbidden() throws Exception {
|
||||
this.mockMvc.perform(post("/login").with(csrf().useInvalidToken())
|
||||
.accept(MediaType.TEXT_HTML)
|
||||
.param("username", "user")
|
||||
.param("password", "password"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginWhenMissingCsrfTokenThenForbidden() throws Exception {
|
||||
this.mockMvc.perform(post("/login")
|
||||
.accept(MediaType.TEXT_HTML)
|
||||
.param("username", "user")
|
||||
.param("password", "password"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void logoutWhenValidCsrfTokenThenSuccess() throws Exception {
|
||||
@@ -1264,6 +1282,24 @@ class CsrfTests {
|
||||
.andExpect(header().string(HttpHeaders.LOCATION, "/"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loginWhenInvalidCsrfTokenThenForbidden() {
|
||||
mockMvc.perform(post("/login").with(csrf().useInvalidToken())
|
||||
.accept(MediaType.TEXT_HTML)
|
||||
.param("username", "user")
|
||||
.param("password", "password"))
|
||||
.andExpect(status().isForbidden)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loginWhenMissingCsrfTokenThenForbidden() {
|
||||
mockMvc.perform(post("/login")
|
||||
.accept(MediaType.TEXT_HTML)
|
||||
.param("username", "user")
|
||||
.param("password", "password"))
|
||||
.andExpect(status().isForbidden)
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
@Throws(Exception::class)
|
||||
|
||||
@@ -131,7 +131,7 @@ public class DefaultSecurityConfig {
|
||||
InMemoryUserDetailsManager inMemoryUserDetailsManager() { <2>
|
||||
String generatedPassword = // ...;
|
||||
return new InMemoryUserDetailsManager(User.withUsername("user")
|
||||
.password(generatedPassword).roles("ROLE_USER").build());
|
||||
.password(generatedPassword).roles("USER").build());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -38,7 +38,7 @@ public class MyApplication {
|
||||
|
||||
@Bean
|
||||
ObservationRegistryCustomizer<ObservationRegistry> addTextHandler() {
|
||||
return (registry) -> registry.observationConfig().observationHandler(new ObservationTextHandler());
|
||||
return (registry) -> registry.observationConfig().observationHandler(new ObservationTextPublisher());
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -67,7 +67,7 @@ class MyApplication {
|
||||
@Bean
|
||||
fun addTextHandler(): ObservationRegistryCustomizer<ObservationRegistry> {
|
||||
return registry: ObservationRegistry -> registry.observationConfig()
|
||||
.observationHandler(ObservationTextHandler());
|
||||
.observationHandler(ObservationTextPublisher());
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
@@ -134,7 +134,7 @@ public class MyApplication {
|
||||
@Bean
|
||||
ObservationRegistry<ObservationRegistry> observationRegistry() {
|
||||
ObservationRegistry registry = ObservationRegistry.create();
|
||||
registry.observationConfig().observationHandler(new ObservationTextHandler());
|
||||
registry.observationConfig().observationHandler(new ObservationTextPublisher());
|
||||
return registry;
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ class MyApplication {
|
||||
@Bean
|
||||
fun observationRegistry(): ObservationRegistry<ObservationRegistry> {
|
||||
ObservationRegistry registry = ObservationRegistry.create()
|
||||
registry.observationConfig().observationHandler(ObservationTextHandler())
|
||||
registry.observationConfig().observationHandler(ObservationTextPublisher())
|
||||
return registry
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.4",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.0-alpha.3",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.10.0",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,11 @@ apply plugin: 'io.spring.convention.docs'
|
||||
apply plugin: 'java'
|
||||
|
||||
antora {
|
||||
playbook = 'cached-antora-playbook.yml'
|
||||
playbookProvider {
|
||||
repository = 'spring-projects/spring-security'
|
||||
branch = 'docs-build'
|
||||
path = 'lib/antora/templates/per-branch-antora-playbook.yml'
|
||||
checkLocalBranch = true
|
||||
}
|
||||
options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
|
||||
environment = [
|
||||
'BUILD_REFNAME': 'HEAD',
|
||||
'BUILD_VERSION': project.version,
|
||||
]
|
||||
}
|
||||
|
||||
tasks.named("generateAntoraYml") {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<property name="relative-to" value="const(THIS_FILE)/../.." />
|
||||
<property name="action-set-mod" value="1" />
|
||||
<property name="detail-mode" value="true" />
|
||||
<property name="hide-deprecated" value="false" />
|
||||
<property name="hide-deprecated" value="true" />
|
||||
<property name="resolve-name-clashes" value="true" />
|
||||
<property name="project-excluded" />
|
||||
<property name="show-needs-to-compile" value="false" />
|
||||
@@ -29,11 +29,8 @@
|
||||
<classpathentry kind="lib" path="oauth2/oauth2-core/build/classes/java/main" module="spring-security-oauth2-core" />
|
||||
<classpathentry kind="lib" path="oauth2/oauth2-jose/build/classes/java/main" module="spring-security-oauth2-jose" />
|
||||
<classpathentry kind="lib" path="oauth2/oauth2-resource-server/build/classes/java/main" module="spring-security-oauth2-resource-server" />
|
||||
<classpathentry kind="lib" path="remoting/build/classes/java/main" module="spring-security-remoting" />
|
||||
<classpathentry kind="lib" path="rsocket/build/classes/java/main" module="spring-security-rsocket" />
|
||||
<classpathentry kind="lib" path="saml2/saml2-service-provider/build/classes/java/main" module="spring-security-saml2-service-provider" />
|
||||
<classpathentry kind="lib" path="saml2/saml2-service-provider/build/classes/java/opensaml3Main" module="spring-security-saml2-service-provider" />
|
||||
<classpathentry kind="lib" path="saml2/saml2-service-provider/build/classes/java/opensaml4Main" module="spring-security-saml2-service-provider" />
|
||||
<classpathentry kind="lib" path="taglibs/build/classes/java/main" module="spring-security-taglibs" />
|
||||
<classpathentry kind="lib" path="test/build/classes/java/main" module="spring-security-test" />
|
||||
<classpathentry kind="lib" path="web/build/classes/java/main" module="spring-security-web" />
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
springBootVersion=3.1.1
|
||||
version=6.1.7-SNAPSHOT
|
||||
version=6.1.10-SNAPSHOT
|
||||
samplesBranch=6.1.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
+11
-11
@@ -6,36 +6,36 @@ io-spring-nohttp = "0.0.11"
|
||||
jakarta-websocket = "2.1.1"
|
||||
org-apache-directory-server = "1.5.5"
|
||||
org-apache-maven-resolver = "1.8.2"
|
||||
org-aspectj = "1.9.21.1"
|
||||
org-aspectj = "1.9.22.1"
|
||||
org-bouncycastle = "1.70"
|
||||
org-eclipse-jetty = "11.0.20"
|
||||
org-eclipse-jetty = "11.0.21"
|
||||
org-jetbrains-kotlin = "1.8.22"
|
||||
org-jetbrains-kotlinx = "1.6.4"
|
||||
org-mockito = "4.8.1"
|
||||
org-opensaml = "4.1.1"
|
||||
org-springframework = "6.0.17"
|
||||
org-springframework = "6.0.20"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.4.14"
|
||||
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.14.3"
|
||||
com-google-inject-guice = "com.google.inject:guice:3.0"
|
||||
com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0"
|
||||
com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:9.24.4"
|
||||
com-nimbusds-oauth2-oidc-sdk = "com.nimbusds:oauth2-oidc-sdk:9.43.3"
|
||||
com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:9.37.3"
|
||||
com-nimbusds-oauth2-oidc-sdk = "com.nimbusds:oauth2-oidc-sdk:9.43.4"
|
||||
com-squareup-okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "com-squareup-okhttp3" }
|
||||
com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "com-squareup-okhttp3" }
|
||||
com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:6.0.11"
|
||||
commons-collections = "commons-collections:commons-collections:3.2.2"
|
||||
io-freefair-gradle-aspectj-plugin = "io.freefair.gradle:aspectj-plugin:6.6.3"
|
||||
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.10.13"
|
||||
io-mockk = "io.mockk:mockk:1.13.9"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2022.0.16"
|
||||
io-mockk = "io.mockk:mockk:1.13.11"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2022.0.19"
|
||||
io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" }
|
||||
io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javaformat:spring-javaformat-checkstyle", version.ref = "io-spring-javaformat" }
|
||||
io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" }
|
||||
io-spring-nohttp-nohttp-checkstyle = { module = "io.spring.nohttp:nohttp-checkstyle", version.ref = "io-spring-nohttp" }
|
||||
io-spring-nohttp-nohttp-gradle = { module = "io.spring.nohttp:nohttp-gradle", version.ref = "io-spring-nohttp" }
|
||||
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.1"
|
||||
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.3"
|
||||
jakarta-annotation-jakarta-annotation-api = "jakarta.annotation:jakarta.annotation-api:2.1.1"
|
||||
jakarta-inject-jakarta-inject-api = "jakarta.inject:jakarta.inject-api:2.0.1"
|
||||
jakarta-persistence-jakarta-persistence-api = "jakarta.persistence:jakarta.persistence-api:3.1.0"
|
||||
@@ -44,7 +44,7 @@ jakarta-servlet-jsp-jakarta-servlet-jsp-api = "jakarta.servlet.jsp:jakarta.servl
|
||||
jakarta-servlet-jsp-jstl-jakarta-servlet-jsp-jstl-api = "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.0"
|
||||
jakarta-websocket-jakarta-websocket-api = { module = "jakarta.websocket:jakarta.websocket-api", version.ref = "jakarta-websocket" }
|
||||
jakarta-websocket-jakarta-websocket-client-api = { module = "jakarta.websocket:jakarta.websocket-client-api", version.ref = "jakarta-websocket" }
|
||||
jakarta-xml-bind-jakarta-xml-bind-api = "jakarta.xml.bind:jakarta.xml.bind-api:4.0.1"
|
||||
jakarta-xml-bind-jakarta-xml-bind-api = "jakarta.xml.bind:jakarta.xml.bind-api:4.0.2"
|
||||
ldapsdk = "ldapsdk:ldapsdk:4.1"
|
||||
net-sourceforge-htmlunit = "net.sourceforge.htmlunit:htmlunit:2.66.0"
|
||||
org-apache-directory-server-apacheds-core = { module = "org.apache.directory.server:apacheds-core", version.ref = "org-apache-directory-server" }
|
||||
@@ -83,7 +83,7 @@ org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:3
|
||||
org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-support:3.141.59"
|
||||
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.1"
|
||||
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
|
||||
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.12"
|
||||
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.13"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2022.0.12"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.0.6"
|
||||
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
|
||||
@@ -97,7 +97,7 @@ org-apache-commons-commons-io = "org.apache.commons:commons-io:1.3.2"
|
||||
io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.1.0"
|
||||
org-gretty-gretty = "org.gretty:gretty:4.0.3"
|
||||
com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versions-plugin:0.38.0"
|
||||
com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.11"
|
||||
com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.13"
|
||||
org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1"
|
||||
org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.29.4"
|
||||
org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1"
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-2
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
+10
-8
@@ -22,6 +22,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -179,16 +180,17 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
}
|
||||
|
||||
private OAuth2AuthenticatedPrincipal convertClaimsSet(Map<String, Object> claims) {
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
Map<String, Object> converted = new LinkedHashMap<>(claims);
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
return Collections.singletonList(v);
|
||||
}
|
||||
return v;
|
||||
});
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString());
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString());
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
// RFC-7662 page 7 directs users to RFC-7519 for defining the values of these
|
||||
// issuer fields.
|
||||
@@ -208,11 +210,11 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
// may be awkward to debug, we do not want to manipulate this value. Previous
|
||||
// versions of Spring Security
|
||||
// would *only* allow valid URLs, which is not what we wish to achieve here.
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
Collection<String> scopes = Arrays.asList(((String) v).split(" "));
|
||||
for (String scope : scopes) {
|
||||
@@ -222,7 +224,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
}
|
||||
return v;
|
||||
});
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(converted, authorities);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
-8
@@ -22,6 +22,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -136,16 +137,17 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
||||
}
|
||||
|
||||
private OAuth2AuthenticatedPrincipal convertClaimsSet(Map<String, Object> claims) {
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
Map<String, Object> converted = new LinkedHashMap<>(claims);
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
return Collections.singletonList(v);
|
||||
}
|
||||
return v;
|
||||
});
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString());
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString());
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
// RFC-7662 page 7 directs users to RFC-7519 for defining the values of these
|
||||
// issuer fields.
|
||||
@@ -165,11 +167,11 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
||||
// may be awkward to debug, we do not want to manipulate this value. Previous
|
||||
// versions of Spring Security
|
||||
// would *only* allow valid URLs, which is not what we wish to achieve here.
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
Collection<String> scopes = Arrays.asList(((String) v).split(" "));
|
||||
for (String scope : scopes) {
|
||||
@@ -179,7 +181,7 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
||||
}
|
||||
return v;
|
||||
});
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(converted, authorities);
|
||||
}
|
||||
|
||||
private OAuth2IntrospectionException onError(Throwable ex) {
|
||||
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -54,6 +54,7 @@ import org.opensaml.saml.saml2.assertion.SubjectConfirmationValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.AudienceRestrictionConditionValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.BearerSubjectConfirmationValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.DelegationRestrictionConditionValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.ProxyRestrictionConditionValidator;
|
||||
import org.opensaml.saml.saml2.core.Assertion;
|
||||
import org.opensaml.saml.saml2.core.Attribute;
|
||||
import org.opensaml.saml.saml2.core.AttributeStatement;
|
||||
@@ -804,6 +805,7 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv
|
||||
return ValidationResult.VALID;
|
||||
}
|
||||
});
|
||||
conditions.add(new ProxyRestrictionConditionValidator());
|
||||
subjects.add(new BearerSubjectConfirmationValidator() {
|
||||
@Override
|
||||
protected ValidationResult validateAddress(SubjectConfirmation confirmation, Assertion assertion,
|
||||
|
||||
+9
-142
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,25 +16,9 @@
|
||||
|
||||
package org.springframework.security.saml2.provider.service.metadata;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An implementation of {@link Saml2MetadataResponseResolver} that identifies which
|
||||
@@ -42,140 +26,23 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.1
|
||||
* @deprecated Please use
|
||||
* {@link org.springframework.security.saml2.provider.service.web.metadata.RequestMatcherMetadataResponseResolver}
|
||||
*/
|
||||
public final class RequestMatcherMetadataResponseResolver implements Saml2MetadataResponseResolver {
|
||||
|
||||
private static final String DEFAULT_METADATA_FILENAME = "saml-{registrationId}-metadata.xml";
|
||||
|
||||
private RequestMatcher matcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher("/saml2/service-provider-metadata/{registrationId}"),
|
||||
new AntPathRequestMatcher("/saml2/metadata/{registrationId}"),
|
||||
new AntPathRequestMatcher("/saml2/metadata"));
|
||||
|
||||
private String filename = DEFAULT_METADATA_FILENAME;
|
||||
|
||||
private final RelyingPartyRegistrationRepository registrations;
|
||||
|
||||
private final Saml2MetadataResolver metadata;
|
||||
@Deprecated
|
||||
public final class RequestMatcherMetadataResponseResolver extends
|
||||
org.springframework.security.saml2.provider.service.web.metadata.RequestMatcherMetadataResponseResolver {
|
||||
|
||||
/**
|
||||
* Construct a {@link RequestMatcherMetadataResponseResolver}
|
||||
* Construct a
|
||||
* {@link org.springframework.security.saml2.provider.service.web.metadata.RequestMatcherMetadataResponseResolver}
|
||||
* @param registrations the source for relying party metadata
|
||||
* @param metadata the strategy for converting {@link RelyingPartyRegistration}s into
|
||||
* metadata
|
||||
*/
|
||||
public RequestMatcherMetadataResponseResolver(RelyingPartyRegistrationRepository registrations,
|
||||
Saml2MetadataResolver metadata) {
|
||||
Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null");
|
||||
Assert.notNull(metadata, "saml2MetadataResolver cannot be null");
|
||||
this.registrations = registrations;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct and serialize a relying party's SAML 2.0 metadata based on the given
|
||||
* {@link HttpServletRequest}. Uses the configured {@link RequestMatcher} to identify
|
||||
* the metadata request, including looking for any indicated {@code registrationId}.
|
||||
*
|
||||
* <p>
|
||||
* If a {@code registrationId} is found in the request, it will attempt to use that,
|
||||
* erroring if no {@link RelyingPartyRegistration} is found.
|
||||
*
|
||||
* <p>
|
||||
* If no {@code registrationId} is found in the request, it will attempt to show all
|
||||
* {@link RelyingPartyRegistration}s in an {@code <md:EntitiesDescriptor>}. To
|
||||
* exercise this functionality, the provided
|
||||
* {@link RelyingPartyRegistrationRepository} needs to implement {@link Iterable}.
|
||||
* @param request the HTTP request
|
||||
* @return a {@link Saml2MetadataResponse} instance
|
||||
* @throws Saml2Exception if the {@link RequestMatcher} specifies a non-existent
|
||||
* {@code registrationId}
|
||||
*/
|
||||
@Override
|
||||
public Saml2MetadataResponse resolve(HttpServletRequest request) {
|
||||
RequestMatcher.MatchResult result = this.matcher.matcher(request);
|
||||
if (!result.isMatch()) {
|
||||
return null;
|
||||
}
|
||||
String registrationId = result.getVariables().get("registrationId");
|
||||
Saml2MetadataResponse response = responseByRegistrationId(request, registrationId);
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
if (this.registrations instanceof Iterable<?>) {
|
||||
Iterable<RelyingPartyRegistration> registrations = (Iterable<RelyingPartyRegistration>) this.registrations;
|
||||
return responseByIterable(request, registrations);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Saml2MetadataResponse responseByRegistrationId(HttpServletRequest request, String registrationId) {
|
||||
if (registrationId == null) {
|
||||
return null;
|
||||
}
|
||||
RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId);
|
||||
if (registration == null) {
|
||||
throw new Saml2Exception("registration not found");
|
||||
}
|
||||
return responseByIterable(request, Collections.singleton(registration));
|
||||
}
|
||||
|
||||
private Saml2MetadataResponse responseByIterable(HttpServletRequest request,
|
||||
Iterable<RelyingPartyRegistration> registrations) {
|
||||
Map<String, RelyingPartyRegistration> results = new LinkedHashMap<>();
|
||||
for (RelyingPartyRegistration registration : registrations) {
|
||||
UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration);
|
||||
String entityId = uriResolver.resolve(registration.getEntityId());
|
||||
results.computeIfAbsent(entityId, (e) -> {
|
||||
String ssoLocation = uriResolver.resolve(registration.getAssertionConsumerServiceLocation());
|
||||
String sloLocation = uriResolver.resolve(registration.getSingleLogoutServiceLocation());
|
||||
String sloResponseLocation = uriResolver.resolve(registration.getSingleLogoutServiceResponseLocation());
|
||||
return registration.mutate()
|
||||
.entityId(entityId)
|
||||
.assertionConsumerServiceLocation(ssoLocation)
|
||||
.singleLogoutServiceLocation(sloLocation)
|
||||
.singleLogoutServiceResponseLocation(sloResponseLocation)
|
||||
.build();
|
||||
});
|
||||
}
|
||||
String metadata = this.metadata.resolve(results.values());
|
||||
String value = (results.size() == 1) ? results.values().iterator().next().getRegistrationId()
|
||||
: UUID.randomUUID().toString();
|
||||
String fileName = this.filename.replace("{registrationId}", value);
|
||||
try {
|
||||
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
|
||||
return new Saml2MetadataResponse(metadata, encodedFileName);
|
||||
}
|
||||
catch (UnsupportedEncodingException ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link RequestMatcher} to identity which requests to generate metadata
|
||||
* for. By default, matches {@code /saml2/metadata},
|
||||
* {@code /saml2/metadata/{registrationId}}, {@code /saml2/service-provider-metadata},
|
||||
* and {@code /saml2/service-provider-metadata/{registrationId}}
|
||||
* @param requestMatcher the {@link RequestMatcher} to use
|
||||
*/
|
||||
public void setRequestMatcher(RequestMatcher requestMatcher) {
|
||||
Assert.notNull(requestMatcher, "requestMatcher cannot be empty");
|
||||
this.matcher = requestMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the metadata filename template. If it contains the {@code {registrationId}}
|
||||
* placeholder, it will be resolved as a random UUID if there are multiple
|
||||
* {@link RelyingPartyRegistration}s. Otherwise, it will be replaced by the
|
||||
* {@link RelyingPartyRegistration}'s id.
|
||||
*
|
||||
* <p>
|
||||
* The default value is {@code saml-{registrationId}-metadata.xml}
|
||||
* @param metadataFilename metadata filename, must contain a {registrationId}
|
||||
*/
|
||||
public void setMetadataFilename(String metadataFilename) {
|
||||
Assert.hasText(metadataFilename, "metadataFilename cannot be empty");
|
||||
this.filename = metadataFilename;
|
||||
super(registrations, metadata);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.metadata;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResolver;
|
||||
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse;
|
||||
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An implementation of {@link Saml2MetadataResponseResolver} that identifies which
|
||||
* {@link RelyingPartyRegistration}s to use with a {@link RequestMatcher}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.1
|
||||
*/
|
||||
public class RequestMatcherMetadataResponseResolver implements Saml2MetadataResponseResolver {
|
||||
|
||||
private static final String DEFAULT_METADATA_FILENAME = "saml-{registrationId}-metadata.xml";
|
||||
|
||||
private RequestMatcher matcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher("/saml2/service-provider-metadata/{registrationId}"),
|
||||
new AntPathRequestMatcher("/saml2/metadata/{registrationId}"),
|
||||
new AntPathRequestMatcher("/saml2/metadata"));
|
||||
|
||||
private String filename = DEFAULT_METADATA_FILENAME;
|
||||
|
||||
private final RelyingPartyRegistrationRepository registrations;
|
||||
|
||||
private final Saml2MetadataResolver metadata;
|
||||
|
||||
/**
|
||||
* Construct a
|
||||
* {@link org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver}
|
||||
* @param registrations the source for relying party metadata
|
||||
* @param metadata the strategy for converting {@link RelyingPartyRegistration}s into
|
||||
* metadata
|
||||
*/
|
||||
public RequestMatcherMetadataResponseResolver(RelyingPartyRegistrationRepository registrations,
|
||||
Saml2MetadataResolver metadata) {
|
||||
Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null");
|
||||
Assert.notNull(metadata, "saml2MetadataResolver cannot be null");
|
||||
this.registrations = registrations;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct and serialize a relying party's SAML 2.0 metadata based on the given
|
||||
* {@link HttpServletRequest}. Uses the configured {@link RequestMatcher} to identify
|
||||
* the metadata request, including looking for any indicated {@code registrationId}.
|
||||
*
|
||||
* <p>
|
||||
* If a {@code registrationId} is found in the request, it will attempt to use that,
|
||||
* erroring if no {@link RelyingPartyRegistration} is found.
|
||||
*
|
||||
* <p>
|
||||
* If no {@code registrationId} is found in the request, it will attempt to show all
|
||||
* {@link RelyingPartyRegistration}s in an {@code <md:EntitiesDescriptor>}. To
|
||||
* exercise this functionality, the provided
|
||||
* {@link RelyingPartyRegistrationRepository} needs to implement {@link Iterable}.
|
||||
* @param request the HTTP request
|
||||
* @return a {@link Saml2MetadataResponse} instance
|
||||
* @throws Saml2Exception if the {@link RequestMatcher} specifies a non-existent
|
||||
* {@code registrationId}
|
||||
*/
|
||||
@Override
|
||||
public Saml2MetadataResponse resolve(HttpServletRequest request) {
|
||||
RequestMatcher.MatchResult result = this.matcher.matcher(request);
|
||||
if (!result.isMatch()) {
|
||||
return null;
|
||||
}
|
||||
String registrationId = result.getVariables().get("registrationId");
|
||||
Saml2MetadataResponse response = responseByRegistrationId(request, registrationId);
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
if (this.registrations instanceof Iterable<?>) {
|
||||
Iterable<RelyingPartyRegistration> registrations = (Iterable<RelyingPartyRegistration>) this.registrations;
|
||||
return responseByIterable(request, registrations);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Saml2MetadataResponse responseByRegistrationId(HttpServletRequest request, String registrationId) {
|
||||
if (registrationId == null) {
|
||||
return null;
|
||||
}
|
||||
RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId);
|
||||
if (registration == null) {
|
||||
throw new Saml2Exception("registration not found");
|
||||
}
|
||||
return responseByIterable(request, Collections.singleton(registration));
|
||||
}
|
||||
|
||||
private Saml2MetadataResponse responseByIterable(HttpServletRequest request,
|
||||
Iterable<RelyingPartyRegistration> registrations) {
|
||||
Map<String, RelyingPartyRegistration> results = new LinkedHashMap<>();
|
||||
for (RelyingPartyRegistration registration : registrations) {
|
||||
RelyingPartyRegistrationPlaceholderResolvers.UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers
|
||||
.uriResolver(request, registration);
|
||||
String entityId = uriResolver.resolve(registration.getEntityId());
|
||||
results.computeIfAbsent(entityId, (e) -> {
|
||||
String ssoLocation = uriResolver.resolve(registration.getAssertionConsumerServiceLocation());
|
||||
String sloLocation = uriResolver.resolve(registration.getSingleLogoutServiceLocation());
|
||||
String sloResponseLocation = uriResolver.resolve(registration.getSingleLogoutServiceResponseLocation());
|
||||
return registration.mutate()
|
||||
.entityId(entityId)
|
||||
.assertionConsumerServiceLocation(ssoLocation)
|
||||
.singleLogoutServiceLocation(sloLocation)
|
||||
.singleLogoutServiceResponseLocation(sloResponseLocation)
|
||||
.build();
|
||||
});
|
||||
}
|
||||
String metadata = this.metadata.resolve(results.values());
|
||||
String value = (results.size() == 1) ? results.values().iterator().next().getRegistrationId()
|
||||
: UUID.randomUUID().toString();
|
||||
String fileName = this.filename.replace("{registrationId}", value);
|
||||
try {
|
||||
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
|
||||
return new Saml2MetadataResponse(metadata, encodedFileName);
|
||||
}
|
||||
catch (UnsupportedEncodingException ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link RequestMatcher} to identity which requests to generate metadata
|
||||
* for. By default, matches {@code /saml2/metadata},
|
||||
* {@code /saml2/metadata/{registrationId}}, {@code /saml2/service-provider-metadata},
|
||||
* and {@code /saml2/service-provider-metadata/{registrationId}}
|
||||
* @param requestMatcher the {@link RequestMatcher} to use
|
||||
*/
|
||||
public void setRequestMatcher(RequestMatcher requestMatcher) {
|
||||
Assert.notNull(requestMatcher, "requestMatcher cannot be empty");
|
||||
this.matcher = requestMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the metadata filename template. If it contains the {@code {registrationId}}
|
||||
* placeholder, it will be resolved as a random UUID if there are multiple
|
||||
* {@link RelyingPartyRegistration}s. Otherwise, it will be replaced by the
|
||||
* {@link RelyingPartyRegistration}'s id.
|
||||
*
|
||||
* <p>
|
||||
* The default value is {@code saml-{registrationId}-metadata.xml}
|
||||
* @param metadataFilename metadata filename, must contain a {registrationId}
|
||||
*/
|
||||
public void setMetadataFilename(String metadataFilename) {
|
||||
Assert.hasText(metadataFilename, "metadataFilename cannot be empty");
|
||||
this.filename = metadataFilename;
|
||||
}
|
||||
|
||||
}
|
||||
+16
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -53,6 +53,7 @@ import org.opensaml.saml.saml2.core.EncryptedAttribute;
|
||||
import org.opensaml.saml.saml2.core.EncryptedID;
|
||||
import org.opensaml.saml.saml2.core.NameID;
|
||||
import org.opensaml.saml.saml2.core.OneTimeUse;
|
||||
import org.opensaml.saml.saml2.core.ProxyRestriction;
|
||||
import org.opensaml.saml.saml2.core.Response;
|
||||
import org.opensaml.saml.saml2.core.StatusCode;
|
||||
import org.opensaml.saml.saml2.core.SubjectConfirmation;
|
||||
@@ -61,6 +62,7 @@ import org.opensaml.saml.saml2.core.impl.AttributeBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.EncryptedAssertionBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.EncryptedIDBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.ProxyRestrictionBuilder;
|
||||
import org.opensaml.xmlsec.encryption.impl.EncryptedDataBuilder;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureConstants;
|
||||
import org.w3c.dom.Element;
|
||||
@@ -741,6 +743,19 @@ public class OpenSaml4AuthenticationProviderTests {
|
||||
.withMessageContaining("did not match any valid issuers");
|
||||
}
|
||||
|
||||
// gh-14931
|
||||
@Test
|
||||
public void authenticateWhenAssertionHasProxyRestrictionThenParses() {
|
||||
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
ProxyRestriction condition = new ProxyRestrictionBuilder().buildObject();
|
||||
assertion.getConditions().getConditions().add(condition);
|
||||
response.getAssertions().add(assertion);
|
||||
Saml2AuthenticationToken token = token(signed(response), verifying(registration()));
|
||||
provider.authenticate(token);
|
||||
}
|
||||
|
||||
private <T extends XMLObject> T build(QName qName) {
|
||||
return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ changelog:
|
||||
labels: ["type: bug"]
|
||||
sort: "title"
|
||||
- title: ":hammer: Dependency Upgrades"
|
||||
emoji: ":hammer:"
|
||||
labels: ["type: dependency-upgrade"]
|
||||
sort: "title"
|
||||
- title: ":nut_and_bolt: Build Updates"
|
||||
labels: ["type: task", "in: build"]
|
||||
sort: "title"
|
||||
issues:
|
||||
exclude:
|
||||
labels: ["status: duplicate"]
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ pluginManagement {
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "com.gradle.enterprise" version "3.12.6"
|
||||
id "io.spring.ge.conventions" version "0.0.15"
|
||||
id "com.gradle.develocity" version "3.17.4"
|
||||
id "io.spring.ge.conventions" version "0.0.17"
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
|
||||
+8
-2
@@ -29,6 +29,7 @@ import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcherEntry;
|
||||
@@ -68,7 +69,7 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho
|
||||
@Override
|
||||
public AuthorizationDecision check(Supplier<Authentication> authentication, HttpServletRequest request) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("Authorizing %s", request));
|
||||
this.logger.trace(LogMessage.format("Authorizing %s", requestLine(request)));
|
||||
}
|
||||
for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) {
|
||||
|
||||
@@ -77,7 +78,8 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho
|
||||
if (matchResult.isMatch()) {
|
||||
AuthorizationManager<RequestAuthorizationContext> manager = mapping.getEntry();
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("Checking authorization on %s using %s", request, manager));
|
||||
this.logger.trace(
|
||||
LogMessage.format("Checking authorization on %s using %s", requestLine(request), manager));
|
||||
}
|
||||
return manager.check(authentication,
|
||||
new RequestAuthorizationContext(request, matchResult.getVariables()));
|
||||
@@ -89,6 +91,10 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho
|
||||
return DENY;
|
||||
}
|
||||
|
||||
private static String requestLine(HttpServletRequest request) {
|
||||
return request.getMethod() + " " + UrlUtils.buildRequestUrl(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for {@link RequestMatcherDelegatingAuthorizationManager}.
|
||||
* @return the new {@link Builder} instance
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -130,9 +130,13 @@ public class StrictHttpFirewall implements HttpFirewall {
|
||||
private static final Predicate<String> ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE = (
|
||||
s) -> ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN.matcher(s).matches();
|
||||
|
||||
private static final Pattern HEADER_VALUE_PATTERN = Pattern.compile("[\\p{IsAssigned}&&[[^\\p{IsControl}]||\\t]]*");
|
||||
|
||||
private static final Predicate<String> HEADER_VALUE_PREDICATE = (s) -> HEADER_VALUE_PATTERN.matcher(s).matches();
|
||||
|
||||
private Predicate<String> allowedHeaderNames = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
|
||||
|
||||
private Predicate<String> allowedHeaderValues = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
|
||||
private Predicate<String> allowedHeaderValues = HEADER_VALUE_PREDICATE;
|
||||
|
||||
private Predicate<String> allowedParameterNames = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -125,7 +125,7 @@ public class AuthenticationWebFilter implements WebFilter {
|
||||
.flatMap(
|
||||
(authentication) -> onAuthenticationSuccess(authentication, new WebFilterExchange(exchange, chain)))
|
||||
.doOnError(AuthenticationException.class,
|
||||
(ex) -> logger.debug(LogMessage.format("Authentication failed: %s", ex.getMessage())));
|
||||
(ex) -> logger.debug(LogMessage.format("Authentication failed: %s", ex.getMessage()), ex));
|
||||
}
|
||||
|
||||
protected Mono<Void> onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) {
|
||||
|
||||
+8
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -781,6 +781,13 @@ public class StrictHttpFirewallTests {
|
||||
assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> request.getHeader("Something"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestGetHeaderWhenHorizontalTabInHeaderValueThenNoException() {
|
||||
this.request.addHeader("Something", "tab\tvalue");
|
||||
HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
|
||||
assertThat(request.getHeader("Something")).isEqualTo("tab\tvalue");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestGetHeaderWhenUndefinedCharacterInHeaderValueThenException() {
|
||||
this.request.addHeader("Something", "bad\uFFFEvalue");
|
||||
|
||||
Reference in New Issue
Block a user