Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2455feb379 | |||
| a972338e1d | |||
| f84c4ea583 | |||
| 2c9dc08e43 | |||
| 5a7f12f1a9 | |||
| c0fe212d02 | |||
| a7105d833d | |||
| 8d6ede27d1 | |||
| 2e53745ddb | |||
| 747b806d37 | |||
| 3b355a0278 | |||
| 6849f4244b | |||
| 82e3061120 | |||
| a6d362fa18 | |||
| fdbfc58d54 | |||
| f905fad988 | |||
| dcfee957b5 | |||
| 37984c2e11 | |||
| e5c167a678 | |||
| d17a533e86 | |||
| b3a43d6154 | |||
| 94749d8942 | |||
| 712595b3a3 | |||
| a87fc4ea8a | |||
| 5495c2eca6 | |||
| 85d7e9ab55 | |||
| e7bff4240a | |||
| 51d96d7417 | |||
| f45be51987 | |||
| 9794952153 | |||
| d1daa7deef | |||
| 4b30acd1e0 | |||
| 4e6b8e4d29 | |||
| e550c48180 | |||
| 6d5f87e631 | |||
| 0ce90da113 | |||
| 794f4dbed9 | |||
| 2202632cad | |||
| a3c06d98ca | |||
| 940efe76fc | |||
| 8fe0303bad | |||
| 8f42c86a57 | |||
| a35a833839 | |||
| a759ede749 | |||
| a4a4c813d4 | |||
| 5dae6da15a | |||
| f3ef54b11b | |||
| 3a9b7a8a29 | |||
| 7db696b176 | |||
| 375326c639 | |||
| 6955c7aaea | |||
| 386e0a7b46 | |||
| 0ab9ad774d | |||
| e1c4177cd8 | |||
| e0a5712474 | |||
| 7f34ef7951 | |||
| c2e97d7661 | |||
| 55e7517414 | |||
| e74ac27b8f | |||
| 7e9f421a77 | |||
| aedffa7919 | |||
| ddd5d2e9cd | |||
| 2471df4d36 | |||
| b20d05724f | |||
| 27cd9fa86c | |||
| 94a66f7a26 | |||
| 9c48546883 | |||
| 7f106f0419 | |||
| 5f80468de3 | |||
| 2f762fefe1 | |||
| bb6045ebea | |||
| 2fdd541ea5 | |||
| 45c37c4454 | |||
| cfb9f1ed32 | |||
| 89246998c4 | |||
| e8c93fdc98 | |||
| 65cce7e305 | |||
| 008296cce2 | |||
| 238bc9733a | |||
| 836b350d2f | |||
| d80fade8b1 | |||
| 0c70f358d5 | |||
| 07454589f4 | |||
| 748c723bba | |||
| 2bd2aa9a75 | |||
| 370bf0ff74 | |||
| 7d38686e09 | |||
| 8b9beb0e1f | |||
| dd5bfc0b31 | |||
| 2cc6cbdb77 |
@@ -1,41 +0,0 @@
|
||||
version: 2
|
||||
|
||||
registries:
|
||||
spring-milestones:
|
||||
type: maven-repository
|
||||
url: https://repo.spring.io/milestone
|
||||
|
||||
updates:
|
||||
|
||||
- package-ecosystem: "gradle"
|
||||
target-branch: "main"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
time: "03:00"
|
||||
timezone: "Etc/UTC"
|
||||
labels: [ "type: dependency-upgrade" ]
|
||||
registries:
|
||||
- "spring-milestones"
|
||||
ignore:
|
||||
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
|
||||
- dependency-name: "org.python:jython" # jython updates break integration tests
|
||||
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
|
||||
- dependency-name: "org.junit:junit-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "org.mockito:mockito-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "*"
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
|
||||
|
||||
# GitHub Actions
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: "main"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: "sjohnr/*"
|
||||
- dependency-name: "spring-io/*"
|
||||
- dependency-name: "spring-security-release-tools/*"
|
||||
+59
-138
@@ -1,151 +1,72 @@
|
||||
version: 2
|
||||
|
||||
registries:
|
||||
spring-milestones:
|
||||
type: maven-repository
|
||||
url: https://repo.spring.io/milestone
|
||||
|
||||
updates:
|
||||
- package-ecosystem: gradle
|
||||
target-branch: 5.8.x
|
||||
directory: /
|
||||
|
||||
- package-ecosystem: "gradle"
|
||||
target-branch: "main"
|
||||
milestone: 319 # 6.2.x
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '03:00'
|
||||
timezone: Etc/UTC
|
||||
labels:
|
||||
- 'type: dependency-upgrade'
|
||||
interval: "daily"
|
||||
time: "03:00"
|
||||
timezone: "Etc/UTC"
|
||||
labels: [ "type: dependency-upgrade" ]
|
||||
registries:
|
||||
- spring-milestones
|
||||
- "spring-milestones"
|
||||
ignore:
|
||||
- dependency-name: com.nimbusds:nimbus-jose-jwt
|
||||
- dependency-name: org.python:jython
|
||||
- dependency-name: org.apache.directory.server:*
|
||||
- dependency-name: org.junit:junit-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: org.mockito:mockito-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: '*'
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
- package-ecosystem: gradle
|
||||
target-branch: 6.1.x
|
||||
directory: /
|
||||
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
|
||||
- dependency-name: "org.python:jython" # jython updates break integration tests
|
||||
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
|
||||
- dependency-name: "org.junit:junit-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "org.mockito:mockito-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "*"
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
|
||||
|
||||
- package-ecosystem: "gradle"
|
||||
target-branch: "6.1.x"
|
||||
milestone: 318 # 6.1.x
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '03:00'
|
||||
timezone: Etc/UTC
|
||||
labels:
|
||||
- 'type: dependency-upgrade'
|
||||
registries:
|
||||
- spring-milestones
|
||||
interval: "daily"
|
||||
time: "03:00"
|
||||
timezone: "Etc/UTC"
|
||||
labels: [ "type: dependency-upgrade" ]
|
||||
ignore:
|
||||
- dependency-name: com.nimbusds:nimbus-jose-jwt
|
||||
- dependency-name: org.python:jython
|
||||
- dependency-name: org.apache.directory.server:*
|
||||
- dependency-name: org.junit:junit-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: org.mockito:mockito-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: '*'
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
- package-ecosystem: gradle
|
||||
target-branch: 6.2.x
|
||||
directory: /
|
||||
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
|
||||
- dependency-name: "org.python:jython" # jython updates break integration tests
|
||||
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
|
||||
- dependency-name: "org.junit:junit-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "org.mockito:mockito-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "*"
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
|
||||
|
||||
- package-ecosystem: "gradle"
|
||||
target-branch: "5.8.x"
|
||||
milestone: 246 # 5.8.x
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '03:00'
|
||||
timezone: Etc/UTC
|
||||
labels:
|
||||
- 'type: dependency-upgrade'
|
||||
registries:
|
||||
- spring-milestones
|
||||
interval: "daily"
|
||||
time: "03:00"
|
||||
timezone: "Etc/UTC"
|
||||
labels: [ "type: dependency-upgrade" ]
|
||||
ignore:
|
||||
- dependency-name: com.nimbusds:nimbus-jose-jwt
|
||||
- dependency-name: org.python:jython
|
||||
- dependency-name: org.apache.directory.server:*
|
||||
- dependency-name: org.junit:junit-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: org.mockito:mockito-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: '*'
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
- package-ecosystem: gradle
|
||||
target-branch: main
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '03:00'
|
||||
timezone: Etc/UTC
|
||||
labels:
|
||||
- 'type: dependency-upgrade'
|
||||
registries:
|
||||
- spring-milestones
|
||||
ignore:
|
||||
- dependency-name: com.nimbusds:nimbus-jose-jwt
|
||||
- dependency-name: org.python:jython
|
||||
- dependency-name: org.apache.directory.server:*
|
||||
- dependency-name: org.junit:junit-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: org.mockito:mockito-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: '*'
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: 5.8.x
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: 6.1.x
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: 6.2.x
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: main
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: docs-build
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
|
||||
- dependency-name: "org.python:jython" # jython updates break integration tests
|
||||
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
|
||||
- dependency-name: "io.mockk:mockk" # mockk updates break tests
|
||||
- dependency-name: "org.opensaml:*" # org.opensaml maintains two different versions, so it must be updated manually
|
||||
- dependency-name: "org.junit:junit-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "org.mockito:mockito-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "*"
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
name: Auto Merge Forward Dependabot Commits
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: dependabot-auto-merge-forward
|
||||
|
||||
jobs:
|
||||
get-supported-branches:
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/retrieve-spring-supported-versions.yml@actions-v1
|
||||
with:
|
||||
project: spring-security
|
||||
type: oss
|
||||
repository_name: spring-projects/spring-security
|
||||
|
||||
auto-merge-forward-dependabot:
|
||||
name: Auto Merge Forward Dependabot Commits
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-supported-branches]
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
- name: Setup GitHub User
|
||||
id: setup-gh-user
|
||||
run: |
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
- name: Run Auto Merge Forward
|
||||
id: run-auto-merge-forward
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/auto-merge-forward@actions-v1
|
||||
with:
|
||||
branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main
|
||||
from-author: dependabot[bot]
|
||||
notify_result:
|
||||
name: Check for failures
|
||||
needs: [ auto-merge-forward-dependabot ]
|
||||
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,13 +1,52 @@
|
||||
name: Merge Dependabot PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
on: pull_request_target
|
||||
|
||||
run-name: Merge Dependabot PR ${{ github.ref_name }}
|
||||
|
||||
permissions: write-all
|
||||
|
||||
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'
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Set Milestone to Dependabot Pull Request
|
||||
id: set-milestone
|
||||
run: |
|
||||
if test -f pom.xml
|
||||
then
|
||||
CURRENT_VERSION=$(mvn help:evaluate -Dexpression="project.version" -q -DforceStdout)
|
||||
else
|
||||
CURRENT_VERSION=$(cat gradle.properties | sed -n '/^version=/ { s/^version=//;p }')
|
||||
fi
|
||||
export CANDIDATE_VERSION=${CURRENT_VERSION/-SNAPSHOT}
|
||||
MILESTONE=$(gh api repos/$GITHUB_REPOSITORY/milestones --jq 'map(select(.due_on != null and (.title | startswith(env.CANDIDATE_VERSION)))) | .[0] | .title')
|
||||
|
||||
if [ -z $MILESTONE ]
|
||||
then
|
||||
gh run cancel ${{ github.run_id }}
|
||||
echo "::warning title=Cannot merge::No scheduled milestone for $CURRENT_VERSION version"
|
||||
else
|
||||
gh pr edit ${{ github.event.pull_request.number }} --milestone $MILESTONE
|
||||
echo mergeEnabled=true >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Merge Dependabot pull request
|
||||
if: steps.set-milestone.outputs.mergeEnabled
|
||||
run: gh pr merge ${{ github.event.pull_request.number }} --auto --rebase
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
# List of active maintenance branches.
|
||||
branch: [ main, 6.2.x, 6.1.x, 5.8.x ]
|
||||
branch: [ main, 6.1.x, 5.8.x ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
name: Update dependabot.yml
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Once per day at midnight UTC
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
get-supported-branches:
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/retrieve-spring-supported-versions.yml@actions-v1
|
||||
with:
|
||||
project: spring-security
|
||||
type: oss
|
||||
repository_name: spring-projects/spring-security
|
||||
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-supported-branches]
|
||||
if: ${{ (github.repository == 'spring-projects/spring-security') && (github.ref == 'refs/heads/main') }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: spring-io/spring-security-release-tools/.github/actions/generate-dependabot-yml@actions-v1
|
||||
name: Update dependabot.yml
|
||||
with:
|
||||
gradle-branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main
|
||||
github-actions-branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main,docs-build
|
||||
gh-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: Update dependabot.yml
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
image::https://badges.gitter.im/Join%20Chat.svg[Gitter,link=https://gitter.im/spring-projects/spring-security?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge]
|
||||
|
||||
image:https://github.com/spring-projects/spring-security/actions/workflows/continuous-integration-workflow.yml/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects/spring-security/actions/workflows/continuous-integration-workflow.yml"]
|
||||
image:https://github.com/spring-projects/spring-security/workflows/CI/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects/spring-security/actions?query=workflow%3ACI"]
|
||||
|
||||
image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?search.rootProjectNames=spring-security"]
|
||||
image:https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Gradle Enterprise", link="https://ge.spring.io/scans?search.rootProjectNames=spring-security"]
|
||||
|
||||
= Spring Security
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ 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();
|
||||
|
||||
}
|
||||
+1
-49
@@ -22,7 +22,6 @@ import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.apereo.cas.client.proxy.ProxyGrantingTicketStorage;
|
||||
import org.apereo.cas.client.util.WebUtils;
|
||||
import org.apereo.cas.client.validation.TicketValidator;
|
||||
@@ -41,20 +40,14 @@ import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Processes a CAS service ticket, obtains proxy granting tickets, and processes proxy
|
||||
@@ -207,10 +200,6 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
|
||||
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
|
||||
.getContextHolderStrategy();
|
||||
|
||||
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
|
||||
|
||||
private RequestCache requestCache = new HttpSessionRequestCache();
|
||||
|
||||
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
|
||||
|
||||
public CasAuthenticationFilter() {
|
||||
@@ -251,22 +240,7 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
|
||||
return null;
|
||||
}
|
||||
String serviceTicket = obtainArtifact(request);
|
||||
if (!StringUtils.hasText(serviceTicket)) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null && session
|
||||
.getAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR) != null) {
|
||||
this.logger.debug("Failed authentication response from CAS gateway request");
|
||||
session.removeAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR);
|
||||
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
|
||||
if (savedRequest != null) {
|
||||
String redirectUrl = savedRequest.getRedirectUrl();
|
||||
this.logger.debug(LogMessage.format("Redirecting to: %s", redirectUrl));
|
||||
this.requestCache.removeRequest(request, response);
|
||||
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceTicket == null) {
|
||||
this.logger.debug("Failed to obtain an artifact (cas ticket)");
|
||||
serviceTicket = "";
|
||||
}
|
||||
@@ -344,28 +318,6 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
|
||||
this.securityContextHolderStrategy = securityContextHolderStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link RedirectStrategy} used to redirect to the saved request if there is
|
||||
* one saved. Defaults to {@link DefaultRedirectStrategy}.
|
||||
* @param redirectStrategy the redirect strategy to use
|
||||
* @since 6.3
|
||||
*/
|
||||
public final void setRedirectStrategy(RedirectStrategy redirectStrategy) {
|
||||
Assert.notNull(redirectStrategy, "redirectStrategy cannot be null");
|
||||
this.redirectStrategy = redirectStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link RequestCache} used to retrieve the saved request in failed gateway
|
||||
* authentication scenarios.
|
||||
* @param requestCache the request cache to use
|
||||
* @since 6.3
|
||||
*/
|
||||
public final void setRequestCache(RequestCache requestCache) {
|
||||
Assert.notNull(requestCache, "requestCache cannot be null");
|
||||
this.requestCache = requestCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the request is elgible to process a service ticket. This method exists
|
||||
* for readability.
|
||||
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.web;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.apereo.cas.client.util.CommonUtils;
|
||||
import org.apereo.cas.client.util.WebUtils;
|
||||
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.GenericFilterBean;
|
||||
|
||||
/**
|
||||
* Redirects the request to the CAS server appending {@code gateway=true} to the URL. Upon
|
||||
* redirection, the {@link ServiceProperties#isSendRenew()} is ignored and considered as
|
||||
* {@code false} to align with the specification says that the {@code sendRenew} parameter
|
||||
* is not compatible with the {@code gateway} parameter. See the <a href=
|
||||
* "https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol-V2-Specification.html#:~:text=This%20parameter%20is%20not%20compatible%20with%20the%20%E2%80%9Crenew%E2%80%9D%20parameter.%20Behavior%20is%20undefined%20if%20both%20are%20set.">CAS
|
||||
* Protocol Specification</a> for more details. To allow other filters to know if the
|
||||
* request is a gateway request, this filter creates a session and add an attribute with
|
||||
* name {@link #CAS_GATEWAY_AUTHENTICATION_ATTR} which can be checked by other filters if
|
||||
* needed. It is recommended that this filter is placed after
|
||||
* {@link CasAuthenticationFilter} if it is defined.
|
||||
*
|
||||
* @author Michael Remond
|
||||
* @author Jerome LELEU
|
||||
* @author Marcus da Coregio
|
||||
* @since 6.3
|
||||
*/
|
||||
public final class CasGatewayAuthenticationRedirectFilter extends GenericFilterBean {
|
||||
|
||||
public static final String CAS_GATEWAY_AUTHENTICATION_ATTR = "CAS_GATEWAY_AUTHENTICATION";
|
||||
|
||||
private final String casLoginUrl;
|
||||
|
||||
private final ServiceProperties serviceProperties;
|
||||
|
||||
private RequestMatcher requestMatcher;
|
||||
|
||||
private RequestCache requestCache = new HttpSessionRequestCache();
|
||||
|
||||
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
|
||||
|
||||
/**
|
||||
* Constructs a new instance of this class
|
||||
* @param serviceProperties the {@link ServiceProperties}
|
||||
*/
|
||||
public CasGatewayAuthenticationRedirectFilter(String casLoginUrl, ServiceProperties serviceProperties) {
|
||||
Assert.hasText(casLoginUrl, "casLoginUrl cannot be null or empty");
|
||||
Assert.notNull(serviceProperties, "serviceProperties cannot be null");
|
||||
this.casLoginUrl = casLoginUrl;
|
||||
this.serviceProperties = serviceProperties;
|
||||
this.requestMatcher = new CasGatewayResolverRequestMatcher(this.serviceProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
|
||||
HttpServletRequest request = (HttpServletRequest) req;
|
||||
HttpServletResponse response = (HttpServletResponse) res;
|
||||
|
||||
if (!this.requestMatcher.matches(request)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
this.requestCache.saveRequest(request, response);
|
||||
HttpSession session = request.getSession(true);
|
||||
session.setAttribute(CAS_GATEWAY_AUTHENTICATION_ATTR, true);
|
||||
String urlEncodedService = WebUtils.constructServiceUrl(request, response, this.serviceProperties.getService(),
|
||||
null, this.serviceProperties.getServiceParameter(), this.serviceProperties.getArtifactParameter(),
|
||||
true);
|
||||
String redirectUrl = CommonUtils.constructRedirectUrl(this.casLoginUrl,
|
||||
this.serviceProperties.getServiceParameter(), urlEncodedService, false, true);
|
||||
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RequestMatcher} used to trigger this filter. Defaults to
|
||||
* {@link CasGatewayResolverRequestMatcher}.
|
||||
* @param requestMatcher the {@link RequestMatcher} to use
|
||||
*/
|
||||
public void setRequestMatcher(RequestMatcher requestMatcher) {
|
||||
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
|
||||
this.requestMatcher = requestMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RequestCache} used to store the current request to be replayed
|
||||
* after redirect from the CAS server. Defaults to {@link HttpSessionRequestCache}.
|
||||
* @param requestCache the {@link RequestCache} to use
|
||||
*/
|
||||
public void setRequestCache(RequestCache requestCache) {
|
||||
Assert.notNull(requestCache, "requestCache cannot be null");
|
||||
this.requestCache = requestCache;
|
||||
}
|
||||
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.web;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apereo.cas.client.authentication.DefaultGatewayResolverImpl;
|
||||
import org.apereo.cas.client.authentication.GatewayResolver;
|
||||
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link RequestMatcher} implementation that delegates the check to an instance of
|
||||
* {@link GatewayResolver}. The request is marked as "gatewayed" using the configured
|
||||
* {@link GatewayResolver} to avoid infinite loop.
|
||||
*
|
||||
* @author Michael Remond
|
||||
* @author Marcus da Coregio
|
||||
* @since 6.3
|
||||
*/
|
||||
public final class CasGatewayResolverRequestMatcher implements RequestMatcher {
|
||||
|
||||
private final ServiceProperties serviceProperties;
|
||||
|
||||
private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl();
|
||||
|
||||
public CasGatewayResolverRequestMatcher(ServiceProperties serviceProperties) {
|
||||
Assert.notNull(serviceProperties, "serviceProperties cannot be null");
|
||||
this.serviceProperties = serviceProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, this.serviceProperties.getService());
|
||||
if (!wasGatewayed) {
|
||||
this.gatewayStorage.storeGatewayInformation(request, this.serviceProperties.getService());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link GatewayResolver} to check if the request was already gatewayed.
|
||||
* Defaults to {@link DefaultGatewayResolverImpl}
|
||||
* @param gatewayStorage the {@link GatewayResolver} to use. Cannot be null.
|
||||
*/
|
||||
public void setGatewayStorage(GatewayResolver gatewayStorage) {
|
||||
Assert.notNull(gatewayStorage, "gatewayStorage cannot be null");
|
||||
this.gatewayStorage = gatewayStorage;
|
||||
}
|
||||
|
||||
}
|
||||
+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/).
|
||||
|
||||
-21
@@ -20,7 +20,6 @@ import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.apereo.cas.client.proxy.ProxyGrantingTicketStorage;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -42,7 +41,6 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -226,25 +224,6 @@ public class CasAuthenticationFilterTests {
|
||||
verify(securityContextRepository).saveContext(any(SecurityContext.class), eq(request), eq(response));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attemptAuthenticationWhenNoServiceTicketAndIsGatewayRequestThenRedirectToSavedRequestAndClearAttribute()
|
||||
throws Exception {
|
||||
CasAuthenticationFilter filter = new CasAuthenticationFilter();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
HttpSession session = request.getSession(true);
|
||||
session.setAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR, true);
|
||||
|
||||
new HttpSessionRequestCache().saveRequest(request, response);
|
||||
|
||||
Authentication authn = filter.attemptAuthentication(request, response);
|
||||
assertThat(authn).isNull();
|
||||
assertThat(response.getStatus()).isEqualTo(302);
|
||||
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost?continue");
|
||||
assertThat(session.getAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfulAuthenticationWhenSecurityContextRepositorySetThenUses() throws ServletException, IOException {
|
||||
SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class);
|
||||
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.web;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link CasGatewayAuthenticationRedirectFilter}.
|
||||
*
|
||||
* @author Jerome LELEU
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
public class CasGatewayAuthenticationRedirectFilterTests {
|
||||
|
||||
private static final String CAS_LOGIN_URL = "http://mycasserver/login";
|
||||
|
||||
CasGatewayAuthenticationRedirectFilter filter = new CasGatewayAuthenticationRedirectFilter(CAS_LOGIN_URL,
|
||||
serviceProperties());
|
||||
|
||||
@Test
|
||||
void doFilterWhenMatchesThenSavesRequestAndSavesAttributeAndSendRedirect() throws IOException, ServletException {
|
||||
RequestCache requestCache = mock();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.filter.setRequestMatcher((req) -> true);
|
||||
this.filter.setRequestCache(requestCache);
|
||||
this.filter.doFilter(request, response, new MockFilterChain());
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
|
||||
assertThat(response.getHeader("Location"))
|
||||
.isEqualTo("http://mycasserver/login?service=http%3A%2F%2Flocalhost%2Flogin%2Fcas&gateway=true");
|
||||
verify(requestCache).saveRequest(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenNotMatchThenContinueFilter() throws ServletException, IOException {
|
||||
this.filter.setRequestMatcher((req) -> false);
|
||||
FilterChain chain = mock();
|
||||
MockHttpServletResponse response = mock();
|
||||
this.filter.doFilter(new MockHttpServletRequest(), response, chain);
|
||||
verify(chain).doFilter(any(), any());
|
||||
verifyNoInteractions(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenSendRenewTrueThenIgnores() throws ServletException, IOException {
|
||||
ServiceProperties serviceProperties = serviceProperties();
|
||||
serviceProperties.setSendRenew(true);
|
||||
this.filter = new CasGatewayAuthenticationRedirectFilter(CAS_LOGIN_URL, serviceProperties);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.filter.setRequestMatcher((req) -> true);
|
||||
this.filter.doFilter(request, response, new MockFilterChain());
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
|
||||
assertThat(response.getHeader("Location"))
|
||||
.isEqualTo("http://mycasserver/login?service=http%3A%2F%2Flocalhost%2Flogin%2Fcas&gateway=true");
|
||||
}
|
||||
|
||||
private static ServiceProperties serviceProperties() {
|
||||
ServiceProperties serviceProperties = new ServiceProperties();
|
||||
serviceProperties.setService("http://localhost/login/cas");
|
||||
return serviceProperties;
|
||||
}
|
||||
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.web;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests {@link CasGatewayResolverRequestMatcher}.
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
class CasGatewayResolverRequestMatcherTests {
|
||||
|
||||
CasGatewayResolverRequestMatcher matcher = new CasGatewayResolverRequestMatcher(new ServiceProperties());
|
||||
|
||||
@Test
|
||||
void constructorWhenServicePropertiesNullThenException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new CasGatewayResolverRequestMatcher(null))
|
||||
.withMessage("serviceProperties cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenAlreadyGatewayedThenReturnsFalse() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.getSession().setAttribute("_const_cas_gateway_", "yes");
|
||||
boolean matches = this.matcher.matches(request);
|
||||
assertThat(matches).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenNotGatewayedThenReturnsTrue() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
boolean matches = this.matcher.matches(request);
|
||||
assertThat(matches).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenNoSessionThenReturnsTrue() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setSession(null);
|
||||
boolean matches = this.matcher.matches(request);
|
||||
assertThat(matches).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenNotGatewayedAndCheckedAgainThenSavesAsGatewayedAndReturnsFalse() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
boolean matches = this.matcher.matches(request);
|
||||
boolean secondMatch = this.matcher.matches(request);
|
||||
assertThat(matches).isTrue();
|
||||
assertThat(secondMatch).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -96,7 +96,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler {
|
||||
pc.getReaderContext()
|
||||
.fatal("You cannot use a spring-security-2.0.xsd or spring-security-3.0.xsd or "
|
||||
+ "spring-security-3.1.xsd schema or spring-security-3.2.xsd schema or spring-security-4.0.xsd schema "
|
||||
+ "with Spring Security 6.3. Please update your schema declarations to the 6.3 schema.",
|
||||
+ "with Spring Security 6.2. Please update your schema declarations to the 6.2 schema.",
|
||||
element);
|
||||
}
|
||||
String name = pc.getDelegate().getLocalName(element);
|
||||
@@ -221,7 +221,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler {
|
||||
|
||||
private boolean matchesVersionInternal(Element element) {
|
||||
String schemaLocation = element.getAttributeNS("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation");
|
||||
return schemaLocation.matches("(?m).*spring-security-6\\.3.*.xsd.*")
|
||||
return schemaLocation.matches("(?m).*spring-security-6\\.2.*.xsd.*")
|
||||
|| schemaLocation.matches("(?m).*spring-security.xsd.*")
|
||||
|| !schemaLocation.matches("(?m).*spring-security.*");
|
||||
}
|
||||
|
||||
+1
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -87,14 +87,4 @@ public @interface EnableMethodSecurity {
|
||||
*/
|
||||
AdviceMode mode() default AdviceMode.PROXY;
|
||||
|
||||
/**
|
||||
* Indicate additional offset in the ordering of the execution of the security
|
||||
* interceptors when multiple advices are applied at a specific joinpoint. I.e.,
|
||||
* precedence of each security interceptor enabled by this annotation will be
|
||||
* calculated as sum of its default precedence and offset. The default is 0.
|
||||
* @return the offset in the order the security advisor should be applied
|
||||
* @since 6.3
|
||||
*/
|
||||
int offset() default 0;
|
||||
|
||||
}
|
||||
|
||||
+3
-25
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -24,13 +24,7 @@ import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportAware;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.authorization.AuthoritiesAuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.Jsr250AuthorizationManager;
|
||||
@@ -48,23 +42,15 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
final class Jsr250MethodSecurityConfiguration implements ImportAware {
|
||||
|
||||
private int interceptorOrderOffset;
|
||||
final class Jsr250MethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor jsr250AuthorizationMethodInterceptor(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
|
||||
Jsr250MethodSecurityConfiguration configuration) {
|
||||
ObjectProvider<ObservationRegistry> registryProvider) {
|
||||
Jsr250AuthorizationManager jsr250 = new Jsr250AuthorizationManager();
|
||||
AuthoritiesAuthorizationManager authoritiesAuthorizationManager = new AuthoritiesAuthorizationManager();
|
||||
RoleHierarchy roleHierarchy = roleHierarchyProvider.getIfAvailable(NullRoleHierarchy::new);
|
||||
authoritiesAuthorizationManager.setRoleHierarchy(roleHierarchy);
|
||||
jsr250.setAuthoritiesAuthorizationManager(authoritiesAuthorizationManager);
|
||||
defaultsProvider.ifAvailable((d) -> jsr250.setRolePrefix(d.getRolePrefix()));
|
||||
SecurityContextHolderStrategy strategy = strategyProvider
|
||||
.getIfAvailable(SecurityContextHolder::getContextHolderStrategy);
|
||||
@@ -72,16 +58,8 @@ final class Jsr250MethodSecurityConfiguration implements ImportAware {
|
||||
registryProvider, jsr250);
|
||||
AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor
|
||||
.jsr250(manager);
|
||||
interceptor.setOrder(interceptor.getOrder() + configuration.interceptorOrderOffset);
|
||||
interceptor.setSecurityContextHolderStrategy(strategy);
|
||||
eventPublisherProvider.ifAvailable(interceptor::setAuthorizationEventPublisher);
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImportMetadata(AnnotationMetadata importMetadata) {
|
||||
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
|
||||
this.interceptorOrderOffset = annotation.offset();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
-43
@@ -27,16 +27,12 @@ import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportAware;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
|
||||
@@ -60,23 +56,18 @@ import org.springframework.util.function.SingletonSupplier;
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
final class PrePostMethodSecurityConfiguration implements ImportAware {
|
||||
|
||||
private int interceptorOrderOffset;
|
||||
final class PrePostMethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor preFilterAuthorizationMethodInterceptor(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<RoleHierarchy> roleHierarchyProvider, PrePostMethodSecurityConfiguration configuration,
|
||||
ApplicationContext context) {
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider, ApplicationContext context) {
|
||||
PreFilterAuthorizationMethodInterceptor preFilter = new PreFilterAuthorizationMethodInterceptor();
|
||||
preFilter.setOrder(preFilter.getOrder() + configuration.interceptorOrderOffset);
|
||||
strategyProvider.ifAvailable(preFilter::setSecurityContextHolderStrategy);
|
||||
preFilter.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
|
||||
defaultsProvider, roleHierarchyProvider, context));
|
||||
preFilter.setExpressionHandler(
|
||||
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
|
||||
return preFilter;
|
||||
}
|
||||
|
||||
@@ -87,14 +78,12 @@ final class PrePostMethodSecurityConfiguration implements ImportAware {
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
|
||||
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
|
||||
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
|
||||
manager.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
|
||||
defaultsProvider, roleHierarchyProvider, context));
|
||||
manager.setExpressionHandler(
|
||||
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
|
||||
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
|
||||
.preAuthorize(manager(manager, registryProvider));
|
||||
preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset);
|
||||
strategyProvider.ifAvailable(preAuthorize::setSecurityContextHolderStrategy);
|
||||
eventPublisherProvider.ifAvailable(preAuthorize::setAuthorizationEventPublisher);
|
||||
return preAuthorize;
|
||||
@@ -107,14 +96,12 @@ final class PrePostMethodSecurityConfiguration implements ImportAware {
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
|
||||
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
|
||||
PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
|
||||
manager.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
|
||||
defaultsProvider, roleHierarchyProvider, context));
|
||||
manager.setExpressionHandler(
|
||||
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
|
||||
AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor
|
||||
.postAuthorize(manager(manager, registryProvider));
|
||||
postAuthorize.setOrder(postAuthorize.getOrder() + configuration.interceptorOrderOffset);
|
||||
strategyProvider.ifAvailable(postAuthorize::setSecurityContextHolderStrategy);
|
||||
eventPublisherProvider.ifAvailable(postAuthorize::setAuthorizationEventPublisher);
|
||||
return postAuthorize;
|
||||
@@ -125,23 +112,17 @@ final class PrePostMethodSecurityConfiguration implements ImportAware {
|
||||
static MethodInterceptor postFilterAuthorizationMethodInterceptor(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<RoleHierarchy> roleHierarchyProvider, PrePostMethodSecurityConfiguration configuration,
|
||||
ApplicationContext context) {
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider, ApplicationContext context) {
|
||||
PostFilterAuthorizationMethodInterceptor postFilter = new PostFilterAuthorizationMethodInterceptor();
|
||||
postFilter.setOrder(postFilter.getOrder() + configuration.interceptorOrderOffset);
|
||||
strategyProvider.ifAvailable(postFilter::setSecurityContextHolderStrategy);
|
||||
postFilter.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
|
||||
defaultsProvider, roleHierarchyProvider, context));
|
||||
postFilter.setExpressionHandler(
|
||||
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
|
||||
return postFilter;
|
||||
}
|
||||
|
||||
private static MethodSecurityExpressionHandler defaultExpressionHandler(
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<RoleHierarchy> roleHierarchyProvider, ApplicationContext context) {
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider, ApplicationContext context) {
|
||||
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
|
||||
RoleHierarchy roleHierarchy = roleHierarchyProvider.getIfAvailable(NullRoleHierarchy::new);
|
||||
handler.setRoleHierarchy(roleHierarchy);
|
||||
defaultsProvider.ifAvailable((d) -> handler.setDefaultRolePrefix(d.getRolePrefix()));
|
||||
handler.setApplicationContext(context);
|
||||
return handler;
|
||||
@@ -152,22 +133,15 @@ final class PrePostMethodSecurityConfiguration implements ImportAware {
|
||||
return new DeferringObservationAuthorizationManager<>(registryProvider, delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImportMetadata(AnnotationMetadata importMetadata) {
|
||||
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
|
||||
this.interceptorOrderOffset = annotation.offset();
|
||||
}
|
||||
|
||||
private static final class DeferringMethodSecurityExpressionHandler implements MethodSecurityExpressionHandler {
|
||||
|
||||
private final Supplier<MethodSecurityExpressionHandler> expressionHandler;
|
||||
|
||||
private DeferringMethodSecurityExpressionHandler(
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
|
||||
ObjectProvider<RoleHierarchy> roleHierarchyProvider, ApplicationContext applicationContext) {
|
||||
this.expressionHandler = SingletonSupplier.of(() -> expressionHandlerProvider.getIfAvailable(
|
||||
() -> defaultExpressionHandler(defaultsProvider, roleHierarchyProvider, applicationContext)));
|
||||
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider, ApplicationContext applicationContext) {
|
||||
this.expressionHandler = SingletonSupplier.of(() -> expressionHandlerProvider
|
||||
.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, applicationContext)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+3
-25
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -24,14 +24,8 @@ import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportAware;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.authorization.AuthoritiesAuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.SecuredAuthorizationManager;
|
||||
@@ -48,38 +42,22 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
final class SecuredMethodSecurityConfiguration implements ImportAware {
|
||||
|
||||
private int interceptorOrderOffset;
|
||||
final class SecuredMethodSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodInterceptor securedAuthorizationMethodInterceptor(
|
||||
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
|
||||
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
|
||||
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
|
||||
SecuredMethodSecurityConfiguration configuration) {
|
||||
ObjectProvider<ObservationRegistry> registryProvider) {
|
||||
SecuredAuthorizationManager secured = new SecuredAuthorizationManager();
|
||||
AuthoritiesAuthorizationManager authoritiesAuthorizationManager = new AuthoritiesAuthorizationManager();
|
||||
RoleHierarchy roleHierarchy = roleHierarchyProvider.getIfAvailable(NullRoleHierarchy::new);
|
||||
authoritiesAuthorizationManager.setRoleHierarchy(roleHierarchy);
|
||||
secured.setAuthoritiesAuthorizationManager(authoritiesAuthorizationManager);
|
||||
SecurityContextHolderStrategy strategy = strategyProvider
|
||||
.getIfAvailable(SecurityContextHolder::getContextHolderStrategy);
|
||||
AuthorizationManager<MethodInvocation> manager = new DeferringObservationAuthorizationManager<>(
|
||||
registryProvider, secured);
|
||||
AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor
|
||||
.secured(manager);
|
||||
interceptor.setOrder(interceptor.getOrder() + configuration.interceptorOrderOffset);
|
||||
interceptor.setSecurityContextHolderStrategy(strategy);
|
||||
eventPublisherProvider.ifAvailable(interceptor::setAuthorizationEventPublisher);
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImportMetadata(AnnotationMetadata importMetadata) {
|
||||
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
|
||||
this.interceptorOrderOffset = annotation.offset();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-69
@@ -17,7 +17,6 @@
|
||||
package org.springframework.security.config.annotation.web.configurers;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
@@ -31,14 +30,12 @@ import org.springframework.security.authorization.AuthorityAuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationManagers;
|
||||
import org.springframework.security.authorization.ObservationAuthorizationManager;
|
||||
import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
|
||||
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
|
||||
@@ -247,14 +244,11 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
* {@link RequestMatcher}s.
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class AuthorizedUrl {
|
||||
|
||||
private final List<? extends RequestMatcher> matchers;
|
||||
|
||||
private boolean not;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
* @param matchers the {@link RequestMatcher} instances to map
|
||||
@@ -267,16 +261,6 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
return this.matchers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Negates the following authorization rule.
|
||||
* @return the {@link AuthorizedUrl} for further customization
|
||||
* @since 6.3
|
||||
*/
|
||||
public AuthorizedUrl not() {
|
||||
this.not = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that URLs are allowed by anyone.
|
||||
* @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
|
||||
@@ -389,21 +373,6 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
return access(AuthenticatedAuthorizationManager.anonymous());
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that a path variable in URL to be compared.
|
||||
*
|
||||
* <p>
|
||||
* For example, <pre>
|
||||
* requestMatchers("/user/{username}").hasVariable("username").equalTo(Authentication::getName)
|
||||
* </pre>
|
||||
* @param variable the variable in URL template to compare.
|
||||
* @return {@link AuthorizedUrlVariable} for further customization.
|
||||
* @since 6.3
|
||||
*/
|
||||
public AuthorizedUrlVariable hasVariable(String variable) {
|
||||
return new AuthorizedUrlVariable(variable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows specifying a custom {@link AuthorizationManager}.
|
||||
* @param manager the {@link AuthorizationManager} to use
|
||||
@@ -413,44 +382,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
public AuthorizationManagerRequestMatcherRegistry access(
|
||||
AuthorizationManager<RequestAuthorizationContext> manager) {
|
||||
Assert.notNull(manager, "manager cannot be null");
|
||||
return (this.not)
|
||||
? AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, AuthorizationManagers.not(manager))
|
||||
: AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that allows configuring {@link RequestMatcher}s with URI path
|
||||
* variables
|
||||
*
|
||||
* @author Taehong Kim
|
||||
* @since 6.3
|
||||
*/
|
||||
public final class AuthorizedUrlVariable {
|
||||
|
||||
private final String variable;
|
||||
|
||||
private AuthorizedUrlVariable(String variable) {
|
||||
this.variable = variable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the value of a path variable in the URI with an `Authentication`
|
||||
* attribute
|
||||
* <p>
|
||||
* For example, <pre>
|
||||
* requestMatchers("/user/{username}").hasVariable("username").equalTo(Authentication::getName));
|
||||
* </pre>
|
||||
* @param function a function to get value from {@link Authentication}.
|
||||
* @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
|
||||
* customization.
|
||||
*/
|
||||
public AuthorizationManagerRequestMatcherRegistry equalTo(Function<Authentication, String> function) {
|
||||
return access((auth, requestContext) -> {
|
||||
String value = requestContext.getVariables().get(this.variable);
|
||||
return new AuthorizationDecision(function.apply(auth.get()).equals(value));
|
||||
});
|
||||
}
|
||||
|
||||
return AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+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;
|
||||
|
||||
+12
-399
@@ -62,7 +62,6 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||
import org.springframework.security.core.session.ReactiveSessionRegistry;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
|
||||
@@ -125,26 +124,19 @@ import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter;
|
||||
import org.springframework.security.web.server.authentication.AuthenticationConverterServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
|
||||
import org.springframework.security.web.server.authentication.ConcurrentSessionControlServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.DelegatingServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler;
|
||||
import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.RegisterSessionServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationEntryPointFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerFormLoginAuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.ServerHttpBasicAuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.ServerMaximumSessionsExceededHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.SessionLimit;
|
||||
import org.springframework.security.web.server.authentication.WebFilterChainServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.logout.DelegatingServerLogoutHandler;
|
||||
import org.springframework.security.web.server.authentication.logout.LogoutWebFilter;
|
||||
import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler;
|
||||
@@ -320,8 +312,6 @@ public class ServerHttpSecurity {
|
||||
|
||||
private LoginPageSpec loginPage = new LoginPageSpec();
|
||||
|
||||
private SessionManagementSpec sessionManagement;
|
||||
|
||||
private ReactiveAuthenticationManager authenticationManager;
|
||||
|
||||
private ServerSecurityContextRepository securityContextRepository;
|
||||
@@ -370,7 +360,6 @@ public class ServerHttpSecurity {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Adds a {@link WebFilter} before specific position.
|
||||
* @param webFilter the {@link WebFilter} to add
|
||||
* @param order the place before which to insert the {@link WebFilter}
|
||||
@@ -754,36 +743,6 @@ public class ServerHttpSecurity {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures Session Management. An example configuration is provided below:
|
||||
* <pre class="code">
|
||||
* @Bean
|
||||
* SecurityWebFilterChain filterChain(ServerHttpSecurity http, ReactiveSessionRegistry sessionRegistry) {
|
||||
* http
|
||||
* // ...
|
||||
* .sessionManagement((sessionManagement) -> sessionManagement
|
||||
* .concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
* .maxSessions(1)
|
||||
* .maxSessionsPreventsLogin(true)
|
||||
* .sessionRegistry(sessionRegistry)
|
||||
* )
|
||||
* );
|
||||
* return http.build();
|
||||
* }
|
||||
* </pre>
|
||||
* @param customizer the {@link Customizer} to provide more options for the
|
||||
* {@link SessionManagementSpec}
|
||||
* @return the {@link ServerHttpSecurity} to continue configuring
|
||||
* @since 6.3
|
||||
*/
|
||||
public ServerHttpSecurity sessionManagement(Customizer<SessionManagementSpec> customizer) {
|
||||
if (this.sessionManagement == null) {
|
||||
this.sessionManagement = new SessionManagementSpec();
|
||||
}
|
||||
customizer.customize(this.sessionManagement);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures password management. An example configuration is provided below:
|
||||
*
|
||||
@@ -1558,9 +1517,6 @@ public class ServerHttpSecurity {
|
||||
}
|
||||
WebFilter securityContextRepositoryWebFilter = securityContextRepositoryWebFilter();
|
||||
this.webFilters.add(securityContextRepositoryWebFilter);
|
||||
if (this.sessionManagement != null) {
|
||||
this.sessionManagement.configure(this);
|
||||
}
|
||||
if (this.httpsRedirectSpec != null) {
|
||||
this.httpsRedirectSpec.configure(this);
|
||||
}
|
||||
@@ -1951,249 +1907,6 @@ public class ServerHttpSecurity {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures how sessions are managed.
|
||||
*/
|
||||
public class SessionManagementSpec {
|
||||
|
||||
private ConcurrentSessionsSpec concurrentSessions;
|
||||
|
||||
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
|
||||
|
||||
private ReactiveSessionRegistry sessionRegistry;
|
||||
|
||||
private SessionLimit sessionLimit = SessionLimit.UNLIMITED;
|
||||
|
||||
private ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler = new InvalidateLeastUsedServerMaximumSessionsExceededHandler();
|
||||
|
||||
/**
|
||||
* Configures how many sessions are allowed for a given user.
|
||||
* @param customizer the customizer to provide more options
|
||||
* @return the {@link SessionManagementSpec} to customize
|
||||
*/
|
||||
public SessionManagementSpec concurrentSessions(Customizer<ConcurrentSessionsSpec> customizer) {
|
||||
if (this.concurrentSessions == null) {
|
||||
this.concurrentSessions = new ConcurrentSessionsSpec();
|
||||
}
|
||||
customizer.customize(this.concurrentSessions);
|
||||
return this;
|
||||
}
|
||||
|
||||
void configure(ServerHttpSecurity http) {
|
||||
if (this.concurrentSessions != null) {
|
||||
ReactiveSessionRegistry reactiveSessionRegistry = getSessionRegistry();
|
||||
ConcurrentSessionControlServerAuthenticationSuccessHandler concurrentSessionControlStrategy = new ConcurrentSessionControlServerAuthenticationSuccessHandler(
|
||||
reactiveSessionRegistry);
|
||||
concurrentSessionControlStrategy.setSessionLimit(this.sessionLimit);
|
||||
concurrentSessionControlStrategy.setMaximumSessionsExceededHandler(this.maximumSessionsExceededHandler);
|
||||
RegisterSessionServerAuthenticationSuccessHandler registerSessionAuthenticationStrategy = new RegisterSessionServerAuthenticationSuccessHandler(
|
||||
reactiveSessionRegistry);
|
||||
this.authenticationSuccessHandler = new DelegatingServerAuthenticationSuccessHandler(
|
||||
concurrentSessionControlStrategy, registerSessionAuthenticationStrategy);
|
||||
SessionRegistryWebFilter sessionRegistryWebFilter = new SessionRegistryWebFilter(
|
||||
reactiveSessionRegistry);
|
||||
configureSuccessHandlerOnAuthenticationFilters();
|
||||
http.addFilterAfter(sessionRegistryWebFilter, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
|
||||
}
|
||||
}
|
||||
|
||||
private void configureSuccessHandlerOnAuthenticationFilters() {
|
||||
if (ServerHttpSecurity.this.formLogin != null) {
|
||||
ServerHttpSecurity.this.formLogin.defaultSuccessHandlers.add(0, this.authenticationSuccessHandler);
|
||||
}
|
||||
if (ServerHttpSecurity.this.oauth2Login != null) {
|
||||
ServerHttpSecurity.this.oauth2Login.defaultSuccessHandlers.add(0, this.authenticationSuccessHandler);
|
||||
}
|
||||
if (ServerHttpSecurity.this.httpBasic != null) {
|
||||
ServerHttpSecurity.this.httpBasic.defaultSuccessHandlers.add(0, this.authenticationSuccessHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private ReactiveSessionRegistry getSessionRegistry() {
|
||||
if (this.sessionRegistry == null) {
|
||||
this.sessionRegistry = getBeanOrNull(ReactiveSessionRegistry.class);
|
||||
}
|
||||
if (this.sessionRegistry == null) {
|
||||
throw new IllegalStateException(
|
||||
"A ReactiveSessionRegistry is needed for concurrent session management");
|
||||
}
|
||||
return this.sessionRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures how many sessions are allowed for a given user.
|
||||
*/
|
||||
public class ConcurrentSessionsSpec {
|
||||
|
||||
/**
|
||||
* Sets the {@link ReactiveSessionRegistry} to use.
|
||||
* @param reactiveSessionRegistry the {@link ReactiveSessionRegistry} to use
|
||||
* @return the {@link ConcurrentSessionsSpec} to continue customizing
|
||||
*/
|
||||
public ConcurrentSessionsSpec sessionRegistry(ReactiveSessionRegistry reactiveSessionRegistry) {
|
||||
SessionManagementSpec.this.sessionRegistry = reactiveSessionRegistry;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum number of sessions allowed for any user. You can use
|
||||
* {@link SessionLimit#of(int)} to specify a positive integer or
|
||||
* {@link SessionLimit#UNLIMITED} to allow unlimited sessions. To customize
|
||||
* the maximum number of sessions on a per-user basis, you can provide a
|
||||
* custom {@link SessionLimit} implementation, like so: <pre>
|
||||
* http
|
||||
* .sessionManagement((sessions) -> sessions
|
||||
* .concurrentSessions((concurrency) -> concurrency
|
||||
* .maximumSessions((authentication) -> {
|
||||
* if (authentication.getName().equals("admin")) {
|
||||
* return Mono.empty() // unlimited sessions for admin
|
||||
* }
|
||||
* return Mono.just(1); // one session for every other user
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
* </pre>
|
||||
* @param sessionLimit the maximum number of sessions allowed for any user
|
||||
* @return the {@link ConcurrentSessionsSpec} to continue customizing
|
||||
*/
|
||||
public ConcurrentSessionsSpec maximumSessions(SessionLimit sessionLimit) {
|
||||
Assert.notNull(sessionLimit, "sessionLimit cannot be null");
|
||||
SessionManagementSpec.this.sessionLimit = sessionLimit;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ServerMaximumSessionsExceededHandler} to use when the
|
||||
* maximum number of sessions is exceeded.
|
||||
* @param maximumSessionsExceededHandler the
|
||||
* {@link ServerMaximumSessionsExceededHandler} to use
|
||||
* @return the {@link ConcurrentSessionsSpec} to continue customizing
|
||||
*/
|
||||
public ConcurrentSessionsSpec maximumSessionsExceededHandler(
|
||||
ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler) {
|
||||
Assert.notNull(maximumSessionsExceededHandler, "maximumSessionsExceededHandler cannot be null");
|
||||
SessionManagementSpec.this.maximumSessionsExceededHandler = maximumSessionsExceededHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class SessionRegistryWebFilter implements WebFilter {
|
||||
|
||||
private final ReactiveSessionRegistry sessionRegistry;
|
||||
|
||||
private SessionRegistryWebFilter(ReactiveSessionRegistry sessionRegistry) {
|
||||
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return chain.filter(new SessionRegistryWebExchange(exchange));
|
||||
}
|
||||
|
||||
private final class SessionRegistryWebExchange extends ServerWebExchangeDecorator {
|
||||
|
||||
private final Mono<WebSession> sessionMono;
|
||||
|
||||
private SessionRegistryWebExchange(ServerWebExchange delegate) {
|
||||
super(delegate);
|
||||
this.sessionMono = delegate.getSession()
|
||||
.flatMap((session) -> SessionRegistryWebFilter.this.sessionRegistry
|
||||
.updateLastAccessTime(session.getId())
|
||||
.thenReturn(session))
|
||||
.map(SessionRegistryWebSession::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<WebSession> getSession() {
|
||||
return this.sessionMono;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class SessionRegistryWebSession implements WebSession {
|
||||
|
||||
private final WebSession session;
|
||||
|
||||
private SessionRegistryWebSession(WebSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.session.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
return this.session.getAttributes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.session.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStarted() {
|
||||
return this.session.isStarted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> changeSessionId() {
|
||||
String currentId = this.session.getId();
|
||||
return SessionRegistryWebFilter.this.sessionRegistry.removeSessionInformation(currentId)
|
||||
.flatMap((information) -> this.session.changeSessionId().thenReturn(information))
|
||||
.flatMap((information) -> {
|
||||
information = information.withSessionId(this.session.getId());
|
||||
return SessionRegistryWebFilter.this.sessionRegistry.saveSessionInformation(information);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> invalidate() {
|
||||
String currentId = this.session.getId();
|
||||
return SessionRegistryWebFilter.this.sessionRegistry.removeSessionInformation(currentId)
|
||||
.flatMap((information) -> this.session.invalidate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> save() {
|
||||
return this.session.save();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return this.session.isExpired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return this.session.getCreationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastAccessTime() {
|
||||
return this.session.getLastAccessTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxIdleTime(Duration maxIdleTime) {
|
||||
this.session.setMaxIdleTime(maxIdleTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getMaxIdleTime() {
|
||||
return this.session.getMaxIdleTime();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures HTTPS redirection rules
|
||||
*
|
||||
@@ -2498,11 +2211,6 @@ public class ServerHttpSecurity {
|
||||
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
||||
|
||||
private final List<ServerAuthenticationSuccessHandler> defaultSuccessHandlers = new ArrayList<>(
|
||||
List.of(new WebFilterChainServerAuthenticationSuccessHandler()));
|
||||
|
||||
private List<ServerAuthenticationSuccessHandler> authenticationSuccessHandlers = new ArrayList<>();
|
||||
|
||||
private HttpBasicSpec() {
|
||||
List<DelegateEntry> entryPoints = new ArrayList<>();
|
||||
entryPoints
|
||||
@@ -2513,40 +2221,6 @@ public class ServerHttpSecurity {
|
||||
this.entryPoint = defaultEntryPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link ServerAuthenticationSuccessHandler} used after authentication
|
||||
* success. Defaults to {@link WebFilterChainServerAuthenticationSuccessHandler}.
|
||||
* Note that this method clears previously added success handlers via
|
||||
* {@link #authenticationSuccessHandler(Consumer)}
|
||||
* @param authenticationSuccessHandler the success handler to use
|
||||
* @return the {@link HttpBasicSpec} to continue configuring
|
||||
* @since 6.3
|
||||
*/
|
||||
public HttpBasicSpec authenticationSuccessHandler(
|
||||
ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
|
||||
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
|
||||
authenticationSuccessHandler((handlers) -> {
|
||||
handlers.clear();
|
||||
handlers.add(authenticationSuccessHandler);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows customizing the list of {@link ServerAuthenticationSuccessHandler}. The
|
||||
* default list contains a
|
||||
* {@link WebFilterChainServerAuthenticationSuccessHandler}.
|
||||
* @param handlersConsumer the handlers consumer
|
||||
* @return the {@link HttpBasicSpec} to continue configuring
|
||||
* @since 6.3
|
||||
*/
|
||||
public HttpBasicSpec authenticationSuccessHandler(
|
||||
Consumer<List<ServerAuthenticationSuccessHandler>> handlersConsumer) {
|
||||
Assert.notNull(handlersConsumer, "handlersConsumer cannot be null");
|
||||
handlersConsumer.accept(this.authenticationSuccessHandlers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link ReactiveAuthenticationManager} used to authenticate. Defaults to
|
||||
* {@link ServerHttpSecurity#authenticationManager(ReactiveAuthenticationManager)}.
|
||||
@@ -2632,17 +2306,9 @@ public class ServerHttpSecurity {
|
||||
authenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
|
||||
authenticationFilter.setAuthenticationConverter(new ServerHttpBasicAuthenticationConverter());
|
||||
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
|
||||
authenticationFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler(http));
|
||||
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.HTTP_BASIC);
|
||||
}
|
||||
|
||||
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
|
||||
if (this.authenticationSuccessHandlers.isEmpty()) {
|
||||
return new DelegatingServerAuthenticationSuccessHandler(this.defaultSuccessHandlers);
|
||||
}
|
||||
return new DelegatingServerAuthenticationSuccessHandler(this.authenticationSuccessHandlers);
|
||||
}
|
||||
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler() {
|
||||
if (this.authenticationFailureHandler != null) {
|
||||
return this.authenticationFailureHandler;
|
||||
@@ -2714,9 +2380,6 @@ public class ServerHttpSecurity {
|
||||
private final RedirectServerAuthenticationSuccessHandler defaultSuccessHandler = new RedirectServerAuthenticationSuccessHandler(
|
||||
"/");
|
||||
|
||||
private final List<ServerAuthenticationSuccessHandler> defaultSuccessHandlers = new ArrayList<>(
|
||||
List.of(this.defaultSuccessHandler));
|
||||
|
||||
private RedirectServerAuthenticationEntryPoint defaultEntryPoint;
|
||||
|
||||
private ReactiveAuthenticationManager authenticationManager;
|
||||
@@ -2731,7 +2394,7 @@ public class ServerHttpSecurity {
|
||||
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
||||
|
||||
private List<ServerAuthenticationSuccessHandler> authenticationSuccessHandlers = new ArrayList<>();
|
||||
private ServerAuthenticationSuccessHandler authenticationSuccessHandler = this.defaultSuccessHandler;
|
||||
|
||||
private FormLoginSpec() {
|
||||
}
|
||||
@@ -2749,34 +2412,14 @@ public class ServerHttpSecurity {
|
||||
|
||||
/**
|
||||
* The {@link ServerAuthenticationSuccessHandler} used after authentication
|
||||
* success. Defaults to {@link RedirectServerAuthenticationSuccessHandler}. Note
|
||||
* that this method clears previously added success handlers via
|
||||
* {@link #authenticationSuccessHandler(Consumer)}
|
||||
* success. Defaults to {@link RedirectServerAuthenticationSuccessHandler}.
|
||||
* @param authenticationSuccessHandler the success handler to use
|
||||
* @return the {@link FormLoginSpec} to continue configuring
|
||||
*/
|
||||
public FormLoginSpec authenticationSuccessHandler(
|
||||
ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
|
||||
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
|
||||
authenticationSuccessHandler((handlers) -> {
|
||||
handlers.clear();
|
||||
handlers.add(authenticationSuccessHandler);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows customizing the list of {@link ServerAuthenticationSuccessHandler}. The
|
||||
* default list contains a {@link RedirectServerAuthenticationSuccessHandler} that
|
||||
* redirects to "/".
|
||||
* @param handlersConsumer the handlers consumer
|
||||
* @return the {@link FormLoginSpec} to continue configuring
|
||||
* @since 6.3
|
||||
*/
|
||||
public FormLoginSpec authenticationSuccessHandler(
|
||||
Consumer<List<ServerAuthenticationSuccessHandler>> handlersConsumer) {
|
||||
Assert.notNull(handlersConsumer, "handlersConsumer cannot be null");
|
||||
handlersConsumer.accept(this.authenticationSuccessHandlers);
|
||||
this.authenticationSuccessHandler = authenticationSuccessHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -2909,18 +2552,11 @@ public class ServerHttpSecurity {
|
||||
authenticationFilter.setRequiresAuthenticationMatcher(this.requiresAuthenticationMatcher);
|
||||
authenticationFilter.setAuthenticationFailureHandler(this.authenticationFailureHandler);
|
||||
authenticationFilter.setAuthenticationConverter(new ServerFormLoginAuthenticationConverter());
|
||||
authenticationFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler(http));
|
||||
authenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
|
||||
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
|
||||
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.FORM_LOGIN);
|
||||
}
|
||||
|
||||
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
|
||||
if (this.authenticationSuccessHandlers.isEmpty()) {
|
||||
return new DelegatingServerAuthenticationSuccessHandler(this.defaultSuccessHandlers);
|
||||
}
|
||||
return new DelegatingServerAuthenticationSuccessHandler(this.authenticationSuccessHandlers);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class LoginPageSpec {
|
||||
@@ -4099,12 +3735,7 @@ public class ServerHttpSecurity {
|
||||
|
||||
private ReactiveOidcSessionRegistry oidcSessionRegistry;
|
||||
|
||||
private final RedirectServerAuthenticationSuccessHandler defaultAuthenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();
|
||||
|
||||
private final List<ServerAuthenticationSuccessHandler> defaultSuccessHandlers = new ArrayList<>(
|
||||
List.of(this.defaultAuthenticationSuccessHandler));
|
||||
|
||||
private List<ServerAuthenticationSuccessHandler> authenticationSuccessHandlers = new ArrayList<>();
|
||||
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
|
||||
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
||||
|
||||
@@ -4152,8 +3783,7 @@ public class ServerHttpSecurity {
|
||||
/**
|
||||
* The {@link ServerAuthenticationSuccessHandler} used after authentication
|
||||
* success. Defaults to {@link RedirectServerAuthenticationSuccessHandler}
|
||||
* redirecting to "/". Note that this method clears previously added success
|
||||
* handlers via {@link #authenticationSuccessHandler(Consumer)}
|
||||
* redirecting to "/".
|
||||
* @param authenticationSuccessHandler the success handler to use
|
||||
* @return the {@link OAuth2LoginSpec} to customize
|
||||
* @since 5.2
|
||||
@@ -4161,25 +3791,7 @@ public class ServerHttpSecurity {
|
||||
public OAuth2LoginSpec authenticationSuccessHandler(
|
||||
ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
|
||||
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
|
||||
authenticationSuccessHandler((handlers) -> {
|
||||
handlers.clear();
|
||||
handlers.add(authenticationSuccessHandler);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows customizing the list of {@link ServerAuthenticationSuccessHandler}. The
|
||||
* default list contains a {@link RedirectServerAuthenticationSuccessHandler} that
|
||||
* redirects to "/".
|
||||
* @param handlersConsumer the handlers consumer
|
||||
* @return the {@link OAuth2LoginSpec} to continue configuring
|
||||
* @since 6.3
|
||||
*/
|
||||
public OAuth2LoginSpec authenticationSuccessHandler(
|
||||
Consumer<List<ServerAuthenticationSuccessHandler>> handlersConsumer) {
|
||||
Assert.notNull(handlersConsumer, "handlersConsumer cannot be null");
|
||||
handlersConsumer.accept(this.authenticationSuccessHandlers);
|
||||
this.authenticationSuccessHandler = authenticationSuccessHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -4436,11 +4048,12 @@ public class ServerHttpSecurity {
|
||||
}
|
||||
|
||||
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
|
||||
this.defaultAuthenticationSuccessHandler.setRequestCache(http.requestCache.requestCache);
|
||||
if (this.authenticationSuccessHandlers.isEmpty()) {
|
||||
return new DelegatingServerAuthenticationSuccessHandler(this.defaultSuccessHandlers);
|
||||
if (this.authenticationSuccessHandler == null) {
|
||||
RedirectServerAuthenticationSuccessHandler handler = new RedirectServerAuthenticationSuccessHandler();
|
||||
handler.setRequestCache(http.requestCache.requestCache);
|
||||
this.authenticationSuccessHandler = handler;
|
||||
}
|
||||
return new DelegatingServerAuthenticationSuccessHandler(this.authenticationSuccessHandlers);
|
||||
return this.authenticationSuccessHandler;
|
||||
}
|
||||
|
||||
private ServerAuthenticationFailureHandler getAuthenticationFailureHandler() {
|
||||
|
||||
+1
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -26,7 +26,6 @@ import org.springframework.security.config.annotation.web.configurers.AuthorizeH
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter
|
||||
import org.springframework.security.web.access.intercept.RequestAuthorizationContext
|
||||
import org.springframework.security.web.access.IpAddressAuthorizationManager
|
||||
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher
|
||||
@@ -223,13 +222,6 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl() {
|
||||
return AuthorityAuthorizationManager.hasAnyRole(*roles)
|
||||
}
|
||||
|
||||
/**
|
||||
* Require a specific IP or range of IP addresses.
|
||||
* @since 6.3
|
||||
*/
|
||||
fun hasIpAddress(ipAddress: String): AuthorizationManager<RequestAuthorizationContext> =
|
||||
IpAddressAuthorizationManager.hasIpAddress(ipAddress)
|
||||
|
||||
/**
|
||||
* Specify that URLs are allowed by anyone.
|
||||
*/
|
||||
|
||||
+1
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -38,8 +38,6 @@ import jakarta.servlet.http.HttpServletRequest
|
||||
* @property loginProcessingUrl the URL to validate the credentials
|
||||
* @property permitAll whether to grant access to the urls for [failureUrl] as well as
|
||||
* for the [HttpSecurityBuilder], the [loginPage] and [loginProcessingUrl] for every user
|
||||
* @property usernameParameter the HTTP parameter to look for the username when performing authentication
|
||||
* @property passwordParameter the HTTP parameter to look for the password when performing authentication
|
||||
*/
|
||||
@SecurityMarker
|
||||
class FormLoginDsl {
|
||||
@@ -50,8 +48,6 @@ class FormLoginDsl {
|
||||
var loginProcessingUrl: String? = null
|
||||
var permitAll: Boolean? = null
|
||||
var authenticationDetailsSource: AuthenticationDetailsSource<HttpServletRequest, *>? = null
|
||||
var usernameParameter: String? = null
|
||||
var passwordParameter: String? = null
|
||||
|
||||
private var defaultSuccessUrlOption: Pair<String, Boolean>? = null
|
||||
|
||||
@@ -99,8 +95,6 @@ class FormLoginDsl {
|
||||
authenticationSuccessHandler?.also { login.successHandler(authenticationSuccessHandler) }
|
||||
authenticationFailureHandler?.also { login.failureHandler(authenticationFailureHandler) }
|
||||
authenticationDetailsSource?.also { login.authenticationDetailsSource(authenticationDetailsSource) }
|
||||
usernameParameter?.also { login.usernameParameter(usernameParameter) }
|
||||
passwordParameter?.also { login.passwordParameter(passwordParameter) }
|
||||
if (disabled) {
|
||||
login.disable()
|
||||
}
|
||||
|
||||
-30
@@ -682,36 +682,6 @@ class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val in
|
||||
this.http.oidcLogout(oidcLogoutCustomizer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures Session Management support.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* @Configuration
|
||||
* @EnableWebFluxSecurity
|
||||
* open class SecurityConfig {
|
||||
*
|
||||
* @Bean
|
||||
* open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
* return http {
|
||||
* sessionManagement {
|
||||
* sessionConcurrency { }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param sessionManagementConfig custom configuration to configure the Session Management
|
||||
* @since 6.3
|
||||
* @see [ServerSessionManagementDsl]
|
||||
*/
|
||||
fun sessionManagement(sessionManagementConfig: ServerSessionManagementDsl.() -> Unit) {
|
||||
val sessionManagementCustomizer = ServerSessionManagementDsl().apply(sessionManagementConfig).get()
|
||||
this.http.sessionManagement(sessionManagementCustomizer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all configurations to the provided [ServerHttpSecurity]
|
||||
*/
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.config.web.server
|
||||
|
||||
import org.springframework.security.core.session.ReactiveSessionRegistry
|
||||
import org.springframework.security.web.server.authentication.ServerMaximumSessionsExceededHandler
|
||||
import org.springframework.security.web.server.authentication.SessionLimit
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure [ServerHttpSecurity] Session Concurrency support using idiomatic Kotlin code.
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
* @since 6.3
|
||||
*/
|
||||
@ServerSecurityMarker
|
||||
class ServerSessionConcurrencyDsl {
|
||||
var maximumSessions: SessionLimit? = null
|
||||
var maximumSessionsExceededHandler: ServerMaximumSessionsExceededHandler? = null
|
||||
var sessionRegistry: ReactiveSessionRegistry? = null
|
||||
|
||||
internal fun get(): (ServerHttpSecurity.SessionManagementSpec.ConcurrentSessionsSpec) -> Unit {
|
||||
return { sessionConcurrency ->
|
||||
maximumSessions?.also {
|
||||
sessionConcurrency.maximumSessions(maximumSessions!!)
|
||||
}
|
||||
maximumSessionsExceededHandler?.also {
|
||||
sessionConcurrency.maximumSessionsExceededHandler(maximumSessionsExceededHandler!!)
|
||||
}
|
||||
sessionRegistry?.also {
|
||||
sessionConcurrency.sessionRegistry(sessionRegistry!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.config.web.server
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure [ServerHttpSecurity] Session Management using idiomatic Kotlin code.
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
* @since 6.3
|
||||
*/
|
||||
@ServerSecurityMarker
|
||||
class ServerSessionManagementDsl {
|
||||
private var sessionConcurrency: ((ServerHttpSecurity.SessionManagementSpec.ConcurrentSessionsSpec) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* Enables Session Management support.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* @Configuration
|
||||
* @EnableWebFluxSecurity
|
||||
* open class SecurityConfig {
|
||||
*
|
||||
* @Bean
|
||||
* open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
* return http {
|
||||
* sessionManagement {
|
||||
* sessionConcurrency {
|
||||
* maximumSessions = { authentication -> Mono.just(1) }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param backChannelConfig custom configurations to configure OIDC 1.0 Back-Channel Logout support
|
||||
* @see [ServerOidcBackChannelLogoutDsl]
|
||||
*/
|
||||
fun sessionConcurrency(sessionConcurrencyConfig: ServerSessionConcurrencyDsl.() -> Unit) {
|
||||
this.sessionConcurrency = ServerSessionConcurrencyDsl().apply(sessionConcurrencyConfig).get()
|
||||
}
|
||||
|
||||
internal fun get(): (ServerHttpSecurity.SessionManagementSpec) -> Unit {
|
||||
return { sessionManagement ->
|
||||
sessionConcurrency?.also { sessionManagement.concurrentSessions(sessionConcurrency) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.3.xsd
|
||||
http\://www.springframework.org/schema/security/spring-security-6.3.xsd=org/springframework/security/config/spring-security-6.3.xsd
|
||||
http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.2.xsd
|
||||
http\://www.springframework.org/schema/security/spring-security-6.2.xsd=org/springframework/security/config/spring-security-6.2.xsd
|
||||
http\://www.springframework.org/schema/security/spring-security-6.1.xsd=org/springframework/security/config/spring-security-6.1.xsd
|
||||
http\://www.springframework.org/schema/security/spring-security-6.0.xsd=org/springframework/security/config/spring-security-6.0.xsd
|
||||
@@ -23,8 +22,7 @@ http\://www.springframework.org/schema/security/spring-security-2.0.xsd=org/spri
|
||||
http\://www.springframework.org/schema/security/spring-security-2.0.1.xsd=org/springframework/security/config/spring-security-2.0.1.xsd
|
||||
http\://www.springframework.org/schema/security/spring-security-2.0.2.xsd=org/springframework/security/config/spring-security-2.0.2.xsd
|
||||
http\://www.springframework.org/schema/security/spring-security-2.0.4.xsd=org/springframework/security/config/spring-security-2.0.4.xsd
|
||||
https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.3.xsd
|
||||
https\://www.springframework.org/schema/security/spring-security-6.3.xsd=org/springframework/security/config/spring-security-6.3.xsd
|
||||
https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.2.xsd
|
||||
https\://www.springframework.org/schema/security/spring-security-6.2.xsd=org/springframework/security/config/spring-security-6.2.xsd
|
||||
https\://www.springframework.org/schema/security/spring-security-6.1.xsd=org/springframework/security/config/spring-security-6.1.xsd
|
||||
https\://www.springframework.org/schema/security/spring-security-6.0.xsd=org/springframework/security/config/spring-security-6.0.xsd
|
||||
|
||||
-1346
File diff suppressed because it is too large
Load Diff
-3812
File diff suppressed because it is too large
Load Diff
+1
@@ -248,6 +248,7 @@ class SpringSecurityCoreVersionSerializableTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("getFilesToDeserialize")
|
||||
@Disabled("The feature is only supported for versions >= 6.3")
|
||||
void shouldBeAbleToDeserializeClassFromPreviousVersion(Path filePath) {
|
||||
try (FileInputStream fileInputStream = new FileInputStream(filePath.toFile());
|
||||
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
|
||||
|
||||
+1
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2022 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,9 +53,6 @@ public interface MethodSecurityService {
|
||||
@RolesAllowed("ADMIN")
|
||||
String jsr250RolesAllowed();
|
||||
|
||||
@RolesAllowed("USER")
|
||||
String jsr250RolesAllowedUser();
|
||||
|
||||
@Secured({ "ROLE_USER", "RUN_AS_SUPER" })
|
||||
Authentication runAs();
|
||||
|
||||
@@ -71,9 +68,6 @@ public interface MethodSecurityService {
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
void preAuthorizeAdmin();
|
||||
|
||||
@PreAuthorize("hasRole('USER')")
|
||||
void preAuthorizeUser();
|
||||
|
||||
@PreAuthorize("hasPermission(#object,'read')")
|
||||
String hasPermission(String object);
|
||||
|
||||
@@ -96,14 +90,6 @@ public interface MethodSecurityService {
|
||||
@PostAuthorize("returnObject.size == 2")
|
||||
List<String> manyAnnotations(List<String> array);
|
||||
|
||||
@PreFilter("filterObject != 'DropOnPreFilter'")
|
||||
@PreAuthorize("#list.remove('DropOnPreAuthorize')")
|
||||
@Secured("ROLE_SECURED")
|
||||
@RolesAllowed("JSR250")
|
||||
@PostAuthorize("#list.remove('DropOnPostAuthorize')")
|
||||
@PostFilter("filterObject != 'DropOnPostFilter'")
|
||||
List<String> allAnnotations(List<String> list);
|
||||
|
||||
@RequireUserRole
|
||||
@RequireAdminRole
|
||||
void repeatedAnnotations();
|
||||
|
||||
+1
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -56,11 +56,6 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String jsr250RolesAllowedUser() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication runAs() {
|
||||
return SecurityContextHolder.getContext().getAuthentication();
|
||||
@@ -78,10 +73,6 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
|
||||
public void preAuthorizeAdmin() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preAuthorizeUser() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preAuthorizePermitAll() {
|
||||
return null;
|
||||
@@ -117,11 +108,6 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
|
||||
return object;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> allAnnotations(List<String> list) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void repeatedAnnotations() {
|
||||
}
|
||||
|
||||
+2
-265
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -36,7 +36,6 @@ import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.AdviceMode;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.annotation.AnnotationConfigurationException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
@@ -47,8 +46,6 @@ import org.springframework.security.access.annotation.ExpressionProtectedBusines
|
||||
import org.springframework.security.access.annotation.Jsr250BusinessServiceImpl;
|
||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
@@ -442,6 +439,7 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
assertThat(this.spring.getContext().containsBean("annotationSecurityAspect$0")).isFalse();
|
||||
}
|
||||
|
||||
// gh-13572
|
||||
@Test
|
||||
public void configureWhenBeanOverridingDisallowedThenWorks() {
|
||||
this.spring.register(MethodSecurityServiceConfig.class, BusinessServiceConfig.class)
|
||||
@@ -449,157 +447,10 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
.autowire();
|
||||
}
|
||||
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
@Test
|
||||
public void methodSecurityAdminWhenRoleHierarchyBeanAvailableThenUses() {
|
||||
this.spring.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class).autowire();
|
||||
this.methodSecurityService.preAuthorizeUser();
|
||||
this.methodSecurityService.securedUser();
|
||||
this.methodSecurityService.jsr250RolesAllowedUser();
|
||||
}
|
||||
|
||||
@WithMockUser
|
||||
@Test
|
||||
public void methodSecurityUserWhenRoleHierarchyBeanAvailableThenUses() {
|
||||
this.spring.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class).autowire();
|
||||
this.methodSecurityService.preAuthorizeUser();
|
||||
this.methodSecurityService.securedUser();
|
||||
this.methodSecurityService.jsr250RolesAllowedUser();
|
||||
}
|
||||
|
||||
@WithMockUser(roles = "ADMIN")
|
||||
@Test
|
||||
public void methodSecurityAdminWhenAuthorizationEventPublisherBeanAvailableThenUses() {
|
||||
this.spring
|
||||
.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class,
|
||||
AuthorizationEventPublisherConfig.class)
|
||||
.autowire();
|
||||
this.methodSecurityService.preAuthorizeUser();
|
||||
this.methodSecurityService.securedUser();
|
||||
this.methodSecurityService.jsr250RolesAllowedUser();
|
||||
}
|
||||
|
||||
@WithMockUser
|
||||
@Test
|
||||
public void methodSecurityUserWhenAuthorizationEventPublisherBeanAvailableThenUses() {
|
||||
this.spring
|
||||
.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class,
|
||||
AuthorizationEventPublisherConfig.class)
|
||||
.autowire();
|
||||
this.methodSecurityService.preAuthorizeUser();
|
||||
this.methodSecurityService.securedUser();
|
||||
this.methodSecurityService.jsr250RolesAllowedUser();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPreFilterThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetPreFilterConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(5);
|
||||
assertThat(filtered).containsExactly("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPreAuthorizeThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetPreAuthorizeConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(4);
|
||||
assertThat(filtered).containsExactly("DropOnPreAuthorize", "DropOnPostAuthorize", "DropOnPostFilter",
|
||||
"DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetSecuredThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetSecuredConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(3);
|
||||
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetJsr250WithInsufficientRolesThenFails() {
|
||||
this.spring.register(ReturnBeforeOffsetJsr250Config.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
assertThatExceptionOfType(AccessDeniedException.class)
|
||||
.isThrownBy(() -> this.methodSecurityService.allAnnotations(new ArrayList<>(list)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "SECURED")
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetJsr250ThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetJsr250Config.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(3);
|
||||
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = { "SECURED" })
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPostAuthorizeWithInsufficientRolesThenFails() {
|
||||
this.spring.register(ReturnBeforeOffsetPostAuthorizeConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
assertThatExceptionOfType(AccessDeniedException.class)
|
||||
.isThrownBy(() -> this.methodSecurityService.allAnnotations(new ArrayList<>(list)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = { "SECURED", "JSR250" })
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPostAuthorizeThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetPostAuthorizeConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(3);
|
||||
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = { "SECURED", "JSR250" })
|
||||
public void allAnnotationsWhenAdviceBeforeOffsetPostFilterThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnBeforeOffsetPostFilterConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(2);
|
||||
assertThat(filtered).containsExactly("DropOnPostFilter", "DoNotDrop");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = { "SECURED", "JSR250" })
|
||||
public void allAnnotationsWhenAdviceAfterAllOffsetThenReturnsFilteredList() {
|
||||
this.spring.register(ReturnAfterAllOffsetConfig.class).autowire();
|
||||
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
|
||||
"DropOnPostFilter", "DoNotDrop");
|
||||
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
|
||||
assertThat(filtered).hasSize(1);
|
||||
assertThat(filtered).containsExactly("DoNotDrop");
|
||||
}
|
||||
|
||||
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
|
||||
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
|
||||
}
|
||||
|
||||
private static Advisor returnAdvisor(int order) {
|
||||
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
|
||||
pointcut.setPattern(".*MethodSecurityServiceImpl.*");
|
||||
MethodInterceptor interceptor = (mi) -> mi.getArguments()[0];
|
||||
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, interceptor);
|
||||
advisor.setOrder(order);
|
||||
return advisor;
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableCustomMethodSecurity
|
||||
static class CustomMethodSecurityServiceConfig {
|
||||
@@ -776,118 +627,4 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
|
||||
static class RoleHierarchyConfig {
|
||||
|
||||
@Bean
|
||||
static RoleHierarchy roleHierarchy() {
|
||||
RoleHierarchyImpl roleHierarchyImpl = new RoleHierarchyImpl();
|
||||
roleHierarchyImpl.setHierarchy("ROLE_ADMIN > ROLE_USER");
|
||||
return roleHierarchyImpl;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetPreFilterConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforePreFilter() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.PRE_FILTER.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetPreAuthorizeConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforePreAuthorize() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetSecuredConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforeSecured() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.SECURED.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetJsr250Config {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforeJsr250() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.JSR250.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetPostAuthorizeConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforePreAuthorize() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnBeforeOffsetPostFilterConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnBeforePostFilter() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.POST_FILTER.getOrder() + OffsetConfig.OFFSET - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(OffsetConfig.class)
|
||||
static class ReturnAfterAllOffsetConfig {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor returnAfterAll() {
|
||||
return returnAdvisor(AuthorizationInterceptorsOrder.POST_FILTER.getOrder() + OffsetConfig.OFFSET + 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity(offset = OffsetConfig.OFFSET, jsr250Enabled = true, securedEnabled = true)
|
||||
static class OffsetConfig {
|
||||
|
||||
static final int OFFSET = 2;
|
||||
|
||||
@Bean
|
||||
MethodSecurityService methodSecurityService() {
|
||||
return new MethodSecurityServiceImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Authz authz() {
|
||||
return new Authz();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-51
@@ -40,10 +40,8 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
@@ -545,17 +543,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
this.mvc.perform(request).andExpect(status().isOk());
|
||||
request = get("/user/deny");
|
||||
this.mvc.perform(request).andExpect(status().isUnauthorized());
|
||||
|
||||
UserDetails user = TestAuthentication.withUsername("taehong").build();
|
||||
Authentication authentication = TestAuthentication.authenticated(user);
|
||||
request = get("/v2/user/{username}", user.getUsername()).with(authentication(authentication));
|
||||
this.mvc.perform(request).andExpect(status().isOk());
|
||||
|
||||
request = get("/v2/user/{username}", "withNoAuthentication");
|
||||
this.mvc.perform(request).andExpect(status().isUnauthorized());
|
||||
|
||||
request = get("/v2/user/{username}", "another").with(authentication(authentication));
|
||||
this.mvc.perform(request).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
private static RequestPostProcessor remoteAddress(String remoteAddress) {
|
||||
@@ -609,20 +596,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenNotConfigAndAuthenticatedThenRespondsWithForbidden() throws Exception {
|
||||
this.spring.register(NotConfig.class, BasicController.class).autowire();
|
||||
MockHttpServletRequestBuilder requestWithUser = get("/").with(user("user"));
|
||||
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenNotConfigAndNotAuthenticatedThenRespondsWithOk() throws Exception {
|
||||
this.spring.register(NotConfig.class, BasicController.class).autowire();
|
||||
MockHttpServletRequestBuilder requestWithUser = get("/");
|
||||
this.mvc.perform(requestWithUser).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class GrantedAuthorityDefaultHasRoleConfig {
|
||||
@@ -1080,7 +1053,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
.httpBasic(withDefaults())
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.requestMatchers("/user/{username}").access(new WebExpressionAuthorizationManager("#username == 'user'"))
|
||||
.requestMatchers("/v2/user/{username}").hasVariable("username").equalTo(Authentication::getName)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
@@ -1094,11 +1066,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
return username;
|
||||
}
|
||||
|
||||
@RequestMapping("/v2/user/{username}")
|
||||
String pathV2(@PathVariable("username") String username) {
|
||||
return username;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1169,24 +1136,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class NotConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain chain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.httpBasic(withDefaults())
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().not().authenticated()
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class AuthorizationEventPublisherConfig {
|
||||
|
||||
|
||||
+3
-3
@@ -65,7 +65,7 @@ public class XsdDocumentedTests {
|
||||
|
||||
String schema31xDocumentLocation = "org/springframework/security/config/spring-security-3.1.xsd";
|
||||
|
||||
String schemaDocumentLocation = "org/springframework/security/config/spring-security-6.3.xsd";
|
||||
String schemaDocumentLocation = "org/springframework/security/config/spring-security-6.2.xsd";
|
||||
|
||||
XmlSupport xml = new XmlSupport();
|
||||
|
||||
@@ -151,8 +151,8 @@ public class XsdDocumentedTests {
|
||||
.list((dir, name) -> name.endsWith(".xsd"));
|
||||
// @formatter:on
|
||||
assertThat(schemas.length)
|
||||
.withFailMessage("the count is equal to 25, if not then schemaDocument needs updating")
|
||||
.isEqualTo(25);
|
||||
.withFailMessage("the count is equal to 24, if not then schemaDocument needs updating")
|
||||
.isEqualTo(24);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-8
@@ -56,11 +56,9 @@ import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.ServerRedirectStrategy;
|
||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilterTests;
|
||||
import org.springframework.security.web.server.authentication.DelegatingServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.logout.DelegatingServerLogoutHandler;
|
||||
import org.springframework.security.web.server.authentication.logout.LogoutWebFilter;
|
||||
@@ -594,7 +592,6 @@ public class ServerHttpSecurityTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldConfigureRequestCacheForOAuth2LoginAuthenticationEntryPointAndSuccessHandler() {
|
||||
ServerRequestCache requestCache = spy(new WebSessionServerRequestCache());
|
||||
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(
|
||||
@@ -616,11 +613,8 @@ public class ServerHttpSecurityTests {
|
||||
OAuth2LoginAuthenticationWebFilter authenticationWebFilter = getWebFilter(securityFilterChain,
|
||||
OAuth2LoginAuthenticationWebFilter.class)
|
||||
.get();
|
||||
DelegatingServerAuthenticationSuccessHandler handler = (DelegatingServerAuthenticationSuccessHandler) ReflectionTestUtils
|
||||
.getField(authenticationWebFilter, "authenticationSuccessHandler");
|
||||
List<ServerAuthenticationSuccessHandler> delegates = (List<ServerAuthenticationSuccessHandler>) ReflectionTestUtils
|
||||
.getField(handler, "delegates");
|
||||
assertThat(ReflectionTestUtils.getField(delegates.get(0), "requestCache")).isSameAs(requestCache);
|
||||
Object handler = ReflectionTestUtils.getField(authenticationWebFilter, "authenticationSuccessHandler");
|
||||
assertThat(ReflectionTestUtils.getField(handler, "requestCache")).isSameAs(requestCache);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
-624
@@ -1,624 +0,0 @@
|
||||
/*
|
||||
* 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.config.web.server;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
|
||||
import org.springframework.security.core.session.ReactiveSessionInformation;
|
||||
import org.springframework.security.core.session.ReactiveSessionRegistry;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler;
|
||||
import org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.authentication.SessionLimit;
|
||||
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
|
||||
import org.springframework.security.web.session.WebSessionStoreReactiveSessionRegistry;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
|
||||
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
public class SessionManagementSpecTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
WebTestClient client;
|
||||
|
||||
@Autowired
|
||||
public void setApplicationContext(ApplicationContext context) {
|
||||
this.client = WebTestClient.bindToApplicationContext(context).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenMaxSessionPreventsLoginThenSecondLoginFails() {
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
|
||||
ResponseCookie firstLoginSessionCookie = loginReturningCookie(data);
|
||||
|
||||
// second login should fail
|
||||
this.client.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectHeader()
|
||||
.location("/login?error");
|
||||
|
||||
// first login should still be valid
|
||||
this.client.mutateWith(csrf())
|
||||
.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpBasicWhenUsingSavingAuthenticationInWebSessionAndPreventLoginThenSecondRequestFails() {
|
||||
this.spring.register(ConcurrentSessionsHttpBasicWithWebSessionMaxSessionPreventsLoginConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
|
||||
// first request be successful
|
||||
ResponseCookie sessionCookie = this.client.get()
|
||||
.uri("/")
|
||||
.headers((headers) -> headers.setBasicAuth("user", "password"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectCookie()
|
||||
.exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
|
||||
// request with no session should fail
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.headers((headers) -> headers.setBasicAuth("user", "password"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isUnauthorized();
|
||||
|
||||
// request with session obtained from first request should be successful
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.headers((headers) -> headers.setBasicAuth("user", "password"))
|
||||
.cookie(sessionCookie.getName(), sessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenMaxSessionPerAuthenticationThenUserLoginFailsAndAdminLoginSucceeds() {
|
||||
ConcurrentSessionsMaxSessionPreventsLoginConfig.sessionLimit = (authentication) -> {
|
||||
if (authentication.getName().equals("admin")) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return Mono.just(1);
|
||||
};
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
MultiValueMap<String, String> adminCreds = new LinkedMultiValueMap<>();
|
||||
adminCreds.add("username", "admin");
|
||||
adminCreds.add("password", "password");
|
||||
|
||||
ResponseCookie userFirstLoginSessionCookie = loginReturningCookie(data);
|
||||
ResponseCookie adminFirstLoginSessionCookie = loginReturningCookie(adminCreds);
|
||||
// second user login should fail
|
||||
this.client.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectHeader()
|
||||
.location("/login?error");
|
||||
// first login should still be valid
|
||||
this.client.mutateWith(csrf())
|
||||
.get()
|
||||
.uri("/")
|
||||
.cookie(userFirstLoginSessionCookie.getName(), userFirstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
ResponseCookie adminSecondLoginSessionCookie = loginReturningCookie(adminCreds);
|
||||
this.client.mutateWith(csrf())
|
||||
.get()
|
||||
.uri("/")
|
||||
.cookie(adminFirstLoginSessionCookie.getName(), adminFirstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
this.client.mutateWith(csrf())
|
||||
.get()
|
||||
.uri("/")
|
||||
.cookie(adminSecondLoginSessionCookie.getName(), adminSecondLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenMaxSessionDoesNotPreventLoginThenSecondLoginSucceedsAndFirstSessionIsInvalidated() {
|
||||
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.sessionLimit = SessionLimit.of(1);
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
|
||||
ResponseCookie firstLoginSessionCookie = loginReturningCookie(data);
|
||||
ResponseCookie secondLoginSessionCookie = loginReturningCookie(data);
|
||||
|
||||
// first login should not be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isFound()
|
||||
.expectHeader()
|
||||
.location("/login");
|
||||
|
||||
// second login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(secondLoginSessionCookie.getName(), secondLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenMaxSessionDoesNotPreventLoginThenLeastRecentlyUsedSessionIsInvalidated() {
|
||||
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.sessionLimit = SessionLimit.of(2);
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.class).autowire();
|
||||
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
|
||||
ResponseCookie firstLoginSessionCookie = loginReturningCookie(data);
|
||||
ResponseCookie secondLoginSessionCookie = loginReturningCookie(data);
|
||||
|
||||
// update last access time for first request
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
ResponseCookie thirdLoginSessionCookie = loginReturningCookie(data);
|
||||
|
||||
// second login should be invalid, it is the least recently used session
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(secondLoginSessionCookie.getName(), secondLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isFound()
|
||||
.expectHeader()
|
||||
.location("/login");
|
||||
|
||||
// first login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
// third login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(thirdLoginSessionCookie.getName(), thirdLoginSessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void oauth2LoginWhenMaxSessionsThenPreventLogin() {
|
||||
OAuth2LoginConcurrentSessionsConfig.maxSessions = 1;
|
||||
OAuth2LoginConcurrentSessionsConfig.preventLogin = true;
|
||||
this.spring.register(OAuth2LoginConcurrentSessionsConfig.class).autowire();
|
||||
prepareOAuth2Config();
|
||||
// @formatter:off
|
||||
ResponseCookie sessionCookie = this.client.get()
|
||||
.uri("/login/oauth2/code/client-credentials")
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().valueEquals("Location", "/")
|
||||
.expectCookie().exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
|
||||
this.client.get()
|
||||
.uri("/login/oauth2/code/client-credentials")
|
||||
.exchange()
|
||||
.expectHeader().location("/login?error");
|
||||
|
||||
this.client.get().uri("/")
|
||||
.cookie(sessionCookie.getName(), sessionCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).isEqualTo("ok");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenUnlimitedSessionsButSessionsInvalidatedManuallyThenInvalidates() {
|
||||
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.sessionLimit = SessionLimit.UNLIMITED;
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.class).autowire();
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
|
||||
ResponseCookie firstLogin = loginReturningCookie(data);
|
||||
ResponseCookie secondLogin = loginReturningCookie(data);
|
||||
this.client.get().uri("/").cookie(firstLogin.getName(), firstLogin.getValue()).exchange().expectStatus().isOk();
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(secondLogin.getName(), secondLogin.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
ReactiveSessionRegistry sessionRegistry = this.spring.getContext().getBean(ReactiveSessionRegistry.class);
|
||||
sessionRegistry.getAllSessions(PasswordEncodedUser.user())
|
||||
.flatMap(ReactiveSessionInformation::invalidate)
|
||||
.blockLast();
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLogin.getName(), firstLogin.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isFound()
|
||||
.expectHeader()
|
||||
.location("/login");
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(secondLogin.getName(), secondLogin.getValue())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isFound()
|
||||
.expectHeader()
|
||||
.location("/login");
|
||||
}
|
||||
|
||||
@Test
|
||||
void oauth2LoginWhenMaxSessionDoesNotPreventLoginThenSecondLoginSucceedsAndFirstSessionIsInvalidated() {
|
||||
OAuth2LoginConcurrentSessionsConfig.maxSessions = 1;
|
||||
OAuth2LoginConcurrentSessionsConfig.preventLogin = false;
|
||||
this.spring.register(OAuth2LoginConcurrentSessionsConfig.class).autowire();
|
||||
prepareOAuth2Config();
|
||||
// @formatter:off
|
||||
ResponseCookie firstLoginCookie = this.client.get()
|
||||
.uri("/login/oauth2/code/client-credentials")
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().valueEquals("Location", "/")
|
||||
.expectCookie().exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
ResponseCookie secondLoginCookie = this.client.get()
|
||||
.uri("/login/oauth2/code/client-credentials")
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().valueEquals("Location", "/")
|
||||
.expectCookie().exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
|
||||
this.client.get().uri("/")
|
||||
.cookie(firstLoginCookie.getName(), firstLoginCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus().isFound()
|
||||
.expectHeader().location("/login");
|
||||
|
||||
this.client.get().uri("/")
|
||||
.cookie(secondLoginCookie.getName(), secondLoginCookie.getValue())
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).isEqualTo("ok");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginWhenAuthenticationSuccessHandlerOverriddenThenConcurrentSessionHandlersBackOff() {
|
||||
this.spring.register(ConcurrentSessionsFormLoginOverrideAuthenticationSuccessHandlerConfig.class).autowire();
|
||||
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
|
||||
data.add("username", "user");
|
||||
data.add("password", "password");
|
||||
// first login should be successful
|
||||
login(data).expectStatus().isFound().expectHeader().location("/");
|
||||
// second login should be successful, there should be no concurrent session
|
||||
// control
|
||||
login(data).expectStatus().isFound().expectHeader().location("/");
|
||||
}
|
||||
|
||||
private void prepareOAuth2Config() {
|
||||
OAuth2LoginConcurrentSessionsConfig config = this.spring.getContext()
|
||||
.getBean(OAuth2LoginConcurrentSessionsConfig.class);
|
||||
ServerAuthenticationConverter converter = config.authenticationConverter;
|
||||
ReactiveAuthenticationManager manager = config.manager;
|
||||
ServerOAuth2AuthorizationRequestResolver resolver = config.resolver;
|
||||
OAuth2AuthorizationExchange exchange = TestOAuth2AuthorizationExchanges.success();
|
||||
OAuth2User user = TestOAuth2Users.create();
|
||||
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.noScopes();
|
||||
OAuth2LoginAuthenticationToken result = new OAuth2LoginAuthenticationToken(
|
||||
TestClientRegistrations.clientRegistration().build(), exchange, user, user.getAuthorities(),
|
||||
accessToken);
|
||||
given(converter.convert(any())).willReturn(Mono.just(new TestingAuthenticationToken("a", "b", "c")));
|
||||
given(manager.authenticate(any())).willReturn(Mono.just(result));
|
||||
given(resolver.resolve(any())).willReturn(Mono.empty());
|
||||
}
|
||||
|
||||
private ResponseCookie loginReturningCookie(MultiValueMap<String, String> data) {
|
||||
return login(data).expectCookie()
|
||||
.exists("SESSION")
|
||||
.returnResult(Void.class)
|
||||
.getResponseCookies()
|
||||
.getFirst("SESSION");
|
||||
}
|
||||
|
||||
private WebTestClient.ResponseSpec login(MultiValueMap<String, String> data) {
|
||||
return this.client.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class ConcurrentSessionsMaxSessionPreventsLoginConfig {
|
||||
|
||||
static SessionLimit sessionLimit = SessionLimit.of(1);
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(sessionLimit)
|
||||
.maximumSessionsExceededHandler(new PreventLoginServerMaximumSessionsExceededHandler())
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class OAuth2LoginConcurrentSessionsConfig {
|
||||
|
||||
static int maxSessions = 1;
|
||||
|
||||
static boolean preventLogin = true;
|
||||
|
||||
ReactiveAuthenticationManager manager = mock(ReactiveAuthenticationManager.class);
|
||||
|
||||
ServerAuthenticationConverter authenticationConverter = mock(ServerAuthenticationConverter.class);
|
||||
|
||||
ServerOAuth2AuthorizationRequestResolver resolver = mock(ServerOAuth2AuthorizationRequestResolver.class);
|
||||
|
||||
ServerAuthenticationSuccessHandler successHandler = mock(ServerAuthenticationSuccessHandler.class);
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges
|
||||
.anyExchange().authenticated()
|
||||
)
|
||||
.oauth2Login((oauth2Login) -> oauth2Login
|
||||
.authenticationConverter(this.authenticationConverter)
|
||||
.authenticationManager(this.manager)
|
||||
.authorizationRequestResolver(this.resolver)
|
||||
)
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(SessionLimit.of(maxSessions))
|
||||
.maximumSessionsExceededHandler(preventLogin
|
||||
? new PreventLoginServerMaximumSessionsExceededHandler()
|
||||
: new InvalidateLeastUsedServerMaximumSessionsExceededHandler())
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
|
||||
return new InMemoryReactiveClientRegistrationRepository(
|
||||
TestClientRegistrations.clientCredentials().build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class ConcurrentSessionsMaxSessionPreventsLoginFalseConfig {
|
||||
|
||||
static SessionLimit sessionLimit = SessionLimit.of(1);
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(sessionLimit)
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class ConcurrentSessionsFormLoginOverrideAuthenticationSuccessHandlerConfig {
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
|
||||
.formLogin((login) -> login
|
||||
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/"))
|
||||
)
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(SessionLimit.of(1))
|
||||
.maximumSessionsExceededHandler(new PreventLoginServerMaximumSessionsExceededHandler())
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config.class)
|
||||
static class ConcurrentSessionsHttpBasicWithWebSessionMaxSessionPreventsLoginConfig {
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
|
||||
.httpBasic((basic) -> basic
|
||||
.securityContextRepository(new WebSessionServerSecurityContextRepository())
|
||||
)
|
||||
.sessionManagement((sessionManagement) -> sessionManagement
|
||||
.concurrentSessions((concurrentSessions) -> concurrentSessions
|
||||
.maximumSessions(SessionLimit.of(1))
|
||||
.maximumSessionsExceededHandler(new PreventLoginServerMaximumSessionsExceededHandler())
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import({ ReactiveAuthenticationTestConfiguration.class, DefaultController.class })
|
||||
static class Config {
|
||||
|
||||
@Bean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME)
|
||||
DefaultWebSessionManager webSessionManager() {
|
||||
return new DefaultWebSessionManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveSessionRegistry reactiveSessionRegistry(DefaultWebSessionManager webSessionManager) {
|
||||
return new WebSessionStoreReactiveSessionRegistry(webSessionManager.getSessionStore());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class DefaultController {
|
||||
|
||||
@GetMapping("/")
|
||||
String index() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
-37
@@ -816,41 +816,4 @@ class AuthorizeHttpRequestsDslTests {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request when ip address does not match then responds with forbidden`() {
|
||||
this.spring.register(HasIpAddressConfig::class.java).autowire()
|
||||
|
||||
this.mockMvc.perform(get("/path")
|
||||
.with { request ->
|
||||
request.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, "/error")
|
||||
request.apply {
|
||||
dispatcherType = DispatcherType.ERROR
|
||||
}
|
||||
})
|
||||
.andExpect(status().isForbidden)
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
open class HasIpAddressConfig {
|
||||
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, hasIpAddress("10.0.0.0/24"))
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@RestController
|
||||
internal class PathController {
|
||||
@RequestMapping("/path")
|
||||
fun path() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-46
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -33,7 +33,6 @@ import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
|
||||
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler
|
||||
@@ -368,50 +367,6 @@ class FormLoginDslTests {
|
||||
verify(exactly = 1) { CustomAuthenticationDetailsSourceConfig.AUTHENTICATION_DETAILS_SOURCE.buildDetails(any()) }
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
open class CustomUsernameParameterConfig {
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
formLogin {
|
||||
usernameParameter = "custom-username"
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `form login when custom username parameter then used`() {
|
||||
this.spring.register(CustomUsernameParameterConfig::class.java, UserConfig::class.java).autowire()
|
||||
|
||||
this.mockMvc.perform(formLogin().userParameter("custom-username"))
|
||||
.andExpect(authenticated())
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
open class CustomPasswordParameterConfig {
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
formLogin {
|
||||
passwordParameter = "custom-password"
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `form login when custom password parameter then used`() {
|
||||
this.spring.register(CustomPasswordParameterConfig::class.java, UserConfig::class.java).autowire()
|
||||
|
||||
this.mockMvc.perform(formLogin().passwordParam("custom-password"))
|
||||
.andExpect(authenticated())
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
open class CustomAuthenticationDetailsSourceConfig {
|
||||
|
||||
-283
@@ -1,283 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.config.web.server
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.context.ApplicationContext
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseCookie
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration
|
||||
import org.springframework.security.core.session.ReactiveSessionRegistry
|
||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler
|
||||
import org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler
|
||||
import org.springframework.security.web.server.authentication.SessionLimit
|
||||
import org.springframework.security.web.session.WebSessionStoreReactiveSessionRegistry
|
||||
import org.springframework.test.web.reactive.server.WebTestClient
|
||||
import org.springframework.util.LinkedMultiValueMap
|
||||
import org.springframework.util.MultiValueMap
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.reactive.config.EnableWebFlux
|
||||
import org.springframework.web.reactive.function.BodyInserters
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
/**
|
||||
* Tests for [ServerSessionManagementDsl]
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension::class)
|
||||
class ServerSessionManagementDslTests {
|
||||
|
||||
@JvmField
|
||||
val spring = SpringTestContext(this)
|
||||
|
||||
private lateinit var client: WebTestClient
|
||||
|
||||
@Autowired
|
||||
fun setup(context: ApplicationContext) {
|
||||
this.client = WebTestClient
|
||||
.bindToApplicationContext(context)
|
||||
.configureClient()
|
||||
.build()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login when max sessions prevent login then second login fails`() {
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginTrueConfig::class.java).autowire()
|
||||
|
||||
val data: MultiValueMap<String, String> = LinkedMultiValueMap()
|
||||
data.add("username", "user")
|
||||
data.add("password", "password")
|
||||
|
||||
val firstLoginSessionCookie = loginReturningCookie(data)
|
||||
|
||||
// second login should fail
|
||||
this.client.mutateWith(SecurityMockServerConfigurers.csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectHeader()
|
||||
.location("/login?error")
|
||||
|
||||
// first login should still be valid
|
||||
this.client.mutateWith(SecurityMockServerConfigurers.csrf())
|
||||
.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie!!.name, firstLoginSessionCookie.value)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login when max sessions does not prevent login then seconds login succeeds and first session is invalidated`() {
|
||||
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.maxSessions = 1
|
||||
this.spring.register(SessionManagementSpecTests.ConcurrentSessionsMaxSessionPreventsLoginFalseConfig::class.java)
|
||||
.autowire()
|
||||
|
||||
val data: MultiValueMap<String, String> = LinkedMultiValueMap()
|
||||
data.add("username", "user")
|
||||
data.add("password", "password")
|
||||
|
||||
val firstLoginSessionCookie = loginReturningCookie(data)
|
||||
val secondLoginSessionCookie = loginReturningCookie(data)
|
||||
|
||||
// first login should not be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie!!.name, firstLoginSessionCookie.value)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isFound()
|
||||
.expectHeader()
|
||||
.location("/login")
|
||||
|
||||
// second login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(secondLoginSessionCookie!!.name, secondLoginSessionCookie.value)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login when max sessions does not prevent login then least recently used session is invalidated`() {
|
||||
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.maxSessions = 2
|
||||
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginFalseConfig::class.java).autowire()
|
||||
val data: MultiValueMap<String, String> = LinkedMultiValueMap()
|
||||
data.add("username", "user")
|
||||
data.add("password", "password")
|
||||
val firstLoginSessionCookie = loginReturningCookie(data)
|
||||
val secondLoginSessionCookie = loginReturningCookie(data)
|
||||
|
||||
// update last access time for first request
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie!!.name, firstLoginSessionCookie.value)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
val thirdLoginSessionCookie = loginReturningCookie(data)
|
||||
|
||||
// second login should be invalid, it is the least recently used session
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(secondLoginSessionCookie!!.name, secondLoginSessionCookie.value)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isFound()
|
||||
.expectHeader()
|
||||
.location("/login")
|
||||
|
||||
// first login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(firstLoginSessionCookie.name, firstLoginSessionCookie.value)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
|
||||
// third login should be valid
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.cookie(thirdLoginSessionCookie!!.name, thirdLoginSessionCookie.value)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
}
|
||||
|
||||
private fun loginReturningCookie(data: MultiValueMap<String, String>): ResponseCookie? {
|
||||
return login(data).expectCookie()
|
||||
.exists("SESSION")
|
||||
.returnResult(Void::class.java)
|
||||
.responseCookies
|
||||
.getFirst("SESSION")
|
||||
}
|
||||
|
||||
private fun login(data: MultiValueMap<String, String>): WebTestClient.ResponseSpec {
|
||||
return client.mutateWith(SecurityMockServerConfigurers.csrf())
|
||||
.post()
|
||||
.uri("/login")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromFormData(data))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is3xxRedirection()
|
||||
.expectHeader()
|
||||
.location("/")
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config::class)
|
||||
open class ConcurrentSessionsMaxSessionPreventsLoginFalseConfig {
|
||||
|
||||
companion object {
|
||||
var maxSessions = 1
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
return http {
|
||||
authorizeExchange {
|
||||
authorize(anyExchange, authenticated)
|
||||
}
|
||||
formLogin { }
|
||||
sessionManagement {
|
||||
sessionConcurrency {
|
||||
maximumSessions = SessionLimit.of(maxSessions)
|
||||
maximumSessionsExceededHandler = InvalidateLeastUsedServerMaximumSessionsExceededHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Import(Config::class)
|
||||
open class ConcurrentSessionsMaxSessionPreventsLoginTrueConfig {
|
||||
|
||||
@Bean
|
||||
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
return http {
|
||||
authorizeExchange {
|
||||
authorize(anyExchange, authenticated)
|
||||
}
|
||||
formLogin { }
|
||||
sessionManagement {
|
||||
sessionConcurrency {
|
||||
maximumSessions = SessionLimit.of(1)
|
||||
maximumSessionsExceededHandler =
|
||||
PreventLoginServerMaximumSessionsExceededHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(
|
||||
ReactiveAuthenticationTestConfiguration::class,
|
||||
DefaultController::class
|
||||
)
|
||||
open class Config {
|
||||
|
||||
@Bean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME)
|
||||
open fun webSessionManager(): DefaultWebSessionManager {
|
||||
return DefaultWebSessionManager()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun reactiveSessionRegistry(webSessionManager: DefaultWebSessionManager): ReactiveSessionRegistry {
|
||||
return WebSessionStoreReactiveSessionRegistry(webSessionManager.sessionStore)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
open class DefaultController {
|
||||
|
||||
@GetMapping("/")
|
||||
fun index(): String {
|
||||
return "ok"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
xmlns:tx="http://www.springframework.org/schema/tx"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd
|
||||
http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd
|
||||
http://www.springframework.org/schema/security org/springframework/security/config/spring-security-6.3.xsd">
|
||||
http://www.springframework.org/schema/security org/springframework/security/config/spring-security-6.2.xsd">
|
||||
|
||||
<tx:annotation-driven />
|
||||
|
||||
|
||||
+3
-3
@@ -125,7 +125,7 @@ public interface SecurityExpressionOperations {
|
||||
* given the permission
|
||||
* @param target the target domain object to check permission on
|
||||
* @param permission the permission to check on the domain object (i.e. "read",
|
||||
* "write", etc.).
|
||||
* "write", etc).
|
||||
* @return true if permission is granted to the {@link #getAuthentication()}, else
|
||||
* false
|
||||
*/
|
||||
@@ -136,8 +136,8 @@ public interface SecurityExpressionOperations {
|
||||
* object with a given id, type, and permission.
|
||||
* @param targetId the identifier of the domain object to determine access
|
||||
* @param targetType the type (i.e. com.example.domain.Message)
|
||||
* @param permission the permission to check on the domain object (i.e. "read",
|
||||
* "write", etc.)
|
||||
* @param permission the perission to check on the domain object (i.e. "read",
|
||||
* "write", etc)
|
||||
* @return true if permission is granted to the {@link #getAuthentication()}, else
|
||||
* false
|
||||
*/
|
||||
|
||||
+32
-150
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
@@ -20,7 +20,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -31,7 +30,6 @@ import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -76,69 +74,31 @@ import org.springframework.util.Assert;
|
||||
* your intentions clearer.
|
||||
*
|
||||
* @author Michael Mayr
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class RoleHierarchyImpl implements RoleHierarchy {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(RoleHierarchyImpl.class);
|
||||
|
||||
/**
|
||||
* Raw hierarchy configuration where each line represents single or multiple level
|
||||
* role chain.
|
||||
*/
|
||||
private String roleHierarchyStringRepresentation = null;
|
||||
|
||||
/**
|
||||
* {@code rolesReachableInOneStepMap} is a Map that under the key of a specific role
|
||||
* name contains a set of all roles reachable from this role in 1 step (i.e. parsed
|
||||
* {@link #roleHierarchyStringRepresentation} grouped by the higher role)
|
||||
*/
|
||||
private Map<String, Set<GrantedAuthority>> rolesReachableInOneStepMap = null;
|
||||
|
||||
/**
|
||||
* {@code rolesReachableInOneOrMoreStepsMap} is a Map that under the key of a specific
|
||||
* role name contains a set of all roles reachable from this role in 1 or more steps
|
||||
* (i.e. fully resolved hierarchy from {@link #rolesReachableInOneStepMap})
|
||||
*/
|
||||
private Map<String, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = null;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link RoleHierarchyImpl#fromHierarchy} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public RoleHierarchyImpl() {
|
||||
|
||||
}
|
||||
|
||||
private RoleHierarchyImpl(Map<String, Set<GrantedAuthority>> hierarchy) {
|
||||
this.rolesReachableInOneOrMoreStepsMap = buildRolesReachableInOneOrMoreStepsMap(hierarchy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a role hierarchy instance with the given definition, similar to the
|
||||
* following:
|
||||
*
|
||||
* <pre>
|
||||
* ROLE_A > ROLE_B
|
||||
* ROLE_B > ROLE_AUTHENTICATED
|
||||
* ROLE_AUTHENTICATED > ROLE_UNAUTHENTICATED
|
||||
* </pre>
|
||||
* @param hierarchy the role hierarchy to use
|
||||
* @return a {@link RoleHierarchyImpl} that uses the given {@code hierarchy}
|
||||
*/
|
||||
public static RoleHierarchyImpl fromHierarchy(String hierarchy) {
|
||||
return new RoleHierarchyImpl(buildRolesReachableInOneStepMap(hierarchy));
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method that creates a {@link Builder} instance with the default role prefix
|
||||
* "ROLE_"
|
||||
* @return a {@link Builder} instance with the default role prefix "ROLE_"
|
||||
* @since 6.3
|
||||
*/
|
||||
public static Builder withDefaultRolePrefix() {
|
||||
return withRolePrefix("ROLE_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method that creates a {@link Builder} instance with the specified role
|
||||
* prefix.
|
||||
* @param rolePrefix the prefix to be used for the roles in the hierarchy.
|
||||
* @return a new {@link Builder} instance with the specified role prefix
|
||||
* @throws IllegalArgumentException if the provided role prefix is null
|
||||
* @since 6.3
|
||||
*/
|
||||
public static Builder withRolePrefix(String rolePrefix) {
|
||||
Assert.notNull(rolePrefix, "rolePrefix must not be null");
|
||||
return new Builder(rolePrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the role hierarchy and pre-calculate for every role the set of all reachable
|
||||
* roles, i.e. all roles lower in the hierarchy of every given role. Pre-calculation
|
||||
@@ -146,15 +106,13 @@ public class RoleHierarchyImpl implements RoleHierarchy {
|
||||
* time). During pre-calculation, cycles in role hierarchy are detected and will cause
|
||||
* a <tt>CycleInRoleHierarchyException</tt> to be thrown.
|
||||
* @param roleHierarchyStringRepresentation - String definition of the role hierarchy.
|
||||
* @deprecated Use {@link RoleHierarchyImpl#fromHierarchy} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public void setHierarchy(String roleHierarchyStringRepresentation) {
|
||||
this.roleHierarchyStringRepresentation = roleHierarchyStringRepresentation;
|
||||
logger.debug(LogMessage.format("setHierarchy() - The following role hierarchy was set: %s",
|
||||
roleHierarchyStringRepresentation));
|
||||
Map<String, Set<GrantedAuthority>> hierarchy = buildRolesReachableInOneStepMap(
|
||||
roleHierarchyStringRepresentation);
|
||||
this.rolesReachableInOneOrMoreStepsMap = buildRolesReachableInOneOrMoreStepsMap(hierarchy);
|
||||
buildRolesReachableInOneStepMap();
|
||||
buildRolesReachableInOneOrMoreStepsMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -198,21 +156,21 @@ public class RoleHierarchyImpl implements RoleHierarchy {
|
||||
* Parse input and build the map for the roles reachable in one step: the higher role
|
||||
* will become a key that references a set of the reachable lower roles.
|
||||
*/
|
||||
private static Map<String, Set<GrantedAuthority>> buildRolesReachableInOneStepMap(String hierarchy) {
|
||||
Map<String, Set<GrantedAuthority>> rolesReachableInOneStepMap = new HashMap<>();
|
||||
for (String line : hierarchy.split("\n")) {
|
||||
private void buildRolesReachableInOneStepMap() {
|
||||
this.rolesReachableInOneStepMap = new HashMap<>();
|
||||
for (String line : this.roleHierarchyStringRepresentation.split("\n")) {
|
||||
// Split on > and trim excessive whitespace
|
||||
String[] roles = line.trim().split("\\s+>\\s+");
|
||||
for (int i = 1; i < roles.length; i++) {
|
||||
String higherRole = roles[i - 1];
|
||||
GrantedAuthority lowerRole = new SimpleGrantedAuthority(roles[i]);
|
||||
Set<GrantedAuthority> rolesReachableInOneStepSet;
|
||||
if (!rolesReachableInOneStepMap.containsKey(higherRole)) {
|
||||
if (!this.rolesReachableInOneStepMap.containsKey(higherRole)) {
|
||||
rolesReachableInOneStepSet = new HashSet<>();
|
||||
rolesReachableInOneStepMap.put(higherRole, rolesReachableInOneStepSet);
|
||||
this.rolesReachableInOneStepMap.put(higherRole, rolesReachableInOneStepSet);
|
||||
}
|
||||
else {
|
||||
rolesReachableInOneStepSet = rolesReachableInOneStepMap.get(higherRole);
|
||||
rolesReachableInOneStepSet = this.rolesReachableInOneStepMap.get(higherRole);
|
||||
}
|
||||
rolesReachableInOneStepSet.add(lowerRole);
|
||||
logger.debug(LogMessage.format(
|
||||
@@ -220,7 +178,6 @@ public class RoleHierarchyImpl implements RoleHierarchy {
|
||||
higherRole, lowerRole));
|
||||
}
|
||||
}
|
||||
return rolesReachableInOneStepMap;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,105 +186,30 @@ public class RoleHierarchyImpl implements RoleHierarchy {
|
||||
* CycleInRoleHierarchyException if a cycle in the role hierarchy definition is
|
||||
* detected)
|
||||
*/
|
||||
private static Map<String, Set<GrantedAuthority>> buildRolesReachableInOneOrMoreStepsMap(
|
||||
Map<String, Set<GrantedAuthority>> hierarchy) {
|
||||
Map<String, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = new HashMap<>();
|
||||
private void buildRolesReachableInOneOrMoreStepsMap() {
|
||||
this.rolesReachableInOneOrMoreStepsMap = new HashMap<>();
|
||||
// iterate over all higher roles from rolesReachableInOneStepMap
|
||||
for (String roleName : hierarchy.keySet()) {
|
||||
Set<GrantedAuthority> rolesToVisitSet = new HashSet<>(hierarchy.get(roleName));
|
||||
for (String roleName : this.rolesReachableInOneStepMap.keySet()) {
|
||||
Set<GrantedAuthority> rolesToVisitSet = new HashSet<>(this.rolesReachableInOneStepMap.get(roleName));
|
||||
Set<GrantedAuthority> visitedRolesSet = new HashSet<>();
|
||||
while (!rolesToVisitSet.isEmpty()) {
|
||||
// take a role from the rolesToVisit set
|
||||
GrantedAuthority lowerRole = rolesToVisitSet.iterator().next();
|
||||
rolesToVisitSet.remove(lowerRole);
|
||||
if (!visitedRolesSet.add(lowerRole) || !hierarchy.containsKey(lowerRole.getAuthority())) {
|
||||
if (!visitedRolesSet.add(lowerRole)
|
||||
|| !this.rolesReachableInOneStepMap.containsKey(lowerRole.getAuthority())) {
|
||||
continue; // Already visited role or role with missing hierarchy
|
||||
}
|
||||
else if (roleName.equals(lowerRole.getAuthority())) {
|
||||
throw new CycleInRoleHierarchyException();
|
||||
}
|
||||
rolesToVisitSet.addAll(hierarchy.get(lowerRole.getAuthority()));
|
||||
rolesToVisitSet.addAll(this.rolesReachableInOneStepMap.get(lowerRole.getAuthority()));
|
||||
}
|
||||
rolesReachableInOneOrMoreStepsMap.put(roleName, visitedRolesSet);
|
||||
this.rolesReachableInOneOrMoreStepsMap.put(roleName, visitedRolesSet);
|
||||
logger.debug(LogMessage.format(
|
||||
"buildRolesReachableInOneOrMoreStepsMap() - From role %s one can reach %s in one or more steps.",
|
||||
roleName, visitedRolesSet));
|
||||
}
|
||||
return rolesReachableInOneOrMoreStepsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for constructing a {@link RoleHierarchyImpl} based on a hierarchical
|
||||
* role structure.
|
||||
*
|
||||
* @author Federico Herrera
|
||||
* @since 6.3
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private final String rolePrefix;
|
||||
|
||||
private final Map<String, Set<GrantedAuthority>> hierarchy;
|
||||
|
||||
private Builder(String rolePrefix) {
|
||||
this.rolePrefix = rolePrefix;
|
||||
this.hierarchy = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new hierarchy branch to define a role and its child roles.
|
||||
* @param role the highest role in this branch
|
||||
* @return a {@link ImpliedRoles} to define the child roles for the
|
||||
* <code>role</code>
|
||||
*/
|
||||
public ImpliedRoles role(String role) {
|
||||
Assert.hasText(role, "role must not be empty");
|
||||
return new ImpliedRoles(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns a {@link RoleHierarchyImpl} describing the defined role
|
||||
* hierarchy.
|
||||
* @return a {@link RoleHierarchyImpl}
|
||||
*/
|
||||
public RoleHierarchyImpl build() {
|
||||
return new RoleHierarchyImpl(this.hierarchy);
|
||||
}
|
||||
|
||||
private Builder addHierarchy(String role, String... impliedRoles) {
|
||||
Set<GrantedAuthority> withPrefix = new HashSet<>();
|
||||
for (String impliedRole : impliedRoles) {
|
||||
withPrefix.add(new SimpleGrantedAuthority(this.rolePrefix.concat(impliedRole)));
|
||||
}
|
||||
this.hierarchy.put(this.rolePrefix.concat(role), withPrefix);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for constructing child roles within a role hierarchy branch.
|
||||
*/
|
||||
public final class ImpliedRoles {
|
||||
|
||||
private final String role;
|
||||
|
||||
private ImpliedRoles(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies implied role(s) for the current role in the hierarchy.
|
||||
* @param impliedRoles role name(s) implied by the role.
|
||||
* @return the same {@link Builder} instance
|
||||
* @throws IllegalArgumentException if <code>impliedRoles</code> is null,
|
||||
* empty or contains any null element.
|
||||
*/
|
||||
public Builder implies(String... impliedRoles) {
|
||||
Assert.notEmpty(impliedRoles, "at least one implied role must be provided");
|
||||
Assert.noNullElements(impliedRoles, "implied role name(s) cannot be empty");
|
||||
return Builder.this.addHierarchy(this.role, impliedRoles);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+1
-28
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2018 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.
|
||||
@@ -23,33 +23,6 @@ import org.springframework.security.core.userdetails.cache.NullUserCache;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementation of {@link UserDetailsService} that utilizes caching through a
|
||||
* {@link UserCache}
|
||||
* <p>
|
||||
* If a null {@link UserDetails} instance is returned from
|
||||
* {@link UserCache#getUserFromCache(String)} to the {@link UserCache} got from
|
||||
* {@link #getUserCache()}, the user load is deferred to the {@link UserDetailsService}
|
||||
* provided during construction. Otherwise, the instance retrieved from the cache is
|
||||
* returned.
|
||||
* <p>
|
||||
* It is initialized with a {@link NullUserCache} by default, so it's strongly recommended
|
||||
* setting your own {@link UserCache} using {@link #setUserCache(UserCache)}, otherwise,
|
||||
* the delegate will be called every time.
|
||||
* <p>
|
||||
* Utilize this class by defining a {@link org.springframework.context.annotation.Bean}
|
||||
* that encapsulates an actual implementation of {@link UserDetailsService} and providing
|
||||
* a {@link UserCache} implementation.
|
||||
* </p>
|
||||
* For example: <pre>
|
||||
* @Bean
|
||||
* public CachingUserDetailsService cachingUserDetailsService(UserCache userCache) {
|
||||
* UserDetailsService delegate = ...;
|
||||
* CachingUserDetailsService service = new CachingUserDetailsService(delegate);
|
||||
* service.setUserCache(userCache);
|
||||
* return service;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @since 2.0
|
||||
*/
|
||||
|
||||
+3
-73
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -23,7 +23,6 @@ import java.util.List;
|
||||
* A factory class to create an {@link AuthorizationManager} instances.
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author Josh Cummings
|
||||
* @since 5.8
|
||||
*/
|
||||
public final class AuthorizationManagers {
|
||||
@@ -38,23 +37,6 @@ public final class AuthorizationManagers {
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static <T> AuthorizationManager<T> anyOf(AuthorizationManager<T>... managers) {
|
||||
return anyOf(new AuthorizationDecision(false), managers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link AuthorizationManager} that grants access if at least one
|
||||
* {@link AuthorizationManager} granted, if <code>managers</code> are empty or
|
||||
* abstained, a default {@link AuthorizationDecision} is returned.
|
||||
* @param <T> the type of object that is being authorized
|
||||
* @param allAbstainDefaultDecision the default decision if all
|
||||
* {@link AuthorizationManager}s abstained
|
||||
* @param managers the {@link AuthorizationManager}s to use
|
||||
* @return the {@link AuthorizationManager} to use
|
||||
* @since 6.3
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static <T> AuthorizationManager<T> anyOf(AuthorizationDecision allAbstainDefaultDecision,
|
||||
AuthorizationManager<T>... managers) {
|
||||
return (authentication, object) -> {
|
||||
List<AuthorizationDecision> decisions = new ArrayList<>();
|
||||
for (AuthorizationManager<T> manager : managers) {
|
||||
@@ -68,7 +50,7 @@ public final class AuthorizationManagers {
|
||||
decisions.add(decision);
|
||||
}
|
||||
if (decisions.isEmpty()) {
|
||||
return allAbstainDefaultDecision;
|
||||
return new AuthorizationDecision(false);
|
||||
}
|
||||
return new CompositeAuthorizationDecision(false, decisions);
|
||||
};
|
||||
@@ -84,23 +66,6 @@ public final class AuthorizationManagers {
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static <T> AuthorizationManager<T> allOf(AuthorizationManager<T>... managers) {
|
||||
return allOf(new AuthorizationDecision(true), managers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link AuthorizationManager} that grants access if all
|
||||
* {@link AuthorizationManager}s granted, if <code>managers</code> are empty or
|
||||
* abstained, a default {@link AuthorizationDecision} is returned.
|
||||
* @param <T> the type of object that is being authorized
|
||||
* @param allAbstainDefaultDecision the default decision if all
|
||||
* {@link AuthorizationManager}s abstained
|
||||
* @param managers the {@link AuthorizationManager}s to use
|
||||
* @return the {@link AuthorizationManager} to use
|
||||
* @since 6.3
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static <T> AuthorizationManager<T> allOf(AuthorizationDecision allAbstainDefaultDecision,
|
||||
AuthorizationManager<T>... managers) {
|
||||
return (authentication, object) -> {
|
||||
List<AuthorizationDecision> decisions = new ArrayList<>();
|
||||
for (AuthorizationManager<T> manager : managers) {
|
||||
@@ -114,31 +79,12 @@ public final class AuthorizationManagers {
|
||||
decisions.add(decision);
|
||||
}
|
||||
if (decisions.isEmpty()) {
|
||||
return allAbstainDefaultDecision;
|
||||
return new AuthorizationDecision(true);
|
||||
}
|
||||
return new CompositeAuthorizationDecision(true, decisions);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link AuthorizationManager} that reverses whatever decision the given
|
||||
* {@link AuthorizationManager} granted. If the given {@link AuthorizationManager}
|
||||
* abstains, then the returned manager also abstains.
|
||||
* @param <T> the type of object that is being authorized
|
||||
* @param manager the {@link AuthorizationManager} to reverse
|
||||
* @return the reversing {@link AuthorizationManager}
|
||||
* @since 6.3
|
||||
*/
|
||||
public static <T> AuthorizationManager<T> not(AuthorizationManager<T> manager) {
|
||||
return (authentication, object) -> {
|
||||
AuthorizationDecision decision = manager.check(authentication, object);
|
||||
if (decision == null) {
|
||||
return null;
|
||||
}
|
||||
return new NotAuthorizationDecision(decision);
|
||||
};
|
||||
}
|
||||
|
||||
private AuthorizationManagers() {
|
||||
}
|
||||
|
||||
@@ -158,20 +104,4 @@ public final class AuthorizationManagers {
|
||||
|
||||
}
|
||||
|
||||
private static final class NotAuthorizationDecision extends AuthorizationDecision {
|
||||
|
||||
private final AuthorizationDecision decision;
|
||||
|
||||
private NotAuthorizationDecision(AuthorizationDecision decision) {
|
||||
super(!decision.isGranted());
|
||||
this.decision = decision;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NotAuthorizationDecision [decision=" + this.decision + ']';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -29,7 +29,6 @@ import org.springframework.lang.NonNull;
|
||||
* For internal use only, as this contract is likely to change
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author DingHao
|
||||
*/
|
||||
abstract class AbstractExpressionAttributeRegistry<T extends ExpressionAttribute> {
|
||||
|
||||
@@ -68,8 +67,4 @@ abstract class AbstractExpressionAttributeRegistry<T extends ExpressionAttribute
|
||||
@NonNull
|
||||
abstract T resolveAttribute(Method method, Class<?> targetClass);
|
||||
|
||||
Class<?> targetClass(Method method, Class<?> targetClass) {
|
||||
return (targetClass != null) ? targetClass : method.getDeclaringClass();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+66
-43
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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,36 +17,31 @@
|
||||
package org.springframework.security.authorization.method;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Executable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationConfigurationException;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.core.annotation.RepeatableContainers;
|
||||
|
||||
/**
|
||||
* A collection of utility methods that check for, and error on, conflicting annotations.
|
||||
* This is specifically important for Spring Security annotations which are not designed
|
||||
* to be repeatable.
|
||||
* A wrapper around {@link AnnotationUtils} that checks for, and errors on, conflicting
|
||||
* annotations. This is specifically important for Spring Security annotations which are
|
||||
* not designed to be repeatable.
|
||||
*
|
||||
* <p>
|
||||
* There are numerous ways that two annotations of the same type may be attached to the
|
||||
* same method. For example, a class may implement a method defined in two separate
|
||||
* interfaces. If both of those interfaces have a {@code @PreAuthorize} annotation, then
|
||||
* it's unclear which {@code @PreAuthorize} expression Spring Security should use.
|
||||
* interfaces. If both of those interfaces have a `@PreAuthorize` annotation, then it's
|
||||
* unclear which `@PreAuthorize` expression Spring Security should use.
|
||||
*
|
||||
* <p>
|
||||
* Another way is when one of Spring Security's annotations is used as a meta-annotation.
|
||||
* In that case, two custom annotations can be declared, each with their own
|
||||
* {@code @PreAuthorize} declaration. If both custom annotations are used on the same
|
||||
* method, then it's unclear which {@code @PreAuthorize} expression Spring Security should
|
||||
* use.
|
||||
* `@PreAuthorize` declaration. If both custom annotations are used on the same method,
|
||||
* then it's unclear which `@PreAuthorize` expression Spring Security should use.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
final class AuthorizationAnnotationUtils {
|
||||
|
||||
@@ -55,17 +50,23 @@ final class AuthorizationAnnotationUtils {
|
||||
* the annotation of type {@code annotationType}, including any annotations using
|
||||
* {@code annotationType} as a meta-annotation.
|
||||
*
|
||||
* <p>
|
||||
* If more than one unique annotation is found, then throw an error.
|
||||
* If more than one is found, then throw an error.
|
||||
* @param method the method declaration to search from
|
||||
* @param annotationType the annotation type to search for
|
||||
* @return a unique instance of the annotation attributed to the method, {@code null}
|
||||
* otherwise
|
||||
* @throws AnnotationConfigurationException if more than one unique instance of the
|
||||
* @return the unique instance of the annotation attributed to the method,
|
||||
* {@code null} otherwise
|
||||
* @throws AnnotationConfigurationException if more than one instance of the
|
||||
* annotation is found
|
||||
*/
|
||||
static <A extends Annotation> A findUniqueAnnotation(Method method, Class<A> annotationType) {
|
||||
return findDistinctAnnotation(method, annotationType);
|
||||
MergedAnnotations mergedAnnotations = MergedAnnotations.from(method,
|
||||
MergedAnnotations.SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none());
|
||||
if (hasDuplicate(mergedAnnotations, annotationType)) {
|
||||
throw new AnnotationConfigurationException("Found more than one annotation of type " + annotationType
|
||||
+ " attributed to " + method
|
||||
+ " Please remove the duplicate annotations and publish a bean to handle your authorization logic.");
|
||||
}
|
||||
return AnnotationUtils.findAnnotation(method, annotationType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,38 +74,60 @@ final class AuthorizationAnnotationUtils {
|
||||
* the annotation of type {@code annotationType}, including any annotations using
|
||||
* {@code annotationType} as a meta-annotation.
|
||||
*
|
||||
* <p>
|
||||
* If more than one unique annotation is found, then throw an error.
|
||||
* If more than one is found, then throw an error.
|
||||
* @param type the type to search from
|
||||
* @param annotationType the annotation type to search for
|
||||
* @return a unique instance of the annotation attributed to the class, {@code null}
|
||||
* otherwise
|
||||
* @throws AnnotationConfigurationException if more than one unique instance of the
|
||||
* @return the unique instance of the annotation attributed to the method,
|
||||
* {@code null} otherwise
|
||||
* @throws AnnotationConfigurationException if more than one instance of the
|
||||
* annotation is found
|
||||
*/
|
||||
static <A extends Annotation> A findUniqueAnnotation(Class<?> type, Class<A> annotationType) {
|
||||
return findDistinctAnnotation(type, annotationType);
|
||||
MergedAnnotations mergedAnnotations = MergedAnnotations.from(type,
|
||||
MergedAnnotations.SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none());
|
||||
if (hasDuplicate(mergedAnnotations, annotationType)) {
|
||||
throw new AnnotationConfigurationException("Found more than one annotation of type " + annotationType
|
||||
+ " attributed to " + type
|
||||
+ " Please remove the duplicate annotations and publish a bean to handle your authorization logic.");
|
||||
}
|
||||
return AnnotationUtils.findAnnotation(type, annotationType);
|
||||
}
|
||||
|
||||
private static <A extends Annotation> A findDistinctAnnotation(AnnotatedElement annotatedElement,
|
||||
private static <A extends Annotation> boolean hasDuplicate(MergedAnnotations mergedAnnotations,
|
||||
Class<A> annotationType) {
|
||||
MergedAnnotations mergedAnnotations = MergedAnnotations.from(annotatedElement, SearchStrategy.TYPE_HIERARCHY,
|
||||
RepeatableContainers.none());
|
||||
MergedAnnotation<Annotation> alreadyFound = null;
|
||||
for (MergedAnnotation<Annotation> mergedAnnotation : mergedAnnotations) {
|
||||
if (isSynthetic(mergedAnnotation.getSource())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<A> annotations = mergedAnnotations.stream(annotationType)
|
||||
.map(MergedAnnotation::withNonMergedAttributes)
|
||||
.map(MergedAnnotation::synthesize)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (mergedAnnotation.getType() != annotationType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return switch (annotations.size()) {
|
||||
case 0 -> null;
|
||||
case 1 -> annotations.get(0);
|
||||
default -> throw new AnnotationConfigurationException("""
|
||||
Please ensure there is one unique annotation of type @%s attributed to %s. \
|
||||
Found %d competing annotations: %s""".formatted(annotationType.getName(), annotatedElement,
|
||||
annotations.size(), annotations));
|
||||
};
|
||||
if (alreadyFound == null) {
|
||||
alreadyFound = mergedAnnotation;
|
||||
continue;
|
||||
}
|
||||
|
||||
// https://github.com/spring-projects/spring-framework/issues/31803
|
||||
if (!mergedAnnotation.getSource().equals(alreadyFound.getSource())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mergedAnnotation.getRoot().getType() != alreadyFound.getRoot().getType()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isSynthetic(Object object) {
|
||||
if (object instanceof Executable) {
|
||||
return ((Executable) object).isSynthetic();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private AuthorizationAnnotationUtils() {
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -44,7 +44,6 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author Josh Cummings
|
||||
* @author DingHao
|
||||
* @since 5.6
|
||||
*/
|
||||
public final class Jsr250AuthorizationManager implements AuthorizationManager<MethodInvocation> {
|
||||
@@ -122,8 +121,7 @@ public final class Jsr250AuthorizationManager implements AuthorizationManager<Me
|
||||
private Annotation findJsr250Annotation(Method method, Class<?> targetClass) {
|
||||
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
|
||||
Annotation annotation = findAnnotation(specificMethod);
|
||||
return (annotation != null) ? annotation
|
||||
: findAnnotation((targetClass != null) ? targetClass : specificMethod.getDeclaringClass());
|
||||
return (annotation != null) ? annotation : findAnnotation(specificMethod.getDeclaringClass());
|
||||
}
|
||||
|
||||
private Annotation findAnnotation(Method method) {
|
||||
|
||||
+5
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -31,7 +31,6 @@ import org.springframework.util.Assert;
|
||||
* For internal use only, as this contract is likely to change.
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author DingHao
|
||||
* @since 5.8
|
||||
*/
|
||||
final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
|
||||
@@ -55,7 +54,7 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA
|
||||
@Override
|
||||
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
|
||||
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
|
||||
PostAuthorize postAuthorize = findPostAuthorizeAnnotation(specificMethod, targetClass);
|
||||
PostAuthorize postAuthorize = findPostAuthorizeAnnotation(specificMethod);
|
||||
if (postAuthorize == null) {
|
||||
return ExpressionAttribute.NULL_ATTRIBUTE;
|
||||
}
|
||||
@@ -64,10 +63,10 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA
|
||||
return new ExpressionAttribute(postAuthorizeExpression);
|
||||
}
|
||||
|
||||
private PostAuthorize findPostAuthorizeAnnotation(Method method, Class<?> targetClass) {
|
||||
private PostAuthorize findPostAuthorizeAnnotation(Method method) {
|
||||
PostAuthorize postAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PostAuthorize.class);
|
||||
return (postAuthorize != null) ? postAuthorize : AuthorizationAnnotationUtils
|
||||
.findUniqueAnnotation(targetClass(method, targetClass), PostAuthorize.class);
|
||||
return (postAuthorize != null) ? postAuthorize
|
||||
: AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PostAuthorize.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -30,7 +30,6 @@ import org.springframework.util.Assert;
|
||||
* For internal use only, as this contract is likely to change.
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author DingHao
|
||||
* @since 5.8
|
||||
*/
|
||||
final class PostFilterExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
|
||||
@@ -54,7 +53,7 @@ final class PostFilterExpressionAttributeRegistry extends AbstractExpressionAttr
|
||||
@Override
|
||||
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
|
||||
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
|
||||
PostFilter postFilter = findPostFilterAnnotation(specificMethod, targetClass);
|
||||
PostFilter postFilter = findPostFilterAnnotation(specificMethod);
|
||||
if (postFilter == null) {
|
||||
return ExpressionAttribute.NULL_ATTRIBUTE;
|
||||
}
|
||||
@@ -63,10 +62,10 @@ final class PostFilterExpressionAttributeRegistry extends AbstractExpressionAttr
|
||||
return new ExpressionAttribute(postFilterExpression);
|
||||
}
|
||||
|
||||
private PostFilter findPostFilterAnnotation(Method method, Class<?> targetClass) {
|
||||
private PostFilter findPostFilterAnnotation(Method method) {
|
||||
PostFilter postFilter = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PostFilter.class);
|
||||
return (postFilter != null) ? postFilter
|
||||
: AuthorizationAnnotationUtils.findUniqueAnnotation(targetClass(method, targetClass), PostFilter.class);
|
||||
: AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PostFilter.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -31,7 +31,6 @@ import org.springframework.util.Assert;
|
||||
* For internal use only, as this contract is likely to change.
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author DingHao
|
||||
* @since 5.8
|
||||
*/
|
||||
final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
|
||||
@@ -59,7 +58,7 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt
|
||||
@Override
|
||||
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
|
||||
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
|
||||
PreAuthorize preAuthorize = findPreAuthorizeAnnotation(specificMethod, targetClass);
|
||||
PreAuthorize preAuthorize = findPreAuthorizeAnnotation(specificMethod);
|
||||
if (preAuthorize == null) {
|
||||
return ExpressionAttribute.NULL_ATTRIBUTE;
|
||||
}
|
||||
@@ -68,10 +67,10 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt
|
||||
return new ExpressionAttribute(preAuthorizeExpression);
|
||||
}
|
||||
|
||||
private PreAuthorize findPreAuthorizeAnnotation(Method method, Class<?> targetClass) {
|
||||
private PreAuthorize findPreAuthorizeAnnotation(Method method) {
|
||||
PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class);
|
||||
return (preAuthorize != null) ? preAuthorize : AuthorizationAnnotationUtils
|
||||
.findUniqueAnnotation(targetClass(method, targetClass), PreAuthorize.class);
|
||||
return (preAuthorize != null) ? preAuthorize
|
||||
: AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PreAuthorize.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -30,7 +30,6 @@ import org.springframework.util.Assert;
|
||||
* For internal use only, as this contract is likely to change.
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author DingHao
|
||||
* @since 5.8
|
||||
*/
|
||||
final class PreFilterExpressionAttributeRegistry
|
||||
@@ -55,7 +54,7 @@ final class PreFilterExpressionAttributeRegistry
|
||||
@Override
|
||||
PreFilterExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
|
||||
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
|
||||
PreFilter preFilter = findPreFilterAnnotation(specificMethod, targetClass);
|
||||
PreFilter preFilter = findPreFilterAnnotation(specificMethod);
|
||||
if (preFilter == null) {
|
||||
return PreFilterExpressionAttribute.NULL_ATTRIBUTE;
|
||||
}
|
||||
@@ -64,10 +63,10 @@ final class PreFilterExpressionAttributeRegistry
|
||||
return new PreFilterExpressionAttribute(preFilterExpression, preFilter.filterTarget());
|
||||
}
|
||||
|
||||
private PreFilter findPreFilterAnnotation(Method method, Class<?> targetClass) {
|
||||
private PreFilter findPreFilterAnnotation(Method method) {
|
||||
PreFilter preFilter = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreFilter.class);
|
||||
return (preFilter != null) ? preFilter
|
||||
: AuthorizationAnnotationUtils.findUniqueAnnotation(targetClass(method, targetClass), PreFilter.class);
|
||||
: AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PreFilter.class);
|
||||
}
|
||||
|
||||
static final class PreFilterExpressionAttribute extends ExpressionAttribute {
|
||||
|
||||
+5
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -41,7 +41,6 @@ import org.springframework.util.Assert;
|
||||
* contains a specified authority from the Spring Security's {@link Secured} annotation.
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author DingHao
|
||||
* @since 5.6
|
||||
*/
|
||||
public final class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
|
||||
@@ -87,14 +86,14 @@ public final class SecuredAuthorizationManager implements AuthorizationManager<M
|
||||
|
||||
private Set<String> resolveAuthorities(Method method, Class<?> targetClass) {
|
||||
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
|
||||
Secured secured = findSecuredAnnotation(specificMethod, targetClass);
|
||||
Secured secured = findSecuredAnnotation(specificMethod);
|
||||
return (secured != null) ? Set.of(secured.value()) : Collections.emptySet();
|
||||
}
|
||||
|
||||
private Secured findSecuredAnnotation(Method method, Class<?> targetClass) {
|
||||
private Secured findSecuredAnnotation(Method method) {
|
||||
Secured secured = AuthorizationAnnotationUtils.findUniqueAnnotation(method, Secured.class);
|
||||
return (secured != null) ? secured : AuthorizationAnnotationUtils
|
||||
.findUniqueAnnotation((targetClass != null) ? targetClass : method.getDeclaringClass(), Secured.class);
|
||||
return (secured != null) ? secured
|
||||
: AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), Secured.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2017 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.
|
||||
@@ -39,6 +39,9 @@ public final class SpringSecurityCoreVersion {
|
||||
|
||||
/**
|
||||
* Global Serialization value for Spring Security classes.
|
||||
*
|
||||
* N.B. Classes are not intended to be serializable between different versions. See
|
||||
* SEC-1709 for why we still need a serial version.
|
||||
*/
|
||||
public static final long SERIAL_VERSION_UID = 620L;
|
||||
|
||||
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* 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.core.session;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Provides an in-memory implementation of {@link ReactiveSessionRegistry}.
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
* @since 6.3
|
||||
*/
|
||||
public class InMemoryReactiveSessionRegistry implements ReactiveSessionRegistry {
|
||||
|
||||
private final ConcurrentMap<Object, Set<String>> sessionIdsByPrincipal;
|
||||
|
||||
private final Map<String, ReactiveSessionInformation> sessionById;
|
||||
|
||||
public InMemoryReactiveSessionRegistry() {
|
||||
this.sessionIdsByPrincipal = new ConcurrentHashMap<>();
|
||||
this.sessionById = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
public InMemoryReactiveSessionRegistry(ConcurrentMap<Object, Set<String>> sessionIdsByPrincipal,
|
||||
Map<String, ReactiveSessionInformation> sessionById) {
|
||||
this.sessionIdsByPrincipal = sessionIdsByPrincipal;
|
||||
this.sessionById = sessionById;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ReactiveSessionInformation> getAllSessions(Object principal) {
|
||||
return Flux.fromIterable(this.sessionIdsByPrincipal.getOrDefault(principal, Collections.emptySet()))
|
||||
.map(this.sessionById::get);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> saveSessionInformation(ReactiveSessionInformation information) {
|
||||
this.sessionById.put(information.getSessionId(), information);
|
||||
this.sessionIdsByPrincipal.computeIfAbsent(information.getPrincipal(), (key) -> new CopyOnWriteArraySet<>())
|
||||
.add(information.getSessionId());
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ReactiveSessionInformation> getSessionInformation(String sessionId) {
|
||||
return Mono.justOrEmpty(this.sessionById.get(sessionId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ReactiveSessionInformation> removeSessionInformation(String sessionId) {
|
||||
return getSessionInformation(sessionId).doOnNext((sessionInformation) -> {
|
||||
this.sessionById.remove(sessionId);
|
||||
Set<String> sessionsUsedByPrincipal = this.sessionIdsByPrincipal.get(sessionInformation.getPrincipal());
|
||||
if (sessionsUsedByPrincipal != null) {
|
||||
sessionsUsedByPrincipal.remove(sessionId);
|
||||
if (sessionsUsedByPrincipal.isEmpty()) {
|
||||
this.sessionIdsByPrincipal.remove(sessionInformation.getPrincipal());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ReactiveSessionInformation> updateLastAccessTime(String sessionId) {
|
||||
ReactiveSessionInformation session = this.sessionById.get(sessionId);
|
||||
if (session != null) {
|
||||
return session.refreshLastRequest().thenReturn(session);
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
}
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.core.session;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
public class ReactiveSessionInformation implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||
|
||||
private Instant lastAccessTime;
|
||||
|
||||
private final Object principal;
|
||||
|
||||
private final String sessionId;
|
||||
|
||||
private boolean expired = false;
|
||||
|
||||
public ReactiveSessionInformation(Object principal, String sessionId, Instant lastAccessTime) {
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
Assert.hasText(sessionId, "sessionId cannot be null");
|
||||
Assert.notNull(lastAccessTime, "lastAccessTime cannot be null");
|
||||
this.principal = principal;
|
||||
this.sessionId = sessionId;
|
||||
this.lastAccessTime = lastAccessTime;
|
||||
}
|
||||
|
||||
public ReactiveSessionInformation withSessionId(String sessionId) {
|
||||
return new ReactiveSessionInformation(this.principal, sessionId, this.lastAccessTime);
|
||||
}
|
||||
|
||||
public Mono<Void> invalidate() {
|
||||
return Mono.fromRunnable(() -> this.expired = true);
|
||||
}
|
||||
|
||||
public Mono<Void> refreshLastRequest() {
|
||||
this.lastAccessTime = Instant.now();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
public Instant getLastAccessTime() {
|
||||
return this.lastAccessTime;
|
||||
}
|
||||
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return this.expired;
|
||||
}
|
||||
|
||||
public void setLastAccessTime(Instant lastAccessTime) {
|
||||
this.lastAccessTime = lastAccessTime;
|
||||
}
|
||||
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* 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.core.session;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Maintains a registry of {@link ReactiveSessionInformation} instances.
|
||||
*
|
||||
* @author Marcus da Coregio
|
||||
* @since 6.3
|
||||
*/
|
||||
public interface ReactiveSessionRegistry {
|
||||
|
||||
/**
|
||||
* Gets all the known {@link ReactiveSessionInformation} instances for the specified
|
||||
* principal.
|
||||
* @param principal the principal
|
||||
* @return the {@link ReactiveSessionInformation} instances associated with the
|
||||
* principal
|
||||
*/
|
||||
Flux<ReactiveSessionInformation> getAllSessions(Object principal);
|
||||
|
||||
/**
|
||||
* Saves the {@link ReactiveSessionInformation}
|
||||
* @param information the {@link ReactiveSessionInformation} to save
|
||||
* @return a {@link Mono} that completes when the session is saved
|
||||
*/
|
||||
Mono<Void> saveSessionInformation(ReactiveSessionInformation information);
|
||||
|
||||
/**
|
||||
* Gets the {@link ReactiveSessionInformation} for the specified session identifier.
|
||||
* @param sessionId the session identifier
|
||||
* @return the {@link ReactiveSessionInformation} for the session.
|
||||
*/
|
||||
Mono<ReactiveSessionInformation> getSessionInformation(String sessionId);
|
||||
|
||||
/**
|
||||
* Removes the specified session from the registry.
|
||||
* @param sessionId the session identifier
|
||||
* @return a {@link Mono} that completes when the session is removed
|
||||
*/
|
||||
Mono<ReactiveSessionInformation> removeSessionInformation(String sessionId);
|
||||
|
||||
/**
|
||||
* Updates the last accessed time of the {@link ReactiveSessionInformation}
|
||||
* @param sessionId the session identifier
|
||||
* @return a {@link Mono} that completes when the session is updated
|
||||
*/
|
||||
Mono<ReactiveSessionInformation> updateLastAccessTime(String sessionId);
|
||||
|
||||
}
|
||||
@@ -67,18 +67,14 @@ public interface UserDetails extends Serializable {
|
||||
* @return <code>true</code> if the user's account is valid (ie non-expired),
|
||||
* <code>false</code> if no longer valid (ie expired)
|
||||
*/
|
||||
default boolean isAccountNonExpired() {
|
||||
return true;
|
||||
}
|
||||
boolean isAccountNonExpired();
|
||||
|
||||
/**
|
||||
* Indicates whether the user is locked or unlocked. A locked user cannot be
|
||||
* authenticated.
|
||||
* @return <code>true</code> if the user is not locked, <code>false</code> otherwise
|
||||
*/
|
||||
default boolean isAccountNonLocked() {
|
||||
return true;
|
||||
}
|
||||
boolean isAccountNonLocked();
|
||||
|
||||
/**
|
||||
* Indicates whether the user's credentials (password) has expired. Expired
|
||||
@@ -86,17 +82,13 @@ public interface UserDetails extends Serializable {
|
||||
* @return <code>true</code> if the user's credentials are valid (ie non-expired),
|
||||
* <code>false</code> if no longer valid (ie expired)
|
||||
*/
|
||||
default boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
boolean isCredentialsNonExpired();
|
||||
|
||||
/**
|
||||
* Indicates whether the user is enabled or disabled. A disabled user cannot be
|
||||
* authenticated.
|
||||
* @return <code>true</code> if the user is enabled, <code>false</code> otherwise
|
||||
*/
|
||||
default boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
boolean isEnabled();
|
||||
|
||||
}
|
||||
|
||||
@@ -28,11 +28,11 @@ DigestAuthenticationFilter.nonceExpired=El nonce ha expirat/ha arribat fora de t
|
||||
DigestAuthenticationFilter.nonceNotNumeric=El nonce token haur\u00eda d'haver produ\u00eft un token num\u00e8ric inicial, per\u00f2 era {0}
|
||||
DigestAuthenticationFilter.nonceNotTwoTokens=El nonce hauria de produ\u00efr dos tokens, i no {0}
|
||||
DigestAuthenticationFilter.usernameNotFound=No s'ha trobat el nom d'usuari {0}
|
||||
ExceptionTranslationFilter.insufficientAuthentication=Per accedir a aquest recurs cal autenticaci\u00f3 completa
|
||||
#ExceptionTranslationFilter.insufficientAuthentication=Full authentication is required to access this resource
|
||||
JdbcDaoImpl.noAuthority=L'usuari {0} no t\u00e9 GrantedAuthority
|
||||
JdbcDaoImpl.notFound=No s'ha trobat l'usuari {0}
|
||||
LdapAuthenticationProvider.badCredentials=Credencials err\u00f2nies
|
||||
LdapAuthenticationProvider.badLdapConnection=Ha fallat la connexi\u00f3 al servidor LDAP
|
||||
#LdapAuthenticationProvider.badLdapConnection=Connection to LDAP server failed
|
||||
LdapAuthenticationProvider.credentialsExpired=Les credencials d'usuari han expirat
|
||||
LdapAuthenticationProvider.disabled=L'usuari est\u00e0 deshabilitat
|
||||
LdapAuthenticationProvider.expired=El compte d'usuari ha expirat
|
||||
|
||||
@@ -32,7 +32,7 @@ ExceptionTranslationFilter.insufficientAuthentication=Para acceder a este recurs
|
||||
JdbcDaoImpl.noAuthority=Usuario {0} no tiene GrantedAuthority
|
||||
JdbcDaoImpl.notFound=Usuario {0} no encontrado
|
||||
LdapAuthenticationProvider.badCredentials=Credenciales err\u00F3neas
|
||||
LdapAuthenticationProvider.badLdapConnection=Fall\u00F3 la conexi\u00F3n al servidor LDAP
|
||||
#LdapAuthenticationProvider.badLdapConnection=Connection to LDAP server failed
|
||||
LdapAuthenticationProvider.credentialsExpired=Las credenciales del usuario han expirado
|
||||
LdapAuthenticationProvider.disabled=El usuario est\u00E1 deshabilitado
|
||||
LdapAuthenticationProvider.expired=La cuenta del usuario ha expirado
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -25,7 +25,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreAuthorize("hasRole('USER')")
|
||||
@RolesAllowed("USER")
|
||||
@RolesAllowed("ADMIN")
|
||||
@Secured("USER")
|
||||
public @interface RequireUserRole {
|
||||
|
||||
|
||||
-96
@@ -26,7 +26,6 @@ import org.springframework.security.core.authority.AuthorityUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
@@ -206,99 +205,4 @@ public class RoleHierarchyImplTests {
|
||||
.containsExactlyInAnyOrderElementsOf(allAuthorities);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromHierarchyWithTextBlock() {
|
||||
RoleHierarchyImpl roleHierarchyImpl = RoleHierarchyImpl.fromHierarchy("""
|
||||
ROLE_A > ROLE_B
|
||||
ROLE_B > ROLE_C
|
||||
ROLE_B > ROLE_D
|
||||
""");
|
||||
List<GrantedAuthority> flatAuthorities = AuthorityUtils.createAuthorityList("ROLE_A");
|
||||
List<GrantedAuthority> allAuthorities = AuthorityUtils.createAuthorityList("ROLE_A", "ROLE_B", "ROLE_C",
|
||||
"ROLE_D");
|
||||
|
||||
assertThat(roleHierarchyImpl).isNotNull();
|
||||
assertThat(roleHierarchyImpl.getReachableGrantedAuthorities(flatAuthorities))
|
||||
.containsExactlyInAnyOrderElementsOf(allAuthorities);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromHierarchyNoCycles() {
|
||||
assertThatNoException().isThrownBy(() -> RoleHierarchyImpl
|
||||
.fromHierarchy("ROLE_A > ROLE_B\nROLE_A > ROLE_C\nROLE_C > ROLE_D\nROLE_B > ROLE_D"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromHierarchyCycles() {
|
||||
assertThatExceptionOfType(CycleInRoleHierarchyException.class)
|
||||
.isThrownBy(() -> RoleHierarchyImpl.fromHierarchy("ROLE_A > ROLE_A"));
|
||||
assertThatExceptionOfType(CycleInRoleHierarchyException.class)
|
||||
.isThrownBy(() -> RoleHierarchyImpl.fromHierarchy("ROLE_A > ROLE_B\nROLE_B > ROLE_A"));
|
||||
assertThatExceptionOfType(CycleInRoleHierarchyException.class)
|
||||
.isThrownBy(() -> RoleHierarchyImpl.fromHierarchy("ROLE_A > ROLE_B\nROLE_B > ROLE_C\nROLE_C > ROLE_A"));
|
||||
assertThatExceptionOfType(CycleInRoleHierarchyException.class).isThrownBy(() -> RoleHierarchyImpl
|
||||
.fromHierarchy("ROLE_A > ROLE_B\nROLE_B > ROLE_C\nROLE_C > ROLE_E\nROLE_E > ROLE_D\nROLE_D > ROLE_B"));
|
||||
assertThatExceptionOfType(CycleInRoleHierarchyException.class)
|
||||
.isThrownBy(() -> RoleHierarchyImpl.fromHierarchy("ROLE_C > ROLE_B\nROLE_B > ROLE_A\nROLE_A > ROLE_B"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuilderWithDefaultRolePrefix() {
|
||||
RoleHierarchyImpl roleHierarchyImpl = RoleHierarchyImpl.withDefaultRolePrefix()
|
||||
.role("A")
|
||||
.implies("B")
|
||||
.role("B")
|
||||
.implies("C", "D")
|
||||
.build();
|
||||
List<GrantedAuthority> flatAuthorities = AuthorityUtils.createAuthorityList("ROLE_A");
|
||||
List<GrantedAuthority> allAuthorities = AuthorityUtils.createAuthorityList("ROLE_A", "ROLE_B", "ROLE_C",
|
||||
"ROLE_D");
|
||||
|
||||
assertThat(roleHierarchyImpl).isNotNull();
|
||||
assertThat(roleHierarchyImpl.getReachableGrantedAuthorities(flatAuthorities))
|
||||
.containsExactlyInAnyOrderElementsOf(allAuthorities);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuilderWithRolePrefix() {
|
||||
RoleHierarchyImpl roleHierarchyImpl = RoleHierarchyImpl.withRolePrefix("CUSTOM_PREFIX_")
|
||||
.role("A")
|
||||
.implies("B")
|
||||
.build();
|
||||
List<GrantedAuthority> flatAuthorities = AuthorityUtils.createAuthorityList("CUSTOM_PREFIX_A");
|
||||
List<GrantedAuthority> allAuthorities = AuthorityUtils.createAuthorityList("CUSTOM_PREFIX_A",
|
||||
"CUSTOM_PREFIX_B");
|
||||
|
||||
assertThat(roleHierarchyImpl).isNotNull();
|
||||
assertThat(roleHierarchyImpl.getReachableGrantedAuthorities(flatAuthorities))
|
||||
.containsExactlyInAnyOrderElementsOf(allAuthorities);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuilderThrowIllegalArgumentExceptionWhenPrefixRoleNull() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> RoleHierarchyImpl.withRolePrefix(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuilderThrowIllegalArgumentExceptionWhenRoleEmpty() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> RoleHierarchyImpl.withDefaultRolePrefix().role(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuilderThrowIllegalArgumentExceptionWhenRoleNull() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> RoleHierarchyImpl.withDefaultRolePrefix().role(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuilderThrowIllegalArgumentExceptionWhenImpliedRolesNull() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RoleHierarchyImpl.withDefaultRolePrefix().role("A").implies((String) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuilderThrowIllegalArgumentExceptionWhenImpliedRolesEmpty() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RoleHierarchyImpl.withDefaultRolePrefix().role("A").implies());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+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
|
||||
|
||||
+3
-3
@@ -35,14 +35,14 @@ public class TestAuthentication extends PasswordEncodedUser {
|
||||
AuthorityUtils.createAuthorityList("ROLE_USER"));
|
||||
|
||||
public static Authentication authenticatedAdmin() {
|
||||
return authenticated(admin());
|
||||
return autheticated(admin());
|
||||
}
|
||||
|
||||
public static Authentication authenticatedUser() {
|
||||
return authenticated(user());
|
||||
return autheticated(user());
|
||||
}
|
||||
|
||||
public static Authentication authenticated(UserDetails user) {
|
||||
public static Authentication autheticated(UserDetails user) {
|
||||
return UsernamePasswordAuthenticationToken.authenticated(user, null, user.getAuthorities());
|
||||
}
|
||||
|
||||
|
||||
+1
-150
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -36,16 +36,6 @@ class AuthorizationManagersTests {
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAnyOfWithAllAbstainDefaultDecisionWhenOneGrantedThenGrantedDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = new AuthorizationDecision(false);
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.anyOf(allAbstainDefaultDecision,
|
||||
(a, o) -> new AuthorizationDecision(false), (a, o) -> new AuthorizationDecision(true));
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNotNull();
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
// gh-13069
|
||||
@Test
|
||||
void checkAnyOfWhenAllNonAbstainingDeniesThenDeniedDecision() {
|
||||
@@ -64,58 +54,6 @@ class AuthorizationManagersTests {
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAnyOfWithAllAbstainDefaultDecisionIsDeniedWhenEmptyThenDeniedDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = new AuthorizationDecision(false);
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.anyOf(allAbstainDefaultDecision);
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNotNull();
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAnyOfWithAllAbstainDefaultDecisionIsGrantedWhenEmptyThenGrantedDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = new AuthorizationDecision(true);
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.anyOf(allAbstainDefaultDecision);
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNotNull();
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAnyOfWithAllAbstainDefaultDecisionIsAbstainWhenEmptyThenAbstainDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = null;
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.anyOf(allAbstainDefaultDecision);
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAnyOfWhenAllAbstainDefaultDecisionIsGrantedAndAllManagersAbstainThenGrantedDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = new AuthorizationDecision(true);
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.anyOf(allAbstainDefaultDecision, (a, o) -> null);
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNotNull();
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAnyOfWhenAllAbstainDefaultDecisionIsDeniedAndAllManagersAbstainThenDeniedDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = new AuthorizationDecision(false);
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.anyOf(allAbstainDefaultDecision, (a, o) -> null);
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNotNull();
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAnyOfWhenAllAbstainDefaultDecisionIsAbstainAndAllManagersAbstainThenAbstainDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = null;
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.anyOf(allAbstainDefaultDecision, (a, o) -> null);
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAllOfWhenAllGrantedThenGrantedDecision() {
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.allOf((a, o) -> new AuthorizationDecision(true),
|
||||
@@ -125,16 +63,6 @@ class AuthorizationManagersTests {
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAllOfWithAllAbstainDefaultDecisionWhenAllGrantedThenGrantedDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = new AuthorizationDecision(false);
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.allOf(allAbstainDefaultDecision,
|
||||
(a, o) -> new AuthorizationDecision(true), (a, o) -> new AuthorizationDecision(true));
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNotNull();
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
// gh-13069
|
||||
@Test
|
||||
void checkAllOfWhenAllNonAbstainingGrantsThenGrantedDecision() {
|
||||
@@ -154,16 +82,6 @@ class AuthorizationManagersTests {
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAllOfWithAllAbstainDefaultDecisionWhenOneDeniedThenDeniedDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = new AuthorizationDecision(true);
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.allOf(allAbstainDefaultDecision,
|
||||
(a, o) -> new AuthorizationDecision(true), (a, o) -> new AuthorizationDecision(false));
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNotNull();
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAllOfWhenEmptyThenGrantedDecision() {
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.allOf();
|
||||
@@ -172,71 +90,4 @@ class AuthorizationManagersTests {
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAllOfWithAllAbstainDefaultDecisionIsDeniedWhenEmptyThenDeniedDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = new AuthorizationDecision(false);
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.allOf(allAbstainDefaultDecision);
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNotNull();
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAllOfWithAllAbstainDefaultDecisionIsGrantedWhenEmptyThenGrantedDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = new AuthorizationDecision(true);
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.allOf(allAbstainDefaultDecision);
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNotNull();
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAllOfWithAllAbstainDefaultDecisionIsAbstainWhenEmptyThenAbstainDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = null;
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.allOf(allAbstainDefaultDecision);
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAllOfWhenAllAbstainDefaultDecisionIsDeniedAndAllManagersAbstainThenDeniedDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = new AuthorizationDecision(false);
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.allOf(allAbstainDefaultDecision, (a, o) -> null);
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNotNull();
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAllOfWhenAllAbstainDefaultDecisionIsGrantedAndAllManagersAbstainThenGrantedDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = new AuthorizationDecision(true);
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.allOf(allAbstainDefaultDecision, (a, o) -> null);
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNotNull();
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAllOfWhenAllAbstainDefaultDecisionIsAbstainAndAllManagersAbstainThenAbstainDecision() {
|
||||
AuthorizationDecision allAbstainDefaultDecision = null;
|
||||
AuthorizationManager<?> composed = AuthorizationManagers.allOf(allAbstainDefaultDecision, (a, o) -> null);
|
||||
AuthorizationDecision decision = composed.check(null, null);
|
||||
assertThat(decision).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkNotWhenEmptyThenAbstainedDecision() {
|
||||
AuthorizationManager<?> negated = AuthorizationManagers.not((a, o) -> null);
|
||||
AuthorizationDecision decision = negated.check(null, null);
|
||||
assertThat(decision).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkNotWhenGrantedThenDeniedDecision() {
|
||||
AuthorizationManager<?> negated = AuthorizationManagers.not((a, o) -> new AuthorizationDecision(true));
|
||||
AuthorizationDecision decision = negated.check(null, null);
|
||||
assertThat(decision).isNotNull();
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
-113
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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,26 +16,18 @@
|
||||
|
||||
package org.springframework.security.authorization.method;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.core.annotation.AnnotationConfigurationException;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link AuthorizationAnnotationUtils}.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @author Sam Brannen
|
||||
* Tests for {@link AuthorizationAnnotationUtils}
|
||||
*/
|
||||
class AuthorizationAnnotationUtilsTests {
|
||||
|
||||
@@ -45,56 +37,15 @@ class AuthorizationAnnotationUtilsTests {
|
||||
Thread.currentThread().getContextClassLoader(), new Class[] { StringRepository.class },
|
||||
(p, m, args) -> null);
|
||||
Method method = proxy.getClass().getDeclaredMethod("findAll");
|
||||
PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class);
|
||||
assertThat(preAuthorize.value()).isEqualTo("hasRole('someRole')");
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class));
|
||||
}
|
||||
|
||||
@Test // gh-13625
|
||||
void annotationsFromSuperSuperInterfaceShouldNotTriggerAnnotationConfigurationException() throws Exception {
|
||||
Method method = HelloImpl.class.getDeclaredMethod("sayHello");
|
||||
PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class);
|
||||
assertThat(preAuthorize.value()).isEqualTo("hasRole('someRole')");
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleIdenticalAnnotationsOnClassShouldNotTriggerAnnotationConfigurationException() {
|
||||
Class<?> clazz = MultipleIdenticalPreAuthorizeAnnotationsOnClass.class;
|
||||
PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(clazz, PreAuthorize.class);
|
||||
assertThat(preAuthorize.value()).isEqualTo("hasRole('someRole')");
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleIdenticalAnnotationsOnMethodShouldNotTriggerAnnotationConfigurationException() throws Exception {
|
||||
Method method = MultipleIdenticalPreAuthorizeAnnotationsOnMethod.class.getDeclaredMethod("method");
|
||||
PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class);
|
||||
assertThat(preAuthorize.value()).isEqualTo("hasRole('someRole')");
|
||||
}
|
||||
|
||||
@Test
|
||||
void competingAnnotationsOnClassShouldTriggerAnnotationConfigurationException() {
|
||||
Class<?> clazz = CompetingPreAuthorizeAnnotationsOnClass.class;
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class)
|
||||
.isThrownBy(() -> AuthorizationAnnotationUtils.findUniqueAnnotation(clazz, PreAuthorize.class))
|
||||
.withMessageContainingAll("Found 2 competing annotations:", "someRole", "otherRole");
|
||||
}
|
||||
|
||||
@Test
|
||||
void competingAnnotationsOnMethodShouldTriggerAnnotationConfigurationException() throws Exception {
|
||||
Method method = CompetingPreAuthorizeAnnotationsOnMethod.class.getDeclaredMethod("method");
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class)
|
||||
.isThrownBy(() -> AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class))
|
||||
.withMessageContainingAll("Found 2 competing annotations:", "someRole", "otherRole");
|
||||
}
|
||||
|
||||
@Test
|
||||
void composedMergedAnnotationsAreNotSupported() {
|
||||
Class<?> clazz = ComposedPreAuthAnnotationOnClass.class;
|
||||
PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(clazz, PreAuthorize.class);
|
||||
|
||||
// If you comment out .map(MergedAnnotation::withNonMergedAttributes) in
|
||||
// AuthorizationAnnotationUtils.findDistinctAnnotation(), the value of
|
||||
// the merged annotation would be "hasRole('composedRole')".
|
||||
assertThat(preAuthorize.value()).isEqualTo("hasRole('metaRole')");
|
||||
Method method = HelloImpl.class.getMethod("sayHello");
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class));
|
||||
}
|
||||
|
||||
private interface BaseRepository<T> {
|
||||
@@ -131,60 +82,4 @@ class AuthorizationAnnotationUtilsTests {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreAuthorize("hasRole('someRole')")
|
||||
private @interface RequireSomeRole {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreAuthorize("hasRole('otherRole')")
|
||||
private @interface RequireOtherRole {
|
||||
|
||||
}
|
||||
|
||||
@RequireSomeRole
|
||||
@PreAuthorize("hasRole('someRole')")
|
||||
private static class MultipleIdenticalPreAuthorizeAnnotationsOnClass {
|
||||
|
||||
}
|
||||
|
||||
private static class MultipleIdenticalPreAuthorizeAnnotationsOnMethod {
|
||||
|
||||
@RequireSomeRole
|
||||
@PreAuthorize("hasRole('someRole')")
|
||||
void method() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RequireOtherRole
|
||||
@PreAuthorize("hasRole('someRole')")
|
||||
private static class CompetingPreAuthorizeAnnotationsOnClass {
|
||||
|
||||
}
|
||||
|
||||
private static class CompetingPreAuthorizeAnnotationsOnMethod {
|
||||
|
||||
@RequireOtherRole
|
||||
@PreAuthorize("hasRole('someRole')")
|
||||
void method() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreAuthorize("hasRole('metaRole')")
|
||||
private @interface ComposedPreAuth {
|
||||
|
||||
@AliasFor(annotation = PreAuthorize.class)
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@ComposedPreAuth("hasRole('composedRole')")
|
||||
private static class ComposedPreAuthAnnotationOnClass {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-51
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -225,56 +225,6 @@ public class Jsr250AuthorizationManagerTests {
|
||||
.isThrownBy(() -> manager.check(authentication, methodInvocation));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkRequiresUserWhenMethodsFromInheritThenApplies() throws Exception {
|
||||
MockMethodInvocation methodInvocation = new MockMethodInvocation(new RolesAllowedClass(),
|
||||
RolesAllowedClass.class, "securedUser");
|
||||
Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
|
||||
AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkPermitAllWhenMethodsFromInheritThenApplies() throws Exception {
|
||||
MockMethodInvocation methodInvocation = new MockMethodInvocation(new PermitAllClass(), PermitAllClass.class,
|
||||
"securedUser");
|
||||
Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
|
||||
AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkDenyAllWhenMethodsFromInheritThenApplies() throws Exception {
|
||||
MockMethodInvocation methodInvocation = new MockMethodInvocation(new DenyAllClass(), DenyAllClass.class,
|
||||
"securedUser");
|
||||
Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
|
||||
AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@RolesAllowed("USER")
|
||||
public static class RolesAllowedClass extends ParentClass {
|
||||
|
||||
}
|
||||
|
||||
@PermitAll
|
||||
public static class PermitAllClass extends ParentClass {
|
||||
|
||||
}
|
||||
|
||||
@DenyAll
|
||||
public static class DenyAllClass extends ParentClass {
|
||||
|
||||
}
|
||||
|
||||
public static class ParentClass {
|
||||
|
||||
public void securedUser() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
|
||||
|
||||
public void doSomething() {
|
||||
|
||||
+1
-24
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -167,29 +167,6 @@ public class PostAuthorizeAuthorizationManagerTests {
|
||||
.isThrownBy(() -> manager.check(authentication, result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkRequiresUserWhenMethodsFromInheritThenApplies() throws Exception {
|
||||
MockMethodInvocation methodInvocation = new MockMethodInvocation(new PostAuthorizeClass(),
|
||||
PostAuthorizeClass.class, "securedUser");
|
||||
MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
|
||||
PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
|
||||
AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, result);
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@PostAuthorize("hasRole('USER')")
|
||||
public static class PostAuthorizeClass extends ParentClass {
|
||||
|
||||
}
|
||||
|
||||
public static class ParentClass {
|
||||
|
||||
public void securedUser() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
|
||||
|
||||
public void doSomething() {
|
||||
|
||||
+1
-29
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -170,34 +170,6 @@ public class PostFilterAuthorizationMethodInterceptorTests {
|
||||
SecurityContextHolder.setContextHolderStrategy(saved);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkPostFilterWhenMethodsFromInheritThenApplies() throws Throwable {
|
||||
String[] array = { "john", "bob" };
|
||||
MockMethodInvocation methodInvocation = new MockMethodInvocation(new PostFilterClass(), PostFilterClass.class,
|
||||
"inheritMethod", new Class[] { String[].class }, new Object[] { array }) {
|
||||
@Override
|
||||
public Object proceed() {
|
||||
return array;
|
||||
}
|
||||
};
|
||||
PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor();
|
||||
Object result = advice.invoke(methodInvocation);
|
||||
assertThat(result).asInstanceOf(InstanceOfAssertFactories.array(String[].class)).containsOnly("john");
|
||||
}
|
||||
|
||||
@PostFilter("filterObject == 'john'")
|
||||
public static class PostFilterClass extends ParentClass {
|
||||
|
||||
}
|
||||
|
||||
public static class ParentClass {
|
||||
|
||||
public String[] inheritMethod(String[] array) {
|
||||
return array;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@PostFilter("filterObject == 'john'")
|
||||
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
|
||||
|
||||
|
||||
+1
-23
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -147,28 +147,6 @@ public class PreAuthorizeAuthorizationManagerTests {
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkRequiresUserWhenMethodsFromInheritThenApplies() throws Exception {
|
||||
MockMethodInvocation methodInvocation = new MockMethodInvocation(new PreAuthorizeClass(),
|
||||
PreAuthorizeClass.class, "securedUser");
|
||||
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
|
||||
AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('USER')")
|
||||
public static class PreAuthorizeClass extends ParentClass {
|
||||
|
||||
}
|
||||
|
||||
public static class ParentClass {
|
||||
|
||||
public void securedUser() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
|
||||
|
||||
public void doSomething() {
|
||||
|
||||
+1
-27
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -224,32 +224,6 @@ public class PreFilterAuthorizationMethodInterceptorTests {
|
||||
SecurityContextHolder.setContextHolderStrategy(saved);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkPreFilterWhenMethodsFromInheritThenApplies() throws Throwable {
|
||||
List<String> list = new ArrayList<>();
|
||||
list.add("john");
|
||||
list.add("bob");
|
||||
MockMethodInvocation invocation = new MockMethodInvocation(new PreFilterClass(), PreFilterClass.class,
|
||||
"inheritMethod", new Class[] { List.class }, new Object[] { list });
|
||||
PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
|
||||
advice.invoke(invocation);
|
||||
assertThat(list).hasSize(1);
|
||||
assertThat(list.get(0)).isEqualTo("john");
|
||||
}
|
||||
|
||||
@PreFilter("filterObject == 'john'")
|
||||
public static class PreFilterClass extends ParentClass {
|
||||
|
||||
}
|
||||
|
||||
public static class ParentClass {
|
||||
|
||||
public void inheritMethod(List<String> list) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@PreFilter("filterObject == 'john'")
|
||||
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
|
||||
|
||||
|
||||
+1
-23
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -167,28 +167,6 @@ public class SecuredAuthorizationManagerTests {
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkRequiresUserWhenMethodsFromInheritThenApplies() throws Exception {
|
||||
MockMethodInvocation methodInvocation = new MockMethodInvocation(new SecuredSonClass(), SecuredSonClass.class,
|
||||
"securedUser");
|
||||
SecuredAuthorizationManager manager = new SecuredAuthorizationManager();
|
||||
AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Secured("ROLE_USER")
|
||||
public static class SecuredSonClass extends ParentClass {
|
||||
|
||||
}
|
||||
|
||||
public static class ParentClass {
|
||||
|
||||
public void securedUser() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
|
||||
|
||||
public void doSomething() {
|
||||
|
||||
+1
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2013 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.
|
||||
@@ -23,7 +23,6 @@ import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
@@ -80,7 +79,6 @@ public class SpringSecurityCoreVersionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("Since 6.3. See gh-3737")
|
||||
public void serialVersionMajorAndMinorVersionMatchBuildVersion() {
|
||||
String version = System.getProperty("springSecurityVersion");
|
||||
// Strip patch version
|
||||
|
||||
@@ -3,9 +3,8 @@ apply plugin: 'io.spring.convention.spring-module'
|
||||
dependencies {
|
||||
management platform(project(":spring-security-dependencies"))
|
||||
optional 'org.springframework:spring-jcl'
|
||||
optional 'org.springframework:spring-core'
|
||||
optional 'org.bouncycastle:bcpkix-jdk15on'
|
||||
|
||||
|
||||
testImplementation "org.assertj:assertj-core"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-params"
|
||||
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-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.crypto.encrypt;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.interfaces.RSAPrivateCrtKey;
|
||||
import java.security.spec.RSAPublicKeySpec;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @author Tim Ysewyn
|
||||
* @since 6.3
|
||||
*/
|
||||
public class KeyStoreKeyFactory {
|
||||
|
||||
private final Resource resource;
|
||||
|
||||
private final char[] password;
|
||||
|
||||
private KeyStore store;
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
private final String type;
|
||||
|
||||
public KeyStoreKeyFactory(Resource resource, char[] password) {
|
||||
this(resource, password, type(resource));
|
||||
}
|
||||
|
||||
private static String type(Resource resource) {
|
||||
String ext = StringUtils.getFilenameExtension(resource.getFilename());
|
||||
return (ext != null) ? ext : "jks";
|
||||
}
|
||||
|
||||
public KeyStoreKeyFactory(Resource resource, char[] password, String type) {
|
||||
this.resource = resource;
|
||||
this.password = password;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public KeyPair getKeyPair(String alias) {
|
||||
return getKeyPair(alias, this.password);
|
||||
}
|
||||
|
||||
public KeyPair getKeyPair(String alias, char[] password) {
|
||||
try {
|
||||
synchronized (this.lock) {
|
||||
if (this.store == null) {
|
||||
synchronized (this.lock) {
|
||||
this.store = KeyStore.getInstance(this.type);
|
||||
try (InputStream stream = this.resource.getInputStream()) {
|
||||
this.store.load(stream, this.password);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RSAPrivateCrtKey key = (RSAPrivateCrtKey) this.store.getKey(alias, password);
|
||||
Certificate certificate = this.store.getCertificate(alias);
|
||||
PublicKey publicKey = null;
|
||||
if (certificate != null) {
|
||||
publicKey = certificate.getPublicKey();
|
||||
}
|
||||
else if (key != null) {
|
||||
RSAPublicKeySpec spec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
|
||||
publicKey = KeyFactory.getInstance("RSA").generatePublic(spec);
|
||||
}
|
||||
return new KeyPair(publicKey, key);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Cannot load keys from store: " + this.resource, ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-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.crypto.encrypt;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @since 6.3
|
||||
*/
|
||||
public enum RsaAlgorithm {
|
||||
|
||||
DEFAULT("RSA", 117), OAEP("RSA/ECB/OAEPPadding", 86);
|
||||
|
||||
private final String name;
|
||||
|
||||
private final int maxLength;
|
||||
|
||||
RsaAlgorithm(String name, int maxLength) {
|
||||
this.name = name;
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
public String getJceName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public int getMaxLength() {
|
||||
return this.maxLength;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-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.crypto.encrypt;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.security.spec.RSAPrivateCrtKeySpec;
|
||||
import java.security.spec.RSAPublicKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
|
||||
/**
|
||||
* Reads RSA key pairs using BC provider classes but without the need to specify a crypto
|
||||
* provider or have BC added as one.
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @author Dave Syer
|
||||
*/
|
||||
final class RsaKeyHelper {
|
||||
|
||||
private static final Charset UTF8 = StandardCharsets.UTF_8;
|
||||
|
||||
private static final String BEGIN = "-----BEGIN";
|
||||
|
||||
private static final Pattern PEM_DATA = Pattern.compile(".*-----BEGIN (.*)-----(.*)-----END (.*)-----",
|
||||
Pattern.DOTALL);
|
||||
|
||||
private static final byte[] PREFIX = new byte[] { 0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a' };
|
||||
|
||||
private RsaKeyHelper() {
|
||||
}
|
||||
|
||||
static KeyPair parseKeyPair(String pemData) {
|
||||
Matcher m = PEM_DATA.matcher(pemData.replaceAll("\n *", "").trim());
|
||||
|
||||
if (!m.matches()) {
|
||||
try {
|
||||
RSAPublicKey publicValue = extractPublicKey(pemData);
|
||||
if (publicValue != null) {
|
||||
return new KeyPair(publicValue, null);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Ignore
|
||||
}
|
||||
throw new IllegalArgumentException("String is not PEM encoded data, nor a public key encoded for ssh");
|
||||
}
|
||||
|
||||
String type = m.group(1);
|
||||
final byte[] content = base64Decode(m.group(2));
|
||||
|
||||
PublicKey publicKey;
|
||||
PrivateKey privateKey = null;
|
||||
|
||||
try {
|
||||
KeyFactory fact = KeyFactory.getInstance("RSA");
|
||||
switch (type) {
|
||||
case "RSA PRIVATE KEY" -> {
|
||||
ASN1Sequence seq = ASN1Sequence.getInstance(content);
|
||||
if (seq.size() != 9) {
|
||||
throw new IllegalArgumentException("Invalid RSA Private Key ASN1 sequence.");
|
||||
}
|
||||
org.bouncycastle.asn1.pkcs.RSAPrivateKey key = org.bouncycastle.asn1.pkcs.RSAPrivateKey
|
||||
.getInstance(seq);
|
||||
RSAPublicKeySpec pubSpec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
|
||||
RSAPrivateCrtKeySpec privSpec = new RSAPrivateCrtKeySpec(key.getModulus(), key.getPublicExponent(),
|
||||
key.getPrivateExponent(), key.getPrime1(), key.getPrime2(), key.getExponent1(),
|
||||
key.getExponent2(), key.getCoefficient());
|
||||
publicKey = fact.generatePublic(pubSpec);
|
||||
privateKey = fact.generatePrivate(privSpec);
|
||||
}
|
||||
case "PUBLIC KEY" -> {
|
||||
KeySpec keySpec = new X509EncodedKeySpec(content);
|
||||
publicKey = fact.generatePublic(keySpec);
|
||||
}
|
||||
case "RSA PUBLIC KEY" -> {
|
||||
ASN1Sequence seq = ASN1Sequence.getInstance(content);
|
||||
org.bouncycastle.asn1.pkcs.RSAPublicKey key = org.bouncycastle.asn1.pkcs.RSAPublicKey
|
||||
.getInstance(seq);
|
||||
RSAPublicKeySpec pubSpec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
|
||||
publicKey = fact.generatePublic(pubSpec);
|
||||
}
|
||||
default -> throw new IllegalArgumentException(type + " is not a supported format");
|
||||
}
|
||||
|
||||
return new KeyPair(publicKey, privateKey);
|
||||
}
|
||||
catch (InvalidKeySpecException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
catch (NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] base64Decode(String string) {
|
||||
try {
|
||||
ByteBuffer bytes = UTF8.newEncoder().encode(CharBuffer.wrap(string));
|
||||
byte[] bytesCopy = new byte[bytes.limit()];
|
||||
System.arraycopy(bytes.array(), 0, bytesCopy, 0, bytes.limit());
|
||||
return Base64.getDecoder().decode(bytesCopy);
|
||||
}
|
||||
catch (CharacterCodingException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static String base64Encode(byte[] bytes) {
|
||||
try {
|
||||
return UTF8.newDecoder().decode(ByteBuffer.wrap(Base64.getEncoder().encode(bytes))).toString();
|
||||
}
|
||||
catch (CharacterCodingException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static KeyPair generateKeyPair() {
|
||||
try {
|
||||
final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyGen.initialize(1024);
|
||||
return keyGen.generateKeyPair();
|
||||
}
|
||||
catch (NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final Pattern SSH_PUB_KEY = Pattern.compile("ssh-(rsa|dsa) ([A-Za-z0-9/+]+=*) (.*)");
|
||||
|
||||
private static RSAPublicKey extractPublicKey(String key) {
|
||||
|
||||
Matcher m = SSH_PUB_KEY.matcher(key);
|
||||
|
||||
if (m.matches()) {
|
||||
String alg = m.group(1);
|
||||
String encKey = m.group(2);
|
||||
// String id = m.group(3);
|
||||
|
||||
if (!"rsa".equalsIgnoreCase(alg)) {
|
||||
throw new IllegalArgumentException("Only RSA is currently supported, but algorithm was " + alg);
|
||||
}
|
||||
|
||||
return parseSSHPublicKey(encKey);
|
||||
}
|
||||
else if (!key.startsWith(BEGIN)) {
|
||||
// Assume it's the plain Base64 encoded ssh key without the
|
||||
// "ssh-rsa" at the start
|
||||
return parseSSHPublicKey(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static RSAPublicKey parsePublicKey(String key) {
|
||||
|
||||
RSAPublicKey publicKey = extractPublicKey(key);
|
||||
|
||||
if (publicKey != null) {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
KeyPair kp = parseKeyPair(key);
|
||||
|
||||
if (kp.getPublic() == null) {
|
||||
throw new IllegalArgumentException("Key data does not contain a public key");
|
||||
}
|
||||
|
||||
return (RSAPublicKey) kp.getPublic();
|
||||
|
||||
}
|
||||
|
||||
static String encodePublicKey(RSAPublicKey key, String id) {
|
||||
StringWriter output = new StringWriter();
|
||||
output.append("ssh-rsa ");
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
try {
|
||||
stream.write(PREFIX);
|
||||
writeBigInteger(stream, key.getPublicExponent());
|
||||
writeBigInteger(stream, key.getModulus());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException("Cannot encode key", ex);
|
||||
}
|
||||
output.append(base64Encode(stream.toByteArray()));
|
||||
output.append(" " + id);
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
private static RSAPublicKey parseSSHPublicKey(String encKey) {
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(base64Decode(encKey));
|
||||
|
||||
byte[] prefix = new byte[11];
|
||||
|
||||
try {
|
||||
if (in.read(prefix) != 11 || !Arrays.equals(PREFIX, prefix)) {
|
||||
throw new IllegalArgumentException("SSH key prefix not found");
|
||||
}
|
||||
|
||||
BigInteger e = new BigInteger(readBigInteger(in));
|
||||
BigInteger n = new BigInteger(readBigInteger(in));
|
||||
|
||||
return createPublicKey(n, e);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static RSAPublicKey createPublicKey(BigInteger n, BigInteger e) {
|
||||
try {
|
||||
return (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(n, e));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeBigInteger(ByteArrayOutputStream stream, BigInteger num) throws IOException {
|
||||
int length = num.toByteArray().length;
|
||||
byte[] data = new byte[4];
|
||||
data[0] = (byte) ((length >> 24) & 0xFF);
|
||||
data[1] = (byte) ((length >> 16) & 0xFF);
|
||||
data[2] = (byte) ((length >> 8) & 0xFF);
|
||||
data[3] = (byte) (length & 0xFF);
|
||||
stream.write(data);
|
||||
stream.write(num.toByteArray());
|
||||
}
|
||||
|
||||
private static byte[] readBigInteger(ByteArrayInputStream in) throws IOException {
|
||||
byte[] b = new byte[4];
|
||||
|
||||
if (in.read(b) != 4) {
|
||||
throw new IOException("Expected length data as 4 bytes");
|
||||
}
|
||||
|
||||
int l = ((b[0] & 0xFF) << 24) | ((b[1] & 0xFF) << 16) | ((b[2] & 0xFF) << 8) | (b[3] & 0xFF);
|
||||
|
||||
b = new byte[l];
|
||||
|
||||
if (in.read(b) != l) {
|
||||
throw new IOException("Expected " + l + " key bytes");
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-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.crypto.encrypt;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @since 6.3
|
||||
*/
|
||||
public interface RsaKeyHolder {
|
||||
|
||||
String getPublicKey();
|
||||
|
||||
}
|
||||
-168
@@ -1,168 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-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.crypto.encrypt;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.interfaces.RSAKey;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Base64;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @since 6.3
|
||||
*/
|
||||
public class RsaRawEncryptor implements BytesEncryptor, TextEncryptor, RsaKeyHolder {
|
||||
|
||||
private static final String DEFAULT_ENCODING = "UTF-8";
|
||||
|
||||
private RsaAlgorithm algorithm = RsaAlgorithm.DEFAULT;
|
||||
|
||||
private Charset charset;
|
||||
|
||||
private RSAPublicKey publicKey;
|
||||
|
||||
private RSAPrivateKey privateKey;
|
||||
|
||||
private Charset defaultCharset;
|
||||
|
||||
public RsaRawEncryptor(RsaAlgorithm algorithm) {
|
||||
this(RsaKeyHelper.generateKeyPair(), algorithm);
|
||||
}
|
||||
|
||||
public RsaRawEncryptor() {
|
||||
this(RsaKeyHelper.generateKeyPair());
|
||||
}
|
||||
|
||||
public RsaRawEncryptor(KeyPair keyPair, RsaAlgorithm algorithm) {
|
||||
this(DEFAULT_ENCODING, keyPair.getPublic(), keyPair.getPrivate(), algorithm);
|
||||
}
|
||||
|
||||
public RsaRawEncryptor(KeyPair keyPair) {
|
||||
this(DEFAULT_ENCODING, keyPair.getPublic(), keyPair.getPrivate());
|
||||
}
|
||||
|
||||
public RsaRawEncryptor(String pemData) {
|
||||
this(RsaKeyHelper.parseKeyPair(pemData));
|
||||
}
|
||||
|
||||
public RsaRawEncryptor(PublicKey publicKey) {
|
||||
this(DEFAULT_ENCODING, publicKey, null);
|
||||
}
|
||||
|
||||
public RsaRawEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey) {
|
||||
this(encoding, publicKey, privateKey, RsaAlgorithm.DEFAULT);
|
||||
}
|
||||
|
||||
public RsaRawEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey, RsaAlgorithm algorithm) {
|
||||
this.charset = Charset.forName(encoding);
|
||||
this.publicKey = (RSAPublicKey) publicKey;
|
||||
this.privateKey = (RSAPrivateKey) privateKey;
|
||||
this.defaultCharset = Charset.forName(DEFAULT_ENCODING);
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPublicKey() {
|
||||
return RsaKeyHelper.encodePublicKey(this.publicKey, "application");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encrypt(String text) {
|
||||
return new String(Base64.getEncoder().encode(encrypt(text.getBytes(this.charset))), this.defaultCharset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String encryptedText) {
|
||||
if (this.privateKey == null) {
|
||||
throw new IllegalStateException("Private key must be provided for decryption");
|
||||
}
|
||||
return new String(decrypt(Base64.getDecoder().decode(encryptedText.getBytes(this.defaultCharset))),
|
||||
this.charset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encrypt(byte[] byteArray) {
|
||||
return encrypt(byteArray, this.publicKey, this.algorithm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decrypt(byte[] encryptedByteArray) {
|
||||
return decrypt(encryptedByteArray, this.privateKey, this.algorithm);
|
||||
}
|
||||
|
||||
private static byte[] encrypt(byte[] text, PublicKey key, RsaAlgorithm alg) {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream(text.length);
|
||||
try {
|
||||
final Cipher cipher = Cipher.getInstance(alg.getJceName());
|
||||
int limit = Math.min(text.length, alg.getMaxLength());
|
||||
int pos = 0;
|
||||
while (pos < text.length) {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
cipher.update(text, pos, limit);
|
||||
pos += limit;
|
||||
limit = Math.min(text.length - pos, alg.getMaxLength());
|
||||
byte[] buffer = cipher.doFinal();
|
||||
output.write(buffer, 0, buffer.length);
|
||||
}
|
||||
return output.toByteArray();
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Cannot encrypt", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decrypt(byte[] text, RSAPrivateKey key, RsaAlgorithm alg) {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream(text.length);
|
||||
try {
|
||||
final Cipher cipher = Cipher.getInstance(alg.getJceName());
|
||||
int maxLength = getByteLength(key);
|
||||
int pos = 0;
|
||||
while (pos < text.length) {
|
||||
int limit = Math.min(text.length - pos, maxLength);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key);
|
||||
cipher.update(text, pos, limit);
|
||||
pos += limit;
|
||||
byte[] buffer = cipher.doFinal();
|
||||
output.write(buffer, 0, buffer.length);
|
||||
}
|
||||
return output.toByteArray();
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Cannot decrypt", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// copied from sun.security.rsa.RSACore.getByteLength(java.math.BigInteger)
|
||||
public static int getByteLength(RSAKey key) {
|
||||
int n = key.getModulus().bitLength();
|
||||
return (n + 7) >> 3;
|
||||
}
|
||||
|
||||
}
|
||||
-247
@@ -1,247 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-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.crypto.encrypt;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Base64;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
import org.springframework.security.crypto.codec.Hex;
|
||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @since 6.3
|
||||
*/
|
||||
public class RsaSecretEncryptor implements BytesEncryptor, TextEncryptor, RsaKeyHolder {
|
||||
|
||||
private static final String DEFAULT_ENCODING = "UTF-8";
|
||||
|
||||
// The secret for encryption is random (so dictionary attack is not a danger)
|
||||
private static final String DEFAULT_SALT = "deadbeef";
|
||||
|
||||
private final String salt;
|
||||
|
||||
private RsaAlgorithm algorithm = RsaAlgorithm.DEFAULT;
|
||||
|
||||
private final Charset charset;
|
||||
|
||||
private final PublicKey publicKey;
|
||||
|
||||
private final PrivateKey privateKey;
|
||||
|
||||
private final Charset defaultCharset;
|
||||
|
||||
private final boolean gcm;
|
||||
|
||||
public RsaSecretEncryptor(RsaAlgorithm algorithm, String salt, boolean gcm) {
|
||||
this(RsaKeyHelper.generateKeyPair(), algorithm, salt, gcm);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(RsaAlgorithm algorithm, String salt) {
|
||||
this(RsaKeyHelper.generateKeyPair(), algorithm, salt);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(RsaAlgorithm algorithm, boolean gcm) {
|
||||
this(RsaKeyHelper.generateKeyPair(), algorithm, DEFAULT_SALT, gcm);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(RsaAlgorithm algorithm) {
|
||||
this(RsaKeyHelper.generateKeyPair(), algorithm);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor() {
|
||||
this(RsaKeyHelper.generateKeyPair());
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(KeyPair keyPair, RsaAlgorithm algorithm, String salt, boolean gcm) {
|
||||
this(DEFAULT_ENCODING, keyPair.getPublic(), keyPair.getPrivate(), algorithm, salt, gcm);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(KeyPair keyPair, RsaAlgorithm algorithm, String salt) {
|
||||
this(DEFAULT_ENCODING, keyPair.getPublic(), keyPair.getPrivate(), algorithm, salt, false);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(KeyPair keyPair, RsaAlgorithm algorithm) {
|
||||
this(DEFAULT_ENCODING, keyPair.getPublic(), keyPair.getPrivate(), algorithm);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(KeyPair keyPair) {
|
||||
this(DEFAULT_ENCODING, keyPair.getPublic(), keyPair.getPrivate());
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(String pemData, RsaAlgorithm algorithm, String salt) {
|
||||
this(RsaKeyHelper.parseKeyPair(pemData), algorithm, salt);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(String pemData, RsaAlgorithm algorithm) {
|
||||
this(RsaKeyHelper.parseKeyPair(pemData), algorithm);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(String pemData) {
|
||||
this(RsaKeyHelper.parseKeyPair(pemData));
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(PublicKey publicKey, RsaAlgorithm algorithm, String salt, boolean gcm) {
|
||||
this(DEFAULT_ENCODING, publicKey, null, algorithm, salt, gcm);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(PublicKey publicKey, RsaAlgorithm algorithm, String salt) {
|
||||
this(DEFAULT_ENCODING, publicKey, null, algorithm, salt, false);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(PublicKey publicKey, RsaAlgorithm algorithm) {
|
||||
this(DEFAULT_ENCODING, publicKey, null, algorithm);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(PublicKey publicKey) {
|
||||
this(DEFAULT_ENCODING, publicKey, null);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey) {
|
||||
this(encoding, publicKey, privateKey, RsaAlgorithm.DEFAULT);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey, RsaAlgorithm algorithm) {
|
||||
this(encoding, publicKey, privateKey, algorithm, DEFAULT_SALT, false);
|
||||
}
|
||||
|
||||
public RsaSecretEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey, RsaAlgorithm algorithm,
|
||||
String salt, boolean gcm) {
|
||||
this.charset = Charset.forName(encoding);
|
||||
this.publicKey = publicKey;
|
||||
this.privateKey = privateKey;
|
||||
this.defaultCharset = Charset.forName(DEFAULT_ENCODING);
|
||||
this.algorithm = algorithm;
|
||||
this.salt = isHex(salt) ? salt : new String(Hex.encode(salt.getBytes(this.defaultCharset)));
|
||||
this.gcm = gcm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPublicKey() {
|
||||
return RsaKeyHelper.encodePublicKey((RSAPublicKey) this.publicKey, "application");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encrypt(String text) {
|
||||
return new String(Base64.getEncoder().encode(encrypt(text.getBytes(this.charset))), this.defaultCharset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String encryptedText) {
|
||||
if (!canDecrypt()) {
|
||||
throw new IllegalStateException("Encryptor is not configured for decryption");
|
||||
}
|
||||
return new String(decrypt(Base64.getDecoder().decode(encryptedText.getBytes(this.defaultCharset))),
|
||||
this.charset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encrypt(byte[] byteArray) {
|
||||
return encrypt(byteArray, this.publicKey, this.algorithm, this.salt, this.gcm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decrypt(byte[] encryptedByteArray) {
|
||||
if (!canDecrypt()) {
|
||||
throw new IllegalStateException("Encryptor is not configured for decryption");
|
||||
}
|
||||
return decrypt(encryptedByteArray, this.privateKey, this.algorithm, this.salt, this.gcm);
|
||||
}
|
||||
|
||||
private static byte[] encrypt(byte[] text, PublicKey key, RsaAlgorithm alg, String salt, boolean gcm) {
|
||||
byte[] random = KeyGenerators.secureRandom(16).generateKey();
|
||||
BytesEncryptor aes = gcm ? Encryptors.stronger(new String(Hex.encode(random)), salt)
|
||||
: Encryptors.standard(new String(Hex.encode(random)), salt);
|
||||
try {
|
||||
final Cipher cipher = Cipher.getInstance(alg.getJceName());
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
byte[] secret = cipher.doFinal(random);
|
||||
ByteArrayOutputStream result = new ByteArrayOutputStream(text.length + 20);
|
||||
writeInt(result, secret.length);
|
||||
result.write(secret);
|
||||
result.write(aes.encrypt(text));
|
||||
return result.toByteArray();
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Cannot encrypt", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeInt(ByteArrayOutputStream result, int length) throws IOException {
|
||||
byte[] data = new byte[2];
|
||||
data[0] = (byte) ((length >> 8) & 0xFF);
|
||||
data[1] = (byte) (length & 0xFF);
|
||||
result.write(data);
|
||||
}
|
||||
|
||||
private static int readInt(ByteArrayInputStream result) throws IOException {
|
||||
byte[] b = new byte[2];
|
||||
result.read(b);
|
||||
return ((b[0] & 0xFF) << 8) | (b[1] & 0xFF);
|
||||
}
|
||||
|
||||
private static byte[] decrypt(byte[] text, PrivateKey key, RsaAlgorithm alg, String salt, boolean gcm) {
|
||||
ByteArrayInputStream input = new ByteArrayInputStream(text);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream(text.length);
|
||||
try {
|
||||
int length = readInt(input);
|
||||
byte[] random = new byte[length];
|
||||
input.read(random);
|
||||
final Cipher cipher = Cipher.getInstance(alg.getJceName());
|
||||
cipher.init(Cipher.DECRYPT_MODE, key);
|
||||
String secret = new String(Hex.encode(cipher.doFinal(random)));
|
||||
byte[] buffer = new byte[text.length - random.length - 2];
|
||||
input.read(buffer);
|
||||
BytesEncryptor aes = gcm ? Encryptors.stronger(secret, salt) : Encryptors.standard(secret, salt);
|
||||
output.write(aes.decrypt(buffer));
|
||||
return output.toByteArray();
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Cannot decrypt", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isHex(String input) {
|
||||
try {
|
||||
Hex.decode(input);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canDecrypt() {
|
||||
return this.privateKey != null;
|
||||
}
|
||||
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-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.crypto.encrypt;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
@DisabledOnOs(OS.WINDOWS)
|
||||
public class KeyStoreKeyFactoryTests {
|
||||
|
||||
@Test
|
||||
public void initializeEncryptorFromKeyStore() {
|
||||
char[] password = "foobar".toCharArray();
|
||||
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), password);
|
||||
RsaSecretEncryptor encryptor = new RsaSecretEncryptor(factory.getKeyPair("test"));
|
||||
assertThat(encryptor.canDecrypt()).as("Should be able to decrypt").isTrue();
|
||||
assertThat(encryptor.decrypt(encryptor.encrypt("foo"))).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initializeEncryptorFromPkcs12KeyStore() {
|
||||
char[] password = "letmein".toCharArray();
|
||||
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("keystore.pkcs12"), password);
|
||||
RsaSecretEncryptor encryptor = new RsaSecretEncryptor(factory.getKeyPair("mytestkey"));
|
||||
assertThat(encryptor.canDecrypt()).as("Should be able to decrypt").isTrue();
|
||||
assertThat(encryptor.decrypt(encryptor.encrypt("foo"))).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initializeEncryptorFromTrustedCertificateInKeyStore() {
|
||||
char[] password = "foobar".toCharArray();
|
||||
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), password);
|
||||
RsaSecretEncryptor encryptor = new RsaSecretEncryptor(factory.getKeyPair("testcertificate"));
|
||||
assertThat(encryptor.canDecrypt()).as("Should not be able to decrypt").isFalse();
|
||||
assertThat(encryptor.encrypt("foo")).isNotEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initializeEncryptorFromTrustedCertificateInPkcs12KeyStore() {
|
||||
char[] password = "letmein".toCharArray();
|
||||
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("keystore.pkcs12"), password);
|
||||
RsaSecretEncryptor encryptor = new RsaSecretEncryptor(factory.getKeyPair("mytestcertificate"));
|
||||
assertThat(encryptor.canDecrypt()).as("Should not be able to decrypt").isFalse();
|
||||
assertThat(encryptor.encrypt("foo")).isNotEqualTo("foo");
|
||||
}
|
||||
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-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.crypto.encrypt;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyPair;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisabledOnOs(OS.WINDOWS)
|
||||
public class RsaKeyHelperTests {
|
||||
|
||||
@Test
|
||||
public void parsePrivateKey() throws Exception {
|
||||
// ssh-keygen -m pem -b 1024 -f src/test/resources/fake.pem
|
||||
String pem = StreamUtils.copyToString(new ClassPathResource("/fake.pem", getClass()).getInputStream(),
|
||||
StandardCharsets.UTF_8);
|
||||
KeyPair result = RsaKeyHelper.parseKeyPair(pem);
|
||||
assertThat(result.getPrivate().getEncoded().length > 0).isTrue();
|
||||
assertThat(result.getPrivate().getAlgorithm()).isEqualTo("RSA");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseSpaceyKey() throws Exception {
|
||||
String pem = StreamUtils.copyToString(new ClassPathResource("/spacey.pem", getClass()).getInputStream(),
|
||||
StandardCharsets.UTF_8);
|
||||
KeyPair result = RsaKeyHelper.parseKeyPair(pem);
|
||||
assertThat(result.getPrivate().getEncoded().length > 0).isTrue();
|
||||
assertThat(result.getPrivate().getAlgorithm()).isEqualTo("RSA");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseBadKey() throws Exception {
|
||||
// ssh-keygen -m pem -b 1024 -f src/test/resources/fake.pem
|
||||
String pem = StreamUtils.copyToString(new ClassPathResource("/bad.pem", getClass()).getInputStream(),
|
||||
StandardCharsets.UTF_8);
|
||||
try {
|
||||
RsaKeyHelper.parseKeyPair(pem);
|
||||
throw new IllegalStateException("Expected IllegalArgumentException");
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
assertThat(ex.getMessage().contains("PEM")).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-154
@@ -1,154 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-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.crypto.encrypt;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class RsaRawEncryptorTests {
|
||||
|
||||
private RsaRawEncryptor encryptor = new RsaRawEncryptor();
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
LONG_STRING = SHORT_STRING + SHORT_STRING + SHORT_STRING + SHORT_STRING;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
LONG_STRING = LONG_STRING + LONG_STRING;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTrip() {
|
||||
assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripOeap() {
|
||||
this.encryptor = new RsaRawEncryptor(RsaAlgorithm.OAEP);
|
||||
assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripLongString() {
|
||||
assertThat(this.encryptor.decrypt(this.encryptor.encrypt(LONG_STRING))).isEqualTo(LONG_STRING);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripLongStringOeap() {
|
||||
this.encryptor = new RsaRawEncryptor(RsaAlgorithm.OAEP);
|
||||
assertThat(this.encryptor.decrypt(this.encryptor.encrypt(LONG_STRING))).isEqualTo(LONG_STRING);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTrip2048Key() {
|
||||
String pemData = "-----BEGIN RSA PRIVATE KEY-----"
|
||||
+ "MIIEpQIBAAKCAQEA5KHEkCudAHCKIUHKyW6Z8dMyQsKrLbpDe0wDzx9MBARcOoS9"
|
||||
+ "ZUjzXwK6p/0RM6aCp+b9kkr37QKQ9K/Am13sr0z8Mkn1Q2cvXiL5gbnY1nYGk8/m"
|
||||
+ "CBX3QEhH2UII4yJsDVx1xmcSorZaWmeNKor7Zl3SZaQpWTvlkMgQKwY8DZL6PPxt"
|
||||
+ "JRPeKmuUY6B59u5okh1G6Y9OnT2dVxAkqT8WgLHu6StxBmueJ272x2sUWUzoDhnP"
|
||||
+ "7JRqa7h7t6fml3o3Op1iCywCOFzCIcK6G/oG/WZ7tbBYkwQdDjn/9VMdKkkPufwq"
|
||||
+ "zt4S75NJygXDwDnNPiTVoaOwrRrL8ahgw6bFCQIDAQABAoIBAECIMHUI+l2fZj2Q"
|
||||
+ "1m4Ym7cYB320eKCFjHqGsCSMDuarXGTgBp1KA/dzS8ASvAI6I3LEzhm2s1fge420"
|
||||
+ "9cZksmOgdSa0nVeTDlmhwY8OJ9gQpDagXas2l/066Zy2+M8zbhAvYsbHXQk0MziF"
|
||||
+ "NeEmLWNtY+9wcINRVrCQ549dSSIDK6UX21oU6d1mrlnF5/bbbdDIM3dKok355jwx"
|
||||
+ "0HFY0tJIs1zArsBVoz3Ccu1MQEfnxEFM1LLPi5rE6cuHIOBinbD1OQ2R/HM2aukG"
|
||||
+ "Rk2m6F3wAieJ7zpt5yaHuuIedn8p8m2NVulXAjgkY2oQl3GGiDH/H7eZlrvQRg6E"
|
||||
+ "D8Bq+ykCgYEA+AfPXVeeVg3Qu0KsNrACek/o92BMY9g3GyPVGULGvq9seoNB86hj"
|
||||
+ "nXasqngBfTlOfJFiahoEzRBB9hIyo1zMw4x99pR8nGxhR3aU+v8EGftMABGHWsB9"
|
||||
+ "Jxj4YQH4fhi57iBa72QmNPbu/1o7y3SEe68E5PJ8KY3jc4xos8Vl658CgYEA6/pk"
|
||||
+ "t6WZII+9lpxQfePQDIlBWAphiQceh995bGXfDmX3vOVmPozix9/fUtF1TeKS/ypw"
|
||||
+ "u++Qmvj5oMsBVrjCyoOYfHKE2vGrLoEzkX/sPO65IsV00geZZoyCEKEE3USJfY46"
|
||||
+ "u0hs61oP8HJyLhLiYiGcFTzZ4nEvvEbiM4E/DlcCgYEA6S0OecZhiK08SpAHrvIR"
|
||||
+ "okN11PqnVkZyqAUr1a+9gI8TAKpdWmA4JlTnRuvDGqLBcsKLLwx+7voVyOyaxpH7"
|
||||
+ "vutZkHNQIw6Q9co5jS4qAPMLJBVWlq7X+eWzvB9KKeG9Cm1IkD4q3Sg4z79Y75D+"
|
||||
+ "6/hCNarxp29JIdwior81bikCgYEApp1P+b7pxGzZPvs1df2hCwjqY0BJJ5goPWVT"
|
||||
+ "dW7kNGVYqz4JmAafpOJz6yTLP2fHxHRxzrBSmKlMj/RmCJZBqv2Jb+zn0zMpW5eM"
|
||||
+ "EqKQ6WDgxSVH23fUHuz8dMNMDPL0ZPtEirGTfgVEFdCov9FDmGgErZYefVzPiI8/"
|
||||
+ "7X/HRtcCgYEApQ2YS+0DLPqaM0cC6/6hDr/jmHLFhHaV6DZR7M9HHDnMN2uMlOEa"
|
||||
+ "RYvXRMBjyQ7LQkwOj6K5k8MVrsDDM5dbekTBgcJMHfM9uViDkB0VPYULORmDJ20N"
|
||||
+ "MLowIAiSon2B2/isatY80YtFq+bRyvPOzjGvinHN3MU1GH/gFuS0fiw=" + "-----END RSA PRIVATE KEY-----";
|
||||
RsaRawEncryptor encryptor_2048 = new RsaRawEncryptor(pemData);
|
||||
assertThat(encryptor_2048.decrypt(encryptor_2048.encrypt("encryptor"))).isEqualTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTrip4096Key() {
|
||||
String pemData = "-----BEGIN RSA PRIVATE KEY-----"
|
||||
+ "MIIJKAIBAAKCAgEAw/OIcO1pv8t/lhXwzc+CqCqAE8+2+BTWd6fHy8P2oGKZK0s3"
|
||||
+ "jxPWdZEbp1soGZobCIjEIuYuuPeinrTFOxtnf/JVfmzGnixRjWzQK0UiM/4z8GW6"
|
||||
+ "7+dzB0+QZlU+PGCL6xra4d3+5EsPQwTDjPJ4OhcA66hWACd3UJpvE2C14YdFkCP/"
|
||||
+ "CUxubz1l+8rFwEtMcw2bVUL/Mt+Sx1CHPFer17VK/sT4urwNG7y9R8WWvNQXgEwg"
|
||||
+ "0im+iJ0zf1u0SdUVj+Q1LwgNRoIx4vec2xAJ6xdqSx3Y3g2twWqUXUBb5K09ajIW"
|
||||
+ "Vuko5kWJVyx1x8LazU+0wQRLVJRYAiUOPLg7PdPAJWaAWmagnkAvl5bqCKi6sIc8"
|
||||
+ "+vKyrPx4VJH5KLsHx8020Wgch/LfHl/vvoHE7Oa81hnyMVsApvNCJdFbiMJ6r2z/"
|
||||
+ "eHqzjY8lzBQHNxh1XJys5teTJsi6N06gCc+OQRyw1FQ8KLgFlLPHNamfMnP5Ju0d"
|
||||
+ "Jv8GzQiMFjudjEYhkh2GPmRus1VYWDwDWhXwp28koWAanfih+Ujc2ZqNUS23hGWz"
|
||||
+ "KbCxRaAwSLqn3vkoYBeDyWWs1r0HnB6gACFaZIk38aiGyg7GjF0286Aq7USqNwKu"
|
||||
+ "Izm4kzIPFrHIbywKq7804J7wXUlaAgf0pNSndMD5OnwudzD+JHLTuOGFNdUCAwEA"
|
||||
+ "AQKCAgBYh2mIY6rYTS9adpUx1uPX6EOvL7QhhwCSVMoupF2Dfqhm5/e0+6hzu1h8"
|
||||
+ "FvIaBwbZpzi977MCPFdLTq6hErODGdBIawqdIbbCp3uxYO2gAeQjY0K+6pmMnwTF"
|
||||
+ "RxP0IUZ1tM9ZJnvnVoYRqFBVGKL607PFxGr+bNY6I1u1rIbf2sax5aFu6Qon1dyC"
|
||||
+ "ks0fIKXsgSRBtCAqMtpUlGxU9eMcdLrqOcGKVDWz52S4zWtZ6pSnkT1u1g9QF33R"
|
||||
+ "t3PPu6afOOJSWlftGBtDyM0kJ63jedO7FkQJprJu5SEctFwQB7jshq6TG4ov5xCy"
|
||||
+ "wtJ/quhBxBYM8ky6bL8KUQWKp02Tyfq0Fo+iwuLxM4N6LxVPFZ6R6jwvazm+ka4S"
|
||||
+ "sZAW/hnH3FdJEAyFcxzhelLdLUrjwrsWjmJBk0pMP5cEleYR8PQh2sHM8ZOX1T5f"
|
||||
+ "4zfyR66+tl1O81T7anbma8l1Wm/QSNZz+8QAM1iNuV+uLsWvmxLAc7NRgjDmiAMn"
|
||||
+ "8VhfUtl0ooOZYkDexqSNaWvIQG+S8Pl28gNxVXkXrXqBGPJn2ptROEJ1/AN1h4cv"
|
||||
+ "2CktVylRFpEI/hxXvKMaAu/tXtvoakvaTA8msl8Otrldsy3EGhgHrDTYIJUg/rRT"
|
||||
+ "TlbRkN/ycaOhA0d4HAewOGul3ss+EtBz+SQBzaWm2Inr8XOJoQKCAQEA4LwW7eGm"
|
||||
+ "MOYspFUbn2tMlnJAng9HKK42o2m6ShYAaQAoLX7LIkQYVS++9CiGCPpoSlwIJWE3"
|
||||
+ "N/qGx0i7REDm+wNu0/4acaMFI+qYtvjKiWwtMOBH3bw1C4/Isc60tFPkI7FEFCiF"
|
||||
+ "SiW3c+Z8B0/IRMb/YF5tZeuWUlAl7PQJ1rMcPUE4O4LXM4BG29hghVGGnp39YsOY"
|
||||
+ "b/6oBApTgdxCaSZhmhDwTMu97n75CK0xzA2vDtHn2Gu3zf4j6bsNot6/7wRtQBMg"
|
||||
+ "1e3kXuwGUZ08QZ7OqATUIZdCeK1PfxypontVh+0LeNjiDU8pW3Q8IMlDT96Fd5U+"
|
||||
+ "BgtjfHmwHXeBmQKCAQEA3zZS619O/IUoWN3rWT4hUSJE3S+FXXcaBaJ7H6r897cl"
|
||||
+ "ju+HSS2CLp/C9ftcQ9ef+pG2arLRZpONd5KhfRyjo0pNp3SwxklnIhNS9abBBCnN"
|
||||
+ "ojeYcVHOcSfmWGlUCQAvv5LeBPSS02pbCE5t/qadglvgKhHqSb2u+FgkdKrV0Mme"
|
||||
+ "sbVy+tyd4F1oBIS0wg1p3mHKvKfb4MEnUDvIvG8rCBUMvAWQmTiuyqFUiuqSwEMy"
|
||||
+ "LANFFV/ZoJ5194ruTXdelcoZjXhd128JJFNp6Jh4eg5OWoBS7e08QHbvUYBppDYO"
|
||||
+ "Iz0N1TipVK9uCqHHtbwIqqxyPVev3QJUYkpl5/tznQKCAQB9izV38F2J5Zu8tbq3"
|
||||
+ "pRZk2TCV280RwbjOMysZZg8WmTrYp4NNAiNhu0l+VgEClPibyavXTeauA+s0+sF6"
|
||||
+ "kJM4WKOaE9Kr9rjRZqWnWXazrFXWfwRGr3QmoE0qX2H9dvv0oHt6k2RalpVUTsas"
|
||||
+ "wvoKyewx5q5QiHoyQ4ncRDwWz3oQEhYa0K3tnFR5TfglofSFOZcqjD/lGKq9jxM1"
|
||||
+ "cVk8Km/NxHapQAw7Zn0yRqaR6ncH3WUaNpq4nadsU817Vdp86MkrSURHnhy8lje1"
|
||||
+ "chQOSGwD2qaymTBN/+twBBATr7iJNXf6K5akfruI1nccjbJntNR0iE/cypHqIISt"
|
||||
+ "AWzJAoIBAFDV5ZWkAIDm4EO+qpq5K2usk2/e49eDaIMd4qUHUXGMfCeVi1LvDjRA"
|
||||
+ "W2Sl0TYogqFF3+AoPjl9uj/RdHZQxto98H1yfwpwTs9CXErmRwRw9y2GIMj5LWBB"
|
||||
+ "aOQf0PUpgiFI2OrGf93cqHcLoD4WrPgmubnCnyxxa0o48Yrmy2Q/gB8vbSJ4fxxf"
|
||||
+ "92mbfbLBFNQaakeEKtbsXIZsADhtshHNPb1h7onuwy5S2sEsTlUegK77yCsDeVb3"
|
||||
+ "zBUH1WFsl257sGFRc/qvFYp4QuSfQxJA2BNiYaYUwjs+V1EWxitYACq206miSYCH"
|
||||
+ "v7xN9ntUS3cz2HNqrB/H1jN6aglnQOkCggEBAJb5FYvQCvw5PJM44nR6/U1cSlr4"
|
||||
+ "lRWcuFp7Xv5kWxSwM5115qic14fByh7DbaTHxxoPEhEA4aJ2QcDa7YWvabVc/VEV"
|
||||
+ "VacAAdg44+WSw6FNni18K53oOKAONgzSQlYUm/jgENIXi+5L0Yq7qAbnldiC6jXr"
|
||||
+ "yqbEwZjmpt8xsBLnl37k/LSLG1GUaYV8AK3s9UDs9/jv5RUrV96jiXed+7pYrjmj"
|
||||
+ "o1yJ4WAqouYHmOQCI3SeFCLT8GCdQ+uE74G5q+Yte6YT9jqSiGDjrst0bjtN640v"
|
||||
+ "YKRG3XK4AE9i4Oinnv/Ua95ql0syphn+CPW2ksmGon5/0mbK5qYsg47Hdls=" + "-----END RSA PRIVATE KEY-----";
|
||||
RsaRawEncryptor encryptor_4096 = new RsaRawEncryptor(pemData);
|
||||
assertThat(encryptor_4096.decrypt(encryptor_4096.encrypt("encryptor"))).isEqualTo("encryptor");
|
||||
}
|
||||
|
||||
private static final String SHORT_STRING = "Bacon ipsum dolor sit amet tail pork loin pork chop filet mignon flank fatback tenderloin boudin shankle corned beef t-bone short ribs. Meatball capicola ball tip short loin beef ribs shoulder, kielbasa pork chop meatloaf biltong porchetta bresaola t-bone spare ribs. Andouille t-bone sausage ground round frankfurter venison. Ground round meatball chicken ribeye doner tongue porchetta.";
|
||||
|
||||
private static String LONG_STRING;
|
||||
|
||||
}
|
||||
-121
@@ -1,121 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-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.crypto.encrypt;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class RsaSecretEncryptorTests {
|
||||
|
||||
private RsaSecretEncryptor encryptor = new RsaSecretEncryptor();
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
LONG_STRING = SHORT_STRING + SHORT_STRING + SHORT_STRING + SHORT_STRING;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
LONG_STRING = LONG_STRING + LONG_STRING;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripKey() {
|
||||
PublicKey key = RsaKeyHelper.generateKeyPair().getPublic();
|
||||
String encoded = RsaKeyHelper.encodePublicKey((RSAPublicKey) key, "application");
|
||||
assertThat(RsaKeyHelper.parsePublicKey(encoded)).isEqualTo(key);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTrip() {
|
||||
assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripWithSalt() {
|
||||
this.encryptor = new RsaSecretEncryptor(RsaAlgorithm.OAEP, "somesalt");
|
||||
assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripWithHexSalt() {
|
||||
this.encryptor = new RsaSecretEncryptor(RsaAlgorithm.OAEP, "beefea");
|
||||
assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripWithLongSalt() {
|
||||
this.encryptor = new RsaSecretEncryptor(RsaAlgorithm.OAEP, "somesaltsomesaltsomesaltsomesaltsomesalt");
|
||||
assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripOaep() {
|
||||
this.encryptor = new RsaSecretEncryptor(RsaAlgorithm.OAEP);
|
||||
assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripOaepGcm() {
|
||||
this.encryptor = new RsaSecretEncryptor(RsaAlgorithm.OAEP, true);
|
||||
assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripWithMixedAlgorithm() {
|
||||
RsaSecretEncryptor oaep = new RsaSecretEncryptor(RsaAlgorithm.OAEP);
|
||||
assertThatIllegalStateException().isThrownBy(() -> oaep.decrypt(this.encryptor.encrypt("encryptor")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripWithMixedSalt() {
|
||||
RsaSecretEncryptor other = new RsaSecretEncryptor(this.encryptor.getPublicKey(), RsaAlgorithm.DEFAULT, "salt");
|
||||
assertThatIllegalStateException().isThrownBy(() -> this.encryptor.decrypt(other.encrypt("encryptor")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripWithPublicKeyEncryption() {
|
||||
RsaSecretEncryptor encryptor = new RsaSecretEncryptor(this.encryptor.getPublicKey());
|
||||
RsaSecretEncryptor decryptor = this.encryptor;
|
||||
assertThat(decryptor.decrypt(encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publicKeyCannotDecrypt() {
|
||||
RsaSecretEncryptor encryptor = new RsaSecretEncryptor(this.encryptor.getPublicKey());
|
||||
assertThat(encryptor.canDecrypt()).as("Encryptor schould not be able to decrypt").isFalse();
|
||||
assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encryptor.encrypt("encryptor")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripLongString() {
|
||||
assertThat(this.encryptor.decrypt(this.encryptor.encrypt(LONG_STRING))).isEqualTo(LONG_STRING);
|
||||
}
|
||||
|
||||
private static final String SHORT_STRING = "Bacon ipsum dolor sit amet tail pork loin pork chop filet mignon flank fatback tenderloin boudin shankle corned beef t-bone short ribs. Meatball capicola ball tip short loin beef ribs shoulder, kielbasa pork chop meatloaf biltong porchetta bresaola t-bone spare ribs. Andouille t-bone sausage ground round frankfurter venison. Ground round meatball chicken ribeye doner tongue porchetta.";
|
||||
|
||||
private static String LONG_STRING;
|
||||
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAwClFgrRa/PUHPIJr9gvIPL6g6Rjp/TVZmVNOf2fL96DYbkj5
|
||||
@@ -1,15 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICWwIBAAKBgQDMWnfaQ0yLFXelprq2S8UurnaGvxFNUdbmTyJeycem5vGLycEY
|
||||
T4KcdVCTU5491cjbk5GcHjoj2efRSO0y0aXIlUJpLofDdML/SuGLZWp/GbEv978M
|
||||
pZIztK8iaIm7D/D7by8aws1RJyD9T+lZDAGY7eFfMp0EQyHOcEL0NGFLuwIDAQAB
|
||||
AoGAWwC6uO8ZaiKwOouqQD4z3FsDG3SA/v7ABaYd9zpCd9gGnyrEm8/kqUoxDLrD
|
||||
EGRg4y+vO2fWmlqSuoeQYf4spf+vi2di+mGIb6nGe7TpMLPa7lFLOSQHZRx5M5H6
|
||||
JDhfhAHlKmF9gLGvDHbpyErzn5YXjcu0PoFiNC1y445D8iECQQDvJzkGbJ9l9vb0
|
||||
oRyGXRDpddUcVMECLLB9NKmTl/zKy/qVPD+zYNoi87ePBJFbgmAXRjhhTk2uSBRP
|
||||
NtVaMoXLAkEA2r+ugzjsLZQIYz/9gxdzdbKWDgpSPbhKCR4bOmrDgJMcOVjtwW+n
|
||||
+liaX6zwI0QEgCAWLzCbbYDmj3kJrRwT0QJAaowg/dm7EmR7FfYJjVs9Q6X5skuY
|
||||
Se27G60wt88JExjZpU9YWgSWaugGKbOxRwHI6dWhHMkUFseKNNiLKUpFDQJALIGP
|
||||
ahdsxiE2S6s7Uy60SSAas6SZ8wDJ320GsS4DtOc5eNmFFjQ3gxH/5rNy8FnoaIEe
|
||||
wl8rYG43er1voI7z4QJAB4qaqBo7eeiRgnUVIccaSZkNIMSrZ9QUjVFRgfLwAXDO
|
||||
Ae+t6V+eB0oaIXczA+BLj3Oe6D3iHRGHrxGlcvDdHw==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,25 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAwClFgrRa/PUHPIJr9gvIPL6g6Rjp/TVZmVNOf2fL96DYbkj5
|
||||
4YbrwfKwjoTjk1M6gLQpOA4Blocx6zN5OnICnVGlVM9xymWxTxxCfc2tE2Fai9I1wchULCChhwm/UU5ZNi3KpXinlyamSYw+lMQkZ8gTXCgOEvs2j9E1quF4pvy1BZKvbD8tUnUQlyiKRnI6gOxQL8B6OAYPRdaa9FVNmrs1B4eDPG918L2f1pT090P1n+tw
|
||||
iejNgQvtSD78/A88qt89OhzscsufALTrBjycn89kkfBd0zbVLF0W6+ZVLZrf97/y
|
||||
LCoGSCcZL9LFPNvNqxOnleviDco7aOs4stQ9jQIDAQABAoIBAQC1TbthyN0YUe+T
|
||||
7dIDAbbZaVrU00biOtXgzjMADmTprP7Hf18UpIIIKfzfWw6FUD+gc1t4oe5pogE9
|
||||
UwGMXUmOORxu2pMYTb5vT9CEdexYnsAZsCo8PdD9GYSNrmquQef2MFpEqYQmHrdC
|
||||
KWpaXn2i1ak+iCRPUGp4YwHpynZVxfE8z/AIsPn6NPDh6SnCXb1rTgQe2UCfXm93
|
||||
UJe5F/OR2kQi5KFO+dxLmCOBCwr6SGCLH+VotGpuxCVRUd9sJ/d4QpDZEgjuf7Ug
|
||||
eQHfgMDS/tc09B9rl0dwKnEa31kcQ9X9KLkKP+w0Pqhh0Emny20eg9jS6XNayg61
|
||||
p/LQtW9BAoGBAO5veKMIcXfZmuh11WIIhdhLKkNtYyt5NDmrV8/IVScLFvjB0ftt
|
||||
8PAtXo/ekOHkyITyIumQ9l4VCvacNw7DyV9FYk4WvrvVYOCL8aZi+O5+12NT67eO
|
||||
Rr/voGlRoV05X7+inc90qbbYJ8lRmLSqvzmsm98mkuhw/FKGRhVZIfAJAoGBAM5R
|
||||
I5vK6cJxOwXQOEGOd5/8B9JMFXyuendXo/N2/NxSQsbx4pc3v2rv/eGJYaY7Nx/y
|
||||
2M/vdWYkpG59PAS3k2TrCA/0SGmyVqY+c8BomKisU5VaBlIPfGuec9tDPgWCp8Ur
|
||||
3Jjt/2sVoa0vMkqymUqMb9HyH9tdI9oyh7EOOrplAoGAR6DlNNUMgVy11K/Rcqns
|
||||
y5WJFMh/ykeXENwQfTNJoXkLZZ+UXVwhzYVTqxTJoZMBSi8TnecWnBzmNj+nqp/W
|
||||
lvBZH+xlUDhB6jMgXUPOVJd2TTigz3vGdVKfdgQ33bGmugM4NWJuuacmDKyem2fQ
|
||||
GptoGBmWeI24v3HnC/LC50ECgYAz0iN8hRnz0db+Xc9TgAJB997LDnszJuvxv9yZ
|
||||
UWCvwiWtrKG6U7FLnd4J4STayPLOnoOgrsexETEP43rIwIdQCMysnTH3AmlLNlKC
|
||||
mIMHksknsUX3JJaevVziTOBuJ+QV3S96ZgUKk5NZWYprQrLIC8AmXodr5NgVfS2h
|
||||
5i4QFQKBgFfbYHiMw5AAUQrBNkrAjLd1wIaO/6qS3w4OsCWKowhfaJLEXAbIRV7s
|
||||
vAtgtlCovdasVj4RRLXFf+73naVTQjBZI+3jWHHyFk3+Zy86mQCSGv9WuDVV1IhS
|
||||
h8InTVvK8wgdgX7qiw3pvU0roqNW4/j4j8OqJO3Zt4KO2iX8htsO
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -129,8 +129,6 @@
|
||||
** Authentication
|
||||
*** xref:reactive/authentication/x509.adoc[X.509 Authentication]
|
||||
*** xref:reactive/authentication/logout.adoc[Logout]
|
||||
*** Session Management
|
||||
**** xref:reactive/authentication/concurrent-sessions-control.adoc[Concurrent Sessions Control]
|
||||
** Authorization
|
||||
*** xref:reactive/authorization/authorize-http-requests.adoc[Authorize HTTP Requests]
|
||||
*** xref:reactive/authorization/method.adoc[EnableReactiveMethodSecurity]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user