+
+
+
+
\ No newline at end of file
diff --git a/.sdkmanrc b/.sdkmanrc
new file mode 100644
index 0000000000..93565edd75
--- /dev/null
+++ b/.sdkmanrc
@@ -0,0 +1,21 @@
+#
+# Copyright 2005-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.
+# You may obtain a copy of the License at
+#
+# http://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.
+#
+# Use `sdk env` to manually apply this file.
+# Set `sdkman_auto_env=true` in $HOME/.sdkman/etc/config to make it automatic.
+#
+# NOTE: Switching branches will NOT trigger a change. Only switching folder will do it. Use `sdk env` to apply when simply switching branches.
+
+java=8.0.402-librca
diff --git a/.settings.xml b/.settings.xml
index 6066f6436c..2cd7c001a7 100644
--- a/.settings.xml
+++ b/.settings.xml
@@ -3,27 +3,44 @@
repo.spring.io
- ${env.CI_DEPLOY_USERNAME}
- ${env.CI_DEPLOY_PASSWORD}
+ ${env.ARTIFACTORY_USERNAME}
+ ${env.ARTIFACTORY_PASSWORD}
+
+
+ spring-snapshots
+ ${env.ARTIFACTORY_USERNAME}
+ ${env.ARTIFACTORY_PASSWORD}
+
+
+ spring-milestones
+ ${env.ARTIFACTORY_USERNAME}
+ ${env.ARTIFACTORY_PASSWORD}
+
+
+ spring-staging
+ ${env.ARTIFACTORY_USERNAME}
+ ${env.ARTIFACTORY_PASSWORD}
-
springtrue
+
+ maven-central
+ Maven Central
+ https://repo.maven.apache.org/maven2
+
+ false
+
+ spring-snapshotsSpring Snapshots
- https://repo.spring.io/libs-snapshot-local
+ https://repo.spring.io/snapshottrue
@@ -31,25 +48,25 @@
spring-milestonesSpring Milestones
- https://repo.spring.io/libs-milestone-local
+ https://repo.spring.io/milestonefalse
-
- spring-releases
- Spring Releases
- https://repo.spring.io/release
+
+
+
+ maven-central
+ Maven Central
+ https://repo.maven.apache.org/maven2false
-
-
-
+ spring-snapshotsSpring Snapshots
- https://repo.spring.io/libs-snapshot-local
+ https://repo.spring.io/snapshottrue
@@ -57,7 +74,7 @@
spring-milestonesSpring Milestones
- https://repo.spring.io/libs-milestone-local
+ https://repo.spring.io/milestonefalse
@@ -65,4 +82,5 @@
+
diff --git a/.springjavaformatconfig b/.springjavaformatconfig
new file mode 100644
index 0000000000..12643781ce
--- /dev/null
+++ b/.springjavaformatconfig
@@ -0,0 +1 @@
+java-baseline=8
\ No newline at end of file
diff --git a/.trivyignore b/.trivyignore
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 14d8a33dca..d2656754a8 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -29,6 +29,13 @@
"mainClass": "org.springframework.cloud.dataflow.server.single.DataFlowServerApplication",
"projectName": "spring-cloud-dataflow-server",
"args": "--spring.config.additional-location=src/config/scdf-mysql.yml"
+ },
+ {
+ "type": "java",
+ "name": "SCDF Debug Attach",
+ "request": "attach",
+ "hostName": "localhost",
+ "port": 5005
}
]
}
\ No newline at end of file
diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc
new file mode 100755
index 0000000000..280d74f428
--- /dev/null
+++ b/CONTRIBUTING.adoc
@@ -0,0 +1,55 @@
+= Contributing to Spring Cloud Dataflow
+
+:github: https://github.com/spring-cloud/spring-cloud-dataflow
+
+Spring Cloud Dataflow is released under the Apache 2.0 license. If you would like to contribute something, or want to hack on the code this document should help you get started.
+
+
+
+== Code of Conduct
+This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of conduct].
+By participating, you are expected to uphold this code. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io.
+
+
+
+== Using GitHub Issues
+We use GitHub issues to track bugs and enhancements.
+If you have a general usage question please ask on https://stackoverflow.com[Stack Overflow].
+The Spring Cloud Dataflow team and the broader community monitor the https://stackoverflow.com/tags/spring-cloud-dataflow[`spring-cloud-dataflow`] tag.
+
+If you are reporting a bug, please help to speed up problem diagnosis by providing as much information as possible.
+Ideally, that would include a small sample project that reproduces the problem.
+
+
+
+== Reporting Security Vulnerabilities
+If you think you have found a security vulnerability in Spring Pulsar please *DO NOT* disclose it publicly until we've had a chance to fix it.
+Please don't report security vulnerabilities using GitHub issues, instead head over to https://spring.io/security-policy and learn how to disclose them responsibly.
+
+
+
+== Sign the Contributor License Agreement
+Before we accept a non-trivial patch or pull request we will need you to https://cla.pivotal.io/sign/spring[sign the Contributor License Agreement].
+Signing the contributor's agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do.
+Active contributors might be asked to join the core team, and given the ability to merge pull requests.
+
+
+=== Code Conventions and Housekeeping
+
+None of the following guidelines is essential for a pull request, but they all help your fellow developers understand and work with your code.
+They can also be added after the original pull request but before a merge.
+
+* Use the Spring Framework code format conventions. If you use Eclipse, you can import formatter settings by using the `eclipse-code-formatter.xml` file from the https://github.com/spring-cloud/spring-cloud-build/blob/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml[Spring Cloud Build] project.
+If you use IntelliJ, you can use the https://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter Plugin] to import the same file.
+* Make sure all new `.java` files have a simple Javadoc class comment with at least an `@author` tag identifying you, and preferably at least a paragraph describing the class's purpose.
+* Add the ASF license header comment to all new `.java` files (to do so, copy it from existing files in the project).
+* Add yourself as an `@author` to the .java files that you modify substantially (more than cosmetic changes).
+* Add some Javadocs and, if you change the namespace, some XSD doc elements.
+* A few unit tests would help a lot as well. Someone has to do it, and your fellow developers appreciate the effort.
+* If no one else uses your branch, rebase it against the current master (or other target branch in the main project).
+* When writing a commit message, follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions].
+If you fix an existing issue, add `Fixes gh-XXXX` (where XXXX is the issue number) at the end of the commit message.
+
+
+== Working with the Code
+For information on editing, building, and testing the code, see the link:${github}/wiki/Working-with-the-Code[Working with the Code] page on the project wiki.
diff --git a/README.md b/README.md
index 16621a73f3..bda8322737 100644
--- a/README.md
+++ b/README.md
@@ -4,21 +4,8 @@
+[](https://github.com/spring-cloud/spring-cloud-dataflow/actions/workflows/ci.yml)
+
*Spring Cloud Data Flow* is a microservices-based toolkit for building streaming and batch data processing pipelines in
Cloud Foundry and Kubernetes.
@@ -66,24 +53,13 @@ For example, if relying on Maven coordinates, an application URI would be of the
connects to the Spring Cloud Data Flow Server's REST API and supports a DSL that simplifies the process of defining a
stream or task and managing its lifecycle.
-**Community Implementations**: There are also community maintained Spring Cloud Data Flow implementations that are currently
-based on the 1.7.x series of Spring Cloud Data Flow.
-
- * [HashiCorp Nomad](https://github.com/donovanmuller/spring-cloud-dataflow-server-nomad)
- * [OpenShift](https://github.com/donovanmuller/spring-cloud-dataflow-server-openshift)
- * [Apache Mesos](https://github.com/trustedchoice/spring-cloud-dataflow-server-mesos)
-
-The [Apache YARN](https://github.com/spring-cloud/spring-cloud-dataflow-server-yarn) implementation has reached end-of-life
-status. Let us know at [Gitter](https://gitter.im/spring-cloud/spring-cloud-dataflow) if you are interested in forking
-the project to continue developing and maintaining it.
-
----
## Building
Clone the repo and type
- $ ./mvnw clean install
+ $ ./mvnw -s .settings.xml clean install
Looking for more information? Follow this [link](https://github.com/spring-cloud/spring-cloud-dataflow/blob/master/spring-cloud-dataflow-docs/src/main/asciidoc/appendix-building.adoc).
@@ -98,9 +74,48 @@ For more information please refer to the [Git documentation, Formatting and Whit
----
+## Running Locally w/ Oracle
+By default, the Dataflow server jar does not include the Oracle database driver dependency.
+If you want to use Oracle for development/testing when running locally, you can specify the `local-dev-oracle` Maven profile when building.
+The following command will include the Oracle driver dependency in the jar:
+```
+$ ./mvnw -s .settings.xml clean package -Plocal-dev-oracle
+```
+You can follow the steps in the [Oracle on Mac ARM64](https://github.com/spring-cloud/spring-cloud-dataflow/wiki/Oracle-on-Mac-ARM64#run-container-in-docker) Wiki to run Oracle XE locally in Docker with Dataflow pointing at it.
+
+> **NOTE:** If you are not running Mac ARM64 just skip the steps related to Homebrew and Colima
+
+----
+
+## Running Locally w/ Microsoft SQL Server
+By default, the Dataflow server jar does not include the MSSQL database driver dependency.
+If you want to use MSSQL for development/testing when running locally, you can specify the `local-dev-mssql` Maven profile when building.
+The following command will include the MSSQL driver dependency in the jar:
+```
+$ ./mvnw -s .settings.xml clean package -Plocal-dev-mssql
+```
+You can follow the steps in the [MSSQL on Mac ARM64](https://github.com/spring-cloud/spring-cloud-dataflow/wiki/MSSQL-on-Mac-ARM64#running-dataflow-locally-against-mssql) Wiki to run MSSQL locally in Docker with Dataflow pointing at it.
+
+> **NOTE:** If you are not running Mac ARM64 just skip the steps related to Homebrew and Colima
+
+----
+
+## Running Locally w/ IBM DB2
+By default, the Dataflow server jar does not include the DB2 database driver dependency.
+If you want to use DB2 for development/testing when running locally, you can specify the `local-dev-db2` Maven profile when building.
+The following command will include the DB2 driver dependency in the jar:
+```
+$ ./mvnw -s .settings.xml clean package -Plocal-dev-db2
+```
+You can follow the steps in the [DB2 on Mac ARM64](https://github.com/spring-cloud/spring-cloud-dataflow/wiki/DB2-on-Mac-ARM64#running-dataflow-locally-against-db2) Wiki to run DB2 locally in Docker with Dataflow pointing at it.
+
+> **NOTE:** If you are not running Mac ARM64 just skip the steps related to Homebrew and Colima
+
+----
+
## Contributing
-We welcome contributions! Follow this [link](https://github.com/spring-cloud/spring-cloud-dataflow/blob/master/spring-cloud-dataflow-docs/src/main/asciidoc/appendix-contributing.adoc) for more information on how to contribute.
+We welcome contributions! See the [CONTRIBUTING](./CONTRIBUTING.adoc) guide for details.
----
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000000..8a9410d248
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,5 @@
+# Security Policy
+## Reporting a Vulnerability
+
+If you think you have found a security vulnerability, please **DO NOT** disclose it publicly until we’ve had a chance to fix it.
+Please don’t report security vulnerabilities using GitHub issues, instead head over to https://spring.io/security-policy and learn how to disclose them responsibly.
diff --git a/build-carvel-package.sh b/build-carvel-package.sh
new file mode 100755
index 0000000000..e0a0d263f2
--- /dev/null
+++ b/build-carvel-package.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+
+function create_and_clear() {
+ rm -rf "$1"
+ mkdir -p "$1"
+}
+
+SCDIR=$(realpath $(dirname "$(readlink -f "${BASH_SOURCE[0]}")"))
+set -euxo pipefail
+pushd $SCDIR > /dev/null
+export DATAFLOW_VERSION=$(./mvnw help:evaluate -o -Dexpression=project.version -q -DforceStdout)
+export SKIPPER_VERSION=$(./mvnw help:evaluate -o -Dexpression=spring-cloud-skipper.version -pl spring-cloud-dataflow-parent -q -DforceStdout)
+
+if [ "$PACKAGE_VERSION" = "" ]; then
+ export PACKAGE_VERSION=$DATAFLOW_VERSION
+fi
+
+# you can launch a local docker registry using docker run -d -p 5000:5000 --name registry registry:2.7
+# export REPO_PREFIX=":5000/"
+readonly REPO_PREFIX="${REPO_PREFIX:-docker.io/}"
+
+export PACKAGE_BUNDLE_REPOSITORY="${REPO_PREFIX}springcloud/scdf-oss-package"
+export REPOSITORY_BUNDLE="${REPO_PREFIX}springcloud/scdf-oss-repo"
+
+export SKIPPER_REPOSITORY="springcloud/spring-cloud-skipper-server"
+export SERVER_REPOSITORY="springcloud/spring-cloud-dataflow-server"
+export CTR_VERSION=$DATAFLOW_VERSION
+export PACKAGE_NAME="scdf"
+export PACKAGE_BUNDLE_TEMPLATE="src/carvel/templates/bundle/package"
+export IMGPKG_LOCK_TEMPLATE="src/carvel/templates/imgpkg"
+export VENDIR_SRC_IN="src/carvel/config"
+export SERVER_VERSION="$DATAFLOW_VERSION"
+
+export PACKAGE_BUNDLE_GENERATED=/tmp/generated/packagebundle
+export IMGPKG_LOCK_GENERATED_IN=/tmp/generated/imgpkgin
+export IMGPKG_LOCK_GENERATED_OUT=/tmp/generated/imgpkgout
+create_and_clear $PACKAGE_BUNDLE_GENERATED
+create_and_clear $IMGPKG_LOCK_GENERATED_IN
+create_and_clear $IMGPKG_LOCK_GENERATED_OUT
+
+echo "bundle-path=$PACKAGE_BUNDLE_GENERATED"
+export SCDF_DIR="$SCDIR"
+
+sh "$SCDIR/.github/actions/build-package-bundle/build-package-bundle.sh"
+
+imgpkg push --bundle "$PACKAGE_BUNDLE_REPOSITORY:$PACKAGE_VERSION" --file "$PACKAGE_BUNDLE_GENERATED"
+
+export REPO_BUNDLE_TEMPLATE="src/carvel/templates/bundle/repo"
+
+export REPO_BUNDLE_RENDERED=/tmp/generated/reporendered
+export REPO_BUNDLE_GENERATED=/tmp/generated/repobundle
+create_and_clear $REPO_BUNDLE_RENDERED
+create_and_clear $REPO_BUNDLE_GENERATED
+
+sh "$SCDIR/.github/actions/build-repository-bundle/build-repository-bundle.sh"
+
+imgpkg push --bundle "$REPOSITORY_BUNDLE:$PACKAGE_VERSION" --file "$REPO_BUNDLE_GENERATED"
+
+popd
diff --git a/build-containers.sh b/build-containers.sh
new file mode 100755
index 0000000000..77b9a21662
--- /dev/null
+++ b/build-containers.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+./mvnw install -s .settings.xml -DskipTests -T 1C -am -pl :spring-cloud-dataflow-server,:spring-cloud-skipper-server,:spring-cloud-dataflow-composed-task-runner
+./mvnw spring-boot:build-image -s .settings.xml -DskipTests -T 1C -pl :spring-cloud-dataflow-server,:spring-cloud-skipper-server,:spring-cloud-dataflow-composed-task-runner
\ No newline at end of file
diff --git a/lib/spring-doc-resources-0.2.5.zip b/lib/spring-doc-resources-0.2.5.zip
new file mode 100644
index 0000000000..b1ff602652
Binary files /dev/null and b/lib/spring-doc-resources-0.2.5.zip differ
diff --git a/models/batch4-5-simple.adoc b/models/batch4-5-simple.adoc
new file mode 100644
index 0000000000..3ee7cdd389
--- /dev/null
+++ b/models/batch4-5-simple.adoc
@@ -0,0 +1,16 @@
+= Simple solution
+
+* SchemaTarget Selection represents a set of schema version, prefix and name.
+* Boot 2 is default and task and batch will remain as current.
+* Boot 3 task and batch tables will have the same prefix BOOT3_
+* Data flow server will set the properties for prefixes for task and batch.
+* Registration will require Schema (Boot2, Boot3) selection indicator.
+* At task launch data flow server will create an entry in the correct task-exectution table and sequence mechanism with given prefix based on registration of task.
+* Ability to disable Boot 3 support. The feature endpoint will include this indicator.
+* The endpoints to list job and task executions will have to accept the BootVersion as an query parameter when it is absent is implies the default condition. `http://localhost:9393/tasks/executions{?schemaTarget}`
+* When using the shell to list executions it will be an optional parameter `--schema-target=boot3`
+* When viewing the Task Execution list or Job Execution list there will be a drop-down with the option of Default and Boot3.
+* The each item in the list of executions do include links to retrieve the entity, and will be encoded with the schemaTarget by the resource assembler.
+
+* The UI only needs to add the drop-downs and passing selection into the query.
+* The user will not have to do anything extra when creating composed tasks.
diff --git a/pom.xml b/pom.xml
index 6f13af2a7b..231aebbb60 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,20 +2,17 @@
4.0.0
- spring-cloud-dataflow-parent
- 2.9.2-SNAPSHOT
+ org.springframework.cloud
+ spring-cloud-dataflow
+ 2.11.6-SNAPSHOT
+ spring-cloud-dataflow
+ Spring Cloud Dataflowpomhttps://cloud.spring.io/spring-cloud-dataflow/Pivotal Software, Inc.https://www.spring.io
-
- org.springframework.cloud
- spring-cloud-dataflow-build
- 2.9.2-SNAPSHOT
-
- Apache License, Version 2.0
@@ -52,410 +49,121 @@
https://github.com/spring-cloud/spring-cloud-dataflow/graphs/contributors
-
- 1.8
- -Xdoclint:none
-
- 3.2.2-SNAPSHOT
-
- 2.9.2-SNAPSHOT
- 2.7.2-SNAPSHOT
- 2.7.2-SNAPSHOT
- 2.7.2-SNAPSHOT
- 2.7.2-SNAPSHOT
-
- 2.8.2-SNAPSHOT
-
- 2.3.4
-
-
- 2.3.7.RELEASE
-
- 1.7.2-SNAPSHOT
-
- 1.2.0.RELEASE
-
- 0.8.5
- 3.0.2
- 2.2.0
- 1.5.5
- 0.5
- 2.11.1
- 3.0.2
- 2.10.6
- 1.11.731
- 1.15.2
-
- 3.0.2
- 2.2.0
- 1.0.4
- 1.0.4
-
+ spring-cloud-dataflow-parent
+ spring-cloud-dataflow-build
+ spring-cloud-dataflow-common
+ spring-cloud-common-security-configspring-cloud-dataflow-container-registryspring-cloud-dataflow-configuration-metadataspring-cloud-dataflow-core-dsl
+ spring-cloud-dataflow-schema-corespring-cloud-dataflow-core
- spring-cloud-dataflow-registry
- spring-cloud-dataflow-rest-resource
- spring-cloud-dataflow-single-step-batch-job
- spring-cloud-dataflow-composed-task-runner
+ spring-cloud-dataflow-schema
+ spring-cloud-dataflow-aggregate-taskspring-cloud-dataflow-server-core
+ spring-cloud-dataflow-rest-resource
+ spring-cloud-dataflow-audit
+ spring-cloud-dataflow-registry
+ spring-cloud-dataflow-platform-kubernetes
+ spring-cloud-dataflow-platform-cloudfoundryspring-cloud-dataflow-autoconfigure
- spring-cloud-dataflow-serverspring-cloud-dataflow-rest-clientspring-cloud-dataflow-shellspring-cloud-dataflow-shell-core
- spring-cloud-dataflow-classic-docs
- spring-cloud-dataflow-docsspring-cloud-dataflow-completion
- spring-cloud-dataflow-dependencies
- spring-cloud-dataflow-platform-kubernetes
- spring-cloud-dataflow-platform-cloudfoundry
+ spring-cloud-skipperspring-cloud-starter-dataflow-serverspring-cloud-starter-dataflow-ui
- spring-cloud-dataflow-audit
- spring-cloud-dataflow-test
+ spring-cloud-dataflow-serverspring-cloud-dataflow-tasklauncher
+ spring-cloud-dataflow-single-step-batch-job
+ spring-cloud-dataflow-composed-task-runner
+ spring-cloud-dataflow-test
+ spring-cloud-dataflow-dependencies
+ spring-cloud-dataflow-classic-docs
+ spring-cloud-dataflow-docs
+ spring-cloud-dataflow-package
-
-
-
- org.springframework.cloud
- spring-cloud-dataflow-common-dependencies
- ${spring-cloud-dataflow-common.version}
- pom
- import
-
-
- org.springframework.cloud
- spring-cloud-task-dependencies
- ${spring-cloud-task.version}
- pom
- import
-
-
- org.springframework.cloud
- spring-cloud-starter-single-step-batch-job
- ${spring-cloud-task.version}
-
-
- org.springframework.cloud
- spring-cloud-skipper-dependencies
- ${spring-cloud-skipper.version}
- pom
- import
-
-
- org.springframework.cloud
- spring-cloud-dataflow-dependencies
- 2.9.2-SNAPSHOT
- pom
- import
-
-
- org.testcontainers
- testcontainers-bom
- ${testcontainers.version}
- pom
- import
-
-
- org.springframework.cloud
- spring-cloud-dataflow-ui
- ${spring-cloud-dataflow-ui.version}
-
-
- org.springframework.cloud
- spring-cloud-deployer-spi
- ${spring-cloud-deployer.version}
-
-
- org.springframework.cloud
- spring-cloud-deployer-resource-support
- ${spring-cloud-deployer.version}
-
-
- org.springframework.cloud
- spring-cloud-deployer-resource-maven
- ${spring-cloud-deployer.version}
-
-
- org.springframework.cloud
- spring-cloud-deployer-resource-docker
- ${spring-cloud-deployer.version}
-
-
- org.springframework.cloud
- spring-cloud-deployer-local
- ${spring-cloud-deployer-local.version}
-
-
- org.springframework.cloud
- spring-cloud-deployer-cloudfoundry
- ${spring-cloud-deployer-cloudfoundry.version}
-
-
- org.springframework.shell
- spring-shell
- ${spring-shell.version}
-
-
- org.springframework.cloud
- spring-cloud-starter-common-security-config-web
- ${spring-cloud-common-security-config.version}
-
-
- org.springframework.cloud
- spring-cloud-deployer-kubernetes
- ${spring-cloud-deployer-kubernetes.version}
-
-
- org.apache.directory.server
- apacheds-protocol-ldap
- ${apache-directory-server.version}
-
-
- io.codearte.props2yaml
- props2yaml
- ${codearte-props2yml.version}
-
-
- org.springframework.security.oauth
- spring-security-oauth2
- ${spring-security-oauth2.version}
-
-
- net.javacrumbs.json-unit
- json-unit-assertj
- ${json-unit.version}
-
-
- com.google.code.findbugs
- jsr305
- ${findbugs.version}
-
-
- joda-time
- joda-time
- ${joda-time.version}
-
-
- com.amazonaws
- aws-java-sdk-ecr
- ${aws-java-sdk-ecr.version}
-
-
-
- com.wavefront
- wavefront-spring-boot-bom
- ${wavefront-spring-boot-bom.version}
- pom
- import
-
-
- org.springframework.cloud.stream.app
- stream-applications-micrometer-common
- ${stream-applications.version}
-
-
- org.springframework.cloud.stream.app
- stream-applications-security-common
- ${stream-applications.version}
-
-
- org.springframework.cloud.stream.app
- stream-applications-postprocessor-common
- ${stream-applications.version}
-
-
- org.springframework.cloud
- spring-cloud-deployer-dependencies
- ${spring-cloud-deployer.version}
- pom
- import
-
-
-
+
+
+ org.codehaus.groovy
+ groovy-eclipse-batch
+ 3.0.8-01
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+
+
+
+
+ org.codehaus.groovy
+ groovy-all
+ 3.0.19
+ pom
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+
+
+ org.junit.platform
+ junit-platform-launcher
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+
+
+
+
+ org.apache.maven.plugins
- maven-surefire-plugin
- 2.22.1
+ maven-compiler-plugin
+ 3.11.0
-
- **/*Tests.java
- **/*Test.java
-
-
- **/Abstract*.java
-
-
- ${argLine}
+ 1.8
+ 1.8
- org.jacoco
- jacoco-maven-plugin
+ org.codehaus.gmaven
+ groovy-maven-plugin
+ 2.1.1
+
+
+ org.codehaus.groovy
+ groovy-eclipse-batch
+ 3.0.8-01
+
+
+ org.codehaus.groovy
+ groovy-all
+ 3.0.19
+ pom
+
+
- agent
-
- prepare-agent
-
-
-
- report
- test
+ validate
- report
+ execute
+
+
+ ${project.basedir}
+
+ ${project.basedir}/src/test/groovy/check-pom.groovy
+
-
- org.apache.maven.plugins
- maven-checkstyle-plugin
-
-
-
-
- org.springframework.boot
- spring-boot-maven-plugin
- ${spring-boot.version}
-
-
- org.sonarsource.scanner.maven
- sonar-maven-plugin
- ${sonar-maven-plugin.version}
-
-
- org.jacoco
- jacoco-maven-plugin
- ${jacoco-maven-plugin.version}
-
-
- org.apache.maven.plugins
- maven-jar-plugin
- 3.0.2
-
-
- org.apache.maven.plugins
- maven-source-plugin
- 3.0.1
-
-
- org.springframework.cloud
- spring-cloud-dataflow-apps-docs-plugin
- ${spring-cloud-dataflow-apps-docs-plugin.version}
-
-
- generate-documentation
- verify
-
- generate-documentation
-
-
-
-
-
- org.springframework.cloud
- spring-cloud-dataflow-apps-metadata-plugin
- ${spring-cloud-dataflow-apps-metadata-plugin.version}
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-jxr-plugin
- 2.5
-
-
-
-
-
- deploymentfiles
-
-
-
- maven-resources-plugin
-
-
- replace-deployment-files
- process-resources
-
- copy-resources
-
-
- true
- ${basedir}/src
-
-
- ${basedir}/src/templates
-
- **/*
-
- true
-
-
-
-
-
-
-
-
-
-
- spring
-
-
- spring-snapshots
- Spring Snapshots
- https://repo.spring.io/libs-snapshot
-
- true
-
-
-
- spring-milestones
- Spring Milestones
- https://repo.spring.io/libs-milestone-local
-
- false
-
-
-
- spring-releases
- Spring Releases
- https://repo.spring.io/release
-
- false
-
-
-
-
-
- spring-snapshots
- Spring Snapshots
- https://repo.spring.io/libs-snapshot-local
-
- true
-
-
-
- spring-milestones
- Spring Milestones
- https://repo.spring.io/libs-milestone-local
-
- false
-
-
-
-
-
diff --git a/spring-cloud-common-security-config/README.md b/spring-cloud-common-security-config/README.md
new file mode 100644
index 0000000000..5466106ed9
--- /dev/null
+++ b/spring-cloud-common-security-config/README.md
@@ -0,0 +1,3 @@
+# Spring Cloud Common Security
+
+This repo holds the security configuration classes that are common across Spring Cloud (Spring Cloud Data Flow/Skipper for now) projects that use **Role** based authentication/authorization for their runtime server application(s).
diff --git a/spring-cloud-common-security-config/pom.xml b/spring-cloud-common-security-config/pom.xml
new file mode 100644
index 0000000000..a2bfe1f9e6
--- /dev/null
+++ b/spring-cloud-common-security-config/pom.xml
@@ -0,0 +1,23 @@
+
+
+ 4.0.0
+ spring-cloud-common-security-config
+ 2.11.6-SNAPSHOT
+ pom
+ spring-cloud-common-security-config
+ Spring Cloud Common Security Config
+
+
+ org.springframework.cloud
+ spring-cloud-dataflow-parent
+ 2.11.6-SNAPSHOT
+ ../spring-cloud-dataflow-parent
+
+
+
+ spring-cloud-common-security-config-core
+ spring-cloud-common-security-config-web
+ spring-cloud-starter-common-security-config-web
+
+
+
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-core/pom.xml b/spring-cloud-common-security-config/spring-cloud-common-security-config-core/pom.xml
new file mode 100644
index 0000000000..930e4260ec
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-core/pom.xml
@@ -0,0 +1,69 @@
+
+
+ 4.0.0
+
+ org.springframework.cloud
+ spring-cloud-common-security-config
+ 2.11.6-SNAPSHOT
+
+ spring-cloud-common-security-config-core
+ spring-cloud-common-security-config-core
+ Spring Cloud Common Security Config Core
+ jar
+
+ true
+ 3.4.1
+
+
+
+ org.springframework.security
+ spring-security-oauth2-client
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ javax.servlet
+ javax.servlet-api
+ provided
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.0
+
+
+ source
+
+ jar
+
+ package
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ ${maven-javadoc-plugin.version}
+
+ -Xdoclint:none
+
+
+
+ javadoc
+
+ jar
+
+ package
+
+
+
+
+
+
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-core/src/main/java/org/springframework/cloud/common/security/core/support/OAuth2AccessTokenProvidingClientHttpRequestInterceptor.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-core/src/main/java/org/springframework/cloud/common/security/core/support/OAuth2AccessTokenProvidingClientHttpRequestInterceptor.java
new file mode 100644
index 0000000000..33bce77f53
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-core/src/main/java/org/springframework/cloud/common/security/core/support/OAuth2AccessTokenProvidingClientHttpRequestInterceptor.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018-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.
+ * 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.cloud.common.security.core.support;
+
+import java.io.IOException;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpRequest;
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.util.Assert;
+
+/**
+ * This implementation of a {@link ClientHttpRequestInterceptor} will retrieve, if available, the OAuth2 Access Token
+ * and add it to the {@code Authorization} HTTP header.
+ *
+ * @author Gunnar Hillert
+ */
+public class OAuth2AccessTokenProvidingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
+
+ private final String staticOauthAccessToken;
+
+ private final OAuth2TokenUtilsService oauth2TokenUtilsService;
+
+ public OAuth2AccessTokenProvidingClientHttpRequestInterceptor(String staticOauthAccessToken) {
+ super();
+ Assert.hasText(staticOauthAccessToken, "staticOauthAccessToken must not be null or empty.");
+ this.staticOauthAccessToken = staticOauthAccessToken;
+ this.oauth2TokenUtilsService = null;
+ }
+
+ public OAuth2AccessTokenProvidingClientHttpRequestInterceptor(OAuth2TokenUtilsService oauth2TokenUtilsService) {
+ super();
+ this.oauth2TokenUtilsService = oauth2TokenUtilsService;
+ this.staticOauthAccessToken = null;
+ }
+
+ @Override
+ public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
+ throws IOException {
+
+ final String tokenToUse;
+
+ if (this.staticOauthAccessToken != null) {
+ tokenToUse = this.staticOauthAccessToken;
+ }
+ else if (this.oauth2TokenUtilsService != null){
+ tokenToUse = this.oauth2TokenUtilsService.getAccessTokenOfAuthenticatedUser();
+ }
+ else {
+ tokenToUse = null;
+ }
+
+ if (tokenToUse != null) {
+ request.getHeaders().add(HttpHeaders.AUTHORIZATION, OAuth2AccessToken.TokenType.BEARER.getValue() + " " + tokenToUse);
+ }
+ return execution.execute(request, body);
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-core/src/main/java/org/springframework/cloud/common/security/core/support/OAuth2TokenUtilsService.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-core/src/main/java/org/springframework/cloud/common/security/core/support/OAuth2TokenUtilsService.java
new file mode 100644
index 0000000000..f03ba97f8a
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-core/src/main/java/org/springframework/cloud/common/security/core/support/OAuth2TokenUtilsService.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2019-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.
+ * 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.cloud.common.security.core.support;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+
+/**
+ * Service providing OAuth2 Security-related utility methods that may
+ * required other Spring Security services.
+ *
+ * @author Gunnar Hillert
+ * @author Corneil du Plessis
+ *
+ */
+public interface OAuth2TokenUtilsService {
+
+ /**
+ * Retrieves the access token from the {@link Authentication} implementation.
+ *
+ * @return Should never return null.
+ */
+ String getAccessTokenOfAuthenticatedUser();
+
+ /**
+ *
+ * @return A client for the token.
+ */
+ OAuth2AuthorizedClient getAuthorizedClient(OAuth2AuthenticationToken auth2AuthenticationToken);
+
+ /**
+ *
+ * @param auth2AuthorizedClient Remove a client
+ */
+ void removeAuthorizedClient(OAuth2AuthorizedClient auth2AuthorizedClient);
+
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-core/src/test/java/org/springframework/cloud/common/security/core/support/OAuth2AccessTokenProvidingClientHttpRequestInterceptorTests.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-core/src/test/java/org/springframework/cloud/common/security/core/support/OAuth2AccessTokenProvidingClientHttpRequestInterceptorTests.java
new file mode 100644
index 0000000000..16456705fa
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-core/src/test/java/org/springframework/cloud/common/security/core/support/OAuth2AccessTokenProvidingClientHttpRequestInterceptorTests.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2018-2020 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.cloud.common.security.core.support;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpRequest;
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ *
+ * @author Gunnar Hillert
+ * @author Corneil du Plessis
+ */
+class OAuth2AccessTokenProvidingClientHttpRequestInterceptorTests {
+
+ @Test
+ void testOAuth2AccessTokenProvidingClientHttpRequestInterceptorWithEmptyConstructior() {
+ try {
+ new OAuth2AccessTokenProvidingClientHttpRequestInterceptor("");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("staticOauthAccessToken must not be null or empty.", e.getMessage());
+ return;
+ }
+ fail("Expected an IllegalArgumentException to be thrown.");
+ }
+
+ @Test
+ void testOAuth2AccessTokenProvidingClientHttpRequestInterceptorWithStaticTokenConstructor() {
+ final OAuth2AccessTokenProvidingClientHttpRequestInterceptor interceptor =
+ new OAuth2AccessTokenProvidingClientHttpRequestInterceptor("foobar");
+
+ final String accessToken = (String) ReflectionTestUtils.getField(interceptor, "staticOauthAccessToken");
+ assertEquals("foobar", accessToken);
+ }
+
+ @Test
+ void testInterceptWithStaticToken() throws IOException {
+ final OAuth2AccessTokenProvidingClientHttpRequestInterceptor interceptor =
+ new OAuth2AccessTokenProvidingClientHttpRequestInterceptor("foobar");
+ final HttpHeaders headers = setupTest(interceptor);
+
+ assertEquals(1, headers.size());
+ assertEquals("Bearer foobar", headers.get("Authorization").get(0));
+ }
+
+ @Test
+ void testInterceptWithAuthentication() throws IOException {
+ final OAuth2TokenUtilsService oauth2TokenUtilsService = mock(OAuth2TokenUtilsService.class);
+ when(oauth2TokenUtilsService.getAccessTokenOfAuthenticatedUser()).thenReturn("foo-bar-123-token");
+
+ final OAuth2AccessTokenProvidingClientHttpRequestInterceptor interceptor =
+ new OAuth2AccessTokenProvidingClientHttpRequestInterceptor(oauth2TokenUtilsService);
+ final HttpHeaders headers = setupTest(interceptor);
+
+ assertEquals(1, headers.size());
+ assertEquals("Bearer foo-bar-123-token", headers.get("Authorization").get(0));
+ }
+
+ @Test
+ void testInterceptWithAuthenticationAndStaticToken() throws IOException {
+ final OAuth2TokenUtilsService oauth2TokenUtilsService = mock(OAuth2TokenUtilsService.class);
+ when(oauth2TokenUtilsService.getAccessTokenOfAuthenticatedUser()).thenReturn("foo-bar-123-token");
+
+ final OAuth2AccessTokenProvidingClientHttpRequestInterceptor interceptor =
+ new OAuth2AccessTokenProvidingClientHttpRequestInterceptor("foobar");
+ final HttpHeaders headers = setupTest(interceptor);
+
+ assertEquals(1, headers.size());
+ assertEquals("Bearer foobar", headers.get("Authorization").get(0));
+ }
+
+ private HttpHeaders setupTest( OAuth2AccessTokenProvidingClientHttpRequestInterceptor interceptor) throws IOException {
+ final HttpRequest request = Mockito.mock(HttpRequest.class);
+ final ClientHttpRequestExecution clientHttpRequestExecution = Mockito.mock(ClientHttpRequestExecution.class);
+ final HttpHeaders headers = new HttpHeaders();
+
+ when(request.getHeaders()).thenReturn(headers);
+ interceptor.intercept(request, null, clientHttpRequestExecution);
+ verify(clientHttpRequestExecution, Mockito.times(1)).execute(request, null);
+ return headers;
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/pom.xml b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/pom.xml
new file mode 100644
index 0000000000..edcb6f2d9e
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/pom.xml
@@ -0,0 +1,126 @@
+
+
+ 4.0.0
+
+ org.springframework.cloud
+ spring-cloud-common-security-config
+ 2.11.6-SNAPSHOT
+
+ spring-cloud-common-security-config-web
+ spring-cloud-common-security-config-web
+ Spring Cloud Common Security Config Web
+ jar
+
+ true
+ 5.0.0-alpha.14
+ 3.4.1
+
+
+
+ org.springframework.cloud
+ spring-cloud-common-security-config-core
+ ${project.version}
+
+
+ org.springframework.security
+ spring-security-oauth2-jose
+
+
+ org.springframework.security
+ spring-security-oauth2-resource-server
+
+
+ org.springframework
+ spring-webflux
+
+
+ io.projectreactor.netty
+ reactor-netty
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.session
+ spring-session-core
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ com.squareup.okhttp3
+ mockwebserver3-junit5
+ ${okhttp3.version}
+ test
+
+
+ com.squareup.okhttp3
+ okhttp
+ ${okhttp3.version}
+ test
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+ 1.8.22
+ test
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ 1.8.22
+ test
+
+
+ javax.validation
+ validation-api
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.0
+
+
+ source
+
+ jar
+
+ package
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ ${maven-javadoc-plugin.version}
+
+ -Xdoclint:none
+
+
+
+ javadoc
+
+ jar
+
+ package
+
+
+
+
+
+
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/AuthorizationProperties.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/AuthorizationProperties.java
new file mode 100644
index 0000000000..8efea5f00e
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/AuthorizationProperties.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2016-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.
+ * 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.cloud.common.security;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Holds configuration for the authorization aspects of security.
+ *
+ * @author Eric Bottard
+ * @author Gunnar Hillert
+ * @author Ilayaperumal Gopinathan
+ * @author Mike Heath
+ */
+public class AuthorizationProperties {
+
+ private String externalAuthoritiesUrl;
+
+ private List rules = new ArrayList<>();
+
+ private String dashboardUrl = "/dashboard";
+
+ private String loginUrl = "/#/login";
+
+ private String loginProcessingUrl = "/login";
+
+ private String logoutUrl = "/logout";
+
+ private String logoutSuccessUrl = "/logout-success.html";
+
+ private List permitAllPaths = new ArrayList<>();
+
+ private List authenticatedPaths = new ArrayList<>();
+
+ /**
+ * Role-mapping configuration per OAuth2 provider.
+ */
+ private final Map providerRoleMappings = new HashMap<>();
+
+ private String defaultProviderId;
+
+ public Map getProviderRoleMappings() {
+ return providerRoleMappings;
+ }
+
+ public List getRules() {
+ return rules;
+ }
+
+ public void setRules(List rules) {
+ this.rules = rules;
+ }
+
+ public String getExternalAuthoritiesUrl() {
+ return externalAuthoritiesUrl;
+ }
+
+ public void setExternalAuthoritiesUrl(String externalAuthoritiesUrl) {
+ this.externalAuthoritiesUrl = externalAuthoritiesUrl;
+ }
+
+ public String getDashboardUrl() {
+ return dashboardUrl;
+ }
+
+ public void setDashboardUrl(String dashboardUrl) {
+ this.dashboardUrl = dashboardUrl;
+ }
+
+ public String getLoginUrl() {
+ return loginUrl;
+ }
+
+ public void setLoginUrl(String loginUrl) {
+ this.loginUrl = loginUrl;
+ }
+
+ public String getLoginProcessingUrl() {
+ return loginProcessingUrl;
+ }
+
+ public void setLoginProcessingUrl(String loginProcessingUrl) {
+ this.loginProcessingUrl = loginProcessingUrl;
+ }
+
+ public String getLogoutUrl() {
+ return logoutUrl;
+ }
+
+ public void setLogoutUrl(String logoutUrl) {
+ this.logoutUrl = logoutUrl;
+ }
+
+ public String getLogoutSuccessUrl() {
+ return logoutSuccessUrl;
+ }
+
+ public void setLogoutSuccessUrl(String logoutSuccessUrl) {
+ this.logoutSuccessUrl = logoutSuccessUrl;
+ }
+
+ public List getPermitAllPaths() {
+ return permitAllPaths;
+ }
+
+ public void setPermitAllPaths(List permitAllPaths) {
+ this.permitAllPaths = permitAllPaths;
+ }
+
+ public List getAuthenticatedPaths() {
+ return authenticatedPaths;
+ }
+
+ public void setAuthenticatedPaths(List authenticatedPaths) {
+ this.authenticatedPaths = authenticatedPaths;
+ }
+
+ public void setDefaultProviderId(String defaultProviderId) {
+ this.defaultProviderId = defaultProviderId;
+ }
+
+ public String getDefaultProviderId() {
+ return defaultProviderId;
+ }
+
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/CommonSecurityAutoConfiguration.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/CommonSecurityAutoConfiguration.java
new file mode 100644
index 0000000000..702e6dd2db
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/CommonSecurityAutoConfiguration.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018-2020 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.cloud.common.security;
+
+import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration;
+import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration;
+import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+@Configuration(proxyBeanMethods = false)
+@AutoConfigureBefore({
+ SecurityAutoConfiguration.class,
+ ManagementWebSecurityAutoConfiguration.class,
+ OAuth2ClientAutoConfiguration.class,
+ OAuth2ResourceServerAutoConfiguration.class})
+@Import({IgnoreAllSecurityConfiguration.class, OAuthSecurityConfiguration.class})
+public class CommonSecurityAutoConfiguration {
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/IgnoreAllSecurityConfiguration.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/IgnoreAllSecurityConfiguration.java
new file mode 100644
index 0000000000..ea3cd363cb
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/IgnoreAllSecurityConfiguration.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018-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.
+ * 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.cloud.common.security;
+
+import org.springframework.cloud.common.security.support.OnOAuth2SecurityDisabled;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+
+/**
+ * Spring Security {@link WebSecurityConfigurer} simply ignoring all paths conditionally if security is not enabled.
+ *
+ * The org.springframework.cloud.common.security.enabled=true property disables this configuration and
+ * fall back to the Spring Boot default security configuration.
+ *
+ * @author Janne Valkealahti
+ * @author Gunnar Hillert
+ * @author Christian Tzolov
+ *
+ */
+@Configuration
+@Conditional(OnOAuth2SecurityDisabled.class)
+public class IgnoreAllSecurityConfiguration implements WebSecurityConfigurer {
+
+ @Override
+ public void init(WebSecurity builder) {
+ }
+
+ @Override
+ public void configure(WebSecurity builder) {
+ builder.ignoring().antMatchers("/**");
+ }
+
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/ManualOAuthAuthenticationProvider.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/ManualOAuthAuthenticationProvider.java
new file mode 100644
index 0000000000..29a09b4855
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/ManualOAuthAuthenticationProvider.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2016-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.
+ * 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.cloud.common.security;
+
+import org.slf4j.LoggerFactory;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
+import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
+import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
+import org.springframework.web.client.ResourceAccessException;
+
+/**
+ * Provides a custom {@link AuthenticationProvider} that allows for authentication
+ * (username and password) against an OAuth Server using a {@code password grant}.
+ *
+ * @author Gunnar Hillert
+ */
+public class ManualOAuthAuthenticationProvider implements AuthenticationProvider {
+
+ private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ManualOAuthAuthenticationProvider.class);
+
+ private final OAuth2AccessTokenResponseClient oAuth2PasswordTokenResponseClient;
+ private final ClientRegistrationRepository clientRegistrationRepository;
+ private final AuthenticationProvider authenticationProvider;
+ private final String providerId;
+
+ public ManualOAuthAuthenticationProvider(
+ OAuth2AccessTokenResponseClient oAuth2PasswordTokenResponseClient,
+ ClientRegistrationRepository clientRegistrationRepository,
+ OpaqueTokenIntrospector opaqueTokenIntrospector,
+ String providerId) {
+
+ this.oAuth2PasswordTokenResponseClient = oAuth2PasswordTokenResponseClient;
+ this.clientRegistrationRepository = clientRegistrationRepository;
+ this.authenticationProvider =
+ new OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector);
+ this.providerId = providerId;
+ }
+
+ @Override
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+ final String username = authentication.getName();
+ final String password = authentication.getCredentials().toString();
+
+ final ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(providerId);
+ final ClientRegistration clientRegistrationPassword = ClientRegistration.withClientRegistration(clientRegistration).authorizationGrantType(AuthorizationGrantType.PASSWORD).build();
+
+ final OAuth2PasswordGrantRequest grantRequest = new OAuth2PasswordGrantRequest(clientRegistrationPassword, username, password);
+ final OAuth2AccessTokenResponse accessTokenResponse;
+ final String accessTokenUri = clientRegistration.getProviderDetails().getTokenUri();
+
+ try {
+ accessTokenResponse = oAuth2PasswordTokenResponseClient.getTokenResponse(grantRequest);
+ logger.warn("Authenticating user '{}' using accessTokenUri '{}'.", username, accessTokenUri);
+ }
+ catch (OAuth2AuthorizationException e) {
+ if (e.getCause() instanceof ResourceAccessException) {
+ final String errorMessage = String.format(
+ "While authenticating user '%s': " + "Unable to access accessTokenUri '%s'.", username,
+ accessTokenUri);
+ logger.error(errorMessage + " Error message: {}.", e.getCause().getMessage());
+ throw new AuthenticationServiceException(errorMessage, e);
+ }
+ else {
+ throw new BadCredentialsException(String.format("Access denied for user '%s'.", username), e);
+ }
+
+ }
+
+ final BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(accessTokenResponse.getAccessToken().getTokenValue());
+
+ Authentication newAuthentication = null;
+ try {
+ newAuthentication = this.authenticationProvider.authenticate(authenticationRequest);
+ SecurityContext context = SecurityContextHolder.createEmptyContext();
+ context.setAuthentication(newAuthentication);
+ SecurityContextHolder.setContext(context);
+ } catch (AuthenticationException failed) {
+ SecurityContextHolder.clearContext();
+ logger.warn("Authentication request for failed!", failed);
+ //this.authenticationFailureHandler.onAuthenticationFailure(request, response, failed);
+ }
+
+ return newAuthentication;
+ }
+
+ @Override
+ public boolean supports(Class> authentication) {
+ return authentication.equals(UsernamePasswordAuthenticationToken.class);
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/OAuthSecurityConfiguration.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/OAuthSecurityConfiguration.java
new file mode 100644
index 0000000000..d10b25a9cd
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/OAuthSecurityConfiguration.java
@@ -0,0 +1,495 @@
+/*
+ * Copyright 2016-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.
+ * 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.cloud.common.security;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.autoconfigure.security.SecurityProperties;
+import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
+import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
+import org.springframework.cloud.common.security.core.support.OAuth2TokenUtilsService;
+import org.springframework.cloud.common.security.support.AccessTokenClearingLogoutSuccessHandler;
+import org.springframework.cloud.common.security.support.AuthoritiesMapper;
+import org.springframework.cloud.common.security.support.CustomAuthoritiesOpaqueTokenIntrospector;
+import org.springframework.cloud.common.security.support.CustomOAuth2OidcUserService;
+import org.springframework.cloud.common.security.support.CustomPlainOAuth2UserService;
+import org.springframework.cloud.common.security.support.DefaultAuthoritiesMapper;
+import org.springframework.cloud.common.security.support.DefaultOAuth2TokenUtilsService;
+import org.springframework.cloud.common.security.support.ExternalOauth2ResourceAuthoritiesMapper;
+import org.springframework.cloud.common.security.support.MappingJwtGrantedAuthoritiesConverter;
+import org.springframework.cloud.common.security.support.OnOAuth2SecurityEnabled;
+import org.springframework.cloud.common.security.support.SecurityConfigUtils;
+import org.springframework.cloud.common.security.support.SecurityStateBean;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.event.EventListener;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient;
+import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
+import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
+import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
+import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
+import org.springframework.security.web.authentication.HttpStatusEntryPoint;
+import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
+import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
+import org.springframework.security.web.util.matcher.AnyRequestMatcher;
+import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.StringUtils;
+import org.springframework.web.HttpMediaTypeNotAcceptableException;
+import org.springframework.web.accept.HeaderContentNegotiationStrategy;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.reactive.function.client.WebClient;
+
+/**
+ * Setup Spring Security OAuth for the Rest Endpoints of Spring Cloud Data Flow.
+ *
+ * @author Gunnar Hillert
+ * @author Ilayaperumal Gopinathan
+ * @author Corneil du Plessis
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
+@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
+@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.ANY)
+@EnableWebSecurity
+@Conditional(OnOAuth2SecurityEnabled.class)
+@Import({
+ OAuthSecurityConfiguration.OAuth2AccessTokenResponseClientConfig.class,
+ OAuthSecurityConfiguration.OAuth2AuthenticationFailureEventConfig.class,
+ OAuthSecurityConfiguration.OpaqueTokenIntrospectorConfig.class,
+ OAuthSecurityConfiguration.OidcUserServiceConfig.class,
+ OAuthSecurityConfiguration.PlainOauth2UserServiceConfig.class,
+ OAuthSecurityConfiguration.WebClientConfig.class,
+ OAuthSecurityConfiguration.AuthoritiesMapperConfig.class,
+ OAuthSecurityConfiguration.OAuth2TokenUtilsServiceConfig.class,
+ OAuthSecurityConfiguration.LogoutSuccessHandlerConfig.class,
+ OAuthSecurityConfiguration.ProviderManagerConfig.class,
+ OAuthSecurityConfiguration.AuthenticationProviderConfig.class
+})
+public class OAuthSecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+ private static final Logger logger = LoggerFactory.getLogger(OAuthSecurityConfiguration.class);
+
+ @Autowired
+ protected OAuth2ClientProperties oauth2ClientProperties;
+
+ @Autowired
+ protected SecurityStateBean securityStateBean;
+
+ @Autowired
+ protected SecurityProperties securityProperties;
+
+ @Autowired
+ protected ApplicationEventPublisher applicationEventPublisher;
+
+ @Autowired
+ protected AuthorizationProperties authorizationProperties;
+
+ @Autowired
+ protected OAuth2ResourceServerProperties oAuth2ResourceServerProperties;
+
+ @Autowired
+ protected OAuth2UserService plainOauth2UserService;
+
+ @Autowired
+ protected OAuth2UserService oidcUserService;
+
+ @Autowired
+ protected LogoutSuccessHandler logoutSuccessHandler;
+
+ protected OpaqueTokenIntrospector opaqueTokenIntrospector;
+
+ protected ProviderManager providerManager;
+
+ public AuthorizationProperties getAuthorizationProperties() {
+ return authorizationProperties;
+ }
+
+ public void setAuthorizationProperties(AuthorizationProperties authorizationProperties) {
+ this.authorizationProperties = authorizationProperties;
+ }
+
+ public OpaqueTokenIntrospector getOpaqueTokenIntrospector() {
+ return opaqueTokenIntrospector;
+ }
+
+ @Autowired(required = false)
+ public void setOpaqueTokenIntrospector(OpaqueTokenIntrospector opaqueTokenIntrospector) {
+ this.opaqueTokenIntrospector = opaqueTokenIntrospector;
+ }
+
+ public ProviderManager getProviderManager() {
+ return providerManager;
+ }
+
+ @Autowired(required = false)
+ public void setProviderManager(ProviderManager providerManager) {
+ this.providerManager = providerManager;
+ }
+
+ public OAuth2ResourceServerProperties getoAuth2ResourceServerProperties() {
+ return oAuth2ResourceServerProperties;
+ }
+
+ public void setoAuth2ResourceServerProperties(OAuth2ResourceServerProperties oAuth2ResourceServerProperties) {
+ this.oAuth2ResourceServerProperties = oAuth2ResourceServerProperties;
+ }
+
+ public SecurityStateBean getSecurityStateBean() {
+ return securityStateBean;
+ }
+
+ public void setSecurityStateBean(SecurityStateBean securityStateBean) {
+ this.securityStateBean = securityStateBean;
+ }
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+
+ final RequestMatcher textHtmlMatcher = new MediaTypeRequestMatcher(
+ new BrowserDetectingContentNegotiationStrategy(),
+ MediaType.TEXT_HTML);
+
+ final BasicAuthenticationEntryPoint basicAuthenticationEntryPoint = new BasicAuthenticationEntryPoint();
+ basicAuthenticationEntryPoint.setRealmName(SecurityConfigUtils.BASIC_AUTH_REALM_NAME);
+ basicAuthenticationEntryPoint.afterPropertiesSet();
+
+ if (opaqueTokenIntrospector != null) {
+ BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter(
+ providerManager, basicAuthenticationEntryPoint);
+ http.addFilter(basicAuthenticationFilter);
+ }
+
+ this.authorizationProperties.getAuthenticatedPaths().add("/");
+ this.authorizationProperties.getAuthenticatedPaths()
+ .add(dashboard(authorizationProperties, "/**"));
+ this.authorizationProperties.getAuthenticatedPaths()
+ .add(this.authorizationProperties.getDashboardUrl());
+ this.authorizationProperties.getPermitAllPaths()
+ .add(this.authorizationProperties.getDashboardUrl());
+ this.authorizationProperties.getPermitAllPaths()
+ .add(dashboard(authorizationProperties, "/**"));
+ ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry security =
+
+ http.authorizeRequests()
+ .antMatchers(this.authorizationProperties.getPermitAllPaths()
+ .toArray(new String[0]))
+ .permitAll()
+ .antMatchers(this.authorizationProperties.getAuthenticatedPaths()
+ .toArray(new String[0]))
+ .authenticated();
+ security = SecurityConfigUtils.configureSimpleSecurity(security, this.authorizationProperties);
+ security.anyRequest().denyAll();
+
+
+ http.httpBasic().and()
+ .logout()
+ .logoutSuccessHandler(logoutSuccessHandler)
+ .and().csrf().disable()
+ .exceptionHandling()
+ // for UI not to send basic auth header
+ .defaultAuthenticationEntryPointFor(
+ new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
+ new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"))
+ .defaultAuthenticationEntryPointFor(
+ new LoginUrlAuthenticationEntryPoint(this.authorizationProperties.getLoginProcessingUrl()),
+ textHtmlMatcher)
+ .defaultAuthenticationEntryPointFor(basicAuthenticationEntryPoint, AnyRequestMatcher.INSTANCE);
+
+ http.oauth2Login().userInfoEndpoint()
+ .userService(this.plainOauth2UserService)
+ .oidcUserService(this.oidcUserService);
+
+ if (opaqueTokenIntrospector != null) {
+ http.oauth2ResourceServer()
+ .opaqueToken()
+ .introspector(opaqueTokenIntrospector);
+ }
+ else if (oAuth2ResourceServerProperties.getJwt().getJwkSetUri() != null) {
+ http.oauth2ResourceServer()
+ .jwt()
+ .jwtAuthenticationConverter(grantedAuthoritiesExtractor());
+ }
+
+ this.securityStateBean.setAuthenticationEnabled(true);
+ }
+
+ protected static String dashboard(AuthorizationProperties authorizationProperties, String path) {
+ return authorizationProperties.getDashboardUrl() + path;
+ }
+
+ protected Converter grantedAuthoritiesExtractor() {
+ String providerId = calculateDefaultProviderId(authorizationProperties, oauth2ClientProperties);
+ ProviderRoleMapping providerRoleMapping = authorizationProperties.getProviderRoleMappings()
+ .get(providerId);
+
+ JwtAuthenticationConverter jwtAuthenticationConverter =
+ new JwtAuthenticationConverter();
+
+ MappingJwtGrantedAuthoritiesConverter converter = new MappingJwtGrantedAuthoritiesConverter();
+ converter.setAuthorityPrefix("");
+ jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(converter);
+ if (providerRoleMapping != null) {
+ converter.setAuthoritiesMapping(providerRoleMapping.getRoleMappings());
+ converter.setGroupAuthoritiesMapping(providerRoleMapping.getGroupMappings());
+ if (StringUtils.hasText(providerRoleMapping.getPrincipalClaimName())) {
+ jwtAuthenticationConverter.setPrincipalClaimName(providerRoleMapping.getPrincipalClaimName());
+ }
+ }
+ return jwtAuthenticationConverter;
+ }
+
+ private static String calculateDefaultProviderId(AuthorizationProperties authorizationProperties, OAuth2ClientProperties oauth2ClientProperties) {
+ if (authorizationProperties.getDefaultProviderId() != null) {
+ return authorizationProperties.getDefaultProviderId();
+ }
+ else if (oauth2ClientProperties.getRegistration().size() == 1) {
+ return oauth2ClientProperties.getRegistration().entrySet().iterator().next()
+ .getKey();
+ }
+ else if (oauth2ClientProperties.getRegistration().size() > 1
+ && !StringUtils.hasText(authorizationProperties.getDefaultProviderId())) {
+ throw new IllegalStateException("defaultProviderId must be set if more than 1 Registration is provided.");
+ }
+ else {
+ throw new IllegalStateException("Unable to retrieve default provider id.");
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.opaquetoken", value = "introspection-uri")
+ protected static class OpaqueTokenIntrospectorConfig {
+ @Bean
+ protected OpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServerProperties oAuth2ResourceServerProperties,
+ AuthoritiesMapper authoritiesMapper) {
+ return new CustomAuthoritiesOpaqueTokenIntrospector(
+ oAuth2ResourceServerProperties.getOpaquetoken().getIntrospectionUri(),
+ oAuth2ResourceServerProperties.getOpaquetoken().getClientId(),
+ oAuth2ResourceServerProperties.getOpaquetoken().getClientSecret(),
+ authoritiesMapper);
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ protected static class OidcUserServiceConfig {
+ @Bean
+ protected OAuth2UserService oidcUserService(AuthoritiesMapper authoritiesMapper) {
+ return new CustomOAuth2OidcUserService(authoritiesMapper);
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ protected static class PlainOauth2UserServiceConfig {
+ @Bean
+ protected OAuth2UserService plainOauth2UserService(AuthoritiesMapper authoritiesMapper) {
+ return new CustomPlainOAuth2UserService(authoritiesMapper);
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ protected static class OAuth2AuthorizedClientManagerConfig {
+ @Bean
+ protected OAuth2AuthorizedClientManager authorizedClientManager(
+ ClientRegistrationRepository clientRegistrationRepository,
+ OAuth2AuthorizedClientRepository authorizedClientRepository) {
+ OAuth2AuthorizedClientProvider authorizedClientProvider =
+ OAuth2AuthorizedClientProviderBuilder.builder()
+ .authorizationCode()
+ .refreshToken()
+ .clientCredentials()
+ .password()
+ .build();
+ DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
+ clientRegistrationRepository, authorizedClientRepository);
+ authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
+ return authorizedClientManager;
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ protected static class WebClientConfig {
+ @Bean
+ protected WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
+ ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
+ new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
+ oauth2Client.setDefaultOAuth2AuthorizedClient(true);
+ return WebClient.builder()
+ .apply(oauth2Client.oauth2Configuration())
+ .build();
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ protected static class AuthoritiesMapperConfig {
+ @Bean
+ protected AuthoritiesMapper authorityMapper(AuthorizationProperties authorizationProperties,
+ OAuth2ClientProperties oAuth2ClientProperties) {
+ AuthoritiesMapper authorityMapper;
+ if (!StringUtils.hasText(authorizationProperties.getExternalAuthoritiesUrl())) {
+ authorityMapper = new DefaultAuthoritiesMapper(
+ authorizationProperties.getProviderRoleMappings(),
+ calculateDefaultProviderId(authorizationProperties, oAuth2ClientProperties));
+ }
+ else {
+ authorityMapper = new ExternalOauth2ResourceAuthoritiesMapper(
+ URI.create(authorizationProperties.getExternalAuthoritiesUrl()));
+ }
+ return authorityMapper;
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ protected static class LogoutSuccessHandlerConfig {
+ @Bean
+ protected LogoutSuccessHandler logoutSuccessHandler(AuthorizationProperties authorizationProperties,
+ OAuth2TokenUtilsService oauth2TokenUtilsService) {
+ AccessTokenClearingLogoutSuccessHandler logoutSuccessHandler =
+ new AccessTokenClearingLogoutSuccessHandler(oauth2TokenUtilsService);
+ logoutSuccessHandler.setDefaultTargetUrl(dashboard(authorizationProperties, "/logout-success-oauth.html"));
+ return logoutSuccessHandler;
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.opaquetoken", value = "introspection-uri")
+ protected static class AuthenticationProviderConfig {
+
+ protected OpaqueTokenIntrospector opaqueTokenIntrospector;
+
+ @Autowired(required = false)
+ public void setOpaqueTokenIntrospector(OpaqueTokenIntrospector opaqueTokenIntrospector) {
+ this.opaqueTokenIntrospector = opaqueTokenIntrospector;
+ }
+
+ @Bean
+ protected AuthenticationProvider authenticationProvider(
+ OAuth2AccessTokenResponseClient oAuth2PasswordTokenResponseClient,
+ ClientRegistrationRepository clientRegistrationRepository,
+ AuthorizationProperties authorizationProperties,
+ OAuth2ClientProperties oauth2ClientProperties) {
+ return new ManualOAuthAuthenticationProvider(
+ oAuth2PasswordTokenResponseClient,
+ clientRegistrationRepository,
+ this.opaqueTokenIntrospector,
+ calculateDefaultProviderId(authorizationProperties, oauth2ClientProperties));
+
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.opaquetoken", value = "introspection-uri")
+ protected static class ProviderManagerConfig {
+ private AuthenticationProvider authenticationProvider;
+
+ protected AuthenticationProvider getAuthenticationProvider() {
+ return authenticationProvider;
+ }
+
+ @Autowired(required = false)
+ protected void setAuthenticationProvider(AuthenticationProvider authenticationProvider) {
+ this.authenticationProvider = authenticationProvider;
+ }
+
+ @Bean
+ protected ProviderManager providerManager() {
+ List providers = new ArrayList<>();
+ providers.add(authenticationProvider);
+ return new ProviderManager(providers);
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ protected static class OAuth2TokenUtilsServiceConfig {
+ @Bean
+ protected OAuth2TokenUtilsService oauth2TokenUtilsService(OAuth2AuthorizedClientService oauth2AuthorizedClientService) {
+ return new DefaultOAuth2TokenUtilsService(oauth2AuthorizedClientService);
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ protected static class OAuth2AuthenticationFailureEventConfig {
+ @EventListener
+ public void handleOAuth2AuthenticationFailureEvent(
+ AbstractAuthenticationFailureEvent authenticationFailureEvent) {
+ logger.warn("An authentication failure event occurred while accessing a REST resource that requires authentication.",
+ authenticationFailureEvent.getException());
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ protected static class OAuth2AccessTokenResponseClientConfig {
+ @Bean
+ OAuth2AccessTokenResponseClient oAuth2PasswordTokenResponseClient() {
+ return new DefaultPasswordTokenResponseClient();
+ }
+ }
+
+ protected static class BrowserDetectingContentNegotiationStrategy extends HeaderContentNegotiationStrategy {
+ @Override
+ public List resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
+ final List supportedMediaTypes = super.resolveMediaTypes(request);
+ final String userAgent = request.getHeader(HttpHeaders.USER_AGENT);
+ if (userAgent != null && userAgent.contains("Mozilla/5.0")
+ && !supportedMediaTypes.contains(MediaType.APPLICATION_JSON)) {
+ return Collections.singletonList(MediaType.TEXT_HTML);
+ }
+ return Collections.singletonList(MediaType.APPLICATION_JSON);
+ }
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/ProviderRoleMapping.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/ProviderRoleMapping.java
new file mode 100644
index 0000000000..fe679e6bc5
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/ProviderRoleMapping.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2019-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.
+ * 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.cloud.common.security;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.cloud.common.security.support.CoreSecurityRoles;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Holds configuration for the authorization aspects of security.
+ *
+ * @author Gunnar Hillert
+ *
+ */
+public class ProviderRoleMapping {
+
+ private String oauthScopePrefix = "dataflow.";
+ private String rolePrefix = "ROLE_";
+ private String groupClaim = "roles";
+ private boolean mapOauthScopes = false;
+ private boolean parseOauthScopePathParts = true;
+ private boolean mapGroupClaims = false;
+ private Map roleMappings = new HashMap<>(0);
+ private Map groupMappings = new HashMap<>(0);
+ private String principalClaimName;
+
+ public ProviderRoleMapping() {
+ super();
+ }
+
+ public ProviderRoleMapping(boolean mapOauthScopes) {
+ this.mapOauthScopes = mapOauthScopes;
+ }
+
+ public ProviderRoleMapping(boolean mapOauthScopes, Map roleMappings) {
+ Assert.notNull(roleMappings, "roleMappings must not be null.");
+ this.mapOauthScopes = mapOauthScopes;
+ this.roleMappings = roleMappings;
+ }
+
+ public boolean isParseOauthScopePathParts() {
+ return parseOauthScopePathParts;
+ }
+
+ /**
+ * Sets whether or not to treat OAuth scopes as URIs during the role mapping.
+ * When set to {@code true} the OAuth scope will be treated as a URI and the leading part will be ignored (eg. 'api://dataflow-server/dataflow.create' will result in 'dataflow.create').
+ * When set to {@code false} the OAuth scope will be used as-is. This is useful in cases where the scope is not a URI and contains '/' leading characters.
+ *
+ * @param parseOauthScopePathParts whether or not to treat OAuth scopes as URIs during the role mapping
+ */
+ public void setParseOauthScopePathParts(boolean parseOauthScopePathParts) {
+ this.parseOauthScopePathParts = parseOauthScopePathParts;
+ }
+
+ public boolean isMapOauthScopes() {
+ return mapOauthScopes;
+ }
+
+ /**
+ * If set to true, Oauth scopes will be mapped to corresponding Data Flow roles.
+ * Otherwise, if set to false, or not set at all, all roles will be assigned to users.
+ *
+ * @param mapOauthScopes If not set defaults to false
+ */
+ public void setMapOauthScopes(boolean mapOauthScopes) {
+ this.mapOauthScopes = mapOauthScopes;
+ }
+
+ public boolean isMapGroupClaims() {
+ return mapGroupClaims;
+ }
+
+ public void setMapGroupClaims(boolean mapGroupClaims) {
+ this.mapGroupClaims = mapGroupClaims;
+ }
+
+ /**
+ * When using OAuth2 with enabled {@link #setMapOauthScopes(boolean)}, you can optionally specify a custom
+ * mapping of OAuth scopes to role names as they exist in the Data Flow application. If not
+ * set, then the OAuth scopes themselves must match the role names:
+ *
+ *
+ *
MANAGE = dataflow.manage
+ *
VIEW = dataflow.view
+ *
CREATE = dataflow.create
+ *
+ *
+ * @return Optional (May be null). Returns a map of scope-to-role mappings.
+ */
+ public Map getRoleMappings() {
+ return roleMappings;
+ }
+
+ public ProviderRoleMapping addRoleMapping(String oauthScope, String roleName) {
+ this.roleMappings.put(oauthScope, roleName);
+ return this;
+ }
+
+ public Map getGroupMappings() {
+ return groupMappings;
+ }
+
+ public void setGroupMappings(Map groupMappings) {
+ this.groupMappings = groupMappings;
+ }
+
+ public String getGroupClaim() {
+ return groupClaim;
+ }
+
+ public void setGroupClaim(String groupClaim) {
+ this.groupClaim = groupClaim;
+ }
+
+ public String getPrincipalClaimName() {
+ return principalClaimName;
+ }
+
+ public void setPrincipalClaimName(String principalClaimName) {
+ this.principalClaimName = principalClaimName;
+ }
+
+ public Map convertGroupMappingKeysToCoreSecurityRoles() {
+
+ final Map groupMappings = new HashMap<>(0);
+
+ if (CollectionUtils.isEmpty(this.groupMappings)) {
+ for (CoreSecurityRoles roleEnum : CoreSecurityRoles.values()) {
+ final String roleName = this.oauthScopePrefix + roleEnum.getKey();
+ groupMappings.put(roleEnum, roleName);
+ }
+ return groupMappings;
+ }
+
+ final List unmappedRoles = new ArrayList<>(0);
+
+ for (CoreSecurityRoles coreRole : CoreSecurityRoles.values()) {
+
+ final String coreSecurityRoleName;
+ if (this.rolePrefix.length() > 0 && !coreRole.getKey().startsWith(rolePrefix)) {
+ coreSecurityRoleName = rolePrefix + coreRole.getKey();
+ }
+ else {
+ coreSecurityRoleName = coreRole.getKey();
+ }
+
+ final String oauthScope = this.groupMappings.get(coreSecurityRoleName);
+
+ if (oauthScope == null) {
+ unmappedRoles.add(coreRole);
+ }
+ else {
+ groupMappings.put(coreRole, oauthScope);
+ }
+ }
+
+ if (!unmappedRoles.isEmpty()) {
+ throw new IllegalArgumentException(
+ String.format("The following %s %s not mapped: %s.",
+ unmappedRoles.size(),
+ unmappedRoles.size() > 1 ? "roles are" : "role is",
+ StringUtils.collectionToDelimitedString(unmappedRoles, ", ")));
+ }
+
+ return groupMappings;
+ }
+
+ /**
+ * @return Map containing the {@link CoreSecurityRoles} as key and the associated role name (String) as value.
+ */
+ public Map convertRoleMappingKeysToCoreSecurityRoles() {
+
+ final Map roleMappings = new HashMap<>(0);
+
+ if (CollectionUtils.isEmpty(this.roleMappings)) {
+ for (CoreSecurityRoles roleEnum : CoreSecurityRoles.values()) {
+ final String roleName = this.oauthScopePrefix + roleEnum.getKey();
+ roleMappings.put(roleEnum, roleName);
+ }
+ return roleMappings;
+ }
+
+ final List unmappedRoles = new ArrayList<>(0);
+
+ for (CoreSecurityRoles coreRole : CoreSecurityRoles.values()) {
+
+ final String coreSecurityRoleName;
+ if (this.rolePrefix.length() > 0 && !coreRole.getKey().startsWith(rolePrefix)) {
+ coreSecurityRoleName = rolePrefix + coreRole.getKey();
+ }
+ else {
+ coreSecurityRoleName = coreRole.getKey();
+ }
+
+ final String oauthScope = this.roleMappings.get(coreSecurityRoleName);
+
+ if (oauthScope == null) {
+ unmappedRoles.add(coreRole);
+ }
+ else {
+ roleMappings.put(coreRole, oauthScope);
+ }
+ }
+
+ if (!unmappedRoles.isEmpty()) {
+ throw new IllegalArgumentException(
+ String.format("The following %s %s not mapped: %s.",
+ unmappedRoles.size(),
+ unmappedRoles.size() > 1 ? "roles are" : "role is",
+ StringUtils.collectionToDelimitedString(unmappedRoles, ", ")));
+ }
+
+ return roleMappings;
+ }
+
+ /**
+ * Sets the prefix which should be added to the authority name (if it doesn't already
+ * exist).
+ *
+ * @param rolePrefix Must not be null
+ *
+ */
+ public void setRolePrefix(String rolePrefix) {
+ Assert.notNull(rolePrefix, "rolePrefix cannot be null");
+ this.rolePrefix = rolePrefix;
+ }
+
+ public String getOauthScopePrefix() {
+ return oauthScopePrefix;
+ }
+
+ /**
+ *
+ * @param oauthScopePrefix Must not be null
+ */
+ public void setOauthScopePrefix(String oauthScopePrefix) {
+ Assert.notNull(rolePrefix, "oauthScopePrefix cannot be null");
+ this.oauthScopePrefix = oauthScopePrefix;
+ }
+
+ public String getRolePrefix() {
+ return rolePrefix;
+ }
+}
diff --git a/spring-cloud-dataflow-completion/src/test/support/boot13/src/main/java/com/acme/boot13/AnotherEnumClass13.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/package-info.java
similarity index 83%
rename from spring-cloud-dataflow-completion/src/test/support/boot13/src/main/java/com/acme/boot13/AnotherEnumClass13.java
rename to spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/package-info.java
index 1260a504bf..458e8c5a6e 100644
--- a/spring-cloud-dataflow-completion/src/test/support/boot13/src/main/java/com/acme/boot13/AnotherEnumClass13.java
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/package-info.java
@@ -13,13 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-package com.acme.boot13;
-
/**
- * An enum used in configuration properties class.
+ * Contains security related configuration classes.
*/
-public enum AnotherEnumClass13 {
- low,
- high;
-}
+package org.springframework.cloud.common.security;
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/AccessTokenClearingLogoutSuccessHandler.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/AccessTokenClearingLogoutSuccessHandler.java
new file mode 100644
index 0000000000..409e6ea3e8
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/AccessTokenClearingLogoutSuccessHandler.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2019-2020 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.cloud.common.security.support;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.cloud.common.security.core.support.OAuth2TokenUtilsService;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
+import org.springframework.util.Assert;
+
+/**
+ * Customized {@link SimpleUrlLogoutSuccessHandler} that will remove the previously authenticated user's
+ * {@link OAuth2AuthorizedClient} from the underlying {@link OAuth2AuthorizedClientService}.
+ *
+ * @author Gunnar Hillert
+ * @since 1.3.0
+ */
+public class AccessTokenClearingLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
+
+ private static final Logger logger = LoggerFactory.getLogger(AccessTokenClearingLogoutSuccessHandler.class);
+
+ final OAuth2TokenUtilsService oauth2TokenUtilsService;
+
+ public AccessTokenClearingLogoutSuccessHandler(OAuth2TokenUtilsService oauth2TokenUtilsService) {
+ Assert.notNull(oauth2TokenUtilsService, "oauth2TokenUtilsService must not be null.");
+ this.oauth2TokenUtilsService = oauth2TokenUtilsService;
+ }
+
+ @Override
+ public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
+ Authentication authentication) throws IOException, ServletException {
+
+ if (authentication instanceof OAuth2AuthenticationToken) {
+ final OAuth2AuthenticationToken oauth2AuthenticationToken = (OAuth2AuthenticationToken) authentication;
+ final OAuth2AuthorizedClient oauth2AuthorizedClient = oauth2TokenUtilsService.getAuthorizedClient(oauth2AuthenticationToken);
+ oauth2TokenUtilsService.removeAuthorizedClient(oauth2AuthorizedClient);
+ logger.info("Removed OAuth2AuthorizedClient.");
+ }
+
+ super.handle(request, response, authentication);
+ }
+
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/AuthoritiesMapper.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/AuthoritiesMapper.java
new file mode 100644
index 0000000000..70e8be71a3
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/AuthoritiesMapper.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2019-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.
+ * 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.cloud.common.security.support;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.security.core.GrantedAuthority;
+
+/**
+ * Maps scopes and claims into authorities.
+ *
+ * @author Gunnar Hillert
+ * @author Janne Valkealahti
+ */
+public interface AuthoritiesMapper {
+
+ /**
+ * Map the provided scopes to authorities.
+ *
+ * @param providerId If null, then the default providerId is used
+ * @param scopes the scopes to map
+ * @param token some implementation may need to make additional requests
+ * @return the mapped authorities
+ */
+ Set mapScopesToAuthorities(String providerId, Set scopes, String token);
+
+ /**
+ * Map the provided claims to authorities.
+ *
+ * @param providerId If null, then the default providerId is used
+ * @param claims the claims to map
+ * @return the mapped authorities
+ */
+ default Set mapClaimsToAuthorities(String providerId, List claims) {
+ return Collections.emptySet();
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/CoreSecurityRoles.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/CoreSecurityRoles.java
new file mode 100644
index 0000000000..c8a3a77206
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/CoreSecurityRoles.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017-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.
+ * 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.cloud.common.security.support;
+
+import java.util.Arrays;
+
+import org.springframework.util.Assert;
+
+/**
+ * Defines the core security roles supported by Spring Cloud Security.
+ *
+ * @author Gunnar Hillert
+ */
+public enum CoreSecurityRoles {
+
+ CREATE("CREATE", "role for create operations"),
+ DEPLOY("DEPLOY", "role for deploy operations"),
+ DESTROY("DESTROY", "role for destroy operations"),
+ MANAGE("MANAGE", "role for the boot management endpoints"),
+ MODIFY("MODIFY", "role for modify operations"),
+ SCHEDULE("SCHEDULE", "role for scheduling operations"),
+ VIEW("VIEW", "view role");
+
+ private String key;
+
+ private String name;
+
+ CoreSecurityRoles(final String key, final String name) {
+ this.key = key;
+ this.name = name;
+ }
+
+ public static CoreSecurityRoles fromKey(String role) {
+
+ Assert.hasText(role, "Parameter role must not be null or empty.");
+
+ for (CoreSecurityRoles roleType : CoreSecurityRoles.values()) {
+ if (roleType.getKey().equals(role)) {
+ return roleType;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Helper class that will return all role names as a string array.
+ *
+ * @return Never null
+ */
+ public static String[] getAllRolesAsStringArray() {
+ return Arrays.stream(CoreSecurityRoles.values()).map(CoreSecurityRoles::getKey)
+ .toArray(size -> new String[size]);
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/CustomAuthoritiesOpaqueTokenIntrospector.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/CustomAuthoritiesOpaqueTokenIntrospector.java
new file mode 100644
index 0000000000..2d914ee09e
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/CustomAuthoritiesOpaqueTokenIntrospector.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2019-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.
+ * 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.cloud.common.security.support;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
+import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
+
+/**
+ *
+ * @author Gunnar Hillert
+ * @since 1.3.0
+ */
+public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
+
+ private static final Logger logger = LoggerFactory.getLogger(CustomAuthoritiesOpaqueTokenIntrospector.class);
+ private final OpaqueTokenIntrospector delegate;
+ private DefaultPrincipalExtractor principalExtractor;
+ private AuthoritiesMapper authorityMapper;
+
+ public CustomAuthoritiesOpaqueTokenIntrospector(
+ String introspectionUri,
+ String clientId,
+ String clientSecret,
+ AuthoritiesMapper authorityMapper) {
+ this.delegate = new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
+ this.principalExtractor = new DefaultPrincipalExtractor();
+ this.authorityMapper = authorityMapper;
+ }
+
+ @Override
+ public OAuth2AuthenticatedPrincipal introspect(String token) {
+ logger.debug("Introspecting");
+ OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
+ Object principalName = principalExtractor.extractPrincipal(principal.getAttributes());
+ return new DefaultOAuth2AuthenticatedPrincipal(
+ principalName.toString(), principal.getAttributes(), extractAuthorities(principal, token));
+ }
+
+ private Collection extractAuthorities(OAuth2AuthenticatedPrincipal principal, String token) {
+ final List scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE);
+ final Set scopesAsSet = new HashSet<>(scopes);
+ final Set authorities = this.authorityMapper.mapScopesToAuthorities(null, scopesAsSet, token);
+ final Set authorities2 = this.authorityMapper.mapClaimsToAuthorities(null, Arrays.asList("groups", "roles"));
+ authorities.addAll(authorities2);
+ return authorities;
+ }
+
+ public void setPrincipalExtractor(DefaultPrincipalExtractor principalExtractor) {
+ this.principalExtractor = principalExtractor;
+ }
+
+ public void setAuthorityMapper(AuthoritiesMapper authorityMapper) {
+ this.authorityMapper = authorityMapper;
+ }
+
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/CustomOAuth2OidcUserService.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/CustomOAuth2OidcUserService.java
new file mode 100644
index 0000000000..7ba93044f1
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/CustomOAuth2OidcUserService.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2019-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.
+ * 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.cloud.common.security.support;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.util.StringUtils;
+
+/**
+ *
+ * @author Gunnar Hillert
+ * @author Janne Valkealahti
+ */
+public class CustomOAuth2OidcUserService implements OAuth2UserService {
+
+ private final static Logger log = LoggerFactory.getLogger(CustomOAuth2OidcUserService.class);
+ final OidcUserService delegate = new OidcUserService();
+ final AuthoritiesMapper authorityMapper;
+
+ public CustomOAuth2OidcUserService(AuthoritiesMapper authorityMapper) {
+ this.authorityMapper = authorityMapper;
+ }
+
+ @Override
+ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
+ log.debug("Load user");
+ final OidcUser oidcUser = delegate.loadUser(userRequest);
+ final OAuth2AccessToken accessToken = userRequest.getAccessToken();
+ final Set mappedAuthorities1 = this.authorityMapper.mapScopesToAuthorities(
+ userRequest.getClientRegistration().getRegistrationId(), accessToken.getScopes(),
+ accessToken.getTokenValue());
+
+ List roleClaims = oidcUser.getClaimAsStringList("groups");
+ if (roleClaims == null) {
+ roleClaims = oidcUser.getClaimAsStringList("roles");
+ }
+ if (roleClaims == null) {
+ roleClaims = new ArrayList<>();
+ }
+ log.debug("roleClaims: {}", roleClaims);
+ Set mappedAuthorities2 = this.authorityMapper
+ .mapClaimsToAuthorities(userRequest.getClientRegistration().getRegistrationId(), roleClaims);
+
+ final String userNameAttributeName = userRequest.getClientRegistration()
+ .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
+
+ log.debug("AccessToken: {}", accessToken.getTokenValue());
+
+ HashSet mappedAuthorities = new HashSet<>(mappedAuthorities1);
+ mappedAuthorities.addAll(mappedAuthorities2);
+
+ final OidcUser oidcUserToReturn;
+ // OidcUser oidcUserToReturn;
+
+ if (StringUtils.hasText(userNameAttributeName)) {
+ oidcUserToReturn = new DefaultOidcUser(mappedAuthorities, userRequest.getIdToken(), oidcUser.getUserInfo(),
+ userNameAttributeName);
+ } else {
+ oidcUserToReturn = new DefaultOidcUser(mappedAuthorities, userRequest.getIdToken(), oidcUser.getUserInfo());
+ }
+ return oidcUserToReturn;
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/CustomPlainOAuth2UserService.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/CustomPlainOAuth2UserService.java
new file mode 100644
index 0000000000..249f6d6688
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/CustomPlainOAuth2UserService.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2019-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.
+ * 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.cloud.common.security.support;
+
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+/**
+ *
+ * @author Gunnar Hillert
+ * @author Janne Valkealahti
+ */
+public class CustomPlainOAuth2UserService implements OAuth2UserService {
+
+ private final static Logger log = LoggerFactory.getLogger(CustomPlainOAuth2UserService.class);
+ final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
+ final AuthoritiesMapper authorityMapper;
+
+ public CustomPlainOAuth2UserService(AuthoritiesMapper authorityMapper) {
+ this.authorityMapper = authorityMapper;
+ }
+
+ @Override
+ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
+ log.debug("Load user");
+ final OAuth2User oauth2User = delegate.loadUser(userRequest);
+ final OAuth2AccessToken accessToken = userRequest.getAccessToken();
+ log.debug("AccessToken: {}", accessToken.getTokenValue());
+
+ final Set mappedAuthorities = this.authorityMapper.mapScopesToAuthorities(
+ userRequest.getClientRegistration().getRegistrationId(), accessToken.getScopes(),
+ accessToken.getTokenValue());
+ final String userNameAttributeName = userRequest.getClientRegistration()
+ .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
+ final OAuth2User oauth2UserToReturn = new DefaultOAuth2User(mappedAuthorities, oauth2User.getAttributes(),
+ userNameAttributeName);
+ return oauth2UserToReturn;
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/DefaultAuthoritiesMapper.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/DefaultAuthoritiesMapper.java
new file mode 100644
index 0000000000..b5e9dc82e4
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/DefaultAuthoritiesMapper.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2019-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.
+ * 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.cloud.common.security.support;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.cloud.common.security.ProviderRoleMapping;
+import org.springframework.security.config.core.GrantedAuthorityDefaults;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Default {@link AuthoritiesMapper}.
+ *
+ * @author Gunnar Hillert
+ * @author Janne Valkealahti
+ */
+public class DefaultAuthoritiesMapper implements AuthoritiesMapper {
+
+ private static final Logger logger = LoggerFactory.getLogger(DefaultAuthoritiesMapper.class);
+ private final Map providerRoleMappings;
+ private final String defaultProviderId;
+
+ public DefaultAuthoritiesMapper(Map providerRoleMappings, String defaultProviderId) {
+ super();
+
+ Assert.notNull(providerRoleMappings, "providerRoleMappings must not be null.");
+ for (Entry providerRoleMappingToValidate : providerRoleMappings.entrySet()) {
+ providerRoleMappingToValidate.getValue().convertRoleMappingKeysToCoreSecurityRoles();
+ }
+
+ this.providerRoleMappings = providerRoleMappings;
+ this.defaultProviderId = defaultProviderId;
+ }
+
+ /**
+ * Convenience constructor that will create a {@link DefaultAuthoritiesMapper} with a
+ * single {@link ProviderRoleMapping}.
+ *
+ * @param providerId Create a ProviderRoleMapping with the specified providerId
+ * @param mapOAuthScopes Shall OAuth scopes be considered?
+ * @param roleMappings Used to populate the ProviderRoleMapping
+ */
+ public DefaultAuthoritiesMapper(String providerId, boolean mapOAuthScopes, Map roleMappings) {
+ Assert.hasText(providerId, "The providerId must not be null or empty.");
+ final ProviderRoleMapping providerRoleMapping = new ProviderRoleMapping(mapOAuthScopes, roleMappings);
+ this.providerRoleMappings = new HashMap(1);
+ this.providerRoleMappings.put(providerId, providerRoleMapping);
+ for (ProviderRoleMapping providerRoleMappingToValidate : providerRoleMappings.values()) {
+ providerRoleMappingToValidate.convertRoleMappingKeysToCoreSecurityRoles();
+ }
+ this.defaultProviderId = providerId;
+ }
+
+ /**
+ * Convenience constructor that will create a {@link DefaultAuthoritiesMapper} with a
+ * single {@link ProviderRoleMapping}.
+ *
+ * @param providerId The provider id for the ProviderRoleMapping
+ * @param mapOAuthScopes Consider scopes?
+ */
+ public DefaultAuthoritiesMapper(String providerId, boolean mapOAuthScopes) {
+ Assert.hasText(providerId, "The providerId must not be null or empty.");
+ final ProviderRoleMapping providerRoleMapping = new ProviderRoleMapping(mapOAuthScopes);
+ this.providerRoleMappings = new HashMap(1);
+ this.providerRoleMappings.put(providerId, providerRoleMapping);
+ for (ProviderRoleMapping providerRoleMappingToValidate : providerRoleMappings.values()) {
+ providerRoleMappingToValidate.convertRoleMappingKeysToCoreSecurityRoles();
+ }
+ this.defaultProviderId = providerId;
+ }
+
+ /**
+ * Convenience constructor that will create a {@link DefaultAuthoritiesMapper} with a
+ * single {@link ProviderRoleMapping}.
+ *
+ * @param providerId The provider id for the ProviderRoleMapping
+ * @param providerRoleMapping The role mappings to add to the {@link ProviderRoleMapping}
+ */
+ public DefaultAuthoritiesMapper(String providerId, ProviderRoleMapping providerRoleMapping) {
+ this.providerRoleMappings = new HashMap(1);
+ this.providerRoleMappings.put(providerId, providerRoleMapping);
+ for (ProviderRoleMapping providerRoleMappingToValidate : providerRoleMappings.values()) {
+ providerRoleMappingToValidate.convertRoleMappingKeysToCoreSecurityRoles();
+ }
+ this.defaultProviderId = providerId;
+ }
+
+ /**
+ * The returned {@link List} of {@link GrantedAuthority}s contains all roles from
+ * {@link CoreSecurityRoles}. The roles are prefixed with the value specified in
+ * {@link GrantedAuthorityDefaults}.
+ *
+ * @param clientIdParam If null, the default defaultProviderId is used
+ * @param scopes Must not be null
+ * @param token Ignored in this implementation
+ */
+ @Override
+ public Set mapScopesToAuthorities(String clientIdParam, Set scopes, String token) {
+ logger.debug("Mapping scopes to authorities");
+ final String clientId;
+ if (clientIdParam == null) {
+ clientId = this.defaultProviderId;
+ }
+ else {
+ clientId = clientIdParam;
+ }
+ Assert.notNull(scopes, "The scopes argument must not be null.");
+
+ final ProviderRoleMapping roleMapping = this.providerRoleMappings.get(clientId);
+
+ if (roleMapping == null) {
+ throw new IllegalArgumentException("No role mapping found for clientId " + clientId);
+ }
+
+ final List rolesAsStrings = new ArrayList<>();
+
+ Set grantedAuthorities = new HashSet<>();
+
+ if (roleMapping.isMapOauthScopes()) {
+ if (!scopes.isEmpty()) {
+ for (Map.Entry roleMappingEngtry : roleMapping.convertRoleMappingKeysToCoreSecurityRoles().entrySet()) {
+ final CoreSecurityRoles role = roleMappingEngtry.getKey();
+ final String expectedOAuthScope = roleMappingEngtry.getValue();
+ Set scopeList = roleMapping.isParseOauthScopePathParts() ? pathParts(scopes) : scopes;
+ for (String scope : scopeList) {
+ if (scope.equalsIgnoreCase(expectedOAuthScope)) {
+ final SimpleGrantedAuthority oauthRoleAuthority = new SimpleGrantedAuthority(roleMapping.getRolePrefix() + role.getKey());
+ rolesAsStrings.add(oauthRoleAuthority.getAuthority());
+ grantedAuthorities.add(oauthRoleAuthority);
+ }
+ }
+ }
+ logger.info("Adding roles: {}.", StringUtils.collectionToCommaDelimitedString(rolesAsStrings));
+ }
+ }
+ else if (!roleMapping.isMapGroupClaims()) {
+ grantedAuthorities =
+ roleMapping.convertRoleMappingKeysToCoreSecurityRoles().entrySet().stream().map(mapEntry -> {
+ final CoreSecurityRoles role = mapEntry.getKey();
+ rolesAsStrings.add(role.getKey());
+ return new SimpleGrantedAuthority(roleMapping.getRolePrefix() + mapEntry.getKey());
+ }).collect(Collectors.toSet());
+ logger.info("Adding ALL roles: {}.", StringUtils.collectionToCommaDelimitedString(rolesAsStrings));
+ }
+ return grantedAuthorities;
+ }
+
+ @Override
+ public Set mapClaimsToAuthorities(String clientIdParam, List claims) {
+ logger.debug("Mapping claims to authorities");
+ final String clientId;
+ if (clientIdParam == null) {
+ clientId = this.defaultProviderId;
+ }
+ else {
+ clientId = clientIdParam;
+ }
+
+ final ProviderRoleMapping groupMapping = this.providerRoleMappings.get(clientId);
+ if (groupMapping == null) {
+ throw new IllegalArgumentException("No role mapping found for clientId " + clientId);
+ }
+
+ final List rolesAsStrings = new ArrayList<>();
+ final Set grantedAuthorities = new HashSet<>();
+
+ if (groupMapping.isMapGroupClaims()) {
+ if (!claims.isEmpty()) {
+ for (Map.Entry roleMappingEngtry : groupMapping.convertGroupMappingKeysToCoreSecurityRoles().entrySet()) {
+ final CoreSecurityRoles role = roleMappingEngtry.getKey();
+ final String expectedOAuthScope = roleMappingEngtry.getValue();
+ logger.debug("Checking group mapping {} {}", role, expectedOAuthScope);
+ for (String claim : claims) {
+ logger.debug("Checking against claim {} {}", claim, expectedOAuthScope);
+ if (claim.equalsIgnoreCase(expectedOAuthScope)) {
+ final SimpleGrantedAuthority oauthRoleAuthority = new SimpleGrantedAuthority(groupMapping.getRolePrefix() + role.getKey());
+ rolesAsStrings.add(oauthRoleAuthority.getAuthority());
+ grantedAuthorities.add(oauthRoleAuthority);
+ logger.debug("Adding to granted authorities {}", oauthRoleAuthority);
+ }
+ }
+ }
+ logger.info("Adding groups: {}.", StringUtils.collectionToCommaDelimitedString(rolesAsStrings));
+ }
+ }
+
+ return grantedAuthorities;
+ }
+
+ private Set pathParts(Set scopes) {
+ // String away leading part if scope is something like
+ // api://dataflow-server/dataflow.create resulting dataflow.create
+ return scopes.stream().map(scope -> {
+ try {
+ URI uri = URI.create(scope);
+ String path = uri.getPath();
+ if (StringUtils.hasText(path) && path.charAt(0) == '/') {
+ return path.substring(1);
+ }
+ } catch (Exception e) {
+ }
+ return scope;
+ })
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/DefaultOAuth2TokenUtilsService.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/DefaultOAuth2TokenUtilsService.java
new file mode 100644
index 0000000000..063c6b7917
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/DefaultOAuth2TokenUtilsService.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2019-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.
+ * 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.cloud.common.security.support;
+
+import org.springframework.cloud.common.security.core.support.OAuth2TokenUtilsService;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.oauth2.core.AbstractOAuth2Token;
+import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Utility methods for retrieving access tokens.
+ *
+ * @author Gunnar Hillert
+ */
+public class DefaultOAuth2TokenUtilsService implements OAuth2TokenUtilsService {
+
+ private final OAuth2AuthorizedClientService oauth2AuthorizedClientService;
+
+ public DefaultOAuth2TokenUtilsService(OAuth2AuthorizedClientService oauth2AuthorizedClientService) {
+ Assert.notNull(oauth2AuthorizedClientService, "oauth2AuthorizedClientService must not be null.");
+ this.oauth2AuthorizedClientService = oauth2AuthorizedClientService;
+ }
+
+ /**
+ * Retrieves the access token from the {@link Authentication} implementation.
+ *
+ * @return May return null.
+ */
+ @Override
+ public String getAccessTokenOfAuthenticatedUser() {
+
+ final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+
+ if (authentication == null) {
+ throw new IllegalStateException("Cannot retrieve the authentication object from the SecurityContext. Are you authenticated?");
+ }
+
+ final String accessTokenOfAuthenticatedUser;
+
+ if (authentication instanceof BearerTokenAuthentication) {
+ accessTokenOfAuthenticatedUser = ((BearerTokenAuthentication) authentication).getToken().getTokenValue();
+ }
+ else if (authentication instanceof OAuth2AuthenticationToken) {
+ final OAuth2AuthenticationToken oauth2AuthenticationToken = (OAuth2AuthenticationToken) authentication;
+ final OAuth2AuthorizedClient oauth2AuthorizedClient = this.getAuthorizedClient(oauth2AuthenticationToken);
+ accessTokenOfAuthenticatedUser = oauth2AuthorizedClient.getAccessToken().getTokenValue();
+ }
+ else if (authentication instanceof JwtAuthenticationToken) {
+ AbstractOAuth2Token token = (AbstractOAuth2Token) authentication.getCredentials();
+ accessTokenOfAuthenticatedUser = token.getTokenValue();
+ }
+ else {
+ throw new IllegalStateException("Unsupported authentication object type " + authentication);
+ }
+
+ return accessTokenOfAuthenticatedUser;
+ }
+
+ @Override
+ public OAuth2AuthorizedClient getAuthorizedClient(OAuth2AuthenticationToken auth2AuthenticationToken) {
+
+ final String principalName = auth2AuthenticationToken.getName();
+ final String clientRegistrationId = auth2AuthenticationToken.getAuthorizedClientRegistrationId();
+
+ if (!StringUtils.hasText(principalName)) {
+ throw new IllegalStateException("The retrieved principalName must not be null or empty.");
+ }
+
+ if (!StringUtils.hasText(clientRegistrationId)) {
+ throw new IllegalStateException("The retrieved clientRegistrationId must not be null or empty.");
+ }
+
+ final OAuth2AuthorizedClient oauth2AuthorizedClient = this.oauth2AuthorizedClientService.loadAuthorizedClient(clientRegistrationId, principalName);
+
+ if (oauth2AuthorizedClient == null) {
+ throw new IllegalStateException(String.format(
+ "No oauth2AuthorizedClient returned for clientRegistrationId '%s' and principalName '%s'.",
+ clientRegistrationId, principalName));
+ }
+ return oauth2AuthorizedClient;
+ }
+
+ @Override
+ public void removeAuthorizedClient(OAuth2AuthorizedClient auth2AuthorizedClient) {
+ Assert.notNull(auth2AuthorizedClient, "The auth2AuthorizedClient must not be null.");
+ this.oauth2AuthorizedClientService.removeAuthorizedClient(
+ auth2AuthorizedClient.getClientRegistration().getRegistrationId(),
+ auth2AuthorizedClient.getPrincipalName());
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/DefaultPrincipalExtractor.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/DefaultPrincipalExtractor.java
new file mode 100644
index 0000000000..a8d5254993
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/DefaultPrincipalExtractor.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018-2020 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.cloud.common.security.support;
+
+import java.util.Map;
+
+/**
+ * The default implementation of the {@link PrincipalExtractor} that extracts the username
+ * of the principal.
+ *
+ * @author Gunnar Hillert
+ *
+ */
+public class DefaultPrincipalExtractor implements PrincipalExtractor {
+
+ private static final String[] PRINCIPAL_KEYS = new String[] { "user_name", "user", "username",
+ "userid", "user_id", "login", "id", "name", "cid", "client_id" };
+
+ @Override
+ public Object extractPrincipal(Map map) {
+ for (String key : PRINCIPAL_KEYS) {
+ if (map.containsKey(key)) {
+ return map.get(key);
+ }
+ }
+ return null;
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/ExternalOauth2ResourceAuthoritiesMapper.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/ExternalOauth2ResourceAuthoritiesMapper.java
new file mode 100644
index 0000000000..de7270da44
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/ExternalOauth2ResourceAuthoritiesMapper.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2018-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.
+ * 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.cloud.common.security.support;
+
+import java.net.URI;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestOperations;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * {@link AuthoritiesMapper} that looks up
+ * {@link CoreSecurityRoles} from an external HTTP resource. Requests to the
+ * external HTTP resource are authenticated by forwarding the user's access
+ * token. The external resource's response body MUST be a JSON array
+ * containing strings with values corresponding to
+ * {@link CoreSecurityRoles#key} values. For example, a response containing
+ * {@code ["VIEW", "CREATE"]} would grant the user
+ * {@code ROLE_VIEW, ROLE_CREATE},
+ *
+ * @author Mike Heath
+ * @author Gunnar Hillert
+ */
+public class ExternalOauth2ResourceAuthoritiesMapper implements AuthoritiesMapper {
+
+ private static final Logger logger = LoggerFactory.getLogger(ExternalOauth2ResourceAuthoritiesMapper.class);
+
+ public static final GrantedAuthority CREATE = new SimpleGrantedAuthority(SecurityConfigUtils.ROLE_PREFIX + CoreSecurityRoles.CREATE.getKey());
+ public static final GrantedAuthority DEPLOY = new SimpleGrantedAuthority(SecurityConfigUtils.ROLE_PREFIX + CoreSecurityRoles.DEPLOY.getKey());
+ public static final GrantedAuthority DESTROY = new SimpleGrantedAuthority(SecurityConfigUtils.ROLE_PREFIX + CoreSecurityRoles.DESTROY.getKey());
+ public static final GrantedAuthority MANAGE = new SimpleGrantedAuthority(SecurityConfigUtils.ROLE_PREFIX + CoreSecurityRoles.MANAGE.getKey());
+ public static final GrantedAuthority MODIFY = new SimpleGrantedAuthority(SecurityConfigUtils.ROLE_PREFIX + CoreSecurityRoles.MODIFY.getKey());
+ public static final GrantedAuthority SCHEDULE = new SimpleGrantedAuthority(SecurityConfigUtils.ROLE_PREFIX + CoreSecurityRoles.SCHEDULE.getKey());
+ public static final GrantedAuthority VIEW = new SimpleGrantedAuthority(SecurityConfigUtils.ROLE_PREFIX + CoreSecurityRoles.VIEW.getKey());
+
+ private final URI roleProviderUri;
+ private final RestOperations restOperations;
+
+ /**
+ *
+ * @param roleProviderUri a HTTP GET request is sent to this URI to fetch
+ * the user's security roles
+ */
+ public ExternalOauth2ResourceAuthoritiesMapper(
+ URI roleProviderUri) {
+ Assert.notNull(roleProviderUri, "The provided roleProviderUri must not be null.");
+ this.roleProviderUri = roleProviderUri;
+
+ final RestTemplate restTemplate = new RestTemplate();
+ restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
+ this.restOperations = restTemplate;
+ }
+
+
+ @Override
+ public Set mapScopesToAuthorities(String providerId, Set scopes, String token) {
+ logger.debug("Getting permissions from {}", roleProviderUri);
+
+ final HttpHeaders headers = new HttpHeaders();
+ headers.add(HttpHeaders.AUTHORIZATION, OAuth2AccessToken.TokenType.BEARER.getValue() + " " + token);
+
+ final HttpEntity entity = new HttpEntity<>(null, headers);
+ final ResponseEntity response = restOperations.exchange(roleProviderUri, HttpMethod.GET, entity, String[].class);
+
+ final Set authorities = new HashSet<>();
+ for (String permission : response.getBody()) {
+ if (!StringUtils.hasText(permission)) {
+ logger.warn("Received an empty permission from {}", roleProviderUri);
+ } else {
+ final CoreSecurityRoles securityRole = CoreSecurityRoles.fromKey(permission.toUpperCase());
+ if (securityRole == null) {
+ logger.warn("Invalid role {} provided by {}", permission, roleProviderUri);
+ } else {
+ switch (securityRole) {
+ case CREATE:
+ authorities.add(CREATE);
+ break;
+ case DEPLOY:
+ authorities.add(DEPLOY);
+ break;
+ case DESTROY:
+ authorities.add(DESTROY);
+ break;
+ case MANAGE:
+ authorities.add(MANAGE);
+ break;
+ case MODIFY:
+ authorities.add(MODIFY);
+ break;
+ case SCHEDULE:
+ authorities.add(SCHEDULE);
+ break;
+ case VIEW:
+ authorities.add(VIEW);
+ break;
+ }
+ }
+ }
+ }
+ logger.info("Roles added for user: {}.", authorities);
+ return authorities;
+ }
+}
+
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/MappingJwtGrantedAuthoritiesConverter.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/MappingJwtGrantedAuthoritiesConverter.java
new file mode 100644
index 0000000000..e31c908e8a
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/MappingJwtGrantedAuthoritiesConverter.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2020-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.
+ * 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.cloud.common.security.support;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Extracts the {@link GrantedAuthority}s from scope attributes typically found
+ * in a {@link Jwt}.
+ *
+ * @author Gunnar Hillert
+ * @author Janne Valkealahti
+ */
+public final class MappingJwtGrantedAuthoritiesConverter implements Converter> {
+
+ private final static Logger log = LoggerFactory.getLogger(MappingJwtGrantedAuthoritiesConverter.class);
+ private static final String DEFAULT_AUTHORITY_PREFIX = "SCOPE_";
+
+ private static final Collection WELL_KNOWN_SCOPES_CLAIM_NAMES =
+ Arrays.asList("scope", "scp");
+ private static final Collection WELL_KNOWN_GROUPS_CLAIM_NAMES =
+ Arrays.asList("groups", "roles");
+
+ private String authorityPrefix = DEFAULT_AUTHORITY_PREFIX;
+
+ private String authoritiesClaimName;
+ private String groupAuthoritiesClaimName;
+
+ private Map roleAuthoritiesMapping = new HashMap<>();
+ private Map groupAuthoritiesMapping = new HashMap<>();
+
+ /**
+ * Extract {@link GrantedAuthority}s from the given {@link Jwt}.
+ *
+ * @param jwt The {@link Jwt} token
+ * @return The {@link GrantedAuthority authorities} read from the token scopes
+ */
+ @Override
+ public Collection convert(Jwt jwt) {
+ log.debug("JWT: {}", jwt.getTokenValue());
+ Set collect = getAuthorities(jwt).stream()
+ .flatMap(authority -> {
+ if (roleAuthoritiesMapping.isEmpty() && groupAuthoritiesMapping.isEmpty()) {
+ return Stream.of(authority);
+ }
+ Stream s1 = roleAuthoritiesMapping.entrySet().stream()
+ .filter(entry -> entry.getValue().equals(authority))
+ .map(entry -> entry.getKey()).distinct();
+ Stream s2 = groupAuthoritiesMapping.entrySet().stream()
+ .filter(entry -> entry.getValue().equals(authority))
+ .map(entry -> entry.getKey()).distinct();
+ return Stream.concat(s1, s2);
+ })
+ .distinct()
+ .map(authority -> new SimpleGrantedAuthority(this.authorityPrefix + authority))
+ .collect(Collectors.toSet());
+ log.debug("JWT granted: {}", collect);
+ return collect;
+ }
+
+ /**
+ * Sets the prefix to use for {@link GrantedAuthority authorities} mapped by this converter.
+ * Defaults to {@link JwtGrantedAuthoritiesConverter#DEFAULT_AUTHORITY_PREFIX}.
+ *
+ * @param authorityPrefix The authority prefix
+ */
+ public void setAuthorityPrefix(String authorityPrefix) {
+ Assert.notNull(authorityPrefix, "authorityPrefix cannot be null");
+ this.authorityPrefix = authorityPrefix;
+ }
+
+ /**
+ * Sets the name of token claim to use for mapping {@link GrantedAuthority
+ * authorities} by this converter. Defaults to
+ * {@link JwtGrantedAuthoritiesConverter#WELL_KNOWN_SCOPES_CLAIM_NAMES}.
+ *
+ * @param authoritiesClaimName The token claim name to map authorities
+ */
+ public void setAuthoritiesClaimName(String authoritiesClaimName) {
+ Assert.hasText(authoritiesClaimName, "authoritiesClaimName cannot be empty");
+ this.authoritiesClaimName = authoritiesClaimName;
+ }
+
+ /**
+ * Set the mapping from resolved authorities from jwt into granted authorities.
+ *
+ * @param authoritiesMapping the authoritiesMapping to set
+ */
+ public void setAuthoritiesMapping(Map authoritiesMapping) {
+ Assert.notNull(authoritiesMapping, "authoritiesMapping cannot be null");
+ this.roleAuthoritiesMapping = authoritiesMapping;
+ }
+
+ /**
+ * Sets the name of token claim to use for group mapping {@link GrantedAuthority
+ * authorities} by this converter. Defaults to
+ * {@link org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter#WELL_KNOWN_AUTHORITIES_CLAIM_NAMES}.
+ *
+ * @param groupAuthoritiesClaimName the token claim name to map group
+ * authorities
+ */
+ public void setGroupAuthoritiesClaimName(String groupAuthoritiesClaimName) {
+ this.groupAuthoritiesClaimName = groupAuthoritiesClaimName;
+ }
+
+ /**
+ * Set the group mapping from resolved authorities from jwt into granted
+ * authorities.
+ *
+ * @param groupAuthoritiesMapping
+ */
+ public void setGroupAuthoritiesMapping(Map groupAuthoritiesMapping) {
+ this.groupAuthoritiesMapping = groupAuthoritiesMapping;
+ }
+
+ private String getAuthoritiesClaimName(Jwt jwt) {
+ if (this.authoritiesClaimName != null) {
+ return this.authoritiesClaimName;
+ }
+ for (String claimName : WELL_KNOWN_SCOPES_CLAIM_NAMES) {
+ if (jwt.hasClaim(claimName)) {
+ return claimName;
+ }
+ }
+ return null;
+ }
+
+ private String getGroupAuthoritiesClaimName(Jwt jwt) {
+ if (this.groupAuthoritiesClaimName != null) {
+ return this.groupAuthoritiesClaimName;
+ }
+ for (String claimName : WELL_KNOWN_GROUPS_CLAIM_NAMES) {
+ if (jwt.hasClaim(claimName)) {
+ return claimName;
+ }
+ }
+ return null;
+ }
+
+ private Collection getAuthorities(Jwt jwt) {
+ String scopeClaimName = getAuthoritiesClaimName(jwt);
+ String groupClaimName = getGroupAuthoritiesClaimName(jwt);
+
+ List claimAsStringList1 = null;
+ List claimAsStringList2 = null;
+
+ // spring-sec does wrong conversion with arrays
+ if (scopeClaimName != null && !ObjectUtils.isArray(jwt.getClaim(scopeClaimName))) {
+ claimAsStringList1 = jwt.getClaimAsStringList(scopeClaimName);
+ }
+ if (groupClaimName != null && !ObjectUtils.isArray(jwt.getClaim(groupClaimName))) {
+ claimAsStringList2 = jwt.getClaimAsStringList(groupClaimName);
+ }
+
+ List claimAsStringList = new ArrayList<>();
+ if (claimAsStringList1 != null) {
+ List collect = claimAsStringList1.stream()
+ .flatMap(c -> Arrays.stream(c.split(" ")))
+ .filter(c -> StringUtils.hasText(c))
+ .collect(Collectors.toList());
+ claimAsStringList.addAll(collect);
+ }
+ if (claimAsStringList2 != null) {
+ claimAsStringList.addAll(claimAsStringList2);
+ }
+ return claimAsStringList;
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/OnOAuth2SecurityDisabled.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/OnOAuth2SecurityDisabled.java
new file mode 100644
index 0000000000..c5ad6f25af
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/OnOAuth2SecurityDisabled.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016-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.
+ * 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.cloud.common.security.support;
+
+import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
+import org.springframework.context.annotation.Condition;
+import org.springframework.context.annotation.Conditional;
+
+/**
+ * {@link Condition} that is only valid if {@code security.basic.enabled} is {@code true}
+ * and the property {@code security.oauth2} exists.
+ *
+ * @author Gunnar Hillert
+ * @since 1.1.0
+ */
+public class OnOAuth2SecurityDisabled extends NoneNestedConditions {
+
+ public OnOAuth2SecurityDisabled() {
+ super(ConfigurationPhase.REGISTER_BEAN);
+ }
+
+ @Conditional(OnOAuth2SecurityEnabled.class)
+ static class OAuthEnabled {
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/OnOAuth2SecurityEnabled.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/OnOAuth2SecurityEnabled.java
new file mode 100644
index 0000000000..fbd0c656b3
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/OnOAuth2SecurityEnabled.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016-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.
+ * 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.cloud.common.security.support;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
+import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
+import org.springframework.boot.context.properties.bind.Bindable;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.context.annotation.Condition;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.core.env.Environment;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+
+/**
+ * {@link Condition} that is only valid if the property
+ * {@code security.oauth2.client.client-id} exists.
+ *
+ * @author Gunnar Hillert
+ * @since 1.1.0
+ */
+public class OnOAuth2SecurityEnabled extends SpringBootCondition {
+
+ @Override
+ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
+ Map properties = getSubProperties(context.getEnvironment(), "spring.security.oauth2");
+ return new ConditionOutcome(!properties.isEmpty(), "OAuth2 Enabled");
+ }
+
+ public static Map getSubProperties(Environment environment, String keyPrefix) {
+ return Binder.get(environment)
+ .bind(keyPrefix, Bindable.mapOf(String.class, String.class))
+ .orElseGet(Collections::emptyMap);
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/PrincipalExtractor.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/PrincipalExtractor.java
new file mode 100644
index 0000000000..4fb9e18b45
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/PrincipalExtractor.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2018-2020 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.cloud.common.security.support;
+
+import java.util.Map;
+
+/**
+ * @author Gunnar Hillert
+ * @since 1.3.0
+ *
+ */
+public interface PrincipalExtractor {
+
+ Object extractPrincipal(Map map);
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/SecurityConfigUtils.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/SecurityConfigUtils.java
new file mode 100644
index 0000000000..272242a8f0
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/SecurityConfigUtils.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2017-2020 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.cloud.common.security.support;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.slf4j.LoggerFactory;
+
+import org.springframework.cloud.common.security.AuthorizationProperties;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * State-holder for computed security meta-information.
+ *
+ * @author Gunnar Hillert
+ */
+public class SecurityConfigUtils {
+
+ private static final org.slf4j.Logger logger = LoggerFactory.getLogger(SecurityConfigUtils.class);
+
+ public static final String ROLE_PREFIX = "ROLE_";
+
+ public static final Pattern AUTHORIZATION_RULE;
+
+ public static final String BASIC_AUTH_REALM_NAME = "Spring";
+
+ static {
+ String methodsRegex = StringUtils.arrayToDelimitedString(HttpMethod.values(), "|");
+ AUTHORIZATION_RULE = Pattern.compile("(" + methodsRegex + ")\\s+(.+)\\s+=>\\s+(.+)");
+ }
+
+ /**
+ * Read the configuration for "simple" (that is, not ACL based) security and apply it.
+ *
+ * @param security The ExpressionUrlAuthorizationConfigurer to apply the authorization rules to
+ * @param authorizationProperties Contains the rules to configure authorization
+ *
+ * @return ExpressionUrlAuthorizationConfigurer
+ */
+ public static ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry configureSimpleSecurity(
+ ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry security,
+ AuthorizationProperties authorizationProperties) {
+ for (String rule : authorizationProperties.getRules()) {
+ Matcher matcher = AUTHORIZATION_RULE.matcher(rule);
+ Assert.isTrue(matcher.matches(),
+ String.format("Unable to parse security rule [%s], expected format is 'HTTP_METHOD ANT_PATTERN => "
+ + "SECURITY_ATTRIBUTE(S)'", rule));
+
+ HttpMethod method = HttpMethod.valueOf(matcher.group(1).trim());
+ String urlPattern = matcher.group(2).trim();
+ String attribute = matcher.group(3).trim();
+
+ logger.info("Authorization '{}' | '{}' | '{}'", method, attribute, urlPattern);
+ security = security.antMatchers(method, urlPattern).access(attribute);
+ }
+ return security;
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/SecurityStateBean.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/SecurityStateBean.java
new file mode 100644
index 0000000000..2641ce9f63
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/SecurityStateBean.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017-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.
+ * 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.cloud.common.security.support;
+
+/**
+ * State-holder for computed security meta-information.
+ *
+ * @author Gunnar Hillert
+ */
+public class SecurityStateBean {
+
+ private boolean authenticationEnabled;
+
+ public SecurityStateBean() {
+ super();
+ }
+
+ public boolean isAuthenticationEnabled() {
+ return authenticationEnabled;
+ }
+
+ public void setAuthenticationEnabled(boolean authenticationEnabled) {
+ this.authenticationEnabled = authenticationEnabled;
+ }
+
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/resources/META-INF/spring.factories b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..0a8aad951c
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+ org.springframework.cloud.common.security.CommonSecurityAutoConfiguration
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/OnOAuth2SecurityDisabledTests.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/OnOAuth2SecurityDisabledTests.java
new file mode 100644
index 0000000000..f39a46fe35
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/OnOAuth2SecurityDisabledTests.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018-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.
+ * 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.cloud.common.security;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.test.util.TestPropertyValues;
+import org.springframework.cloud.common.security.support.OnOAuth2SecurityDisabled;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class OnOAuth2SecurityDisabledTests {
+
+ @Test
+ public void noPropertySet() throws Exception {
+ AnnotationConfigApplicationContext context = load(Config.class);
+ assertThat(context.containsBean("myBean")).isTrue();
+ context.close();
+ }
+
+ @Test
+ public void propertyClientIdSet() throws Exception {
+ AnnotationConfigApplicationContext context =
+ load(Config.class, "spring.security.oauth2.client.registration.uaa.client-id:12345");
+ assertThat(context.containsBean("myBean")).isFalse();
+ context.close();
+ }
+
+ private AnnotationConfigApplicationContext load(Class> config, String... env) {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ TestPropertyValues.of(env).applyTo(context);
+ context.register(config);
+ context.refresh();
+ return context;
+ }
+
+ @Configuration
+ @Conditional(OnOAuth2SecurityDisabled.class)
+ public static class Config {
+ @Bean
+ public String myBean() {
+ return "myBean";
+ }
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/OnOAuth2SecurityEnabledTests.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/OnOAuth2SecurityEnabledTests.java
new file mode 100644
index 0000000000..4bcfe1789c
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/OnOAuth2SecurityEnabledTests.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2016-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.
+ * 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.cloud.common.security;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.test.util.TestPropertyValues;
+import org.springframework.cloud.common.security.support.OnOAuth2SecurityEnabled;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * @author Gunnar Hillert
+ */
+public class OnOAuth2SecurityEnabledTests {
+
+ @Test
+ public void noPropertySet() throws Exception {
+ AnnotationConfigApplicationContext context = load(Config.class);
+ assertThat(context.containsBean("myBean")).isFalse();
+ context.close();
+ }
+
+ @Test
+ public void propertySecurityOauth() throws Exception {
+ assertThatThrownBy(() -> {
+ load(Config.class, "spring.security.oauth2");
+ }).isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ public void propertyClientId() throws Exception {
+ AnnotationConfigApplicationContext context = load(Config.class,
+ "spring.security.oauth2.client.registration.uaa.client-id:12345");
+ assertThat(context.containsBean("myBean")).isTrue();
+ context.close();
+ }
+
+ @Test
+ public void clientIdOnlyWithNoValue() throws Exception {
+ AnnotationConfigApplicationContext context = load(Config.class,
+ "spring.security.oauth2.client.registration.uaa.client-id");
+ assertThat(context.containsBean("myBean")).isTrue();
+ context.close();
+ }
+
+ private AnnotationConfigApplicationContext load(Class> config, String... env) {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ TestPropertyValues.of(env).applyTo(context);
+ context.register(config);
+ context.refresh();
+ return context;
+ }
+
+ @Configuration
+ @Conditional(OnOAuth2SecurityEnabled.class)
+ public static class Config {
+ @Bean
+ public String myBean() {
+ return "myBean";
+ }
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/support/DefaultAuthoritiesMapperTests.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/support/DefaultAuthoritiesMapperTests.java
new file mode 100644
index 0000000000..fde46cb93f
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/support/DefaultAuthoritiesMapperTests.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2017-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.
+ * 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.cloud.common.security.support;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.cloud.common.security.ProviderRoleMapping;
+import org.springframework.security.core.GrantedAuthority;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * @author Gunnar Hillert
+ */
+public class DefaultAuthoritiesMapperTests {
+
+ @Test
+ public void testNullConstructor() throws Exception {
+ assertThatThrownBy(() -> {
+ new DefaultAuthoritiesMapper(null, "");
+ }).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("providerRoleMappings must not be null.");
+ }
+
+ @Test
+ public void testMapScopesToAuthoritiesWithNullParameters() throws Exception {
+ DefaultAuthoritiesMapper authoritiesMapper = new DefaultAuthoritiesMapper(Collections.emptyMap(), "");
+
+ assertThatThrownBy(() -> {
+ authoritiesMapper.mapScopesToAuthorities(null, null, null);
+ }).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("The scopes argument must not be null.");
+ assertThatThrownBy(() -> {
+ authoritiesMapper.mapScopesToAuthorities("myClientId", null, null);
+ }).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("The scopes argument must not be null.");
+ }
+
+ @Test
+ public void testThat7AuthoritiesAreReturned() throws Exception {
+ DefaultAuthoritiesMapper authoritiesMapper = new DefaultAuthoritiesMapper("uaa", false);
+ Set authorities = authoritiesMapper.mapScopesToAuthorities("uaa", Collections.emptySet(), null);
+
+ assertThat(authorities).hasSize(7);
+ assertThat(authorities.stream().map(authority -> authority.getAuthority()).collect(Collectors.toList()))
+ .containsExactlyInAnyOrder("ROLE_MANAGE", "ROLE_CREATE", "ROLE_VIEW", "ROLE_DEPLOY", "ROLE_MODIFY",
+ "ROLE_SCHEDULE", "ROLE_DESTROY");
+ }
+
+ @Test
+ public void testEmptyMapConstructor() throws Exception {
+ Set scopes = new HashSet<>();
+ scopes.add("dataflow.manage");
+ scopes.add("dataflow.view");
+ scopes.add("dataflow.create");
+
+ DefaultAuthoritiesMapper authoritiesMapper = new DefaultAuthoritiesMapper("uaa", true);
+ Collection extends GrantedAuthority> authorities = authoritiesMapper.mapScopesToAuthorities("uaa", scopes, null);
+
+ assertThat(authorities).hasSize(3);
+ assertThat(authorities.stream().map(authority -> authority.getAuthority()).collect(Collectors.toList()))
+ .containsExactlyInAnyOrder("ROLE_MANAGE", "ROLE_CREATE", "ROLE_VIEW");
+ }
+
+ @Test
+ public void testMapConstructorWithIncompleteRoleMappings() throws Exception {
+ ProviderRoleMapping roleMapping = new ProviderRoleMapping();
+ roleMapping.setMapOauthScopes(true);
+ roleMapping.addRoleMapping("ROLE_MANAGE", "foo-scope-in-oauth");
+ assertThatThrownBy(() -> {
+ new DefaultAuthoritiesMapper("uaa", roleMapping);
+ }).isInstanceOf(IllegalArgumentException.class).hasMessageContaining(
+ "The following 6 roles are not mapped: CREATE, DEPLOY, DESTROY, MODIFY, SCHEDULE, VIEW.");
+ }
+
+ @Test
+ public void testThat7MappedAuthoritiesAreReturned() throws Exception {
+ Map roleMappings = new HashMap<>();
+ roleMappings.put("ROLE_MANAGE", "foo-manage");
+ roleMappings.put("ROLE_VIEW", "bar-view");
+ roleMappings.put("ROLE_CREATE", "blubba-create");
+ roleMappings.put("ROLE_MODIFY", "foo-modify");
+ roleMappings.put("ROLE_DEPLOY", "foo-deploy");
+ roleMappings.put("ROLE_DESTROY", "foo-destroy");
+ roleMappings.put("ROLE_SCHEDULE", "foo-schedule");
+
+ ProviderRoleMapping providerRoleMapping = new ProviderRoleMapping();
+ providerRoleMapping.setMapOauthScopes(true);
+ providerRoleMapping.getRoleMappings().putAll(roleMappings);
+
+ Set scopes = new HashSet<>();
+ scopes.add("foo-manage");
+ scopes.add("bar-view");
+ scopes.add("blubba-create");
+ scopes.add("foo-modify");
+ scopes.add("foo-deploy");
+ scopes.add("foo-destroy");
+ scopes.add("foo-schedule");
+
+ DefaultAuthoritiesMapper defaultAuthoritiesMapper = new DefaultAuthoritiesMapper("uaa", providerRoleMapping);
+ Collection extends GrantedAuthority> authorities = defaultAuthoritiesMapper.mapScopesToAuthorities("uaa",
+ scopes, null);
+
+ assertThat(authorities).hasSize(7);
+ assertThat(authorities.stream().map(authority -> authority.getAuthority()).collect(Collectors.toList()))
+ .containsExactlyInAnyOrder("ROLE_CREATE", "ROLE_DEPLOY", "ROLE_DESTROY", "ROLE_MANAGE", "ROLE_MODIFY",
+ "ROLE_SCHEDULE", "ROLE_VIEW");
+ }
+
+ @Test
+ public void testThat3MappedAuthoritiesAreReturnedForDefaultMapping() throws Exception {
+ ProviderRoleMapping providerRoleMapping = new ProviderRoleMapping();
+ providerRoleMapping.setMapOauthScopes(true);
+
+ Set scopes = new HashSet<>();
+ scopes.add("dataflow.manage");
+ scopes.add("dataflow.view");
+ scopes.add("dataflow.create");
+
+ DefaultAuthoritiesMapper defaultAuthoritiesExtractor = new DefaultAuthoritiesMapper("uaa", providerRoleMapping);
+ Collection extends GrantedAuthority> authorities = defaultAuthoritiesExtractor.mapScopesToAuthorities("uaa",
+ scopes, null);
+
+ assertThat(authorities).hasSize(3);
+ assertThat(authorities.stream().map(authority -> authority.getAuthority()).collect(Collectors.toList()))
+ .containsExactlyInAnyOrder("ROLE_MANAGE", "ROLE_CREATE", "ROLE_VIEW");
+ }
+
+ @Test
+ public void testThat7MappedAuthoritiesAreReturnedForDefaultMappingWithoutMappingScopes() throws Exception {
+ Set scopes = new HashSet<>();
+ scopes.add("dataflow.manage");
+ scopes.add("dataflow.view");
+ scopes.add("dataflow.create");
+
+ DefaultAuthoritiesMapper defaultAuthoritiesExtractor = new DefaultAuthoritiesMapper("uaa", false);
+ Collection extends GrantedAuthority> authorities = defaultAuthoritiesExtractor.mapScopesToAuthorities("uaa",
+ scopes, null);
+
+ assertThat(authorities).hasSize(7);
+ assertThat(authorities.stream().map(authority -> authority.getAuthority()).collect(Collectors.toList()))
+ .containsExactlyInAnyOrder("ROLE_CREATE", "ROLE_DEPLOY", "ROLE_DESTROY", "ROLE_MANAGE", "ROLE_MODIFY",
+ "ROLE_SCHEDULE", "ROLE_VIEW");
+ }
+
+ @Test
+ public void testThat2MappedAuthoritiesAreReturnedForDefaultMapping() throws Exception {
+ Set scopes = new HashSet<>();
+ scopes.add("dataflow.view");
+ scopes.add("dataflow.create");
+
+ DefaultAuthoritiesMapper defaultAuthoritiesExtractor = new DefaultAuthoritiesMapper("uaa", true);
+ Collection extends GrantedAuthority> authorities = defaultAuthoritiesExtractor.mapScopesToAuthorities("uaa",
+ scopes, null);
+
+ assertThat(authorities).hasSize(2);
+ assertThat(authorities.stream().map(authority -> authority.getAuthority()).collect(Collectors.toList()))
+ .containsExactlyInAnyOrder("ROLE_CREATE", "ROLE_VIEW");
+ }
+
+ @Test
+ public void testThat7AuthoritiesAreReturnedAndOneOAuthScopeCoversMultipleServerRoles() throws Exception {
+ Map roleMappings = new HashMap<>();
+ roleMappings.put("ROLE_MANAGE", "foo-manage");
+ roleMappings.put("ROLE_VIEW", "foo-manage");
+ roleMappings.put("ROLE_DEPLOY", "foo-manage");
+ roleMappings.put("ROLE_DESTROY", "foo-manage");
+ roleMappings.put("ROLE_MODIFY", "foo-manage");
+ roleMappings.put("ROLE_SCHEDULE", "foo-manage");
+ roleMappings.put("ROLE_CREATE", "blubba-create");
+
+ Set scopes = new HashSet<>();
+ scopes.add("foo-manage");
+ scopes.add("blubba-create");
+
+ DefaultAuthoritiesMapper defaultAuthoritiesExtractor = new DefaultAuthoritiesMapper("uaa", true, roleMappings);
+ Collection extends GrantedAuthority> authorities = defaultAuthoritiesExtractor.mapScopesToAuthorities("uaa",
+ scopes, null);
+
+ assertThat(authorities).hasSize(7);
+ assertThat(authorities.stream().map(authority -> authority.getAuthority()).collect(Collectors.toList()))
+ .containsExactlyInAnyOrder("ROLE_CREATE", "ROLE_DEPLOY", "ROLE_DESTROY", "ROLE_MANAGE", "ROLE_MODIFY",
+ "ROLE_SCHEDULE", "ROLE_VIEW");
+ }
+
+ @Test
+ public void testThatUriStyleScopeRemovesLeadingPart() throws Exception {
+ Map roleMappings = new HashMap<>();
+ roleMappings.put("ROLE_MANAGE", "foo-manage");
+ roleMappings.put("ROLE_VIEW", "foo-manage");
+ roleMappings.put("ROLE_DEPLOY", "foo-manage");
+ roleMappings.put("ROLE_DESTROY", "foo-manage");
+ roleMappings.put("ROLE_MODIFY", "foo-manage");
+ roleMappings.put("ROLE_SCHEDULE", "foo-manage");
+ roleMappings.put("ROLE_CREATE", "blubba-create");
+
+ Set scopes = new HashSet<>();
+ scopes.add("api://foobar/foo-manage");
+ scopes.add("blubba-create");
+
+ DefaultAuthoritiesMapper defaultAuthoritiesExtractor = new DefaultAuthoritiesMapper("uaa", true, roleMappings);
+ Collection extends GrantedAuthority> authorities = defaultAuthoritiesExtractor.mapScopesToAuthorities("uaa",
+ scopes, null);
+
+ assertThat(authorities).hasSize(7);
+ assertThat(authorities.stream().map(authority -> authority.getAuthority()).collect(Collectors.toList()))
+ .containsExactlyInAnyOrder("ROLE_CREATE", "ROLE_DEPLOY", "ROLE_DESTROY", "ROLE_MANAGE", "ROLE_MODIFY",
+ "ROLE_SCHEDULE", "ROLE_VIEW");
+ }
+
+ @Test
+ public void testThatUriStyleScopeParsingCanBeDisabled() throws Exception {
+ Map roleMappings = new HashMap<>();
+ roleMappings.put("ROLE_MANAGE", "/ROLE/2000803042");
+ roleMappings.put("ROLE_VIEW", "/ROLE/2000803036");
+ roleMappings.put("ROLE_DEPLOY", "/ROLE/2000803039");
+ roleMappings.put("ROLE_DESTROY", "/ROLE/20008030340");
+ roleMappings.put("ROLE_MODIFY", "/ROLE/2000803037");
+ roleMappings.put("ROLE_SCHEDULE", "/ROLE/2000803038");
+ roleMappings.put("ROLE_CREATE", "/ROLE/2000803041");
+
+ ProviderRoleMapping providerRoleMapping = new ProviderRoleMapping();
+ providerRoleMapping.setMapOauthScopes(true);
+ providerRoleMapping.setParseOauthScopePathParts(false);
+ providerRoleMapping.getRoleMappings().putAll(roleMappings);
+
+ Set scopes = new HashSet<>();
+ scopes.add("/ROLE/2000803042");
+ scopes.add("/ROLE/2000803036");
+ scopes.add("/ROLE/2000803039");
+ scopes.add("/ROLE/20008030340");
+ scopes.add("/ROLE/2000803037");
+ scopes.add("/ROLE/2000803038");
+ scopes.add("/ROLE/2000803041");
+
+ DefaultAuthoritiesMapper defaultAuthoritiesMapper = new DefaultAuthoritiesMapper("uaa", providerRoleMapping);
+ Collection extends GrantedAuthority> authorities = defaultAuthoritiesMapper.mapScopesToAuthorities("uaa",
+ scopes, null);
+
+ assertThat(authorities).hasSize(7);
+ assertThat(authorities.stream().map(authority -> authority.getAuthority()).collect(Collectors.toList()))
+ .containsExactlyInAnyOrder("ROLE_CREATE", "ROLE_DEPLOY", "ROLE_DESTROY", "ROLE_MANAGE", "ROLE_MODIFY",
+ "ROLE_SCHEDULE", "ROLE_VIEW");
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/support/ExternalOauth2ResourceAuthoritiesMapperTests.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/support/ExternalOauth2ResourceAuthoritiesMapperTests.java
new file mode 100644
index 0000000000..a303a2da44
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/support/ExternalOauth2ResourceAuthoritiesMapperTests.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2018-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.
+ * 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.cloud.common.security.support;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import mockwebserver3.MockResponse;
+import mockwebserver3.MockWebServer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+/**
+ * @author Mike Heath
+ * @author Gunnar Hillert
+ * @author Corneil du Plessis
+ */
+public class ExternalOauth2ResourceAuthoritiesMapperTests {
+
+ public MockWebServer mockBackEnd;
+
+
+ @BeforeEach
+ public void setUp() throws IOException {
+ mockBackEnd = new MockWebServer();
+ mockBackEnd.start();
+ }
+ @AfterEach
+ public void tearDown() throws IOException {
+ mockBackEnd.shutdown();
+ }
+
+
+ @Test
+ public void testExtractAuthorities() throws Exception {
+ assertAuthorities2(mockBackEnd.url("/authorities").uri(), "VIEW");
+ assertAuthorities2(mockBackEnd.url("/authorities").uri(), "VIEW", "CREATE", "MANAGE");
+ assertAuthorities2(mockBackEnd.url("/").uri(), "MANAGE");
+ assertAuthorities2(mockBackEnd.url("/").uri(), "DEPLOY", "DESTROY", "MODIFY", "SCHEDULE");
+ assertThat(mockBackEnd.getRequestCount()).isEqualTo(4);
+ }
+
+ private void assertAuthorities2(URI uri, String... roles) throws Exception {
+ ObjectMapper objectMapper = new ObjectMapper();
+ mockBackEnd.enqueue(new MockResponse().newBuilder()
+ .body(objectMapper.writeValueAsString(roles))
+ .addHeader("Content-Type", "application/json").build());
+
+ final ExternalOauth2ResourceAuthoritiesMapper authoritiesExtractor =
+ new ExternalOauth2ResourceAuthoritiesMapper(uri);
+ final Set grantedAuthorities = authoritiesExtractor.mapScopesToAuthorities(null, new HashSet<>(), "1234567");
+ for (String role : roles) {
+ assertThat(grantedAuthorities).containsAnyOf(new SimpleGrantedAuthority(SecurityConfigUtils.ROLE_PREFIX + role));
+ }
+ assertThat(mockBackEnd.takeRequest().getHeaders().get("Authorization")).isEqualTo("Bearer 1234567");
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/support/MappingJwtGrantedAuthoritiesConverterTests.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/support/MappingJwtGrantedAuthoritiesConverterTests.java
new file mode 100644
index 0000000000..ac5fb55274
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/support/MappingJwtGrantedAuthoritiesConverterTests.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2020-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.
+ * 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.cloud.common.security.support;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link MappingJwtGrantedAuthoritiesConverter}
+ *
+ */
+public class MappingJwtGrantedAuthoritiesConverterTests {
+
+ public static Jwt.Builder jwt() {
+ return Jwt.withTokenValue("token")
+ .header("alg", "none")
+ .audience(Arrays.asList("https://audience.example.org"))
+ .expiresAt(Instant.MAX)
+ .issuedAt(Instant.MIN)
+ .issuer("https://issuer.example.org")
+ .jti("jti")
+ .notBefore(Instant.MIN)
+ .subject("mock-test-subject");
+ }
+
+ public static Jwt user() {
+ return jwt()
+ .claim("sub", "mock-test-subject")
+ .build();
+ }
+
+ @Test
+ public void convertWhenTokenHasScopeAttributeThenTranslatedToAuthorities() {
+ Jwt jwt = jwt().claim("scope", "message:read message:write").build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).containsExactlyInAnyOrder(
+ new SimpleGrantedAuthority("SCOPE_message:read"),
+ new SimpleGrantedAuthority("SCOPE_message:write"));
+ }
+
+ @Test
+ public void convertWithCustomAuthorityPrefixWhenTokenHasScopeAttributeThenTranslatedToAuthoritiesViaMapping() {
+ Jwt jwt = jwt().claim("scope", "message:read message:write").build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
+ Map authoritiesMapping = new HashMap<>();
+ authoritiesMapping.put("READ", "message:read");
+ authoritiesMapping.put("WRITE", "message:write");
+ jwtGrantedAuthoritiesConverter.setAuthoritiesMapping(authoritiesMapping);
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).containsExactly(
+ new SimpleGrantedAuthority("ROLE_READ"),
+ new SimpleGrantedAuthority("ROLE_WRITE"));
+ }
+
+ @Test
+ public void convertWithCustomAuthorityWhenTokenHasScopeAttributeThenTranslatedToAuthoritiesViaMapping() {
+ Jwt jwt = jwt().claim("scope", "message:read message:write").build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
+ Map authoritiesMapping = new HashMap<>();
+ authoritiesMapping.put("ROLE_READ", "message:read");
+ authoritiesMapping.put("ROLE_WRITE", "message:write");
+ jwtGrantedAuthoritiesConverter.setAuthoritiesMapping(authoritiesMapping);
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).containsExactly(
+ new SimpleGrantedAuthority("ROLE_READ"),
+ new SimpleGrantedAuthority("ROLE_WRITE"));
+ }
+
+ @Test
+ public void convertWithCustomAuthorityPrefixWhenTokenHasScopeAttributeThenTranslatedToAuthorities() {
+ Jwt jwt = jwt().claim("scope", "message:read message:write").build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).containsExactlyInAnyOrder(
+ new SimpleGrantedAuthority("ROLE_message:read"),
+ new SimpleGrantedAuthority("ROLE_message:write"));
+ }
+
+ @Test
+ public void convertWhenTokenHasEmptyScopeAttributeThenTranslatedToNoAuthorities() {
+ Jwt jwt = jwt().claim("scope", "").build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).isEmpty();
+ }
+
+ @Test
+ public void convertWhenTokenHasScpAttributeThenTranslatedToAuthorities() {
+ Jwt jwt = jwt().claim("scp", Arrays.asList("message:read", "message:write")).build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).containsExactlyInAnyOrder(
+ new SimpleGrantedAuthority("SCOPE_message:read"),
+ new SimpleGrantedAuthority("SCOPE_message:write"));
+ }
+
+ @Test
+ public void convertWithCustomAuthorityPrefixWhenTokenHasScpAttributeThenTranslatedToAuthorities() {
+ Jwt jwt = jwt().claim("scp", Arrays.asList("message:read", "message:write")).build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).containsExactlyInAnyOrder(
+ new SimpleGrantedAuthority("ROLE_message:read"),
+ new SimpleGrantedAuthority("ROLE_message:write"));
+ }
+
+ @Test
+ public void convertWhenTokenHasEmptyScpAttributeThenTranslatedToNoAuthorities() {
+ Jwt jwt = jwt().claim("scp", Collections.emptyList()).build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).isEmpty();
+ }
+
+ @Test
+ public void convertWhenTokenHasBothScopeAndScpThenScopeAttributeIsTranslatedToAuthorities() {
+ Jwt jwt = jwt()
+ .claim("scp", Arrays.asList("message:read", "message:write"))
+ .claim("scope", "missive:read missive:write")
+ .build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).containsExactly(
+ new SimpleGrantedAuthority("SCOPE_missive:read"),
+ new SimpleGrantedAuthority("SCOPE_missive:write"));
+ }
+
+ @Test
+ public void convertWhenTokenHasEmptyScopeAndNonEmptyScpThenScopeAttributeIsTranslatedToNoAuthorities() {
+ Jwt jwt = jwt()
+ .claim("scp", Arrays.asList("message:read", "message:write"))
+ .claim("scope", "")
+ .build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).isEmpty();
+ }
+
+ @Test
+ public void convertWhenTokenHasEmptyScopeAndEmptyScpAttributeThenTranslatesToNoAuthorities() {
+ Jwt jwt = jwt()
+ .claim("scp", Collections.emptyList())
+ .claim("scope", Collections.emptyList())
+ .build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).isEmpty();
+ }
+
+ @Test
+ public void convertWhenTokenHasNoScopeAndNoScpAttributeThenTranslatesToNoAuthorities() {
+ Jwt jwt = jwt().claim("xxx", Arrays.asList("message:read", "message:write")).build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).isEmpty();
+ }
+
+ @Test
+ public void convertWhenTokenHasUnsupportedTypeForScopeThenTranslatesToNoAuthorities() {
+ Jwt jwt = jwt().claim("scope", new String[] {"message:read", "message:write"}).build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).isEmpty();
+ }
+
+ @Test
+ public void convertWhenTokenHasCustomClaimNameThenCustomClaimNameAttributeIsTranslatedToAuthorities() {
+ Jwt jwt = jwt()
+ .claim("xxx", Arrays.asList("message:read", "message:write"))
+ .claim("scope", "missive:read missive:write")
+ .build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("xxx");
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).containsExactlyInAnyOrder(
+ new SimpleGrantedAuthority("SCOPE_message:read"),
+ new SimpleGrantedAuthority("SCOPE_message:write"));
+ }
+
+ @Test
+ public void convertWhenTokenHasEmptyCustomClaimNameThenCustomClaimNameAttributeIsTranslatedToNoAuthorities() {
+ Jwt jwt = jwt()
+ .claim("roles", Collections.emptyList())
+ .claim("scope", "missive:read missive:write")
+ .build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).isEmpty();
+ }
+
+ @Test
+ public void convertWhenTokenHasNoCustomClaimNameThenCustomClaimNameAttributeIsTranslatedToNoAuthorities() {
+ Jwt jwt = jwt().claim("scope", "missive:read missive:write").build();
+
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+
+ assertThat(authorities).isEmpty();
+ }
+
+ @Test
+ public void convertWhenTokenHasGroupClaims() {
+ Jwt jwt = jwt().claim("groups", Arrays.asList("role1")).build();
+ MappingJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new MappingJwtGrantedAuthoritiesConverter();
+ Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
+ assertThat(authorities).containsExactlyInAnyOrder(new SimpleGrantedAuthority("SCOPE_role1"));
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/support/OAuth2TokenUtilsServiceTests.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/support/OAuth2TokenUtilsServiceTests.java
new file mode 100644
index 0000000000..d0aa68a8c2
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/support/OAuth2TokenUtilsServiceTests.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2019-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.
+ * 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.cloud.common.security.support;
+
+import java.time.Instant;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.cloud.common.security.core.support.OAuth2TokenUtilsService;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ *
+ * @author Gunnar Hillert
+ *
+ */
+public class OAuth2TokenUtilsServiceTests {
+
+ @Test
+ public void testGetAccessTokenOfAuthenticatedUserWithNoAuthentication() {
+ SecurityContextHolder.getContext().setAuthentication(null);
+
+ final OAuth2AuthorizedClientService oauth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class);
+ OAuth2TokenUtilsService oAuth2TokenUtilsService = new DefaultOAuth2TokenUtilsService(oauth2AuthorizedClientService);
+
+ assertThatThrownBy(() -> {
+ oAuth2TokenUtilsService.getAccessTokenOfAuthenticatedUser();
+ }).isInstanceOf(IllegalStateException.class).hasMessageContaining(
+ "Cannot retrieve the authentication object from the SecurityContext. Are you authenticated?");
+ }
+
+ @Test
+ public void testGetAccessTokenOfAuthenticatedUserWithWrongAuthentication() {
+ final Authentication authentication = mock(Authentication.class);
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ final OAuth2AuthorizedClientService oauth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class);
+ OAuth2TokenUtilsService oAuth2TokenUtilsService = new DefaultOAuth2TokenUtilsService(oauth2AuthorizedClientService);
+
+ assertThatThrownBy(() -> {
+ oAuth2TokenUtilsService.getAccessTokenOfAuthenticatedUser();
+ }).isInstanceOf(IllegalStateException.class).hasMessageContaining("Unsupported authentication object type");
+ SecurityContextHolder.getContext().setAuthentication(null);
+ }
+
+ @Test
+ public void testGetAccessTokenOfAuthenticatedUserWithEmptyPrincipalName() {
+ final OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
+ when(authentication.getName()).thenReturn("");
+ when(authentication.getAuthorizedClientRegistrationId()).thenReturn("uaa");
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ final OAuth2AuthorizedClientService oauth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class);
+ OAuth2TokenUtilsService oAuth2TokenUtilsService = new DefaultOAuth2TokenUtilsService(oauth2AuthorizedClientService);
+
+ assertThatThrownBy(() -> {
+ oAuth2TokenUtilsService.getAccessTokenOfAuthenticatedUser();
+ }).isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("The retrieved principalName must not be null or empty.");
+ SecurityContextHolder.getContext().setAuthentication(null);
+ }
+
+ @Test
+ public void testGetAccessTokenOfAuthenticatedUserWithEmptyClientRegistrationId() {
+ final OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
+ when(authentication.getName()).thenReturn("FOO");
+ when(authentication.getAuthorizedClientRegistrationId()).thenReturn("");
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ final OAuth2AuthorizedClientService oauth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class);
+ OAuth2TokenUtilsService oAuth2TokenUtilsService = new DefaultOAuth2TokenUtilsService(oauth2AuthorizedClientService);
+
+ assertThatThrownBy(() -> {
+ oAuth2TokenUtilsService.getAccessTokenOfAuthenticatedUser();
+ }).isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("The retrieved clientRegistrationId must not be null or empty.");
+ SecurityContextHolder.getContext().setAuthentication(null);
+ }
+
+ @Test
+ public void testGetAccessTokenOfAuthenticatedUserWithWrongClientRegistrationId() {
+ final OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
+ when(authentication.getName()).thenReturn("my-username");
+ when(authentication.getAuthorizedClientRegistrationId()).thenReturn("CID");
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ final OAuth2AuthorizedClientService oauth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class);
+ when(oauth2AuthorizedClientService.loadAuthorizedClient("uaa", "my-username")).thenReturn(getOAuth2AuthorizedClient());
+ final OAuth2TokenUtilsService oauth2TokenUtilsService = new DefaultOAuth2TokenUtilsService(oauth2AuthorizedClientService);
+
+ assertThatThrownBy(() -> {
+ oauth2TokenUtilsService.getAccessTokenOfAuthenticatedUser();
+ }).isInstanceOf(IllegalStateException.class).hasMessageContaining(
+ "No oauth2AuthorizedClient returned for clientRegistrationId 'CID' and principalName 'my-username'.");
+ SecurityContextHolder.getContext().setAuthentication(null);
+ }
+
+ @Test
+ public void testGetAccessTokenOfAuthenticatedUserWithAuthentication() {
+ final OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
+ when(authentication.getName()).thenReturn("my-username");
+ when(authentication.getAuthorizedClientRegistrationId()).thenReturn("uaa");
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ final OAuth2AuthorizedClientService oauth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class);
+ when(oauth2AuthorizedClientService.loadAuthorizedClient("uaa", "my-username")).thenReturn(getOAuth2AuthorizedClient());
+ final OAuth2TokenUtilsService oauth2TokenUtilsService = new DefaultOAuth2TokenUtilsService(oauth2AuthorizedClientService);
+
+ assertThat(oauth2TokenUtilsService.getAccessTokenOfAuthenticatedUser()).isEqualTo("foo-bar-123-token");
+ SecurityContextHolder.getContext().setAuthentication(null);
+ }
+
+ private OAuth2AuthorizedClient getOAuth2AuthorizedClient() {
+ final ClientRegistration clientRegistration = ClientRegistration
+ .withRegistrationId("uaa")
+ .clientId("clientId")
+ .clientSecret("clientSecret")
+ .redirectUri("blubba")
+ .authorizationUri("blubba")
+ .tokenUri("blubba")
+ .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+ .build();
+ final OAuth2AccessToken accessToken = new OAuth2AccessToken(TokenType.BEARER, "foo-bar-123-token", Instant.now(), Instant.now().plusMillis(100000));
+ final OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, "my-username", accessToken);
+ return authorizedClient;
+ }
+
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/pom.xml b/spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/pom.xml
new file mode 100644
index 0000000000..26784c8560
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/pom.xml
@@ -0,0 +1,68 @@
+
+
+ 4.0.0
+
+ org.springframework.cloud
+ spring-cloud-common-security-config
+ 2.11.6-SNAPSHOT
+
+ spring-cloud-starter-common-security-config-web
+ spring-cloud-starter-common-security-config-web
+ Spring Cloud Starter Common Security Config Web
+ pom
+
+ true
+ 5.0.0-alpha.14
+
+
+
+ org.springframework.cloud
+ spring-cloud-common-security-config-web
+ ${project.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ com.squareup.okhttp3
+ mockwebserver3-junit5
+ ${okhttp3.version}
+ test
+
+
+ com.squareup.okhttp3
+ okhttp
+ ${okhttp3.version}
+ test
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+ 1.8.22
+ test
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.3.0
+
+
+
+ test-jar
+
+
+
+
+
+
+
diff --git a/spring-cloud-dataflow-completion/src/test/support/common/src/main/java/com/acme/common/SomeEnum.java b/spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/src/test/java/org/springframework/cloud/common/security/SpringCloudCommonSecurityApplicationTests.java
similarity index 61%
rename from spring-cloud-dataflow-completion/src/test/support/common/src/main/java/com/acme/common/SomeEnum.java
rename to spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/src/test/java/org/springframework/cloud/common/security/SpringCloudCommonSecurityApplicationTests.java
index 71c096cc1d..df2f761d8e 100644
--- a/spring-cloud-dataflow-completion/src/test/support/common/src/main/java/com/acme/common/SomeEnum.java
+++ b/spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/src/test/java/org/springframework/cloud/common/security/SpringCloudCommonSecurityApplicationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016 the original author or authors.
+ * Copyright 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.
@@ -13,17 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.springframework.cloud.common.security;
-package com.acme.common;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.test.context.SpringBootTest;
/**
- * An enum class used in {@link ConfigProperties}. Useful to test, because this class has
- * to be accessible to the ClassLoader used to retrieve metadata.
+ * Testing startup and configuration
*
- * @author Eric Bottard
+ * @author Corneil du Plessis
*/
-public enum SomeEnum {
- one,
- two,
- three;
+@SpringBootTest
+class SpringCloudCommonSecurityApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
}
diff --git a/spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/src/test/java/org/springframework/cloud/common/security/SpringCloudCommonSecurityTestApplication.java b/spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/src/test/java/org/springframework/cloud/common/security/SpringCloudCommonSecurityTestApplication.java
new file mode 100644
index 0000000000..08c8855d75
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/src/test/java/org/springframework/cloud/common/security/SpringCloudCommonSecurityTestApplication.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 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.
+ * 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.cloud.common.security;
+
+import java.security.Principal;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
+import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
+import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
+import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration;
+import org.springframework.context.annotation.Import;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Minimal application to verify configuration
+ *
+ * @author Corneil du Plessis
+ */
+@SpringBootApplication(exclude = {
+ MetricsAutoConfiguration.class,
+ ManagementWebSecurityAutoConfiguration.class,
+ SecurityAutoConfiguration.class,
+ UserDetailsServiceAutoConfiguration.class,
+ SessionAutoConfiguration.class
+})
+
+@Import({CommonSecurityAutoConfiguration.class, TestOAuthSecurityConfiguration.class})
+public class SpringCloudCommonSecurityTestApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringCloudCommonSecurityTestApplication.class, args);
+ }
+
+ @RestController
+ public static class SimpleController {
+ @GetMapping("/user")
+ public String getUser(Principal principal) {
+ return principal.getName();
+ }
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/src/test/java/org/springframework/cloud/common/security/TestOAuthSecurityConfiguration.java b/spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/src/test/java/org/springframework/cloud/common/security/TestOAuthSecurityConfiguration.java
new file mode 100644
index 0000000000..0b1b2ea2e8
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/src/test/java/org/springframework/cloud/common/security/TestOAuthSecurityConfiguration.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 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.
+ * 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.cloud.common.security;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.common.security.support.OnOAuth2SecurityEnabled;
+import org.springframework.cloud.common.security.support.SecurityStateBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * We need to mimic the configuration of Dataflow and Skipper
+ *
+ * @author Corneil du Plessis
+ */
+@Configuration(proxyBeanMethods = false)
+@Conditional(OnOAuth2SecurityEnabled.class)
+@Import(TestOAuthSecurityConfiguration.SecurityStateBeanConfig.class)
+public class TestOAuthSecurityConfiguration extends OAuthSecurityConfiguration {
+
+ @Configuration(proxyBeanMethods = false)
+ public static class SecurityStateBeanConfig {
+ @Bean
+ public SecurityStateBean securityStateBean() {
+ return new SecurityStateBean();
+ }
+
+ @Bean
+ @ConfigurationProperties(prefix = "spring.cloud.common.security.test.authorization")
+ public AuthorizationProperties authorizationProperties() {
+ return new AuthorizationProperties();
+ }
+ }
+}
diff --git a/spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/src/test/resources/application.yml b/spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/src/test/resources/application.yml
new file mode 100644
index 0000000000..e5de703119
--- /dev/null
+++ b/spring-cloud-common-security-config/spring-cloud-starter-common-security-config-web/src/test/resources/application.yml
@@ -0,0 +1,40 @@
+logging:
+# file:
+# name: sccsc-test.log
+ level:
+ org.springframework: DEBUG
+spring:
+ security:
+ oauth2:
+ client:
+ registration:
+ uaa:
+ redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
+ authorization-grant-type: authorization_code
+ client-id: myclient
+ client-secret: mysecret
+ access-token-uri: http://127.0.0.1:8888/oauth/token
+ user-authorization-uri: http://127.0.0.1:8888/oauth/authorize
+ provider:
+ uaa:
+ authorization-uri: http://127.0.0.1:8888/oauth/authorize
+ user-info-uri: http://127.0.0.1:8888/me
+ token-uri: http://127.0.0.1:8888/oauth/token
+ resourceserver:
+ opaquetoken:
+ introspection-uri: http://127.0.0.1:8888/oauth/check_token
+ client-id: myclient
+ client-secret: mysecret
+ cloud:
+ common:
+ security:
+ test:
+ authorization:
+ check-token-access: isAuthenticated()
+ authorization:
+ enabled: true
+ permit-all-paths: "/user,./assets/**,/dashboard/logout-success-oauth.html"
+ authenticated-paths: "/user"
+ rules:
+ # User
+ - GET /user => hasRole('ROLE_VIEW')
diff --git a/spring-cloud-dataflow-aggregate-task/README.adoc b/spring-cloud-dataflow-aggregate-task/README.adoc
new file mode 100644
index 0000000000..54ea33b3a3
--- /dev/null
+++ b/spring-cloud-dataflow-aggregate-task/README.adoc
@@ -0,0 +1,10 @@
+= Spring Cloud Dataflow Aggregate Task Module
+
+Spring Cloud Task and Spring Batch utilize a series of database tables to support storing data about Boot Application executions as well as Job executions.
+For each major release of these projects, their database schemas adjust to meet the needs for the latest release.
+SCDF supports applications that may use the current release of these projects as well as a previous release.
+The `spring-cloud-dataflow-aggregate-task` module provides support for dataflow to query and mutate data in each of the schema versions.
+
+== Tests
+
+The tests for this module are located in the `spring-cloud-dataflow-server` module
\ No newline at end of file
diff --git a/spring-cloud-dataflow-aggregate-task/pom.xml b/spring-cloud-dataflow-aggregate-task/pom.xml
new file mode 100644
index 0000000000..88f9fe8db9
--- /dev/null
+++ b/spring-cloud-dataflow-aggregate-task/pom.xml
@@ -0,0 +1,110 @@
+
+
+ 4.0.0
+
+ org.springframework.cloud
+ spring-cloud-dataflow-parent
+ 2.11.6-SNAPSHOT
+ ../spring-cloud-dataflow-parent
+
+ spring-cloud-dataflow-aggregate-task
+ spring-cloud-dataflow-aggregate-task
+ Spring Cloud Data Flow Aggregate Task
+
+ jar
+
+ true
+ 3.4.1
+
+
+
+ org.springframework
+ spring-core
+
+
+ org.springframework
+ spring-context
+ compile
+
+
+ org.springframework.cloud
+ spring-cloud-task-batch
+
+
+ org.springframework.cloud
+ spring-cloud-dataflow-core
+ ${project.version}
+
+
+ org.springframework.cloud
+ spring-cloud-dataflow-registry
+ ${project.version}
+
+
+ org.springframework.cloud
+ spring-cloud-dataflow-schema
+ ${project.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ com.h2database
+ h2
+ test
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ false
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.0
+
+
+ source
+
+ jar
+
+ package
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ ${maven-javadoc-plugin.version}
+
+
+ javadoc
+
+ jar
+
+ package
+
+
+
+
+
+
diff --git a/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/AggregateExecutionSupport.java b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/AggregateExecutionSupport.java
new file mode 100644
index 0000000000..3f8d12ab7d
--- /dev/null
+++ b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/AggregateExecutionSupport.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 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.cloud.dataflow.aggregate.task;
+
+import org.springframework.cloud.dataflow.core.AppRegistration;
+import org.springframework.cloud.dataflow.core.TaskDefinition;
+import org.springframework.cloud.dataflow.schema.AggregateTaskExecution;
+import org.springframework.cloud.dataflow.schema.SchemaVersionTarget;
+import org.springframework.cloud.task.repository.TaskExecution;
+
+/**
+ * Allows users to retrieve Task execution and SchemaVersion information from either {@link TaskExecution} as well as
+ * Task Name.
+ * @author Corneil du Plessis
+ */
+public interface AggregateExecutionSupport {
+
+ /**
+ * Retrieves the {@link AggregateTaskExecution} for the task execution and {@link TaskDefinitionReader} provided.
+ * @param execution A {@link TaskExecution} that contains the TaskName that will be used to find the {@link AggregateTaskExecution}.
+ * @param taskDefinitionReader {@link TaskDefinitionReader} that will be used to find the {@link SchemaVersionTarget} for the task execution.
+ * @param taskDeploymentReader {@link TaskDeploymentReader} will be used to read the deployment.
+ * @return The {@link AggregateTaskExecution} containing the {@link SchemaVersionTarget} for the TaskExecution.
+ */
+ AggregateTaskExecution from(TaskExecution execution, TaskDefinitionReader taskDefinitionReader, TaskDeploymentReader taskDeploymentReader);
+
+ /**
+ * Retrieves the {@link SchemaVersionTarget} for the task name.
+ * @param taskName The name of the {@link org.springframework.cloud.dataflow.core.TaskDefinition} from which the {@link SchemaVersionTarget} will be retreived.
+ * @param taskDefinitionReader {@link TaskDefinitionReader} that will be used to find the {@link SchemaVersionTarget}
+ * @return The {@link SchemaVersionTarget} for the taskName specified.
+ */
+ SchemaVersionTarget findSchemaVersionTarget(String taskName, TaskDefinitionReader taskDefinitionReader);
+ SchemaVersionTarget findSchemaVersionTarget(String taskName, String version, TaskDefinitionReader taskDefinitionReader);
+ SchemaVersionTarget findSchemaVersionTarget(String taskName, TaskDefinition taskDefinition);
+ SchemaVersionTarget findSchemaVersionTarget(String taskName, String version, TaskDefinition taskDefinition);
+
+ /**
+ * Retrieve the {@link AppRegistration} for the registeredName.
+ * @param registeredName Registered name for registration to find.
+ * @return The application registration
+ */
+ AppRegistration findTaskAppRegistration(String registeredName);
+ AppRegistration findTaskAppRegistration(String registeredName, String version);
+
+ /**
+ * Return the {@link AggregateTaskExecution} for the {@link TaskExecution} and Schema Target name specified.
+ * @param execution The task execution
+ * @param schemaTarget The schemaTarget of the task execution
+ * @param platformName The platform name of the task execution
+ * @return The task execution
+ */
+ AggregateTaskExecution from(TaskExecution execution, String schemaTarget, String platformName);
+}
diff --git a/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/AggregateTaskConfiguration.java b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/AggregateTaskConfiguration.java
new file mode 100644
index 0000000000..6b8b81dd2a
--- /dev/null
+++ b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/AggregateTaskConfiguration.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2017-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.
+ * 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.cloud.dataflow.aggregate.task;
+
+import javax.annotation.PostConstruct;
+import javax.sql.DataSource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.cloud.dataflow.aggregate.task.impl.AggregateDataFlowTaskExecutionQueryDao;
+import org.springframework.cloud.dataflow.aggregate.task.impl.DefaultAggregateExecutionSupport;
+import org.springframework.cloud.dataflow.aggregate.task.impl.DefaultAggregateTaskExplorer;
+import org.springframework.cloud.dataflow.aggregate.task.impl.DefaultTaskRepositoryContainer;
+import org.springframework.cloud.dataflow.registry.service.AppRegistryService;
+import org.springframework.cloud.dataflow.schema.service.SchemaService;
+import org.springframework.cloud.dataflow.schema.service.SchemaServiceConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.util.Assert;
+
+/**
+ * Configuration for aggregate task related components.
+ *
+ * @author Corneil du Plessis
+ */
+@Configuration
+@Import(SchemaServiceConfiguration.class)
+public class AggregateTaskConfiguration {
+ private static final Logger logger = LoggerFactory.getLogger(AggregateTaskConfiguration.class);
+
+
+ @Bean
+ public DataflowTaskExecutionQueryDao dataflowTaskExecutionQueryDao(
+ DataSource dataSource,
+ SchemaService schemaService
+ ) {
+ return new AggregateDataFlowTaskExecutionQueryDao(dataSource, schemaService);
+ }
+
+ @Bean
+ public AggregateExecutionSupport aggregateExecutionSupport(
+ AppRegistryService registryService,
+ SchemaService schemaService
+ ) {
+ return new DefaultAggregateExecutionSupport(registryService, schemaService);
+ }
+
+ @Bean
+ public TaskRepositoryContainer taskRepositoryContainer(
+ DataSource dataSource,
+ SchemaService schemaService
+ ) {
+ return new DefaultTaskRepositoryContainer(dataSource, schemaService);
+ }
+
+ @Bean
+ public AggregateTaskExplorer aggregateTaskExplorer(
+ DataSource dataSource,
+ DataflowTaskExecutionQueryDao taskExecutionQueryDao,
+ SchemaService schemaService,
+ AggregateExecutionSupport aggregateExecutionSupport,
+ TaskDefinitionReader taskDefinitionReader,
+ TaskDeploymentReader taskDeploymentReader
+ ) {
+ Assert.notNull(dataSource, "dataSource required");
+ Assert.notNull(taskExecutionQueryDao, "taskExecutionQueryDao required");
+ Assert.notNull(schemaService, "schemaService required");
+ Assert.notNull(aggregateExecutionSupport, "aggregateExecutionSupport required");
+ Assert.notNull(taskDefinitionReader, "taskDefinitionReader required");
+ Assert.notNull(taskDeploymentReader, "taskDeploymentReader required");
+ return new DefaultAggregateTaskExplorer(dataSource,
+ taskExecutionQueryDao,
+ schemaService,
+ aggregateExecutionSupport,
+ taskDefinitionReader,
+ taskDeploymentReader);
+ }
+
+ @PostConstruct
+ public void setup() {
+ logger.info("created: org.springframework.cloud.dataflow.aggregate.task.AggregateTaskConfiguration");
+ }
+}
diff --git a/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/AggregateTaskExplorer.java b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/AggregateTaskExplorer.java
new file mode 100644
index 0000000000..a5390fc1a0
--- /dev/null
+++ b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/AggregateTaskExplorer.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2023-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.cloud.dataflow.aggregate.task;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.cloud.dataflow.schema.AggregateTaskExecution;
+import org.springframework.cloud.task.repository.TaskExecution;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+/**
+ * Provides for exploring tasks for multiple schema targets.
+ *
+ * @author Corneil du Plessis
+ */
+public interface AggregateTaskExplorer {
+ /**
+ * find a task execution given an execution id and schema target.
+ *
+ * @param executionId the task execution id
+ * @param schemaTarget the schema target
+ * @return the task execution
+ */
+ AggregateTaskExecution getTaskExecution(long executionId, String schemaTarget);
+
+ /**
+ * find a task execution given an external execution id and platform name.
+ *
+ * @param externalExecutionId the external execution id
+ * @param platform the platform name
+ * @return the task execution
+ */
+ AggregateTaskExecution getTaskExecutionByExternalExecutionId(String externalExecutionId, String platform);
+
+ List findChildTaskExecutions(long executionId, String schemaTarget);
+
+ List findChildTaskExecutions(Collection parentIds, String schemaTarget);
+
+ /**
+ * Retrieve a collection of taskExecutions that have the task name provided.
+ *
+ * @param taskName the name of the task
+ * @param pageable the constraints for the search
+ * @return the set of running executions for tasks with the specified name
+ */
+ Page findRunningTaskExecutions(String taskName, Pageable pageable);
+
+ /**
+ * Retrieve a list of available task names.
+ *
+ * @return the task names that have been executed
+ */
+ List getTaskNames();
+
+ /**
+ * Get number of executions for a taskName.
+ *
+ * @param taskName the name of the task to be searched
+ * @return the number of running tasks that have the taskname specified
+ */
+ long getTaskExecutionCountByTaskName(String taskName);
+
+ /**
+ * Retrieves current number of task executions.
+ *
+ * @return current number of task executions.
+ */
+ long getTaskExecutionCount();
+
+ /**
+ * Retrieves current number of running task executions.
+ *
+ * @return current number of running task executions.
+ */
+ long getRunningTaskExecutionCount();
+
+ /**
+ * Get a list of executions for a task by name and completion status.
+ *
+ * @param taskName the name of the task to be searched
+ * @param onlyCompleted whether to include only completed tasks
+ * @return list of task executions
+ */
+ List findTaskExecutions(String taskName, boolean onlyCompleted);
+
+ /**
+ * Get a list of executions for a task by name, completion status and end time.
+ *
+ * @param taskName the name of the task to be searched
+ * @param endTime the tasks that ended before the endTime
+ * @return list of task executions
+ * @since 2.11.0
+ */
+ List findTaskExecutionsBeforeEndTime(String taskName, Date endTime);
+
+ /**
+ * Get a collection/page of executions.
+ *
+ * @param taskName the name of the task to be searched
+ * @param pageable the constraints for the search
+ * @return list of task executions
+ */
+ Page findTaskExecutionsByName(String taskName, Pageable pageable);
+
+ /**
+ * Retrieves all the task executions within the pageable constraints sorted by start
+ * date descending, taskExecution id descending.
+ *
+ * @param pageable the constraints for the search
+ * @return page containing the results from the search
+ */
+ Page findAll(Pageable pageable);
+
+ /**
+ * Retrieves all the task executions within the pageable constraints sorted by start
+ * date descending, taskExecution id descending.
+ *
+ * @param pageable the constraints for the search
+ * @param thinResults Indicated if arguments will be populated
+ * @return page containing the results from the search
+ */
+ Page findAll(Pageable pageable, boolean thinResults);
+ /**
+ * Returns the id of the TaskExecution that the requested Spring Batch job execution
+ * was executed within the context of. Returns null if none were found.
+ *
+ * @param jobExecutionId the id of the JobExecution
+ * @param schemaTarget the schema target
+ * @return the id of the {@link TaskExecution}
+ */
+ Long getTaskExecutionIdByJobExecutionId(long jobExecutionId, String schemaTarget);
+
+ /**
+ * Returns a Set of JobExecution ids for the jobs that were executed within the scope
+ * of the requested task.
+ *
+ * @param taskExecutionId id of the {@link TaskExecution}
+ * @param schemaTarget the schema target
+ * @return a Set of the ids of the job executions executed within the
+ * task.
+ */
+ Set getJobExecutionIdsByTaskExecutionId(long taskExecutionId, String schemaTarget);
+
+ /**
+ * Returns a {@link List} of the latest {@link TaskExecution} for 1 or more task
+ * names.
+ *
+ * Latest is defined by the most recent start time. A {@link TaskExecution} does not
+ * have to be finished (The results may including pending {@link TaskExecution}s).
+ *
+ * It is theoretically possible that a {@link TaskExecution} with the same name to
+ * have more than 1 {@link TaskExecution} for the exact same start time. In that case
+ * the {@link TaskExecution} with the highest Task Execution ID is returned.
+ *
+ * This method will not consider end times in its calculations. Thus, when a task
+ * execution {@code A} starts after task execution {@code B} but finishes BEFORE task
+ * execution {@code A}, then task execution {@code B} is being returned.
+ *
+ * @param taskNames At least 1 task name must be provided
+ * @return List of TaskExecutions. May be empty but never null.
+ */
+ List getLatestTaskExecutionsByTaskNames(String... taskNames);
+
+ /**
+ * Returns the latest task execution for a given task name. Will ultimately apply the
+ * same algorithm underneath as {@link #getLatestTaskExecutionsByTaskNames(String...)}
+ * but will only return a single result.
+ *
+ * @param taskName Must not be null or empty
+ * @return The latest Task Execution or null
+ * @see #getLatestTaskExecutionsByTaskNames(String...)
+ */
+ AggregateTaskExecution getLatestTaskExecutionForTaskName(String taskName);
+}
diff --git a/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/DataflowTaskExecutionQueryDao.java b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/DataflowTaskExecutionQueryDao.java
new file mode 100644
index 0000000000..c33e14fb66
--- /dev/null
+++ b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/DataflowTaskExecutionQueryDao.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2017-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.
+ * 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.cloud.dataflow.aggregate.task;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+
+import org.springframework.cloud.dataflow.schema.AggregateTaskExecution;
+import org.springframework.cloud.task.repository.TaskExecution;
+import org.springframework.cloud.task.repository.dao.TaskExecutionDao;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.lang.NonNull;
+
+/**
+ * Repository to access {@link TaskExecution}s. Mirrors the {@link TaskExecutionDao}
+ * but contains Spring Cloud Data Flow specific operations. This functionality might
+ * be migrated to Spring Cloud Task itself.
+ *
+ * @author Corneil du Plessis
+ * @since 2.11.0
+ */
+public interface DataflowTaskExecutionQueryDao {
+ /**
+ * Retrieves a task execution from the task repository.
+ *
+ * @param executionId the id associated with the task execution.
+ * @param schemaTarget the schema target.
+ * @return a fully qualified TaskExecution instance.
+ */
+ AggregateTaskExecution getTaskExecution(long executionId, String schemaTarget);
+
+ /**
+ * Retrieves a list of task executions where the provided execution id and schemaTarget represents the parent of task execution.
+ *
+ * @param executionId parent task execution id
+ * @param schemaTarget parent task schema target
+ * @return the task executions
+ */
+ List findChildTaskExecutions(long executionId, String schemaTarget);
+
+ /**
+ * Retrieves a list of task executions where the provided execution ids and schemaTarget represents the parents of task executions.
+ *
+ * @param parentIds parent task execution ids
+ * @param schemaTarget parent task schema target
+ * @return the task executions
+ */
+ List findChildTaskExecutions(Collection parentIds, String schemaTarget);
+
+ /**
+ * Find task executions by task name and completion status.
+ *
+ * @param taskName the name of the task to search for in the repository.
+ * @param completed whether to include only completed task executions.
+ * @return list of task executions
+ */
+ List findTaskExecutions(String taskName, boolean completed);
+
+ /**
+ * Find task executions by task name whose end date is before the specified date.
+ *
+ * @param taskName the name of the task to search for in the repository.
+ * @param endTime the time before the task ended.
+ * @return list of task executions.
+ */
+ List findTaskExecutionsBeforeEndTime(String taskName, @NonNull Date endTime);
+
+ /**
+ * Retrieves current number of task executions for a taskName.
+ *
+ * @param taskName the name of the task
+ * @return current number of task executions for the taskName.
+ */
+ long getTaskExecutionCountByTaskName(String taskName);
+
+ /**
+ * Retrieves current number of task executions for a taskName and with a non-null endTime before the specified date.
+ *
+ * @param taskName the name of the task
+ * @param endTime the time before task ended
+ * @return the number of completed task executions
+ */
+ long getCompletedTaskExecutionCountByTaskNameAndBeforeDate(String taskName, @NonNull Date endTime);
+
+ /**
+ * Retrieves current number of task executions for a taskName and with a non-null endTime.
+ *
+ * @param taskName the name of the task
+ * @return the number of completed task executions
+ */
+ long getCompletedTaskExecutionCountByTaskName(String taskName);
+
+ /**
+ * Retrieves current number of task executions for a taskName and with an endTime of
+ * null.
+ *
+ * @param taskName the name of the task to search for in the repository.
+ * @return the number of running task executions
+ */
+ long getRunningTaskExecutionCountByTaskName(String taskName);
+
+ /**
+ * Retrieves current number of task executions with an endTime of null.
+ *
+ * @return current number of task executions.
+ */
+ long getRunningTaskExecutionCount();
+
+ /**
+ * Retrieves current number of task executions.
+ *
+ * @return current number of task executions.
+ */
+ long getTaskExecutionCount();
+
+ /**
+ * Retrieves a set of task executions that are running for a taskName.
+ *
+ * @param taskName the name of the task to search for in the repository.
+ * @param pageable the constraints for the search.
+ * @return set of running task executions.
+ */
+ Page findRunningTaskExecutions(String taskName, Pageable pageable);
+
+ /**
+ * Retrieves a subset of task executions by task name, start location and size.
+ *
+ * @param taskName the name of the task to search for in the repository.
+ * @param pageable the constraints for the search.
+ * @return a list that contains task executions from the query bound by the start
+ * position and count specified by the user.
+ */
+ Page findTaskExecutionsByName(String taskName, Pageable pageable);
+
+ /**
+ * Retrieves a sorted list of distinct task names for the task executions.
+ *
+ * @return a list of distinct task names from the task repository..
+ */
+ List getTaskNames();
+
+ /**
+ * Retrieves all the task executions within the pageable constraints.
+ *
+ * @param pageable the constraints for the search
+ * @return page containing the results from the search
+ */
+
+ Page findAll(Pageable pageable);
+
+ /**
+ * Retrieves all the task executions within the pageable constraints.
+ * @param pageable the constraints for the search
+ * @param thinResults Indicated if arguments will be populated
+ * @return page containing the results from the search
+ */
+
+ Page findAll(Pageable pageable, boolean thinResults);
+
+ /**
+ * Returns a {@link List} of the latest {@link TaskExecution} for 1 or more task
+ * names.
+ *
+ * Latest is defined by the most recent start time. A {@link TaskExecution} does not
+ * have to be finished (The results may including pending {@link TaskExecution}s).
+ *
+ * It is theoretically possible that a {@link TaskExecution} with the same name to
+ * have more than 1 {@link TaskExecution} for the exact same start time. In that case
+ * the {@link TaskExecution} with the highest Task Execution ID is returned.
+ *
+ * This method will not consider end times in its calculations. Thus, when a task
+ * execution {@code A} starts after task execution {@code B} but finishes BEFORE task
+ * execution {@code A}, then task execution {@code B} is being returned.
+ *
+ * @param taskNames At least 1 task name must be provided
+ * @return List of TaskExecutions. May be empty but never null.
+ */
+ List getLatestTaskExecutionsByTaskNames(String... taskNames);
+
+ /**
+ * Returns the latest task execution for a given task name. Will ultimately apply the
+ * same algorithm underneath as {@link #getLatestTaskExecutionsByTaskNames(String...)}
+ * but will only return a single result.
+ *
+ * @param taskName Must not be null or empty
+ * @return The latest Task Execution or null
+ * @see #getLatestTaskExecutionsByTaskNames(String...)
+ */
+ AggregateTaskExecution getLatestTaskExecutionForTaskName(String taskName);
+
+ AggregateTaskExecution geTaskExecutionByExecutionId(String executionId, String taskName);
+
+}
diff --git a/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/TaskDefinitionReader.java b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/TaskDefinitionReader.java
new file mode 100644
index 0000000000..a88434e8b4
--- /dev/null
+++ b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/TaskDefinitionReader.java
@@ -0,0 +1,7 @@
+package org.springframework.cloud.dataflow.aggregate.task;
+
+import org.springframework.cloud.dataflow.core.TaskDefinition;
+
+public interface TaskDefinitionReader {
+ TaskDefinition findTaskDefinition(String taskName);
+}
diff --git a/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/TaskDeploymentReader.java b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/TaskDeploymentReader.java
new file mode 100644
index 0000000000..768ee84069
--- /dev/null
+++ b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/TaskDeploymentReader.java
@@ -0,0 +1,11 @@
+package org.springframework.cloud.dataflow.aggregate.task;
+
+import java.util.List;
+
+import org.springframework.cloud.dataflow.core.TaskDeployment;
+
+public interface TaskDeploymentReader {
+ TaskDeployment getDeployment(String externalTaskId);
+ TaskDeployment getDeployment(String externalTaskId, String platform);
+ TaskDeployment findByDefinitionName(String definitionName);
+}
diff --git a/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/TaskRepositoryContainer.java b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/TaskRepositoryContainer.java
new file mode 100644
index 0000000000..77dae057a2
--- /dev/null
+++ b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/TaskRepositoryContainer.java
@@ -0,0 +1,7 @@
+package org.springframework.cloud.dataflow.aggregate.task;
+
+import org.springframework.cloud.task.repository.TaskRepository;
+
+public interface TaskRepositoryContainer {
+ TaskRepository get(String schemaTarget);
+}
diff --git a/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/AggregateDataFlowTaskExecutionQueryDao.java b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/AggregateDataFlowTaskExecutionQueryDao.java
new file mode 100644
index 0000000000..e494b87ee7
--- /dev/null
+++ b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/AggregateDataFlowTaskExecutionQueryDao.java
@@ -0,0 +1,619 @@
+/*
+ * Copyright 2023-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.cloud.dataflow.aggregate.task.impl;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.sql.DataSource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.batch.item.database.Order;
+import org.springframework.cloud.dataflow.aggregate.task.DataflowTaskExecutionQueryDao;
+import org.springframework.cloud.dataflow.schema.AggregateTaskExecution;
+import org.springframework.cloud.dataflow.schema.service.SchemaService;
+import org.springframework.cloud.task.repository.database.PagingQueryProvider;
+import org.springframework.cloud.task.repository.database.support.SqlPagingQueryProviderFactoryBean;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.jdbc.core.RowCallbackHandler;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.jdbc.core.namedparam.SqlParameterSource;
+import org.springframework.lang.NonNull;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Provide aggregate data for Boot 3 and Boot <=2 TaskExecutions.
+ *
+ * @author Corneil du Plessis
+ */
+
+public class AggregateDataFlowTaskExecutionQueryDao implements DataflowTaskExecutionQueryDao {
+ private final static Logger logger = LoggerFactory.getLogger(AggregateDataFlowTaskExecutionQueryDao.class);
+
+ /**
+ * SELECT clause for task execution.
+ */
+ public static final String SELECT_CLAUSE = "TASK_EXECUTION_ID, "
+ + "START_TIME, END_TIME, TASK_NAME, EXIT_CODE, "
+ + "EXIT_MESSAGE, ERROR_MESSAGE, LAST_UPDATED, "
+ + "EXTERNAL_EXECUTION_ID, PARENT_EXECUTION_ID, SCHEMA_TARGET ";
+
+ /**
+ * FROM clause for task execution.
+ */
+ public static final String FROM_CLAUSE = "AGGREGATE_TASK_EXECUTION";
+
+ /**
+ * WHERE clause for running task.
+ */
+ public static final String RUNNING_TASK_WHERE_CLAUSE = "where TASK_NAME = :taskName AND END_TIME IS NULL ";
+
+ /**
+ * WHERE clause for task name.
+ */
+ public static final String TASK_NAME_WHERE_CLAUSE = "where TASK_NAME = :taskName ";
+
+ private static final String FIND_TASK_ARGUMENTS = "SELECT TASK_EXECUTION_ID, "
+ + "TASK_PARAM from AGGREGATE_TASK_EXECUTION_PARAMS where TASK_EXECUTION_ID = :taskExecutionId and SCHEMA_TARGET = :schemaTarget";
+
+ private static final String FIND_TASKS_ARGUMENTS = "SELECT TASK_EXECUTION_ID, "
+ + "TASK_PARAM from AGGREGATE_TASK_EXECUTION_PARAMS where TASK_EXECUTION_ID IN (:taskExecutionIds) and SCHEMA_TARGET = :schemaTarget";
+
+ private static final String GET_EXECUTIONS = "SELECT " + SELECT_CLAUSE +
+ " from AGGREGATE_TASK_EXECUTION";
+
+ private static final String GET_EXECUTION_BY_ID = GET_EXECUTIONS +
+ " where TASK_EXECUTION_ID = :taskExecutionId and SCHEMA_TARGET = :schemaTarget";
+
+ private final static String GET_CHILD_EXECUTION_BY_ID = GET_EXECUTIONS +
+ " where PARENT_EXECUTION_ID = :taskExecutionId" +
+ " and (SELECT COUNT(*) FROM AGGREGATE_TASK_EXECUTION_PARAMS P " +
+ " WHERE P.TASK_EXECUTION_ID=TASK_EXECUTION_ID " +
+ " AND P.SCHEMA_TARGET=SCHEMA_TARGET" +
+ " AND P.TASK_PARAM = :schemaTarget) > 0";
+
+ private final static String GET_CHILD_EXECUTION_BY_IDS = GET_EXECUTIONS +
+ " where PARENT_EXECUTION_ID IN (:taskExecutionIds)" +
+ " and (SELECT COUNT(*) FROM AGGREGATE_TASK_EXECUTION_PARAMS P " +
+ " WHERE P.TASK_EXECUTION_ID=TASK_EXECUTION_ID " +
+ " AND P.SCHEMA_TARGET=SCHEMA_TARGET" +
+ " AND P.TASK_PARAM = :schemaTarget) > 0";
+
+ private static final String GET_EXECUTION_BY_EXTERNAL_EXECUTION_ID = GET_EXECUTIONS +
+ " where EXTERNAL_EXECUTION_ID = :externalExecutionId and TASK_NAME = :taskName";
+
+ private static final String GET_EXECUTIONS_BY_NAME_COMPLETED = GET_EXECUTIONS +
+ " where TASK_NAME = :taskName AND END_TIME IS NOT NULL";
+
+ private static final String GET_EXECUTIONS_BY_NAME = GET_EXECUTIONS +
+ " where TASK_NAME = :taskName";
+
+ private static final String GET_EXECUTIONS_COMPLETED = GET_EXECUTIONS +
+ " where END_TIME IS NOT NULL";
+
+ private static final String GET_EXECUTION_BY_NAME_COMPLETED_BEFORE_END_TIME = GET_EXECUTIONS +
+ " where TASK_NAME = :taskName AND END_TIME IS NOT NULL AND END_TIME < :endTime";
+
+ private static final String GET_EXECUTIONS_COMPLETED_BEFORE_END_TIME = GET_EXECUTIONS +
+ " where END_TIME IS NOT NULL AND END_TIME < :endTime";
+
+ private static final String TASK_EXECUTION_COUNT = "SELECT COUNT(*) FROM "
+ + "AGGREGATE_TASK_EXECUTION ";
+
+ private static final String TASK_EXECUTION_COUNT_BY_NAME = "SELECT COUNT(*) FROM "
+ + "AGGREGATE_TASK_EXECUTION where TASK_NAME = :taskName";
+
+ private static final String TASK_EXECUTION_COUNT_BY_NAME_AND_BEFORE_END_TIME = "SELECT COUNT(*) FROM "
+ + "AGGREGATE_TASK_EXECUTION where TASK_NAME = :taskName AND END_TIME < :endTime";
+
+ private static final String COMPLETED_TASK_EXECUTION_COUNT = "SELECT COUNT(*) FROM "
+ + "AGGREGATE_TASK_EXECUTION WHERE END_TIME IS NOT NULL";
+
+ private static final String COMPLETED_TASK_EXECUTION_COUNT_AND_BEFORE_END_TIME = "SELECT COUNT(*) FROM "
+ + "AGGREGATE_TASK_EXECUTION WHERE END_TIME IS NOT NULL AND END_TIME < :endTime";
+
+ private static final String COMPLETED_TASK_EXECUTION_COUNT_BY_NAME = "SELECT COUNT(*) FROM "
+ + "AGGREGATE_TASK_EXECUTION where TASK_NAME = :taskName AND END_TIME IS NOT NULL ";
+
+ private static final String COMPLETED_TASK_EXECUTION_COUNT_BY_NAME_AND_BEFORE_END_TIME = "SELECT COUNT(*) FROM "
+ + "AGGREGATE_TASK_EXECUTION where TASK_NAME = :taskName AND END_TIME IS NOT NULL AND END_TIME < :endTime ";
+
+
+ private static final String RUNNING_TASK_EXECUTION_COUNT_BY_NAME = "SELECT COUNT(*) FROM "
+ + "AGGREGATE_TASK_EXECUTION where TASK_NAME = :taskName AND END_TIME IS NULL ";
+
+ private static final String RUNNING_TASK_EXECUTION_COUNT = "SELECT COUNT(*) FROM "
+ + "AGGREGATE_TASK_EXECUTION where END_TIME IS NULL ";
+
+ private static final String LAST_TASK_EXECUTIONS_BY_TASK_NAMES = "select TE2.* from ("
+ + "select MAX(TE.TASK_EXECUTION_ID) as TASK_EXECUTION_ID, TE.TASK_NAME, TE.START_TIME from ("
+ + "select TASK_NAME, MAX(START_TIME) as START_TIME"
+ + " FROM AGGREGATE_TASK_EXECUTION where TASK_NAME in (:taskNames)"
+ + " GROUP BY TASK_NAME) TE_MAX"
+ + " inner join AGGREGATE_TASK_EXECUTION TE ON TE.TASK_NAME = TE_MAX.TASK_NAME AND TE.START_TIME = TE_MAX.START_TIME"
+ + " group by TE.TASK_NAME, TE.START_TIME" + ") TE1"
+ + " inner join AGGREGATE_TASK_EXECUTION TE2 ON TE1.TASK_EXECUTION_ID = TE2.TASK_EXECUTION_ID AND TE1.SCHEMA_TARGET = TE2.SCHEMA_TARGET"
+ + " order by TE2.START_TIME DESC, TE2.TASK_EXECUTION_ID DESC";
+
+ private static final String FIND_TASK_NAMES = "SELECT distinct TASK_NAME from AGGREGATE_TASK_EXECUTION order by TASK_NAME";
+
+ private static final Set validSortColumns = new HashSet<>(10);
+
+ static {
+ validSortColumns.add("TASK_EXECUTION_ID");
+ validSortColumns.add("START_TIME");
+ validSortColumns.add("END_TIME");
+ validSortColumns.add("TASK_NAME");
+ validSortColumns.add("EXIT_CODE");
+ validSortColumns.add("EXIT_MESSAGE");
+ validSortColumns.add("ERROR_MESSAGE");
+ validSortColumns.add("LAST_UPDATED");
+ validSortColumns.add("EXTERNAL_EXECUTION_ID");
+ validSortColumns.add("PARENT_EXECUTION_ID");
+ validSortColumns.add("SCHEMA_TARGET");
+ }
+
+ private final NamedParameterJdbcTemplate jdbcTemplate;
+
+ private final DataSource dataSource;
+
+ private final LinkedHashMap orderMap;
+
+ private final SchemaService schemaService;
+
+ /**
+ * Initializes the AggregateDataFlowJobExecutionDao.
+ *
+ * @param dataSource used by the dao to execute queries and update the tables.
+ * @param schemaService used the find schema target information
+ */
+ public AggregateDataFlowTaskExecutionQueryDao(DataSource dataSource, SchemaService schemaService) {
+ Assert.notNull(dataSource, "The dataSource must not be null.");
+ this.jdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
+ this.dataSource = dataSource;
+ this.schemaService = schemaService;
+ this.orderMap = new LinkedHashMap<>();
+ this.orderMap.put("START_TIME", Order.DESCENDING);
+ this.orderMap.put("TASK_EXECUTION_ID", Order.DESCENDING);
+ }
+
+ @Override
+ public AggregateTaskExecution geTaskExecutionByExecutionId(String externalExecutionId, String taskName) {
+ final SqlParameterSource queryParameters = new MapSqlParameterSource()
+ .addValue("externalExecutionId", externalExecutionId)
+ .addValue("taskName", taskName);
+
+ try {
+ return this.jdbcTemplate.queryForObject(
+ GET_EXECUTION_BY_EXTERNAL_EXECUTION_ID,
+ queryParameters,
+ new CompositeTaskExecutionRowMapper(true)
+ );
+ } catch (EmptyResultDataAccessException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public AggregateTaskExecution getTaskExecution(long executionId, String schemaTarget) {
+ final SqlParameterSource queryParameters = new MapSqlParameterSource()
+ .addValue("taskExecutionId", executionId, Types.BIGINT)
+ .addValue("schemaTarget", schemaTarget);
+
+ try {
+ return this.jdbcTemplate.queryForObject(
+ GET_EXECUTION_BY_ID,
+ queryParameters,
+ new CompositeTaskExecutionRowMapper(true)
+ );
+ } catch (EmptyResultDataAccessException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public List findChildTaskExecutions(long executionId, String schemaTarget) {
+ final SqlParameterSource queryParameters = new MapSqlParameterSource()
+ .addValue("taskExecutionId", executionId, Types.BIGINT)
+ .addValue("schemaTarget", "--spring.cloud.task.parent-schema-target=" + schemaTarget);
+
+ try {
+ return this.jdbcTemplate.query(
+ GET_CHILD_EXECUTION_BY_ID,
+ queryParameters,
+ new CompositeTaskExecutionRowMapper(true)
+ );
+ } catch (EmptyResultDataAccessException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public List findChildTaskExecutions(Collection parentIds, String schemaTarget) {
+ final SqlParameterSource queryParameters = new MapSqlParameterSource()
+ .addValue("taskExecutionIds", parentIds)
+ .addValue("schemaTarget", "--spring.cloud.task.parent-schema-target=" + schemaTarget);
+
+ try {
+ List result = this.jdbcTemplate.query(
+ GET_CHILD_EXECUTION_BY_IDS,
+ queryParameters,
+ new CompositeTaskExecutionRowMapper(false)
+ );
+ populateArguments(schemaTarget, result);
+ return result;
+ } catch (EmptyResultDataAccessException e) {
+ return null;
+ }
+ }
+
+ private void populateArguments(String schemaTarget, List result) {
+ List ids = result.stream().map(AggregateTaskExecution::getExecutionId).collect(Collectors.toList());
+ Map> paramMap = getTaskArgumentsForTasks(ids, schemaTarget);
+ result.forEach(aggregateTaskExecution -> {
+ List params = paramMap.get(aggregateTaskExecution.getExecutionId());
+ if(params != null) {
+ aggregateTaskExecution.setArguments(params);
+ }
+ });
+ }
+
+ @Override
+ public List findTaskExecutions(String taskName, boolean completed) {
+ List result;
+ if (StringUtils.hasLength(taskName)) {
+ final SqlParameterSource queryParameters = new MapSqlParameterSource()
+ .addValue("taskName", taskName);
+ String query = completed ? GET_EXECUTIONS_BY_NAME_COMPLETED : GET_EXECUTIONS_BY_NAME;
+ result = this.jdbcTemplate.query(query, queryParameters, new CompositeTaskExecutionRowMapper(false));
+ } else {
+ result = this.jdbcTemplate.query(completed ? GET_EXECUTIONS_COMPLETED : GET_EXECUTIONS, Collections.emptyMap(), new CompositeTaskExecutionRowMapper(false));
+ }
+ result.stream()
+ .collect(Collectors.groupingBy(AggregateTaskExecution::getSchemaTarget))
+ .forEach(this::populateArguments);
+ return result;
+ }
+
+ @Override
+ public List findTaskExecutionsBeforeEndTime(String taskName, @NonNull Date endTime) {
+ final SqlParameterSource queryParameters = new MapSqlParameterSource()
+ .addValue("taskName", taskName)
+ .addValue("endTime", endTime);
+ String query;
+ query = taskName.isEmpty() ? GET_EXECUTIONS_COMPLETED_BEFORE_END_TIME : GET_EXECUTION_BY_NAME_COMPLETED_BEFORE_END_TIME;
+ List result = this.jdbcTemplate.query(query, queryParameters, new CompositeTaskExecutionRowMapper(false));
+ result.stream()
+ .collect(Collectors.groupingBy(AggregateTaskExecution::getSchemaTarget))
+ .forEach(this::populateArguments);
+ return result;
+ }
+
+ @Override
+ public long getTaskExecutionCountByTaskName(String taskName) {
+ Long count;
+ if (StringUtils.hasText(taskName)) {
+ final SqlParameterSource queryParameters = new MapSqlParameterSource()
+ .addValue("taskName", taskName, Types.VARCHAR);
+
+ try {
+ count = this.jdbcTemplate.queryForObject(TASK_EXECUTION_COUNT_BY_NAME, queryParameters, Long.class);
+ } catch (EmptyResultDataAccessException e) {
+ count = 0L;
+ }
+ } else {
+ count = this.jdbcTemplate.queryForObject(TASK_EXECUTION_COUNT, Collections.emptyMap(), Long.class);
+ }
+ return count != null ? count : 0L;
+ }
+
+ @Override
+ public long getCompletedTaskExecutionCountByTaskName(String taskName) {
+ Long count;
+ if (StringUtils.hasText(taskName)) {
+ final SqlParameterSource queryParameters = new MapSqlParameterSource()
+ .addValue("taskName", taskName, Types.VARCHAR);
+
+ try {
+ count = this.jdbcTemplate.queryForObject(COMPLETED_TASK_EXECUTION_COUNT_BY_NAME, queryParameters, Long.class);
+ } catch (EmptyResultDataAccessException e) {
+ count = 0L;
+ }
+ } else {
+ count = this.jdbcTemplate.queryForObject(COMPLETED_TASK_EXECUTION_COUNT, Collections.emptyMap(), Long.class);
+ }
+ return count != null ? count : 0L;
+ }
+
+ @Override
+ public long getCompletedTaskExecutionCountByTaskNameAndBeforeDate(String taskName, @NonNull Date endTime) {
+ Long count;
+ if (StringUtils.hasText(taskName)) {
+ final SqlParameterSource queryParameters = new MapSqlParameterSource()
+ .addValue("taskName", taskName, Types.VARCHAR)
+ .addValue("endTime", endTime, Types.DATE);
+
+ try {
+ count = this.jdbcTemplate.queryForObject(COMPLETED_TASK_EXECUTION_COUNT_BY_NAME_AND_BEFORE_END_TIME, queryParameters, Long.class);
+ } catch (EmptyResultDataAccessException e) {
+ count = 0L;
+ }
+ } else {
+ final SqlParameterSource queryParameters = new MapSqlParameterSource()
+ .addValue("endTime", endTime, Types.DATE);
+ count = this.jdbcTemplate.queryForObject(COMPLETED_TASK_EXECUTION_COUNT_AND_BEFORE_END_TIME, queryParameters, Long.class);
+ }
+ return count != null ? count : 0L;
+ }
+
+ @Override
+ public long getRunningTaskExecutionCountByTaskName(String taskName) {
+ Long count;
+ if (StringUtils.hasText(taskName)) {
+ final SqlParameterSource queryParameters = new MapSqlParameterSource()
+ .addValue("taskName", taskName, Types.VARCHAR);
+
+ try {
+ logger.debug("getRunningTaskExecutionCountByTaskName:{}:sql={}", taskName, RUNNING_TASK_EXECUTION_COUNT_BY_NAME);
+ count = this.jdbcTemplate.queryForObject(RUNNING_TASK_EXECUTION_COUNT_BY_NAME, queryParameters, Long.class);
+ } catch (EmptyResultDataAccessException e) {
+ count = 0L;
+ }
+ } else {
+ logger.debug("getRunningTaskExecutionCountByTaskName:{}:sql={}", taskName, RUNNING_TASK_EXECUTION_COUNT);
+ count = this.jdbcTemplate.queryForObject(RUNNING_TASK_EXECUTION_COUNT, Collections.emptyMap(), Long.class);
+
+ }
+ return count != null ? count : 0L;
+ }
+
+ @Override
+ public long getRunningTaskExecutionCount() {
+ try {
+ final SqlParameterSource queryParameters = new MapSqlParameterSource();
+ Long result = this.jdbcTemplate.queryForObject(RUNNING_TASK_EXECUTION_COUNT, queryParameters, Long.class);
+ return result != null ? result : 0L;
+ } catch (EmptyResultDataAccessException e) {
+ return 0;
+ }
+ }
+
+ @Override
+ public List getLatestTaskExecutionsByTaskNames(String... taskNames) {
+ Assert.notEmpty(taskNames, "At least 1 task name must be provided.");
+ final List taskNamesAsList = new ArrayList<>();
+
+ for (String taskName : taskNames) {
+ if (StringUtils.hasText(taskName)) {
+ taskNamesAsList.add(taskName);
+ }
+ }
+
+ Assert.isTrue(taskNamesAsList.size() == taskNames.length, String.format(
+ "Task names must not contain any empty elements but %s of %s were empty or null.",
+ taskNames.length - taskNamesAsList.size(), taskNames.length));
+
+ try {
+ final Map> paramMap = Collections
+ .singletonMap("taskNames", taskNamesAsList);
+ List result = this.jdbcTemplate.query(LAST_TASK_EXECUTIONS_BY_TASK_NAMES, paramMap, new CompositeTaskExecutionRowMapper(false));
+ result.stream()
+ .collect(Collectors.groupingBy(AggregateTaskExecution::getSchemaTarget))
+ .forEach(this::populateArguments);
+ return result;
+ } catch (EmptyResultDataAccessException e) {
+ return Collections.emptyList();
+ }
+ }
+
+ @Override
+ public AggregateTaskExecution getLatestTaskExecutionForTaskName(String taskName) {
+ Assert.hasText(taskName, "The task name must not be empty.");
+ final List taskExecutions = this
+ .getLatestTaskExecutionsByTaskNames(taskName);
+ if (taskExecutions.isEmpty()) {
+ return null;
+ } else if (taskExecutions.size() == 1) {
+ return taskExecutions.get(0);
+ } else {
+ throw new IllegalStateException(
+ "Only expected a single TaskExecution but received "
+ + taskExecutions.size());
+ }
+ }
+
+ @Override
+ public long getTaskExecutionCount() {
+ try {
+ Long count = this.jdbcTemplate.queryForObject(TASK_EXECUTION_COUNT, new MapSqlParameterSource(), Long.class);
+ return count != null ? count : 0;
+ } catch (EmptyResultDataAccessException e) {
+ return 0;
+ }
+ }
+
+ @Override
+ public Page findRunningTaskExecutions(String taskName, Pageable pageable) {
+ return queryForPageableResults(pageable, SELECT_CLAUSE, FROM_CLAUSE,
+ RUNNING_TASK_WHERE_CLAUSE,
+ new MapSqlParameterSource("taskName", taskName),
+ getRunningTaskExecutionCountByTaskName(taskName), false);
+ }
+
+ @Override
+ public Page findTaskExecutionsByName(String taskName, Pageable pageable) {
+ return queryForPageableResults(pageable, SELECT_CLAUSE, FROM_CLAUSE,
+ TASK_NAME_WHERE_CLAUSE, new MapSqlParameterSource("taskName", taskName),
+ getTaskExecutionCountByTaskName(taskName), false);
+ }
+
+ @Override
+ public List getTaskNames() {
+ return this.jdbcTemplate.queryForList(FIND_TASK_NAMES,
+ new MapSqlParameterSource(), String.class);
+ }
+
+ @Override
+ public Page findAll(Pageable pageable) {
+ return queryForPageableResults(pageable, SELECT_CLAUSE, FROM_CLAUSE, null,
+ new MapSqlParameterSource(), getTaskExecutionCount(), false);
+ }
+
+ @Override
+ public Page findAll(Pageable pageable, boolean thinResults) {
+ return queryForPageableResults(pageable, SELECT_CLAUSE, FROM_CLAUSE, null,
+ new MapSqlParameterSource(), getTaskExecutionCount(), thinResults);
+ }
+
+ private Page queryForPageableResults(
+ Pageable pageable,
+ String selectClause,
+ String fromClause,
+ String whereClause,
+ MapSqlParameterSource queryParameters,
+ long totalCount,
+ boolean thinResults
+ ) {
+ SqlPagingQueryProviderFactoryBean factoryBean = new SqlPagingQueryProviderFactoryBean();
+ factoryBean.setSelectClause(selectClause);
+ factoryBean.setFromClause(fromClause);
+ if (StringUtils.hasText(whereClause)) {
+ factoryBean.setWhereClause(whereClause);
+ }
+ final Sort sort = pageable.getSort();
+ final LinkedHashMap sortOrderMap = new LinkedHashMap<>();
+
+ if (sort != null) {
+ for (Sort.Order sortOrder : sort) {
+ if (validSortColumns.contains(sortOrder.getProperty().toUpperCase())) {
+ sortOrderMap.put(sortOrder.getProperty(),
+ sortOrder.isAscending() ? Order.ASCENDING : Order.DESCENDING);
+ } else {
+ throw new IllegalArgumentException(
+ String.format("Invalid sort option selected: %s", sortOrder.getProperty()));
+ }
+ }
+ }
+
+ if (!CollectionUtils.isEmpty(sortOrderMap)) {
+ factoryBean.setSortKeys(sortOrderMap);
+ } else {
+ factoryBean.setSortKeys(this.orderMap);
+ }
+
+ factoryBean.setDataSource(this.dataSource);
+ PagingQueryProvider pagingQueryProvider;
+ try {
+ pagingQueryProvider = factoryBean.getObject();
+ pagingQueryProvider.init(this.dataSource);
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ String query = pagingQueryProvider.getPageQuery(pageable);
+ List resultList = this.jdbcTemplate.query(query,
+ queryParameters, new CompositeTaskExecutionRowMapper(!thinResults));
+ resultList.stream()
+ .collect(Collectors.groupingBy(AggregateTaskExecution::getSchemaTarget))
+ .forEach(this::populateArguments);
+ return new PageImpl<>(resultList, pageable, totalCount);
+ }
+
+
+ private class CompositeTaskExecutionRowMapper implements RowMapper {
+ final boolean mapRow;
+ private CompositeTaskExecutionRowMapper(boolean mapRow) {
+ this.mapRow = mapRow;
+ }
+
+ @Override
+ public AggregateTaskExecution mapRow(ResultSet rs, int rowNum) throws SQLException {
+ long id = rs.getLong("TASK_EXECUTION_ID");
+ Long parentExecutionId = rs.getLong("PARENT_EXECUTION_ID");
+ if (rs.wasNull()) {
+ parentExecutionId = null;
+ }
+ String schemaTarget = rs.getString("SCHEMA_TARGET");
+ if (schemaTarget != null && schemaService.getTarget(schemaTarget) == null) {
+ logger.warn("Cannot find schemaTarget:{}", schemaTarget);
+ }
+ return new AggregateTaskExecution(id,
+ getNullableExitCode(rs),
+ rs.getString("TASK_NAME"),
+ rs.getTimestamp("START_TIME"),
+ rs.getTimestamp("END_TIME"),
+ rs.getString("EXIT_MESSAGE"),
+ mapRow ? getTaskArguments(id, schemaTarget) : Collections.emptyList(),
+ rs.getString("ERROR_MESSAGE"),
+ rs.getString("EXTERNAL_EXECUTION_ID"),
+ parentExecutionId,
+ null,
+ null,
+ schemaTarget
+ );
+ }
+
+ private Integer getNullableExitCode(ResultSet rs) throws SQLException {
+ int exitCode = rs.getInt("EXIT_CODE");
+ return !rs.wasNull() ? exitCode : null;
+ }
+ }
+
+ private List getTaskArguments(long taskExecutionId, String schemaTarget) {
+ final List params = new ArrayList<>();
+ RowCallbackHandler handler = rs -> params.add(rs.getString(2));
+ MapSqlParameterSource parameterSource = new MapSqlParameterSource("taskExecutionId", taskExecutionId)
+ .addValue("schemaTarget", schemaTarget);
+ this.jdbcTemplate.query(
+ FIND_TASK_ARGUMENTS,
+ parameterSource,
+ handler);
+ return params;
+ }
+ private Map> getTaskArgumentsForTasks(Collection taskExecutionIds, String schemaTarget) {
+ if(taskExecutionIds.isEmpty()) {
+ return Collections.emptyMap();
+ } else {
+ final Map> result = new HashMap<>();
+ RowCallbackHandler handler = rs -> result.computeIfAbsent(rs.getLong(1), a -> new ArrayList<>())
+ .add(rs.getString(2));
+ MapSqlParameterSource parameterSource = new MapSqlParameterSource("taskExecutionIds", taskExecutionIds)
+ .addValue("schemaTarget", schemaTarget);
+ this.jdbcTemplate.query(FIND_TASKS_ARGUMENTS, parameterSource, handler);
+ return result;
+ }
+ }
+}
diff --git a/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/DefaultAggregateExecutionSupport.java b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/DefaultAggregateExecutionSupport.java
new file mode 100644
index 0000000000..342b09e4d4
--- /dev/null
+++ b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/DefaultAggregateExecutionSupport.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2023-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.cloud.dataflow.aggregate.task.impl;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.cloud.dataflow.aggregate.task.AggregateExecutionSupport;
+import org.springframework.cloud.dataflow.aggregate.task.TaskDefinitionReader;
+import org.springframework.cloud.dataflow.aggregate.task.TaskDeploymentReader;
+import org.springframework.cloud.dataflow.core.AppRegistration;
+import org.springframework.cloud.dataflow.core.ApplicationType;
+import org.springframework.cloud.dataflow.core.TaskDefinition;
+import org.springframework.cloud.dataflow.core.TaskDeployment;
+import org.springframework.cloud.dataflow.registry.service.AppRegistryService;
+import org.springframework.cloud.dataflow.schema.AggregateTaskExecution;
+import org.springframework.cloud.dataflow.schema.SchemaVersionTarget;
+import org.springframework.cloud.dataflow.schema.service.SchemaService;
+import org.springframework.cloud.task.repository.TaskExecution;
+import org.springframework.util.StringUtils;
+
+/**
+ * Provides support for access to SchemaVersionTarget information and conversion of execution data to composite executions.
+ *
+ * @author Corneil du Plessis
+ */
+
+public class DefaultAggregateExecutionSupport implements AggregateExecutionSupport {
+ private static final Logger logger = LoggerFactory.getLogger(AggregateExecutionSupport.class);
+
+ private final AppRegistryService registryService;
+
+ private final SchemaService schemaService;
+
+ public DefaultAggregateExecutionSupport(
+ AppRegistryService registryService,
+ SchemaService schemaService
+ ) {
+ this.registryService = registryService;
+ this.schemaService = schemaService;
+ }
+
+ @Override
+ public AggregateTaskExecution from(TaskExecution execution, TaskDefinitionReader taskDefinitionReader, TaskDeploymentReader taskDeploymentReader) {
+ TaskDefinition taskDefinition = taskDefinitionReader.findTaskDefinition(execution.getTaskName());
+ TaskDeployment deployment = null;
+ if (StringUtils.hasText(execution.getExternalExecutionId())) {
+ deployment = taskDeploymentReader.getDeployment(execution.getExternalExecutionId());
+ } else {
+ if(taskDefinition == null) {
+ logger.warn("TaskDefinition not found for " + execution.getTaskName());
+ } else {
+ deployment = taskDeploymentReader.findByDefinitionName(taskDefinition.getName());
+ }
+ }
+ SchemaVersionTarget versionTarget = findSchemaVersionTarget(execution.getTaskName(), taskDefinition);
+ return from(execution, versionTarget.getName(), deployment != null ? deployment.getPlatformName() : null);
+ }
+
+ @Override
+ public SchemaVersionTarget findSchemaVersionTarget(String taskName, TaskDefinitionReader taskDefinitionReader) {
+ logger.debug("findSchemaVersionTarget:{}", taskName);
+ TaskDefinition definition = taskDefinitionReader.findTaskDefinition(taskName);
+ return findSchemaVersionTarget(taskName, definition);
+ }
+
+ @Override
+ public SchemaVersionTarget findSchemaVersionTarget(String taskName, String version, TaskDefinitionReader taskDefinitionReader) {
+ logger.debug("findSchemaVersionTarget:{}:{}", taskName, version);
+ TaskDefinition definition = taskDefinitionReader.findTaskDefinition(taskName);
+ return findSchemaVersionTarget(taskName, version, definition);
+ }
+
+ @Override
+ public SchemaVersionTarget findSchemaVersionTarget(String taskName, TaskDefinition taskDefinition) {
+ return findSchemaVersionTarget(taskName, null, taskDefinition);
+ }
+
+ @Override
+ public SchemaVersionTarget findSchemaVersionTarget(String taskName, String version, TaskDefinition taskDefinition) {
+ logger.debug("findSchemaVersionTarget:{}:{}", taskName, version);
+ String registeredName = taskDefinition != null ? taskDefinition.getRegisteredAppName() : taskName;
+ AppRegistration registration = findTaskAppRegistration(registeredName, version);
+ if (registration == null) {
+ if(StringUtils.hasLength(version)) {
+ logger.warn("Cannot find AppRegistration for {}:{}", taskName, version);
+ } else {
+ logger.warn("Cannot find AppRegistration for {}", taskName);
+ }
+ return SchemaVersionTarget.defaultTarget();
+ }
+ final AppRegistration finalRegistration = registration;
+ List versionTargets = schemaService.getTargets().getSchemas()
+ .stream()
+ .filter(target -> target.getSchemaVersion().equals(finalRegistration.getBootVersion()))
+ .collect(Collectors.toList());
+ if (versionTargets.isEmpty()) {
+ logger.warn("Cannot find a SchemaVersionTarget for {}", registration.getBootVersion());
+ return SchemaVersionTarget.defaultTarget();
+ }
+ if (versionTargets.size() > 1) {
+ throw new IllegalStateException("Multiple SchemaVersionTargets for " + registration.getBootVersion());
+ }
+ SchemaVersionTarget schemaVersionTarget = versionTargets.get(0);
+ logger.debug("findSchemaVersionTarget:{}:{}:{}={}", taskName, registeredName, version, schemaVersionTarget);
+ return schemaVersionTarget;
+ }
+
+ @Override
+ public AppRegistration findTaskAppRegistration(String registeredName) {
+ return findTaskAppRegistration(registeredName, null);
+ }
+
+ @Override
+ public AppRegistration findTaskAppRegistration(String registeredAppName, String version) {
+ AppRegistration registration = StringUtils.hasLength(version) ?
+ registryService.find(registeredAppName, ApplicationType.task, version) :
+ registryService.find(registeredAppName, ApplicationType.task);
+ if (registration == null) {
+ registration = StringUtils.hasLength(version) ?
+ registryService.find(registeredAppName, ApplicationType.app, version) :
+ registryService.find(registeredAppName, ApplicationType.app);
+ }
+ logger.debug("findTaskAppRegistration:{}:{}={}", registeredAppName, version, registration);
+ return registration;
+ }
+
+ @Override
+ public AggregateTaskExecution from(TaskExecution execution, String schemaTarget, String platformName) {
+ if (execution != null) {
+ return new AggregateTaskExecution(
+ execution.getExecutionId(),
+ execution.getExitCode(),
+ execution.getTaskName(),
+ execution.getStartTime(),
+ execution.getEndTime(),
+ execution.getExitMessage(),
+ execution.getArguments(),
+ execution.getErrorMessage(),
+ execution.getExternalExecutionId(),
+ execution.getParentExecutionId(),
+ platformName,
+ null,
+ schemaTarget);
+ }
+ return null;
+ }
+}
diff --git a/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/DefaultAggregateTaskExplorer.java b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/DefaultAggregateTaskExplorer.java
new file mode 100644
index 0000000000..a933e1c498
--- /dev/null
+++ b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/DefaultAggregateTaskExplorer.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2023-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.cloud.dataflow.aggregate.task.impl;
+
+import javax.annotation.PostConstruct;
+import javax.sql.DataSource;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.cloud.dataflow.aggregate.task.AggregateExecutionSupport;
+import org.springframework.cloud.dataflow.aggregate.task.AggregateTaskExplorer;
+import org.springframework.cloud.dataflow.aggregate.task.DataflowTaskExecutionQueryDao;
+import org.springframework.cloud.dataflow.aggregate.task.TaskDefinitionReader;
+import org.springframework.cloud.dataflow.aggregate.task.TaskDeploymentReader;
+import org.springframework.cloud.dataflow.core.TaskDefinition;
+import org.springframework.cloud.dataflow.core.TaskDeployment;
+import org.springframework.cloud.dataflow.core.database.support.MultiSchemaTaskExecutionDaoFactoryBean;
+import org.springframework.cloud.dataflow.schema.AggregateTaskExecution;
+import org.springframework.cloud.dataflow.schema.SchemaVersionTarget;
+import org.springframework.cloud.dataflow.schema.service.SchemaService;
+import org.springframework.cloud.task.repository.TaskExecution;
+import org.springframework.cloud.task.repository.TaskExplorer;
+import org.springframework.cloud.task.repository.support.SimpleTaskExplorer;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Implements CompositeTaskExplorer. This class will be responsible for retrieving task execution data for all schema targets.
+ *
+ * @author Corneil du Plessis
+ */
+public class DefaultAggregateTaskExplorer implements AggregateTaskExplorer {
+ private final static Logger logger = LoggerFactory.getLogger(DefaultAggregateTaskExplorer.class);
+
+ private final Map taskExplorers;
+
+ private final AggregateExecutionSupport aggregateExecutionSupport;
+
+ private final DataflowTaskExecutionQueryDao taskExecutionQueryDao;
+
+ private final TaskDefinitionReader taskDefinitionReader;
+
+ private final TaskDeploymentReader taskDeploymentReader;
+
+ public DefaultAggregateTaskExplorer(
+ DataSource dataSource,
+ DataflowTaskExecutionQueryDao taskExecutionQueryDao,
+ SchemaService schemaService,
+ AggregateExecutionSupport aggregateExecutionSupport,
+ TaskDefinitionReader taskDefinitionReader,
+ TaskDeploymentReader taskDeploymentReader
+ ) {
+ this.taskExecutionQueryDao = taskExecutionQueryDao;
+ this.aggregateExecutionSupport = aggregateExecutionSupport;
+ this.taskDefinitionReader = taskDefinitionReader;
+ this.taskDeploymentReader = taskDeploymentReader;
+ Map result = new HashMap<>();
+ for (SchemaVersionTarget target : schemaService.getTargets().getSchemas()) {
+ TaskExplorer explorer = new SimpleTaskExplorer(new MultiSchemaTaskExecutionDaoFactoryBean(dataSource, target.getTaskPrefix()));
+ result.put(target.getName(), explorer);
+ }
+ taskExplorers = Collections.unmodifiableMap(result);
+ }
+
+ @Override
+ public AggregateTaskExecution getTaskExecution(long executionId, String schemaTarget) {
+ if (!StringUtils.hasText(schemaTarget)) {
+ schemaTarget = SchemaVersionTarget.defaultTarget().getName();
+ }
+ TaskExplorer taskExplorer = taskExplorers.get(schemaTarget);
+ Assert.notNull(taskExplorer, "Expected taskExplorer for " + schemaTarget);
+ TaskExecution taskExecution = taskExplorer.getTaskExecution(executionId);
+ TaskDeployment deployment = null;
+ if (taskExecution != null) {
+ if (StringUtils.hasText(taskExecution.getExternalExecutionId())) {
+ deployment = taskDeploymentReader.getDeployment(taskExecution.getExternalExecutionId());
+ } else {
+ TaskDefinition definition = taskDefinitionReader.findTaskDefinition(taskExecution.getTaskName());
+ if (definition == null) {
+ logger.warn("Cannot find definition for " + taskExecution.getTaskName());
+ } else {
+ deployment = taskDeploymentReader.findByDefinitionName(definition.getName());
+ }
+ }
+ }
+ return aggregateExecutionSupport.from(taskExecution, schemaTarget, deployment != null ? deployment.getPlatformName() : null);
+ }
+
+ @Override
+ public AggregateTaskExecution getTaskExecutionByExternalExecutionId(String externalExecutionId, String platform) {
+ TaskDeployment deployment = taskDeploymentReader.getDeployment(externalExecutionId, platform);
+ if (deployment != null) {
+ return this.taskExecutionQueryDao.geTaskExecutionByExecutionId(externalExecutionId, deployment.getTaskDefinitionName());
+ }
+ return null;
+ }
+
+ @Override
+ public List findChildTaskExecutions(long executionId, String schemaTarget) {
+ return this.taskExecutionQueryDao.findChildTaskExecutions(executionId, schemaTarget);
+ }
+
+ @Override
+ public List findChildTaskExecutions(Collection parentIds, String schemaTarget) {
+ return this.taskExecutionQueryDao.findChildTaskExecutions(parentIds, schemaTarget);
+ }
+
+ @Override
+ public Page findRunningTaskExecutions(String taskName, Pageable pageable) {
+ SchemaVersionTarget target = aggregateExecutionSupport.findSchemaVersionTarget(taskName, taskDefinitionReader);
+ Assert.notNull(target, "Expected to find SchemaVersionTarget for " + taskName);
+ TaskExplorer taskExplorer = taskExplorers.get(target.getName());
+ Assert.notNull(taskExplorer, "Expected TaskExplorer for " + target.getName());
+ TaskDefinition definition = taskDefinitionReader.findTaskDefinition(taskName);
+ if (definition == null) {
+ logger.warn("Cannot find TaskDefinition for " + taskName);
+ }
+ TaskDeployment deployment = definition != null ? taskDeploymentReader.findByDefinitionName(definition.getName()) : null;
+ final String platformName = deployment != null ? deployment.getPlatformName() : null;
+ Page executions = taskExplorer.findRunningTaskExecutions(taskName, pageable);
+ List taskExecutions = executions.getContent()
+ .stream()
+ .map(execution -> aggregateExecutionSupport.from(execution, target.getName(), platformName))
+ .collect(Collectors.toList());
+ return new PageImpl<>(taskExecutions, executions.getPageable(), executions.getTotalElements());
+ }
+
+ @Override
+ public List getTaskNames() {
+ List result = new ArrayList<>();
+ for (TaskExplorer explorer : taskExplorers.values()) {
+ result.addAll(explorer.getTaskNames());
+ }
+ return result;
+ }
+
+ @Override
+ public long getTaskExecutionCountByTaskName(String taskName) {
+ long result = 0;
+ for (TaskExplorer explorer : taskExplorers.values()) {
+ result += explorer.getTaskExecutionCountByTaskName(taskName);
+ }
+ return result;
+ }
+
+ @Override
+ public long getTaskExecutionCount() {
+ long result = 0;
+ for (TaskExplorer explorer : taskExplorers.values()) {
+ result += explorer.getTaskExecutionCount();
+ }
+ return result;
+ }
+
+ @Override
+ public long getRunningTaskExecutionCount() {
+ long result = 0;
+ for (TaskExplorer explorer : taskExplorers.values()) {
+ result += explorer.getRunningTaskExecutionCount();
+ }
+ return result;
+ }
+
+ @Override
+ public List findTaskExecutions(String taskName, boolean completed) {
+ return this.taskExecutionQueryDao.findTaskExecutions(taskName, completed);
+ }
+
+ @Override
+ public List findTaskExecutionsBeforeEndTime(String taskName, Date endTime) {
+ return this.taskExecutionQueryDao.findTaskExecutionsBeforeEndTime(taskName, endTime);
+ }
+
+ @Override
+ public Page findTaskExecutionsByName(String taskName, Pageable pageable) {
+
+ String platformName = getPlatformName(taskName);
+ SchemaVersionTarget target = aggregateExecutionSupport.findSchemaVersionTarget(taskName, taskDefinitionReader);
+ Assert.notNull(target, "Expected to find SchemaVersionTarget for " + taskName);
+ TaskExplorer taskExplorer = taskExplorers.get(target.getName());
+ Assert.notNull(taskExplorer, "Expected TaskExplorer for " + target.getName());
+ Page executions = taskExplorer.findTaskExecutionsByName(taskName, pageable);
+ List taskExecutions = executions.getContent()
+ .stream()
+ .map(execution -> aggregateExecutionSupport.from(execution, target.getName(), platformName))
+ .collect(Collectors.toList());
+ return new PageImpl<>(taskExecutions, executions.getPageable(), executions.getTotalElements());
+ }
+
+ private String getPlatformName(String taskName) {
+ String platformName = null;
+ TaskDefinition taskDefinition = taskDefinitionReader.findTaskDefinition(taskName);
+ if (taskDefinition != null) {
+ TaskDeployment taskDeployment = taskDeploymentReader.findByDefinitionName(taskDefinition.getName());
+ platformName = taskDeployment != null ? taskDeployment.getPlatformName() : null;
+ } else {
+ logger.warn("TaskDefinition not found for " + taskName);
+ }
+ return platformName;
+ }
+
+ @Override
+ public Page findAll(Pageable pageable) {
+ return taskExecutionQueryDao.findAll(pageable);
+ }
+
+ @Override
+ public Page findAll(Pageable pageable, boolean thinResults) {
+ return taskExecutionQueryDao.findAll(pageable, thinResults);
+ }
+
+ @Override
+ public Long getTaskExecutionIdByJobExecutionId(long jobExecutionId, String schemaTarget) {
+ if (!StringUtils.hasText(schemaTarget)) {
+ schemaTarget = SchemaVersionTarget.defaultTarget().getName();
+ }
+ TaskExplorer taskExplorer = taskExplorers.get(schemaTarget);
+ Assert.notNull(taskExplorer, "Expected TaskExplorer for " + schemaTarget);
+ return taskExplorer.getTaskExecutionIdByJobExecutionId(jobExecutionId);
+ }
+
+ @Override
+ public Set getJobExecutionIdsByTaskExecutionId(long taskExecutionId, String schemaTarget) {
+ if (!StringUtils.hasText(schemaTarget)) {
+ schemaTarget = SchemaVersionTarget.defaultTarget().getName();
+ }
+ TaskExplorer taskExplorer = taskExplorers.get(schemaTarget);
+ Assert.notNull(taskExplorer, "Expected TaskExplorer for " + schemaTarget);
+ return taskExplorer.getJobExecutionIdsByTaskExecutionId(taskExecutionId);
+ }
+ private static void add(Map> setMap, String key, String value) {
+ Set set = setMap.computeIfAbsent(key, (v) -> new HashSet<>());
+ set.add(value);
+ }
+ @Override
+ public List getLatestTaskExecutionsByTaskNames(String... taskNames) {
+ List result = new ArrayList<>();
+ Map> targetToTaskNames = new HashMap<>();
+ Map taskNamePlatform = new HashMap<>();
+ for (String taskName : taskNames) {
+ SchemaVersionTarget target = aggregateExecutionSupport.findSchemaVersionTarget(taskName, taskDefinitionReader);
+ String platformName = getPlatformName(taskName);
+ Assert.notNull(target, "Expected to find SchemaVersionTarget for " + taskName);
+ add(targetToTaskNames, target.getName(), taskName);
+ if(platformName != null) {
+ taskNamePlatform.put(taskName, platformName);
+ }
+ }
+ for(String target : targetToTaskNames.keySet()) {
+ Set tasks = targetToTaskNames.get(target);
+ if(!tasks.isEmpty()) {
+ TaskExplorer taskExplorer = taskExplorers.get(target);
+ Assert.notNull(taskExplorer, "Expected TaskExplorer for " + target);
+ List taskExecutions = taskExplorer
+ .getLatestTaskExecutionsByTaskNames(tasks.toArray(new String[0]))
+ .stream()
+ .map(execution -> aggregateExecutionSupport.from(execution, target, taskNamePlatform.get(execution.getTaskName())))
+ .collect(Collectors.toList());
+ result.addAll(taskExecutions);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public AggregateTaskExecution getLatestTaskExecutionForTaskName(String taskName) {
+
+ SchemaVersionTarget target = aggregateExecutionSupport.findSchemaVersionTarget(taskName, taskDefinitionReader);
+ Assert.notNull(target, "Expected to find SchemaVersionTarget for " + taskName);
+ TaskExplorer taskExplorer = taskExplorers.get(target.getName());
+ Assert.notNull(taskExplorer, "Expected TaskExplorer for " + target.getName());
+ return aggregateExecutionSupport.from(taskExplorer.getLatestTaskExecutionForTaskName(taskName), target.getName(), getPlatformName(taskName));
+ }
+
+ @PostConstruct
+ public void setup() {
+ logger.info("created: org.springframework.cloud.dataflow.aggregate.task.impl.DefaultAggregateTaskExplorer");
+ }
+}
diff --git a/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/DefaultTaskRepositoryContainer.java b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/DefaultTaskRepositoryContainer.java
new file mode 100644
index 0000000000..3db52d91cc
--- /dev/null
+++ b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/DefaultTaskRepositoryContainer.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 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.cloud.dataflow.aggregate.task.impl;
+
+import javax.annotation.PostConstruct;
+import javax.sql.DataSource;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.cloud.dataflow.aggregate.task.TaskRepositoryContainer;
+import org.springframework.cloud.dataflow.core.database.support.MultiSchemaTaskExecutionDaoFactoryBean;
+import org.springframework.cloud.dataflow.schema.SchemaVersionTarget;
+import org.springframework.cloud.dataflow.schema.service.SchemaService;
+import org.springframework.cloud.task.repository.TaskRepository;
+import org.springframework.cloud.task.repository.support.SimpleTaskRepository;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * This class manages a collection of TaskRepositories for all schemas.
+ * In the future there will be a datasource container for all names datasources.
+ *
+ * @author Corneil du Plessis
+ */
+public class DefaultTaskRepositoryContainer implements TaskRepositoryContainer {
+ private final static Logger logger = LoggerFactory.getLogger(DefaultTaskRepositoryContainer.class);
+
+ private final Map taskRepositories = new HashMap<>();
+
+ public DefaultTaskRepositoryContainer(DataSource dataSource, SchemaService schemaService) {
+ for (SchemaVersionTarget target : schemaService.getTargets().getSchemas()) {
+ MultiSchemaTaskExecutionDaoFactoryBean taskExecutionDaoFactoryBean = new MultiSchemaTaskExecutionDaoFactoryBean(dataSource, target.getTaskPrefix());
+ add(target.getName(), new SimpleTaskRepository(taskExecutionDaoFactoryBean));
+ }
+ }
+
+ private void add(String schemaTarget, TaskRepository taskRepository) {
+ taskRepositories.put(schemaTarget, taskRepository);
+ }
+
+ @Override
+ public TaskRepository get(String schemaTarget) {
+ if(!StringUtils.hasText(schemaTarget)) {
+ schemaTarget = SchemaVersionTarget.defaultTarget().getName();
+ }
+ TaskRepository repository = taskRepositories.get(schemaTarget);
+ Assert.notNull(repository, "Expected TaskRepository for " + schemaTarget);
+ return repository;
+ }
+
+ @PostConstruct
+ public void setup() {
+ logger.info("created: org.springframework.cloud.dataflow.aggregate.task.impl.DefaultTaskRepositoryContainer");
+ }
+}
diff --git a/spring-cloud-dataflow-aggregate-task/src/test/resources/logback-test.xml b/spring-cloud-dataflow-aggregate-task/src/test/resources/logback-test.xml
new file mode 100644
index 0000000000..fe13492971
--- /dev/null
+++ b/spring-cloud-dataflow-aggregate-task/src/test/resources/logback-test.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/spring-cloud-dataflow-audit/pom.xml b/spring-cloud-dataflow-audit/pom.xml
index 6e47601a1f..8bcb261942 100644
--- a/spring-cloud-dataflow-audit/pom.xml
+++ b/spring-cloud-dataflow-audit/pom.xml
@@ -1,21 +1,32 @@
-
+4.0.0spring-cloud-dataflow-parentorg.springframework.cloud
- 2.9.2-SNAPSHOT
+ 2.11.6-SNAPSHOT
+ ../spring-cloud-dataflow-parentspring-cloud-dataflow-audit
+ spring-cloud-dataflow-audit
+ Spring Cloud Data Flow Audit
+
jar
+
+ true
+ 3.4.1
+ org.springframework.cloudspring-cloud-dataflow-core
+ ${project.version}org.springframework.cloudspring-cloud-dataflow-rest-resource
+ ${project.version}org.springframework.boot
@@ -23,4 +34,45 @@
test
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 1.8
+ 1.8
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ ${maven-javadoc-plugin.version}
+
+
+ javadoc
+
+ jar
+
+ package
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.0
+
+
+ source
+
+ jar
+
+ package
+
+
+
+
+
diff --git a/spring-cloud-dataflow-audit/src/main/java/org/springframework/cloud/dataflow/audit/service/DefaultAuditRecordService.java b/spring-cloud-dataflow-audit/src/main/java/org/springframework/cloud/dataflow/audit/service/DefaultAuditRecordService.java
index 7eff50695e..84eccdc2df 100644
--- a/spring-cloud-dataflow-audit/src/main/java/org/springframework/cloud/dataflow/audit/service/DefaultAuditRecordService.java
+++ b/spring-cloud-dataflow-audit/src/main/java/org/springframework/cloud/dataflow/audit/service/DefaultAuditRecordService.java
@@ -16,6 +16,7 @@
package org.springframework.cloud.dataflow.audit.service;
import java.time.Instant;
+import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@@ -26,6 +27,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.cloud.dataflow.audit.repository.AuditRecordRepository;
+import org.springframework.cloud.dataflow.core.ArgumentSanitizer;
import org.springframework.cloud.dataflow.core.AuditActionType;
import org.springframework.cloud.dataflow.core.AuditOperationType;
import org.springframework.cloud.dataflow.core.AuditRecord;
@@ -38,74 +40,107 @@
*
* @author Gunnar Hillert
* @author Daniel Serleg
+ * @author Corneil du Plessis
*/
public class DefaultAuditRecordService implements AuditRecordService {
- private static final Logger logger = LoggerFactory.getLogger(DefaultAuditRecordService.class);
-
- private final AuditRecordRepository auditRecordRepository;
-
- private final ObjectMapper objectMapper;
-
- public DefaultAuditRecordService(AuditRecordRepository auditRecordRepository) {
- Assert.notNull(auditRecordRepository, "auditRecordRepository must not be null.");
- this.auditRecordRepository = auditRecordRepository;
- this.objectMapper = new ObjectMapper();
- this.objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
- }
-
- public DefaultAuditRecordService(AuditRecordRepository auditRecordRepository, ObjectMapper objectMapper) {
- Assert.notNull(auditRecordRepository, "auditRecordRepository must not be null.");
- Assert.notNull(objectMapper, "objectMapper must not be null.");
- this.auditRecordRepository = auditRecordRepository;
- this.objectMapper = objectMapper;
- }
-
- @Override
- public AuditRecord populateAndSaveAuditRecord(AuditOperationType auditOperationType,
- AuditActionType auditActionType,
- String correlationId, String data, String platformName) {
- Assert.notNull(auditActionType, "auditActionType must not be null.");
- Assert.notNull(auditOperationType, "auditOperationType must not be null.");
-
- final AuditRecord auditRecord = new AuditRecord();
- auditRecord.setAuditAction(auditActionType);
- auditRecord.setAuditOperation(auditOperationType);
- auditRecord.setCorrelationId(correlationId);
- auditRecord.setAuditData(data);
- auditRecord.setPlatformName(platformName);
- return this.auditRecordRepository.save(auditRecord);
- }
-
- @Override
- public AuditRecord populateAndSaveAuditRecordUsingMapData(AuditOperationType auditOperationType,
- AuditActionType auditActionType,
- String correlationId, Map data, String platformName) {
- String dataAsString;
- try {
- dataAsString = objectMapper.writeValueAsString(data);
- }
- catch (JsonProcessingException e) {
- logger.error("Error serializing audit record data. Data = " + data);
- dataAsString = "Error serializing audit record data. Data = " + data;
- }
- return this.populateAndSaveAuditRecord(auditOperationType, auditActionType, correlationId, dataAsString, platformName);
- }
-
- @Override
- public Page findAuditRecordByAuditOperationTypeAndAuditActionTypeAndDate(
- Pageable pageable,
- AuditActionType[] actions,
- AuditOperationType[] operations,
- Instant fromDate,
- Instant toDate) {
- return this.auditRecordRepository.findByActionTypeAndOperationTypeAndDate(operations, actions, fromDate, toDate,
- pageable);
- }
-
- @Override
- public Optional findById(Long id) {
- return this.auditRecordRepository.findById(id);
- }
+ private static final Logger logger = LoggerFactory.getLogger(DefaultAuditRecordService.class);
+
+ private final AuditRecordRepository auditRecordRepository;
+
+ private final ObjectMapper objectMapper;
+
+ private final ArgumentSanitizer sanitizer;
+
+ public DefaultAuditRecordService(AuditRecordRepository auditRecordRepository) {
+
+ this(auditRecordRepository, null);
+ }
+
+ public DefaultAuditRecordService(AuditRecordRepository auditRecordRepository, ObjectMapper objectMapper) {
+
+ Assert.notNull(auditRecordRepository, "auditRecordRepository must not be null.");
+ this.auditRecordRepository = auditRecordRepository;
+ if (objectMapper == null) {
+ objectMapper = new ObjectMapper();
+ objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+ }
+ this.objectMapper = objectMapper;
+ this.sanitizer = new ArgumentSanitizer();
+ }
+
+ @Override
+ public AuditRecord populateAndSaveAuditRecord(AuditOperationType auditOperationType,
+ AuditActionType auditActionType,
+ String correlationId, String data, String platformName) {
+
+ Assert.notNull(auditActionType, "auditActionType must not be null.");
+ Assert.notNull(auditOperationType, "auditOperationType must not be null.");
+
+ final AuditRecord auditRecord = new AuditRecord();
+ auditRecord.setAuditAction(auditActionType);
+ auditRecord.setAuditOperation(auditOperationType);
+ auditRecord.setCorrelationId(correlationId);
+ auditRecord.setAuditData(data);
+ auditRecord.setPlatformName(platformName);
+ return this.auditRecordRepository.save(auditRecord);
+ }
+
+ @Override
+ public AuditRecord populateAndSaveAuditRecordUsingMapData(
+ AuditOperationType auditOperationType,
+ AuditActionType auditActionType,
+ String correlationId, Map data,
+ String platformName
+ ) {
+
+ String dataAsString;
+ try {
+ Map sanitizedData = sanitizeMap(data);
+ dataAsString = objectMapper.writeValueAsString(sanitizedData);
+ } catch (JsonProcessingException e) {
+ logger.error("Error serializing audit record data. Data = " + data);
+ dataAsString = "Error serializing audit record data. Data = " + data;
+ }
+ return this.populateAndSaveAuditRecord(auditOperationType, auditActionType, correlationId, dataAsString, platformName);
+ }
+
+ private Map sanitizeMap(Map data) {
+
+ final Map result = new HashMap<>();
+ data.forEach((k, v) -> result.put(k, sanitize(k, v)));
+ return result;
+ }
+
+ private Object sanitize(String key, Object value) {
+
+ if (value instanceof String) {
+ return sanitizer.sanitize(key, (String) value);
+ } else if (value instanceof Map) {
+ Map input = (Map) value;
+ return sanitizeMap(input);
+ } else {
+ return value;
+ }
+ }
+
+
+ @Override
+ public Page findAuditRecordByAuditOperationTypeAndAuditActionTypeAndDate(
+ Pageable pageable,
+ AuditActionType[] actions,
+ AuditOperationType[] operations,
+ Instant fromDate,
+ Instant toDate) {
+
+ return this.auditRecordRepository.findByActionTypeAndOperationTypeAndDate(operations, actions, fromDate, toDate,
+ pageable);
+ }
+
+ @Override
+ public Optional findById(Long id) {
+
+ return this.auditRecordRepository.findById(id);
+ }
}
diff --git a/spring-cloud-dataflow-audit/src/test/java/org/springframework/cloud/dataflow/server/audit/service/DefaultAuditRecordServiceTests.java b/spring-cloud-dataflow-audit/src/test/java/org/springframework/cloud/dataflow/server/audit/service/DefaultAuditRecordServiceTests.java
index 5b12a0bf03..8029ee0135 100644
--- a/spring-cloud-dataflow-audit/src/test/java/org/springframework/cloud/dataflow/server/audit/service/DefaultAuditRecordServiceTests.java
+++ b/spring-cloud-dataflow-audit/src/test/java/org/springframework/cloud/dataflow/server/audit/service/DefaultAuditRecordServiceTests.java
@@ -15,13 +15,16 @@
*/
package org.springframework.cloud.dataflow.server.audit.service;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.cloud.dataflow.audit.repository.AuditRecordRepository;
@@ -32,8 +35,10 @@
import org.springframework.cloud.dataflow.core.AuditRecord;
import org.springframework.data.domain.PageRequest;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
@@ -45,218 +50,255 @@
/**
* @author Gunnar Hillert
+ * @author Corneil du Plessis
*/
public class DefaultAuditRecordServiceTests {
- private AuditRecordRepository auditRecordRepository;
-
- @Before
- public void setupMock() {
- this.auditRecordRepository = mock(AuditRecordRepository.class);
- }
-
- @Test
- public void testInitializationWithNullParameters() {
- try {
- new DefaultAuditRecordService(null);
- }
- catch (IllegalArgumentException e) {
- assertEquals("auditRecordRepository must not be null.", e.getMessage());
- return;
- }
- fail("Expected an Exception to be thrown.");
- }
-
- @Test
- public void testPopulateAndSaveAuditRecord() {
- final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository);
- auditRecordService.populateAndSaveAuditRecord(AuditOperationType.SCHEDULE, AuditActionType.CREATE, "1234",
- "my data", "test-platform");
-
- final ArgumentCaptor argument = ArgumentCaptor.forClass(AuditRecord.class);
- verify(this.auditRecordRepository, times(1)).save(argument.capture());
- verifyNoMoreInteractions(this.auditRecordRepository);
-
- AuditRecord auditRecord = argument.getValue();
-
- assertEquals(AuditActionType.CREATE, auditRecord.getAuditAction());
- assertEquals(AuditOperationType.SCHEDULE, auditRecord.getAuditOperation());
- assertEquals("1234", auditRecord.getCorrelationId());
- assertEquals("my data", auditRecord.getAuditData());
- assertEquals("test-platform", auditRecord.getPlatformName());
- }
-
- @Test
- public void testPopulateAndSaveAuditRecordWithNullAuditActionType() {
- final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository);
-
- try {
- auditRecordService.populateAndSaveAuditRecord(AuditOperationType.SCHEDULE, null, "1234", "my audit data", "test-platform");
- }
- catch (IllegalArgumentException e) {
- assertEquals("auditActionType must not be null.", e.getMessage());
- return;
- }
- fail("Expected an Exception to be thrown.");
- }
-
- @Test
- public void testPopulateAndSaveAuditRecordWithNullAuditOperationType() {
- final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository);
-
- try {
- auditRecordService.populateAndSaveAuditRecord(null, AuditActionType.CREATE, "1234", "my audit data", "test-platform");
- }
- catch (IllegalArgumentException e) {
- assertEquals("auditOperationType must not be null.", e.getMessage());
- return;
- }
- fail("Expected an Exception to be thrown.");
- }
-
- @Test
- public void testPopulateAndSaveAuditRecordWithMapData() {
- final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository);
-
- final Map mapAuditData = new HashMap<>(2);
- mapAuditData.put("foo1", "bar1");
- mapAuditData.put("foofoo", "barbar");
-
- auditRecordService.populateAndSaveAuditRecordUsingMapData(AuditOperationType.SCHEDULE, AuditActionType.CREATE,
- "1234", mapAuditData, "test-platform");
-
- final ArgumentCaptor argument = ArgumentCaptor.forClass(AuditRecord.class);
- verify(this.auditRecordRepository, times(1)).save(argument.capture());
- verifyNoMoreInteractions(this.auditRecordRepository);
-
- final AuditRecord auditRecord = argument.getValue();
-
- assertEquals(AuditActionType.CREATE, auditRecord.getAuditAction());
- assertEquals(AuditOperationType.SCHEDULE, auditRecord.getAuditOperation());
- assertEquals("1234", auditRecord.getCorrelationId());
- assertEquals("{\"foofoo\":\"barbar\",\"foo1\":\"bar1\"}", auditRecord.getAuditData());
- assertEquals("test-platform", auditRecord.getPlatformName());
- }
-
- @Test
- public void testPopulateAndSaveAuditRecordUsingMapDataWithNullAuditActionType() {
- final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository);
-
- final Map mapAuditData = new HashMap<>(2);
- mapAuditData.put("foo", "bar");
-
- try {
- auditRecordService.populateAndSaveAuditRecordUsingMapData(AuditOperationType.SCHEDULE, null, "1234",
- mapAuditData, null);
- }
- catch (IllegalArgumentException e) {
- assertEquals("auditActionType must not be null.", e.getMessage());
- return;
- }
- fail("Expected an Exception to be thrown.");
- }
-
- @Test
- public void testPopulateAndSaveAuditRecordUsingMapDataWithNullAuditOperationType() {
- final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository);
-
- final Map mapAuditData = new HashMap<>(2);
- mapAuditData.put("foo", "bar");
-
- try {
- auditRecordService.populateAndSaveAuditRecordUsingMapData(null, AuditActionType.CREATE, "1234",
- mapAuditData, null);
- }
- catch (IllegalArgumentException e) {
- assertEquals("auditOperationType must not be null.", e.getMessage());
- return;
- }
- fail("Expected an Exception to be thrown.");
- }
-
- @Test
- public void testPopulateAndSaveAuditRecordUsingMapDataThrowingJsonProcessingException()
- throws JsonProcessingException {
- final ObjectMapper objectMapper = mock(ObjectMapper.class);
- when(objectMapper.writeValueAsString(any(Object.class))).thenThrow(new JsonProcessingException("Error") {
- private static final long serialVersionUID = 1L;
- });
-
- final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository,
- objectMapper);
-
- final Map mapAuditData = new HashMap<>(2);
- mapAuditData.put("foo", "bar");
-
- auditRecordService.populateAndSaveAuditRecordUsingMapData(AuditOperationType.SCHEDULE, AuditActionType.CREATE,
- "1234", mapAuditData, "test-platform");
-
- final ArgumentCaptor argument = ArgumentCaptor.forClass(AuditRecord.class);
- verify(this.auditRecordRepository, times(1)).save(argument.capture());
- verifyNoMoreInteractions(this.auditRecordRepository);
-
- AuditRecord auditRecord = argument.getValue();
-
- assertEquals(AuditActionType.CREATE, auditRecord.getAuditAction());
- assertEquals(AuditOperationType.SCHEDULE, auditRecord.getAuditOperation());
- assertEquals("1234", auditRecord.getCorrelationId());
- assertEquals("Error serializing audit record data. Data = {foo=bar}", auditRecord.getAuditData());
- assertEquals("test-platform", auditRecord.getPlatformName());
- }
-
- @Test
- public void testFindAuditRecordByAuditOperationTypeAndAuditActionType() {
- AuditRecordService auditRecordService = new DefaultAuditRecordService(auditRecordRepository);
-
- AuditActionType[] auditActionTypes = { AuditActionType.CREATE };
- AuditOperationType[] auditOperationTypes = { AuditOperationType.STREAM };
- PageRequest pageRequest = PageRequest.of(0, 1);
- auditRecordService.findAuditRecordByAuditOperationTypeAndAuditActionTypeAndDate(pageRequest, auditActionTypes,
- auditOperationTypes, null, null);
-
- verify(this.auditRecordRepository, times(1)).findByActionTypeAndOperationTypeAndDate(eq(auditOperationTypes),
- eq(auditActionTypes), isNull(), isNull(), eq(pageRequest));
- verifyNoMoreInteractions(this.auditRecordRepository);
- }
-
- @Test
- public void testFindAuditRecordByAuditOperationTypeAndAuditActionTypeWithNullAuditActionType() {
- AuditRecordService auditRecordService = new DefaultAuditRecordService(auditRecordRepository);
-
- AuditOperationType[] auditOperationTypes = { AuditOperationType.STREAM };
- PageRequest pageRequest = PageRequest.of(0, 1);
- auditRecordService.findAuditRecordByAuditOperationTypeAndAuditActionTypeAndDate(pageRequest, null,
- auditOperationTypes, null, null);
-
- verify(this.auditRecordRepository, times(1)).findByActionTypeAndOperationTypeAndDate(eq(auditOperationTypes),
- isNull(), isNull(), isNull(), eq(pageRequest));
- verifyNoMoreInteractions(this.auditRecordRepository);
- }
-
- @Test
- public void testFindAuditRecordByAuditOperationTypeAndAuditActionTypeWithNullOperationType() {
- AuditRecordService auditRecordService = new DefaultAuditRecordService(auditRecordRepository);
-
- AuditActionType[] auditActionTypes = { AuditActionType.CREATE };
- PageRequest pageRequest = PageRequest.of(0, 1);
- auditRecordService.findAuditRecordByAuditOperationTypeAndAuditActionTypeAndDate(pageRequest, auditActionTypes,
- null, null, null);
-
- verify(this.auditRecordRepository, times(1)).findByActionTypeAndOperationTypeAndDate(isNull(),
- eq(auditActionTypes), isNull(), isNull(), eq(pageRequest));
- verifyNoMoreInteractions(this.auditRecordRepository);
- }
-
- @Test
- public void testFindAuditRecordByAuditOperationTypeAndAuditActionTypeWithNullActionAndOperationType() {
- AuditRecordService auditRecordService = new DefaultAuditRecordService(auditRecordRepository);
-
- PageRequest pageRequest = PageRequest.of(0, 1);
- auditRecordService.findAuditRecordByAuditOperationTypeAndAuditActionTypeAndDate(pageRequest, null, null, null,
- null);
-
- verify(this.auditRecordRepository, times(1)).findByActionTypeAndOperationTypeAndDate(isNull(), isNull(),
- isNull(), isNull(), eq(pageRequest));
- verifyNoMoreInteractions(this.auditRecordRepository);
- }
+ private AuditRecordRepository auditRecordRepository;
+
+ @BeforeEach
+ public void setupMock() {
+ this.auditRecordRepository = mock(AuditRecordRepository.class);
+ }
+
+ @Test
+ public void testInitializationWithNullParameters() {
+ try {
+ new DefaultAuditRecordService(null);
+ } catch (IllegalArgumentException e) {
+ assertEquals("auditRecordRepository must not be null.", e.getMessage());
+ return;
+ }
+ fail("Expected an Exception to be thrown.");
+ }
+
+ @Test
+ public void testPopulateAndSaveAuditRecord() {
+ final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository);
+ auditRecordService.populateAndSaveAuditRecord(AuditOperationType.SCHEDULE, AuditActionType.CREATE, "1234",
+ "my data", "test-platform");
+
+ final ArgumentCaptor argument = ArgumentCaptor.forClass(AuditRecord.class);
+ verify(this.auditRecordRepository, times(1)).save(argument.capture());
+ verifyNoMoreInteractions(this.auditRecordRepository);
+
+ AuditRecord auditRecord = argument.getValue();
+
+ assertEquals(AuditActionType.CREATE, auditRecord.getAuditAction());
+ assertEquals(AuditOperationType.SCHEDULE, auditRecord.getAuditOperation());
+ assertEquals("1234", auditRecord.getCorrelationId());
+ assertEquals("my data", auditRecord.getAuditData());
+ assertEquals("test-platform", auditRecord.getPlatformName());
+ }
+
+ @Test
+ public void testPopulateAndSaveAuditRecordWithNullAuditActionType() {
+ final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository);
+
+ try {
+ auditRecordService.populateAndSaveAuditRecord(AuditOperationType.SCHEDULE, null, "1234", "my audit data", "test-platform");
+ } catch (IllegalArgumentException e) {
+ assertEquals("auditActionType must not be null.", e.getMessage());
+ return;
+ }
+ fail("Expected an Exception to be thrown.");
+ }
+
+ @Test
+ public void testPopulateAndSaveAuditRecordWithNullAuditOperationType() {
+ final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository);
+
+ try {
+ auditRecordService.populateAndSaveAuditRecord(null, AuditActionType.CREATE, "1234", "my audit data", "test-platform");
+ } catch (IllegalArgumentException e) {
+ assertEquals("auditOperationType must not be null.", e.getMessage());
+ return;
+ }
+ fail("Expected an Exception to be thrown.");
+ }
+
+ @Test
+ public void testPopulateAndSaveAuditRecordWithMapData() throws JsonProcessingException {
+ final ObjectMapper mapper = new ObjectMapper();
+ final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository, mapper);
+
+ final Map mapAuditData = new HashMap<>(2);
+ mapAuditData.put("foo1", "bar1");
+ mapAuditData.put("foofoo", "barbar");
+
+ auditRecordService.populateAndSaveAuditRecordUsingMapData(AuditOperationType.SCHEDULE, AuditActionType.CREATE,
+ "1234", mapAuditData, "test-platform");
+
+ final ArgumentCaptor argument = ArgumentCaptor.forClass(AuditRecord.class);
+ verify(this.auditRecordRepository, times(1)).save(argument.capture());
+ verifyNoMoreInteractions(this.auditRecordRepository);
+
+ final AuditRecord auditRecord = argument.getValue();
+
+ assertEquals(AuditActionType.CREATE, auditRecord.getAuditAction());
+ assertEquals(AuditOperationType.SCHEDULE, auditRecord.getAuditOperation());
+ assertEquals("1234", auditRecord.getCorrelationId());
+ assertEquals(mapper.convertValue(mapAuditData, JsonNode.class), mapper.readTree(auditRecord.getAuditData()));
+ assertEquals("test-platform", auditRecord.getPlatformName());
+ }
+
+ @Test
+ public void testPopulateAndSaveAuditRecordUsingMapDataWithNullAuditActionType() {
+ final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository);
+
+ final Map mapAuditData = new HashMap<>(2);
+ mapAuditData.put("foo", "bar");
+
+ try {
+ auditRecordService.populateAndSaveAuditRecordUsingMapData(AuditOperationType.SCHEDULE, null, "1234",
+ mapAuditData, null);
+ } catch (IllegalArgumentException e) {
+ assertEquals("auditActionType must not be null.", e.getMessage());
+ return;
+ }
+ fail("Expected an Exception to be thrown.");
+ }
+
+ @Test
+ public void testPopulateAndSaveAuditRecordUsingMapDataWithNullAuditOperationType() {
+ final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository);
+
+ final Map mapAuditData = new HashMap<>(2);
+ mapAuditData.put("foo", "bar");
+
+ try {
+ auditRecordService.populateAndSaveAuditRecordUsingMapData(null, AuditActionType.CREATE, "1234",
+ mapAuditData, null);
+ } catch (IllegalArgumentException e) {
+ assertEquals("auditOperationType must not be null.", e.getMessage());
+ return;
+ }
+ fail("Expected an Exception to be thrown.");
+ }
+
+ @Test
+ public void testPopulateAndSaveAuditRecordUsingMapDataThrowingJsonProcessingException()
+ throws JsonProcessingException {
+ final ObjectMapper objectMapper = mock(ObjectMapper.class);
+ when(objectMapper.writeValueAsString(any(Object.class))).thenThrow(new JsonProcessingException("Error") {
+ private static final long serialVersionUID = 1L;
+ });
+
+ final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository,
+ objectMapper);
+
+ final Map mapAuditData = new HashMap<>(2);
+ mapAuditData.put("foo", "bar");
+
+ auditRecordService.populateAndSaveAuditRecordUsingMapData(AuditOperationType.SCHEDULE, AuditActionType.CREATE,
+ "1234", mapAuditData, "test-platform");
+
+ final ArgumentCaptor argument = ArgumentCaptor.forClass(AuditRecord.class);
+ verify(this.auditRecordRepository, times(1)).save(argument.capture());
+ verifyNoMoreInteractions(this.auditRecordRepository);
+
+ AuditRecord auditRecord = argument.getValue();
+
+ assertEquals(AuditActionType.CREATE, auditRecord.getAuditAction());
+ assertEquals(AuditOperationType.SCHEDULE, auditRecord.getAuditOperation());
+ assertEquals("1234", auditRecord.getCorrelationId());
+ assertEquals("test-platform", auditRecord.getPlatformName());
+ assertEquals("Error serializing audit record data. Data = {foo=bar}", auditRecord.getAuditData());
+
+
+ }
+
+ @Test
+ public void testPopulateAndSaveAuditRecordUsingSensitiveMapData() {
+ final ObjectMapper objectMapper = new ObjectMapper();
+ final AuditRecordService auditRecordService = new DefaultAuditRecordService(this.auditRecordRepository, objectMapper);
+
+ final Map mapAuditData = new HashMap<>(2);
+ mapAuditData.put("foo", "bar");
+ mapAuditData.put("spring.cloud.config.password", "12345");
+ final Map child = new HashMap<>();
+ child.put("password", "54321");
+ child.put("bar1", "foo2");
+ mapAuditData.put("spring.child", child);
+ mapAuditData.put("spring.empty", Collections.emptyMap());
+
+ auditRecordService.populateAndSaveAuditRecordUsingMapData(AuditOperationType.SCHEDULE, AuditActionType.CREATE,
+ "1234", mapAuditData, "test-platform");
+
+ final ArgumentCaptor argument = ArgumentCaptor.forClass(AuditRecord.class);
+ verify(this.auditRecordRepository, times(1)).save(argument.capture());
+ verifyNoMoreInteractions(this.auditRecordRepository);
+
+ AuditRecord auditRecord = argument.getValue();
+
+ assertEquals(AuditActionType.CREATE, auditRecord.getAuditAction());
+ assertEquals(AuditOperationType.SCHEDULE, auditRecord.getAuditOperation());
+ assertEquals("1234", auditRecord.getCorrelationId());
+
+ assertEquals("test-platform", auditRecord.getPlatformName());
+ System.out.println("auditData=" + auditRecord.getAuditData());
+ assertTrue(auditRecord.getAuditData().contains("\"******\""));
+ assertTrue(auditRecord.getAuditData().contains("\"bar\""));
+ assertTrue(auditRecord.getAuditData().contains("\"foo\""));
+ assertTrue(auditRecord.getAuditData().contains("\"spring.cloud.config.password\""));
+ assertTrue(auditRecord.getAuditData().contains("\"password\""));
+ assertFalse(auditRecord.getAuditData().contains("54321"));
+ assertFalse(auditRecord.getAuditData().contains("12345"));
+ }
+
+ @Test
+ public void testFindAuditRecordByAuditOperationTypeAndAuditActionType() {
+ AuditRecordService auditRecordService = new DefaultAuditRecordService(auditRecordRepository);
+
+ AuditActionType[] auditActionTypes = {AuditActionType.CREATE};
+ AuditOperationType[] auditOperationTypes = {AuditOperationType.STREAM};
+ PageRequest pageRequest = PageRequest.of(0, 1);
+ auditRecordService.findAuditRecordByAuditOperationTypeAndAuditActionTypeAndDate(pageRequest, auditActionTypes,
+ auditOperationTypes, null, null);
+
+ verify(this.auditRecordRepository, times(1)).findByActionTypeAndOperationTypeAndDate(eq(auditOperationTypes),
+ eq(auditActionTypes), isNull(), isNull(), eq(pageRequest));
+ verifyNoMoreInteractions(this.auditRecordRepository);
+ }
+
+ @Test
+ public void testFindAuditRecordByAuditOperationTypeAndAuditActionTypeWithNullAuditActionType() {
+ AuditRecordService auditRecordService = new DefaultAuditRecordService(auditRecordRepository);
+
+ AuditOperationType[] auditOperationTypes = {AuditOperationType.STREAM};
+ PageRequest pageRequest = PageRequest.of(0, 1);
+ auditRecordService.findAuditRecordByAuditOperationTypeAndAuditActionTypeAndDate(pageRequest, null,
+ auditOperationTypes, null, null);
+
+ verify(this.auditRecordRepository, times(1)).findByActionTypeAndOperationTypeAndDate(eq(auditOperationTypes),
+ isNull(), isNull(), isNull(), eq(pageRequest));
+ verifyNoMoreInteractions(this.auditRecordRepository);
+ }
+
+ @Test
+ public void testFindAuditRecordByAuditOperationTypeAndAuditActionTypeWithNullOperationType() {
+ AuditRecordService auditRecordService = new DefaultAuditRecordService(auditRecordRepository);
+
+ AuditActionType[] auditActionTypes = {AuditActionType.CREATE};
+ PageRequest pageRequest = PageRequest.of(0, 1);
+ auditRecordService.findAuditRecordByAuditOperationTypeAndAuditActionTypeAndDate(pageRequest, auditActionTypes,
+ null, null, null);
+
+ verify(this.auditRecordRepository, times(1)).findByActionTypeAndOperationTypeAndDate(isNull(),
+ eq(auditActionTypes), isNull(), isNull(), eq(pageRequest));
+ verifyNoMoreInteractions(this.auditRecordRepository);
+ }
+
+ @Test
+ public void testFindAuditRecordByAuditOperationTypeAndAuditActionTypeWithNullActionAndOperationType() {
+ AuditRecordService auditRecordService = new DefaultAuditRecordService(auditRecordRepository);
+
+ PageRequest pageRequest = PageRequest.of(0, 1);
+ auditRecordService.findAuditRecordByAuditOperationTypeAndAuditActionTypeAndDate(pageRequest, null, null, null,
+ null);
+
+ verify(this.auditRecordRepository, times(1)).findByActionTypeAndOperationTypeAndDate(isNull(), isNull(),
+ isNull(), isNull(), eq(pageRequest));
+ verifyNoMoreInteractions(this.auditRecordRepository);
+ }
}
diff --git a/spring-cloud-dataflow-autoconfigure/pom.xml b/spring-cloud-dataflow-autoconfigure/pom.xml
index 87fb52b0ac..f67bca5186 100644
--- a/spring-cloud-dataflow-autoconfigure/pom.xml
+++ b/spring-cloud-dataflow-autoconfigure/pom.xml
@@ -1,15 +1,21 @@
-
+4.0.0org.springframework.cloudspring-cloud-dataflow-parent
- 2.9.2-SNAPSHOT
+ 2.11.6-SNAPSHOT
+ ../spring-cloud-dataflow-parentspring-cloud-dataflow-autoconfigurejarspring-cloud-dataflow-autoconfigureData Flow Autoconfig
+
+ true
+ 3.4.1
+ org.springframework.boot
@@ -18,10 +24,12 @@
org.springframework.cloudspring-cloud-dataflow-server-core
+ ${project.version}org.springframework.cloudspring-cloud-dataflow-platform-kubernetes
+ ${project.version}io.fabric8
@@ -30,6 +38,7 @@