From e472308e7b4eb586cf0fdb68a1f97c099cbf9e34 Mon Sep 17 00:00:00 2001 From: Markus Strehle <11627201+strehle@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:55:43 +0100 Subject: [PATCH 01/10] Sonar fix for split (#2641) --- .../cloudfoundry/identity/uaa/util/UaaHttpRequestUtils.java | 3 ++- .../identity/uaa/util/UaaHttpRequestUtilsTest.java | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtils.java b/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtils.java index a100c934a3a..c5d28ab3f45 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtils.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtils.java @@ -172,10 +172,11 @@ public UaaConnectionKeepAliveStrategy(long connectionKeepAliveMax) { } } + @SuppressWarnings("java:S1168") private static String[] split(final String s) { if (TextUtils.isBlank(s)) { return null; } - return s.split(" *, *"); + return stream(s.split(",")).map(String::trim).toList().toArray(String[]::new); } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtilsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtilsTest.java index 54cd97a00e9..8149b3f6ffb 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtilsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaHttpRequestUtilsTest.java @@ -99,6 +99,7 @@ public void testHttpProxy() throws Exception { @Test public void testHttpsProxy() throws Exception { String host = "localhost"; + System.setProperty("https.protocols", " TLSv1.2, TLSv1.3 "); System.setProperty(HTTPS_HOST_PROPERTY, host); System.setProperty(HTTPS_PORT_PROPERTY, String.valueOf(httpServer.getAddress().getPort())); testHttpProxy("https://google.com:443/", httpServer.getAddress().getPort(), host, false); From a0613a9225293c608a1fa8207775f591d3240f8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 18:47:29 +0100 Subject: [PATCH 02/10] build(deps): bump versions.tomcatCargoVersion from 9.0.83 to 9.0.84 (#2642) Bumps `versions.tomcatCargoVersion` from 9.0.83 to 9.0.84. Updates `org.apache.tomcat.embed:tomcat-embed-el` from 9.0.83 to 9.0.84 Updates `org.apache.tomcat.embed:tomcat-embed-core` from 9.0.83 to 9.0.84 Updates `org.apache.tomcat.embed:tomcat-embed-jasper` from 9.0.83 to 9.0.84 Updates `org.apache.tomcat:tomcat-jdbc` from 9.0.83 to 9.0.84 --- updated-dependencies: - dependency-name: org.apache.tomcat.embed:tomcat-embed-el dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.tomcat.embed:tomcat-embed-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.tomcat.embed:tomcat-embed-jasper dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.tomcat:tomcat-jdbc dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 2e138764d3b..19f82f6c50d 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -12,7 +12,7 @@ versions.springBootVersion = "2.7.18" versions.springSecurityJwtVersion = "1.1.1.RELEASE" versions.springSecurityOAuthVersion = "2.5.2.RELEASE" versions.springSecuritySamlVersion = "1.0.10.RELEASE" -versions.tomcatCargoVersion = "9.0.83" +versions.tomcatCargoVersion = "9.0.84" versions.guavaVersion = "32.1.3-jre" versions.seleniumVersion = "4.13.0" versions.braveVersion = "5.16.0" From 46f1ae4b55e9e5a9fc2e6af5101a6d869ddca661 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 20:26:51 +0100 Subject: [PATCH 03/10] build(deps): bump github/codeql-action from 2 to 3 (#2644) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a324fb12e40..7a03399d988 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -53,7 +53,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -67,4 +67,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From e438c35bfebb6b4ded48fa9842bef65c21d39b51 Mon Sep 17 00:00:00 2001 From: Cloud Foundry Identity Team Date: Thu, 14 Dec 2023 08:10:00 +0000 Subject: [PATCH 04/10] Update UAA image reference in k8s deployment template to cloudfoundry/uaa@sha256:4c7f2d881bc9c4a075232064e906d032c9512b03b87c06cbb2501560cb2f14e4 --- k8s/templates/values/image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/templates/values/image.yml b/k8s/templates/values/image.yml index 3658543281f..c87d02bb66f 100644 --- a/k8s/templates/values/image.yml +++ b/k8s/templates/values/image.yml @@ -1,3 +1,3 @@ #@data/values --- -image: "index.docker.io/cloudfoundry/uaa@sha256:1dffd343dde85b492e26b83df4d3dea3bdc23611870c801bdf7e9bda11f9fba4" +image: "cloudfoundry/uaa@sha256:4c7f2d881bc9c4a075232064e906d032c9512b03b87c06cbb2501560cb2f14e4" From b03cc052fc3a3366080bfb54dc8eeada7b95b3ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:46:37 +0100 Subject: [PATCH 05/10] build(deps): bump k8s.io/client-go from 0.28.4 to 0.29.0 in /k8s (#2646) Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.28.4 to 0.29.0. - [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/kubernetes/client-go/compare/v0.28.4...v0.29.0) --- updated-dependencies: - dependency-name: k8s.io/client-go dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- k8s/go.mod | 14 +++++++------- k8s/go.sum | 34 +++++++++++++++++----------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/k8s/go.mod b/k8s/go.mod index caa62c61520..bc4d1007b2b 100644 --- a/k8s/go.mod +++ b/k8s/go.mod @@ -6,14 +6,14 @@ require ( github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.30.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.28.4 - k8s.io/apimachinery v0.28.4 - k8s.io/client-go v0.28.4 + k8s.io/api v0.29.0 + k8s.io/apimachinery v0.29.0 + k8s.io/client-go v0.29.0 ) require ( github.com/fsnotify/fsnotify v1.4.9 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -27,9 +27,9 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/klog/v2 v2.100.1 // indirect - k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/k8s/go.sum b/k8s/go.sum index 3412e9c790c..e3345a21c8c 100644 --- a/k8s/go.sum +++ b/k8s/go.sum @@ -4,9 +4,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= @@ -24,6 +23,7 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -67,8 +67,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -138,19 +138,19 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= -k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= -k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= -k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= -k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= -k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= -k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= -k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= +k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= +k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= +k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= +k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= +k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= From 3566dc4b909c673f09581127b2613f79b3b03b7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 13:45:20 +0100 Subject: [PATCH 06/10] build(deps): bump actions/upload-artifact from 3 to 4 (#2648) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/generate-api-docs.yml | 2 +- .github/workflows/integration-tests.yml | 2 +- .github/workflows/unit-tests.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/generate-api-docs.yml b/.github/workflows/generate-api-docs.yml index b7b96564996..54ef0d90569 100644 --- a/.github/workflows/generate-api-docs.yml +++ b/.github/workflows/generate-api-docs.yml @@ -20,7 +20,7 @@ jobs: - name: Generate https://docs.cloudfoundry.org/api/uaa/ run: /root/uaa/scripts/generate-docs.sh - name: Documentation Upload - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Docs folder path: /root/uaa/uaa/build/docs/version/ \ No newline at end of file diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index cb26c26a58c..b9d66f09e1d 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -28,7 +28,7 @@ jobs: run: /root/uaa/scripts/integration-tests.sh $DB,default continue-on-error: true - name: Test result upload - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: steps.testrun.outcome == 'failure' with: name: Server test diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index ddbf8dde0c6..ef10b080a13 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -28,7 +28,7 @@ jobs: run: /root/uaa/scripts/unit-tests.sh $DB,default continue-on-error: true - name: Test result upload - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: steps.testrun.outcome == 'failure' with: name: Server test From 2aa488abb8c0e0ca019512278ce4457b8824f7f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:05:45 +0100 Subject: [PATCH 07/10] build(deps): bump versions.braveVersion from 5.16.0 to 5.17.0 (#2649) Bumps `versions.braveVersion` from 5.16.0 to 5.17.0. Updates `io.zipkin.brave:brave-instrumentation-spring-webmvc` from 5.16.0 to 5.17.0 Updates `io.zipkin.brave:brave-context-slf4j` from 5.16.0 to 5.17.0 --- updated-dependencies: - dependency-name: io.zipkin.brave:brave-instrumentation-spring-webmvc dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.zipkin.brave:brave-context-slf4j dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 19f82f6c50d..939cc933a75 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -15,7 +15,7 @@ versions.springSecuritySamlVersion = "1.0.10.RELEASE" versions.tomcatCargoVersion = "9.0.84" versions.guavaVersion = "32.1.3-jre" versions.seleniumVersion = "4.13.0" -versions.braveVersion = "5.16.0" +versions.braveVersion = "5.17.0" // Versions we're overriding from the Spring Boot Bom (Dependabot does not issue PRs to bump these versions, so we need to manually bump them) ext["mariadb.version"] = "2.7.11" // Bumping to v3 breaks some pipeline jobs (and compatibility with Amazon Aurora MySQL), so pinning to v2 for now. v2 (current version) is stable and will be supported until about September 2025 (https://mariadb.com/kb/en/about-mariadb-connector-j/). From 40c5744916a7bd2cfff4c0654fdd8cd21db4197e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 10:35:48 +0100 Subject: [PATCH 08/10] build(deps): bump versions.guavaVersion from 32.1.3-jre to 33.0.0-jre (#2650) Bumps `versions.guavaVersion` from 32.1.3-jre to 33.0.0-jre. Updates `com.google.guava:guava` from 32.1.3-jre to 33.0.0-jre - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) Updates `com.google.guava:guava-testlib` from 32.1.3-jre to 33.0.0-jre - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava dependency-type: direct:production update-type: version-update:semver-major - dependency-name: com.google.guava:guava-testlib dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 939cc933a75..fb3e4527e56 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -13,7 +13,7 @@ versions.springSecurityJwtVersion = "1.1.1.RELEASE" versions.springSecurityOAuthVersion = "2.5.2.RELEASE" versions.springSecuritySamlVersion = "1.0.10.RELEASE" versions.tomcatCargoVersion = "9.0.84" -versions.guavaVersion = "32.1.3-jre" +versions.guavaVersion = "33.0.0-jre" versions.seleniumVersion = "4.13.0" versions.braveVersion = "5.17.0" From ee332416f4904d61d7f13aae4001aa660876ff35 Mon Sep 17 00:00:00 2001 From: Vladimir Savchenko Date: Tue, 19 Dec 2023 20:11:07 +0200 Subject: [PATCH 09/10] Update documentation for adding SAP IAS as OIDC Provider (#2613) * Update sap-public-oidc-provider.md * Update sap-public-oidc-provider.md * remove unknown claims these claims are not needed in IAS even if they are there, but the mapping should be done based on standard OIDC claims. Using scopes: - email - profile ensures, that standard claims are returned --------- Co-authored-by: Markus Strehle <11627201+strehle@users.noreply.github.com> --- .../sap-public-oidc-provider.md | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/docs/OIDC-Provider-Examples/sap-public-oidc-provider.md b/docs/OIDC-Provider-Examples/sap-public-oidc-provider.md index 2ff5850b23f..c6915b48bc1 100644 --- a/docs/OIDC-Provider-Examples/sap-public-oidc-provider.md +++ b/docs/OIDC-Provider-Examples/sap-public-oidc-provider.md @@ -4,33 +4,51 @@ SAP IAS can be setup as an [OIDC provider](https://help.sap.com/viewer/6d6d63354 In order to prevent storing a client secret in UAA configuration and all of it's successor problems like secret rotation and so on, register the external OIDC provider with a public client. -1. Create an OIDC application and set it with [type public](https://help.sap.com/viewer/6d6d63354d1242d185ab4830fc04feb1/Cloud/en-US/a721157cd40544eb9bad40085cf8ec15.html). - Register the "Redirect URIs" in the application section "OpenID Connect Configuration" - - Add following URI in list field: - `http://{UAA_HOST}/login/callback/{origin}`. [Additional documentation for achieving this can be found here](https://help.sap.com/viewer/6d6d63354d1242d185ab4830fc04feb1/Cloud/en-US/1ae324ee3b2d4a728650eb022d5fd910.html). +1. Create an OIDC application and set it with [type public](https://help.sap.com/viewer/6d6d63354d1242d185ab4830fc04feb1/Cloud/en-US/a721157cd40544eb9bad40085cf8ec15.html) + * in Trust / OpenID Configuration / Grant Types / Authorization Code Flow / Enforce PKCE (S256) +3. Register the "Redirect URIs" in the application section "OpenID Connect Configuration" + * Add following URI in list field: + `https://{UAA_HOST}/login/callback/{origin}`. [Additional documentation for achieving this can be found here](https://help.sap.com/viewer/6d6d63354d1242d185ab4830fc04feb1/Cloud/en-US/1ae324ee3b2d4a728650eb022d5fd910.html). + * E.g. for a UAA part of a CF-Deployment, this is `https://login.cf./login/callback/{origin}` + * `{origin}` - is the id you of the OIDC provider you will use in UAA in the next step -2. Copy client id. +2. Go to "Client Authentication" section and check "Allow Public Client Flows". + * This will generate the "client id" on the top of the page + * Copy "client id", to use for the uaa configuration. -3. Minimal OIDC configuration needs to be added in login.ym. - Read configuration refer to '[https://.accounts.ondemand.com/.well-known/openid-configuration](https://help.sap.com/viewer/6d6d63354d1242d185ab4830fc04feb1/Cloud/en-US/c297516bae4547eb82eeed80fea2b937.html)' for discoveryUrl and issuer +4. Minimal OIDC configuration needs to be added in `uaa.yml` or `login.yml` (depending on the setup). + Read configuration refer to '[https://.accounts.ondemand.com/.well-known/openid-configuration](https://help.sap.com/viewer/6d6d63354d1242d185ab4830fc04feb1/Cloud/en-US/c297516bae4547eb82eeed80fea2b937.html)' for discoveryUrl and issuer. E.g. in the example below `ias.public` was selected as `{origin}` login: oauth: providers: ias.public: type: oidc1.0 - discoveryUrl: https://trailaccount.accounts.ondemand.com/.well-known/openid-configuration - issuer: https://trailaccount.accounts.ondemand.com + discoveryUrl: https://.accounts.ondemand.com/.well-known/openid-configuration + issuer: https://.accounts.ondemand.com scopes: - openid - email - profile linkText: Login with IAS-Public showLinkText: true - relyingPartyId: 3feb7ecb-d106-4432-b335-aca2689ad123 + relyingPartyId: + addShadowUserOnLogin: true + +6. Ensure that the scope `openid`, `email` and `profile` is included in the `scopes` property. Then UAA shadow user (if addShadowUserOnLogin=true) is created with all properties. -4. Ensure that the scope `openid`, `email` and `profile` is included in the`scopes` property. Then UAA shadow user (if addShadowUserOnLogin=true) is created - with all properties. +7. Restart UAA. + * You may see `Login with IAS-Public` link on your login page. + * Or if the link is not displayed, you need to enter the `{origin}` manually and then login against it -5. Restart UAA. You will see `Login with IAS-Public` link on your login page. +9. (optional) For CF Login, use `cf login --sso` and select the provider. + * Trying to login with User/Pass requires a confidential OAuth Client, creating a Secret in the Client Authentication tab, adding it as `relyingPartySecret` property and disabling "Enforce PKCE" + +8. (Optional) Use e-mail for Login Id instead of P-user + 1. In IAS Admin Page, under "Trust / Single Sign-on / Subject Name Identifier / Basic Configuration" + * Select "Select a basic attribute" : "Email" + 2. In `uaa.yml` append the following configuration, to the `login.oauth.providers.{origin}` section (at the same level as the other properties from the example above): + ``` + attributeMappings: + user_name: "email" + ``` From 63ad5f35b25edf65f6732d1599c970929449416d Mon Sep 17 00:00:00 2001 From: Klaus Kiefer Date: Wed, 20 Dec 2023 12:42:21 +0100 Subject: [PATCH 10/10] Add allowedGroups to UserConfig in IdZ configuration (#2606) * fix sonar issue: Utility classes should not have public constructors * fix: possibly unnecessary method call * fix: define Java version * handle in separate PR * feature: add attribute userConfig.allowedGroups to IdZ * revert format changes * revert format changes * fix Sonar warnings * remove TODO * create new needed bean similar than the other beans created * is an optional entry in identity zone configuration * fix sonar smells * combine default and allowed groups * fix Sonar warnings * add test class * add testResultingAllowedGroups * add testScopesIncludesAllowedAuthoritiesForUser * zoneId must not be null * use right asserts * remove null-check * add integration tests * fix createNotAllowedGroupFails * introduce list allowedGroups * update allowedGroups on server * pass IdZ id * determine zone id * update integration tests We have 4 tests 2 positive and 2 negative 2 using allowedGroups 2 relying on defaultGroups because allowedGroups is empty * cleanup in integration tests, fixes mftop refactor to omit dublicates * change log level to info * remove log message * check for groups which would be not allowed after the update * must not remove default group from configuration * remove sonar comment --------- Co-authored-by: Markus Strehle <11627201+strehle@users.noreply.github.com> Co-authored-by: d036670 --- .../identity/uaa/zone/UserConfig.java | 28 ++++++ .../identity/uaa/zone/IdentityZoneTest.java | 17 +++- .../identity/uaa/zone/UserConfigTest.java | 51 ++++++++++ .../identity/uaa/zone/SampleIdentityZone.json | 4 + .../IdentityZoneConfigurationBootstrap.java | 9 ++ .../oauth/UaaAuthorizationRequestManager.java | 5 + .../scim/jdbc/JdbcScimGroupProvisioning.java | 33 +++++++ .../uaa/user/JdbcUaaUserDatabase.java | 4 + ...ralIdentityZoneConfigurationValidator.java | 2 + .../uaa/zone/IdentityZoneEndpoints.java | 13 +++ .../uaa/zone/UserConfigValidator.java | 18 ++++ ...entityZoneConfigurationBootstrapTests.java | 50 ++++++---- .../IdentityZoneConfigurationTests.java | 2 + .../identity/uaa/oauth/TokenTestSupport.java | 3 + .../UaaAuthorizationRequestManagerTests.java | 24 +++++ .../LoginSamlAuthenticationProviderTests.java | 7 +- .../jdbc/JdbcScimGroupProvisioningTests.java | 28 ++++++ .../uaa/user/JdbcUaaUserDatabaseTests.java | 1 + .../uaa/zone/UserConfigValidatorTest.java | 34 +++++++ .../webapp/WEB-INF/spring/scim-endpoints.xml | 1 + .../ScimGroupEndpointsIntegrationTests.java | 95 +++++++++++++++++++ .../util/IntegrationTestUtils.java | 20 ++++ .../mock/zones/IdentityZoneEndpointDocs.java | 5 + .../IdentityZoneEndpointsMockMvcTests.java | 20 ++++ 24 files changed, 449 insertions(+), 25 deletions(-) create mode 100644 model/src/test/java/org/cloudfoundry/identity/uaa/zone/UserConfigTest.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/zone/UserConfigValidator.java create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/zone/UserConfigValidatorTest.java diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/zone/UserConfig.java b/model/src/main/java/org/cloudfoundry/identity/uaa/zone/UserConfig.java index f3fdb393105..702cde9b7b8 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/zone/UserConfig.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/zone/UserConfig.java @@ -3,7 +3,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.HashSet; import java.util.List; +import java.util.Set; @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) @@ -27,4 +29,30 @@ public List getDefaultGroups() { public void setDefaultGroups(List defaultGroups) { this.defaultGroups = defaultGroups; } + + // in addition to defaultGroups, which are implicitely allowed + private List allowedGroups = null; + + public List getAllowedGroups() { + return allowedGroups; + } + + public void setAllowedGroups(List allowedGroups) { + this.allowedGroups = allowedGroups; + } + + public boolean allGroupsAllowed() { + return (allowedGroups == null); + } + + // return defaultGroups plus allowedGroups + public Set resultingAllowedGroups() { + if (allGroupsAllowed()) { + return null; // null = all groups allowed + } else { + HashSet allAllowedGroups = new HashSet<>(allowedGroups); + if (defaultGroups != null) allAllowedGroups.addAll(defaultGroups); + return allAllowedGroups; + } + } } diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneTest.java b/model/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneTest.java index 8b4db207c7c..59fa780794f 100644 --- a/model/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneTest.java +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneTest.java @@ -10,11 +10,14 @@ import java.util.Calendar; import java.util.Date; +import java.util.List; +import java.util.Set; import java.util.stream.Stream; import static org.cloudfoundry.identity.uaa.test.ModelTestUtils.getResourceAsString; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; class IdentityZoneTest { @@ -124,8 +127,16 @@ void hashCode_usesOnlyId(IdentityZone zone, int expectedHashCode) { @Test void deserialize() { - final String sampleIdentityZone = getResourceAsString(getClass(), "SampleIdentityZone.json"); - - JsonUtils.readValue(sampleIdentityZone, IdentityZone.class); + final String sampleIdentityZoneJson = getResourceAsString(getClass(), "SampleIdentityZone.json"); + IdentityZone sampleIdentityZone = JsonUtils.readValue(sampleIdentityZoneJson, IdentityZone.class); + assertEquals("f7758816-ab47-48d9-9d24-25b10b92d4cc", sampleIdentityZone.getId()); + assertEquals("demo", sampleIdentityZone.getSubdomain()); + assertEquals(List.of("openid", "password.write", "uaa.user", "approvals.me", + "profile", "roles", "user_attributes", "uaa.offline_token"), + sampleIdentityZone.getConfig().getUserConfig().getDefaultGroups()); + assertEquals(Set.of("openid", "password.write", "uaa.user", "approvals.me", + "profile", "roles", "user_attributes", "uaa.offline_token", + "scim.me", "cloud_controller.user"), + sampleIdentityZone.getConfig().getUserConfig().resultingAllowedGroups()); } } \ No newline at end of file diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/zone/UserConfigTest.java b/model/src/test/java/org/cloudfoundry/identity/uaa/zone/UserConfigTest.java new file mode 100644 index 00000000000..560f2129b55 --- /dev/null +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/zone/UserConfigTest.java @@ -0,0 +1,51 @@ +package org.cloudfoundry.identity.uaa.zone; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.Set; + +import org.junit.Test; + +public class UserConfigTest { + + @Test + public void testDefaultConfig() { + UserConfig userConfig = new UserConfig(); + assertTrue(userConfig.getDefaultGroups().contains("openid")); + assertNull(userConfig.getAllowedGroups()); // all groups allowed + assertNull(userConfig.resultingAllowedGroups()); // all groups allowed + } + + @Test + public void testResultingAllowedGroups() { + UserConfig userConfig = new UserConfig(); + userConfig.setDefaultGroups(List.of("openid")); + userConfig.setAllowedGroups(List.of("uaa.user")); + assertEquals(List.of("openid"), userConfig.getDefaultGroups()); + assertEquals(List.of("uaa.user"), userConfig.getAllowedGroups()); + assertEquals(Set.of("openid", "uaa.user"), userConfig.resultingAllowedGroups()); + } + + @Test + public void testNoDefaultGroups() { + UserConfig userConfig = new UserConfig(); + userConfig.setDefaultGroups(null); + userConfig.setAllowedGroups(List.of("uaa.user")); + assertNull(userConfig.getDefaultGroups()); + assertEquals(List.of("uaa.user"), userConfig.getAllowedGroups()); + assertEquals(Set.of("uaa.user"), userConfig.resultingAllowedGroups()); + } + + @Test + public void testNoDefaultAndNoAllowedGroups() { + UserConfig userConfig = new UserConfig(); + userConfig.setDefaultGroups(null); + userConfig.setAllowedGroups(null); + assertNull(userConfig.getDefaultGroups()); + assertNull(userConfig.getAllowedGroups()); // all groups allowed + assertNull(userConfig.resultingAllowedGroups()); // all groups allowed + } +} diff --git a/model/src/test/resources/org/cloudfoundry/identity/uaa/zone/SampleIdentityZone.json b/model/src/test/resources/org/cloudfoundry/identity/uaa/zone/SampleIdentityZone.json index 0aa5600263c..3cbc9812f5e 100644 --- a/model/src/test/resources/org/cloudfoundry/identity/uaa/zone/SampleIdentityZone.json +++ b/model/src/test/resources/org/cloudfoundry/identity/uaa/zone/SampleIdentityZone.json @@ -122,6 +122,10 @@ "roles", "user_attributes", "uaa.offline_token" + ], + "allowedGroups": [ + "scim.me", + "cloud_controller.user" ] } }, diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/IdentityZoneConfigurationBootstrap.java b/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/IdentityZoneConfigurationBootstrap.java index 645aefed2b0..dc7a96b73ee 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/IdentityZoneConfigurationBootstrap.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/IdentityZoneConfigurationBootstrap.java @@ -67,6 +67,8 @@ public class IdentityZoneConfigurationBootstrap implements InitializingBean { private Collection defaultUserGroups; + private Collection allowedUserGroups; + private IdentityZoneValidator validator = (config, mode) -> config; private Map branding; @@ -133,6 +135,9 @@ public void afterPropertiesSet() throws InvalidIdentityZoneDetailsException { definition.getUserConfig().setDefaultGroups(new LinkedList<>(defaultUserGroups)); } + if (allowedUserGroups!=null) { + definition.getUserConfig().setAllowedGroups(new LinkedList<>(allowedUserGroups)); + } identityZone.setConfig(definition); @@ -254,6 +259,10 @@ public void setDefaultUserGroups(Collection defaultUserGroups) { this.defaultUserGroups = defaultUserGroups; } + public void setAllowedUserGroups(Collection allowedUserGroups) { + this.allowedUserGroups = allowedUserGroups; + } + public boolean isDisableSamlInResponseToCheck() { return disableSamlInResponseToCheck; } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationRequestManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationRequestManager.java index 0859400ca64..10e761dc95f 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationRequestManager.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationRequestManager.java @@ -243,13 +243,18 @@ protected void checkClientIdpAuthorization(BaseClientDetails client, UaaUser use * @param authorities the users authorities * @return modified requested scopes adapted according to the rules specified */ + @SuppressWarnings("java:S1874") private Set checkUserScopes(Set requestedScopes, Collection authorities, ClientDetails clientDetails) { Set allowed = new LinkedHashSet<>(AuthorityUtils.authorityListToSet(authorities)); // Add in all default requestedScopes Collection defaultScopes = IdentityZoneHolder.get().getConfig().getUserConfig().getDefaultGroups(); + Collection allowedScopes = IdentityZoneHolder.get().getConfig().getUserConfig().resultingAllowedGroups(); allowed.addAll(defaultScopes); + if (allowedScopes != null) { + allowed.retainAll(allowedScopes); + } // Find intersection of user authorities, default requestedScopes and client requestedScopes: Set result = intersectScopes(new LinkedHashSet<>(requestedScopes), clientDetails.getScope(), allowed); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioning.java index d1423649a16..433fca9ae5a 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioning.java @@ -13,6 +13,9 @@ import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceNotFoundException; import org.cloudfoundry.identity.uaa.util.beans.DbUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.cloudfoundry.identity.uaa.zone.JdbcIdentityZoneProvisioning; +import org.cloudfoundry.identity.uaa.zone.ZoneDoesNotExistsException; import org.cloudfoundry.identity.uaa.zone.event.IdentityZoneModifiedEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,6 +28,7 @@ import java.sql.Timestamp; import java.util.Date; import java.util.List; +import java.util.Set; import java.util.UUID; import static org.cloudfoundry.identity.uaa.zone.ZoneManagementScopes.getSystemScopes; @@ -66,6 +70,7 @@ public Logger getLogger() { private JdbcScimGroupExternalMembershipManager jdbcScimGroupExternalMembershipManager; private JdbcScimGroupMembershipManager jdbcScimGroupMembershipManager; + private JdbcIdentityZoneProvisioning jdbcIdentityZoneProvisioning; public JdbcScimGroupProvisioning( final JdbcTemplate jdbcTemplate, @@ -153,6 +158,10 @@ public void setJdbcScimGroupMembershipManager(final JdbcScimGroupMembershipManag this.jdbcScimGroupMembershipManager = jdbcScimGroupMembershipManager; } + public void setJdbcIdentityZoneProvisioning(JdbcIdentityZoneProvisioning jdbcIdentityZoneProvisioning) { + this.jdbcIdentityZoneProvisioning = jdbcIdentityZoneProvisioning; + } + void createAndIgnoreDuplicate(final String name, final String zoneId) { try { create(new ScimGroup(null, name, zoneId), zoneId); @@ -220,9 +229,24 @@ public ScimGroup retrieve(String id, final String zoneId) throws ScimResourceNot } } + @SuppressWarnings("java:S1874") + private Set getAllowedUserGroups(String zoneId) { + Set zoneAllowedGroups = null; // default: all groups allowed + try { + IdentityZone currentZone = IdentityZoneHolder.get(); + zoneAllowedGroups = (currentZone.getId().equals(zoneId)) ? + currentZone.getConfig().getUserConfig().resultingAllowedGroups() : + jdbcIdentityZoneProvisioning.retrieve(zoneId).getConfig().getUserConfig().resultingAllowedGroups(); + } catch (ZoneDoesNotExistsException e) { + logger.debug("could not retrieve identity zone with id: {}", zoneId); + } + return zoneAllowedGroups; + } + @Override public ScimGroup create(final ScimGroup group, final String zoneId) throws InvalidScimResourceException { validateZoneId(zoneId); + validateAllowedUserGroups(zoneId, group); final String id = UUID.randomUUID().toString(); logger.debug("creating new group with id: {}", id); try { @@ -247,6 +271,7 @@ public ScimGroup create(final ScimGroup group, final String zoneId) throws Inval @Override public ScimGroup update(final String id, final ScimGroup group, final String zoneId) throws InvalidScimResourceException, ScimResourceNotFoundException { + validateAllowedUserGroups(zoneId, group); try { validateZoneId(zoneId); validateGroup(group); @@ -324,4 +349,12 @@ protected void validateOrderBy(String orderBy) throws IllegalArgumentException { super.validateOrderBy(orderBy, GROUP_FIELDS); } + private void validateAllowedUserGroups(String zoneId, ScimGroup group) { + Set allowedGroups = getAllowedUserGroups(zoneId); + if ((allowedGroups != null) && (!allowedGroups.contains(group.getDisplayName()))) { + throw new InvalidScimResourceException("The group with displayName: " + group.getDisplayName() + + " is not allowed in Identity Zone " + zoneId); + } + } + } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java b/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java index 28e0caba27c..77ffc79ab9f 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java @@ -265,6 +265,10 @@ private String getAuthorities(final String userId) throws SQLException { Set authorities = new HashSet<>(); getAuthorities(authorities, Collections.singletonList(userId)); authorities.addAll(identityZoneManager.getCurrentIdentityZone().getConfig().getUserConfig().getDefaultGroups()); + Set allowedGroups = identityZoneManager.getCurrentIdentityZone().getConfig().getUserConfig().resultingAllowedGroups(); + if (allowedGroups != null) { + authorities.retainAll(allowedGroups); + } return StringUtils.collectionToCommaDelimitedString(new HashSet<>(authorities)); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/GeneralIdentityZoneConfigurationValidator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/GeneralIdentityZoneConfigurationValidator.java index 70f430b7e7f..12535ae4295 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/GeneralIdentityZoneConfigurationValidator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/GeneralIdentityZoneConfigurationValidator.java @@ -74,6 +74,8 @@ public IdentityZoneConfiguration validate(IdentityZone zone, IdentityZoneValidat validateRegexStrings(config.getCorsPolicy().getXhrConfiguration().getAllowedOrigins(), "config.corsPolicy.xhrConfiguration.allowedOrigins"); validateRegexStrings(config.getCorsPolicy().getDefaultConfiguration().getAllowedUris(), "config.corsPolicy.defaultConfiguration.allowedUris"); validateRegexStrings(config.getCorsPolicy().getDefaultConfiguration().getAllowedOrigins(), "config.corsPolicy.defaultConfiguration.allowedOrigins"); + + UserConfigValidator.validate(config.getUserConfig()); } if (config.getBranding() != null && config.getBranding().getConsent() != null) { diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneEndpoints.java b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneEndpoints.java index 1389890c106..778dabe37e6 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneEndpoints.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneEndpoints.java @@ -47,6 +47,7 @@ import java.util.Locale; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; import static java.util.Optional.ofNullable; import static org.springframework.http.HttpStatus.CONFLICT; @@ -267,6 +268,18 @@ public ResponseEntity updateIdentityZone( body.setId(id); body = validator.validate(body, IdentityZoneValidator.Mode.MODIFY); + // check for groups which would be not allowed after the update + UserConfig userConfig = body.getConfig().getUserConfig(); + if (!userConfig.allGroupsAllowed()) { + List existingGroupNames = groupProvisioning.retrieveAll(body.getId()) + .stream() + .map(ScimGroup::getDescription) + .collect(Collectors.toList()); + if (!userConfig.resultingAllowedGroups().containsAll(existingGroupNames)) { + throw new UnprocessableEntityException("The identity zone details contains not-allowed groups."); + } + } + logger.debug("Zone - updating id[{}] subdomain[{}]", UaaStringUtils.getCleanedUserControlString(id), UaaStringUtils.getCleanedUserControlString(body.getSubdomain()) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/UserConfigValidator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/UserConfigValidator.java new file mode 100644 index 00000000000..e16cb667926 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/UserConfigValidator.java @@ -0,0 +1,18 @@ +package org.cloudfoundry.identity.uaa.zone; + +import java.util.Set; + +public class UserConfigValidator { + + // add a private constructor to hide the implicit public one + private UserConfigValidator() { + } + + public static void validate(UserConfig config) throws InvalidIdentityZoneConfigurationException { + Set allowedGroups = (config == null) ? null : config.resultingAllowedGroups(); + if ((allowedGroups != null) && (allowedGroups.isEmpty())) { + String message = "At least one group must be allowed"; + throw new InvalidIdentityZoneConfigurationException(message); + } + } +} diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityZoneConfigurationBootstrapTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityZoneConfigurationBootstrapTests.java index 4d67bdfe45b..dc846518e1a 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityZoneConfigurationBootstrapTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityZoneConfigurationBootstrapTests.java @@ -64,14 +64,14 @@ public class IdentityZoneConfigurationBootstrapTests { "wTKZHjWybPHsW2q8Z6Moz5dvE+XMd11c5NtIG2/L97I=\n" + "-----END RSA PRIVATE KEY-----"; - public static final String ID = "id"; + private static final String ID = "id"; private IdentityZoneProvisioning provisioning; private IdentityZoneConfigurationBootstrap bootstrap; private Map links = new HashMap<>(); private GeneralIdentityZoneValidator validator; @BeforeEach - public void configureProvisioning(@Autowired JdbcTemplate jdbcTemplate) throws SQLException { + void configureProvisioning(@Autowired JdbcTemplate jdbcTemplate) throws SQLException { TestUtils.cleanAndSeedDb(jdbcTemplate); provisioning = new JdbcIdentityZoneProvisioning(jdbcTemplate); bootstrap = new IdentityZoneConfigurationBootstrap(provisioning); @@ -98,7 +98,7 @@ public void configureProvisioning(@Autowired JdbcTemplate jdbcTemplate) throws S } @Test - public void testClientSecretPolicy() throws Exception { + void testClientSecretPolicy() throws Exception { bootstrap.setClientSecretPolicy(new ClientSecretPolicy(0, 255, 0, 1, 1, 1, 6)); bootstrap.afterPropertiesSet(); IdentityZone uaa = provisioning.retrieve(IdentityZone.getUaaZoneId()); @@ -112,7 +112,7 @@ public void testClientSecretPolicy() throws Exception { } @Test - public void test_multiple_keys() throws InvalidIdentityZoneDetailsException { + void test_multiple_keys() throws InvalidIdentityZoneDetailsException { bootstrap.setSamlSpPrivateKey(SamlTestUtils.PROVIDER_PRIVATE_KEY); bootstrap.setSamlSpCertificate(SamlTestUtils.PROVIDER_CERTIFICATE); bootstrap.setSamlSpPrivateKeyPassphrase(SamlTestUtils.PROVIDER_PRIVATE_KEY_PASSWORD); @@ -156,7 +156,7 @@ void test_keyId_null_exception() { } @Test - public void testDefaultSamlKeys() throws Exception { + void testDefaultSamlKeys() throws Exception { bootstrap.setSamlSpPrivateKey(SamlTestUtils.PROVIDER_PRIVATE_KEY); bootstrap.setSamlSpCertificate(SamlTestUtils.PROVIDER_CERTIFICATE); bootstrap.setSamlSpPrivateKeyPassphrase(SamlTestUtils.PROVIDER_PRIVATE_KEY_PASSWORD); @@ -168,7 +168,7 @@ public void testDefaultSamlKeys() throws Exception { } @Test - public void enable_in_response_to() throws Exception { + void enable_in_response_to() throws Exception { bootstrap.setDisableSamlInResponseToCheck(false); bootstrap.afterPropertiesSet(); IdentityZone uaa = provisioning.retrieve(IdentityZone.getUaaZoneId()); @@ -176,7 +176,7 @@ public void enable_in_response_to() throws Exception { } @Test - public void saml_disable_in_response_to() throws Exception { + void saml_disable_in_response_to() throws Exception { bootstrap.setDisableSamlInResponseToCheck(true); bootstrap.afterPropertiesSet(); IdentityZone uaa = provisioning.retrieve(IdentityZone.getUaaZoneId()); @@ -184,7 +184,7 @@ public void saml_disable_in_response_to() throws Exception { } @Test - public void testDefaultGroups() throws Exception { + void testDefaultGroups() throws Exception { String[] groups = {"group1", "group2", "group3"}; bootstrap.setDefaultUserGroups(Arrays.asList(groups)); bootstrap.afterPropertiesSet(); @@ -193,7 +193,17 @@ public void testDefaultGroups() throws Exception { } @Test - public void tokenPolicy_configured_fromValuesInYaml() throws Exception { + void testAllowedGroups() throws Exception { + String[] groups = {"group1", "group2", "group3"}; + bootstrap.setDefaultUserGroups(Arrays.asList(groups)); + bootstrap.setAllowedUserGroups(Arrays.asList(groups)); + bootstrap.afterPropertiesSet(); + IdentityZone uaa = provisioning.retrieve(IdentityZone.getUaaZoneId()); + assertThat(uaa.getConfig().getUserConfig().resultingAllowedGroups(), containsInAnyOrder(groups)); + } + + @Test + void tokenPolicy_configured_fromValuesInYaml() throws Exception { TokenPolicy tokenPolicy = new TokenPolicy(); Map keys = new HashMap<>(); keys.put(ID, PRIVATE_KEY); @@ -214,7 +224,7 @@ public void tokenPolicy_configured_fromValuesInYaml() throws Exception { } @Test - public void disable_self_service_links() throws Exception { + void disable_self_service_links() throws Exception { bootstrap.setSelfServiceLinksEnabled(false); bootstrap.afterPropertiesSet(); @@ -223,7 +233,7 @@ public void disable_self_service_links() throws Exception { } @Test - public void set_home_redirect() throws Exception { + void set_home_redirect() throws Exception { bootstrap.setHomeRedirect("http://some.redirect.com/redirect"); bootstrap.afterPropertiesSet(); @@ -232,7 +242,7 @@ public void set_home_redirect() throws Exception { } @Test - public void signup_link_configured() throws Exception { + void signup_link_configured() throws Exception { links.put("signup", "/configured_signup"); bootstrap.setSelfServiceLinks(links); bootstrap.afterPropertiesSet(); @@ -243,7 +253,7 @@ public void signup_link_configured() throws Exception { } @Test - public void passwd_link_configured() throws Exception { + void passwd_link_configured() throws Exception { links.put("passwd", "/configured_passwd"); bootstrap.setSelfServiceLinks(links); bootstrap.afterPropertiesSet(); @@ -254,7 +264,7 @@ public void passwd_link_configured() throws Exception { } @Test - public void test_logout_redirect() throws Exception { + void test_logout_redirect() throws Exception { bootstrap.setLogoutDefaultRedirectUrl("/configured_login"); bootstrap.setLogoutDisableRedirectParameter(false); bootstrap.setLogoutRedirectParameterName("test"); @@ -269,7 +279,7 @@ public void test_logout_redirect() throws Exception { @Test - public void test_prompts() throws Exception { + void test_prompts() throws Exception { List prompts = Arrays.asList( new Prompt("name1", "type1", "text1"), new Prompt("name2", "type2", "text2") @@ -281,7 +291,7 @@ public void test_prompts() throws Exception { } @Test - public void idpDiscoveryEnabled() throws Exception { + void idpDiscoveryEnabled() throws Exception { bootstrap.setIdpDiscoveryEnabled(true); bootstrap.afterPropertiesSet(); IdentityZoneConfiguration config = provisioning.retrieve(IdentityZone.getUaaZoneId()).getConfig(); @@ -289,18 +299,18 @@ public void idpDiscoveryEnabled() throws Exception { } @Test - public void testMfaDisabledByDefault() { + void testMfaDisabledByDefault() { assertFalse(bootstrap.isMfaEnabled()); } @Test - public void testMfaDisabledWithInvalidName() throws Exception { + void testMfaDisabledWithInvalidName() throws Exception { bootstrap.setMfaProviderName("NotExistingProvider"); assertThrows(InvalidIdentityZoneDetailsException.class, () -> bootstrap.afterPropertiesSet()); } @Test - public void testMfaEnabledValidName() throws Exception { + void testMfaEnabledValidName() throws Exception { bootstrap.setMfaProviderName("testProvider"); bootstrap.setMfaEnabled(true); bootstrap.afterPropertiesSet(); @@ -310,7 +320,7 @@ public void testMfaEnabledValidName() throws Exception { } @Test - public void testMfaEnabledInvalidName() throws Exception { + void testMfaEnabledInvalidName() throws Exception { bootstrap.setMfaProviderName("InvalidProvider"); bootstrap.setMfaEnabled(true); assertThrows(InvalidIdentityZoneDetailsException.class, () -> bootstrap.afterPropertiesSet()); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityZoneConfigurationTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityZoneConfigurationTests.java index 323dcb54bed..fda80e8b5f7 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityZoneConfigurationTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityZoneConfigurationTests.java @@ -40,6 +40,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.springframework.http.HttpHeaders.ACCEPT; @@ -74,6 +75,7 @@ public void default_user_groups_when_json_is_deserialized() { "user_attributes", "uaa.offline_token" )); + assertNull(definition.getUserConfig().resultingAllowedGroups()); s = JsonUtils.writeValueAsString(definition); assertThat(s, containsString("userConfig")); assertThat(s, containsString("uaa.offline_token")); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/TokenTestSupport.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/TokenTestSupport.java index d69c3614584..950aaf2f29a 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/TokenTestSupport.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/TokenTestSupport.java @@ -306,6 +306,9 @@ public TokenTestSupport(UaaTokenEnhancer tokenEnhancer, KeyInfoService keyInfo) IdentityZoneHolder.get().getConfig().getUserConfig().setDefaultGroups( new LinkedList<>(AuthorityUtils.authorityListToSet(USER_AUTHORITIES)) ); + IdentityZoneHolder.get().getConfig().getUserConfig().setAllowedGroups( + new LinkedList<>(AuthorityUtils.authorityListToSet(USER_AUTHORITIES)) + ); } public UaaTokenServices getUaaTokenServices() { diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationRequestManagerTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationRequestManagerTests.java index 5a8b76c69ed..b663fb7aa51 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationRequestManagerTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationRequestManagerTests.java @@ -148,6 +148,7 @@ void test_user_token_request() { parameters.put("expires_in", "44000"); parameters.put(OAuth2Utils.GRANT_TYPE, TokenConstants.GRANT_TYPE_USER_TOKEN); IdentityZoneHolder.get().getConfig().getUserConfig().setDefaultGroups(Collections.singletonList("uaa.user")); + IdentityZoneHolder.get().getConfig().getUserConfig().setAllowedGroups(null); // all groups allowed client.setScope(StringUtils.commaDelimitedListToSet("aud1.test,aud2.test,uaa.user")); when(clientDetailsService.loadClientByClientId(recipient.getClientId(), "uaa")).thenReturn(recipient); ReflectionTestUtils.setField(factory, "uaaUserDatabase", null); @@ -175,6 +176,17 @@ void testScopeIncludesAuthoritiesForUser() { factory.validateParameters(request.getRequestParameters(), client); } + @Test + void testScopesIncludesAllowedAuthoritiesForUser() { + when(mockSecurityContextAccessor.isUser()).thenReturn(true); + when(mockSecurityContextAccessor.getAuthorities()).thenReturn((Collection)AuthorityUtils.commaSeparatedStringToAuthorityList("foo.bar,spam.baz,space.1.developer")); + IdentityZoneHolder.get().getConfig().getUserConfig().setAllowedGroups(Arrays.asList("openid","foo.bar")); + client.setScope(StringUtils.commaDelimitedListToSet("foo.bar,spam.baz,space.1.developer")); + AuthorizationRequest request = factory.createAuthorizationRequest(parameters); + assertEquals(StringUtils.commaDelimitedListToSet("foo.bar"), new TreeSet(request.getScope())); + factory.validateParameters(request.getRequestParameters(), client); + } + @Test void testWildcardScopesIncludesAuthoritiesForUser() { when(mockSecurityContextAccessor.isUser()).thenReturn(true); @@ -185,10 +197,22 @@ void testWildcardScopesIncludesAuthoritiesForUser() { factory.validateParameters(request.getRequestParameters(), client); } + @Test + void testWildcardScopesIncludesAllowedAuthoritiesForUser() { + when(mockSecurityContextAccessor.isUser()).thenReturn(true); + when(mockSecurityContextAccessor.getAuthorities()).thenReturn((Collection)AuthorityUtils.commaSeparatedStringToAuthorityList("space.1.developer,space.2.developer,space.1.admin")); + IdentityZoneHolder.get().getConfig().getUserConfig().setAllowedGroups(Arrays.asList("openid","space.1.developer")); + client.setScope(StringUtils.commaDelimitedListToSet("space.*.developer")); + AuthorizationRequest request = factory.createAuthorizationRequest(parameters); + assertEquals(StringUtils.commaDelimitedListToSet("space.1.developer"), new TreeSet(request.getScope())); + factory.validateParameters(request.getRequestParameters(), client); + } + @Test void testOpenidScopeIncludeIsAResourceId() { parameters.put("scope", "openid foo.bar"); IdentityZoneHolder.get().getConfig().getUserConfig().setDefaultGroups(Collections.singletonList("openid")); + IdentityZoneHolder.get().getConfig().getUserConfig().setAllowedGroups(Arrays.asList("openid","foo.bar")); client.setScope(StringUtils.commaDelimitedListToSet("openid,foo.bar")); AuthorizationRequest request = factory.createAuthorizationRequest(parameters); assertEquals(StringUtils.commaDelimitedListToSet("openid,foo.bar"), new TreeSet(request.getScope())); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProviderTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProviderTests.java index b93ae0eb322..c4c196227ef 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProviderTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProviderTests.java @@ -134,6 +134,7 @@ class LoginSamlAuthenticationProviderTests { private static final String SAML_ADMIN = "saml.admin"; private static final String SAML_TEST = "saml.test"; private static final String SAML_NOT_MAPPED = "saml.unmapped"; + private static final String UAA_USER = "uaa.user"; private static final String UAA_SAML_USER = "uaa.saml.user"; private static final String UAA_SAML_ADMIN = "uaa.saml.admin"; private static final String UAA_SAML_TEST = "uaa.saml.test"; @@ -178,8 +179,10 @@ void configureProvider() throws SAMLException, SecurityException, DecryptionExce ScimGroupProvisioning groupProvisioning = new JdbcScimGroupProvisioning( jdbcTemplate, new JdbcPagingListFactory(jdbcTemplate, limitSqlAdapter), dbUtils); - identityZoneManager.getCurrentIdentityZone().getConfig().getUserConfig().setDefaultGroups(Collections.singletonList("uaa.user")); - groupProvisioning.createOrGet(new ScimGroup(null, "uaa.user", identityZoneManager.getCurrentIdentityZone().getId()), identityZoneManager.getCurrentIdentityZone().getId()); + identityZoneManager.getCurrentIdentityZone().getConfig().getUserConfig().setDefaultGroups(Collections.singletonList(UAA_USER)); + identityZoneManager.getCurrentIdentityZone().getConfig().getUserConfig().setAllowedGroups(Arrays.asList(UAA_USER, SAML_USER, + SAML_ADMIN,SAML_TEST,SAML_NOT_MAPPED, UAA_SAML_USER,UAA_SAML_ADMIN,UAA_SAML_TEST)); + groupProvisioning.createOrGet(new ScimGroup(null, UAA_USER, identityZoneManager.getCurrentIdentityZone().getId()), identityZoneManager.getCurrentIdentityZone().getId()); providerDefinition = new SamlIdentityProviderDefinition(); userProvisioning = new JdbcScimUserProvisioning(jdbcTemplate, new JdbcPagingListFactory(jdbcTemplate, limitSqlAdapter), passwordEncoder); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioningTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioningTests.java index 4c9d43418bb..36f526ddee7 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioningTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioningTests.java @@ -8,12 +8,14 @@ import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceConstraintFailedException; +import org.cloudfoundry.identity.uaa.scim.exception.InvalidScimResourceException; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceNotFoundException; import org.cloudfoundry.identity.uaa.scim.test.TestUtils; import org.cloudfoundry.identity.uaa.util.beans.DbUtils; import org.cloudfoundry.identity.uaa.util.TimeServiceImpl; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.cloudfoundry.identity.uaa.zone.JdbcIdentityZoneProvisioning; import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; import org.cloudfoundry.identity.uaa.zone.ZoneManagementScopes; import org.cloudfoundry.identity.uaa.zone.event.IdentityZoneModifiedEvent; @@ -87,6 +89,7 @@ void initJdbcScimGroupProvisioningTests() throws SQLException { zone.setId(zoneId); IdentityZoneHolder.set(zone); IdentityZoneHolder.get().getConfig().getUserConfig().setDefaultGroups(new ArrayList<>()); + IdentityZoneHolder.get().getConfig().getUserConfig().setAllowedGroups(null); validateGroupCountInZone(0, zoneId); @@ -106,6 +109,7 @@ void initJdbcScimGroupProvisioningTests() throws SQLException { new JdbcScimGroupExternalMembershipManager(jdbcTemplate, dbUtils); jdbcScimGroupExternalMembershipManager.setScimGroupProvisioning(dao); dao.setJdbcScimGroupExternalMembershipManager(jdbcScimGroupExternalMembershipManager); + dao.setJdbcIdentityZoneProvisioning(new JdbcIdentityZoneProvisioning(jdbcTemplate)); g1Id = "g1"; g2Id = "g2"; @@ -294,6 +298,30 @@ void canCreateAndGetGroupWithQuotes() { assertEquals(g.getId(), same.getId()); } + @Test + void cannotCreateNotAllowedGroup() { + IdentityZoneHolder.get().getConfig().getUserConfig().setAllowedGroups(Arrays.asList("allowedGroup")); + assertThrowsWithMessageThat( + InvalidScimResourceException.class, + () -> internalCreateGroup("notAllowedGroup"), + containsString("is not allowed") + ); + } + + @Test + void cannotUpdateNotAllowedGroup() { + IdentityZoneHolder.get().getConfig().getUserConfig().setAllowedGroups(Arrays.asList("allowedGroup")); + ScimGroup g = dao.retrieve(g1Id, zoneId); + g.setDisplayName("notAllowedGroup"); + g.setDescription("description-update"); + try { + dao.update(g1Id, g, zoneId); + fail(); + } catch(InvalidScimResourceException e) { + assertTrue(e.getMessage().contains("is not allowed")); + } + } + @Test void canUpdateGroup() { ScimGroup g = dao.retrieve(g1Id, zoneId); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabaseTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabaseTests.java index 11a07316da2..f270caf291e 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabaseTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabaseTests.java @@ -116,6 +116,7 @@ private static void setUpIdentityZone(IdentityZoneManager mockIdentityZoneManage when(mockIdentityZone.getConfig()).thenReturn(mockIdentityZoneConfiguration); when(mockIdentityZoneConfiguration.getUserConfig()).thenReturn(mockUserConfig); when(mockUserConfig.getDefaultGroups()).thenReturn(UserConfig.DEFAULT_ZONE_GROUPS); + when(mockUserConfig.resultingAllowedGroups()).thenReturn(null); // allow all groups } @AfterEach diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/zone/UserConfigValidatorTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/zone/UserConfigValidatorTest.java new file mode 100644 index 00000000000..8cc53698d3a --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/zone/UserConfigValidatorTest.java @@ -0,0 +1,34 @@ +package org.cloudfoundry.identity.uaa.zone; + +import org.junit.Test; + +import java.util.Collections; + + +public class UserConfigValidatorTest { + + @Test + public void testDefaultConfig() throws InvalidIdentityZoneConfigurationException { + UserConfigValidator.validate(new UserConfig()); // defaultGroups not empty, allowedGroups is null + } + + @Test + public void testNullConfig() throws InvalidIdentityZoneConfigurationException { + UserConfigValidator.validate(null); + } + + @Test + public void testAllowedGroupsEmpty() throws InvalidIdentityZoneConfigurationException { + UserConfig userConfig = new UserConfig(); + userConfig.setAllowedGroups(Collections.emptyList()); + UserConfigValidator.validate(userConfig); + } + + @Test(expected = InvalidIdentityZoneConfigurationException.class) + public void testNoGroupsAllowed() throws InvalidIdentityZoneConfigurationException { + UserConfig userConfig = new UserConfig(); + userConfig.setDefaultGroups(Collections.emptyList()); + userConfig.setAllowedGroups(Collections.emptyList()); // no groups allowed + UserConfigValidator.validate(userConfig); + } +} \ No newline at end of file diff --git a/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml index 7014e090cea..90626f42280 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml @@ -112,6 +112,7 @@ + allowedGroups = List.of(DELETE_ME, CF_DEV, CF_MGR, CFID); + private final String groupEndpoint = "/Groups"; private final String userEndpoint = "/Users"; @@ -207,6 +215,93 @@ public void createGroupSucceeds() { assertEquals(g1, g2); } + @Test + public void createAllowedGroupSucceeds() throws URISyntaxException { + String testZoneId = "testzone1"; + assertTrue("Expected testzone1.localhost and testzone2.localhost to resolve to 127.0.0.1", doesSupportZoneDNS()); + String adminToken = IntegrationTestUtils.getClientCredentialsToken(serverRunning.getBaseUrl(), "admin", "adminsecret"); + IdentityZoneConfiguration config = new IdentityZoneConfiguration(); + config.getUserConfig().setAllowedGroups(allowedGroups); + String zoneUrl = serverRunning.getBaseUrl().replace("localhost", testZoneId + ".localhost"); + String inZoneAdminToken = IntegrationTestUtils.createClientAdminTokenInZone(serverRunning.getBaseUrl(), adminToken, testZoneId, config); + ScimGroup g1 = new ScimGroup(null, CFID, testZoneId); + // Check we can GET the group + ScimGroup g2 = IntegrationTestUtils.createOrUpdateGroup(inZoneAdminToken, null, zoneUrl, g1); + assertEquals(g1.getDisplayName(), g2.getDisplayName()); + assertEquals(g1.getDisplayName(), IntegrationTestUtils.getGroup(inZoneAdminToken, null, zoneUrl, g1.getDisplayName()).getDisplayName()); + IntegrationTestUtils.deleteZone(serverRunning.getBaseUrl(), testZoneId, adminToken); + } + + @Test + public void createNotAllowedGroupFailsCorrectly() throws URISyntaxException { + String testZoneId = "testzone1"; + assertTrue("Expected testzone1.localhost and testzone2.localhost to resolve to 127.0.0.1", doesSupportZoneDNS()); + final String NOT_ALLOWED = "not_allowed_" + new RandomValueStringGenerator().generate().toLowerCase(); + String adminToken = IntegrationTestUtils.getClientCredentialsToken(serverRunning.getBaseUrl(), "admin", "adminsecret"); + ScimGroup g1 = new ScimGroup(null, NOT_ALLOWED, testZoneId); + IdentityZoneConfiguration config = new IdentityZoneConfiguration(); + config.getUserConfig().setAllowedGroups(allowedGroups); + String zoneUrl = serverRunning.getBaseUrl().replace("localhost", testZoneId + ".localhost"); + String inZoneAdminToken = IntegrationTestUtils.createClientAdminTokenInZone(serverRunning.getBaseUrl(), adminToken, testZoneId, config); + RestTemplate template = new RestTemplate(); + HttpEntity entity = new HttpEntity<>(JsonUtils.writeValueAsBytes(g1), IntegrationTestUtils.getAuthenticatedHeaders(inZoneAdminToken)); + try { + template.exchange(zoneUrl + "/Groups", HttpMethod.POST, entity, HashMap.class); + fail("must fail"); + } catch (HttpClientErrorException e) { + assertTrue(e.getStatusCode().is4xxClientError()); + assertEquals(400, e.getRawStatusCode()); + assertThat(e.getMessage(), + containsString("The group with displayName: "+ g1.getDisplayName() +" is not allowed in Identity Zone " + testZoneId)); + } finally { + IntegrationTestUtils.deleteZone(serverRunning.getBaseUrl(), testZoneId, adminToken); + } + } + + @Test + public void relyOnDefaultGroupsShouldAllowedGroupSucceed() throws URISyntaxException { + String testZoneId = "testzone1"; + assertTrue("Expected testzone1.localhost and testzone2.localhost to resolve to 127.0.0.1", doesSupportZoneDNS()); + String adminToken = IntegrationTestUtils.getClientCredentialsToken(serverRunning.getBaseUrl(), "admin", "adminsecret"); + IdentityZoneConfiguration config = new IdentityZoneConfiguration(); + config.getUserConfig().setAllowedGroups(List.of()); + config.getUserConfig().setDefaultGroups(defaultGroups); + String zoneUrl = serverRunning.getBaseUrl().replace("localhost", testZoneId + ".localhost"); + String inZoneAdminToken = IntegrationTestUtils.createClientAdminTokenInZone(serverRunning.getBaseUrl(), adminToken, testZoneId, config); + ScimGroup ccRead = new ScimGroup(null, "cloud_controller_service_permissions.read", testZoneId); + ScimGroup g1 = IntegrationTestUtils.createGroup(inZoneAdminToken, null, zoneUrl, ccRead); + // Check we can GET the group + ScimGroup g2 = IntegrationTestUtils.createOrUpdateGroup(inZoneAdminToken, null, zoneUrl, g1); + assertEquals("cloud_controller_service_permissions.read", g2.getDisplayName()); + assertEquals("cloud_controller_service_permissions.read", IntegrationTestUtils.getGroup(inZoneAdminToken, null, zoneUrl, g1.getDisplayName()).getDisplayName()); + IntegrationTestUtils.deleteZone(serverRunning.getBaseUrl(), testZoneId, adminToken); + } + + @Test + public void changeDefaultGroupsAllowedGroupsUsageShouldSucceed() throws URISyntaxException { + String testZoneId = "testzone1"; + assertTrue("Expected testzone1.localhost and testzone2.localhost to resolve to 127.0.0.1", doesSupportZoneDNS()); + String adminToken = IntegrationTestUtils.getClientCredentialsToken(serverRunning.getBaseUrl(), "admin", "adminsecret"); + IdentityZoneConfiguration config = new IdentityZoneConfiguration(); + final String ALLOWED = "allowed_" + new RandomValueStringGenerator().generate().toLowerCase(); + List newDefaultGroups = new ArrayList(defaultGroups); + newDefaultGroups.add(ALLOWED); + config.getUserConfig().setAllowedGroups(List.of()); + config.getUserConfig().setDefaultGroups(newDefaultGroups); + String zoneUrl = serverRunning.getBaseUrl().replace("localhost", testZoneId + ".localhost"); + String inZoneAdminToken = IntegrationTestUtils.createClientAdminTokenInZone(serverRunning.getBaseUrl(), adminToken, testZoneId, config); + RestTemplate template = new RestTemplate(); + ScimGroup g1 = new ScimGroup(null,ALLOWED, testZoneId); + HttpEntity entity = new HttpEntity<>(JsonUtils.writeValueAsBytes(g1), IntegrationTestUtils.getAuthenticatedHeaders(inZoneAdminToken)); + try { + template.exchange(zoneUrl + "/Groups", HttpMethod.POST, entity, HashMap.class); + } catch (Exception e) { + fail("must not fail"); + } finally { + IntegrationTestUtils.deleteZone(serverRunning.getBaseUrl(), testZoneId, adminToken); + } + } + @Test public void createGroupWithMembersSucceeds() { ScimGroup g1 = createGroup(CFID, JOEL, DALE, VIDYA); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java index 048cfffb0b7..ab05a136118 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java @@ -1569,4 +1569,24 @@ public StatelessRequestFactory() { } } + public static HttpHeaders getAuthenticatedHeaders(String token) { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + token); + return headers; + } + + public static String createClientAdminTokenInZone(String baseUrl, String uaaAdminToken, String zoneId, IdentityZoneConfiguration config) { + RestTemplate identityClient = getClientCredentialsTemplate(getClientCredentialsResource(baseUrl, + new String[] { "zones.write", "zones.read", "scim.zones" }, "identity", "identitysecret")); + createZoneOrUpdateSubdomain(identityClient, baseUrl, zoneId, zoneId, config); + String zoneUrl = baseUrl.replace("localhost", zoneId + ".localhost"); + BaseClientDetails zoneClient = new BaseClientDetails("admin-client-in-zone", null, "openid", + "authorization_code,client_credentials", "uaa.admin,scim.read,scim.write,zones.testzone1.admin ", zoneUrl); + zoneClient.setClientSecret("admin-secret-in-zone"); + createOrUpdateClient(uaaAdminToken, baseUrl, zoneId, zoneClient); + return getClientCredentialsToken(zoneUrl, "admin-client-in-zone", "admin-secret-in-zone"); + } + } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointDocs.java index f58d568267f..7b512fd4e0d 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointDocs.java @@ -156,6 +156,7 @@ class IdentityZoneEndpointDocs extends EndpointDocs { private static final String SAML_ACTIVE_KEY_ID_DESC = "The ID of the key that should be used for signing metadata and assertions."; private static final String DEFAULT_ZONE_GROUPS_DESC = "Default groups each user in the zone inherits."; + private static final String ALLOWED_ZONE_GROUPS_DESC = "Allowed groups in the zone. Defaults to null (all groups allowed)"; private static final String SERVICE_PROVIDER_ID = "cloudfoundry-saml-login"; private static final String MFA_CONFIG_ENABLED_DESC = "Set `true` to enable Multi-factor Authentication (MFA) for the current zone. Defaults to `false`"; private static final String MFA_CONFIG_PROVIDER_NAME_DESC = "The unique `name` of the MFA provider to use for this zone."; @@ -307,6 +308,7 @@ void createIdentityZone() throws Exception { fieldWithPath("config.corsPolicy.defaultConfiguration.maxAge").description(CORS_XHR_MAXAGE_DESC).attributes(key("constraints").value("Optional")), fieldWithPath("config.userConfig.defaultGroups").description(DEFAULT_ZONE_GROUPS_DESC).attributes(key("constraints").value("Optional")), + fieldWithPath("config.userConfig.allowedGroups").description(ALLOWED_ZONE_GROUPS_DESC).attributes(key("constraints").value("Optional")).optional().type(ARRAY), fieldWithPath("config.mfaConfig.enabled").description(MFA_CONFIG_ENABLED_DESC).attributes(key("constraints").value("Optional")), fieldWithPath("config.mfaConfig.providerName").description(MFA_CONFIG_PROVIDER_NAME_DESC).attributes(key("constraints").value("Required when `config.mfaConfig.enabled` is `true`")).optional().type(STRING), @@ -470,6 +472,7 @@ void getAllIdentityZones() throws Exception { fieldWithPath("[].config.corsPolicy.defaultConfiguration.maxAge").optional().description(CORS_XHR_MAXAGE_DESC).attributes(key("constraints").value("Optional")), fieldWithPath("[].config.userConfig.defaultGroups").description(DEFAULT_ZONE_GROUPS_DESC).attributes(key("constraints").value("Optional")), + fieldWithPath("[].config.userConfig.allowedGroups").description(ALLOWED_ZONE_GROUPS_DESC).attributes(key("constraints").value("Optional")).optional().type(ARRAY), fieldWithPath("[].config.mfaConfig.enabled").optional().description(MFA_CONFIG_ENABLED_DESC).attributes(key("constraints").value("Optional")), fieldWithPath("[].config.mfaConfig.providerName").optional().description(MFA_CONFIG_PROVIDER_NAME_DESC).attributes(key("constraints").value("Required when `config.mfaConfig.enabled` is `true`")).optional().type(STRING), @@ -617,6 +620,7 @@ void updateIdentityZone() throws Exception { fieldWithPath("config.corsPolicy.defaultConfiguration.maxAge").description(CORS_XHR_MAXAGE_DESC).attributes(key("constraints").value("Optional")), fieldWithPath("config.userConfig.defaultGroups").description(DEFAULT_ZONE_GROUPS_DESC).attributes(key("constraints").value("Optional")), + fieldWithPath("config.userConfig.allowedGroups").description(ALLOWED_ZONE_GROUPS_DESC).attributes(key("constraints").value("Optional")).optional().type(ARRAY), fieldWithPath("config.mfaConfig.enabled").description(MFA_CONFIG_ENABLED_DESC).attributes(key("constraints").value("Optional")), fieldWithPath("config.mfaConfig.providerName").description(MFA_CONFIG_PROVIDER_NAME_DESC).attributes(key("constraints").value("Required when `config.mfaConfig.enabled` is `true`")).optional().type(STRING), @@ -801,6 +805,7 @@ private Snippet getResponseFields() { fieldWithPath("config.corsPolicy.xhrConfiguration.maxAge").description(CORS_XHR_MAXAGE_DESC), fieldWithPath("config.userConfig.defaultGroups").description(DEFAULT_ZONE_GROUPS_DESC), + fieldWithPath("config.userConfig.allowedGroups").description(ALLOWED_ZONE_GROUPS_DESC).optional().type(ARRAY), fieldWithPath("config.mfaConfig.enabled").description(MFA_CONFIG_ENABLED_DESC), fieldWithPath("config.mfaConfig.providerName").description(MFA_CONFIG_PROVIDER_NAME_DESC).optional().type(STRING), diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java index 6ef61f3a73e..c29d1be61bc 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java @@ -507,6 +507,26 @@ void createZoneWithNoSubdomainFailsWithUnprocessableEntity() throws Exception { assertEquals(0, zoneModifiedEventListener.getEventCount()); } + @Test + void createZoneWithNoAllowedGroupsFailsWithUnprocessableEntity() throws Exception { + String id = generator.generate(); + IdentityZone zone = this.createSimpleIdentityZone(id); + zone.getConfig().getUserConfig().setDefaultGroups(Collections.emptyList()); + zone.getConfig().getUserConfig().setAllowedGroups(Collections.emptyList()); // no groups allowed + + mockMvc.perform( + post("/identity-zones") + .header("Authorization", "Bearer " + identityClientToken) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(zone))) + .andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.error").value("invalid_identity_zone")) + .andExpect(jsonPath("$.error_description").value("The identity zone details are invalid. " + + "The zone configuration is invalid. At least one group must be allowed")); + + assertEquals(0, zoneModifiedEventListener.getEventCount()); + } + @Test void testCreateZoneInsufficientScope() throws Exception { String id = new RandomValueStringGenerator().generate();