From decfc35ac3b1fa24ee1aa7ba3e3fb27a8222625e Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 21 Apr 2022 11:56:14 -0400 Subject: [PATCH 001/182] chore(dependencies): Autobump korkVersion (#1547) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9ce8e51188..f11fce205e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.30.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.138.0 +korkVersion=7.139.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.23.0 From 1b86cb89dcc010b3faf500c6f14242a95254f4a6 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 21 Apr 2022 10:35:08 -0700 Subject: [PATCH 002/182] feat(tasks): Allow max polling of task status to be set (#1546) * feat(tasks): Allow max polling of task status to be set * fix(typo): Fix typo on service * feat(service): Switch to config properties and make time between polls also configurable * fix(spring): Fixed camel case prefix on config properties * fix(properties): Move properties to a new config file * fix(duplicate: Remove extraaneous component annotation * chore(cleanup): remove blank space --- .../gate/config/TaskServiceProperties.java | 29 ++++++++++ .../spinnaker/gate/services/TaskService.java | 31 +++++------ .../gate/services/TaskServiceTest.java | 53 +++++++++++++++++++ 3 files changed, 95 insertions(+), 18 deletions(-) create mode 100644 gate-core/src/main/java/com/netflix/spinnaker/gate/config/TaskServiceProperties.java create mode 100644 gate-core/src/test/java/com/netflix/spinnaker/gate/services/TaskServiceTest.java diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/config/TaskServiceProperties.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/config/TaskServiceProperties.java new file mode 100644 index 0000000000..66efa0c2f9 --- /dev/null +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/config/TaskServiceProperties.java @@ -0,0 +1,29 @@ +/* + * Copyright 2022 Netflix, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties("task-service") +@Data +public class TaskServiceProperties { + private int maxNumberOfPolls = 32; + private int defaultIntervalBetweenPolls = 1000; +} diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/TaskService.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/TaskService.java index aa584f5174..8b09b5bd8f 100644 --- a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/TaskService.java +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/TaskService.java @@ -16,27 +16,35 @@ package com.netflix.spinnaker.gate.services; +import com.netflix.spinnaker.gate.config.TaskServiceProperties; import com.netflix.spinnaker.gate.services.internal.ClouddriverServiceSelector; import com.netflix.spinnaker.gate.services.internal.OrcaServiceSelector; import com.netflix.spinnaker.security.AuthenticatedRequest; import java.util.*; +import lombok.Data; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service +@Data public class TaskService { private final Logger log = LoggerFactory.getLogger(getClass()); private OrcaServiceSelector orcaServiceSelector; private ClouddriverServiceSelector clouddriverServiceSelector; + private TaskServiceProperties taskServiceProperties; + @Autowired public TaskService( OrcaServiceSelector orcaServiceSelector, - ClouddriverServiceSelector clouddriverServiceSelector) { + ClouddriverServiceSelector clouddriverServiceSelector, + TaskServiceProperties taskServiceProperties) { this.orcaServiceSelector = orcaServiceSelector; this.clouddriverServiceSelector = clouddriverServiceSelector; + this.taskServiceProperties = taskServiceProperties; } public Map create(Map body) { @@ -124,7 +132,10 @@ public Map createAndWaitForCompletion(Map body, int maxPolls) { } public Map createAndWaitForCompletion(Map body) { - return createAndWaitForCompletion(body, 32, 1000); + return createAndWaitForCompletion( + body, + taskServiceProperties.getMaxNumberOfPolls(), + taskServiceProperties.getDefaultIntervalBetweenPolls()); } /** @deprecated This pipeline operation does not belong here. */ @@ -149,20 +160,4 @@ public void setApplicationForTask(String id) { log.error("Error loading execution {} from orca", id, e); } } - - public OrcaServiceSelector getOrcaServiceSelector() { - return orcaServiceSelector; - } - - public void setOrcaServiceSelector(OrcaServiceSelector orcaServiceSelector) { - this.orcaServiceSelector = orcaServiceSelector; - } - - public ClouddriverServiceSelector getClouddriverServiceSelector() { - return clouddriverServiceSelector; - } - - public void setClouddriverServiceSelector(ClouddriverServiceSelector clouddriverServiceSelector) { - this.clouddriverServiceSelector = clouddriverServiceSelector; - } } diff --git a/gate-core/src/test/java/com/netflix/spinnaker/gate/services/TaskServiceTest.java b/gate-core/src/test/java/com/netflix/spinnaker/gate/services/TaskServiceTest.java new file mode 100644 index 0000000000..3e7e21bddd --- /dev/null +++ b/gate-core/src/test/java/com/netflix/spinnaker/gate/services/TaskServiceTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2022 Netflix, Inc. + * + * 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. + */ +package com.netflix.spinnaker.gate.services; + +import static org.mockito.Mockito.*; + +import com.netflix.spinnaker.gate.config.TaskServiceProperties; +import com.netflix.spinnaker.gate.services.internal.ClouddriverServiceSelector; +import com.netflix.spinnaker.gate.services.internal.OrcaService; +import com.netflix.spinnaker.gate.services.internal.OrcaServiceSelector; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = {TaskService.class, TaskServiceProperties.class}) +public class TaskServiceTest { + + @MockBean private OrcaServiceSelector selector; + @MockBean private ClouddriverServiceSelector clouddriverServiceSelector; + @MockBean private OrcaService orcaService; + + @Autowired TaskService taskService; + + @Test + public void callAsManyTimesAsSet() { + Map operation = new LinkedHashMap(); + + Map task = Map.of("ref", "apps/bob/someRandomId"); + when(selector.select()).thenReturn(orcaService); + when(orcaService.doOperation(operation)).thenReturn(task); + taskService.createAndWaitForCompletion(operation, 32, 1); + verify(orcaService, times(32)).getTask("someRandomId"); + } +} From 2a07109ab0baa3a60d322d6203037692daeb2576 Mon Sep 17 00:00:00 2001 From: kskewes-sf <96093759+kskewes-sf@users.noreply.github.com> Date: Wed, 27 Apr 2022 17:02:05 +1200 Subject: [PATCH 003/182] fix(ci): fetch previous tag from git instead of API (#1551) Previously the release info script would query the GitHub HTTP API and retrieve the latest "Release" (git tag) published. The latest "Release" could (likely) be for a different `release-*` branch or even `master` rather than the previous tag on the same branch as the new tag. This resulted in "Release" changelogs containing commits that dont exist in the tags history. By instead querying git itself we can fetch the previous tag in history and then generate an accurate changelog, eg: between `v1.7.4` and `v1.7.3` rather than potentially `v1.7.4` and `v1.10.0`. Run script through `shellcheck` and `shfmt`. --- .github/workflows/release_info.sh | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release_info.sh b/.github/workflows/release_info.sh index 0cadf01b76..3c3a158aa5 100755 --- a/.github/workflows/release_info.sh +++ b/.github/workflows/release_info.sh @@ -1,20 +1,24 @@ #!/bin/bash -x -# Only look to the latest release to determine the previous tag -- this allows us to skip unsupported tag formats (like `version-1.0.0`) -export PREVIOUS_TAG=`curl --silent "https://api.github.com/repos/$1/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'` -echo "PREVIOUS_TAG=$PREVIOUS_TAG" -export NEW_TAG=${GITHUB_REF/refs\/tags\//} +NEW_TAG=${GITHUB_REF/refs\/tags\//} +export NEW_TAG echo "NEW_TAG=$NEW_TAG" -export CHANGELOG=`git log $NEW_TAG...$PREVIOUS_TAG --oneline` +# Glob match previous tags which should be format v1.2.3. Avoids Deck's npm tagging. +PREVIOUS_TAG=$(git describe --abbrev=0 --tags "${NEW_TAG}"^ --match 'v[0-9]*') +export PREVIOUS_TAG +echo "PREVIOUS_TAG=$PREVIOUS_TAG" +CHANGELOG=$(git log "$NEW_TAG"..."$PREVIOUS_TAG" --oneline) +export CHANGELOG echo "CHANGELOG=$CHANGELOG" -#Format the changelog so it's markdown compatible +# Format the changelog so it's markdown compatible CHANGELOG="${CHANGELOG//$'%'/%25}" CHANGELOG="${CHANGELOG//$'\n'/%0A}" CHANGELOG="${CHANGELOG//$'\r'/%0D}" # If the previous release tag is the same as this tag the user likely cut a release (and in the process created a tag), which means we can skip the need to create a release -export SKIP_RELEASE=`[[ "$PREVIOUS_TAG" = "$NEW_TAG" ]] && echo "true" || echo "false"` +SKIP_RELEASE=$([[ "$PREVIOUS_TAG" = "$NEW_TAG" ]] && echo "true" || echo "false") +export SKIP_RELEASE # https://github.com/fsaintjacques/semver-tool/blob/master/src/semver#L5-L14 NAT='0|[1-9][0-9]*' @@ -28,8 +32,10 @@ SEMVER_REGEX="\ (\\+${FIELD}(\\.${FIELD})*)?$" # Used in downstream steps to determine if the release should be marked as a "prerelease" and if the build should build candidate release artifacts -export IS_CANDIDATE=`[[ $NEW_TAG =~ $SEMVER_REGEX && ! -z ${BASH_REMATCH[4]} ]] && echo "true" || echo "false"` +IS_CANDIDATE=$([[ $NEW_TAG =~ $SEMVER_REGEX && -n ${BASH_REMATCH[4]} ]] && echo "true" || echo "false") +export IS_CANDIDATE # This is the version string we will pass to the build, trim off leading 'v' if present -export RELEASE_VERSION=`[[ $NEW_TAG =~ $SEMVER_REGEX ]] && echo "${NEW_TAG:1}" || echo "${NEW_TAG}"` +RELEASE_VERSION=$([[ $NEW_TAG =~ $SEMVER_REGEX ]] && echo "${NEW_TAG:1}" || echo "${NEW_TAG}") +export RELEASE_VERSION echo "RELEASE_VERSION=$RELEASE_VERSION" From a0044b8408cd0b8cbe971dab6f462425ece670b4 Mon Sep 17 00:00:00 2001 From: kskewes-sf <96093759+kskewes-sf@users.noreply.github.com> Date: Wed, 27 Apr 2022 17:07:06 +1200 Subject: [PATCH 004/182] chore(ci): Mergify - merge Autobumps on release-* (#1552) And request review for failed autobump builds on master and release branches. Lastly, remove Travis CI related rule. It's not used. Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .mergify.yml | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index 3b41473867..876d88127a 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -28,20 +28,6 @@ pull_request_rules: name: default label: add: ["auto merged"] - # This rule exists to handle release branches that are still building using Travis CI instead of - # using Github actions. It can be deleted once all active release branches are running Github actions. - - name: Automatically merge release branch changes on Travis CI success and release manager review - conditions: - - base~=^release- - - status-success=continuous-integration/travis-ci/pr - - "label=ready to merge" - - "approved-reviews-by=@release-managers" - actions: - queue: - method: squash - name: default - label: - add: ["auto merged"] - name: Automatically merge PRs from maintainers on CI success and review conditions: - base=master @@ -56,7 +42,7 @@ pull_request_rules: add: ["auto merged"] - name: Automatically merge autobump PRs on CI success conditions: - - base=master + - base~=^(master|release-) - status-success=build - "label~=autobump-*" - "author:spinnakerbot" @@ -68,7 +54,7 @@ pull_request_rules: add: ["auto merged"] - name: Request reviews for autobump PRs on CI failure conditions: - - base=master + - base~=^(master|release-) - status-failure=build - "label~=autobump-*" - base=master From 98c298f27e4c42fb24c3604c6b74682d8f1e2d6d Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 27 May 2022 11:15:52 -0400 Subject: [PATCH 005/182] chore(dependencies): Autobump korkVersion (#1557) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f11fce205e..d84e844215 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.30.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.139.0 +korkVersion=7.140.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.23.0 From 24eb976388b3cbfc5f85f095841f9e987e100d51 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 6 Jun 2022 13:05:14 -0400 Subject: [PATCH 006/182] chore(dependencies): Autobump korkVersion (#1558) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d84e844215..de198a453c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.30.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.140.0 +korkVersion=7.141.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.23.0 From f0b0adc4a0dbdf032a52844feff7e4a7e04ced9a Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 14 Jun 2022 14:37:46 -0400 Subject: [PATCH 007/182] chore(dependencies): Autobump korkVersion (#1559) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index de198a453c..ab20dbcefd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.30.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.141.0 +korkVersion=7.142.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.23.0 From 0e438e253524d8354c7f6f6c393b1aab3e97c64e Mon Sep 17 00:00:00 2001 From: Jalander Ramagiri <97948659+rjalander@users.noreply.github.com> Date: Tue, 12 Jul 2022 13:33:00 +0100 Subject: [PATCH 008/182] fix(restart-pipeline) : CheckPrecondition doesn't evaluate expression correctly when upstream stages get restarted (#1560) * Update context with PipelineConfig * adding spacing and formatting as per review --- .../gate/controllers/PipelineController.groovy | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/PipelineController.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/PipelineController.groovy index 418cfa280b..9fced36317 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/PipelineController.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/PipelineController.groovy @@ -203,6 +203,22 @@ class PipelineController { @ApiOperation(value = "Restart a stage execution", response = HashMap.class) @PutMapping("/{id}/stages/{stageId}/restart") Map restartStage(@PathVariable("id") String id, @PathVariable("stageId") String stageId, @RequestBody Map context) { + Map pipelineMap = getPipeline(id) + + String pipelineName = pipelineMap.get("name"); + String application = pipelineMap.get("application"); + + List pipelineConfigs = front50Service.getPipelineConfigsForApplication(application, true) + + if (pipelineConfigs!=null && !pipelineConfigs.isEmpty()){ + Optional filterResult = pipelineConfigs.stream() + .filter({pipeline -> ((String) pipeline.get("name")) != null && ((String) pipeline.get("name")).trim().equalsIgnoreCase(pipelineName)}) + .findFirst() + if (filterResult.isPresent()){ + context = filterResult.get() + } + } + pipelineService.restartPipelineStage(id, stageId, context) } From f69a9014d7a861b2d7f27ea75952fdf10c685e88 Mon Sep 17 00:00:00 2001 From: Patrik Greco Date: Wed, 20 Jul 2022 20:52:05 +0200 Subject: [PATCH 009/182] chore(build): Gradle 7 compatibility (#1561) This change makes the project compatible with Gradle 7 --- gate-web/gate-web.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gate-web/gate-web.gradle b/gate-web/gate-web.gradle index 7573ae62ea..f50cdb0e98 100644 --- a/gate-web/gate-web.gradle +++ b/gate-web/gate-web.gradle @@ -42,8 +42,8 @@ dependencies { implementation "com.netflix.frigga:frigga" implementation "redis.clients:jedis" - implementation 'commons-io:commons-io' - implementation 'org.springframework.session:spring-session-data-redis' + implementation "commons-io:commons-io" + implementation "org.springframework.session:spring-session-data-redis" implementation "de.huxhorn.sulky:de.huxhorn.sulky.ulid" implementation "org.apache.commons:commons-lang3" @@ -77,7 +77,7 @@ dependencies { // Add each included authz provider as a runtime dependency gradle.includedProviderProjects.each { - runtime project(it) + runtimeOnly project(it) } } From 419f468ab15ffb8a54e9890f2072c04264df1a50 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 27 Jul 2022 19:34:03 -0400 Subject: [PATCH 010/182] chore(dependencies): Autobump korkVersion (#1562) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ab20dbcefd..d6276f7e11 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.30.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.142.0 +korkVersion=7.144.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.23.0 From 86e6684e117156cfdbaf1cd9000653dcf5f97887 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 1 Aug 2022 17:11:24 -0400 Subject: [PATCH 011/182] chore(dependencies): Autobump fiatVersion (#1563) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d6276f7e11..c42449016a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.30.0 +fiatVersion=1.32.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.144.0 kotlinVersion=1.4.0 From d98ef197e4496a49b7a3072803b286360df5c16d Mon Sep 17 00:00:00 2001 From: Cameron Motevasselani Date: Tue, 2 Aug 2022 19:30:45 -0700 Subject: [PATCH 012/182] chore(dockerfile): upgrade to latest alpine image (#1564) Co-authored-by: Cameron Motevasselani --- Dockerfile.compile | 2 +- Dockerfile.slim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.compile b/Dockerfile.compile index dc53055801..d31a686faa 100644 --- a/Dockerfile.compile +++ b/Dockerfile.compile @@ -1,4 +1,4 @@ -FROM alpine:3.11 +FROM alpine:3.16 RUN apk add --update \ openjdk11 \ && rm -rf /var/cache/apk diff --git a/Dockerfile.slim b/Dockerfile.slim index 7b7845b919..6a412a1210 100644 --- a/Dockerfile.slim +++ b/Dockerfile.slim @@ -1,4 +1,4 @@ -FROM alpine:3.11 +FROM alpine:3.16 LABEL maintainer="sig-platform@spinnaker.io" RUN apk --no-cache add --update bash openjdk11-jre RUN addgroup -S -g 10111 spinnaker From 763aa281df55b2b24500185f46ed04e80559cbd9 Mon Sep 17 00:00:00 2001 From: Marcin Flis Date: Sun, 14 Aug 2022 00:28:59 +0200 Subject: [PATCH 013/182] chore(build): Build docker images for multiple architectures (#1567) Motivation: Allow running & experimenting with Spinnaker locally on Macbok M1. Relevant docs: https://github.com/docker/build-push-action/blob/master/docs/advanced/multi-platform.md Similar MR: https://github.com/spinnaker/rosco/pull/886 --- .github/workflows/build.yml | 10 ++++++++-- .github/workflows/pr.yml | 10 ++++++++-- .github/workflows/release.yml | 10 ++++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f396a4cd1..eac2ff9c41 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,10 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - uses: actions/setup-java@v2 with: java-version: 11 @@ -45,10 +49,11 @@ jobs: - name: Build and publish slim container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . file: Dockerfile.slim + platforms: linux/amd64,linux/arm64 push: true tags: | "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ github.ref_name }}-latest-unvalidated" @@ -58,10 +63,11 @@ jobs: - name: Build and publish ubuntu container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . file: Dockerfile.ubuntu + platforms: linux/amd64,linux/arm64 push: true tags: | "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ github.ref_name }}-latest-unvalidated-ubuntu" diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 41d84a9bcf..4a86f9c25a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,6 +13,10 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - uses: actions/setup-java@v2 with: java-version: 11 @@ -28,20 +32,22 @@ jobs: ORG_GRADLE_PROJECT_version: ${{ steps.build_variables.outputs.VERSION }} run: ./gradlew build ${{ steps.build_variables.outputs.REPO }}-web:installDist - name: Build slim container image - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . file: Dockerfile.slim + platforms: linux/amd64,linux/arm64 tags: | "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:latest" "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}" "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:latest-slim" "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-slim" - name: Build ubuntu container image - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . file: Dockerfile.ubuntu + platforms: linux/amd64,linux/arm64 tags: | "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:latest-ubuntu" "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-ubuntu" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b40b05013..baf5f14280 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,10 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - uses: actions/setup-java@v2 with: java-version: 11 @@ -82,10 +86,11 @@ jobs: - name: Build and publish slim container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . file: Dockerfile.slim + platforms: linux/amd64,linux/arm64 push: true tags: | "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.release_info.outputs.RELEASE_VERSION }}-unvalidated" @@ -94,10 +99,11 @@ jobs: - name: Build and publish ubuntu container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . file: Dockerfile.ubuntu + platforms: linux/amd64,linux/arm64 push: true tags: | "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.release_info.outputs.RELEASE_VERSION }}-unvalidated-ubuntu" From 9f09d3afad3493c7ade97a6c78c750c9b5f066fc Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 15 Aug 2022 15:02:24 -0400 Subject: [PATCH 014/182] chore(dependencies): Autobump korkVersion (#1568) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c42449016a..05b4da304c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.32.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.144.0 +korkVersion=7.145.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.23.0 From fe063a8b0e65755989ca940f61ae0d8da381735c Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 25 Aug 2022 15:33:31 -0400 Subject: [PATCH 015/182] chore(dependencies): Autobump fiatVersion (#1571) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 05b4da304c..cb99f371e4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.32.0 +fiatVersion=1.33.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.145.0 kotlinVersion=1.4.0 From 6d8d8a37ded1dd9ad8baf81f6fb43a8fff854086 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 12 Sep 2022 14:31:59 -0400 Subject: [PATCH 016/182] chore(dependencies): Autobump korkVersion (#1572) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cb99f371e4..9c9f909628 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.33.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.145.0 +korkVersion=7.146.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.23.0 From be5a24477e8715fca7a58aad3d7679bfb957aac2 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 13 Sep 2022 16:21:01 -0400 Subject: [PATCH 017/182] chore(dependencies): Autobump fiatVersion (#1573) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9c9f909628..73abcd569f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.33.0 +fiatVersion=1.34.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.146.0 kotlinVersion=1.4.0 From 1a539888d6280499ca3500e5127ff7316b4c3bab Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 14 Sep 2022 12:00:42 -0400 Subject: [PATCH 018/182] chore(dependencies): Autobump fiatVersion (#1574) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 73abcd569f..7644b2073a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.34.0 +fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.146.0 kotlinVersion=1.4.0 From 07d43384bc05abc0fa1ef30b350f90a55eb120b5 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 27 Sep 2022 14:05:33 -0400 Subject: [PATCH 019/182] chore(dependencies): Autobump korkVersion (#1575) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7644b2073a..5a2a9d3251 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.146.0 +korkVersion=7.147.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.23.0 From f82fbb582c84648910f509913b32283a2085acf1 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 3 Oct 2022 11:36:14 -0400 Subject: [PATCH 020/182] chore(dependencies): Autobump korkVersion (#1577) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5a2a9d3251..b9543ec632 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.147.0 +korkVersion=7.148.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.23.0 From c225d565c2146f467ac6fd4698f06dd84d2564bb Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 10 Oct 2022 13:48:22 -0400 Subject: [PATCH 021/182] chore(dependencies): Autobump korkVersion (#1578) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b9543ec632..e09392d86e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.148.0 +korkVersion=7.149.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.23.0 From d22379a28c286774da9d7f0542e43f6e0bbef2ba Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 19 Oct 2022 12:25:11 -0400 Subject: [PATCH 022/182] chore(dependencies): Autobump korkVersion (#1580) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e09392d86e..3e0efd9af4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.149.0 +korkVersion=7.150.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.23.0 From cc9d9a023ab7372d01d078ed7ba3c3bca209c3e2 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 18 Nov 2022 13:19:25 -0500 Subject: [PATCH 023/182] chore(dependencies): Autobump spinnakerGradleVersion (#1585) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3e0efd9af4..e5440056b4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.150.0 kotlinVersion=1.4.0 org.gradle.parallel=true -spinnakerGradleVersion=8.23.0 +spinnakerGradleVersion=8.24.0 targetJava11=true # To enable a composite reference to a project, set the From 94a844f387d727ab920af906b6104af976b2788d Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 18 Nov 2022 20:14:25 -0500 Subject: [PATCH 024/182] chore(dependencies): Autobump korkVersion (#1587) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e5440056b4..77e8223ee6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.150.0 +korkVersion=7.151.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.24.0 From 1f3a501f47ede202527d7ea175884cada2ac6401 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Sat, 19 Nov 2022 11:29:18 -0500 Subject: [PATCH 025/182] chore(dependencies): Autobump korkVersion (#1588) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 77e8223ee6..4ac6c9c4b0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.151.0 +korkVersion=7.152.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.24.0 From 507f0b8049db728536618bc945bc39ba674d02f0 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 24 Nov 2022 10:56:30 -0500 Subject: [PATCH 026/182] chore(dependencies): Autobump korkVersion (#1590) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4ac6c9c4b0..49538229c1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.152.0 +korkVersion=7.153.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.24.0 From 4f1438c92470870eeaa565e8712426f862686a0a Mon Sep 17 00:00:00 2001 From: Sandesh <30489233+j-sandy@users.noreply.github.com> Date: Thu, 8 Dec 2022 11:59:28 +0530 Subject: [PATCH 027/182] refactor(web): Clean up redundant spring property in gradle file (#1597) The properties spring.config.additional-location and spring.profiles.active are redundant in gate-web.gradle file. These properties are set by class com.netflix.spinnaker.kork.boot.DefaultPropertiesBuilder in com.netflix.spinnaker.gate.Main. So removing it from gradle file. --- gate-web/gate-web.gradle | 9 --------- 1 file changed, 9 deletions(-) diff --git a/gate-web/gate-web.gradle b/gate-web/gate-web.gradle index f50cdb0e98..1d6bcf3344 100644 --- a/gate-web/gate-web.gradle +++ b/gate-web/gate-web.gradle @@ -1,14 +1,5 @@ apply plugin: 'io.spinnaker.package' -ext { - springConfigLocation = System.getProperty('spring.config.additional-location', "${System.getProperty('user.home')}/.spinnaker/".toString()) - springProfiles = System.getProperty('spring.profiles.active', "test,local") -} - -run { - systemProperty('spring.config.additional-location', project.springConfigLocation) - systemProperty('spring.profiles.active', project.springProfiles) -} mainClassName = 'com.netflix.spinnaker.gate.Main' repositories { From dabd6548245cda73025ee405e6514157722976d7 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 8 Dec 2022 19:47:16 -0500 Subject: [PATCH 028/182] chore(dependencies): Autobump korkVersion (#1598) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 49538229c1..27414509ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.153.0 +korkVersion=7.154.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.24.0 From 9588d8caf5a74b8e17e1171dc206dcd8906e9a81 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 8 Dec 2022 20:52:47 -0500 Subject: [PATCH 029/182] chore(dependencies): Autobump korkVersion (#1599) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 27414509ee..35d0d5ea62 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.154.0 +korkVersion=7.155.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.24.0 From eac5aa169fafcb7f8efd965026ddd0d506cee31c Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 8 Dec 2022 21:58:05 -0500 Subject: [PATCH 030/182] chore(dependencies): Autobump korkVersion (#1600) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 35d0d5ea62..dabd98f7c8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.155.0 +korkVersion=7.156.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.24.0 From 58407492c5a6e2ad096d6ee2547e0e9d47bcd533 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 9 Dec 2022 01:00:44 -0500 Subject: [PATCH 031/182] chore(dependencies): Autobump korkVersion (#1601) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index dabd98f7c8..5f279fdf97 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.156.0 +korkVersion=7.157.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.24.0 From 3005eca1c6cf60cd25e237e449e471b84b8d5c51 Mon Sep 17 00:00:00 2001 From: David Byron <82477955+dbyron-sf@users.noreply.github.com> Date: Fri, 9 Dec 2022 10:36:12 -0800 Subject: [PATCH 032/182] chore(dependencies): remove dependency on groovy-all (#1602) with a specific goal to get org.testng:testng:7.4.0 out of shipping code, since it's vulnerable to CVE-2022-4065. --- build.gradle | 2 +- gate-oauth2/gate-oauth2.gradle | 1 + gate-web/gate-web.gradle | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c258b8376e..b6f145ebb8 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ allprojects { annotationProcessor "org.projectlombok:lombok" testAnnotationProcessor "org.projectlombok:lombok" - implementation "org.codehaus.groovy:groovy-all" + implementation "org.codehaus.groovy:groovy" implementation "net.logstash.logback:logstash-logback-encoder" implementation "org.jetbrains.kotlin:kotlin-reflect" diff --git a/gate-oauth2/gate-oauth2.gradle b/gate-oauth2/gate-oauth2.gradle index 53962e0588..91403e7ea9 100644 --- a/gate-oauth2/gate-oauth2.gradle +++ b/gate-oauth2/gate-oauth2.gradle @@ -4,6 +4,7 @@ dependencies { implementation "io.spinnaker.fiat:fiat-api:$fiatVersion" implementation "io.spinnaker.kork:kork-exceptions" implementation "io.spinnaker.kork:kork-security" + implementation "org.codehaus.groovy:groovy-json" implementation "org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure" implementation "org.springframework.session:spring-session-core" } diff --git a/gate-web/gate-web.gradle b/gate-web/gate-web.gradle index 1d6bcf3344..dd6638685a 100644 --- a/gate-web/gate-web.gradle +++ b/gate-web/gate-web.gradle @@ -34,6 +34,7 @@ dependencies { implementation "redis.clients:jedis" implementation "commons-io:commons-io" + implementation "org.codehaus.groovy:groovy-templates" implementation "org.springframework.session:spring-session-data-redis" implementation "de.huxhorn.sulky:de.huxhorn.sulky.ulid" implementation "org.apache.commons:commons-lang3" From 4017f88e90b46f1101b637b5e21395d6c9cf58fe Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 15 Dec 2022 11:52:10 -0500 Subject: [PATCH 033/182] chore(dependencies): Autobump korkVersion (#1603) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5f279fdf97..92134d940a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.157.0 +korkVersion=7.158.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.24.0 From 133a90d67b44c3bcf8105f9ffe7a00f6a760a0f8 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 21 Dec 2022 23:49:04 -0500 Subject: [PATCH 034/182] chore(dependencies): Autobump spinnakerGradleVersion (#1604) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 92134d940a..f5a95d917f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.158.0 kotlinVersion=1.4.0 org.gradle.parallel=true -spinnakerGradleVersion=8.24.0 +spinnakerGradleVersion=8.25.0 targetJava11=true # To enable a composite reference to a project, set the From 57432d8b2386c2b7fa24bd63f0426746e36d4a16 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 12 Jan 2023 16:47:48 -0500 Subject: [PATCH 035/182] chore(dependencies): Autobump korkVersion (#1605) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f5a95d917f..8eeeba6a7f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.158.0 +korkVersion=7.159.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.25.0 From 5daf7d5ca8302c3244ab967ec532e620c898af39 Mon Sep 17 00:00:00 2001 From: Sandesh <30489233+j-sandy@users.noreply.github.com> Date: Mon, 23 Jan 2023 22:40:37 +0530 Subject: [PATCH 036/182] chore(web): clean up for spring property setup (#1606) Using `com.netflix.spinnaker.kork.boot.DefaultPropertiesBuilder` class to setup spring and other related properties required for the spring boot application to start. --- gate-web/gate-web.gradle | 1 + .../groovy/com/netflix/spinnaker/gate/Main.groovy | 11 ++--------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/gate-web/gate-web.gradle b/gate-web/gate-web.gradle index dd6638685a..3e0fcafff4 100644 --- a/gate-web/gate-web.gradle +++ b/gate-web/gate-web.gradle @@ -28,6 +28,7 @@ dependencies { implementation "io.spinnaker.fiat:fiat-core:$fiatVersion" implementation "io.spinnaker.fiat:fiat-api:$fiatVersion" + implementation "io.spinnaker.kork:kork-config" implementation "io.spinnaker.kork:kork-plugins" implementation "io.spinnaker.kork:kork-web" implementation "com.netflix.frigga:frigga" diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/Main.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/Main.groovy index a7fab3f352..2fd1db765e 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/Main.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/Main.groovy @@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration import org.springframework.boot.builder.SpringApplicationBuilder import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.scheduling.annotation.EnableAsync +import com.netflix.spinnaker.kork.boot.DefaultPropertiesBuilder @EnableAsync @EnableConfigurationProperties @@ -41,15 +42,7 @@ import org.springframework.scheduling.annotation.EnableAsync ) class Main { - static final Map DEFAULT_PROPS = [ - 'netflix.environment': 'test', - 'netflix.account': '${netflix.environment}', - 'netflix.stack': 'test', - 'spring.config.additional-location': '${user.home}/.spinnaker/', - 'spring.application.name': 'gate', - 'spring.config.name': 'spinnaker,${spring.application.name}', - 'spring.profiles.active': '${netflix.environment},local' - ] + static final Map DEFAULT_PROPS = new DefaultPropertiesBuilder().property("spring.application.name", "gate").build() static void main(String... args) { new SpringApplicationBuilder().properties(DEFAULT_PROPS).sources(Main).run(args) From 05a2f39e7af9a93b63bb54e7bb44f0dfb82cb555 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 26 Jan 2023 17:02:02 -0500 Subject: [PATCH 037/182] chore(dependencies): Autobump korkVersion (#1607) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8eeeba6a7f..f516122847 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.159.0 +korkVersion=7.160.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.25.0 From 11d0862df93d7dc4032ddc2741e447ed12e77e86 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 30 Jan 2023 10:28:21 -0500 Subject: [PATCH 038/182] chore(dependencies): Autobump korkVersion (#1608) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f516122847..43d97be8e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.160.0 +korkVersion=7.160.1 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.25.0 From abf0fcfcb3059e320ada9871667efbae65893b45 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 30 Jan 2023 11:27:35 -0500 Subject: [PATCH 039/182] chore(dependencies): Autobump korkVersion (#1609) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 43d97be8e0..6a03a51683 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.160.1 +korkVersion=7.160.2 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.25.0 From dfb77de0db6043cc33f6eccab01ea6080f71490a Mon Sep 17 00:00:00 2001 From: Kim Choy <60162636+kchoy-sfdc@users.noreply.github.com> Date: Fri, 3 Feb 2023 15:10:40 -0800 Subject: [PATCH 040/182] feat(web): Add X-SPINNAKER-* optional data to HTTP response header (#1610) * test(web): Add unit test for RequestIdInterceptor * feat(web): Refactor RequestIdInterceptor to be able to set additional X-SPINNAKER header info besides request ID to response header, and renaming it to ResponseHeaderInterceptor to better reflect its functionality. The motivation for this enhancement is to allow better correlation of API interaction with users making these requests. A new configuration property "interceptors.responseHeader.fields" is added for configuring the X-SPINNAKER-* header fields to be added to the response header by the interceptor. This property takes a comma delimited string or yaml list of the header fields, for example to add X-SPINNAKER-REQUEST-ID and X-SPINNAKER-USER to the response header: interceptors.responseHeader.fields=X-SPINNAKER-REQUEST-ID,X-SPINNAKER-USER or in yaml form: interceptors: responseHeader: fields: - X-SPINNAKER-REQUEST-ID - X-SPINNAKER-USER When this configuration property is not defined, the interceptor defaults to adding X-SPINNAKER-REQUEST-ID field to the response, which is the current behavior. --- .../gate/config/GateWebConfig.groovy | 11 +- .../interceptors/RequestIdInterceptor.java | 38 --- .../ResponseHeaderInterceptor.java | 60 +++++ ...derInterceptorConfigurationProperties.java | 30 +++ ...nterceptorConfigurationPropertiesTest.java | 89 +++++++ .../ResponseHeaderInterceptorTest.java | 224 ++++++++++++++++++ 6 files changed, 411 insertions(+), 41 deletions(-) delete mode 100644 gate-web/src/main/groovy/com/netflix/spinnaker/gate/interceptors/RequestIdInterceptor.java create mode 100644 gate-web/src/main/groovy/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptor.java create mode 100644 gate-web/src/main/groovy/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorConfigurationProperties.java create mode 100644 gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorConfigurationPropertiesTest.java create mode 100644 gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorTest.java diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateWebConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateWebConfig.groovy index 00f63e1de3..199426b311 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateWebConfig.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateWebConfig.groovy @@ -19,13 +19,14 @@ package com.netflix.spinnaker.gate.config import com.netflix.spectator.api.Registry import com.netflix.spinnaker.gate.filters.ContentCachingFilter import com.netflix.spinnaker.gate.interceptors.RequestContextInterceptor -import com.netflix.spinnaker.gate.interceptors.RequestIdInterceptor - +import com.netflix.spinnaker.gate.interceptors.ResponseHeaderInterceptor +import com.netflix.spinnaker.gate.interceptors.ResponseHeaderInterceptorConfigurationProperties import com.netflix.spinnaker.gate.retrofit.UpstreamBadRequest import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService import com.netflix.spinnaker.kork.web.interceptors.MetricsInterceptor import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.ApplicationContext import org.springframework.context.annotation.Bean import org.springframework.context.annotation.ComponentScan @@ -45,6 +46,7 @@ import javax.servlet.http.HttpServletResponse @Configuration @ComponentScan +@EnableConfigurationProperties(ResponseHeaderInterceptorConfigurationProperties.class) public class GateWebConfig implements WebMvcConfigurer { @Autowired Registry registry @@ -58,6 +60,9 @@ public class GateWebConfig implements WebMvcConfigurer { @Value('${rate-limit.learning:true}') Boolean rateLimitLearningMode + @Autowired + ResponseHeaderInterceptorConfigurationProperties responseHeaderInterceptorConfigurationProperties + @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor( @@ -66,7 +71,7 @@ public class GateWebConfig implements WebMvcConfigurer { ) ) - registry.addInterceptor(new RequestIdInterceptor()) + registry.addInterceptor(new ResponseHeaderInterceptor(responseHeaderInterceptorConfigurationProperties)) registry.addInterceptor(new RequestContextInterceptor()) } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/interceptors/RequestIdInterceptor.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/interceptors/RequestIdInterceptor.java deleted file mode 100644 index af0c619f85..0000000000 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/interceptors/RequestIdInterceptor.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2017 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.interceptors; - -import static com.netflix.spinnaker.kork.common.Header.REQUEST_ID; - -import com.netflix.spinnaker.security.AuthenticatedRequest; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; - -/** - * Return value of SPINNAKER_REQUEST_ID (set via - * com.netflix.spinnaker.filters.AuthenticatedRequestFilter) to gate callers as a response header. - */ -public class RequestIdInterceptor extends HandlerInterceptorAdapter { - @Override - public boolean preHandle( - HttpServletRequest request, HttpServletResponse response, Object handler) { - AuthenticatedRequest.getSpinnakerRequestId() - .ifPresent(requestId -> response.setHeader(REQUEST_ID.getHeader(), requestId)); - return true; - } -} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptor.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptor.java new file mode 100644 index 0000000000..5245e1c81c --- /dev/null +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptor.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017 Netflix, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.interceptors; + +import static com.netflix.spinnaker.kork.common.Header.REQUEST_ID; + +import com.netflix.spinnaker.security.AuthenticatedRequest; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +/** + * Return values (e.g. X-SPINNAKER-*) stored in the AuthenticatedRequest (backed by MDC and set via + * com.netflix.spinnaker.filters.AuthenticatedRequestFilter) to gate callers as a response header. + * For X-SPINNAKER-REQUEST-ID, if its value is absent from AuthenticatedRequest, the value of + * X-SPINNAKER-EXECUTION-ID is returned as the request ID, or a UUID is generated and returned if + * X-SPINNAKER-EXECUTION-ID is also absent. For other fields, no values are returned if they are + * absent from AuthenticatedRequest. + */ +public class ResponseHeaderInterceptor extends HandlerInterceptorAdapter { + + private final ResponseHeaderInterceptorConfigurationProperties properties; + + public ResponseHeaderInterceptor(ResponseHeaderInterceptorConfigurationProperties properties) { + this.properties = properties; + } + + @Override + public boolean preHandle( + HttpServletRequest request, HttpServletResponse response, Object handler) { + for (String field : this.properties.getFields()) { + // getSpinnakerRequestId() contains logic to either return the spinnaker + // execution id or generate a new one if no current value exists in MDC, + // the generic get() does not contain such logic + // check whether we are processing the request id to make sure we call + // the right method to retain the above logic + Optional value = + field.equals(REQUEST_ID.getHeader()) + ? AuthenticatedRequest.getSpinnakerRequestId() + : AuthenticatedRequest.get(field); + value.ifPresent(v -> response.setHeader(field, v)); + } + return true; + } +} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorConfigurationProperties.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorConfigurationProperties.java new file mode 100644 index 0000000000..ab5dea55ed --- /dev/null +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorConfigurationProperties.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 Salesforce, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.interceptors; + +import com.netflix.spinnaker.kork.common.Header; +import java.util.List; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "interceptors.response-header") +public class ResponseHeaderInterceptorConfigurationProperties { + // default to having request id in response header, the original behavior + // before other fields are added + private List fields = List.of(Header.REQUEST_ID.getHeader()); +} diff --git a/gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorConfigurationPropertiesTest.java b/gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorConfigurationPropertiesTest.java new file mode 100644 index 0000000000..4879532bae --- /dev/null +++ b/gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorConfigurationPropertiesTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2022 Salesforce, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.interceptors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import com.netflix.spinnaker.kork.common.Header; +import org.junit.jupiter.api.Test; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +public class ResponseHeaderInterceptorConfigurationPropertiesTest { + + @Test + public void testResponseHeaderInterceptorSettingsDefault() { + ApplicationContextRunner runner = + new ApplicationContextRunner() + .withUserConfiguration(ResponseHeaderInterceptorTestConfiguration.class); + + runner.run( + ctx -> { + ResponseHeaderInterceptorConfigurationProperties properties = + ctx.getBean(ResponseHeaderInterceptorConfigurationProperties.class); + + assertThat(properties.getFields().size(), equalTo(1)); + assertThat(properties.getFields(), contains(Header.REQUEST_ID.getHeader())); + }); + } + + @Test + public void testResponseHeaderInterceptorSettingsAllFields() { + String[] values = { + "X-SPINNAKER-REQUEST-ID", + "X-SPINNAKER-USER", + "X-SPINNAKER-EXECUTION-ID", + "X-SPINNAKER-EXECUTION-TYPE", + "X-SPINNAKER-APPLICATION" + }; + String value = String.join(",", values); + + ApplicationContextRunner runner = + new ApplicationContextRunner() + .withPropertyValues("interceptors.responseHeader.fields=" + value) + .withUserConfiguration(ResponseHeaderInterceptorTestConfiguration.class); + + runner.run( + ctx -> { + ResponseHeaderInterceptorConfigurationProperties properties = + ctx.getBean(ResponseHeaderInterceptorConfigurationProperties.class); + + assertThat(properties.getFields().size(), equalTo(values.length)); + assertThat(properties.getFields(), containsInAnyOrder(values)); + }); + } + + @Test + public void testResponseHeaderInterceptorSettingsNoFields() { + ApplicationContextRunner runner = + new ApplicationContextRunner() + .withPropertyValues("interceptors.responseHeader.fields=") + .withUserConfiguration(ResponseHeaderInterceptorTestConfiguration.class); + + runner.run( + ctx -> { + ResponseHeaderInterceptorConfigurationProperties properties = + ctx.getBean(ResponseHeaderInterceptorConfigurationProperties.class); + + assertThat(properties.getFields(), empty()); + }); + } + + @EnableConfigurationProperties(ResponseHeaderInterceptorConfigurationProperties.class) + static class ResponseHeaderInterceptorTestConfiguration {} +} diff --git a/gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorTest.java b/gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorTest.java new file mode 100644 index 0000000000..351c346790 --- /dev/null +++ b/gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorTest.java @@ -0,0 +1,224 @@ +/* + * Copyright 2022 Salesforce, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.interceptors; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +import com.netflix.spinnaker.filters.AuthenticatedRequestFilter; +import com.netflix.spinnaker.gate.Main; +import com.netflix.spinnaker.kork.common.Header; +import com.netflix.spinnaker.security.AuthenticatedRequest; +import java.util.EnumSet; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; + +public class ResponseHeaderInterceptorTest { + + private static final String API_BASE = "/responseHeader"; + private static final String API_PATH = "/api"; + private static final String TEST_REQUEST_ID = "Test-Request-ID"; + private static final String TEST_USER = "Test-User"; + private static final String TEST_EXECUTION_ID = "Test-Execution-ID"; + private static final String TEST_EXECUTION_TYPE = "Test-Execution-Type"; + private static final String TEST_APPLICATION = "Test-Application"; + + @RestController + @RequestMapping(value = API_BASE) + static class TestController { + @RequestMapping(value = API_PATH, method = RequestMethod.GET) + public void api() {} + } + + private MockMvc mockMvc; + + private AuthenticatedRequestFilter authenticatedRequestFilter; + + @BeforeEach + private void setup() { + AuthenticatedRequest.clear(); + authenticatedRequestFilter = new AuthenticatedRequestFilter(true); + } + + @Nested + @SpringBootTest(classes = {Main.class, ResponseHeaderInterceptorTest.TestController.class}) + @TestPropertySource( + properties = { + "spring.config.location=classpath:gate-test.yml", + "interceptors.responseHeader.fields=X-SPINNAKER-REQUEST-ID, X-SPINNAKER-USER, X-SPINNAKER-EXECUTION-ID, X-SPINNAKER-EXECUTION-TYPE, X-SPINNAKER-APPLICATION" + }) + @DisplayName("All fields defined in response header property") + class AllFieldsDefinedInPropertyTest { + @Autowired private WebApplicationContext webApplicationContext; + + @BeforeEach + private void setup() { + mockMvc = + webAppContextSetup(webApplicationContext).addFilters(authenticatedRequestFilter).build(); + } + + @Test + public void testRequestIdExistsInAuthenticatedRequest() throws Exception { + AuthenticatedRequest.setRequestId(TEST_REQUEST_ID); + + mockMvc + .perform(get(API_BASE + API_PATH)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(header().exists(Header.REQUEST_ID.getHeader())) + .andExpect(header().string(Header.REQUEST_ID.getHeader(), equalTo(TEST_REQUEST_ID))) + .andExpect(header().doesNotExist((Header.USER.getHeader()))) + .andExpect(header().doesNotExist((Header.EXECUTION_ID.getHeader()))) + .andExpect(header().doesNotExist((Header.EXECUTION_TYPE.getHeader()))) + .andExpect(header().doesNotExist((Header.APPLICATION.getHeader()))); + } + + @Test + public void testAllHeaderFieldsInAuthenticatedRequest() throws Exception { + AuthenticatedRequest.setRequestId(TEST_REQUEST_ID); + AuthenticatedRequest.setUser(TEST_USER); + AuthenticatedRequest.setExecutionId(TEST_EXECUTION_ID); + AuthenticatedRequest.setExecutionType(TEST_EXECUTION_TYPE); + AuthenticatedRequest.setApplication(TEST_APPLICATION); + + mockMvc + .perform(get(API_BASE + API_PATH)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(header().exists(Header.REQUEST_ID.getHeader())) + .andExpect(header().string(Header.REQUEST_ID.getHeader(), equalTo(TEST_REQUEST_ID))) + .andExpect(header().exists(Header.USER.getHeader())) + .andExpect(header().string(Header.USER.getHeader(), equalTo(TEST_USER))) + .andExpect(header().exists(Header.EXECUTION_ID.getHeader())) + .andExpect(header().string(Header.EXECUTION_ID.getHeader(), equalTo(TEST_EXECUTION_ID))) + .andExpect(header().exists(Header.EXECUTION_TYPE.getHeader())) + .andExpect( + header().string(Header.EXECUTION_TYPE.getHeader(), equalTo(TEST_EXECUTION_TYPE))) + .andExpect(header().exists(Header.APPLICATION.getHeader())) + .andExpect(header().string(Header.APPLICATION.getHeader(), equalTo(TEST_APPLICATION))); + } + + @Test + public void testNoHeaderFieldsInAuthenticatedRequest() throws Exception { + // AuthenticatedRequest generates an uuid as request id if none exists + // so there will always be a request id in the response header if the + // interceptor is enabled + mockMvc + .perform(get(API_BASE + API_PATH)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(header().exists(Header.REQUEST_ID.getHeader())) + .andExpect(header().string(Header.REQUEST_ID.getHeader(), notNullValue())); + } + } + + @Nested + @SpringBootTest(classes = {Main.class, ResponseHeaderInterceptorTest.TestController.class}) + @TestPropertySource( + properties = { + "spring.config.location=classpath:gate-test.yml", + "interceptors.responseHeader.fields=X-SPINNAKER-USER, X-SPINNAKER-EXECUTION-ID, X-SPINNAKER-APPLICATION" + }) + @DisplayName("Partial list of fields defined in response header property") + class PartialFieldsDefinedInPropertyTest { + @Autowired private WebApplicationContext webApplicationContext; + + @BeforeEach + private void setup() { + mockMvc = + webAppContextSetup(webApplicationContext).addFilters(authenticatedRequestFilter).build(); + } + + @Test + public void testPartialFieldsConfiguredAllHeaderFieldsInAuthenticatedRequest() + throws Exception { + AuthenticatedRequest.setRequestId(TEST_REQUEST_ID); + AuthenticatedRequest.setUser(TEST_USER); + AuthenticatedRequest.setExecutionId(TEST_EXECUTION_ID); + AuthenticatedRequest.setExecutionType(TEST_EXECUTION_TYPE); + AuthenticatedRequest.setApplication(TEST_APPLICATION); + + mockMvc + .perform(get(API_BASE + API_PATH)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(header().doesNotExist(Header.REQUEST_ID.getHeader())) + .andExpect(header().exists(Header.USER.getHeader())) + .andExpect(header().string(Header.USER.getHeader(), equalTo(TEST_USER))) + .andExpect(header().exists(Header.EXECUTION_ID.getHeader())) + .andExpect(header().string(Header.EXECUTION_ID.getHeader(), equalTo(TEST_EXECUTION_ID))) + .andExpect(header().doesNotExist(Header.EXECUTION_TYPE.getHeader())) + .andExpect(header().exists(Header.APPLICATION.getHeader())) + .andExpect(header().string(Header.APPLICATION.getHeader(), equalTo(TEST_APPLICATION))); + } + } + + @Nested + @SpringBootTest(classes = {Main.class, ResponseHeaderInterceptorTest.TestController.class}) + @TestPropertySource( + properties = { + "spring.config.location=classpath:gate-test.yml", + "interceptors.responseHeader.fields=" + }) + @DisplayName("Empty fields defined in response header property") + class EmptyFieldsDefinedInPropertyTest { + @Autowired private WebApplicationContext webApplicationContext; + + @BeforeEach + private void setup() { + mockMvc = + webAppContextSetup(webApplicationContext).addFilters(authenticatedRequestFilter).build(); + } + + @Test + public void testNoHeaderFieldsInAuthenticatedRequest() throws Exception { + // test scenario where fields property is configured to empty list so no + // fields should be added to response header, overriding default behavior + // where request id is added + EnumSet
headers = + EnumSet.of( + Header.REQUEST_ID, + Header.USER, + Header.EXECUTION_ID, + Header.EXECUTION_TYPE, + Header.APPLICATION); + + ResultActions actions = + mockMvc.perform(get(API_BASE + API_PATH)).andDo(print()).andExpect(status().isOk()); + + for (Header header : headers) { + actions.andExpect(header().doesNotExist(header.getHeader())); + } + } + } +} From a511ed3dfd2586e6f1286ec1df7312a1717cf1e9 Mon Sep 17 00:00:00 2001 From: Kim Choy <60162636+kchoy-sfdc@users.noreply.github.com> Date: Mon, 6 Feb 2023 09:36:09 -0800 Subject: [PATCH 041/182] fix(web): Fixes ArtifactController and ArtifactService to close the InputStream object used in the fetch api and add unit test for controller (#1611) --- .../gate/controllers/ArtifactController.java | 11 +- .../gate/services/ArtifactService.java | 14 +- .../controllers/ArtifactControllerTest.java | 132 ++++++++++++++++++ 3 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 gate-web/src/test/java/com/netflix/spinnaker/gate/controllers/ArtifactControllerTest.java diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ArtifactController.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ArtifactController.java index 4d1ba03658..b1acc70383 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ArtifactController.java +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ArtifactController.java @@ -18,10 +18,10 @@ import com.netflix.spinnaker.gate.services.ArtifactService; import io.swagger.annotations.ApiOperation; -import java.io.IOException; -import java.io.OutputStream; +import java.io.InputStream; import java.util.List; import java.util.Map; +import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -51,10 +51,9 @@ List all(@RequestHeader(value = "X-RateLimit-App", required = false) String StreamingResponseBody fetch( @RequestBody Map artifact, @RequestHeader(value = "X-RateLimit-App", required = false) String sourceApp) { - return new StreamingResponseBody() { - public void writeTo(OutputStream outputStream) throws IOException { - artifactService.getArtifactContents(sourceApp, artifact, outputStream); - outputStream.flush(); + return outputStream -> { + try (InputStream inputStream = artifactService.getArtifactContents(sourceApp, artifact)) { + IOUtils.copy(inputStream, outputStream); } }; } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ArtifactService.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ArtifactService.java index 7445a99f78..3d35f5da6d 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ArtifactService.java +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ArtifactService.java @@ -19,15 +19,13 @@ import com.netflix.spinnaker.gate.services.internal.ClouddriverServiceSelector; import com.netflix.spinnaker.gate.services.internal.IgorService; import groovy.transform.CompileStatic; -import java.io.OutputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.List; import java.util.Map; import java.util.Optional; -import lombok.SneakyThrows; -import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import retrofit.client.Response; @CompileStatic @Component @@ -56,11 +54,9 @@ public List getArtifactVersions( return clouddriverServiceSelector.select().getArtifactVersions(accountName, type, artifactName); } - @SneakyThrows - public void getArtifactContents( - String selectorKey, Map artifact, OutputStream outputStream) { - Response contentResponse = clouddriverServiceSelector.select().getArtifactContent(artifact); - IOUtils.copy(contentResponse.getBody().in(), outputStream); + public InputStream getArtifactContents(String selectorKey, Map artifact) + throws IOException { + return clouddriverServiceSelector.select().getArtifactContent(artifact).getBody().in(); } public List getVersionsOfArtifactForProvider( diff --git a/gate-web/src/test/java/com/netflix/spinnaker/gate/controllers/ArtifactControllerTest.java b/gate-web/src/test/java/com/netflix/spinnaker/gate/controllers/ArtifactControllerTest.java new file mode 100644 index 0000000000..1ee9123aa0 --- /dev/null +++ b/gate-web/src/test/java/com/netflix/spinnaker/gate/controllers/ArtifactControllerTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 2022 Salesforce, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.controllers; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.spinnaker.gate.Main; +import com.netflix.spinnaker.gate.services.internal.ClouddriverService; +import com.netflix.spinnaker.gate.services.internal.ClouddriverServiceSelector; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Map; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.web.context.WebApplicationContext; +import retrofit.client.Response; +import retrofit.mime.TypedByteArray; + +@SpringBootTest(classes = {Main.class}) +@TestPropertySource(properties = {"spring.config.location=classpath:gate-test.yml"}) +public class ArtifactControllerTest { + + private static final String API_BASE = "/artifacts"; + private static final String API_FETCH = "/fetch"; + private static final String ARTIFACT_DATA = "Some data"; + + private MockMvc mockMvc; + + @MockBean private ClouddriverServiceSelector mockClouddriverServiceSelector; + + @MockBean private ClouddriverService mockClouddriverService; + + @MockBean private InputStream mockInputStream; + + @MockBean private TypedByteArray mockBody; + + @Autowired private ObjectMapper objectMapper; + + @Autowired private WebApplicationContext webApplicationContext; + + @Test + void TestFetch() throws Exception { + + TypedByteArray responseBody = + new TypedByteArray(null, objectMapper.writeValueAsBytes(ARTIFACT_DATA)); + Response response = + new Response("https://localhost", 200, "Some reason", new ArrayList<>(), responseBody); + + when(mockClouddriverServiceSelector.select()).thenReturn(mockClouddriverService); + when(mockClouddriverService.getArtifactContent(anyMap())).thenReturn(response); + + mockMvc = webAppContextSetup(webApplicationContext).build(); + + MvcResult result = + mockMvc + .perform( + put(API_BASE + API_FETCH) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(Map.of("k1", "v1")))) + .andExpect(request().asyncStarted()) + .andDo(print()) + .andReturn(); + + mockMvc + .perform(asyncDispatch(result)) + .andExpect(content().bytes(objectMapper.writeValueAsBytes(ARTIFACT_DATA))) + .andDo(print()); + } + + @Test + void TestFetchInputStreamIsClosed() throws Exception { + + Response response = + new Response("https://localhost", 200, "Some reason", new ArrayList<>(), mockBody); + + when(mockClouddriverServiceSelector.select()).thenReturn(mockClouddriverService); + when(mockClouddriverService.getArtifactContent(anyMap())).thenReturn(response); + when(mockBody.in()).thenReturn(mockInputStream); + // IOUtil copy default buffer size is 8K, so should expect two read() calls + when(mockInputStream.read(any(byte[].class))) + .thenReturn(ARTIFACT_DATA.length()) + .thenReturn(IOUtils.EOF); + + mockMvc = webAppContextSetup(webApplicationContext).build(); + + MvcResult result = + mockMvc + .perform( + put(API_BASE + API_FETCH) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(Map.of("k1", "v1")))) + .andExpect(request().asyncStarted()) + .andDo(print()) + .andReturn(); + + mockMvc.perform(asyncDispatch(result)).andDo(print()); + + verify(mockInputStream, Mockito.times(1)).close(); + } +} From 6c7c5fccd7877d454a1f420dc02c1a40b68caaa6 Mon Sep 17 00:00:00 2001 From: Kim Choy <60162636+kchoy-sfdc@users.noreply.github.com> Date: Tue, 7 Feb 2023 16:15:40 -0800 Subject: [PATCH 042/182] refactor(web): Remove unnecessary code from ResponseHeaderInterceptorTest (#1613) --- .../interceptors/ResponseHeaderInterceptorTest.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorTest.java b/gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorTest.java index 351c346790..d3055732a3 100644 --- a/gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorTest.java +++ b/gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorTest.java @@ -24,7 +24,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; -import com.netflix.spinnaker.filters.AuthenticatedRequestFilter; import com.netflix.spinnaker.gate.Main; import com.netflix.spinnaker.kork.common.Header; import com.netflix.spinnaker.security.AuthenticatedRequest; @@ -62,12 +61,9 @@ public void api() {} private MockMvc mockMvc; - private AuthenticatedRequestFilter authenticatedRequestFilter; - @BeforeEach private void setup() { AuthenticatedRequest.clear(); - authenticatedRequestFilter = new AuthenticatedRequestFilter(true); } @Nested @@ -83,8 +79,7 @@ class AllFieldsDefinedInPropertyTest { @BeforeEach private void setup() { - mockMvc = - webAppContextSetup(webApplicationContext).addFilters(authenticatedRequestFilter).build(); + mockMvc = webAppContextSetup(webApplicationContext).build(); } @Test @@ -155,8 +150,7 @@ class PartialFieldsDefinedInPropertyTest { @BeforeEach private void setup() { - mockMvc = - webAppContextSetup(webApplicationContext).addFilters(authenticatedRequestFilter).build(); + mockMvc = webAppContextSetup(webApplicationContext).build(); } @Test @@ -196,8 +190,7 @@ class EmptyFieldsDefinedInPropertyTest { @BeforeEach private void setup() { - mockMvc = - webAppContextSetup(webApplicationContext).addFilters(authenticatedRequestFilter).build(); + mockMvc = webAppContextSetup(webApplicationContext).build(); } @Test From 66e030e449141627807c6f09e9edd1327d01d7ef Mon Sep 17 00:00:00 2001 From: Kim Choy <60162636+kchoy-sfdc@users.noreply.github.com> Date: Tue, 7 Feb 2023 22:02:12 -0800 Subject: [PATCH 043/182] feat(web): Enable extracting Spinnaker related headers from request into authenticated request MDC, by setting the extractSpinnakerHeaders flag to true when creating the AuthenticatedRequestFilter in GateConfig. This allows the propagation of request headers with X-SPINNAKER prefix downstream through the AuthenticatedRequest MDC for consumption e.g. response interceptor (#1612) --- gate-web/gate-web.gradle | 1 + .../spinnaker/gate/config/GateConfig.groovy | 2 +- ...eConfigAuthenticatedRequestFilterTest.java | 138 ++++++++++++++++++ 3 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 gate-web/src/test/java/com/netflix/spinnaker/gate/config/GateConfigAuthenticatedRequestFilterTest.java diff --git a/gate-web/gate-web.gradle b/gate-web/gate-web.gradle index 3e0fcafff4..9b13c1914f 100644 --- a/gate-web/gate-web.gradle +++ b/gate-web/gate-web.gradle @@ -66,6 +66,7 @@ dependencies { testImplementation "org.springframework.security:spring-security-oauth2-jose" testImplementation "com.unboundid:unboundid-ldapsdk" testImplementation "io.spinnaker.kork:kork-jedis-test" + testImplementation "io.spinnaker.kork:kork-test" testRuntimeOnly "io.spinnaker.kork:kork-retrofit" // Add each included authz provider as a runtime dependency diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy index 322fba59ac..7af5c95062 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy @@ -390,7 +390,7 @@ class GateConfig extends RedisHttpSessionConfiguration { FilterRegistrationBean authenticatedRequestFilter() { // no need to force the `AuthenticatedRequestFilter` to create a request id as that is // handled by the `RequestTimingFilter`. - def frb = new FilterRegistrationBean(new AuthenticatedRequestFilter(false, true, false, false)) + def frb = new FilterRegistrationBean(new AuthenticatedRequestFilter(true, true, false, false)) frb.order = Ordered.LOWEST_PRECEDENCE - 1 return frb } diff --git a/gate-web/src/test/java/com/netflix/spinnaker/gate/config/GateConfigAuthenticatedRequestFilterTest.java b/gate-web/src/test/java/com/netflix/spinnaker/gate/config/GateConfigAuthenticatedRequestFilterTest.java new file mode 100644 index 0000000000..df63f32fa0 --- /dev/null +++ b/gate-web/src/test/java/com/netflix/spinnaker/gate/config/GateConfigAuthenticatedRequestFilterTest.java @@ -0,0 +1,138 @@ +/* + * Copyright 2023 Salesforce, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.config; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +import ch.qos.logback.classic.Level; +import com.netflix.spinnaker.gate.Main; +import com.netflix.spinnaker.kork.common.Header; +import com.netflix.spinnaker.kork.test.log.MemoryAppender; +import com.netflix.spinnaker.security.AuthenticatedRequest; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest( + classes = {Main.class, GateConfigAuthenticatedRequestFilterTest.TestController.class}) +@TestPropertySource(properties = {"spring.config.location=classpath:gate-test.yml"}) +public class GateConfigAuthenticatedRequestFilterTest { + + private static final String API_BASE = "/test"; + private static final String API_PATH = "/api"; + private static final String TEST_USER = "Test-User"; + private static final String TEST_EXECUTION_ID = "Test-Execution-ID"; + private static final String TEST_EXECUTION_TYPE = "Test-Execution-Type"; + private static final String TEST_APPLICATION = "Test-Application"; + + private static final String LOG_MESSAGE = " logged in api: "; + private static final String NULL_VALUE = "null"; + + @RestController + @RequestMapping(value = API_BASE) + static class TestController { + private final Logger log = LoggerFactory.getLogger(getClass()); + + @RequestMapping(value = API_PATH, method = RequestMethod.GET) + public void api() { + log.info( + Header.USER.name() + + LOG_MESSAGE + + AuthenticatedRequest.get(Header.USER).orElse(NULL_VALUE)); + log.info( + Header.APPLICATION.name() + + LOG_MESSAGE + + AuthenticatedRequest.get(Header.APPLICATION).orElse(NULL_VALUE)); + log.info( + Header.EXECUTION_ID.name() + + LOG_MESSAGE + + AuthenticatedRequest.get(Header.EXECUTION_ID).orElse(NULL_VALUE)); + log.info( + Header.EXECUTION_TYPE.name() + + LOG_MESSAGE + + AuthenticatedRequest.get(Header.EXECUTION_TYPE).orElse(NULL_VALUE)); + } + } + + @Autowired + @Qualifier("authenticatedRequestFilter") + private FilterRegistrationBean filterRegistrationBean; + + @Autowired private WebApplicationContext webApplicationContext; + + private MockMvc mockMvc; + + // Without setting the extractSpinnakerHeaders flag to true when creating + // AuthenticatedRequestFilter, X-SPINNAKER-* headers would not be copied into + // AuthenticatedRequest MDC for downstream consumption + @Test + void TestHeaderAvailableInAuthenticatedRequestMDC() throws Exception { + mockMvc = + webAppContextSetup(webApplicationContext) + .addFilters(filterRegistrationBean.getFilter()) + .build(); + + MemoryAppender memoryAppender = + new MemoryAppender(GateConfigAuthenticatedRequestFilterTest.TestController.class); + + mockMvc + .perform( + get(API_BASE + API_PATH) + .header(Header.USER.getHeader(), TEST_USER) + .header(Header.APPLICATION.getHeader(), TEST_APPLICATION) + .header(Header.EXECUTION_ID.getHeader(), TEST_EXECUTION_ID) + .header(Header.EXECUTION_TYPE.getHeader(), TEST_EXECUTION_TYPE)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(header().exists(Header.REQUEST_ID.getHeader())); + + List messages = memoryAppender.layoutSearch(LOG_MESSAGE, Level.INFO); + String expectedUserLog = Header.USER.name() + LOG_MESSAGE + TEST_USER; + String expectedApplicationLog = Header.APPLICATION.name() + LOG_MESSAGE + TEST_APPLICATION; + String expectedExecutionIdLog = Header.EXECUTION_ID.name() + LOG_MESSAGE + TEST_EXECUTION_ID; + String expectedExecutionTypeLog = + Header.EXECUTION_TYPE.name() + LOG_MESSAGE + TEST_EXECUTION_TYPE; + + assertThat(messages.size(), equalTo(4)); + assertThat( + messages, + contains( + containsString(expectedUserLog), + containsString(expectedApplicationLog), + containsString(expectedExecutionIdLog), + containsString(expectedExecutionTypeLog))); + } +} From 3f1a4cb428e979b5d0ec8505de08e97a216772d5 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 8 Feb 2023 14:46:42 -0500 Subject: [PATCH 044/182] chore(dependencies): Autobump korkVersion (#1614) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6a03a51683..a4d622d609 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.160.2 +korkVersion=7.161.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.25.0 From e356ec68a0f0419ccf5561fd2ebbe06625775f97 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 8 Feb 2023 17:19:42 -0500 Subject: [PATCH 045/182] chore(dependencies): Autobump korkVersion (#1615) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a4d622d609..3e76634a05 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.161.0 +korkVersion=7.162.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.25.0 From 1168a086164b96c3755e92b0d2d3f6c642be7d50 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 8 Feb 2023 18:30:16 -0500 Subject: [PATCH 046/182] chore(dependencies): Autobump korkVersion (#1616) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3e76634a05..ad03146586 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.162.0 +korkVersion=7.163.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.25.0 From f015771816063e5c7cc9149572680d2f993b12bd Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 8 Feb 2023 19:58:23 -0500 Subject: [PATCH 047/182] chore(dependencies): Autobump korkVersion (#1617) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ad03146586..063e336847 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.163.0 +korkVersion=7.164.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.25.0 From 944f3f438f9e34b7bc3e9570f82ce84969682bc9 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 9 Feb 2023 01:35:32 -0500 Subject: [PATCH 048/182] chore(dependencies): Autobump korkVersion (#1618) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 063e336847..69a958526f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.164.0 +korkVersion=7.165.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.25.0 From 05405e883c28597f2c9e5a1c839e648f5c7d174e Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 20 Feb 2023 14:51:21 -0500 Subject: [PATCH 049/182] chore(dependencies): Autobump korkVersion (#1619) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 69a958526f..4a287a4e83 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.165.0 +korkVersion=7.166.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.25.0 From db9e72d3c1680fe59f7bc833bf53316025b7b1e6 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 20 Feb 2023 16:08:58 -0500 Subject: [PATCH 050/182] chore(dependencies): Autobump korkVersion (#1620) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4a287a4e83..afb802b573 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.166.0 +korkVersion=7.167.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.25.0 From 576e6ca2d44f0947015ceedfd271007725e7d879 Mon Sep 17 00:00:00 2001 From: Sandesh <30489233+j-sandy@users.noreply.github.com> Date: Thu, 2 Mar 2023 21:11:17 +0530 Subject: [PATCH 051/182] fix(tests): Introduce junit5 vintage engine for running junit4 test cases over junit5 in gate (#1623) Spring boot 2.4.x removed JUnit5 vintage engine from spring-boot-starter-test. [https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.4-Release-Notes#junit-5s-vintage-engine-removed-from-spring-boot-starter-test] It is required for executing junit4 based test cases in gate. So, introducing junit-vintage-engine dependency in build.gradle, using testRuntimeOnly() as suggested in section 3.1 of https://junit.org/junit5/docs/5.6.2/user-guide/index.pdf After applying this fix, coverage increased from 74 to 265 test case executions. --- build.gradle | 2 ++ gate-plugins-test/gate-plugins-test.gradle | 6 ------ gate-plugins/gate-plugins.gradle | 4 ---- gate-web/gate-web.gradle | 1 - gradle/kotlin-test.gradle | 6 ------ 5 files changed, 2 insertions(+), 17 deletions(-) diff --git a/build.gradle b/build.gradle index b6f145ebb8..438ed08f7b 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,7 @@ allprojects { testImplementation "org.springframework.boot:spring-boot-starter-test" testImplementation "org.hamcrest:hamcrest-core" testRuntimeOnly "cglib:cglib-nodep" + testRuntimeOnly "org.junit.vintage:junit-vintage-engine" testRuntimeOnly "org.objenesis:objenesis" } @@ -62,6 +63,7 @@ allprojects { testLogging { exceptionFormat = 'full' } + useJUnitPlatform() } } diff --git a/gate-plugins-test/gate-plugins-test.gradle b/gate-plugins-test/gate-plugins-test.gradle index e9089cb81c..c245fc7491 100644 --- a/gate-plugins-test/gate-plugins-test.gradle +++ b/gate-plugins-test/gate-plugins-test.gradle @@ -13,9 +13,3 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") } - -test { - useJUnitPlatform { - includeEngines("junit-jupiter", "junit-vintage") - } -} diff --git a/gate-plugins/gate-plugins.gradle b/gate-plugins/gate-plugins.gradle index 2b8a830362..36154c4ff3 100644 --- a/gate-plugins/gate-plugins.gradle +++ b/gate-plugins/gate-plugins.gradle @@ -32,7 +32,3 @@ dependencies { implementation "org.springframework:spring-web" implementation "org.pf4j:pf4j-update" } - -test { - useJUnitPlatform() -} diff --git a/gate-web/gate-web.gradle b/gate-web/gate-web.gradle index 9b13c1914f..8d5d49f054 100644 --- a/gate-web/gate-web.gradle +++ b/gate-web/gate-web.gradle @@ -96,5 +96,4 @@ test { } } } - useJUnitPlatform() } diff --git a/gradle/kotlin-test.gradle b/gradle/kotlin-test.gradle index 17ccb437f8..5ffe2c3cd8 100644 --- a/gradle/kotlin-test.gradle +++ b/gradle/kotlin-test.gradle @@ -30,12 +30,6 @@ dependencies { testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" } -test { - useJUnitPlatform { - includeEngines "junit-jupiter" - } -} - compileTestKotlin { kotlinOptions { languageVersion = "1.4" From 6c93cc6239c88086e30ed70f4185adc8d1b928d3 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 13 Mar 2023 17:55:10 -0400 Subject: [PATCH 052/182] chore(dependencies): Autobump korkVersion (#1628) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index afb802b573..93999d64ac 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.167.0 +korkVersion=7.168.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.25.0 From 5916b56d0400b364f19e7cf0eb7479e31c1fb620 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 17 Mar 2023 19:12:04 -0400 Subject: [PATCH 053/182] chore(dependencies): Autobump korkVersion (#1629) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 93999d64ac..078fed8096 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.36.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.168.0 +korkVersion=7.169.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.25.0 From aff7f442157beecbf9994fab29ebe4a8aeba1da4 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 24 Mar 2023 19:10:31 -0400 Subject: [PATCH 054/182] chore(dependencies): Autobump fiatVersion (#1630) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 078fed8096..090230fcac 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.36.0 +fiatVersion=1.37.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.169.0 kotlinVersion=1.4.0 From 45b65d5dede70c246559503c72ac7488c548c4cf Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 29 Mar 2023 15:35:29 -0400 Subject: [PATCH 055/182] chore(dependencies): Autobump spinnakerGradleVersion (#1634) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 090230fcac..630e42cc0d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.169.0 kotlinVersion=1.4.0 org.gradle.parallel=true -spinnakerGradleVersion=8.25.0 +spinnakerGradleVersion=8.26.0 targetJava11=true # To enable a composite reference to a project, set the From 505f955c4b0953ed0284605dbf9d54870f949d38 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 30 Mar 2023 10:37:26 -0400 Subject: [PATCH 056/182] chore(dependencies): Autobump fiatVersion (#1635) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 630e42cc0d..68878c12ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.37.0 +fiatVersion=1.38.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.169.0 kotlinVersion=1.4.0 From 6e751aa7d7c9a4f116b85b6f4b73d5203d902ce7 Mon Sep 17 00:00:00 2001 From: David Byron <82477955+dbyron-sf@users.noreply.github.com> Date: Thu, 30 Mar 2023 09:15:51 -0700 Subject: [PATCH 057/182] chore(gha): update to docker/login-action@v2 to stay up to date (#1636) similar to https://github.com/spinnaker/fiat/pull/1036 --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eac2ff9c41..752351bb86 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,7 +40,7 @@ jobs: - name: Login to GAR # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/login-action@v1 + uses: docker/login-action@v2 # use service account flow defined at: https://github.com/docker/login-action#service-account-based-authentication-1 with: registry: us-docker.pkg.dev diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index baf5f14280..9a0d740abd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,7 +77,7 @@ jobs: - name: Login to GAR # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/login-action@v1 + uses: docker/login-action@v2 # use service account flow defined at: https://github.com/docker/login-action#service-account-based-authentication-1 with: registry: us-docker.pkg.dev From 9e491704774cb279c4baa0960554951f2f645c84 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 31 Mar 2023 13:37:47 -0400 Subject: [PATCH 058/182] chore(dependencies): Autobump korkVersion (#1637) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 68878c12ee..b4f85fd6e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.38.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.169.0 +korkVersion=7.170.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.26.0 From 237d7138f5a522529d2077cf44e0c2c14763c87e Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 31 Mar 2023 15:07:47 -0400 Subject: [PATCH 059/182] chore(dependencies): Autobump fiatVersion (#1638) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b4f85fd6e0..31cd882d86 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.38.0 +fiatVersion=1.39.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.170.0 kotlinVersion=1.4.0 From c5ea73b345357328d0be59527842f8dfb2517d57 Mon Sep 17 00:00:00 2001 From: David Byron <82477955+dbyron-sf@users.noreply.github.com> Date: Fri, 31 Mar 2023 14:49:17 -0700 Subject: [PATCH 060/182] feat(gha): configure dependabot to keep github actions up to date (#1639) see https://github.com/spinnaker/spin/pull/358/commits/7fa164603113cbd3798590e540ffd78f78547087 --- .github/dependabot.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..f9ecf576e1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" From 19691bb7ecef653a32ee710a63205b9d37a4f6f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Mar 2023 22:09:35 +0000 Subject: [PATCH 061/182] chore(deps): bump google-github-actions/auth from 0 to 1 (#1641) Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 0 to 1. - [Release notes](https://github.com/google-github-actions/auth/releases) - [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md) - [Commits](https://github.com/google-github-actions/auth/compare/v0...v1) --- updated-dependencies: - dependency-name: google-github-actions/auth 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/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a0d740abd..2ca202a80e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,7 +61,7 @@ jobs: - name: Login to Google Cloud # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: 'google-github-actions/auth@v0' + uses: 'google-github-actions/auth@v1' # use service account flow defined at: https://github.com/google-github-actions/upload-cloud-storage#authenticating-via-service-account-key-json with: credentials_json: '${{ secrets.GAR_JSON_KEY }}' From 063ec6c74bd444781584f1f5598347728b8c9991 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Mar 2023 22:18:14 +0000 Subject: [PATCH 062/182] chore(deps): bump actions/checkout from 2 to 3 (#1642) Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout 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/build.yml | 2 +- .github/workflows/pr.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 752351bb86..9b60dcbb6a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: if: startsWith(github.repository, 'spinnaker/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up QEMU diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 4a86f9c25a..21d3d92fe2 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -10,7 +10,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up QEMU diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ca202a80e..f2c9deb63f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up QEMU From 0ddf0ebc66add810d468ddeff3408865b3def73c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Mar 2023 22:26:35 +0000 Subject: [PATCH 063/182] chore(deps): bump google-github-actions/upload-cloud-storage from 0 to 1 (#1644) Bumps [google-github-actions/upload-cloud-storage](https://github.com/google-github-actions/upload-cloud-storage) from 0 to 1. - [Release notes](https://github.com/google-github-actions/upload-cloud-storage/releases) - [Changelog](https://github.com/google-github-actions/upload-cloud-storage/blob/main/CHANGELOG.md) - [Commits](https://github.com/google-github-actions/upload-cloud-storage/compare/v0...v1) --- updated-dependencies: - dependency-name: google-github-actions/upload-cloud-storage 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> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f2c9deb63f..28f4020ee4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,7 +69,7 @@ jobs: # https://console.cloud.google.com/storage/browser/halconfig # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: 'google-github-actions/upload-cloud-storage@v0' + uses: 'google-github-actions/upload-cloud-storage@v1' with: path: 'halconfig/' destination: 'halconfig/${{ steps.build_variables.outputs.REPO }}/${{ steps.release_info.outputs.RELEASE_VERSION }}' From 5c0e799c163f05bf808de55a2458ffbc1eb8c936 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Mar 2023 22:37:12 +0000 Subject: [PATCH 064/182] chore(deps): bump docker/build-push-action from 3 to 4 (#1640) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3 to 4. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/build-push-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> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- .github/workflows/pr.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b60dcbb6a..ea3327249e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: - name: Build and publish slim container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . file: Dockerfile.slim @@ -63,7 +63,7 @@ jobs: - name: Build and publish ubuntu container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . file: Dockerfile.ubuntu diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 21d3d92fe2..1531a3859f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -32,7 +32,7 @@ jobs: ORG_GRADLE_PROJECT_version: ${{ steps.build_variables.outputs.VERSION }} run: ./gradlew build ${{ steps.build_variables.outputs.REPO }}-web:installDist - name: Build slim container image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . file: Dockerfile.slim @@ -43,7 +43,7 @@ jobs: "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:latest-slim" "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-slim" - name: Build ubuntu container image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . file: Dockerfile.ubuntu diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 28f4020ee4..a8316aa768 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,7 +86,7 @@ jobs: - name: Build and publish slim container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . file: Dockerfile.slim @@ -99,7 +99,7 @@ jobs: - name: Build and publish ubuntu container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . file: Dockerfile.ubuntu From cdafe32cd54f7601a5dc61d5691c7b526f83794a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Mar 2023 22:45:47 +0000 Subject: [PATCH 065/182] chore(deps): bump actions/setup-java from 2 to 3 (#1643) Bumps [actions/setup-java](https://github.com/actions/setup-java) from 2 to 3. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/setup-java 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> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/pr.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ea3327249e..b6f714c780 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: java-version: 11 distribution: 'zulu' diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 1531a3859f..2f0ac57bcd 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -17,7 +17,7 @@ jobs: uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: java-version: 11 distribution: 'zulu' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8316aa768..7d6315a0f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: java-version: 11 distribution: 'zulu' From e6753c09afe0f36e4dd36d3fe783edfcaaf63b94 Mon Sep 17 00:00:00 2001 From: David Byron <82477955+dbyron-sf@users.noreply.github.com> Date: Fri, 31 Mar 2023 16:48:42 -0700 Subject: [PATCH 066/182] chore(gha): replace action for creating github releases (#1645) see https://github.com/spinnaker/rosco/pull/966 for background --- .github/workflows/release.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d6315a0f6..a24c19e566 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -110,13 +110,12 @@ jobs: "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.release_info.outputs.RELEASE_VERSION }}-${{ steps.build_variables.outputs.VERSION }}-unvalidated-ubuntu" - name: Create release if: steps.release_info.outputs.SKIP_RELEASE == 'false' - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: softprops/action-gh-release@v1 with: - tag_name: ${{ github.ref }} - release_name: ${{ github.event.repository.name }} ${{ github.ref }} body: | ${{ steps.release_info.outputs.CHANGELOG }} draft: false + name: ${{ github.event.repository.name }} ${{ github.ref_name }} prerelease: ${{ steps.release_info.outputs.IS_CANDIDATE }} + tag_name: ${{ github.ref }} + token: ${{ secrets.GITHUB_TOKEN }} From 0a32290a1ad3c1fdf505982cd84ca8851f171963 Mon Sep 17 00:00:00 2001 From: David Byron <82477955+dbyron-sf@users.noreply.github.com> Date: Sat, 1 Apr 2023 21:25:11 -0700 Subject: [PATCH 067/182] chore(gha): replace deprecated set-output commands with environment files (#1646) to avoid warning messages like Run echo ::set-output name=REPO::${GITHUB_REPOSITORY##*/} Warning: The `set-output` command is deprecated and will be disabled soon. Please upgrade to using Environment Files. For more information see: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/ Warning: The `set-output` command is deprecated and will be disabled soon. Please upgrade to using Environment Files. For more information see: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/ --- .github/workflows/build.yml | 4 ++-- .github/workflows/pr.yml | 4 ++-- .github/workflows/release.yml | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6f714c780..1bba1c90b4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,8 +31,8 @@ jobs: - name: Prepare build variables id: build_variables run: | - echo ::set-output name=REPO::${GITHUB_REPOSITORY##*/} - echo ::set-output name=VERSION::"$(git describe --tags --abbrev=0 --match="v[0-9]*" | cut -c2-)-dev-${GITHUB_REF_NAME}-$(git rev-parse --short HEAD)-$(date --utc +'%Y%m%d%H%M')" + echo REPO="${GITHUB_REPOSITORY##*/}" >> $GITHUB_OUTPUT + echo VERSION="$(git describe --tags --abbrev=0 --match='v[0-9]*' | cut -c2-)-dev-${GITHUB_REF_NAME}-$(git rev-parse --short HEAD)-$(date --utc +'%Y%m%d%H%M')" >> $GITHUB_OUTPUT - name: Build env: ORG_GRADLE_PROJECT_version: ${{ steps.build_variables.outputs.VERSION }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2f0ac57bcd..8119027332 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -25,8 +25,8 @@ jobs: - name: Prepare build variables id: build_variables run: | - echo ::set-output name=REPO::${GITHUB_REPOSITORY##*/} - echo ::set-output name=VERSION::"$(git describe --tags --abbrev=0 --match="v[0-9]*" | cut -c2-)-dev-pr-$(git rev-parse --short HEAD)-$(date --utc +'%Y%m%d%H%M')" + echo REPO="${GITHUB_REPOSITORY##*/}" >> $GITHUB_OUTPUT + echo VERSION="$(git describe --tags --abbrev=0 --match='v[0-9]*' | cut -c2-)-dev-pr-$(git rev-parse --short HEAD)-$(date --utc +'%Y%m%d%H%M')" >> $GITHUB_OUTPUT - name: Build env: ORG_GRADLE_PROJECT_version: ${{ steps.build_variables.outputs.VERSION }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a24c19e566..238fb3aaca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,15 +32,15 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | . .github/workflows/release_info.sh ${{ github.event.repository.full_name }} - echo ::set-output name=CHANGELOG::$(echo -e "${CHANGELOG}") - echo ::set-output name=SKIP_RELEASE::${SKIP_RELEASE} - echo ::set-output name=IS_CANDIDATE::${IS_CANDIDATE} - echo ::set-output name=RELEASE_VERSION::${RELEASE_VERSION} + echo CHANGELOG=$(echo -e "${CHANGELOG}") >> $GITHUB_OUTPUT + echo SKIP_RELEASE="${SKIP_RELEASE}" >> $GITHUB_OUTPUT + echo IS_CANDIDATE="${IS_CANDIDATE}" >> $GITHUB_OUTPUT + echo RELEASE_VERSION="${RELEASE_VERSION}" >> $GITHUB_OUTPUT - name: Prepare build variables id: build_variables run: | - echo ::set-output name=REPO::${GITHUB_REPOSITORY##*/} - echo ::set-output name=VERSION::"$(git rev-parse --short HEAD)-$(date --utc +'%Y%m%d%H%M')" + echo REPO="${GITHUB_REPOSITORY##*/}" >> $GITHUB_OUTPUT + echo VERSION="$(git rev-parse --short HEAD)-$(date --utc +'%Y%m%d%H%M')" >> $GITHUB_OUTPUT - name: Release build env: ORG_GRADLE_PROJECT_version: ${{ steps.release_info.outputs.RELEASE_VERSION }} From ff34450520d775e1316f49e173cd3c777c176036 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 3 Apr 2023 12:27:47 -0400 Subject: [PATCH 068/182] chore(dependencies): Autobump korkVersion (#1647) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 31cd882d86..5b1b775759 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.39.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.170.0 +korkVersion=7.171.1 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.26.0 From 722f49b3381cac5aa935e90e5ffd92ba00e8d520 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 5 Apr 2023 07:05:36 -0400 Subject: [PATCH 069/182] chore(dependencies): Autobump korkVersion (#1648) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5b1b775759..119d68090d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.39.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.171.1 +korkVersion=7.171.2 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.26.0 From b4c8648ee21e60bbafb9e5c8d4f9b94bcc4ed8bf Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 17 Apr 2023 14:59:00 -0400 Subject: [PATCH 070/182] chore(dependencies): Autobump korkVersion (#1652) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 119d68090d..c5624e1db9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.39.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.171.2 +korkVersion=7.172.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.26.0 From b185a310035c1304f6762db03c0e296ca4471588 Mon Sep 17 00:00:00 2001 From: David Byron <82477955+dbyron-sf@users.noreply.github.com> Date: Tue, 2 May 2023 09:41:37 -0700 Subject: [PATCH 071/182] fix(web/test): move GateConfigAuthenticatedRequestFilterTest out of the com.netflix.spinnaker.gate.config package (#1655) so it doesn't pollute the spring context in other tests --- .../GateConfigAuthenticatedRequestFilterTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename gate-web/src/test/java/com/netflix/spinnaker/gate/{config => testconfig}/GateConfigAuthenticatedRequestFilterTest.java (99%) diff --git a/gate-web/src/test/java/com/netflix/spinnaker/gate/config/GateConfigAuthenticatedRequestFilterTest.java b/gate-web/src/test/java/com/netflix/spinnaker/gate/testconfig/GateConfigAuthenticatedRequestFilterTest.java similarity index 99% rename from gate-web/src/test/java/com/netflix/spinnaker/gate/config/GateConfigAuthenticatedRequestFilterTest.java rename to gate-web/src/test/java/com/netflix/spinnaker/gate/testconfig/GateConfigAuthenticatedRequestFilterTest.java index df63f32fa0..17ae5dec70 100644 --- a/gate-web/src/test/java/com/netflix/spinnaker/gate/config/GateConfigAuthenticatedRequestFilterTest.java +++ b/gate-web/src/test/java/com/netflix/spinnaker/gate/testconfig/GateConfigAuthenticatedRequestFilterTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.netflix.spinnaker.gate.config; +package com.netflix.spinnaker.gate.testconfig; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; From fe954a1a450b427b6f4da61acf71d2543721b9c0 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 2 May 2023 12:49:52 -0400 Subject: [PATCH 072/182] chore(dependencies): Autobump korkVersion (#1654) Co-authored-by: root Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c5624e1db9..b071f02c89 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.39.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.172.0 +korkVersion=7.173.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.26.0 From e7d331a274a0e2ef1073cd660103c96f0eb19893 Mon Sep 17 00:00:00 2001 From: David Byron <82477955+dbyron-sf@users.noreply.github.com> Date: Fri, 12 May 2023 07:58:29 -0700 Subject: [PATCH 073/182] fix(web/test): remove race in FunctionalSpec (#1657) * fix(web/test): ctx may be null if there's a failure early in test setup * fix(web/test): remove race in FunctionalSpec There's a window in time between closing the socket and starting the server that another process could use the same port. Remove this race to prevent failures like: FunctionalSpec > should call ApplicationService for applications FAILED org.gradle.internal.exceptions.DefaultMultiCauseException: Multiple Failures (2 failures) org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'; nested exception is org.springframework.boot.web.server.PortInUseException: Port 59629 is already in use java.lang.NullPointerException: Cannot invoke method close() on null object at org.junit.vintage.engine.execution.TestRun.getStoredResultOrSuccessful(TestRun.java:196) at org.junit.vintage.engine.execution.RunListenerAdapter.fireExecutionFinished(RunListenerAdapter.java:226) at org.junit.vintage.engine.execution.RunListenerAdapter.testFinished(RunListenerAdapter.java:192) at org.junit.vintage.engine.execution.RunListenerAdapter.testFinished(RunListenerAdapter.java:79) at org.junit.runner.notification.SynchronizedRunListener.testFinished(SynchronizedRunListener.java:87) at org.junit.runner.notification.RunNotifier$9.notifyListener(RunNotifier.java:225) at org.junit.runner.notification.RunNotifier$SafeNotifier.run(RunNotifier.java:72) at org.junit.runner.notification.RunNotifier.fireTestFinished(RunNotifier.java:222) at org.spockframework.runtime.JUnitSupervisor.afterFeature(JUnitSupervisor.java:197) at org.spockframework.runtime.BaseSpecRunner.runFeature(BaseSpecRunner.java:236) at org.spockframework.runtime.BaseSpecRunner.runFeatures(BaseSpecRunner.java:185) at org.spockframework.runtime.BaseSpecRunner.doRunSpec(BaseSpecRunner.java:95) at org.spockframework.runtime.BaseSpecRunner$1.invoke(BaseSpecRunner.java:81) at org.spockframework.runtime.BaseSpecRunner.invokeRaw(BaseSpecRunner.java:484) at org.spockframework.runtime.BaseSpecRunner.invoke(BaseSpecRunner.java:467) at org.spockframework.runtime.BaseSpecRunner.runSpec(BaseSpecRunner.java:73) at org.spockframework.runtime.BaseSpecRunner.run(BaseSpecRunner.java:64) at org.spockframework.runtime.Sputnik.run(Sputnik.java:63) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at org.junit.runner.JUnitCore.run(JUnitCore.java:115) at org.junit.vintage.engine.execution.RunnerExecutor.execute(RunnerExecutor.java:43) at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183) at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195) at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133) at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801) at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484) at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474) at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150) at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173) at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497) at org.junit.vintage.engine.VintageTestEngine.executeAllChildren(VintageTestEngine.java:82) at org.junit.vintage.engine.VintageTestEngine.execute(VintageTestEngine.java:73) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75) at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24) at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94) at com.sun.proxy.$Proxy2.stop(Unknown Source) at org.gradle.api.internal.tasks.testing.worker.TestWorker.stop(TestWorker.java:133) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24) at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182) at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164) at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:414) at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56) at java.base/java.lang.Thread.run(Thread.java:834) Caused by: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'; nested exception is org.springframework.boot.web.server.PortInUseException: Port 59629 is already in use at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) at java.base/java.lang.Iterable.forEach(Iterable.java:75) at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:780) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:453) at org.springframework.boot.SpringApplication.run(SpringApplication.java:343) at com.netflix.spinnaker.gate.FunctionalSpec.setup(FunctionalSpec.groovy:101) Caused by: org.springframework.boot.web.server.PortInUseException: Port 59629 is already in use at org.springframework.boot.web.server.PortInUseException.lambda$throwIfPortBindingException$0(PortInUseException.java:70) at org.springframework.boot.web.server.PortInUseException.lambda$ifPortBindingException$1(PortInUseException.java:85) at org.springframework.boot.web.server.PortInUseException.ifCausedBy(PortInUseException.java:103) at org.springframework.boot.web.server.PortInUseException.ifPortBindingException(PortInUseException.java:82) at org.springframework.boot.web.server.PortInUseException.throwIfPortBindingException(PortInUseException.java:69) at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.start(TomcatWebServer.java:228) at org.springframework.boot.web.servlet.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:43) at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178) ... 12 more Caused by: java.lang.IllegalArgumentException: standardService.connector.startFailed at org.apache.catalina.core.StandardService.addConnector(StandardService.java:238) at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.addPreviouslyRemovedConnectors(TomcatWebServer.java:282) at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.start(TomcatWebServer.java:213) ... 14 more Caused by: org.apache.catalina.LifecycleException: Protocol handler start failed at org.apache.catalina.connector.Connector.startInternal(Connector.java:1076) at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) at org.apache.catalina.core.StandardService.addConnector(StandardService.java:234) ... 16 more Caused by: java.net.BindException: Address already in use at java.base/sun.nio.ch.Net.bind(Net.java:455) at java.base/sun.nio.ch.Net.bind(Net.java:447) at java.base/sun.nio.ch.ServerSocketChannelImpl.bind(ServerSocketChannelImpl.java:227) at org.apache.tomcat.util.net.NioEndpoint.initServerSocket(NioEndpoint.java:275) at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:230) at org.apache.tomcat.util.net.AbstractEndpoint.bindWithCleanup(AbstractEndpoint.java:1227) at org.apache.tomcat.util.net.AbstractEndpoint.start(AbstractEndpoint.java:1313) at org.apache.coyote.AbstractProtocol.start(AbstractProtocol.java:615) at org.apache.catalina.connector.Connector.startInternal(Connector.java:1073) ... 18 more --- .../com/netflix/spinnaker/gate/FunctionalSpec.groovy | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy index f4a48925c4..b2519c39a4 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy @@ -86,11 +86,7 @@ class FunctionalSpec extends Specification { serviceConfiguration = new ServiceConfiguration() fiatStatus = Mock(FiatStatus) - - def sock = new ServerSocket(0) - def localPort = sock.localPort - sock.close() - System.setProperty("server.port", localPort.toString()) + System.setProperty("server.port", "0") // to get a random port System.setProperty("saml.enabled", "false") System.setProperty('spring.session.store-type', 'NONE') System.setProperty("spring.main.allow-bean-definition-overriding", "true") @@ -100,6 +96,7 @@ class FunctionalSpec extends Specification { spring.setSources([FunctionalConfiguration] as Set) ctx = spring.run() + def localPort = ctx.environment.getProperty("local.server.port") api = new RestAdapter.Builder() .setEndpoint("http://localhost:${localPort}") .setClient(new OkClient()) @@ -109,7 +106,7 @@ class FunctionalSpec extends Specification { } def cleanup() { - ctx.close() + ctx?.close() } void "should call ApplicationService for applications"() { From eba609028e3d1c60b1ed7e91ab2cbf3e93990191 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 18 May 2023 13:05:02 -0400 Subject: [PATCH 074/182] chore(dependencies): Autobump spinnakerGradleVersion (#1658) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b071f02c89..00057e3ef5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.173.0 kotlinVersion=1.4.0 org.gradle.parallel=true -spinnakerGradleVersion=8.26.0 +spinnakerGradleVersion=8.27.0 targetJava11=true # To enable a composite reference to a project, set the From cd7ae859d3bdea17dfb616efcd0f03e8b1e9db4a Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 25 May 2023 17:38:35 -0400 Subject: [PATCH 075/182] chore(dependencies): Autobump korkVersion (#1659) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 00057e3ef5..fd1335d0f7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.39.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.173.0 +korkVersion=7.174.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.27.0 From 0f18f9aa4da0857ba44048a935a0052601b13388 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 2 Jun 2023 16:01:30 -0400 Subject: [PATCH 076/182] chore(dependencies): Autobump korkVersion (#1661) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index fd1335d0f7..b0df41e947 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.39.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.174.0 +korkVersion=7.175.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.27.0 From 4ac475e90ba1feca15ea82a2c301e10b3199207e Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 5 Jun 2023 16:11:44 -0400 Subject: [PATCH 077/182] chore(dependencies): Autobump fiatVersion (#1662) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b0df41e947..ce7909dce7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.39.0 +fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.175.0 kotlinVersion=1.4.0 From a81d281757464a0bab6952aaad6943dd2d01ff59 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 12 Jun 2023 19:37:52 -0400 Subject: [PATCH 078/182] chore(dependencies): Autobump korkVersion (#1664) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ce7909dce7..f3cfeee208 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.175.0 +korkVersion=7.176.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.27.0 From 31296ec8c6a635e84b3c6d4410107491d07012fe Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 13 Jun 2023 01:17:05 -0400 Subject: [PATCH 079/182] chore(dependencies): Autobump spinnakerGradleVersion (#1665) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f3cfeee208..4312f9a287 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.176.0 kotlinVersion=1.4.0 org.gradle.parallel=true -spinnakerGradleVersion=8.27.0 +spinnakerGradleVersion=8.28.0 targetJava11=true # To enable a composite reference to a project, set the From a29ebbf44dfc4ab29172f4b43b0db548705c0cb4 Mon Sep 17 00:00:00 2001 From: Sandesh <30489233+j-sandy@users.noreply.github.com> Date: Wed, 14 Jun 2023 00:17:35 +0530 Subject: [PATCH 080/182] chore(build): upgrade gradle to 7.6.1 (#1660) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 61574 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 269 ++++++++++++++--------- gradlew.bat | 15 +- 4 files changed, 175 insertions(+), 112 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..943f0cbfa754578e88a3dae77fce6e3dea56edbf 100644 GIT binary patch delta 39316 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 delta 36846 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 CcabXq diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 28ff446a21..508322917b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c81..65dcd68d65 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,105 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f938..6689b85bee 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From fd09c1abd788ee42d282c008fa0f1920f6920de1 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 13 Jun 2023 15:19:17 -0400 Subject: [PATCH 081/182] chore(dependencies): Autobump korkVersion (#1666) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4312f9a287..c1a6655a7e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.176.0 +korkVersion=7.177.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.28.0 From c906480298386eb6e2e00b589650182e35ea8bee Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 13 Jun 2023 19:28:42 -0400 Subject: [PATCH 082/182] chore(dependencies): Autobump korkVersion (#1667) * chore(dependencies): Autobump korkVersion * Adapt for okhttp3 4.x * refactor(okhttp): replace deprecated RequestBody.create with toRequestBody extension method in ProxyController --------- Co-authored-by: root Co-authored-by: Dimitar Roustchev Co-authored-by: David Byron --- .../plugins/web/publish/PluginPublishController.kt | 6 ++++-- .../spinnaker/gate/controllers/ProxyController.kt | 14 ++++++-------- gradle.properties | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/gate-plugins/src/main/kotlin/com/netflix/spinnaker/gate/plugins/web/publish/PluginPublishController.kt b/gate-plugins/src/main/kotlin/com/netflix/spinnaker/gate/plugins/web/publish/PluginPublishController.kt index 24cf4d28f0..234ec6c22c 100644 --- a/gate-plugins/src/main/kotlin/com/netflix/spinnaker/gate/plugins/web/publish/PluginPublishController.kt +++ b/gate-plugins/src/main/kotlin/com/netflix/spinnaker/gate/plugins/web/publish/PluginPublishController.kt @@ -28,10 +28,12 @@ import io.swagger.annotations.ApiOperation import java.lang.String.format import lombok.SneakyThrows import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody import org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping @@ -92,7 +94,7 @@ class PluginPublishController( .addFormDataPart( "plugin", format("%s-%s.zip", pluginId, pluginVersion), - RequestBody.create(MediaType.parse("application/octet-stream"), body) + body.toRequestBody(("application/octet-stream").toMediaType()) ) .build() ) @@ -100,7 +102,7 @@ class PluginPublishController( val response = okHttpClient.newCall(request).execute() if (!response.isSuccessful) { - val reason = response.body()?.string() ?: "Unknown reason: ${response.code()}" + val reason = response.body?.string() ?: "Unknown reason: ${response.code}" throw SystemException("Failed to upload plugin binary: $reason") } }.call() diff --git a/gate-proxy/src/main/kotlin/com/netflix/spinnaker/gate/controllers/ProxyController.kt b/gate-proxy/src/main/kotlin/com/netflix/spinnaker/gate/controllers/ProxyController.kt index a6d7be313b..557e2e39aa 100644 --- a/gate-proxy/src/main/kotlin/com/netflix/spinnaker/gate/controllers/ProxyController.kt +++ b/gate-proxy/src/main/kotlin/com/netflix/spinnaker/gate/controllers/ProxyController.kt @@ -26,8 +26,9 @@ import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException import com.netflix.spinnaker.kork.web.interceptors.Criticality import com.netflix.spinnaker.security.AuthenticatedRequest import okhttp3.Request -import okhttp3.RequestBody import okhttp3.internal.http.HttpMethod +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody import java.net.SocketException import java.util.stream.Collectors import javax.servlet.http.HttpServletRequest @@ -120,7 +121,7 @@ class ProxyController( .toString() .substringAfter("/proxies/$proxyId") - val proxiedUrlBuilder = Request.Builder().url(proxyConfig.uri + proxyPath).build().url().newBuilder() + val proxiedUrlBuilder = Request.Builder().url(proxyConfig.uri + proxyPath).build().url.newBuilder() for ((key, value) in requestParams) { proxiedUrlBuilder.addQueryParameter(key, value) } @@ -134,10 +135,7 @@ class ProxyController( val method = request.method val body = if (HttpMethod.permitsRequestBody(method) && request.contentType != null) { - RequestBody.create( - okhttp3.MediaType.parse(request.contentType), - request.reader.lines().collect(Collectors.joining(System.lineSeparator())) - ) + request.reader.lines().collect(Collectors.joining(System.lineSeparator())).toRequestBody(request.contentType.toMediaType()) } else { null } @@ -145,9 +143,9 @@ class ProxyController( val response = proxy.okHttpClient.newCall( Request.Builder().url(proxiedUrl).method(method, body).build() ).execute() - statusCode = response.code() + statusCode = response.code contentType = response.header("Content-Type") ?: contentType - responseBody = response.body()?.string() ?: "" + responseBody = response.body?.string() ?: "" } catch (e: SocketException) { log.error("Exception processing proxy request", e) statusCode = HttpStatus.GATEWAY_TIMEOUT.value() diff --git a/gradle.properties b/gradle.properties index c1a6655a7e..4fc8ebc2c3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.177.0 +korkVersion=7.178.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.28.0 From eb67d73e9503f9c117a333479cb7e864e0a2a372 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 14 Jun 2023 12:19:21 -0400 Subject: [PATCH 083/182] chore(dependencies): Autobump spinnakerGradleVersion (#1668) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4fc8ebc2c3..cd06015f1a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.178.0 kotlinVersion=1.4.0 org.gradle.parallel=true -spinnakerGradleVersion=8.28.0 +spinnakerGradleVersion=8.29.0 targetJava11=true # To enable a composite reference to a project, set the From bf40abf63d2bc5bf2d4b89d202d29a5545a63f11 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 16 Jun 2023 05:48:18 -0400 Subject: [PATCH 084/182] chore(dependencies): Autobump spinnakerGradleVersion (#1669) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cd06015f1a..eb0ac0e245 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.178.0 kotlinVersion=1.4.0 org.gradle.parallel=true -spinnakerGradleVersion=8.29.0 +spinnakerGradleVersion=8.30.0 targetJava11=true # To enable a composite reference to a project, set the From 9e3c69e2bae88e32ea36b2b7d919931233771be8 Mon Sep 17 00:00:00 2001 From: Sandesh <30489233+j-sandy@users.noreply.github.com> Date: Sat, 17 Jun 2023 15:35:03 +0530 Subject: [PATCH 085/182] cleanup(preview): remove preview feature of version ordering (VERSION_ORDERING_V2) for gradle dependencies (#1670) With gradle 6.x, used [feature preview API](https://docs.gradle.org/7.6.1/userguide/feature_lifecycle.html#feature_preview) to take advantage of [improvised dependency version ordering](https://docs.gradle.org/6.5/release-notes.html#improved-dependency-version-ordering). This feature is made public for gradle 7.x. So, removing it from settings.gradle as we moved to gradle 7.6.1 --- settings.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/settings.gradle b/settings.gradle index 89088475ca..debb60fc5a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,8 +22,6 @@ } } -enableFeaturePreview("VERSION_ORDERING_V2") - rootProject.name = "gate" include "gate-api", From efe816af358e7889ab8ac3982b1a16890b89d3dd Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 26 Jun 2023 13:12:12 -0400 Subject: [PATCH 086/182] chore(dependencies): Autobump korkVersion (#1672) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index eb0ac0e245..d513e82e6a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.178.0 +korkVersion=7.179.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.30.0 From fff621ad5ecfbb6b973b001567aa37eda83c7f0e Mon Sep 17 00:00:00 2001 From: Matt <6519811+mattgogerly@users.noreply.github.com> Date: Wed, 28 Jun 2023 21:06:44 +0100 Subject: [PATCH 087/182] fix(retrofit): use OkHttpClient from Kork (#1673) --- .../gate/config/RetrofitConfig.groovy | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RetrofitConfig.groovy diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RetrofitConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RetrofitConfig.groovy deleted file mode 100644 index 5814ed8afe..0000000000 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RetrofitConfig.groovy +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.config - -import com.netflix.spinnaker.config.OkHttp3ClientConfiguration -import okhttp3.OkHttpClient -import org.springframework.beans.factory.config.ConfigurableBeanFactory -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Scope - -@Configuration -class RetrofitConfig { - @Bean - @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) - OkHttpClient okHttpClient(OkHttp3ClientConfiguration okHttpClientConfig) { - return okHttpClientConfig.create().build() - } - -} From 04d808c66189d8218fc0d68deab31fe29d7d5756 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Thu, 29 Jun 2023 15:52:46 -0500 Subject: [PATCH 088/182] Migration of various Groovy classes to Java (#1663) * refactor(core): Rewrite some Groovy as Java * refactor(core): Rewrite AuthConfig to Java * refactor(core): Rewrite CorsFilter in Java * refactor(core): Rewrite GateOriginValidator in Java * refactor(core): Rewrite Slf4jRetrofitLogger in Java * refactor(core): Rewrite Headers in Java * refactor(core): Rewrite AnonymousConfig in Java * fix(web): Use Jackson2ObjectMapperBuilder in GateConfig * refactor(web): Fix raw type usage * refactor(x509): Convert X509Config to Java * refactor(x509): Convert OidRolesExtractor to Java And add some tests that generate certificates directly. * chore(core): Revert copyright notice to original * refactor(core): Move Java files to src/main/java * chore(core): Add removed comments back * fix(core): Add null check to PermissionRevokingLogoutSuccessHandler * refactor(x509): Move Java classes into `src/main/java` * refactor(saml): Rewrite SAML Groovy code in Java * refactor(core): Move Java classes to src/main/java * refactor(web): Centralize configuration properties enablement * chore(x509): Use more appropriate property injection * refactor(core): Rewrite PermissionService in Java --- .../gate/security/basic/BasicAuthConfig.java | 6 - .../spinnaker/gate/config/AuthConfig.groovy | 148 -------- .../spinnaker/gate/filters/CorsFilter.groovy | 69 ---- .../gate/filters/FiatSessionFilter.groovy | 108 ------ .../gate/filters/GateOriginValidator.groovy | 88 ----- .../security/anonymous/AnonymousConfig.groovy | 91 ----- .../gate/services/PermissionService.groovy | 201 ---------- .../spinnaker/gate/config/AuthConfig.java | 120 ++++++ .../spinnaker/gate/config/Headers.java} | 6 +- .../gate/config/MultiAuthSupport.java | 0 ...ermissionRevokingLogoutSuccessHandler.java | 59 +++ .../gate/config/RequestMatcherProvider.java | 0 .../spinnaker/gate/filters/CorsFilter.java | 63 ++++ .../gate/filters/FiatSessionFilter.java | 94 +++++ .../gate/filters/GateOriginValidator.java | 101 +++++ .../gate/filters/OriginValidator.java | 0 .../gate/retrofit/Slf4jRetrofitLogger.java} | 32 +- .../gate/retrofit/UpstreamBadRequest.java | 4 +- .../gate/security/AllowedAccountsSupport.java | 0 .../security/RequestIdentityExtractor.java | 0 .../gate/security/SpinnakerAuthConfig.java} | 16 +- .../gate/security/SpinnakerUser.java} | 18 +- .../SpringSecurityAnnotationConfig.java} | 11 +- .../security/anonymous/AnonymousConfig.java | 93 +++++ .../gate/services/PermissionService.java | 227 +++++++++++ .../ServiceAccountFilterConfigProps.java | 0 .../internal/GoogleCloudBuildTrigger.java | 0 .../gate/config/AuthConfigTest.groovy | 40 +- .../anonymous/AnonymousConfigSpec.groovy | 9 +- .../services/PermissionServiceSpec.groovy | 10 +- .../gate/security/iap/IapSsoConfig.java | 6 - .../gate/security/ldap/LdapSsoConfig.groovy | 6 - .../security/oauth2/OAuth2SsoConfig.groovy | 5 - gate-saml/gate-saml.gradle | 1 - .../gate/security/saml/SamlSsoConfig.groovy | 355 ------------------ .../gate/security/saml/SAMLSSOConfig.java | 125 ++++++ .../saml/SAMLSecurityConfigProperties.java | 159 ++++++++ .../security/saml/SAMLUserDetailsService.java | 228 +++++++++++ .../SAMLSecurityConfigPropertiesSpec.groovy | 2 +- .../spinnaker/gate/config/GateConfig.groovy | 65 +--- .../gate/config/RateLimiterConfig.java | 6 +- .../spinnaker/gate/config/GateCorsConfig.java | 9 +- .../src/main/resources/application.properties | 25 ++ gate-x509/gate-x509.gradle | 12 + .../security/x509/OidRolesExtractor.groovy | 45 --- .../gate/security/x509/X509Config.groovy | 78 ---- .../gate/security/x509/OidRolesExtractor.java | 71 ++++ .../gate/security/x509/X509Config.java | 79 ++++ .../security/x509/X509IdentityExtractor.java | 0 .../security/x509/X509RolesExtractor.java | 0 .../x509/X509UserIdentifierExtractor.java | 0 .../x509/OidRolesExtractorSpec.groovy | 53 +++ 52 files changed, 1597 insertions(+), 1347 deletions(-) delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/AuthConfig.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/filters/CorsFilter.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/filters/FiatSessionFilter.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/filters/GateOriginValidator.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/anonymous/AnonymousConfig.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/PermissionService.groovy create mode 100644 gate-core/src/main/java/com/netflix/spinnaker/gate/config/AuthConfig.java rename gate-core/src/main/{groovy/com/netflix/spinnaker/gate/config/Headers.groovy => java/com/netflix/spinnaker/gate/config/Headers.java} (87%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/config/MultiAuthSupport.java (100%) create mode 100644 gate-core/src/main/java/com/netflix/spinnaker/gate/config/PermissionRevokingLogoutSuccessHandler.java rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/config/RequestMatcherProvider.java (100%) create mode 100644 gate-core/src/main/java/com/netflix/spinnaker/gate/filters/CorsFilter.java create mode 100644 gate-core/src/main/java/com/netflix/spinnaker/gate/filters/FiatSessionFilter.java create mode 100644 gate-core/src/main/java/com/netflix/spinnaker/gate/filters/GateOriginValidator.java rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/filters/OriginValidator.java (100%) rename gate-core/src/main/{groovy/com/netflix/spinnaker/gate/retrofit/Slf4jRetrofitLogger.groovy => java/com/netflix/spinnaker/gate/retrofit/Slf4jRetrofitLogger.java} (56%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/retrofit/UpstreamBadRequest.java (94%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/security/AllowedAccountsSupport.java (100%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/security/RequestIdentityExtractor.java (100%) rename gate-core/src/main/{groovy/com/netflix/spinnaker/gate/security/SpinnakerAuthConfig.groovy => java/com/netflix/spinnaker/gate/security/SpinnakerAuthConfig.java} (70%) rename gate-core/src/main/{groovy/com/netflix/spinnaker/gate/security/SpinnakerUser.groovy => java/com/netflix/spinnaker/gate/security/SpinnakerUser.java} (71%) rename gate-core/src/main/{groovy/com/netflix/spinnaker/gate/security/SpringSecurityAnnotationConfig.groovy => java/com/netflix/spinnaker/gate/security/SpringSecurityAnnotationConfig.java} (80%) create mode 100644 gate-core/src/main/java/com/netflix/spinnaker/gate/security/anonymous/AnonymousConfig.java create mode 100644 gate-core/src/main/java/com/netflix/spinnaker/gate/services/PermissionService.java rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/services/ServiceAccountFilterConfigProps.java (100%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/services/internal/GoogleCloudBuildTrigger.java (100%) delete mode 100644 gate-saml/src/main/groovy/com/netflix/spinnaker/gate/security/saml/SamlSsoConfig.groovy create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSSOConfig.java create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigProperties.java create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLUserDetailsService.java create mode 100644 gate-web/src/main/resources/application.properties delete mode 100644 gate-x509/src/main/groovy/com/netflix/spinnaker/gate/security/x509/OidRolesExtractor.groovy delete mode 100644 gate-x509/src/main/groovy/com/netflix/spinnaker/gate/security/x509/X509Config.groovy create mode 100644 gate-x509/src/main/java/com/netflix/spinnaker/gate/security/x509/OidRolesExtractor.java create mode 100644 gate-x509/src/main/java/com/netflix/spinnaker/gate/security/x509/X509Config.java rename gate-x509/src/main/{groovy => java}/com/netflix/spinnaker/gate/security/x509/X509IdentityExtractor.java (100%) rename gate-x509/src/main/{groovy => java}/com/netflix/spinnaker/gate/security/x509/X509RolesExtractor.java (100%) rename gate-x509/src/main/{groovy => java}/com/netflix/spinnaker/gate/security/x509/X509UserIdentifierExtractor.java (100%) diff --git a/gate-basic/src/main/java/com/netflix/spinnaker/gate/security/basic/BasicAuthConfig.java b/gate-basic/src/main/java/com/netflix/spinnaker/gate/security/basic/BasicAuthConfig.java index 87cde9c3a2..4816a70761 100644 --- a/gate-basic/src/main/java/com/netflix/spinnaker/gate/security/basic/BasicAuthConfig.java +++ b/gate-basic/src/main/java/com/netflix/spinnaker/gate/security/basic/BasicAuthConfig.java @@ -24,7 +24,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; @@ -62,9 +61,4 @@ protected void configure(HttpSecurity http) throws Exception { .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")); authConfig.configure(http); } - - @Override - public void configure(WebSecurity web) throws Exception { - authConfig.configure(web); - } } diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/AuthConfig.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/AuthConfig.groovy deleted file mode 100644 index 01399bd769..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/AuthConfig.groovy +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.config - -import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties -import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator -import com.netflix.spinnaker.fiat.shared.FiatStatus -import com.netflix.spinnaker.gate.filters.FiatSessionFilter -import com.netflix.spinnaker.gate.services.PermissionService -import com.netflix.spinnaker.gate.services.ServiceAccountFilterConfigProps -import com.netflix.spinnaker.security.User -import groovy.util.logging.Slf4j -import org.springframework.beans.factory.InitializingBean -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.autoconfigure.security.SecurityProperties -import org.springframework.boot.context.properties.EnableConfigurationProperties -import org.springframework.context.annotation.Configuration -import org.springframework.http.HttpMethod -import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.builders.WebSecurity -import org.springframework.security.core.Authentication -import org.springframework.security.web.authentication.AnonymousAuthenticationFilter -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler -import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler -import org.springframework.stereotype.Component - -import javax.servlet.Filter -import javax.servlet.ServletException -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse - -@Slf4j -@Configuration -@EnableConfigurationProperties([ServiceConfiguration, ServiceAccountFilterConfigProps]) -class AuthConfig { - - @Autowired - PermissionRevokingLogoutSuccessHandler permissionRevokingLogoutSuccessHandler - - @Autowired - SecurityProperties securityProperties - - @Autowired - FiatClientConfigurationProperties configProps - - @Autowired - FiatStatus fiatStatus - - @Autowired - FiatPermissionEvaluator permissionEvaluator - - @Autowired - RequestMatcherProvider requestMatcherProvider - - @Value('${security.debug:false}') - boolean securityDebug - - @Value('${fiat.session-filter.enabled:true}') - boolean fiatSessionFilterEnabled - - @Value('${security.webhooks.default-auth-enabled:false}') - boolean webhookDefaultAuthEnabled - - void configure(HttpSecurity http) throws Exception { - // @formatter:off - http - .requestMatcher(requestMatcherProvider.requestMatcher()) - .authorizeRequests() - .antMatchers("/error").permitAll() - .antMatchers('/favicon.ico').permitAll() - .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() - .antMatchers(PermissionRevokingLogoutSuccessHandler.LOGGED_OUT_URL).permitAll() - .antMatchers('/auth/user').permitAll() - .antMatchers('/plugins/deck/**').permitAll() - .antMatchers(HttpMethod.POST, '/webhooks/**').permitAll() - .antMatchers(HttpMethod.POST, '/notifications/callbacks/**').permitAll() - .antMatchers(HttpMethod.POST, '/managed/notifications/callbacks/**').permitAll() - .antMatchers('/health').permitAll() - .antMatchers('/**').authenticated() - if (fiatSessionFilterEnabled) { - Filter fiatSessionFilter = new FiatSessionFilter( - fiatSessionFilterEnabled, - fiatStatus, - permissionEvaluator) - - http.addFilterBefore(fiatSessionFilter, AnonymousAuthenticationFilter.class) - } - - if (webhookDefaultAuthEnabled) { - http.authorizeRequests().antMatchers(HttpMethod.POST, '/webhooks/**').authenticated() - } - - http.logout() - .logoutUrl("/auth/logout") - .logoutSuccessHandler(permissionRevokingLogoutSuccessHandler) - .permitAll() - .and() - .csrf() - .disable() - // @formatter:on - } - - void configure(WebSecurity web) throws Exception { - web.debug(securityDebug) - } - - @Component - static class PermissionRevokingLogoutSuccessHandler implements LogoutSuccessHandler, InitializingBean { - - static final String LOGGED_OUT_URL = "/auth/loggedOut" - - @Autowired - PermissionService permissionService - - SimpleUrlLogoutSuccessHandler delegate = new SimpleUrlLogoutSuccessHandler(); - - @Override - void afterPropertiesSet() throws Exception { - delegate.setDefaultTargetUrl(LOGGED_OUT_URL) - } - - @Override - void onLogoutSuccess(HttpServletRequest request, - HttpServletResponse response, - Authentication authentication) throws IOException, ServletException { - def username = (authentication?.getPrincipal() as User)?.username - if (username) { - permissionService.logout(username) - } - delegate.onLogoutSuccess(request, response, authentication) - } - } -} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/filters/CorsFilter.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/filters/CorsFilter.groovy deleted file mode 100644 index 27cbd48c65..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/filters/CorsFilter.groovy +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2017 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.filters - -import com.netflix.spinnaker.gate.config.Headers -import groovy.util.logging.Slf4j -import net.logstash.logback.argument.StructuredArguments - -import javax.servlet.Filter -import javax.servlet.FilterChain -import javax.servlet.FilterConfig -import javax.servlet.ServletException -import javax.servlet.ServletRequest -import javax.servlet.ServletResponse -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse - -@Slf4j -class CorsFilter implements Filter { - - private final OriginValidator originValidator - - CorsFilter(OriginValidator originValidator) { - this.originValidator = originValidator - } - - @Override - void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) - throws IOException, ServletException { - HttpServletResponse response = (HttpServletResponse) res - HttpServletRequest request = (HttpServletRequest) req - String origin = request.getHeader("Origin") - if (!originValidator.isValidOrigin(origin)) { - origin = '*' - } else if (!originValidator.isExpectedOrigin(origin)) { - log.debug("CORS request with full authentication support from non-default origin header. Request Method: {}. Origin header: {}.", - StructuredArguments.kv("requestMethod", request.getMethod()), - StructuredArguments.kv("origin", origin)) - } - response.setHeader("Access-Control-Allow-Credentials", "true") - response.setHeader("Access-Control-Allow-Origin", origin) - response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT, PATCH") - response.setHeader("Access-Control-Max-Age", "3600") - response.setHeader("Access-Control-Allow-Headers", "x-requested-with, content-type, authorization, X-RateLimit-App, X-Spinnaker-Priority") - response.setHeader("Access-Control-Expose-Headers", [Headers.AUTHENTICATION_REDIRECT_HEADER_NAME].join(", ")) - chain.doFilter(req, res) - } - - @Override - void init(FilterConfig filterConfig) {} - - @Override - void destroy() {} - -} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/filters/FiatSessionFilter.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/filters/FiatSessionFilter.groovy deleted file mode 100644 index 7e07baa48b..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/filters/FiatSessionFilter.groovy +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2017 Google, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.filters - -import com.netflix.spinnaker.fiat.model.UserPermission -import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator -import com.netflix.spinnaker.fiat.shared.FiatStatus -import com.netflix.spinnaker.security.AuthenticatedRequest -import groovy.util.logging.Slf4j -import org.springframework.security.core.context.SecurityContextHolder - -import javax.servlet.Filter -import javax.servlet.FilterChain -import javax.servlet.FilterConfig -import javax.servlet.ServletException -import javax.servlet.ServletRequest -import javax.servlet.ServletResponse -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpSession - -import static net.logstash.logback.argument.StructuredArguments.value - -@Slf4j -class FiatSessionFilter implements Filter { - - boolean enabled - - FiatStatus fiatStatus - - FiatPermissionEvaluator permissionEvaluator - - FiatSessionFilter(boolean enabled, - FiatStatus fiatStatus, - FiatPermissionEvaluator permissionEvaluator) { - this.enabled = enabled - this.fiatStatus = fiatStatus - this.permissionEvaluator = permissionEvaluator - } - - /** - * This filter checks if the user has an entry in Fiat, and if not, forces them to re-login. This - * is handy for (re)populating the Fiat user repo for a deployment with existing users & sessions. - */ - @Override - void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - UserPermission.View fiatPermission = null - - if (fiatStatus.isEnabled() && this.enabled) { - String user = AuthenticatedRequest.getSpinnakerUser().orElse(null) - log.debug("Fiat session filter - found user: ${user}") - - if (user != null) { - fiatPermission = permissionEvaluator.getPermission(user) - if (fiatPermission == null) { - HttpServletRequest httpReq = (HttpServletRequest) request - HttpSession session = httpReq.getSession(false) - if (session != null) { - log.info("Invalidating user '{}' session '{}' because Fiat permission was not found.", - value("user", user), - value("session", session)) - session.invalidate() - SecurityContextHolder.clearContext() - } - } - } else { - log.warn("Authenticated user was not present in authenticated request. Check authentication settings.") - } - } else { - if (log.isDebugEnabled()) { - log.debug("Skipping Fiat session filter: Both `services.fiat.enabled` " + - "(${fiatStatus.isEnabled()}) and the FiatSessionFilter (${this.enabled}) " + - "need to be enabled.") - } - } - - try { - chain.doFilter(request, response) - } finally { - if (fiatPermission != null && fiatPermission.isLegacyFallback()) { - log.info("Invalidating fallback permissions for ${fiatPermission.name}") - permissionEvaluator.invalidatePermission(fiatPermission.name) - } - } - } - - @Override - void init(FilterConfig filterConfig) throws ServletException { - } - - @Override - void destroy() { - } -} - diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/filters/GateOriginValidator.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/filters/GateOriginValidator.groovy deleted file mode 100644 index 5104a7df0a..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/filters/GateOriginValidator.groovy +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2017 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.filters - -import java.util.regex.Pattern - -class GateOriginValidator implements OriginValidator { - private final URI deckUri - private final Pattern redirectHosts - private final Pattern allowedOrigins - private final boolean expectLocalhost - - GateOriginValidator(String deckUri, String redirectHostsPattern, String allowedOriginsPattern, boolean expectLocalhost) { - this.deckUri = deckUri ? deckUri.toURI() : null - this.redirectHosts = redirectHostsPattern ? Pattern.compile(redirectHostsPattern) : null - this.allowedOrigins = allowedOriginsPattern ? Pattern.compile(allowedOriginsPattern) : null - this.expectLocalhost = expectLocalhost - } - - boolean isExpectedOrigin(String origin) { - if (!origin) { - return false - } - - if (!deckUri) { - return false - } - - try { - def uri = URI.create(origin) - if (!(uri.scheme && uri.host)) { - return false - } - - if (expectLocalhost && uri.host.equalsIgnoreCase("localhost")) { - return true - } - - return deckUri.scheme == uri.scheme && deckUri.host == uri.host && deckUri.port == uri.port - } catch (URISyntaxException use) { - return false - } - } - - @Override - boolean isValidOrigin(String origin) { - if (!origin) { - return false - } - - try { - def uri = URI.create(origin) - if (!(uri.scheme && uri.host)) { - return false - } - - if (allowedOrigins) { - return allowedOrigins.matcher(origin).matches() - } - - if (redirectHosts) { - return redirectHosts.matcher(uri.host).matches() - } - - if (!deckUri) { - return false - } - - return deckUri.scheme == uri.scheme && deckUri.host == uri.host && deckUri.port == uri.port - } catch (URISyntaxException) { - return false - } - } -} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/anonymous/AnonymousConfig.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/anonymous/AnonymousConfig.groovy deleted file mode 100644 index 3e073e32ac..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/anonymous/AnonymousConfig.groovy +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2015 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.security.anonymous - -import com.netflix.spinnaker.fiat.shared.FiatStatus -import com.netflix.spinnaker.gate.security.SpinnakerAuthConfig -import com.netflix.spinnaker.gate.services.CredentialsService -import com.netflix.spinnaker.security.User -import groovy.util.logging.Slf4j -import org.apache.commons.lang3.exception.ExceptionUtils -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean -import org.springframework.context.annotation.Configuration -import org.springframework.core.Ordered -import org.springframework.core.annotation.Order -import org.springframework.scheduling.annotation.Scheduled -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 java.util.concurrent.CopyOnWriteArrayList - -/** - * Requires auth.anonymous.enabled to be true in Fiat configs to work properly. This - * is because anonymous users are a special permissions case, because the "user" doesn't actually - * exist in the backing UserRolesProvider. - */ -@ConditionalOnMissingBean(annotation = SpinnakerAuthConfig.class) -@Configuration -@Slf4j -@EnableWebSecurity -@Order(Ordered.LOWEST_PRECEDENCE) -class AnonymousConfig extends WebSecurityConfigurerAdapter { - static String key = "spinnaker-anonymous" - static String defaultEmail = "anonymous" - - @Autowired - CredentialsService credentialsService - - @Autowired - FiatStatus fiatStatus - - List anonymousAllowedAccounts = new CopyOnWriteArrayList<>() - - void configure(HttpSecurity http) { - updateAnonymousAccounts() - // Not using the ImmutableUser version in order to update allowedAccounts. - def principal = new User(email: defaultEmail, allowedAccounts: anonymousAllowedAccounts) - - http - .anonymous() - .key(key) - .principal(principal) - .and() - .csrf() - .disable() - } - - @Scheduled(fixedDelay = 60000L) - void updateAnonymousAccounts() { - if (fiatStatus.isEnabled()) { - return - } - - try { - def newAnonAccounts = credentialsService.getAccountNames([]) ?: [] - - def toAdd = newAnonAccounts - anonymousAllowedAccounts - def toRemove = anonymousAllowedAccounts - newAnonAccounts - - anonymousAllowedAccounts.removeAll(toRemove) - anonymousAllowedAccounts.addAll(toAdd) - } catch (Exception e) { - log.warn("Error while updating anonymous accounts", e) - } - } -} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/PermissionService.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/PermissionService.groovy deleted file mode 100644 index da34d17409..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/PermissionService.groovy +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright 2016 Google, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.services - - -import com.netflix.spinnaker.fiat.model.UserPermission -import com.netflix.spinnaker.fiat.model.resources.Role -import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator -import com.netflix.spinnaker.fiat.shared.FiatService -import com.netflix.spinnaker.fiat.shared.FiatStatus -import com.netflix.spinnaker.gate.security.SpinnakerUser -import com.netflix.spinnaker.gate.services.internal.ExtendedFiatService -import com.netflix.spinnaker.kork.core.RetrySupport -import com.netflix.spinnaker.kork.exceptions.SpinnakerException -import com.netflix.spinnaker.kork.exceptions.SystemException -import com.netflix.spinnaker.security.AuthenticatedRequest -import com.netflix.spinnaker.security.User -import groovy.transform.PackageScope -import groovy.util.logging.Slf4j -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.http.HttpStatus -import org.springframework.stereotype.Component -import retrofit.RetrofitError - -import javax.annotation.Nonnull -import java.time.Duration - -import static com.netflix.spinnaker.gate.retrofit.UpstreamBadRequest.classifyError - -@Slf4j -@Component -class PermissionService { - - @Autowired - FiatService fiatService - - @Autowired - ExtendedFiatService extendedFiatService - - @Autowired - ServiceAccountFilterConfigProps serviceAccountFilterConfigProps - - @Autowired - @Qualifier("fiatLoginService") - Optional fiatLoginService - - @Autowired - FiatPermissionEvaluator permissionEvaluator - - @Autowired - FiatStatus fiatStatus - - boolean isEnabled() { - return fiatStatus.isEnabled() - } - - private FiatService getFiatServiceForLogin() { - return fiatLoginService.orElse(fiatService); - } - - void login(String userId) { - if (fiatStatus.isEnabled()) { - try { - AuthenticatedRequest.allowAnonymous({ - fiatServiceForLogin.loginUser(userId, "") - permissionEvaluator.invalidatePermission(userId) - }) - } catch (RetrofitError e) { - throw classifyError(e) - } - } - } - - void loginWithRoles(String userId, Collection roles) { - if (fiatStatus.isEnabled()) { - try { - AuthenticatedRequest.allowAnonymous({ - fiatServiceForLogin.loginWithRoles(userId, roles) - permissionEvaluator.invalidatePermission(userId) - }) - } catch (RetrofitError e) { - throw classifyError(e) - } - } - } - - void logout(String userId) { - if (fiatStatus.isEnabled()) { - try { - fiatServiceForLogin.logoutUser(userId) - permissionEvaluator.invalidatePermission(userId) - } catch (RetrofitError e) { - throw classifyError(e) - } - } - } - - void sync() { - if (fiatStatus.isEnabled()) { - try { - fiatServiceForLogin.sync(Collections.emptyList()) - } catch (RetrofitError e) { - throw classifyError(e) - } - } - } - - Set getRoles(String userId) { - if (!fiatStatus.isEnabled()) { - return [] - } - try { - return permissionEvaluator.getPermission(userId)?.roles ?: [] - } catch (RetrofitError e) { - throw classifyError(e) - } - } - - //VisibleForTesting - @PackageScope List lookupServiceAccounts(String userId) { - try { - return extendedFiatService.getUserServiceAccounts(userId) - } catch (RetrofitError re) { - boolean notFound = re.response?.status == HttpStatus.NOT_FOUND.value() - if (notFound) { - return [] - } - boolean shouldRetry = re.response == null || HttpStatus.valueOf(re.response.status).is5xxServerError() - throw new SystemException("getUserServiceAccounts failed", re).setRetryable(shouldRetry) - } - } - - List getServiceAccountsForApplication(@SpinnakerUser User user, @Nonnull String application) { - if (!serviceAccountFilterConfigProps.enabled || - !user || - !application || - !fiatStatus.enabled || - serviceAccountFilterConfigProps.matchAuthorizations.isEmpty()) { - return getServiceAccounts(user); - } - - List filteredServiceAccounts - RetrySupport retry = new RetrySupport() - try { - List serviceAccounts = retry.retry({ lookupServiceAccounts(user.username) }, 3, Duration.ofMillis(50), false) - - filteredServiceAccounts = serviceAccounts.findResults { - if (it.applications.find { it.name.equalsIgnoreCase(application) && it.authorizations.find { serviceAccountFilterConfigProps.matchAuthorizations.contains(it) } }) { - return it.name - } - return null - } - } catch (SpinnakerException se) { - log.error("failed to lookup user {} service accounts for application {}, falling back to all user service accounts", user, application, se) - return getServiceAccounts(user) - } - - // if there are no service accounts for the requested application, fall back to the full list of service accounts for the user - // to avoid a chicken and egg problem of trying to enable security for the first time on an application - return filteredServiceAccounts ?: getServiceAccounts(user) - } - - List getServiceAccounts(@SpinnakerUser User user) { - - if (!user) { - log.debug("getServiceAccounts: Spinnaker user is null.") - return [] - } - - if (!fiatStatus.isEnabled()) { - log.debug("getServiceAccounts: Fiat disabled.") - return [] - } - - try { - UserPermission.View view = permissionEvaluator.getPermission(user.username) - return view.getServiceAccounts().collect { it.name } - } catch (RetrofitError re) { - throw classifyError(re) - } - } - - boolean isAdmin(String userId) { - return permissionEvaluator.getPermission(userId)?.isAdmin() - } -} diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/config/AuthConfig.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/config/AuthConfig.java new file mode 100644 index 0000000000..a1c68c287c --- /dev/null +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/config/AuthConfig.java @@ -0,0 +1,120 @@ +/* + * Copyright 2016 Netflix, Inc. + * Copyright 2023 Apple, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.config; + +import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties; +import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator; +import com.netflix.spinnaker.fiat.shared.FiatStatus; +import com.netflix.spinnaker.gate.filters.FiatSessionFilter; +import com.netflix.spinnaker.gate.services.ServiceAccountFilterConfigProps; +import com.netflix.spinnaker.kork.annotations.NonnullByDefault; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; + +@Configuration +@EnableConfigurationProperties({ + ServiceConfiguration.class, + ServiceAccountFilterConfigProps.class, + FiatClientConfigurationProperties.class, + DynamicRoutingConfigProperties.class +}) +@NonnullByDefault +@RequiredArgsConstructor +public class AuthConfig { + private final PermissionRevokingLogoutSuccessHandler permissionRevokingLogoutSuccessHandler; + private final FiatStatus fiatStatus; + private final FiatPermissionEvaluator permissionEvaluator; + private final RequestMatcherProvider requestMatcherProvider; + + @Setter( + onMethod_ = {@Autowired}, + onParam_ = {@Value("${security.debug:false}")}) + private boolean securityDebug; + + @Setter( + onMethod_ = {@Autowired}, + onParam_ = {@Value("${fiat.session-filter.enabled:true}")}) + private boolean fiatSessionFilterEnabled; + + @Setter( + onMethod_ = {@Autowired}, + onParam_ = {@Value("${security.webhooks.default-auth-enabled:false}")}) + private boolean webhookDefaultAuthEnabled; + + @Bean + public WebSecurityCustomizer securityDebugCustomizer() { + return web -> web.debug(securityDebug); + } + + public void configure(HttpSecurity http) throws Exception { + http.requestMatcher(requestMatcherProvider.requestMatcher()) + .authorizeRequests( + registry -> { + registry + .antMatchers("/error") + .permitAll() + .antMatchers("/favicon.ico") + .permitAll() + .antMatchers(HttpMethod.OPTIONS, "/**") + .permitAll() + .antMatchers(PermissionRevokingLogoutSuccessHandler.LOGGED_OUT_URL) + .permitAll() + .antMatchers("/auth/user") + .permitAll() + .antMatchers("/plugins/deck/**") + .permitAll(); + var webhooks = registry.antMatchers(HttpMethod.POST, "/webhooks/**"); + if (webhookDefaultAuthEnabled) { + webhooks.authenticated(); + } else { + webhooks.permitAll(); + } + registry + .antMatchers(HttpMethod.POST, "/notifications/callbacks/**") + .permitAll() + .antMatchers(HttpMethod.POST, "/managed/notifications/callbacks/**") + .permitAll() + .antMatchers("/health") + .permitAll() + .antMatchers("/**") + .authenticated(); + }) + .logout( + logout -> + logout + .logoutUrl("/auth/logout") + .logoutSuccessHandler(permissionRevokingLogoutSuccessHandler) + .permitAll()) + .csrf(AbstractHttpConfigurer::disable); + + if (fiatSessionFilterEnabled) { + var filter = new FiatSessionFilter(fiatStatus, permissionEvaluator); + http.addFilterBefore(filter, AnonymousAuthenticationFilter.class); + } + } +} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/Headers.groovy b/gate-core/src/main/java/com/netflix/spinnaker/gate/config/Headers.java similarity index 87% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/Headers.groovy rename to gate-core/src/main/java/com/netflix/spinnaker/gate/config/Headers.java index 4c66162b0e..a09ed8da96 100644 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/Headers.groovy +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/config/Headers.java @@ -14,8 +14,8 @@ * limitations under the License. */ -package com.netflix.spinnaker.gate.config +package com.netflix.spinnaker.gate.config; -class Headers { - public static final String AUTHENTICATION_REDIRECT_HEADER_NAME = "X-AUTH-REDIRECT-URL" +public class Headers { + public static final String AUTHENTICATION_REDIRECT_HEADER_NAME = "X-AUTH-REDIRECT-URL"; } diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/MultiAuthSupport.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/config/MultiAuthSupport.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/MultiAuthSupport.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/config/MultiAuthSupport.java diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/config/PermissionRevokingLogoutSuccessHandler.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/config/PermissionRevokingLogoutSuccessHandler.java new file mode 100644 index 0000000000..4a4b48c057 --- /dev/null +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/config/PermissionRevokingLogoutSuccessHandler.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016 Netflix, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.config; + +import com.netflix.spinnaker.gate.services.PermissionService; +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PermissionRevokingLogoutSuccessHandler + implements LogoutSuccessHandler, InitializingBean { + public static final String LOGGED_OUT_URL = "/auth/loggedOut"; + + private final PermissionService permissionService; + private final SimpleUrlLogoutSuccessHandler delegate = new SimpleUrlLogoutSuccessHandler(); + + @Override + public void afterPropertiesSet() throws Exception { + delegate.setDefaultTargetUrl(LOGGED_OUT_URL); + } + + @Override + public void onLogoutSuccess( + HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException, ServletException { + if (authentication != null) { + var principal = authentication.getPrincipal(); + if (principal instanceof UserDetails) { + var username = ((UserDetails) principal).getUsername(); + permissionService.logout(username); + } + } + delegate.onLogoutSuccess(request, response, authentication); + } +} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/RequestMatcherProvider.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/config/RequestMatcherProvider.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/RequestMatcherProvider.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/config/RequestMatcherProvider.java diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/filters/CorsFilter.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/filters/CorsFilter.java new file mode 100644 index 0000000000..7331ff7393 --- /dev/null +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/filters/CorsFilter.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017 Netflix, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.filters; + +import static net.logstash.logback.argument.StructuredArguments.kv; + +import com.netflix.spinnaker.gate.config.Headers; +import com.netflix.spinnaker.kork.annotations.NonnullByDefault; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.web.filter.OncePerRequestFilter; + +@Log4j2 +@NonnullByDefault +@RequiredArgsConstructor +public class CorsFilter extends OncePerRequestFilter { + private final OriginValidator originValidator; + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + String origin = request.getHeader("Origin"); + if (!originValidator.isValidOrigin(origin)) { + origin = "*"; + } else if (!originValidator.isExpectedOrigin(origin)) { + log.debug( + "CORS request with full authentication support from non-default origin header. Request Method: {}. Origin header: {}.", + kv("requestMethod", request.getMethod()), + kv("origin", origin)); + } + + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT, PATCH"); + response.setHeader("Access-Control-Max-Age", "3600"); + response.setHeader( + "Access-Control-Allow-Headers", + "x-requested-with, content-type, authorization, X-RateLimit-App, X-Spinnaker-Priority"); + response.setHeader( + "Access-Control-Expose-Headers", Headers.AUTHENTICATION_REDIRECT_HEADER_NAME); + chain.doFilter(request, response); + } +} diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/filters/FiatSessionFilter.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/filters/FiatSessionFilter.java new file mode 100644 index 0000000000..52dcb1fc03 --- /dev/null +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/filters/FiatSessionFilter.java @@ -0,0 +1,94 @@ +/* + * Copyright 2017 Google, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.filters; + +import static net.logstash.logback.argument.StructuredArguments.value; + +import com.netflix.spinnaker.fiat.model.UserPermission; +import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator; +import com.netflix.spinnaker.fiat.shared.FiatStatus; +import com.netflix.spinnaker.kork.annotations.NonnullByDefault; +import com.netflix.spinnaker.security.AuthenticatedRequest; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import lombok.extern.log4j.Log4j2; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +@Log4j2 +@NonnullByDefault +public class FiatSessionFilter extends OncePerRequestFilter { + private final FiatStatus fiatStatus; + private final FiatPermissionEvaluator permissionEvaluator; + + public FiatSessionFilter(FiatStatus fiatStatus, FiatPermissionEvaluator permissionEvaluator) { + this.fiatStatus = fiatStatus; + this.permissionEvaluator = permissionEvaluator; + } + + /** + * This filter checks if the user has an entry in Fiat, and if not, forces them to re-login. This + * is handy for (re)populating the Fiat user repo for a deployment with existing users & sessions. + */ + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + UserPermission.View fiatPermission = null; + + if (fiatStatus.isEnabled()) { + final String user = AuthenticatedRequest.getSpinnakerUser().orElse(null); + log.debug("Fiat session filter - found user: {}", user); + + if (user != null) { + fiatPermission = permissionEvaluator.getPermission(user); + if (fiatPermission == null) { + HttpSession session = request.getSession(false); + if (session != null) { + log.info( + "Invalidating user '{}' session '{}' because Fiat permission was not found.", + value("user", user), + value("session", session)); + session.invalidate(); + SecurityContextHolder.clearContext(); + } + } + } else { + log.warn( + "Authenticated user was not present in authenticated request. Check authentication settings."); + } + + } else { + log.debug( + "Skipping Fiat session filter: Both `services.fiat.enabled` ({}) and the FiatSessionFilter need to be enabled.", + fiatStatus.isEnabled()); + } + + try { + chain.doFilter(request, response); + } finally { + if (fiatPermission != null && fiatPermission.isLegacyFallback()) { + log.info("Invalidating fallback permissions for {}", fiatPermission.getName()); + permissionEvaluator.invalidatePermission(fiatPermission.getName()); + } + } + } +} diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/filters/GateOriginValidator.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/filters/GateOriginValidator.java new file mode 100644 index 0000000000..5053879a72 --- /dev/null +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/filters/GateOriginValidator.java @@ -0,0 +1,101 @@ +/* + * Copyright 2017 Netflix, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.filters; + +import java.net.URI; +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import org.springframework.util.StringUtils; + +public class GateOriginValidator implements OriginValidator { + private final URI deckUri; + private final Pattern redirectHosts; + private final Pattern allowedOrigins; + private final boolean expectLocalhost; + + public GateOriginValidator( + @Nullable String deckUri, + @Nullable String redirectHostsPattern, + @Nullable String allowedOriginsPattern, + boolean expectLocalhost) { + this.deckUri = deckUri != null ? URI.create(deckUri) : null; + this.redirectHosts = + redirectHostsPattern != null ? Pattern.compile(redirectHostsPattern) : null; + this.allowedOrigins = + allowedOriginsPattern != null ? Pattern.compile(allowedOriginsPattern) : null; + this.expectLocalhost = expectLocalhost; + } + + public boolean isExpectedOrigin(String origin) { + if (!StringUtils.hasLength(origin)) { + return false; + } + + if (deckUri == null) { + return false; + } + + try { + URI uri = URI.create(origin); + if (!StringUtils.hasLength(uri.getScheme()) || !StringUtils.hasLength(uri.getHost())) { + return false; + } + + if (expectLocalhost && uri.getHost().equalsIgnoreCase("localhost")) { + return true; + } + + return deckUri.getScheme().equals(uri.getScheme()) + && deckUri.getHost().equals(uri.getHost()) + && deckUri.getPort() == uri.getPort(); + } catch (IllegalArgumentException ignored) { + return false; + } + } + + @Override + public boolean isValidOrigin(String origin) { + if (!StringUtils.hasLength(origin)) { + return false; + } + + try { + URI uri = URI.create(origin); + if (!StringUtils.hasLength(uri.getScheme()) || !StringUtils.hasLength(uri.getHost())) { + return false; + } + + if (allowedOrigins != null) { + return allowedOrigins.matcher(origin).matches(); + } + + if (redirectHosts != null) { + return redirectHosts.matcher(uri.getHost()).matches(); + } + + if (deckUri == null) { + return false; + } + + return deckUri.getScheme().equals(uri.getScheme()) + && deckUri.getHost().equals(uri.getHost()) + && deckUri.getPort() == uri.getPort(); + } catch (IllegalArgumentException ignored) { + return false; + } + } +} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/filters/OriginValidator.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/filters/OriginValidator.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/filters/OriginValidator.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/filters/OriginValidator.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/retrofit/Slf4jRetrofitLogger.groovy b/gate-core/src/main/java/com/netflix/spinnaker/gate/retrofit/Slf4jRetrofitLogger.java similarity index 56% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/retrofit/Slf4jRetrofitLogger.groovy rename to gate-core/src/main/java/com/netflix/spinnaker/gate/retrofit/Slf4jRetrofitLogger.java index 77cd103ebb..84a14175a0 100644 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/retrofit/Slf4jRetrofitLogger.groovy +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/retrofit/Slf4jRetrofitLogger.java @@ -14,25 +14,25 @@ * limitations under the License. */ -package com.netflix.spinnaker.gate.retrofit +package com.netflix.spinnaker.gate.retrofit; -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import retrofit.RestAdapter +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import retrofit.RestAdapter; -class Slf4jRetrofitLogger implements RestAdapter.Log { - private final Logger logger +public class Slf4jRetrofitLogger implements RestAdapter.Log { + public Slf4jRetrofitLogger(Class type) { + this(LoggerFactory.getLogger(type)); + } - public Slf4jRetrofitLogger(Class type) { - this(LoggerFactory.getLogger(type)) - } + public Slf4jRetrofitLogger(Logger logger) { + this.logger = logger; + } - public Slf4jRetrofitLogger(Logger logger) { - this.logger = logger - } + @Override + public void log(String message) { + logger.info(message); + } - @Override - void log(String message) { - logger.info(message) - } + private final Logger logger; } diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/retrofit/UpstreamBadRequest.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/retrofit/UpstreamBadRequest.java similarity index 94% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/retrofit/UpstreamBadRequest.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/retrofit/UpstreamBadRequest.java index a84e5762ef..e0b38769b0 100644 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/retrofit/UpstreamBadRequest.java +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/retrofit/UpstreamBadRequest.java @@ -48,7 +48,7 @@ public Object getError() { return error; } - public static Exception classifyError(RetrofitError error) { + public static RuntimeException classifyError(RetrofitError error) { if (error.getKind() == HTTP && error.getResponse().getStatus() < INTERNAL_SERVER_ERROR.value()) { return new UpstreamBadRequest(error); @@ -57,7 +57,7 @@ public static Exception classifyError(RetrofitError error) { } } - public static Exception classifyError( + public static RuntimeException classifyError( RetrofitError error, Collection supportedHttpStatuses) { if (error.getKind() == HTTP && supportedHttpStatuses.contains(error.getResponse().getStatus())) { diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/AllowedAccountsSupport.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/security/AllowedAccountsSupport.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/AllowedAccountsSupport.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/security/AllowedAccountsSupport.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/RequestIdentityExtractor.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/security/RequestIdentityExtractor.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/RequestIdentityExtractor.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/security/RequestIdentityExtractor.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/SpinnakerAuthConfig.groovy b/gate-core/src/main/java/com/netflix/spinnaker/gate/security/SpinnakerAuthConfig.java similarity index 70% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/SpinnakerAuthConfig.groovy rename to gate-core/src/main/java/com/netflix/spinnaker/gate/security/SpinnakerAuthConfig.java index 0afbad0a5b..c0f8d8e274 100644 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/SpinnakerAuthConfig.groovy +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/security/SpinnakerAuthConfig.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package com.netflix.spinnaker.gate.security +package com.netflix.spinnaker.gate.security; -import java.lang.annotation.ElementType -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy -import java.lang.annotation.Target +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** - * Any non-anonymous Spinnaker authentication mechanism should have this annotation included on its @Configuration bean. + * Any non-anonymous Spinnaker authentication mechanism should have this annotation included on + * its @Configuration bean. */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -@interface SpinnakerAuthConfig { -} +public @interface SpinnakerAuthConfig {} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/SpinnakerUser.groovy b/gate-core/src/main/java/com/netflix/spinnaker/gate/security/SpinnakerUser.java similarity index 71% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/SpinnakerUser.groovy rename to gate-core/src/main/java/com/netflix/spinnaker/gate/security/SpinnakerUser.java index b1be8d665d..2267fde87d 100644 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/SpinnakerUser.groovy +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/security/SpinnakerUser.java @@ -14,17 +14,15 @@ * limitations under the License. */ -package com.netflix.spinnaker.gate.security +package com.netflix.spinnaker.gate.security; -import org.springframework.security.core.annotation.AuthenticationPrincipal +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.security.core.annotation.AuthenticationPrincipal; -import java.lang.annotation.ElementType -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy -import java.lang.annotation.Target - -@Target([ElementType.PARAMETER]) +@Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @AuthenticationPrincipal -@interface SpinnakerUser { -} +public @interface SpinnakerUser {} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/SpringSecurityAnnotationConfig.groovy b/gate-core/src/main/java/com/netflix/spinnaker/gate/security/SpringSecurityAnnotationConfig.java similarity index 80% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/SpringSecurityAnnotationConfig.groovy rename to gate-core/src/main/java/com/netflix/spinnaker/gate/security/SpringSecurityAnnotationConfig.java index 70e8d5fe0a..c3e185a0b9 100644 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/security/SpringSecurityAnnotationConfig.groovy +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/security/SpringSecurityAnnotationConfig.java @@ -14,14 +14,13 @@ * limitations under the License. */ -package com.netflix.spinnaker.gate.security +package com.netflix.spinnaker.gate.security; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean -import org.springframework.context.annotation.Configuration -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) @ConditionalOnBean(annotation = SpinnakerAuthConfig.class) -class SpringSecurityAnnotationConfig { -} +public class SpringSecurityAnnotationConfig {} diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/security/anonymous/AnonymousConfig.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/security/anonymous/AnonymousConfig.java new file mode 100644 index 0000000000..70c1c5e093 --- /dev/null +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/security/anonymous/AnonymousConfig.java @@ -0,0 +1,93 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.security.anonymous; + +import com.netflix.spinnaker.fiat.shared.FiatStatus; +import com.netflix.spinnaker.gate.security.SpinnakerAuthConfig; +import com.netflix.spinnaker.gate.services.CredentialsService; +import com.netflix.spinnaker.security.User; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Scheduled; +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.util.CollectionUtils; + +/** + * Requires auth.anonymous.enabled to be true in Fiat configs to work properly. This is because + * anonymous users are a special permissions case, because the "user" doesn't actually exist in the + * backing UserRolesProvider. + */ +@ConditionalOnMissingBean(annotation = SpinnakerAuthConfig.class) +@Configuration +@Log4j2 +@EnableWebSecurity +@Order(Ordered.LOWEST_PRECEDENCE) +@RequiredArgsConstructor +public class AnonymousConfig extends WebSecurityConfigurerAdapter { + private static final String key = "spinnaker-anonymous"; + private static final String defaultEmail = "anonymous"; + + private final CredentialsService credentialsService; + private final FiatStatus fiatStatus; + @Getter private final List anonymousAllowedAccounts = new CopyOnWriteArrayList<>(); + + @Override + @SuppressWarnings("deprecation") + public void configure(HttpSecurity http) throws Exception { + updateAnonymousAccounts(); + // Not using the ImmutableUser version in order to update allowedAccounts. + User principal = new User(); + principal.setEmail(defaultEmail); + principal.setAllowedAccounts(anonymousAllowedAccounts); + + http.anonymous().key(key).principal(principal).and().csrf().disable(); + } + + @Scheduled(fixedDelay = 60000L) + public void updateAnonymousAccounts() { + if (fiatStatus.isEnabled()) { + return; + } + + try { + Collection names = credentialsService.getAccountNames(Set.of()); + Collection newAnonAccounts = !CollectionUtils.isEmpty(names) ? names : Set.of(); + + var toAdd = new HashSet<>(newAnonAccounts); + anonymousAllowedAccounts.forEach(toAdd::remove); + var toRemove = new HashSet<>(anonymousAllowedAccounts); + newAnonAccounts.forEach(toRemove::remove); + + anonymousAllowedAccounts.removeAll(toRemove); + anonymousAllowedAccounts.addAll(toAdd); + } catch (Exception e) { + log.warn("Error while updating anonymous accounts", e); + } + } +} diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/PermissionService.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/PermissionService.java new file mode 100644 index 0000000000..7481359b03 --- /dev/null +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/PermissionService.java @@ -0,0 +1,227 @@ +/* + * Copyright 2016 Google, Inc. + * Copyright 2023 Apple, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.services; + +import com.netflix.spinnaker.fiat.model.UserPermission; +import com.netflix.spinnaker.fiat.model.resources.Role; +import com.netflix.spinnaker.fiat.model.resources.ServiceAccount; +import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator; +import com.netflix.spinnaker.fiat.shared.FiatService; +import com.netflix.spinnaker.fiat.shared.FiatStatus; +import com.netflix.spinnaker.gate.retrofit.UpstreamBadRequest; +import com.netflix.spinnaker.gate.security.SpinnakerUser; +import com.netflix.spinnaker.gate.services.internal.ExtendedFiatService; +import com.netflix.spinnaker.kork.core.RetrySupport; +import com.netflix.spinnaker.kork.exceptions.SpinnakerException; +import com.netflix.spinnaker.kork.exceptions.SystemException; +import com.netflix.spinnaker.security.AuthenticatedRequest; +import com.netflix.spinnaker.security.User; +import java.time.Duration; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import retrofit.RetrofitError; + +@Log4j2 +@Component +@RequiredArgsConstructor +public class PermissionService { + private final FiatService fiatService; + private final ExtendedFiatService extendedFiatService; + private final ServiceAccountFilterConfigProps serviceAccountFilterConfigProps; + private final FiatPermissionEvaluator permissionEvaluator; + private final FiatStatus fiatStatus; + + @Setter( + onParam_ = {@Qualifier("fiatLoginService")}, + onMethod_ = {@Autowired(required = false)}) + private FiatService fiatLoginService; + + public boolean isEnabled() { + return fiatStatus.isEnabled(); + } + + private FiatService getFiatServiceForLogin() { + return fiatLoginService != null ? fiatLoginService : fiatService; + } + + public void login(final String userId) { + if (fiatStatus.isEnabled()) { + try { + AuthenticatedRequest.allowAnonymous( + () -> { + getFiatServiceForLogin().loginUser(userId, null); + permissionEvaluator.invalidatePermission(userId); + return null; + }); + } catch (RetrofitError e) { + throw UpstreamBadRequest.classifyError(e); + } + } + } + + public void loginWithRoles(final String userId, final Collection roles) { + if (fiatStatus.isEnabled()) { + try { + AuthenticatedRequest.allowAnonymous( + () -> { + getFiatServiceForLogin().loginWithRoles(userId, roles); + permissionEvaluator.invalidatePermission(userId); + return null; + }); + } catch (RetrofitError e) { + throw UpstreamBadRequest.classifyError(e); + } + } + } + + public void logout(String userId) { + if (fiatStatus.isEnabled()) { + try { + getFiatServiceForLogin().logoutUser(userId); + permissionEvaluator.invalidatePermission(userId); + } catch (RetrofitError e) { + throw UpstreamBadRequest.classifyError(e); + } + } + } + + public void sync() { + if (fiatStatus.isEnabled()) { + try { + getFiatServiceForLogin().sync(List.of()); + } catch (RetrofitError e) { + throw UpstreamBadRequest.classifyError(e); + } + } + } + + public Set getRoles(String userId) { + if (!fiatStatus.isEnabled()) { + return Set.of(); + } + try { + var permission = permissionEvaluator.getPermission(userId); + var roles = permission != null ? permission.getRoles() : null; + return roles != null ? roles : Set.of(); + } catch (RetrofitError e) { + throw UpstreamBadRequest.classifyError(e); + } + } + + List lookupServiceAccounts(String userId) { + try { + return extendedFiatService.getUserServiceAccounts(userId); + } catch (RetrofitError re) { + var response = re.getResponse(); + if (response != null && response.getStatus() == HttpStatus.NOT_FOUND.value()) { + return List.of(); + } + boolean shouldRetry = + response == null || HttpStatus.valueOf(response.getStatus()).is5xxServerError(); + throw new SystemException("getUserServiceAccounts failed", re).setRetryable(shouldRetry); + } + } + + public List getServiceAccountsForApplication( + @SpinnakerUser final User user, @Nonnull final String application) { + var matchAuthorizations = serviceAccountFilterConfigProps.getMatchAuthorizations(); + boolean requiresFiltering = + fiatStatus.isEnabled() + && serviceAccountFilterConfigProps.isEnabled() + && user != null + && StringUtils.hasLength(application) + && !CollectionUtils.isEmpty(matchAuthorizations); + if (!requiresFiltering) { + return getServiceAccounts(user); + } + + List filteredServiceAccounts; + RetrySupport retry = new RetrySupport(); + try { + var serviceAccounts = + retry.retry( + () -> lookupServiceAccounts(user.getUsername()), 3, Duration.ofMillis(50), false); + filteredServiceAccounts = + serviceAccounts.stream() + .filter( + permission -> + permission.getApplications().stream() + .anyMatch( + app -> + application.equalsIgnoreCase(app.getName()) + && !Collections.disjoint( + matchAuthorizations, app.getAuthorizations()))) + .map(UserPermission.View::getName) + .collect(Collectors.toList()); + } catch (SpinnakerException se) { + log.error( + "failed to lookup user {} service accounts for application {}, falling back to all user service accounts", + user, + application, + se); + return getServiceAccounts(user); + } + + // if there are no service accounts for the requested application, fall back to the full list of + // service accounts for the user to avoid a chicken and egg problem of trying to enable security + // for the first time on an application + return !filteredServiceAccounts.isEmpty() ? filteredServiceAccounts : getServiceAccounts(user); + } + + public List getServiceAccounts(@SpinnakerUser User user) { + if (user == null) { + log.debug("getServiceAccounts: Spinnaker user is null."); + return List.of(); + } + + if (!fiatStatus.isEnabled()) { + log.debug("getServiceAccounts: Fiat disabled."); + return List.of(); + } + + try { + var permission = permissionEvaluator.getPermission(user.getUsername()); + if (permission == null) { + return List.of(); + } + return permission.getServiceAccounts().stream() + .map(ServiceAccount.View::getName) + .collect(Collectors.toList()); + } catch (RetrofitError re) { + throw UpstreamBadRequest.classifyError(re); + } + } + + public boolean isAdmin(String userId) { + var permission = permissionEvaluator.getPermission(userId); + return permission != null && permission.isAdmin(); + } +} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/ServiceAccountFilterConfigProps.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/ServiceAccountFilterConfigProps.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/ServiceAccountFilterConfigProps.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/services/ServiceAccountFilterConfigProps.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/internal/GoogleCloudBuildTrigger.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/GoogleCloudBuildTrigger.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/internal/GoogleCloudBuildTrigger.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/GoogleCloudBuildTrigger.java diff --git a/gate-core/src/test/groovy/com/netflix/spinnaker/gate/config/AuthConfigTest.groovy b/gate-core/src/test/groovy/com/netflix/spinnaker/gate/config/AuthConfigTest.groovy index 052ae29669..730a52e93b 100644 --- a/gate-core/src/test/groovy/com/netflix/spinnaker/gate/config/AuthConfigTest.groovy +++ b/gate-core/src/test/groovy/com/netflix/spinnaker/gate/config/AuthConfigTest.groovy @@ -15,15 +15,15 @@ * */ package com.netflix.spinnaker.gate.config -import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties + import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator import com.netflix.spinnaker.fiat.shared.FiatStatus -import org.springframework.boot.autoconfigure.security.SecurityProperties import org.springframework.security.config.annotation.ObjectPostProcessor import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.web.util.matcher.AnyRequestMatcher import spock.lang.Specification + import java.util.stream.Collectors class AuthConfigTest extends Specification { @@ -35,19 +35,16 @@ class AuthConfigTest extends Specification { requestMatcher() >> requestMatcher } def authConfig = new AuthConfig( - permissionRevokingLogoutSuccessHandler: Mock(AuthConfig.PermissionRevokingLogoutSuccessHandler), - securityProperties: Mock(SecurityProperties), - configProps: Mock(FiatClientConfigurationProperties), - fiatStatus: Mock(FiatStatus), - permissionEvaluator: Mock(FiatPermissionEvaluator), - requestMatcherProvider: mockRequestMatcherProvider, - securityDebug: false, - fiatSessionFilterEnabled: false, - ) + Mock(PermissionRevokingLogoutSuccessHandler), + Mock(FiatStatus), + Mock(FiatPermissionEvaluator), + mockRequestMatcherProvider) + authConfig.securityDebug = false + authConfig.fiatSessionFilterEnabled = false def httpSecurity = new HttpSecurity( Mock(ObjectPostProcessor), Mock(AuthenticationManagerBuilder), - new HashMap>() + [:] ) when: @@ -72,20 +69,17 @@ class AuthConfigTest extends Specification { requestMatcher() >> requestMatcher } def authConfig = new AuthConfig( - permissionRevokingLogoutSuccessHandler: Mock(AuthConfig.PermissionRevokingLogoutSuccessHandler), - securityProperties: Mock(SecurityProperties), - configProps: Mock(FiatClientConfigurationProperties), - fiatStatus: Mock(FiatStatus), - permissionEvaluator: Mock(FiatPermissionEvaluator), - requestMatcherProvider: mockRequestMatcherProvider, - securityDebug: false, - fiatSessionFilterEnabled: false, - webhookDefaultAuthEnabled: true, - ) + Mock(PermissionRevokingLogoutSuccessHandler), + Mock(FiatStatus), + Mock(FiatPermissionEvaluator), + mockRequestMatcherProvider) + authConfig.securityDebug = false + authConfig.fiatSessionFilterEnabled = false + authConfig.webhookDefaultAuthEnabled = true def httpSecurity = new HttpSecurity( Mock(ObjectPostProcessor), Mock(AuthenticationManagerBuilder), - new HashMap>() + [:] ) when: diff --git a/gate-core/src/test/groovy/com/netflix/spinnaker/gate/security/anonymous/AnonymousConfigSpec.groovy b/gate-core/src/test/groovy/com/netflix/spinnaker/gate/security/anonymous/AnonymousConfigSpec.groovy index ecbd710e70..c8edcc1d6c 100644 --- a/gate-core/src/test/groovy/com/netflix/spinnaker/gate/security/anonymous/AnonymousConfigSpec.groovy +++ b/gate-core/src/test/groovy/com/netflix/spinnaker/gate/security/anonymous/AnonymousConfigSpec.groovy @@ -21,8 +21,6 @@ import com.netflix.spinnaker.gate.services.CredentialsService import spock.lang.Specification import spock.lang.Unroll -import java.util.concurrent.CopyOnWriteArrayList - class AnonymousConfigSpec extends Specification { @Unroll def "should update accounts when fiat is not enabled"() { @@ -33,11 +31,8 @@ class AnonymousConfigSpec extends Specification { } and: - AnonymousConfig config = new AnonymousConfig( - anonymousAllowedAccounts: new CopyOnWriteArrayList(oldAccounts), - credentialsService: credentialsService, - fiatStatus: fiatStatus - ) + AnonymousConfig config = new AnonymousConfig(credentialsService, fiatStatus) + config.anonymousAllowedAccounts.addAll(oldAccounts) when: config.updateAnonymousAccounts() diff --git a/gate-core/src/test/groovy/com/netflix/spinnaker/gate/services/PermissionServiceSpec.groovy b/gate-core/src/test/groovy/com/netflix/spinnaker/gate/services/PermissionServiceSpec.groovy index c009f0db9c..bc93465fc5 100644 --- a/gate-core/src/test/groovy/com/netflix/spinnaker/gate/services/PermissionServiceSpec.groovy +++ b/gate-core/src/test/groovy/com/netflix/spinnaker/gate/services/PermissionServiceSpec.groovy @@ -39,7 +39,7 @@ class PermissionServiceSpec extends Specification { def extendedFiatService = Stub(ExtendedFiatService) { getUserServiceAccounts(user) >> { throw theFailure } } - def subject = new PermissionService(extendedFiatService: extendedFiatService) + def subject = new PermissionService(null, extendedFiatService, null, null, null) when: def result = subject.lookupServiceAccounts(user) @@ -61,7 +61,7 @@ class PermissionServiceSpec extends Specification { def extendedFiatService = Stub(ExtendedFiatService) { getUserServiceAccounts(user) >> { throw theFailure } } - def subject = new PermissionService(extendedFiatService: extendedFiatService) + def subject = new PermissionService(null, extendedFiatService, null, null, null) when: subject.lookupServiceAccounts(user) @@ -123,11 +123,7 @@ class PermissionServiceSpec extends Specification { def extendedFiatService = Mock(ExtendedFiatService) def result = hasResult ? lookupResult : [] - def subject = new PermissionService( - fiatStatus: fiatStatus, - permissionEvaluator: permissionEvaluator, - extendedFiatService: extendedFiatService, - serviceAccountFilterConfigProps: cfgProps) + def subject = new PermissionService(null, extendedFiatService, cfgProps, permissionEvaluator, fiatStatus) when: subject.getServiceAccountsForApplication(user, application) diff --git a/gate-iap/src/main/java/com/netflix/spinnaker/gate/security/iap/IapSsoConfig.java b/gate-iap/src/main/java/com/netflix/spinnaker/gate/security/iap/IapSsoConfig.java index 1385a6da8c..d739908617 100644 --- a/gate-iap/src/main/java/com/netflix/spinnaker/gate/security/iap/IapSsoConfig.java +++ b/gate-iap/src/main/java/com/netflix/spinnaker/gate/security/iap/IapSsoConfig.java @@ -32,7 +32,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; @@ -101,9 +100,4 @@ public void configure(HttpSecurity http) throws Exception { authConfig.configure(http); http.addFilterBefore(iapAuthenticationFilter(), BasicAuthenticationFilter.class); } - - @Override - public void configure(WebSecurity web) throws Exception { - authConfig.configure(web); - } } diff --git a/gate-ldap/src/main/groovy/com/netflix/spinnaker/gate/security/ldap/LdapSsoConfig.groovy b/gate-ldap/src/main/groovy/com/netflix/spinnaker/gate/security/ldap/LdapSsoConfig.groovy index e9aea89fa1..4bcbb69e48 100644 --- a/gate-ldap/src/main/groovy/com/netflix/spinnaker/gate/security/ldap/LdapSsoConfig.groovy +++ b/gate-ldap/src/main/groovy/com/netflix/spinnaker/gate/security/ldap/LdapSsoConfig.groovy @@ -31,7 +31,6 @@ import org.springframework.ldap.core.DirContextAdapter import org.springframework.ldap.core.DirContextOperations import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.builders.WebSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.core.GrantedAuthority @@ -98,11 +97,6 @@ class LdapSsoConfig extends WebSecurityConfigurerAdapter { http.addFilterBefore(new BasicAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter) } - @Override - void configure(WebSecurity web) throws Exception { - authConfig.configure(web) - } - @Component static class LdapUserContextMapper implements UserDetailsContextMapper { diff --git a/gate-oauth2/src/main/groovy/com/netflix/spinnaker/gate/security/oauth2/OAuth2SsoConfig.groovy b/gate-oauth2/src/main/groovy/com/netflix/spinnaker/gate/security/oauth2/OAuth2SsoConfig.groovy index af8b6c5438..654b523681 100644 --- a/gate-oauth2/src/main/groovy/com/netflix/spinnaker/gate/security/oauth2/OAuth2SsoConfig.groovy +++ b/gate-oauth2/src/main/groovy/com/netflix/spinnaker/gate/security/oauth2/OAuth2SsoConfig.groovy @@ -29,7 +29,6 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Primary import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.builders.WebSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.core.AuthenticationException @@ -88,10 +87,6 @@ class OAuth2SsoConfig extends WebSecurityConfigurerAdapter { http.addFilterBefore(externalAuthTokenFilter, AbstractPreAuthenticatedProcessingFilter.class) } - void configure(WebSecurity web) throws Exception { - authConfig.configure(web) - } - /** * Use this class to specify how to map fields from the userInfoUri response to what's expected to be in the User. */ diff --git a/gate-saml/gate-saml.gradle b/gate-saml/gate-saml.gradle index 5c9dc672ca..18dda69573 100644 --- a/gate-saml/gate-saml.gradle +++ b/gate-saml/gate-saml.gradle @@ -4,7 +4,6 @@ dependencies{ implementation "io.spinnaker.fiat:fiat-api:$fiatVersion" implementation "io.spinnaker.kork:kork-exceptions" implementation "io.spinnaker.kork:kork-security" - implementation "com.netflix.spectator:spectator-api" implementation 'org.springframework:spring-context' implementation 'org.springframework.session:spring-session-core' implementation 'org.springframework.boot:spring-boot-autoconfigure' diff --git a/gate-saml/src/main/groovy/com/netflix/spinnaker/gate/security/saml/SamlSsoConfig.groovy b/gate-saml/src/main/groovy/com/netflix/spinnaker/gate/security/saml/SamlSsoConfig.groovy deleted file mode 100644 index 2739db43de..0000000000 --- a/gate-saml/src/main/groovy/com/netflix/spinnaker/gate/security/saml/SamlSsoConfig.groovy +++ /dev/null @@ -1,355 +0,0 @@ -/* - * Copyright 2014 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.security.saml - -import com.netflix.spectator.api.Registry -import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties -import com.netflix.spinnaker.gate.config.AuthConfig -import com.netflix.spinnaker.gate.security.AllowedAccountsSupport -import com.netflix.spinnaker.gate.security.SpinnakerAuthConfig -import com.netflix.spinnaker.gate.services.PermissionService -import com.netflix.spinnaker.kork.core.RetrySupport -import com.netflix.spinnaker.security.User -import groovy.util.logging.Slf4j -import org.opensaml.saml2.core.Assertion -import org.opensaml.saml2.core.Attribute -import org.opensaml.xml.schema.XSAny -import org.opensaml.xml.schema.XSString -import org.opensaml.xml.security.BasicSecurityConfiguration -import org.opensaml.xml.signature.SignatureConstants -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression -import org.springframework.boot.autoconfigure.web.ServerProperties -import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.security.authentication.BadCredentialsException -import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.builders.WebSecurity -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter -import org.springframework.security.core.userdetails.UserDetailsService -import org.springframework.security.core.userdetails.UsernameNotFoundException -import org.springframework.security.extensions.saml2.config.SAMLConfigurer -import org.springframework.security.saml.websso.WebSSOProfileConsumerImpl -import org.springframework.security.saml.SAMLCredential -import org.springframework.security.saml.userdetails.SAMLUserDetailsService -import org.springframework.security.web.authentication.RememberMeServices -import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices -import org.springframework.session.web.http.DefaultCookieSerializer -import org.springframework.stereotype.Component - -import javax.annotation.PostConstruct -import java.security.KeyStore - -import static org.springframework.security.extensions.saml2.config.SAMLConfigurer.saml - -@ConditionalOnExpression('${saml.enabled:false}') -@Configuration -@SpinnakerAuthConfig -@EnableWebSecurity -@Slf4j -class SamlSsoConfig extends WebSecurityConfigurerAdapter { - - @Autowired - ServerProperties serverProperties - - @Autowired - DefaultCookieSerializer defaultCookieSerializer - - @Autowired - AuthConfig authConfig - - @Component - @ConfigurationProperties("saml") - static class SAMLSecurityConfigProperties { - String keyStore - String keyStorePassword - String keyStoreAliasName - - // SAML DSL uses a metadata URL instead of hard coding a certificate/issuerId/redirectBase into the config. - String metadataUrl - // The parts of this endpoint passed to/used by the SAML IdP. - String redirectProtocol = "https" - String redirectHostname - String redirectBasePath = "/" - // The application identifier given to the IdP for this app. - String issuerId - - List requiredRoles - boolean sortRoles = false - boolean forceLowercaseRoles = true - UserAttributeMapping userAttributeMapping = new UserAttributeMapping() - long maxAuthenticationAge = 7200 - - String signatureDigest = "SHA1" // SHA1 is the default registered in DefaultSecurityConfigurationBootstrap.populateSignatureParams - - /** - * Ensure that the keystore exists and can be accessed with the given keyStorePassword and keyStoreAliasName - */ - @PostConstruct - void validate() { - if (metadataUrl && metadataUrl.startsWith("/")) { - metadataUrl = "file:" + metadataUrl - } - - if (keyStore) { - if (!keyStore.startsWith("file:")) { - keyStore = "file:" + keyStore - } - new File(new URI(keyStore)).withInputStream { is -> - def keystore = KeyStore.getInstance(KeyStore.getDefaultType()) - - // will throw an exception if `keyStorePassword` is invalid - keystore.load(is, keyStorePassword.toCharArray()) - - if (keyStoreAliasName && !keystore.aliases().find { it.equalsIgnoreCase(keyStoreAliasName) }) { - throw new IllegalStateException("Keystore '${keyStore}' does not contain alias '${keyStoreAliasName}'") - } - } - } - - // Validate signature digest algorithm - if (SignatureAlgorithms.fromName(signatureDigest) == null) { - throw new IllegalStateException("Invalid saml.signatureDigest value '${signatureDigest}'. Valid values are ${SignatureAlgorithms.values()}") - } - } - } - - static class UserAttributeMapping { - String firstName = "User.FirstName" - String lastName = "User.LastName" - String roles = "memberOf" - String rolesDelimiter = ";" - String username - String email - } - - @Autowired - SAMLSecurityConfigProperties samlSecurityConfigProperties - - @Autowired - SAMLUserDetailsService samlUserDetailsService - - @Override - void configure(HttpSecurity http) { - //We need our session cookie to come across when we get redirected back from the IdP: - defaultCookieSerializer.setSameSite(null) - authConfig.configure(http) - - http - .rememberMe() - .rememberMeServices(rememberMeServices(userDetailsService())) - - // @formatter:off - SAMLConfigurer saml = saml() - saml - .userDetailsService(samlUserDetailsService) - .identityProvider() - .metadataFilePath(samlSecurityConfigProperties.metadataUrl) - .discoveryEnabled(false) - .and() - .webSSOProfileConsumer(getWebSSOProfileConsumerImpl()) - .serviceProvider() - .entityId(samlSecurityConfigProperties.issuerId) - .protocol(samlSecurityConfigProperties.redirectProtocol) - .hostname(samlSecurityConfigProperties.redirectHostname ?: serverProperties?.address?.hostName) - .basePath(samlSecurityConfigProperties.redirectBasePath) - .keyStore() - .storeFilePath(samlSecurityConfigProperties.keyStore) - .password(samlSecurityConfigProperties.keyStorePassword) - .keyname(samlSecurityConfigProperties.keyStoreAliasName) - .keyPassword(samlSecurityConfigProperties.keyStorePassword) - - saml.init(http) - initSignatureDigest() // Need to be after SAMLConfigurer initializes the global SecurityConfiguration - - // @formatter:on - - } - - private void initSignatureDigest() { - def secConfig = org.opensaml.Configuration.getGlobalSecurityConfiguration() - if (secConfig != null && secConfig instanceof BasicSecurityConfiguration) { - BasicSecurityConfiguration basicSecConfig = (BasicSecurityConfiguration) secConfig - def algo = SignatureAlgorithms.fromName(samlSecurityConfigProperties.signatureDigest) - log.info("Using ${algo} digest for signing SAML messages") - basicSecConfig.registerSignatureAlgorithmURI("RSA", algo.rsaSignatureMethod) - basicSecConfig.setSignatureReferenceDigestMethod(algo.digestMethod) - } else { - log.warn("Unable to find global BasicSecurityConfiguration (found '${secConfig}'). Ignoring signatureDigest configuration value.") - } - } - - void configure(WebSecurity web) throws Exception { - authConfig.configure(web) - } - - public WebSSOProfileConsumerImpl getWebSSOProfileConsumerImpl() { - WebSSOProfileConsumerImpl profileConsumer = new WebSSOProfileConsumerImpl(); - profileConsumer.setMaxAuthenticationAge(samlSecurityConfigProperties.maxAuthenticationAge); - return profileConsumer; - } - - @Bean - public RememberMeServices rememberMeServices(UserDetailsService userDetailsService) { - TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("password", userDetailsService) - rememberMeServices.setCookieName("cookieName") - rememberMeServices.setParameter("rememberMe") - rememberMeServices - } - - @Bean - SAMLUserDetailsService samlUserDetailsService() { - // TODO(ttomsu): This is a NFLX specific user extractor. Make a more generic one? - new SAMLUserDetailsService() { - - @Autowired - PermissionService permissionService - - @Autowired - AllowedAccountsSupport allowedAccountsSupport - - @Autowired - FiatClientConfigurationProperties fiatClientConfigurationProperties - - @Autowired - Registry registry - - RetrySupport retrySupport = new RetrySupport() - - @Override - User loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException { - def assertion = credential.authenticationAssertion - def attributes = extractAttributes(assertion) - def userAttributeMapping = samlSecurityConfigProperties.userAttributeMapping - - def subjectNameId = assertion.getSubject().nameID.value - def email = attributes[userAttributeMapping.email]?.get(0) ?: subjectNameId - String username = attributes[userAttributeMapping.username]?.get(0) ?: subjectNameId - def roles = extractRoles(email, attributes, userAttributeMapping, samlSecurityConfigProperties.forceLowercaseRoles) - - if (samlSecurityConfigProperties.sortRoles) { - roles = roles.sort() - } - - if (samlSecurityConfigProperties.requiredRoles) { - if (!samlSecurityConfigProperties.requiredRoles.any { it in roles }) { - throw new BadCredentialsException("User $email does not have all roles $samlSecurityConfigProperties.requiredRoles") - } - } - - def id = registry - .createId("fiat.login") - .withTag("type", "saml") - - try { - retrySupport.retry({ -> - permissionService.loginWithRoles(username, roles) - }, 5, 2000, false) - - log.debug("Successful SAML authentication (user: {}, roleCount: {}, roles: {})", username, roles.size(), roles) - id = id.withTag("success", true).withTag("fallback", "none") - } catch (Exception e) { - log.debug( - "Unsuccessful SAML authentication (user: {}, roleCount: {}, roles: {}, legacyFallback: {})", - username, - roles.size(), - roles, - fiatClientConfigurationProperties.legacyFallback, - e - ) - id = id.withTag("success", false).withTag("fallback", fiatClientConfigurationProperties.legacyFallback) - - if (!fiatClientConfigurationProperties.legacyFallback) { - throw e - } - } finally { - registry.counter(id).increment() - } - - return new User( - email: email, - firstName: attributes[userAttributeMapping.firstName]?.get(0), - lastName: attributes[userAttributeMapping.lastName]?.get(0), - roles: roles, - allowedAccounts: allowedAccountsSupport.filterAllowedAccounts(username, roles), - username: username - ) - } - - Set extractRoles(String email, - Map> attributes, - UserAttributeMapping userAttributeMapping, - boolean forceLowercaseRoles) { - def assertionRoles = attributes[userAttributeMapping.roles].collect { String roles -> - def commonNames = roles.split(userAttributeMapping.rolesDelimiter) - commonNames.collect { - return it.indexOf("CN=") < 0 ? it : it.substring(it.indexOf("CN=") + 3, it.indexOf(",")) - } - }.flatten() as Set - - if (forceLowercaseRoles) { - assertionRoles = assertionRoles*.toLowerCase() - } - - return assertionRoles - } - - static Map> extractAttributes(Assertion assertion) { - def attributes = [:] - assertion.attributeStatements*.attributes.flatten().each { Attribute attribute -> - def name = attribute.name - def values = attribute.attributeValues.findResults { - switch (it) { - case XSString: - return (it as XSString)?.value - case XSAny: - return (it as XSAny)?.textContent - } - return null - } ?: [] - attributes[name] = values - } - - return attributes - } - } - } - - // Available digests taken from org.opensaml.xml.signature.SignatureConstants (RSA signatures) - private enum SignatureAlgorithms { - SHA1(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1, SignatureConstants.ALGO_ID_DIGEST_SHA1), - SHA256(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256, SignatureConstants.ALGO_ID_DIGEST_SHA256), - SHA384(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA384, SignatureConstants.ALGO_ID_DIGEST_SHA384), - SHA512(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512, SignatureConstants.ALGO_ID_DIGEST_SHA512), - RIPEMD160(SignatureConstants.ALGO_ID_SIGNATURE_RSA_RIPEMD160, SignatureConstants.ALGO_ID_DIGEST_RIPEMD160), - MD5(SignatureConstants.ALGO_ID_SIGNATURE_NOT_RECOMMENDED_RSA_MD5, SignatureConstants.ALGO_ID_DIGEST_NOT_RECOMMENDED_MD5) - - String rsaSignatureMethod - String digestMethod - SignatureAlgorithms(String rsaSignatureMethod, String digestMethod) { - this.rsaSignatureMethod = rsaSignatureMethod - this.digestMethod = digestMethod - } - - static SignatureAlgorithms fromName(String digestName) { - SignatureAlgorithms.find { it -> (it.name() == digestName.toUpperCase()) } as SignatureAlgorithms - } - } - -} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSSOConfig.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSSOConfig.java new file mode 100644 index 0000000000..18763f8f6a --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSSOConfig.java @@ -0,0 +1,125 @@ +/* + * Copyright 2014 Netflix, Inc. + * Copyright 2023 Apple, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.security.saml; + +import static org.springframework.security.extensions.saml2.config.SAMLConfigurer.saml; + +import com.netflix.spinnaker.gate.config.AuthConfig; +import com.netflix.spinnaker.gate.security.SpinnakerAuthConfig; +import java.net.InetAddress; +import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.opensaml.xml.security.BasicSecurityConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +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.saml.userdetails.SAMLUserDetailsService; +import org.springframework.security.saml.websso.WebSSOProfileConsumerImpl; +import org.springframework.session.web.http.DefaultCookieSerializer; +import org.springframework.util.StringUtils; + +/** + * Configures SAML2 authentication for Spinnaker. + * + * @see SAML + * 2.0 configuration docs + */ +@Log4j2 +@ConditionalOnExpression("${saml.enabled:false}") +@Configuration +@SpinnakerAuthConfig +@EnableWebSecurity +@EnableConfigurationProperties(SAMLSecurityConfigProperties.class) +@ComponentScan +@RequiredArgsConstructor +public class SAMLSSOConfig extends WebSecurityConfigurerAdapter { + private final DefaultCookieSerializer defaultCookieSerializer; + private final AuthConfig authConfig; + private final SAMLUserDetailsService samlUserDetailsService; + private final SAMLSecurityConfigProperties samlSecurityConfigProperties; + @Nullable private final ServerProperties serverProperties; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // We need our session cookie to come across when we get redirected back from the IdP: + defaultCookieSerializer.setSameSite(null); + authConfig.configure(http); + + http.rememberMe() + .key("password") + .rememberMeCookieName("cookieName") + .rememberMeParameter("rememberMe"); + + var webSSOProfileConsumer = new WebSSOProfileConsumerImpl(); + webSSOProfileConsumer.setMaxAuthenticationAge( + samlSecurityConfigProperties.getMaxAuthenticationAge()); + + var hostname = samlSecurityConfigProperties.getRedirectHostname(); + if (!StringUtils.hasLength(hostname) && serverProperties != null) { + InetAddress address = serverProperties.getAddress(); + if (address != null) { + hostname = address.getHostName(); + } + } + + // @formatter:off + + saml() + .userDetailsService(samlUserDetailsService) + .identityProvider() + .metadataFilePath(samlSecurityConfigProperties.getMetadataUrl()) + .discoveryEnabled(false) + .and() + .webSSOProfileConsumer(webSSOProfileConsumer) + .serviceProvider() + .entityId(samlSecurityConfigProperties.getIssuerId()) + .protocol(samlSecurityConfigProperties.getRedirectProtocol()) + .hostname(hostname) + .basePath(samlSecurityConfigProperties.getRedirectBasePath()) + .keyStore() + .storeFilePath(samlSecurityConfigProperties.getKeyStore()) + .password(samlSecurityConfigProperties.getKeyStorePassword()) + .keyname(samlSecurityConfigProperties.getKeyStoreAliasName()) + .keyPassword(samlSecurityConfigProperties.getKeyStorePassword()) + .and() + .and() + .init(http); + + // @formatter:on + + // Need to be after SAMLConfigurer initializes the global SecurityConfiguration + var secConfig = org.opensaml.Configuration.getGlobalSecurityConfiguration(); + if (secConfig instanceof BasicSecurityConfiguration) { + var config = (BasicSecurityConfiguration) secConfig; + var digest = samlSecurityConfigProperties.signatureDigest(); + log.info("Using {} digest for signing SAML messages", digest); + config.registerSignatureAlgorithmURI("RSA", digest.getSignatureMethod()); + config.setSignatureReferenceDigestMethod(digest.getDigestMethod()); + } else { + log.warn( + "Unable to find global BasicSecurityConfiguration (found '{}'). Ignoring signatureDigest configuration value.", + secConfig); + } + } +} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigProperties.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigProperties.java new file mode 100644 index 0000000000..5644fb435a --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigProperties.java @@ -0,0 +1,159 @@ +/* + * Copyright 2014 Netflix, Inc. + * Copyright 2023 Apple, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.security.saml; + +import com.netflix.spinnaker.kork.exceptions.ConfigurationException; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import javax.annotation.PostConstruct; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.SignatureMethod; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.opensaml.xml.signature.SignatureConstants; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.util.StringUtils; + +@Getter +@Setter +@ConfigurationProperties("saml") +public class SAMLSecurityConfigProperties { + private static final String FILE_SCHEME = "file:"; + + private String keyStore; + private String keyStoreType = "PKCS12"; + private String keyStorePassword; + private String keyStoreAliasName; + + // SAML DSL uses a metadata URL instead of hard coding a certificate/issuerId/redirectBase into + // the config. + private String metadataUrl; + // The parts of this endpoint passed to/used by the SAML IdP. + private String redirectProtocol = "https"; + private String redirectHostname; + private String redirectBasePath = "/"; + // The application identifier given to the IdP for this app. + private String issuerId; + + private List requiredRoles; + private boolean sortRoles = false; + private boolean forceLowercaseRoles = true; + + @NestedConfigurationProperty + private UserAttributeMapping userAttributeMapping = new UserAttributeMapping(); + + private long maxAuthenticationAge = 7200; + + // SHA1 is the default registered in DefaultSecurityConfigurationBootstrap.populateSignatureParams + private String signatureDigest = "SHA1"; + + public SignatureDigest signatureDigest() { + return SignatureDigest.fromName(signatureDigest); + } + + /** + * Ensure that the keystore exists and can be accessed with the given keyStorePassword and + * keyStoreAliasName. Validates the configured signature/digest is supported. + */ + @PostConstruct + public void validate() throws IOException, GeneralSecurityException { + if (StringUtils.hasLength(metadataUrl) && metadataUrl.startsWith("/")) { + metadataUrl = FILE_SCHEME + metadataUrl; + } + if (StringUtils.hasLength(keyStore)) { + if (!keyStore.startsWith(FILE_SCHEME)) { + keyStore = FILE_SCHEME + keyStore; + } + var path = Path.of(URI.create(keyStore)); + var keystore = KeyStore.getInstance(keyStoreType); + var password = keyStorePassword != null ? keyStorePassword.toCharArray() : new char[0]; + try (var stream = Files.newInputStream(path)) { + // will throw an exception if `keyStorePassword` is invalid or if the key store file is + // invalid + keystore.load(stream, password); + } + if (StringUtils.hasLength(keyStoreAliasName)) { + var aliases = caseInsensitiveSetFromAliasEnumeration(keystore.aliases()); + if (!aliases.contains(keyStoreAliasName)) { + throw new ConfigurationException( + String.format( + "Keystore '%s' does not contain alias '%s'; found aliases: %s", + keyStore, keyStoreAliasName, aliases)); + } + } + } + // Validate signature digest algorithm + Objects.requireNonNull(signatureDigest()); + } + + private static Set caseInsensitiveSetFromAliasEnumeration( + Enumeration enumeration) { + Set set = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + while (enumeration.hasMoreElements()) { + set.add(enumeration.nextElement()); + } + return set; + } + + @Getter + @Setter + public static class UserAttributeMapping { + private String firstName = "User.FirstName"; + private String lastName = "User.LastName"; + private String roles = "memberOf"; + private String rolesDelimiter = ";"; + private String username; + private String email; + } + + // only RSA-based signatures explicitly supported here (baseline requirement for XML signatures) + @Getter + @RequiredArgsConstructor + public enum SignatureDigest { + @Deprecated + SHA1(SignatureMethod.RSA_SHA1, DigestMethod.SHA1), + SHA256(SignatureMethod.RSA_SHA256, DigestMethod.SHA256), + SHA384(SignatureMethod.RSA_SHA384, DigestMethod.SHA384), + SHA512(SignatureMethod.RSA_SHA512, DigestMethod.SHA512), + @Deprecated + RIPEMD160(SignatureConstants.ALGO_ID_SIGNATURE_RSA_RIPEMD160, DigestMethod.RIPEMD160), + @Deprecated + MD5( + SignatureConstants.ALGO_ID_SIGNATURE_NOT_RECOMMENDED_RSA_MD5, + SignatureConstants.ALGO_ID_DIGEST_NOT_RECOMMENDED_MD5), + ; + private final String signatureMethod; + private final String digestMethod; + + public static SignatureDigest fromName(String name) { + return valueOf(name.toUpperCase(Locale.ROOT)); + } + } +} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLUserDetailsService.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLUserDetailsService.java new file mode 100644 index 0000000000..c187c3e5f9 --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLUserDetailsService.java @@ -0,0 +1,228 @@ +/* + * Copyright 2014 Netflix, Inc. + * Copyright 2023 Apple, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.security.saml; + +import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties; +import com.netflix.spinnaker.gate.security.AllowedAccountsSupport; +import com.netflix.spinnaker.gate.services.PermissionService; +import com.netflix.spinnaker.kork.core.RetrySupport; +import com.netflix.spinnaker.kork.exceptions.ConfigurationException; +import com.netflix.spinnaker.security.User; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import java.time.Duration; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import lombok.extern.log4j.Log4j2; +import org.opensaml.saml2.core.Assertion; +import org.opensaml.saml2.core.Attribute; +import org.opensaml.saml2.core.AttributeStatement; +import org.opensaml.xml.schema.XSAny; +import org.opensaml.xml.schema.XSString; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.saml.SAMLCredential; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +@Log4j2 +@Service +@ConditionalOnProperty("saml.enabled") +public class SAMLUserDetailsService + implements org.springframework.security.saml.userdetails.SAMLUserDetailsService { + private static final String COUNTER_NAME = "fiat.login"; + private static final Tag TYPE = Tag.of("type", "saml"); + private static final Tag SUCCESS = Tag.of("success", "true"); + private static final Tag FAILURE = Tag.of("success", "false"); + private static final Tag NO_FALLBACK = Tag.of("fallback", "none"); + + private final PermissionService permissionService; + private final AllowedAccountsSupport allowedAccountsSupport; + private final FiatClientConfigurationProperties fiatClientConfigurationProperties; + private final SAMLSecurityConfigProperties samlSecurityConfigProperties; + private final Counter successes; + private final Counter failures; + private final RetrySupport retrySupport = new RetrySupport(); + + public SAMLUserDetailsService( + PermissionService permissionService, + AllowedAccountsSupport allowedAccountsSupport, + FiatClientConfigurationProperties fiatClientConfigurationProperties, + SAMLSecurityConfigProperties samlSecurityConfigProperties, + MeterRegistry meterRegistry) { + this.permissionService = permissionService; + this.allowedAccountsSupport = allowedAccountsSupport; + this.fiatClientConfigurationProperties = fiatClientConfigurationProperties; + this.samlSecurityConfigProperties = samlSecurityConfigProperties; + successes = meterRegistry.counter(COUNTER_NAME, Tags.of(TYPE, SUCCESS, NO_FALLBACK)); + failures = + meterRegistry.counter( + COUNTER_NAME, + Tags.of( + TYPE, + FAILURE, + Tag.of( + "fallback", + Boolean.toString(fiatClientConfigurationProperties.isLegacyFallback())))); + } + + @Override + public Object loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException { + var assertion = credential.getAuthenticationAssertion(); + var attributes = extractAttributes(assertion); + var userAttributeMapping = samlSecurityConfigProperties.getUserAttributeMapping(); + @SuppressWarnings("deprecation") + var user = new User(); + + var subjectNameId = assertion.getSubject().getNameID().getValue(); + var emailAttributeValue = + CollectionUtils.firstElement(attributes.get(userAttributeMapping.getEmail())); + var email = emailAttributeValue != null ? emailAttributeValue : subjectNameId; + user.setEmail(email); + var usernameAttributeValue = + CollectionUtils.firstElement(attributes.get(userAttributeMapping.getUsername())); + var username = usernameAttributeValue != null ? usernameAttributeValue : subjectNameId; + user.setUsername(username); + var roles = extractRoles(attributes); + user.setRoles(roles); + + if (!CollectionUtils.isEmpty(samlSecurityConfigProperties.getRequiredRoles())) { + var requiredRoles = Set.copyOf(samlSecurityConfigProperties.getRequiredRoles()); + // check for at least one common role in both sets + if (Collections.disjoint(roles, requiredRoles)) { + throw new BadCredentialsException( + String.format("User %s is not in any required role from %s", email, requiredRoles)); + } + } + + Supplier login = + () -> { + permissionService.loginWithRoles(username, roles); + return null; + }; + + try { + retrySupport.retry(login, 5, Duration.ofSeconds(2), false); + log.debug( + "Successful SAML authentication (user: {}, roleCount: {}, roles: {})", + username, + roles.size(), + roles); + successes.increment(); + } catch (Exception e) { + boolean legacyFallback = fiatClientConfigurationProperties.isLegacyFallback(); + log.debug( + "Unsuccessful SAML authentication (user: {}, roleCount: {}, roles: {}, legacyFallback: {})", + username, + roles.size(), + roles, + legacyFallback, + e); + failures.increment(); + + if (!legacyFallback) { + throw e; + } + } + + user.setFirstName( + CollectionUtils.firstElement(attributes.get(userAttributeMapping.getFirstName()))); + user.setLastName( + CollectionUtils.firstElement(attributes.get(userAttributeMapping.getLastName()))); + user.setAllowedAccounts(allowedAccountsSupport.filterAllowedAccounts(username, roles)); + + return user; + } + + private Set extractRoles(Map> attributes) { + var userAttributeMapping = samlSecurityConfigProperties.getUserAttributeMapping(); + var roleStream = + attributes.getOrDefault(userAttributeMapping.getRoles(), List.of()).stream() + .flatMap(roles -> Stream.of(roles.split(userAttributeMapping.getRolesDelimiter()))) + .map(SAMLUserDetailsService::parseRole); + if (samlSecurityConfigProperties.isForceLowercaseRoles()) { + roleStream = roleStream.map(String::toLowerCase); + } + if (samlSecurityConfigProperties.isSortRoles()) { + roleStream = roleStream.sorted(); + } + return roleStream.collect(Collectors.toCollection(LinkedHashSet::new)); + } + + private static Map> extractAttributes(Assertion assertion) { + return assertion.getAttributeStatements().stream() + .flatMap(SAMLUserDetailsService::streamAttributes) + .collect( + Collectors.groupingBy( + Attribute::getName, + Collectors.flatMapping( + SAMLUserDetailsService::streamAttributeValues, Collectors.toList()))); + } + + private static Stream streamAttributes(AttributeStatement statement) { + return statement.getAttributes().stream(); + } + + private static Stream streamAttributeValues(Attribute attribute) { + return attribute.getAttributeValues().stream() + .map( + object -> { + if (object instanceof XSString) { + return ((XSString) object).getValue(); + } + if (object instanceof XSAny) { + return ((XSAny) object).getTextContent(); + } + return null; + }) + .filter(Objects::nonNull); + } + + private static String parseRole(String role) { + if (!role.contains("CN=")) { + return role; + } + try { + return new LdapName(role) + .getRdns().stream() + .filter(rdn -> rdn.getType().equals("CN")) + .map(rdn -> (String) rdn.getValue()) + .findFirst() + .orElseThrow( + () -> + new ConfigurationException( + String.format( + "SAML role '%s' contains 'CN=' but cannot be parsed as a DN", role))); + } catch (InvalidNameException e) { + throw new ConfigurationException( + String.format("Unable to parse SAML role name '%s'", role), e); + } + } +} diff --git a/gate-saml/src/test/groovy/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigPropertiesSpec.groovy b/gate-saml/src/test/groovy/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigPropertiesSpec.groovy index 943cbf5898..7a5f7cfc53 100644 --- a/gate-saml/src/test/groovy/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigPropertiesSpec.groovy +++ b/gate-saml/src/test/groovy/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigPropertiesSpec.groovy @@ -24,7 +24,7 @@ class SAMLSecurityConfigPropertiesSpec extends Specification { @Unroll def "should validate that the keystore exists and the password/alias are valid"() { given: - def ssoConfig = new SamlSsoConfig.SAMLSecurityConfigProperties( + def ssoConfig = new SAMLSecurityConfigProperties( keyStore: keyStore.toString(), keyStorePassword: keyStorePassword, keyStoreAliasName: keyStoreAliasName ) diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy index 7af5c95062..b649669cf7 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy @@ -16,15 +16,12 @@ package com.netflix.spinnaker.gate.config -import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.dataformat.yaml.YAMLMapper -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.netflix.spectator.api.Registry import com.netflix.spinnaker.config.DefaultServiceEndpoint import com.netflix.spinnaker.config.OkHttp3ClientConfiguration import com.netflix.spinnaker.config.PluginsAutoConfiguration -import com.netflix.spinnaker.config.okhttp3.OkHttpClientProvider import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator import com.netflix.spinnaker.fiat.shared.FiatService @@ -52,11 +49,9 @@ import com.netflix.spinnaker.okhttp.OkHttpClientConfigurationProperties import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Value import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty -import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.web.servlet.FilterRegistrationBean import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -64,7 +59,7 @@ import org.springframework.context.annotation.Import import org.springframework.context.annotation.Primary import org.springframework.core.Ordered import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter -import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder import org.springframework.session.data.redis.config.ConfigureRedisAction import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration import org.springframework.util.CollectionUtils @@ -72,7 +67,6 @@ import org.springframework.web.client.RestTemplate import redis.clients.jedis.JedisPool import retrofit.Endpoint -import javax.servlet.* import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -81,7 +75,6 @@ import static retrofit.Endpoints.newFixedEndpoint @CompileStatic @Configuration @Slf4j -@EnableConfigurationProperties([FiatClientConfigurationProperties, DynamicRoutingConfigProperties]) @Import([PluginsAutoConfiguration, DeckPluginConfiguration, PluginWebConfiguration]) class GateConfig extends RedisHttpSessionConfiguration { @@ -150,23 +143,21 @@ class GateConfig extends RedisHttpSessionConfiguration { @Autowired ServiceConfiguration serviceConfiguration + @Autowired + Jackson2ObjectMapperBuilder objectMapperBuilder + /** * This needs to be before the yaml converter in order for json to be the default * response type. */ @Bean AbstractJackson2HttpMessageConverter jsonHttpMessageConverter() { - ObjectMapper objectMapper = new ObjectMapper() - .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL) - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .registerModule(new JavaTimeModule()) - - return new JsonHttpMessageConverter(objectMapper) + return new JsonHttpMessageConverter(objectMapperBuilder.build()) } @Bean AbstractJackson2HttpMessageConverter yamlHttpMessageConverter() { - return new YamlHttpMessageConverter(new YAMLMapper()) + return new YamlHttpMessageConverter(objectMapperBuilder.factory(new YAMLFactory()).build()) } @Bean @@ -210,12 +201,6 @@ class GateConfig extends RedisHttpSessionConfiguration { createClient "clouddriver", ClouddriverService } - @Bean - @ConditionalOnProperty("services.keel.enabled") - KeelService keelService(OkHttpClientProvider clientProvider) { - createClient "keel", KeelService - } - @Bean ClouddriverServiceSelector clouddriverServiceSelector(ClouddriverService defaultClouddriverService, DynamicConfigService dynamicConfigService, @@ -335,11 +320,7 @@ class GateConfig extends RedisHttpSessionConfiguration { } private T buildService(String serviceName, Class type, Endpoint endpoint) { - // New role providers break deserialization if this is not enabled. - ObjectMapper objectMapper = new ObjectMapper() - .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL) - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .registerModule(new JavaTimeModule()) + ObjectMapper objectMapper = objectMapperBuilder.build() serviceClientProvider.getService(type, new DefaultServiceEndpoint(serviceName, endpoint.url), objectMapper) @@ -374,8 +355,8 @@ class GateConfig extends RedisHttpSessionConfiguration { } @Bean - FilterRegistrationBean resetAuthenticatedRequestFilter() { - def frb = new FilterRegistrationBean(new ResetAuthenticatedRequestFilter()) + FilterRegistrationBean resetAuthenticatedRequestFilter() { + def frb = new FilterRegistrationBean<>(new ResetAuthenticatedRequestFilter()) frb.order = Ordered.HIGHEST_PRECEDENCE return frb } @@ -387,32 +368,18 @@ class GateConfig extends RedisHttpSessionConfiguration { * Additionally forwards request origin metadata (deck vs api). */ @Bean - FilterRegistrationBean authenticatedRequestFilter() { + FilterRegistrationBean authenticatedRequestFilter() { // no need to force the `AuthenticatedRequestFilter` to create a request id as that is // handled by the `RequestTimingFilter`. - def frb = new FilterRegistrationBean(new AuthenticatedRequestFilter(true, true, false, false)) + def frb = new FilterRegistrationBean<>(new AuthenticatedRequestFilter(true, true, false, false)) frb.order = Ordered.LOWEST_PRECEDENCE - 1 return frb } - /** - * This pulls the `springSecurityFilterChain` in front of the {@link AuthenticatedRequestFilter}, - * because the user must be authenticated through the security filter chain before their username/credentials - * can be pulled and forwarded in the AuthenticatedRequestFilter. - */ - @Bean - FilterRegistrationBean securityFilterChain( - @Qualifier(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) Filter securityFilter) { - def frb = new FilterRegistrationBean(securityFilter) - frb.order = 0 - frb.name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME - return frb - } - @Bean @ConditionalOnProperty("request-logging.enabled") - FilterRegistrationBean requestLoggingFilter() { - def frb = new FilterRegistrationBean(new RequestLoggingFilter()) + FilterRegistrationBean requestLoggingFilter() { + def frb = new FilterRegistrationBean<>(new RequestLoggingFilter()) // this filter should be placed very early in the request chain to ensure we track an accurate start time and // have a request id available to propagate across thread and service boundaries. frb.order = Ordered.HIGHEST_PRECEDENCE + 1 @@ -420,8 +387,8 @@ class GateConfig extends RedisHttpSessionConfiguration { } @Bean - FilterRegistrationBean requestSheddingFilter(DynamicConfigService dynamicConfigService) { - def frb = new FilterRegistrationBean(new RequestSheddingFilter(dynamicConfigService, registry)) + FilterRegistrationBean requestSheddingFilter(DynamicConfigService dynamicConfigService) { + def frb = new FilterRegistrationBean<>(new RequestSheddingFilter(dynamicConfigService, registry)) /* * This filter should: diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RateLimiterConfig.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RateLimiterConfig.java index 514b08b58a..f0e383ebd4 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RateLimiterConfig.java +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RateLimiterConfig.java @@ -59,13 +59,13 @@ RateLimitPrincipalProvider staticRateLimiterPrincipalProvider() { } @Bean - FilterRegistrationBean rateLimitingFilter( + FilterRegistrationBean rateLimitingFilter( RateLimiter rateLimiter, Registry registry, RateLimitPrincipalProvider rateLimitPrincipalProvider, Optional> requestIdentityExtractors) { - FilterRegistrationBean frb = - new FilterRegistrationBean( + var frb = + new FilterRegistrationBean<>( new RateLimitingFilter( rateLimiter, registry, diff --git a/gate-web/src/main/java/com/netflix/spinnaker/gate/config/GateCorsConfig.java b/gate-web/src/main/java/com/netflix/spinnaker/gate/config/GateCorsConfig.java index 8d7b60c198..f3102894d7 100644 --- a/gate-web/src/main/java/com/netflix/spinnaker/gate/config/GateCorsConfig.java +++ b/gate-web/src/main/java/com/netflix/spinnaker/gate/config/GateCorsConfig.java @@ -55,16 +55,15 @@ OriginValidator gateOriginValidator( @Bean @ConditionalOnProperty(name = "cors.allow-mode", havingValue = "regex", matchIfMissing = true) - FilterRegistrationBean regExCorsFilter(OriginValidator gateOriginValidator) { - FilterRegistrationBean filterRegBean = - new FilterRegistrationBean<>(new CorsFilter(gateOriginValidator)); + FilterRegistrationBean regExCorsFilter(OriginValidator gateOriginValidator) { + var filterRegBean = new FilterRegistrationBean<>(new CorsFilter(gateOriginValidator)); filterRegBean.setOrder(Ordered.HIGHEST_PRECEDENCE); return filterRegBean; } @Bean @ConditionalOnProperty(name = "cors.allow-mode", havingValue = "list") - FilterRegistrationBean allowedOriginCorsFilter( + FilterRegistrationBean allowedOriginCorsFilter( @Value("${cors.allowed-origins:*}") List allowedOriginList) { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); @@ -74,7 +73,7 @@ FilterRegistrationBean allowedOriginCorsFilter( config.setMaxAge(MAX_AGE_IN_SECONDS); config.addAllowedMethod("*"); // Enable CORS for all methods. source.registerCorsConfiguration("/**", config); // Enable CORS for all paths - FilterRegistrationBean filterRegBean = + var filterRegBean = new FilterRegistrationBean<>(new org.springframework.web.filter.CorsFilter(source)); filterRegBean.setOrder(Ordered.HIGHEST_PRECEDENCE); return filterRegBean; diff --git a/gate-web/src/main/resources/application.properties b/gate-web/src/main/resources/application.properties new file mode 100644 index 0000000000..3d38c74dce --- /dev/null +++ b/gate-web/src/main/resources/application.properties @@ -0,0 +1,25 @@ +# +# Copyright 2023 Apple, Inc. +# +# 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. +# + +# https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.html +# Note: fail on unknown properties is disabled by default here. This also pulls in some common Jackson modules. +# New role providers break deserialization if this is not enabled. +spring.jackson.deserialization.read-unknown-enum-values-as-null=true + +# This defaults to -100, but for reasons (?) is set to 0 +# https://github.com/spinnaker/gate/pull/230 +# Either way, this filter needs to come before the AuthenticatedRequestFilter +spring.security.filter.order=0 diff --git a/gate-x509/gate-x509.gradle b/gate-x509/gate-x509.gradle index 769a71d518..73ceb39e2a 100644 --- a/gate-x509/gate-x509.gradle +++ b/gate-x509/gate-x509.gradle @@ -7,4 +7,16 @@ dependencies { implementation "com.github.ben-manes.caffeine:caffeine" implementation "io.spinnaker.fiat:fiat-api:$fiatVersion" implementation "io.spinnaker.fiat:fiat-core:$fiatVersion" + testImplementation "org.bouncycastle:bcpkix-jdk15on" +} + +sourceSets { + main { + java { + srcDirs = [] + } + groovy { + srcDirs += ['src/main/java'] + } + } } diff --git a/gate-x509/src/main/groovy/com/netflix/spinnaker/gate/security/x509/OidRolesExtractor.groovy b/gate-x509/src/main/groovy/com/netflix/spinnaker/gate/security/x509/OidRolesExtractor.groovy deleted file mode 100644 index 699e3cc36a..0000000000 --- a/gate-x509/src/main/groovy/com/netflix/spinnaker/gate/security/x509/OidRolesExtractor.groovy +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2017 Target, Inc. - * - * 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. - */ -package com.netflix.spinnaker.gate.security.x509 - -import org.bouncycastle.asn1.ASN1InputStream -import org.bouncycastle.asn1.ASN1OctetString -import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty -import org.springframework.stereotype.Component - -import java.security.cert.X509Certificate - -@Component -@ConditionalOnProperty("x509.role-oid") -class OidRolesExtractor implements X509RolesExtractor { - - @Value('${x509.role-oid:}') - String roleOid - - @Override - Collection fromCertificate(X509Certificate cert) { - byte[] bytes = cert.getExtensionValue(roleOid) - - if (bytes == null) { - return [] - } - ASN1OctetString octetString = (ASN1OctetString) new ASN1InputStream(new ByteArrayInputStream(bytes)).readObject() - ASN1InputStream inputStream = new ASN1InputStream(new ByteArrayInputStream(octetString.getOctets())) - def groups = inputStream.readObject()?.toString()?.split("\\n") - return groups.findAll{ !it.isEmpty() } - } -} diff --git a/gate-x509/src/main/groovy/com/netflix/spinnaker/gate/security/x509/X509Config.groovy b/gate-x509/src/main/groovy/com/netflix/spinnaker/gate/security/x509/X509Config.groovy deleted file mode 100644 index a9a35df62b..0000000000 --- a/gate-x509/src/main/groovy/com/netflix/spinnaker/gate/security/x509/X509Config.groovy +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2015 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.security.x509 - -import com.netflix.spinnaker.gate.config.AuthConfig -import com.netflix.spinnaker.gate.security.SpinnakerAuthConfig -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.core.annotation.Order -import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.builders.WebSecurity -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter -import org.springframework.security.web.context.NullSecurityContextRepository -import org.springframework.security.web.util.matcher.AnyRequestMatcher - -@ConditionalOnExpression('${x509.enabled:false}') -@Configuration -@SpinnakerAuthConfig -@EnableWebSecurity -//ensure this configures after a standard WebSecurityConfigurerAdapter (1000) so -// it becomes the fallthrough for a mixed mode of some SSO + x509 for API calls -// and otherwise will just work(tm) if it is the only WebSecurityConfigurerAdapter -// present as well -@Order(2000) -class X509Config extends WebSecurityConfigurerAdapter { - - @Value('${x509.subject-principal-regex:}') - String subjectPrincipalRegex - - @Autowired - AuthConfig authConfig - - @Autowired - X509AuthenticationUserDetailsService x509AuthenticationUserDetailsService - - @Override - void configure(HttpSecurity http) { - authConfig.configure(http) - http.securityContext().securityContextRepository(new NullSecurityContextRepository()) - http.x509().authenticationUserDetailsService(x509AuthenticationUserDetailsService) - - if (subjectPrincipalRegex) { - http.x509().subjectPrincipalRegex(subjectPrincipalRegex) - } - //x509 is the catch-all if configured, this will auth apiPort connections and - // any additional ports that get installed and removes the requestMatcher - // installed by authConfig - http.requestMatcher(AnyRequestMatcher.INSTANCE) - } - - @Override - void configure(WebSecurity web) throws Exception { - authConfig.configure(web) - } - - @Bean - X509IdentityExtractor x509IdentityExtractor() { - return new X509IdentityExtractor(x509AuthenticationUserDetailsService) - } -} diff --git a/gate-x509/src/main/java/com/netflix/spinnaker/gate/security/x509/OidRolesExtractor.java b/gate-x509/src/main/java/com/netflix/spinnaker/gate/security/x509/OidRolesExtractor.java new file mode 100644 index 0000000000..2b8ebcadc8 --- /dev/null +++ b/gate-x509/src/main/java/com/netflix/spinnaker/gate/security/x509/OidRolesExtractor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2017 Target, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.security.x509; + +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import lombok.AccessLevel; +import lombok.Setter; +import lombok.SneakyThrows; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1String; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +@ConditionalOnProperty("x509.role-oid") +public class OidRolesExtractor implements X509RolesExtractor { + private static final Pattern ROLE_SEPARATOR = Pattern.compile("\\n"); + + @Setter( + value = AccessLevel.PACKAGE, + onMethod_ = {@Autowired}, + onParam_ = {@Value("${x509.role-oid:}")}) + private String roleOid; + + @Override + @SneakyThrows + public Collection fromCertificate(X509Certificate cert) { + byte[] bytes = cert.getExtensionValue(roleOid); + + if (bytes == null) { + return List.of(); + } + ASN1OctetString octetString = ASN1OctetString.getInstance(bytes); + var primitive = ASN1Primitive.fromByteArray(octetString.getOctets()); + String string; + if (primitive instanceof ASN1String) { + // when using OID 1.2.840.10070.8.1, this is an ASN1UTF8String + string = ((ASN1String) primitive).getString(); + } else { + // hope for the best (note that ASN1Sequence::toString and ASN1Set::toString are formatted + // like AbstractCollection::toString which is probably not what you want) + string = primitive.toString(); + } + return ROLE_SEPARATOR + .splitAsStream(string) + .filter(StringUtils::hasLength) + .collect(Collectors.toSet()); + } +} diff --git a/gate-x509/src/main/java/com/netflix/spinnaker/gate/security/x509/X509Config.java b/gate-x509/src/main/java/com/netflix/spinnaker/gate/security/x509/X509Config.java new file mode 100644 index 0000000000..7d040fe16f --- /dev/null +++ b/gate-x509/src/main/java/com/netflix/spinnaker/gate/security/x509/X509Config.java @@ -0,0 +1,79 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.security.x509; + +import com.netflix.spinnaker.gate.config.AuthConfig; +import com.netflix.spinnaker.gate.security.SpinnakerAuthConfig; +import com.netflix.spinnaker.kork.annotations.NonnullByDefault; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +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.web.context.NullSecurityContextRepository; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; +import org.springframework.util.StringUtils; + +@ConditionalOnExpression("${x509.enabled:false}") +@Configuration +@SpinnakerAuthConfig +@EnableWebSecurity +// ensure this configures after a standard WebSecurityConfigurerAdapter (100) so +// it becomes the fallthrough for a mixed mode of some SSO + x509 for API calls +// and otherwise will just work(tm) if it is the only WebSecurityConfigurerAdapter +// present as well +@Order(2000) +@RequiredArgsConstructor +@NonnullByDefault +public class X509Config extends WebSecurityConfigurerAdapter { + private final AuthConfig authConfig; + private final X509AuthenticationUserDetailsService x509AuthenticationUserDetailsService; + + @Setter( + onMethod_ = {@Autowired}, + onParam_ = {@Value("${x509.subject-principal-regex:}")}) + private String subjectPrincipalRegex; + + @Override + public void configure(HttpSecurity http) throws Exception { + authConfig.configure(http); + http.securityContext( + context -> context.securityContextRepository(new NullSecurityContextRepository())) + .x509( + x509 -> { + x509.authenticationUserDetailsService(x509AuthenticationUserDetailsService); + if (StringUtils.hasLength(subjectPrincipalRegex)) { + x509.subjectPrincipalRegex(subjectPrincipalRegex); + } + }) + // x509 is the catch-all if configured, this will auth apiPort connections and + // any additional ports that get installed and removes the requestMatcher + // installed by authConfig + .requestMatcher(AnyRequestMatcher.INSTANCE); + } + + @Bean + public X509IdentityExtractor x509IdentityExtractor() { + return new X509IdentityExtractor(x509AuthenticationUserDetailsService); + } +} diff --git a/gate-x509/src/main/groovy/com/netflix/spinnaker/gate/security/x509/X509IdentityExtractor.java b/gate-x509/src/main/java/com/netflix/spinnaker/gate/security/x509/X509IdentityExtractor.java similarity index 100% rename from gate-x509/src/main/groovy/com/netflix/spinnaker/gate/security/x509/X509IdentityExtractor.java rename to gate-x509/src/main/java/com/netflix/spinnaker/gate/security/x509/X509IdentityExtractor.java diff --git a/gate-x509/src/main/groovy/com/netflix/spinnaker/gate/security/x509/X509RolesExtractor.java b/gate-x509/src/main/java/com/netflix/spinnaker/gate/security/x509/X509RolesExtractor.java similarity index 100% rename from gate-x509/src/main/groovy/com/netflix/spinnaker/gate/security/x509/X509RolesExtractor.java rename to gate-x509/src/main/java/com/netflix/spinnaker/gate/security/x509/X509RolesExtractor.java diff --git a/gate-x509/src/main/groovy/com/netflix/spinnaker/gate/security/x509/X509UserIdentifierExtractor.java b/gate-x509/src/main/java/com/netflix/spinnaker/gate/security/x509/X509UserIdentifierExtractor.java similarity index 100% rename from gate-x509/src/main/groovy/com/netflix/spinnaker/gate/security/x509/X509UserIdentifierExtractor.java rename to gate-x509/src/main/java/com/netflix/spinnaker/gate/security/x509/X509UserIdentifierExtractor.java diff --git a/gate-x509/src/test/groovy/com/netflix/spinnaker/gate/security/x509/OidRolesExtractorSpec.groovy b/gate-x509/src/test/groovy/com/netflix/spinnaker/gate/security/x509/OidRolesExtractorSpec.groovy index f40351316d..17eef13194 100644 --- a/gate-x509/src/test/groovy/com/netflix/spinnaker/gate/security/x509/OidRolesExtractorSpec.groovy +++ b/gate-x509/src/test/groovy/com/netflix/spinnaker/gate/security/x509/OidRolesExtractorSpec.groovy @@ -17,11 +17,18 @@ package com.netflix.spinnaker.gate.security.x509 +import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.DERUTF8String +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder import spock.lang.Specification import spock.lang.Unroll +import javax.security.auth.x500.X500Principal +import java.security.KeyPairGenerator import java.security.cert.CertificateFactory import java.security.cert.X509Certificate +import java.time.Duration class OidRolesExtractorSpec extends Specification { @@ -47,4 +54,50 @@ class OidRolesExtractorSpec extends Specification { "one role" | "memberOfOneRole.crt" | 1 "more roles" | "memberOfTwoRoles.crt" | 2 } + + def "should return roles listed in oid extension - #description"() { + given: + String roleOid = '1.2.840.10070.8.1' + def extractor = new OidRolesExtractor(roleOid: roleOid) + def certificate = generateCertificate(roleOid, roles) + + when: + def extractedRoles = extractor.fromCertificate(certificate) + + then: + roles.containsAll(extractedRoles) && extractedRoles.containsAll(roles) + + where: + description | roles + 'empty list' | [] + 'one group' | ['groupA'] + 'two groups' | ['groupA', 'groupB'] + 'three groups' | ['groupA', 'groupC', 'groupE'] + } + + private static X509Certificate generateCertificate(String roleOid, List roles) { + // generate a P.256 keypair + def generator = KeyPairGenerator.getInstance('EC') + generator.initialize(256) + def keypair = generator.generateKeyPair() + def signer = new JcaContentSignerBuilder('SHA256withECDSA') + .build(keypair.private) + // set up a self-signed certificate that expires in an hour + def subject = new X500Principal('CN=spinnaker') + def notBefore = new Date() + def notAfter = Date.from(notBefore.toInstant() + Duration.ofHours(1)) + def serial = BigInteger.valueOf(notBefore.time) + // standard spinnaker OID extension: encode the roles in a string separated by newlines + def oid = new ASN1ObjectIdentifier(roleOid) + def encodedRoles = new DERUTF8String(roles.join('\n')) + // generate a self-signed certificate with only the roles extension specified; + // a real certificate should also set the key usage, basic constraints, and extended key usage extensions + def holder = new JcaX509v3CertificateBuilder( + subject, serial, notBefore, notAfter, subject, keypair.public) + .addExtension(oid, false, encodedRoles) + .build(signer) + // convert from bouncycastle to plain java + CertificateFactory.getInstance('X.509') + .generateCertificate(new ByteArrayInputStream(holder.encoded)) as X509Certificate + } } From 2aaef6b7515ef53b5c24b82a04756453fb64990a Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 3 Jul 2023 14:14:54 -0400 Subject: [PATCH 089/182] chore(dependencies): Autobump korkVersion (#1675) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d513e82e6a..f55deb531f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.179.0 +korkVersion=7.180.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.30.0 From ea7d82346f44eca3a55affdf7d05c11a7e211f5c Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 5 Jul 2023 20:31:34 -0400 Subject: [PATCH 090/182] chore(dependencies): Autobump korkVersion (#1676) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f55deb531f..7bf123609f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.180.0 +korkVersion=7.181.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.30.0 From 8a91b5395656d7009a4d74941c0a119415066126 Mon Sep 17 00:00:00 2001 From: xibz Date: Thu, 6 Jul 2023 12:54:48 -0500 Subject: [PATCH 091/182] feat(artifacts): Add new ArtifactStore endpoints (#1674) This commit adds new endpoints and connections to the various services that require direct retrieval of the artifact, eg deck will now lazily load by calling the gate fetch artifact endpoint Signed-off-by: benjamin-j-powell Co-authored-by: Benevolent Benjamin Powell --- gate-core/gate-core.gradle | 1 + .../gate/services/internal/ClouddriverService.java | 5 +++++ gate-web/gate-web.gradle | 2 ++ .../spinnaker/gate/controllers/ArtifactController.java | 9 +++++++++ .../netflix/spinnaker/gate/services/ArtifactService.java | 5 +++++ 5 files changed, 22 insertions(+) diff --git a/gate-core/gate-core.gradle b/gate-core/gate-core.gradle index 4418ae225b..f10a87b06f 100644 --- a/gate-core/gate-core.gradle +++ b/gate-core/gate-core.gradle @@ -21,6 +21,7 @@ dependencies { api "com.squareup.retrofit:retrofit" + implementation "io.spinnaker.kork:kork-artifacts" implementation "io.spinnaker.kork:kork-plugins" implementation "com.jakewharton.retrofit:retrofit1-okhttp3-client:1.1.0" implementation "com.squareup.retrofit:converter-jackson" diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/ClouddriverService.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/ClouddriverService.java index ec4390716f..9910b7d3b1 100644 --- a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/ClouddriverService.java +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/ClouddriverService.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import com.netflix.spinnaker.kork.artifacts.model.Artifact; import com.netflix.spinnaker.kork.plugins.SpinnakerPluginDescriptor; import java.util.ArrayList; import java.util.Collection; @@ -400,6 +401,10 @@ List getFunctions( @GET("/installedPlugins") List getInstalledPlugins(); + @GET("/artifacts/content-address/{application}/{hash}") + Artifact.StoredView getStoredArtifact( + @Path("application") String application, @Path("hash") String hash); + @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) class Account { diff --git a/gate-web/gate-web.gradle b/gate-web/gate-web.gradle index 8d5d49f054..8a44168169 100644 --- a/gate-web/gate-web.gradle +++ b/gate-web/gate-web.gradle @@ -28,6 +28,8 @@ dependencies { implementation "io.spinnaker.fiat:fiat-core:$fiatVersion" implementation "io.spinnaker.fiat:fiat-api:$fiatVersion" + implementation "io.spinnaker.kork:kork-artifacts" + implementation "io.spinnaker.kork:kork-core" implementation "io.spinnaker.kork:kork-config" implementation "io.spinnaker.kork:kork-plugins" implementation "io.spinnaker.kork:kork-web" diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ArtifactController.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ArtifactController.java index b1acc70383..8caba35f76 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ArtifactController.java +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/ArtifactController.java @@ -17,6 +17,7 @@ package com.netflix.spinnaker.gate.controllers; import com.netflix.spinnaker.gate.services.ArtifactService; +import com.netflix.spinnaker.kork.artifacts.model.Artifact; import io.swagger.annotations.ApiOperation; import java.io.InputStream; import java.util.List; @@ -97,4 +98,12 @@ Map getArtifact( @PathVariable String version) { return artifactService.getArtifactByVersion(provider, packageName, version); } + + @ApiOperation(value = "Retrieve artifact by content hash") + @RequestMapping(value = "/content-address/{application}/{hash}", method = RequestMethod.GET) + Artifact.StoredView getStoredArtifact( + @PathVariable(value = "application") String application, + @PathVariable(value = "hash") String hash) { + return artifactService.getStoredArtifact(application, hash); + } } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ArtifactService.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ArtifactService.java index 3d35f5da6d..2c73013ca4 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ArtifactService.java +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/ArtifactService.java @@ -18,6 +18,7 @@ import com.netflix.spinnaker.gate.services.internal.ClouddriverServiceSelector; import com.netflix.spinnaker.gate.services.internal.IgorService; +import com.netflix.spinnaker.kork.artifacts.model.Artifact; import groovy.transform.CompileStatic; import java.io.IOException; import java.io.InputStream; @@ -59,6 +60,10 @@ public InputStream getArtifactContents(String selectorKey, Map a return clouddriverServiceSelector.select().getArtifactContent(artifact).getBody().in(); } + public Artifact.StoredView getStoredArtifact(String application, String hash) { + return clouddriverServiceSelector.select().getStoredArtifact(application, hash); + } + public List getVersionsOfArtifactForProvider( String provider, String packageName, String releaseStatus) { if (!igorService.isPresent()) { From f8e078928f2df2e209047cbaef21138f3b7fe8af Mon Sep 17 00:00:00 2001 From: Sandesh <30489233+j-sandy@users.noreply.github.com> Date: Mon, 10 Jul 2023 22:09:37 +0530 Subject: [PATCH 092/182] refactor(tests): convert junit4 based test cases to junit5 and clean up in gate (#1677) Before refactor, total test cases executed were 266. After refactor, total test cases executed are 266. --- .../netflix/spinnaker/gate/services/TaskServiceTest.java | 8 ++++---- gate-plugins-test/gate-plugins-test.gradle | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/gate-core/src/test/java/com/netflix/spinnaker/gate/services/TaskServiceTest.java b/gate-core/src/test/java/com/netflix/spinnaker/gate/services/TaskServiceTest.java index 3e7e21bddd..cfc23b2470 100644 --- a/gate-core/src/test/java/com/netflix/spinnaker/gate/services/TaskServiceTest.java +++ b/gate-core/src/test/java/com/netflix/spinnaker/gate/services/TaskServiceTest.java @@ -23,14 +23,14 @@ import com.netflix.spinnaker.gate.services.internal.OrcaServiceSelector; import java.util.LinkedHashMap; import java.util.Map; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; -@RunWith(SpringJUnit4ClassRunner.class) +@ExtendWith(SpringExtension.class) @SpringBootTest(classes = {TaskService.class, TaskServiceProperties.class}) public class TaskServiceTest { diff --git a/gate-plugins-test/gate-plugins-test.gradle b/gate-plugins-test/gate-plugins-test.gradle index c245fc7491..2c3bf31579 100644 --- a/gate-plugins-test/gate-plugins-test.gradle +++ b/gate-plugins-test/gate-plugins-test.gradle @@ -10,6 +10,5 @@ dependencies { testImplementation("io.spinnaker.kork:kork-plugins") testImplementation("io.spinnaker.kork:kork-plugins-tck") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") } From 591479699e7060dd705611f5e2a13239b821ae2e Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 12 Jul 2023 12:49:10 -0400 Subject: [PATCH 093/182] chore(dependencies): Autobump korkVersion (#1678) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7bf123609f..61c36a3ca4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.181.0 +korkVersion=7.182.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.30.0 From bdc1e9d1e780710f4646a0ed9e4c9c42f7877ff4 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 12 Jul 2023 18:15:52 -0400 Subject: [PATCH 094/182] chore(dependencies): Autobump korkVersion (#1679) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 61c36a3ca4..7dad66b95a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.182.0 +korkVersion=7.182.1 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.30.0 From 73a1dbaafd1680f4a6fccecd2cfb29afb4e29cac Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 18 Jul 2023 17:36:12 -0400 Subject: [PATCH 095/182] chore(dependencies): Autobump spinnakerGradleVersion (#1680) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7dad66b95a..ddd4c67d75 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.182.1 kotlinVersion=1.4.0 org.gradle.parallel=true -spinnakerGradleVersion=8.30.0 +spinnakerGradleVersion=8.31.0 targetJava11=true # To enable a composite reference to a project, set the From b3989c3e98b76a9018183d01defbca2c8e1f63de Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 25 Jul 2023 12:12:32 -0500 Subject: [PATCH 096/182] chore(cleanup): Removing un-implemented dead code (#1681) --- .../gate/config/OidcClientConfig.java | 31 -------------- .../controllers/OidcConfigController.java | 41 ------------------- .../gate/services/NoopOidcConfigService.java | 32 --------------- .../gate/services/OidcConfigService.java | 26 ------------ 4 files changed, 130 deletions(-) delete mode 100644 gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/OidcClientConfig.java delete mode 100644 gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/OidcConfigController.java delete mode 100644 gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/NoopOidcConfigService.java delete mode 100644 gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/OidcConfigService.java diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/OidcClientConfig.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/OidcClientConfig.java deleted file mode 100644 index 30d7cbe502..0000000000 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/OidcClientConfig.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2017 Netflix, Inc. - * - * 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. - */ -package com.netflix.spinnaker.gate.config; - -import com.netflix.spinnaker.gate.services.NoopOidcConfigService; -import com.netflix.spinnaker.gate.services.OidcConfigService; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class OidcClientConfig { - @Bean - @ConditionalOnMissingBean(OidcConfigService.class) - OidcConfigService noopOidcConfigService() { - return new NoopOidcConfigService(); - } -} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/OidcConfigController.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/OidcConfigController.java deleted file mode 100644 index ee4a4cb319..0000000000 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/OidcConfigController.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2018 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.controllers; - -import com.netflix.spinnaker.gate.services.OidcConfigService; -import java.util.List; -import java.util.Map; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class OidcConfigController { - @Autowired OidcConfigService oidcConfigService; - - @RequestMapping(value = "/oidcConfigs", method = RequestMethod.GET) - List byApp(@RequestParam(value = "app") String app) { - return oidcConfigService.getOidcConfigs(app); - } - - @RequestMapping(value = "/oidcConfig", method = RequestMethod.GET) - Map byId(@RequestParam(value = "id") String id) { - return oidcConfigService.getOidcConfig(id); - } -} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/NoopOidcConfigService.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/NoopOidcConfigService.java deleted file mode 100644 index 2229e2cd58..0000000000 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/NoopOidcConfigService.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2018 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.services; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class NoopOidcConfigService implements OidcConfigService { - public List getOidcConfigs(String app) { - return new ArrayList<>(); - } - - public Map getOidcConfig(String id) { - return new HashMap<>(); - } -} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/OidcConfigService.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/OidcConfigService.java deleted file mode 100644 index 50700c03cc..0000000000 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/OidcConfigService.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2018 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.services; - -import java.util.List; -import java.util.Map; - -public interface OidcConfigService { - List getOidcConfigs(String app); - - Map getOidcConfig(String id); -} From 311f5d1d683c0f8b58939779c38ba9531592c44a Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 10 Aug 2023 15:09:10 -0400 Subject: [PATCH 097/182] chore(dependencies): Autobump korkVersion (#1682) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ddd4c67d75..04b1cbe378 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.182.1 +korkVersion=7.183.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From 9affd98c2670f617eca0e35d688ebd8028b8c8ef Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 10 Aug 2023 16:51:13 -0400 Subject: [PATCH 098/182] chore(dependencies): Autobump korkVersion (#1683) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 04b1cbe378..1aa1f9e477 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.183.0 +korkVersion=7.184.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From a72b87ba07d3e80192b408610a764f846cd727c6 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 11 Aug 2023 13:06:16 -0400 Subject: [PATCH 099/182] chore(dependencies): Autobump korkVersion (#1684) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 1aa1f9e477..49560e5a21 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.184.0 +korkVersion=7.185.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From d11ae32bf04e7fc017d3259f25973e66e135aa0a Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 11 Aug 2023 14:22:29 -0400 Subject: [PATCH 100/182] chore(dependencies): Autobump korkVersion (#1685) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 49560e5a21..5dcb7659ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.185.0 +korkVersion=7.186.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From f57df150573ed2eb1e5866f56d6de76587616cca Mon Sep 17 00:00:00 2001 From: Jalander Ramagiri <97948659+rjalander@users.noreply.github.com> Date: Mon, 14 Aug 2023 09:46:08 +0100 Subject: [PATCH 101/182] feat(cdevents-webhooks) : Consume CDEvents webhook API implementation (#1651) * feat(cdevents-webhooks) : CDEvents webhook API implementation * test(cdevents-webhooks): test for cdevents API * fix(cdevents-webhooks) : Response type to match with events-broker expectations * addressing review comments * removing the redundant config file * fix review comment on using CloudEventHttpMessageConverter Signed-off-by: Jalander Ramagiri * adding index to cloudEvent HttpMessageConverter Signed-off-by: Jalander Ramagiri * fix: Placing the CloudEventHandlerConfiguration in gate-web project * fix: code violation --------- Signed-off-by: Jalander Ramagiri --- gate-core/gate-core.gradle | 4 ++ .../gate/services/internal/EchoService.java | 12 ++++ gate-web/gate-web.gradle | 4 ++ .../CloudEventHandlerConfiguration.java | 37 ++++++++++++ .../spinnaker/gate/config/GateConfig.groovy | 6 +- .../gate/controllers/WebhookController.groovy | 12 ++++ .../gate/services/WebhookService.groovy | 9 +++ .../controllers/WebhookControllerSpec.groovy | 56 +++++++++++++++++-- 8 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/CloudEventHandlerConfiguration.java diff --git a/gate-core/gate-core.gradle b/gate-core/gate-core.gradle index f10a87b06f..7ad8a916ad 100644 --- a/gate-core/gate-core.gradle +++ b/gate-core/gate-core.gradle @@ -38,6 +38,10 @@ dependencies { implementation "com.netflix.spectator:spectator-api" implementation "com.github.ben-manes.caffeine:guava" implementation "org.apache.commons:commons-lang3" + + implementation "io.cloudevents:cloudevents-spring:2.5.0" + implementation "io.cloudevents:cloudevents-json-jackson:2.5.0" + implementation "io.cloudevents:cloudevents-http-basic:2.5.0" } sourceSets { diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/EchoService.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/EchoService.java index e79a34bd75..a5092926ec 100644 --- a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/EchoService.java +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/EchoService.java @@ -1,8 +1,10 @@ package com.netflix.spinnaker.gate.services.internal; import com.netflix.spinnaker.kork.plugins.SpinnakerPluginDescriptor; +import io.cloudevents.CloudEvent; import java.util.List; import java.util.Map; +import org.springframework.http.ResponseEntity; import retrofit.http.Body; import retrofit.http.GET; import retrofit.http.Header; @@ -17,6 +19,16 @@ public interface EchoService { @POST("/webhooks/{type}/{source}") Map webhooks(@Path("type") String type, @Path("source") String source, @Body Map event); + @Headers("Accept: application/json") + @POST("/webhooks/cdevents/{source}") + ResponseEntity webhooks( + @Path("source") String source, + @Body CloudEvent cdevent, + @Header("Ce-Id") String cdId, + @Header("Ce-Specversion") String cdSpecVersion, + @Header("Ce-Type") String cdType, + @Header("Ce-Source") String cdSource); + @Headers("Accept: application/json") @POST("/webhooks/{type}/{source}") Map webhooks( diff --git a/gate-web/gate-web.gradle b/gate-web/gate-web.gradle index 8a44168169..c923764e0d 100644 --- a/gate-web/gate-web.gradle +++ b/gate-web/gate-web.gradle @@ -52,6 +52,10 @@ dependencies { implementation "io.springfox:springfox-swagger2" + implementation "io.cloudevents:cloudevents-spring:2.5.0" + implementation "io.cloudevents:cloudevents-json-jackson:2.5.0" + implementation "io.cloudevents:cloudevents-http-basic:2.5.0" + runtimeOnly "io.spinnaker.kork:kork-runtime" runtimeOnly "org.springframework.boot:spring-boot-properties-migrator" diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/CloudEventHandlerConfiguration.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/CloudEventHandlerConfiguration.java new file mode 100644 index 0000000000..f7c2165e7e --- /dev/null +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/CloudEventHandlerConfiguration.java @@ -0,0 +1,37 @@ +/* + * + * 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. + */ + +package com.netflix.spinnaker.gate.config; + +import io.cloudevents.spring.mvc.CloudEventHttpMessageConverter; +import java.util.List; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CloudEventHandlerConfiguration implements WebMvcConfigurer { + + @Override + public void configureMessageConverters(List> converters) { + converters.add(cloudEventHttpMessageConverter()); + } + + @Bean + public CloudEventHttpMessageConverter cloudEventHttpMessageConverter() { + return new CloudEventHttpMessageConverter(); + } +} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy index b649669cf7..0a2a5df143 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy @@ -17,6 +17,7 @@ package com.netflix.spinnaker.gate.config import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.netflix.spectator.api.Registry import com.netflix.spinnaker.config.DefaultServiceEndpoint @@ -321,9 +322,10 @@ class GateConfig extends RedisHttpSessionConfiguration { private T buildService(String serviceName, Class type, Endpoint endpoint) { ObjectMapper objectMapper = objectMapperBuilder.build() - + if(serviceName.equals("echo")) { + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + } serviceClientProvider.getService(type, new DefaultServiceEndpoint(serviceName, endpoint.url), objectMapper) - } private SelectableService createClientSelector(String serviceName, Class type) { diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/WebhookController.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/WebhookController.groovy index 5a08935d7f..6a14096c7e 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/WebhookController.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/WebhookController.groovy @@ -19,12 +19,15 @@ package com.netflix.spinnaker.gate.controllers import com.netflix.spinnaker.gate.services.WebhookService import io.swagger.annotations.ApiOperation import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMethod import org.springframework.web.bind.annotation.RestController +import io.cloudevents.CloudEvent @RestController @RequestMapping("/webhooks") @@ -48,6 +51,15 @@ class WebhookController { } } + @ApiOperation(value = "Endpoint for posting webhooks to Spinnaker's CDEvents webhook service") + @RequestMapping(value = "/cdevents/{source}", method = RequestMethod.POST) + ResponseEntity webhooks(@PathVariable String source, + @RequestBody CloudEvent cdevent, + @RequestHeader HttpHeaders headers) + { + webhookService.webhooks(source, cdevent, headers) + } + @ApiOperation(value = "Retrieve a list of preconfigured webhooks in Orca") @RequestMapping(value = "/preconfigured", method = RequestMethod.GET) List preconfiguredWebhooks() { diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/WebhookService.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/WebhookService.groovy index 4a0394a9d1..ce653961d0 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/WebhookService.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/WebhookService.groovy @@ -20,7 +20,10 @@ import com.netflix.spinnaker.gate.services.internal.EchoService import com.netflix.spinnaker.gate.services.internal.OrcaServiceSelector import com.netflix.spinnaker.security.AuthenticatedRequest import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import org.springframework.http.ResponseEntity import org.springframework.stereotype.Component +import io.cloudevents.CloudEvent @Component class WebhookService { @@ -53,6 +56,12 @@ class WebhookService { }) } + ResponseEntity webhooks(String source, CloudEvent cdevent, HttpHeaders headers) { + return AuthenticatedRequest.allowAnonymous( { + echoService.webhooks(source, cdevent, headers.get("Ce-Id").get(0), headers.get("Ce-Specversion").get(0), headers.get("Ce-Type").get(0), headers.get("Ce-Source").get(0)) + }) + } + List preconfiguredWebhooks() { return AuthenticatedRequest.allowAnonymous({ orcaServiceSelector.select().preconfiguredWebhooks() diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/WebhookControllerSpec.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/WebhookControllerSpec.groovy index 74f80b3669..13ec85ba46 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/WebhookControllerSpec.groovy +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/WebhookControllerSpec.groovy @@ -16,23 +16,25 @@ package com.netflix.spinnaker.gate.controllers +import com.fasterxml.jackson.databind.ObjectMapper +import com.netflix.spinnaker.gate.services.WebhookService import com.netflix.spinnaker.gate.services.internal.EchoService import com.netflix.spinnaker.gate.services.internal.OrcaServiceSelector -import com.netflix.spinnaker.gate.services.WebhookService import com.squareup.okhttp.mockwebserver.MockWebServer -import org.mockito.MockitoAnnotations; +import io.cloudevents.spring.mvc.CloudEventHttpMessageConverter +import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.mock.web.MockHttpServletResponse import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.web.util.NestedServletException import retrofit.RestAdapter -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import retrofit.client.OkClient -import retrofit.http.* import spock.lang.Specification +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + class WebhooksControllerSpec extends Specification { MockMvc mockMvc @@ -59,7 +61,9 @@ class WebhooksControllerSpec extends Specification { webhookService = new WebhookService(echoService: echoService, orcaServiceSelector: orcaServiceSelector) server.start() - mockMvc = MockMvcBuilders.standaloneSetup(new WebhookController(webhookService: webhookService)).build() + mockMvc = MockMvcBuilders.standaloneSetup(new WebhookController(webhookService: webhookService)) + .setMessageConverters(new CloudEventHttpMessageConverter()) + .build() } void 'handles null Maps'() { @@ -91,4 +95,44 @@ class WebhooksControllerSpec extends Specification { NestedServletException ex = thrown() ex.message.startsWith("Request processing failed; nested exception is retrofit.RetrofitError: Failed to connect to localhost") } + + void 'handles CDEvents API with BAD_REQUEST'() { + given: + + when: + MockHttpServletResponse response = mockMvc.perform(post("/webhooks/cdevents/artifactPackaged") + .accept(MediaType.APPLICATION_JSON)) + .andReturn().response + + then: + response.status == 400 + } + + void 'handles CDEvents API server Ping'() { + given: + HttpHeaders headers = new HttpHeaders(); + headers.add("Ce-Id", "1234") + headers.add("Ce-Specversion", "1.0") + headers.add("Ce-Type", "dev.cdevents.artifact.packaged") + headers.add("Ce-Source", "spinnaker.test.io") + headers.add("Content-Type", "application/cloudevents+json") + String payload = "{\"id\": \"1234\", \"subject\": \"event\"}" + Map cdEvent = [ + specversion: "1.0", + type: "dev.cdevents.artifact.packaged", + source: "/spinnaker.test.io", + id: "12345", + data: payload + ] + + when: + mockMvc.perform(post("/webhooks/cdevents/artifactPackaged") + .headers(headers) + .content(new ObjectMapper().writeValueAsString(cdEvent))) + .andExpect(status().isOk()).andReturn() + + then: + NestedServletException ex = thrown() + ex.message.startsWith("Request processing failed; nested exception is retrofit.RetrofitError: Failed to connect to localhost") + } } From 832b9b6fe5baa5d3b28885ac497d55e6663c75c6 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 24 Aug 2023 12:02:30 -0400 Subject: [PATCH 102/182] chore(dependencies): Autobump korkVersion (#1692) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5dcb7659ee..da5beb819e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.186.0 +korkVersion=7.187.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From a93bc107a2ea4ccda925ca6aef28c43f6ca0d73a Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 24 Aug 2023 17:56:55 -0400 Subject: [PATCH 103/182] chore(dependencies): Autobump korkVersion (#1693) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index da5beb819e..f61913e117 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.41.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.187.0 +korkVersion=7.188.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From e45a9de3a12f53ffccc86fc13899bef30c87ceba Mon Sep 17 00:00:00 2001 From: Omar Date: Mon, 28 Aug 2023 08:47:42 -0700 Subject: [PATCH 104/182] fix(md): update env model to have post deploy (#1576) Currently, keel supports post deploy actions. Expose this field in the Gate API Co-authored-by: Jason --- .../spinnaker/gate/model/manageddelivery/Environment.java | 1 + 1 file changed, 1 insertion(+) diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/Environment.java b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/Environment.java index 148d519ff7..28e3397bf9 100644 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/Environment.java +++ b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/Environment.java @@ -28,5 +28,6 @@ public class Environment { Collection> constraints; Collection notifications; Map locations; + List> postDeploy; List> verifyWith; } From 92fa1971ee9e23c3202be53e0929cd93670d788e Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 29 Aug 2023 14:18:13 -0400 Subject: [PATCH 105/182] chore(dependencies): Autobump fiatVersion (#1698) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f61913e117..844a986256 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.41.0 +fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.188.0 kotlinVersion=1.4.0 From 7e403b0645e4f36c415e3a2425da6947af336a10 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 31 Aug 2023 18:33:45 -0500 Subject: [PATCH 106/182] fix(cachingFilter: Allow disabling the content caching filter (#1699) * fix(cachingFilter: Allow disabling the content caching filter * fix(cachingFilter: Allow disabling the content caching filter --- .../com/netflix/spinnaker/gate/config/GateWebConfig.groovy | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateWebConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateWebConfig.groovy index 199426b311..4acd511ec0 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateWebConfig.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateWebConfig.groovy @@ -23,6 +23,7 @@ import com.netflix.spinnaker.gate.interceptors.ResponseHeaderInterceptor import com.netflix.spinnaker.gate.interceptors.ResponseHeaderInterceptorConfigurationProperties import com.netflix.spinnaker.gate.retrofit.UpstreamBadRequest import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import com.netflix.spinnaker.kork.web.interceptors.MetricsInterceptor import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value @@ -80,7 +81,10 @@ public class GateWebConfig implements WebMvcConfigurer { return new HandlerMappingIntrospector(context) } + + // Add the ability to disable as this breaks numerous integration patterns @Bean + @ConditionalOnProperty(value = "content.cachingFilter.enabled", matchIfMissing = true) Filter contentCachingFilter() { // This filter simply buffers the response so that Content-Length header can be set return new ContentCachingFilter() From 853e343e3f73eea399d88dc3a6f6beb09ac99fa3 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 6 Sep 2023 12:02:40 -0400 Subject: [PATCH 107/182] chore(dependencies): Autobump korkVersion (#1706) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 844a986256..e573bfaf2f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.188.0 +korkVersion=7.189.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From d69b93d0e9b80ef572e5067ed4e1fe893827806a Mon Sep 17 00:00:00 2001 From: Sandesh <30489233+j-sandy@users.noreply.github.com> Date: Mon, 11 Sep 2023 20:35:54 +0530 Subject: [PATCH 108/182] fix(spring/profile): fix the profile property to be processed by spring boot 2.4+ (#1709) For spring boot 2.4+, config file processing of profile specific document will consider `spring.config.activate.on-profile` property. As described in this blog: https://spring.io/blog/2020/08/14/config-file-processing-in-spring-boot-2-4 --- gate-plugins/src/test/resources/gate-test.yml | 4 +++- gate-web/config/gate.yml | 12 +++++++++--- gate-web/src/test/resources/gate-test.yml | 12 +++++++++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/gate-plugins/src/test/resources/gate-test.yml b/gate-plugins/src/test/resources/gate-test.yml index d348453491..c4340e0fc8 100644 --- a/gate-plugins/src/test/resources/gate-test.yml +++ b/gate-plugins/src/test/resources/gate-test.yml @@ -30,7 +30,9 @@ services: --- spring: - profiles: test + config: + activate: + on-profile: test spinnaker: extensions: diff --git a/gate-web/config/gate.yml b/gate-web/config/gate.yml index 54f01b2a10..e731f1d70a 100644 --- a/gate-web/config/gate.yml +++ b/gate-web/config/gate.yml @@ -118,7 +118,9 @@ spring.session.store-type: redis --- spring: - profiles: googleOAuth + config: + activate: + on-profile: googleOAuth security: oauth2: @@ -139,7 +141,9 @@ security: --- spring: - profiles: azureOAuth + config: + activate: + on-profile: azureOAuth security: oauth2: @@ -162,7 +166,9 @@ security: --- spring: - profiles: githubOAuth + config: + activate: + on-profile: githubOAuth security: oauth2: diff --git a/gate-web/src/test/resources/gate-test.yml b/gate-web/src/test/resources/gate-test.yml index 192f24b03c..45341085fd 100644 --- a/gate-web/src/test/resources/gate-test.yml +++ b/gate-web/src/test/resources/gate-test.yml @@ -37,7 +37,9 @@ slack: --- spring: - profiles: alloworigincors + config: + activate: + on-profile: alloworigincors cors: allow-mode: "list" @@ -48,7 +50,9 @@ cors: --- spring: - profiles: regexcors + config: + activate: + on-profile: regexcors cors: allowedOriginsPattern: '^https?://(?:localhost|[^/]+\.somewhere\.net)(?::[1-9]\d*)?/?$' @@ -57,7 +61,9 @@ cors: --- spring: - profiles: test + config: + activate: + on-profile: test spinnaker: extensions: From e8eea5221d8cc65387f09bb6acfa9e6f41d44629 Mon Sep 17 00:00:00 2001 From: Sandesh <30489233+j-sandy@users.noreply.github.com> Date: Thu, 14 Sep 2023 23:43:51 +0530 Subject: [PATCH 109/182] refactor(web/test): replace assert with assertThat and introduce explicit mocks (#1710) Used assetThat() to cleanup junit4 linkages. Introduced explicit mocks to reduce dependence on `spockframework`/`groovy` transformations using these tests under same package. Test coverage remain same before and after refactor (Tests executed 268). --- .../com/netflix/spinnaker/gate/MainSpec.java | 25 ++++++++++++++++--- .../controllers/ArtifactControllerTest.java | 18 +++++++++++++ .../ResponseHeaderInterceptorTest.java | 22 ++++++++++++++++ ...eConfigAuthenticatedRequestFilterTest.java | 25 +++++++++++++++++++ 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/MainSpec.java b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/MainSpec.java index 222cd784f7..e32af71e8e 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/MainSpec.java +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/MainSpec.java @@ -1,10 +1,18 @@ package com.netflix.spinnaker.gate; +import static org.assertj.core.api.Assertions.assertThat; + +import com.netflix.spinnaker.fiat.shared.FiatService; +import com.netflix.spinnaker.gate.services.ApplicationService; +import com.netflix.spinnaker.gate.services.PermissionService; +import com.netflix.spinnaker.gate.services.internal.ClouddriverService; +import com.netflix.spinnaker.gate.services.internal.ExtendedFiatService; +import com.netflix.spinnaker.gate.services.internal.Front50Service; import com.netflix.spinnaker.kork.client.ServiceClientProvider; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -14,11 +22,22 @@ @ActiveProfiles("test") @TestPropertySource(properties = {"spring.config.location=classpath:gate-test.yml"}) public class MainSpec { + @MockBean private ClouddriverService mockClouddriverService; + + @MockBean private ServiceClientProvider serviceClientProvider; + + @MockBean private ApplicationService mockApplicationService; + + @MockBean private PermissionService mockPermissionService; + + @MockBean private FiatService mockFiatService; + + @MockBean private ExtendedFiatService mockExtendedFiatService; - @Autowired ServiceClientProvider serviceClientProvider; + @MockBean private Front50Service mockFront50Service; @Test public void startupTest() { - assert serviceClientProvider != null; + assertThat(serviceClientProvider != null); } } diff --git a/gate-web/src/test/java/com/netflix/spinnaker/gate/controllers/ArtifactControllerTest.java b/gate-web/src/test/java/com/netflix/spinnaker/gate/controllers/ArtifactControllerTest.java index 1ee9123aa0..e711d908fc 100644 --- a/gate-web/src/test/java/com/netflix/spinnaker/gate/controllers/ArtifactControllerTest.java +++ b/gate-web/src/test/java/com/netflix/spinnaker/gate/controllers/ArtifactControllerTest.java @@ -28,9 +28,15 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.spinnaker.fiat.shared.FiatService; import com.netflix.spinnaker.gate.Main; +import com.netflix.spinnaker.gate.services.ApplicationService; +import com.netflix.spinnaker.gate.services.PermissionService; import com.netflix.spinnaker.gate.services.internal.ClouddriverService; import com.netflix.spinnaker.gate.services.internal.ClouddriverServiceSelector; +import com.netflix.spinnaker.gate.services.internal.ExtendedFiatService; +import com.netflix.spinnaker.gate.services.internal.Front50Service; +import com.netflix.spinnaker.kork.client.ServiceClientProvider; import java.io.InputStream; import java.util.ArrayList; import java.util.Map; @@ -62,6 +68,18 @@ public class ArtifactControllerTest { @MockBean private ClouddriverService mockClouddriverService; + @MockBean private Front50Service mockFront50Service; + + @MockBean private FiatService mocFiatService; + + @MockBean private ExtendedFiatService mockExtendedFiatService; + + @MockBean private PermissionService mockPermissionService; + + @MockBean private ApplicationService mockApplicationService; + + @MockBean private ServiceClientProvider mockServiceClientProvider; + @MockBean private InputStream mockInputStream; @MockBean private TypedByteArray mockBody; diff --git a/gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorTest.java b/gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorTest.java index d3055732a3..71988f27a4 100644 --- a/gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorTest.java +++ b/gate-web/src/test/java/com/netflix/spinnaker/gate/interceptors/ResponseHeaderInterceptorTest.java @@ -24,7 +24,14 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; +import com.netflix.spinnaker.fiat.shared.FiatService; import com.netflix.spinnaker.gate.Main; +import com.netflix.spinnaker.gate.services.ApplicationService; +import com.netflix.spinnaker.gate.services.PermissionService; +import com.netflix.spinnaker.gate.services.internal.ClouddriverService; +import com.netflix.spinnaker.gate.services.internal.ExtendedFiatService; +import com.netflix.spinnaker.gate.services.internal.Front50Service; +import com.netflix.spinnaker.kork.client.ServiceClientProvider; import com.netflix.spinnaker.kork.common.Header; import com.netflix.spinnaker.security.AuthenticatedRequest; import java.util.EnumSet; @@ -34,6 +41,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; @@ -52,6 +60,20 @@ public class ResponseHeaderInterceptorTest { private static final String TEST_EXECUTION_TYPE = "Test-Execution-Type"; private static final String TEST_APPLICATION = "Test-Application"; + @MockBean private ClouddriverService mockClouddriverService; + + @MockBean private ServiceClientProvider mockServiceClientProvider; + + @MockBean private ApplicationService mockApplicationService; + + @MockBean private PermissionService mockPermissionService; + + @MockBean private FiatService mockFiatService; + + @MockBean private ExtendedFiatService mockExtendedFiatService; + + @MockBean private Front50Service mockFront50Service; + @RestController @RequestMapping(value = API_BASE) static class TestController { diff --git a/gate-web/src/test/java/com/netflix/spinnaker/gate/testconfig/GateConfigAuthenticatedRequestFilterTest.java b/gate-web/src/test/java/com/netflix/spinnaker/gate/testconfig/GateConfigAuthenticatedRequestFilterTest.java index 17ae5dec70..ada2b93fb9 100644 --- a/gate-web/src/test/java/com/netflix/spinnaker/gate/testconfig/GateConfigAuthenticatedRequestFilterTest.java +++ b/gate-web/src/test/java/com/netflix/spinnaker/gate/testconfig/GateConfigAuthenticatedRequestFilterTest.java @@ -27,7 +27,15 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; import ch.qos.logback.classic.Level; +import com.netflix.spinnaker.fiat.shared.FiatService; import com.netflix.spinnaker.gate.Main; +import com.netflix.spinnaker.gate.services.ApplicationService; +import com.netflix.spinnaker.gate.services.PermissionService; +import com.netflix.spinnaker.gate.services.internal.ClouddriverService; +import com.netflix.spinnaker.gate.services.internal.ClouddriverServiceSelector; +import com.netflix.spinnaker.gate.services.internal.ExtendedFiatService; +import com.netflix.spinnaker.gate.services.internal.Front50Service; +import com.netflix.spinnaker.kork.client.ServiceClientProvider; import com.netflix.spinnaker.kork.common.Header; import com.netflix.spinnaker.kork.test.log.MemoryAppender; import com.netflix.spinnaker.security.AuthenticatedRequest; @@ -38,6 +46,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; @@ -61,6 +70,22 @@ public class GateConfigAuthenticatedRequestFilterTest { private static final String LOG_MESSAGE = " logged in api: "; private static final String NULL_VALUE = "null"; + @MockBean private ServiceClientProvider mockServiceClientProvider; + + @MockBean private ClouddriverService mockClouddriverService; + + @MockBean private ClouddriverServiceSelector mockClouddriverServiceSelector; + + @MockBean private ApplicationService mockApplicationService; + + @MockBean private FiatService mockFiatService; + + @MockBean private PermissionService mockPermissionService; + + @MockBean private ExtendedFiatService mockExtendedFiatService; + + @MockBean private Front50Service mockFront50Service; + @RestController @RequestMapping(value = API_BASE) static class TestController { From 21837d38bb8c284e68e6391035437df777339b3a Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 15 Sep 2023 13:05:20 -0400 Subject: [PATCH 110/182] chore(dependencies): Autobump korkVersion (#1711) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e573bfaf2f..8cb9b4cdae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.189.0 +korkVersion=7.190.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From 0c04a78c522832048cdb234de7322fbae490e4f2 Mon Sep 17 00:00:00 2001 From: Sandesh <30489233+j-sandy@users.noreply.github.com> Date: Wed, 20 Sep 2023 21:15:29 +0530 Subject: [PATCH 111/182] chore(dependency): add explicit dependency of groovy-json in gate-web while upgrading groovy 3.x (#1712) With groovy 2.5.15 and spockframework 1.3-groovy-2.5, groovy-json appear as transitive dependency of [spockframework](https://mvnrepository.com/artifact/org.spockframework/spock-core/1.3-groovy-2.5) as shown below: ``` $ ./gradlew gate-web:dI --dependency groovy-json --configuration testCompileClasspath > Task :gate-web:dependencyInsight org.codehaus.groovy:groovy-json:2.5.15 Variant compile: | Attribute Name | Provided | Requested | |--------------------------------|----------|-------------------| | org.gradle.status | release | | | org.gradle.category | library | library | | org.gradle.libraryelements | jar | classes+resources | | org.gradle.usage | java-api | java-api | | org.gradle.dependency.bundling | | external | | org.gradle.jvm.environment | | standard-jvm | | org.gradle.jvm.version | | 11 | Selection reasons: - By constraint - Forced org.codehaus.groovy:groovy-json:2.5.15 \--- io.spinnaker.kork:kork-bom:7.190.0 \--- testCompileClasspath org.codehaus.groovy:groovy-json:2.5.4 -> 2.5.15 +--- org.spockframework:spock-core:1.3-groovy-2.5 | +--- testCompileClasspath (requested org.spockframework:spock-core) | +--- io.spinnaker.kork:kork-bom:7.190.0 | | \--- testCompileClasspath | \--- org.spockframework:spock-spring:1.3-groovy-2.5 | +--- testCompileClasspath (requested org.spockframework:spock-spring) | \--- io.spinnaker.kork:kork-bom:7.190.0 (*) \--- org.spockframework:spock-spring:1.3-groovy-2.5 (*) ``` While upgrading groovy 3.0.10 and spockframework 2.0-groovy-3.0, groovy-json is not part of [spockframework](https://mvnrepository.com/artifact/org.spockframework/spock-core/2.0-groovy-3.0) and and encounter below error during test compilation: ``` > Task :gate-web:compileTestGroovy FAILED startup failed: /gate/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/ApplicationControllerSpec.groovy: 22: unable to resolve class groovy.json.JsonSlurper @ line 22, column 1. import groovy.json.JsonSlurper ^ /gate/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/BuildControllerSpec.groovy: 23: unable to resolve class groovy.json.JsonSlurper @ line 23, column 1. import groovy.json.JsonSlurper ^ /gate/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/EcsClusterControllerSpec.groovy: 22: unable to resolve class groovy.json.JsonSlurper @ line 22, column 1. import groovy.json.JsonSlurper ^ 3 errors ``` So, adding explicit dependency of groovy-json in gate-web.gradle --- gate-web/gate-web.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/gate-web/gate-web.gradle b/gate-web/gate-web.gradle index c923764e0d..18d3615d94 100644 --- a/gate-web/gate-web.gradle +++ b/gate-web/gate-web.gradle @@ -73,6 +73,7 @@ dependencies { testImplementation "com.unboundid:unboundid-ldapsdk" testImplementation "io.spinnaker.kork:kork-jedis-test" testImplementation "io.spinnaker.kork:kork-test" + testImplementation "org.codehaus.groovy:groovy-json" testRuntimeOnly "io.spinnaker.kork:kork-retrofit" // Add each included authz provider as a runtime dependency From 346ca355c2971604007cf895af84b9c4c2f749f2 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Wed, 20 Sep 2023 12:09:23 -0500 Subject: [PATCH 112/182] More groovy ports (#1705) * refactor(core): Move managed delivery model classes * refactor(core): Remove unused Eureka leftovers * refactor(core): Migrate remaining Groovy code to Java --------- Co-authored-by: Jason Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../spinnaker/gate/config/Service.groovy | 57 --------- .../gate/config/ServiceConfiguration.groovy | 81 ------------ .../model/discovery/DataCenterInfo.groovy | 26 ---- .../model/discovery/DataCenterMetadata.groovy | 37 ------ .../discovery/DiscoveryApplication.groovy | 42 ------- .../discovery/DiscoveryApplications.groovy | 28 ----- .../model/discovery/DiscoveryInstance.groovy | 86 ------------- .../gate/model/discovery/Port.groovy | 34 ------ .../gate/services/CredentialsService.groovy | 61 ---------- .../DefaultProviderLookupService.groovy | 105 ---------------- .../gate/services/EurekaLookupService.groovy | 115 ------------------ .../services/internal/EurekaService.groovy | 33 ----- .../spinnaker/gate/config/Service.java | 73 +++++++++++ .../gate/config/ServiceConfiguration.java | 89 ++++++++++++++ .../manageddelivery/ConstraintState.java | 0 .../manageddelivery/ConstraintStatus.java | 0 .../model/manageddelivery/DeliveryConfig.java | 0 .../model/manageddelivery/Environment.java | 0 .../EnvironmentArtifactPin.java | 0 .../EnvironmentArtifactVeto.java | 0 .../model/manageddelivery/GraphQLRequest.java | 0 .../model/manageddelivery/Notification.java | 0 .../OverrideVerificationRequest.java | 0 .../gate/model/manageddelivery/Resource.java | 0 .../RetryVerificationRequest.java | 0 .../gate/services/AccountLookupService.java} | 11 +- .../gate/services/CredentialsService.java | 86 +++++++++++++ .../DefaultProviderLookupService.java | 113 +++++++++++++++++ .../gate/services/ProviderLookupService.java} | 8 +- .../spinnaker/config/GremlinConfig.java | 8 -- .../spinnaker/gate/config/GateConfig.groovy | 4 - .../CredentialsControllerSpec.groovy | 5 +- .../services/CredentialsServiceSpec.groovy | 6 +- 33 files changed, 373 insertions(+), 735 deletions(-) delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/Service.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/ServiceConfiguration.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DataCenterInfo.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DataCenterMetadata.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DiscoveryApplication.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DiscoveryApplications.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DiscoveryInstance.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/Port.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/CredentialsService.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/DefaultProviderLookupService.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/EurekaLookupService.groovy delete mode 100644 gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/internal/EurekaService.groovy create mode 100644 gate-core/src/main/java/com/netflix/spinnaker/gate/config/Service.java create mode 100644 gate-core/src/main/java/com/netflix/spinnaker/gate/config/ServiceConfiguration.java rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/model/manageddelivery/ConstraintState.java (100%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/model/manageddelivery/ConstraintStatus.java (100%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/model/manageddelivery/DeliveryConfig.java (100%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/model/manageddelivery/Environment.java (100%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/model/manageddelivery/EnvironmentArtifactPin.java (100%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/model/manageddelivery/EnvironmentArtifactVeto.java (100%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/model/manageddelivery/GraphQLRequest.java (100%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/model/manageddelivery/Notification.java (100%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/model/manageddelivery/OverrideVerificationRequest.java (100%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/model/manageddelivery/Resource.java (100%) rename gate-core/src/main/{groovy => java}/com/netflix/spinnaker/gate/model/manageddelivery/RetryVerificationRequest.java (100%) rename gate-core/src/main/{groovy/com/netflix/spinnaker/gate/services/AccountLookupService.groovy => java/com/netflix/spinnaker/gate/services/AccountLookupService.java} (78%) create mode 100644 gate-core/src/main/java/com/netflix/spinnaker/gate/services/CredentialsService.java create mode 100644 gate-core/src/main/java/com/netflix/spinnaker/gate/services/DefaultProviderLookupService.java rename gate-core/src/main/{groovy/com/netflix/spinnaker/gate/services/ProviderLookupService.groovy => java/com/netflix/spinnaker/gate/services/ProviderLookupService.java} (81%) diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/Service.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/Service.groovy deleted file mode 100644 index 7596fead44..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/Service.groovy +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2015 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.config - -import groovy.transform.CompileStatic - -@CompileStatic -class Service { - boolean enabled = true - String vipAddress - String baseUrl - MultiBaseUrl shards - int priority = 1 - Map config = [:] - - void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl - } - - static class BaseUrl { - String vipAddress - String baseUrl - int priority = 1 - Map config = [:] - } - - static class MultiBaseUrl { - String baseUrl - List baseUrls - } - - List getBaseUrls() { - if (shards?.baseUrl) { - return [ - new BaseUrl(baseUrl: shards.baseUrl) - ] - } else if (shards?.baseUrls) { - return shards.baseUrls - } - - return [new BaseUrl(baseUrl: baseUrl)] - } -} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/ServiceConfiguration.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/ServiceConfiguration.groovy deleted file mode 100644 index f3a2068032..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/config/ServiceConfiguration.groovy +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2015 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.config - -import groovy.transform.CompileStatic -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.context.ApplicationContext -import org.springframework.stereotype.Component -import retrofit.Endpoint - -import javax.annotation.PostConstruct - -import static retrofit.Endpoints.newFixedEndpoint -import static retrofit.Endpoints.newFixedEndpoint -import static retrofit.Endpoints.newFixedEndpoint - -@CompileStatic -@Component -@ConfigurationProperties -class ServiceConfiguration { - List healthCheckableServices - List discoveryHosts - Map services = [:] - Map integrations = [:] - - @Autowired - ApplicationContext ctx - - @PostConstruct - void postConstruct() { - // this check is done in a @PostConstruct to avoid Spring's list merging in @ConfigurationProperties (vs. overriding) - healthCheckableServices = healthCheckableServices ?: [ - "orca", "clouddriver", "echo", "igor", "flex", "front50", "mahe", "mine", "keel" - ] - } - - Service getService(String name) { - (services + integrations)[name] - } - - Endpoint getServiceEndpoint(String serviceName, String dynamicName = null) { - Service service = getService(serviceName) - - if (service == null) { - throw new IllegalArgumentException("Unknown service ${serviceName}") - } - - Endpoint endpoint - if (dynamicName == null) { - // TODO: move Netflix-specific logic out of the OSS implementation - endpoint = discoveryHosts && service.vipAddress ? - newFixedEndpoint("niws://${service.vipAddress}") - : newFixedEndpoint(service.baseUrl) - } else { - if (!service.getConfig().containsKey("dynamicEndpoints")) { - throw new IllegalArgumentException("Unknown dynamicEndpoint ${dynamicName} for service ${serviceName}") - } - endpoint = newFixedEndpoint(((Map) service.getConfig().get("dynamicEndpoints")).get(dynamicName)) - } - - return endpoint - } - - -} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DataCenterInfo.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DataCenterInfo.groovy deleted file mode 100644 index 01fd0fbe85..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DataCenterInfo.groovy +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2014 Netflix, Inc. - * - * 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. - */ - - -package com.netflix.spinnaker.gate.model.discovery - -import groovy.transform.EqualsAndHashCode - -@EqualsAndHashCode -class DataCenterInfo { - String name - DataCenterMetadata metadata -} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DataCenterMetadata.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DataCenterMetadata.groovy deleted file mode 100644 index badabe3fa4..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DataCenterMetadata.groovy +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2014 Netflix, Inc. - * - * 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. - */ - - -package com.netflix.spinnaker.gate.model.discovery - -import com.fasterxml.jackson.annotation.JsonProperty -import groovy.transform.EqualsAndHashCode - -@EqualsAndHashCode -class DataCenterMetadata { - @JsonProperty('availability-zone') - String availabilityZone - - @JsonProperty('instance-id') - String instanceId - - @JsonProperty('ami-id') - String amiId - - @JsonProperty('instance-type') - String instanceType -} - diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DiscoveryApplication.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DiscoveryApplication.groovy deleted file mode 100644 index 8889e05050..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DiscoveryApplication.groovy +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2014 Netflix, Inc. - * - * 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. - */ - - -package com.netflix.spinnaker.gate.model.discovery -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.annotation.JsonRootName -import groovy.transform.EqualsAndHashCode - -import java.util.concurrent.ThreadLocalRandom - -@JsonRootName("application") -@EqualsAndHashCode -class DiscoveryApplication { - - static DiscoveryInstance getRandomUpInstance(List apps) { - List candidates = apps.collect { it.instances.findAll { it.status == 'UP' && it.port.enabled } }.flatten() - if (!candidates) { - return null - } - candidates[ThreadLocalRandom.current().nextInt(candidates.size())] - } - - String name - - @JsonProperty('instance') - List instances -} - diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DiscoveryApplications.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DiscoveryApplications.groovy deleted file mode 100644 index 2b3850cc6c..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DiscoveryApplications.groovy +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2014 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.model.discovery - -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.annotation.JsonRootName -import groovy.transform.EqualsAndHashCode - -@JsonRootName('applications') -@EqualsAndHashCode -class DiscoveryApplications { - @JsonProperty('application') - List applications -} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DiscoveryInstance.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DiscoveryInstance.groovy deleted file mode 100644 index ad25dfa827..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/DiscoveryInstance.groovy +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2014 Netflix, Inc. - * - * 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. - */ - - -package com.netflix.spinnaker.gate.model.discovery - -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonProperty -import groovy.transform.* - -@CompileStatic -@Immutable -@EqualsAndHashCode(cache = true) -class DiscoveryInstance { - public static final String HEALTH_TYPE = 'Discovery' - - public String getType() { - HEALTH_TYPE - } - String hostName - Port port - Port securePort - String application - String ipAddress - String status - String overriddenStatus - String state - - String availabilityZone - String instanceId - String amiId - String instanceType - - String healthCheckUrl - String vipAddress - Long lastUpdatedTimestamp - String asgName - - @JsonCreator - public static DiscoveryInstance buildInstance(@JsonProperty('hostName') String hostName, - @JsonProperty('port') Port port, - @JsonProperty('securePort') Port securePort, - @JsonProperty('app') String app, - @JsonProperty('ipAddr') String ipAddr, - @JsonProperty('status') String status, - @JsonProperty('overriddenstatus') String overriddenstatus, - @JsonProperty('dataCenterInfo') DataCenterInfo dataCenterInfo, - @JsonProperty('healthCheckUrl') String healthCheckUrl, - @JsonProperty('vipAddress') String vipAddress, - @JsonProperty('lastUpdatedTimestamp') long lastUpdatedTimestamp, - @JsonProperty('asgName') String asgName) { - def meta = dataCenterInfo.metadata - new DiscoveryInstance( - hostName, - port, - securePort, - app, - ipAddr, - status, - overriddenstatus, - status, - meta?.availabilityZone, - meta?.instanceId, - meta?.amiId, - meta?.instanceType, - healthCheckUrl, - vipAddress, - lastUpdatedTimestamp, - asgName) - } -} - - diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/Port.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/Port.groovy deleted file mode 100644 index d3e11a8097..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/discovery/Port.groovy +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2014 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.model.discovery - -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonProperty -import groovy.transform.CompileStatic -import groovy.transform.Immutable - -@CompileStatic -@Immutable -class Port { - boolean enabled - int port - - @JsonCreator - public static Port buildPort(@JsonProperty('@enabled') boolean enabled, @JsonProperty('$') int port) { - new Port(enabled, port) - } -} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/CredentialsService.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/CredentialsService.groovy deleted file mode 100644 index a9201cbec2..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/CredentialsService.groovy +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2014 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.services - -import com.netflix.spinnaker.fiat.shared.FiatStatus -import com.netflix.spinnaker.gate.services.internal.ClouddriverService.AccountDetails -import groovy.util.logging.Slf4j -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.stereotype.Service - -@Slf4j -@Service -class CredentialsService { - @Autowired - AccountLookupService accountLookupService - - @Autowired - FiatStatus fiatStatus - - Collection getAccountNames(Collection userRoles) { - getAccounts(userRoles, false)*.name - } - - Collection getAccountNames(Collection userRoles, boolean ignoreFiatStatus) { - getAccounts(userRoles, ignoreFiatStatus)*.name - } - - /** - * Returns all account names that a user with the specified list of userRoles has access to. - */ - List getAccounts(Collection userRoles, boolean ignoreFiatStatus) { - final Set userRolesLower = userRoles*.toLowerCase() as Set - return accountLookupService.getAccounts().findAll { AccountDetails account -> - if (!ignoreFiatStatus && fiatStatus.isEnabled()) { - return true // Returned list is filtered later. - } - - if (!account.permissions) { - return true - } - - Set permissions = account.permissions.WRITE*.toLowerCase() ?: [] - - return userRolesLower.intersect(permissions) as Boolean - } ?: [] - } -} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/DefaultProviderLookupService.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/DefaultProviderLookupService.groovy deleted file mode 100644 index fc584e3839..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/DefaultProviderLookupService.groovy +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.services - -import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.databind.ObjectMapper -import com.google.common.util.concurrent.UncheckedExecutionException -import com.netflix.spinnaker.gate.services.internal.ClouddriverService -import com.netflix.spinnaker.gate.services.internal.ClouddriverService.AccountDetails -import com.netflix.spinnaker.security.AuthenticatedRequest -import groovy.util.logging.Slf4j -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.scheduling.annotation.Scheduled -import org.springframework.stereotype.Component - -import java.util.concurrent.ExecutionException -import java.util.concurrent.atomic.AtomicReference - -/** - * DefaultProviderLookupService. - */ -@Slf4j -@Component("providerLookupService") -class DefaultProviderLookupService implements ProviderLookupService, AccountLookupService { - - private static final String FALLBACK = "unknown" - private static final TypeReference> JSON_LIST = new TypeReference>() {} - private static final TypeReference> ACCOUNT_DETAILS_LIST = new TypeReference>() {} - - private final ClouddriverService clouddriverService - private final ObjectMapper mapper = new ObjectMapper() - - private final AtomicReference> accountsCache = new AtomicReference<>([]) - - @Autowired - DefaultProviderLookupService(ClouddriverService clouddriverService) { - this.clouddriverService = clouddriverService - } - - @Scheduled(fixedDelay = 30000L) - void refreshCache() { - try { - def accounts = AuthenticatedRequest.allowAnonymous { clouddriverService.getAccountDetails() } - //migration support, prefer permissions configuration, translate requiredGroupMembership - // (for credentialsservice in non fiat mode) into permissions collection. - // - // Ignore explicitly set requiredGroupMemberships if permissions are also present. - for (account in accounts) { - if (account.permissions != null) { - account.permissions = account.permissions.collectEntries { String perm, Collection roles -> - Set rolesLower = roles*.toLowerCase() - [(perm): rolesLower] - } - if (account.requiredGroupMembership) { - Set rgmSet = account.requiredGroupMembership*.toLowerCase() - if (account.permissions.WRITE != rgmSet) { - log.warn("on Account $account.name: preferring permissions: $account.permissions over requiredGroupMemberships: $rgmSet for authz decision") - } - } - - } else { - account.requiredGroupMembership = account.requiredGroupMembership.collect { it.toLowerCase() } - if (account.requiredGroupMembership) { - account.permissions = [READ: account.requiredGroupMembership, WRITE: account.requiredGroupMembership] - } else { - account.permissions = [:] - } - } - } - accountsCache.set(accounts) - } catch (Exception e) { - log.error("Unable to refresh account details cache", e) - } - } - - @Override - String providerForAccount(String account) { - try { - return accountsCache.get()?.find { it.name == account }?.type ?: FALLBACK - } catch (ExecutionException | UncheckedExecutionException ex) { - return FALLBACK - } - } - - @Override - List getAccounts() { - final List original = accountsCache.get() - final List accountsCopy = mapper.convertValue(original, JSON_LIST) - return mapper.convertValue(accountsCopy, ACCOUNT_DETAILS_LIST) - } -} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/EurekaLookupService.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/EurekaLookupService.groovy deleted file mode 100644 index 579d17f5b0..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/EurekaLookupService.groovy +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2014 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.services - -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.ObjectMapper -import com.jakewharton.retrofit.Ok3Client -import com.netflix.spinnaker.gate.config.ServiceConfiguration -import com.netflix.spinnaker.gate.model.discovery.DiscoveryApplication -import com.netflix.spinnaker.gate.retrofit.Slf4jRetrofitLogger -import com.netflix.spinnaker.gate.services.internal.EurekaService -import groovy.transform.Immutable -import okhttp3.OkHttpClient -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.stereotype.Component -import retrofit.RestAdapter -import retrofit.RetrofitError -import retrofit.converter.JacksonConverter - -import javax.annotation.PostConstruct -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit - -import static retrofit.Endpoints.newFixedEndpoint - -@Component -class EurekaLookupService { - private static final Map instanceCache = new ConcurrentHashMap<>() - - @Autowired - ServiceConfiguration serviceConfiguration - - @Autowired - OkHttpClient okHttpClient - - @PostConstruct - void init() { - Executors.newScheduledThreadPool(1).scheduleAtFixedRate({ - for (vip in instanceCache.keySet()) { - def cached = instanceCache[vip] - if (cached.expired) { - getApplications(vip) - } - } - }, 0, 30, TimeUnit.SECONDS) - } - - List getApplications(String vip) { - if (instanceCache.containsKey(vip) && !instanceCache[vip].expired) { - return instanceCache[vip].applications - } - List hosts = [] - hosts.addAll(serviceConfiguration.discoveryHosts) - Collections.shuffle(hosts) - - def app = null - for (host in hosts) { - EurekaService eureka = getEurekaService(host) - try { - app = eureka.getVips(vip) - if (app && app.applications) { - instanceCache[vip] = new CachedDiscoveryApplication(applications: app.applications) - break - } - } catch (RetrofitError e) { - if (e.response.status != 404) { - throw e - } - } - } - if (!app) { - return null - } - app.applications - } - - private EurekaService getEurekaService(String host) { - def endpoint = newFixedEndpoint(host) - new RestAdapter.Builder() - .setEndpoint(endpoint) - .setConverter(new JacksonConverter(new ObjectMapper().configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true))) - .setClient(new Ok3Client(okHttpClient)) - .setLogLevel(RestAdapter.LogLevel.BASIC) - .setLog(new Slf4jRetrofitLogger(EurekaService)) - .build() - .create(EurekaService) - } - - @Immutable(knownImmutables = ["applications"]) - static class CachedDiscoveryApplication { - private final Long ttl = TimeUnit.SECONDS.toMillis(60) - private final Long cacheTime = System.currentTimeMillis() - final List applications - - boolean isExpired() { - (System.currentTimeMillis() - cacheTime) > ttl - } - } -} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/internal/EurekaService.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/internal/EurekaService.groovy deleted file mode 100644 index 12d0e5ddd0..0000000000 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/internal/EurekaService.groovy +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2014 Netflix, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.services.internal - -import com.netflix.spinnaker.gate.model.discovery.DiscoveryApplications -import retrofit.http.GET -import retrofit.http.Headers -import retrofit.http.Path - -interface EurekaService { - - @Headers("Accept: application/json") - @GET("/discovery/v2/vips/{vipAddress}") - DiscoveryApplications getVips(@Path("vipAddress") String vipAddress) - - @Headers("Accept: application/json") - @GET("/discovery/v2/svips/{secureVipAddress}") - DiscoveryApplications getSecureVips(@Path("secureVipAddress") String secureVipAddress) -} diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/config/Service.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/config/Service.java new file mode 100644 index 0000000000..a2b8345b1e --- /dev/null +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/config/Service.java @@ -0,0 +1,73 @@ +/* + * Copyright 2015 Netflix, Inc. + * Copyright 2023 Apple, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.config; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +@Getter +@Setter +public class Service { + private boolean enabled = true; + private String baseUrl; + private MultiBaseUrl shards; + private Map config = new LinkedHashMap<>(); + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + } + + public List getBaseUrls() { + if (shards != null) { + String baseUrl = shards.getBaseUrl(); + if (StringUtils.hasLength(baseUrl)) { + return List.of(new BaseUrl(baseUrl)); + } + List baseUrls = shards.getBaseUrls(); + if (!CollectionUtils.isEmpty(baseUrls)) { + return baseUrls; + } + } + return List.of(new BaseUrl(baseUrl)); + } + + @Getter + @Setter + public static class MultiBaseUrl { + private String baseUrl; + private List baseUrls; + } + + @Getter + @Setter + @NoArgsConstructor + @RequiredArgsConstructor + public static class BaseUrl { + @Nonnull private String baseUrl; + private int priority = 1; + private Map config = new LinkedHashMap<>(); + } +} diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/config/ServiceConfiguration.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/config/ServiceConfiguration.java new file mode 100644 index 0000000000..dd86d96fc6 --- /dev/null +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/config/ServiceConfiguration.java @@ -0,0 +1,89 @@ +/* + * Copyright 2015 Netflix, Inc. + * Copyright 2023 Apple, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.config; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import retrofit.Endpoint; +import retrofit.Endpoints; + +@Getter +@Setter +@Component +@ConfigurationProperties +public class ServiceConfiguration { + private static final String DYNAMIC_ENDPOINTS = "dynamicEndpoints"; + + private List healthCheckableServices = new ArrayList<>(); + private Map services = new LinkedHashMap<>(); + private Map integrations = new LinkedHashMap<>(); + + @PostConstruct + void postConstruct() { + // this check is done in a @PostConstruct to avoid Spring's list merging in + // @ConfigurationProperties (vs. overriding) + if (CollectionUtils.isEmpty(healthCheckableServices)) { + healthCheckableServices = + List.of("orca", "clouddriver", "echo", "igor", "flex", "front50", "mahe", "mine", "keel"); + } + } + + @Nullable + public Service getService(@Nonnull String name) { + if (services.containsKey(name)) { + return services.get(name); + } + return integrations.get(name); + } + + @Nonnull + public Endpoint getServiceEndpoint(@Nonnull String serviceName) { + return getServiceEndpoint(serviceName, null); + } + + @Nonnull + public Endpoint getServiceEndpoint(@Nonnull String serviceName, @Nullable String dynamicName) { + Service service = getService(serviceName); + if (service == null) { + throw new IllegalArgumentException("Unknown service " + serviceName); + } + if (dynamicName == null) { + String serviceBaseUrl = service.getBaseUrl(); + return Endpoints.newFixedEndpoint(serviceBaseUrl); + } + Map config = service.getConfig(); + if (!config.containsKey(DYNAMIC_ENDPOINTS)) { + throw new IllegalArgumentException( + String.format("Unknown dynamicEndpoint %s for service %s", dynamicName, serviceName)); + } + @SuppressWarnings("unchecked") + Map dynamicEndpoints = (Map) config.get(DYNAMIC_ENDPOINTS); + String dynamicEndpoint = dynamicEndpoints.get(dynamicName); + return Endpoints.newFixedEndpoint(dynamicEndpoint); + } +} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/ConstraintState.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/ConstraintState.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/ConstraintState.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/ConstraintState.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/ConstraintStatus.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/ConstraintStatus.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/ConstraintStatus.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/ConstraintStatus.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/DeliveryConfig.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/DeliveryConfig.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/DeliveryConfig.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/DeliveryConfig.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/Environment.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/Environment.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/Environment.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/Environment.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/EnvironmentArtifactPin.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/EnvironmentArtifactPin.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/EnvironmentArtifactPin.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/EnvironmentArtifactPin.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/EnvironmentArtifactVeto.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/EnvironmentArtifactVeto.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/EnvironmentArtifactVeto.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/EnvironmentArtifactVeto.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/GraphQLRequest.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/GraphQLRequest.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/GraphQLRequest.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/GraphQLRequest.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/Notification.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/Notification.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/Notification.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/Notification.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/OverrideVerificationRequest.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/OverrideVerificationRequest.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/OverrideVerificationRequest.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/OverrideVerificationRequest.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/Resource.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/Resource.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/Resource.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/Resource.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/RetryVerificationRequest.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/RetryVerificationRequest.java similarity index 100% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/model/manageddelivery/RetryVerificationRequest.java rename to gate-core/src/main/java/com/netflix/spinnaker/gate/model/manageddelivery/RetryVerificationRequest.java diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/AccountLookupService.groovy b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/AccountLookupService.java similarity index 78% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/AccountLookupService.groovy rename to gate-core/src/main/java/com/netflix/spinnaker/gate/services/AccountLookupService.java index ee9b05d8a4..e25e4003f8 100644 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/AccountLookupService.groovy +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/AccountLookupService.java @@ -1,7 +1,7 @@ /* * Copyright 2016 Netflix, Inc. * - * Licensed under the Apache License, Version 2.0 (the "License") + * 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 * @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.netflix.spinnaker.gate.services +package com.netflix.spinnaker.gate.services; -import com.netflix.spinnaker.gate.services.internal.ClouddriverService.AccountDetails +import com.netflix.spinnaker.gate.services.internal.ClouddriverService; +import java.util.List; -interface AccountLookupService { - List getAccounts() +public interface AccountLookupService { + List getAccounts(); } diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/CredentialsService.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/CredentialsService.java new file mode 100644 index 0000000000..56288b17e6 --- /dev/null +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/CredentialsService.java @@ -0,0 +1,86 @@ +/* + * Copyright 2014 Netflix, Inc. + * Copyright 2023 Apple, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.services; + +import com.netflix.spinnaker.fiat.model.Authorization; +import com.netflix.spinnaker.fiat.shared.FiatStatus; +import com.netflix.spinnaker.gate.services.internal.ClouddriverService; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class CredentialsService { + private final AccountLookupService accountLookupService; + private final FiatStatus fiatStatus; + + public Collection getAccountNames(@Nullable Collection userRoles) { + return getAccounts(userRoles, false).stream() + .map(ClouddriverService.Account::getName) + .collect(Collectors.toList()); + } + + public Collection getAccountNames( + @Nullable Collection userRoles, boolean ignoreFiatStatus) { + return getAccounts(userRoles, ignoreFiatStatus).stream() + .map(ClouddriverService.Account::getName) + .collect(Collectors.toList()); + } + + /** Returns all account names that a user with the specified list of userRoles has access to. */ + List getAccounts( + @Nullable Collection userRoles, boolean ignoreFiatStatus) { + Set userRolesLower = + userRoles == null + ? Set.of() + : userRoles.stream() + .filter(Objects::nonNull) + .map(role -> role.toLowerCase(Locale.ROOT)) + .collect(Collectors.toSet()); + return accountLookupService.getAccounts().stream() + .filter( + account -> { + if (!ignoreFiatStatus && fiatStatus.isEnabled()) { + return true; // Returned list is filtered later. + } + + Map> permissions = account.getPermissions(); + if (CollectionUtils.isEmpty(permissions)) { + return true; + } + Set permittedRoles = + permissions.getOrDefault(Authorization.WRITE.name(), Set.of()).stream() + .map(role -> role.toLowerCase(Locale.ROOT)) + .collect(Collectors.toSet()); + return !Collections.disjoint(userRolesLower, permittedRoles); + }) + .collect(Collectors.toList()); + } +} diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/DefaultProviderLookupService.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/DefaultProviderLookupService.java new file mode 100644 index 0000000000..aa9219f80a --- /dev/null +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/DefaultProviderLookupService.java @@ -0,0 +1,113 @@ +/* + * Copyright 2016 Netflix, Inc. + * Copyright 2023 Apple, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.services; + +import com.netflix.spinnaker.fiat.model.Authorization; +import com.netflix.spinnaker.gate.services.internal.ClouddriverService; +import com.netflix.spinnaker.security.AuthenticatedRequest; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +@Log4j2 +@Service("providerLookupService") +@RequiredArgsConstructor +public class DefaultProviderLookupService implements ProviderLookupService, AccountLookupService { + private static final String FALLBACK = "unknown"; + private final ClouddriverService clouddriverService; + + private volatile List accountsCache = List.of(); + + @Scheduled(fixedDelay = 30, timeUnit = TimeUnit.SECONDS) + void refreshCache() { + try { + accountsCache = loadAccounts(); + } catch (Exception e) { + log.error("Unable to refresh account details cache", e); + } + } + + private List loadAccounts() { + var accounts = AuthenticatedRequest.allowAnonymous(clouddriverService::getAccountDetails); + // migration support, prefer permissions configuration, translate requiredGroupMembership + // (for CredentialsService in non fiat mode) into permissions collection. + // + // Ignore explicitly set requiredGroupMemberships if permissions are also present. + for (var account : accounts) { + Map> permissions = account.getPermissions(); + Collection requiredGroupMembership = account.getRequiredGroupMembership(); + if (permissions != null) { + for (var entry : permissions.entrySet()) { + entry.setValue(toLowerCase(entry.getValue()).collect(Collectors.toList())); + } + + if (!CollectionUtils.isEmpty(requiredGroupMembership)) { + Set rgmSet = toLowerCase(requiredGroupMembership).collect(Collectors.toSet()); + Collection permittedRoles = permissions.get(Authorization.WRITE.name()); + if (!rgmSet.equals(permittedRoles)) { + log.warn( + "On account {}: preferring permissions: {} over requiredGroupMemberships: {} for authorization decision", + account.getName(), + permissions, + rgmSet); + } + } + } else { + if (CollectionUtils.isEmpty(requiredGroupMembership)) { + account.setPermissions(Map.of()); + } else { + List rgm = toLowerCase(requiredGroupMembership).collect(Collectors.toList()); + account.setRequiredGroupMembership(rgm); + account.setPermissions( + Map.of( + Authorization.READ.name(), rgm, + Authorization.WRITE.name(), rgm)); + } + } + } + return accounts; + } + + @Override + public String providerForAccount(String account) { + return accountsCache.stream() + .filter(it -> account.equals(it.getName())) + .map(ClouddriverService.Account::getType) + .findFirst() + .orElse(FALLBACK); + } + + @Override + public List getAccounts() { + return accountsCache; + } + + private static Stream toLowerCase(Collection strings) { + return strings.stream().map(s -> s.toLowerCase(Locale.ROOT)); + } +} diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/ProviderLookupService.groovy b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/ProviderLookupService.java similarity index 81% rename from gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/ProviderLookupService.groovy rename to gate-core/src/main/java/com/netflix/spinnaker/gate/services/ProviderLookupService.java index 32b4794354..9f278219e5 100644 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/ProviderLookupService.groovy +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/ProviderLookupService.java @@ -1,7 +1,7 @@ /* * Copyright 2016 Netflix, Inc. * - * Licensed under the Apache License, Version 2.0 (the "License") + * 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 * @@ -14,8 +14,8 @@ * limitations under the License. */ -package com.netflix.spinnaker.gate.services +package com.netflix.spinnaker.gate.services; -interface ProviderLookupService { - String providerForAccount(String account) +public interface ProviderLookupService { + String providerForAccount(String account); } diff --git a/gate-integrations-gremlin/src/main/java/com/netflix/spinnaker/config/GremlinConfig.java b/gate-integrations-gremlin/src/main/java/com/netflix/spinnaker/config/GremlinConfig.java index e535e91325..31841916f9 100644 --- a/gate-integrations-gremlin/src/main/java/com/netflix/spinnaker/config/GremlinConfig.java +++ b/gate-integrations-gremlin/src/main/java/com/netflix/spinnaker/config/GremlinConfig.java @@ -21,11 +21,9 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.jakewharton.retrofit.Ok3Client; -import com.netflix.spectator.api.Registry; import com.netflix.spinnaker.gate.config.Service; import com.netflix.spinnaker.gate.config.ServiceConfiguration; import com.netflix.spinnaker.gate.retrofit.Slf4jRetrofitLogger; -import com.netflix.spinnaker.gate.services.EurekaLookupService; import com.netflix.spinnaker.gate.services.gremlin.GremlinService; import groovy.transform.CompileStatic; import groovy.util.logging.Slf4j; @@ -48,8 +46,6 @@ class GremlinConfig { GremlinService gremlinService( OkHttpClient okHttpClient, ServiceConfiguration serviceConfiguration, - Registry registry, - EurekaLookupService eurekaLookupService, RequestInterceptor spinnakerRequestInterceptor, @Value("${retrofit.log-level:BASIC}") String retrofitLogLevel) { return createClient( @@ -57,8 +53,6 @@ GremlinService gremlinService( GremlinService.class, okHttpClient, serviceConfiguration, - registry, - eurekaLookupService, spinnakerRequestInterceptor, retrofitLogLevel); } @@ -68,8 +62,6 @@ private T createClient( Class type, OkHttpClient okHttpClient, ServiceConfiguration serviceConfiguration, - Registry registry, - EurekaLookupService eurekaLookupService, RequestInterceptor spinnakerRequestInterceptor, String retrofitLogLevel) { Service service = serviceConfiguration.getService(serviceName); diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy index 0a2a5df143..4cda1529ae 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy @@ -36,7 +36,6 @@ import com.netflix.spinnaker.gate.filters.RequestSheddingFilter import com.netflix.spinnaker.gate.filters.ResetAuthenticatedRequestFilter import com.netflix.spinnaker.gate.plugins.deck.DeckPluginConfiguration import com.netflix.spinnaker.gate.plugins.web.PluginWebConfiguration -import com.netflix.spinnaker.gate.services.EurekaLookupService import com.netflix.spinnaker.gate.services.internal.* import com.netflix.spinnaker.kork.client.ServiceClientProvider import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService @@ -138,9 +137,6 @@ class GateConfig extends RedisHttpSessionConfiguration { @Autowired Registry registry - @Autowired - EurekaLookupService eurekaLookupService - @Autowired ServiceConfiguration serviceConfiguration diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/CredentialsControllerSpec.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/CredentialsControllerSpec.groovy index 8db2c6664d..a072882437 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/CredentialsControllerSpec.groovy +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/CredentialsControllerSpec.groovy @@ -49,10 +49,7 @@ class CredentialsControllerSpec extends Specification { } @Subject - CredentialsService credentialsService = new CredentialsService( - accountLookupService: accountLookupService, - fiatStatus: fiatStatus - ) + CredentialsService credentialsService = new CredentialsService(accountLookupService, fiatStatus) FiatPermissionEvaluator fpe = Stub(FiatPermissionEvaluator) AllowedAccountsSupport allowedAccountsSupport = new AllowedAccountsSupport(fiatStatus, fpe, credentialsService) diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/services/CredentialsServiceSpec.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/services/CredentialsServiceSpec.groovy index ccbfe956c0..93a7e3083c 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/services/CredentialsServiceSpec.groovy +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/services/CredentialsServiceSpec.groovy @@ -16,7 +16,6 @@ package com.netflix.spinnaker.gate.services -import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties import com.netflix.spinnaker.fiat.shared.FiatStatus import com.netflix.spinnaker.gate.services.internal.ClouddriverService import com.netflix.spinnaker.gate.services.internal.ClouddriverService.AccountDetails @@ -41,10 +40,7 @@ class CredentialsServiceSpec extends Specification { } @Subject - CredentialsService credentialsService = new CredentialsService( - accountLookupService: accountLookupService, - fiatStatus: fiatStatus - ) + CredentialsService credentialsService = new CredentialsService(accountLookupService, fiatStatus) expect: credentialsService.getAccountNames(roles) == expectedAccounts From 84550aea5e864519e9f846ac4bd6753d3487ab61 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 26 Sep 2023 22:27:47 -0400 Subject: [PATCH 113/182] chore(dependencies): Autobump korkVersion (#1713) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8cb9b4cdae..580003ebf9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.190.0 +korkVersion=7.191.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From 9f664b074c254880101acd571af2b77583796e80 Mon Sep 17 00:00:00 2001 From: Matt Gogerly <6519811+mattgogerly@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:41:37 +0100 Subject: [PATCH 114/182] JRE 17 config for Gate (#1714) * fix(java17): refactor test fixture to not mock static class * fix(java17): add Jackson converters to RestAdapters to avoid GSON * feat(java17): add configuration to build JRE 17 images and run tests with JRE 17 --- .github/workflows/build.yml | 29 ++++++++++++++++++- .github/workflows/pr.yml | 24 ++++++++++++++- .github/workflows/release.yml | 29 ++++++++++++++++++- Dockerfile.java11.slim | 9 ++++++ Dockerfile.java11.ubuntu | 8 +++++ Dockerfile.slim | 2 +- Dockerfile.ubuntu | 2 +- build.gradle | 11 +++++++ .../gate/plugins/deck/DeckPluginCacheTest.kt | 17 +++++++---- .../spinnaker/gate/FunctionalSpec.groovy | 2 ++ .../controllers/WebhookControllerSpec.groovy | 2 ++ 11 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 Dockerfile.java11.slim create mode 100644 Dockerfile.java11.ubuntu diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1bba1c90b4..5db104b580 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,9 @@ jobs: uses: docker/setup-buildx-action@v2 - uses: actions/setup-java@v3 with: - java-version: 11 + java-version: | + 17 + 11 distribution: 'zulu' cache: 'gradle' - name: Prepare build variables @@ -72,3 +74,28 @@ jobs: tags: | "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ github.ref_name }}-latest-unvalidated-ubuntu" "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-unvalidated-ubuntu" + - name: Build and publish slim JRE 11 container image + # Only run this on repositories in the 'spinnaker' org, not on forks. + if: startsWith(github.repository, 'spinnaker/') + uses: docker/build-push-action@v4 + with: + context: . + file: Dockerfile.java11.slim + platforms: linux/amd64,linux/arm64 + push: true + tags: | + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ github.ref_name }}-latest-java11-unvalidated" + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-java11-unvalidated" + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ github.ref_name }}-latest-java11-unvalidated-slim" + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-java11-unvalidated-slim" + - name: Build and publish ubuntu JRE 11 container image + # Only run this on repositories in the 'spinnaker' org, not on forks. + if: startsWith(github.repository, 'spinnaker/') + uses: docker/build-push-action@v4 + with: + context: . + file: Dockerfile.java11.ubuntu + push: true + tags: | + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ github.ref_name }}-latest-java11-unvalidated-ubuntu" + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-java11-unvalidated-ubuntu" \ No newline at end of file diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 8119027332..b199e1c487 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -19,7 +19,9 @@ jobs: uses: docker/setup-buildx-action@v2 - uses: actions/setup-java@v3 with: - java-version: 11 + java-version: | + 17 + 11 distribution: 'zulu' cache: 'gradle' - name: Prepare build variables @@ -51,3 +53,23 @@ jobs: tags: | "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:latest-ubuntu" "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-ubuntu" + - name: Build slim JRE 11 container image + uses: docker/build-push-action@v4 + with: + context: . + file: Dockerfile.java11.slim + platforms: linux/amd64,linux/arm64 + tags: | + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:latest-java11" + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-java11" + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:latest-java11-slim" + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-java11-slim" + - name: Build ubuntu JRE 11 container image + uses: docker/build-push-action@v4 + with: + context: . + file: Dockerfile.java11.ubuntu + platforms: linux/amd64,linux/arm64 + tags: | + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:latest-java11-ubuntu" + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-java11-ubuntu" \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 238fb3aaca..cee1cb4c43 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,9 @@ jobs: uses: docker/setup-buildx-action@v2 - uses: actions/setup-java@v3 with: - java-version: 11 + java-version: | + 17 + 11 distribution: 'zulu' cache: 'gradle' - name: Assemble release info @@ -108,6 +110,31 @@ jobs: tags: | "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.release_info.outputs.RELEASE_VERSION }}-unvalidated-ubuntu" "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.release_info.outputs.RELEASE_VERSION }}-${{ steps.build_variables.outputs.VERSION }}-unvalidated-ubuntu" + - name: Build and publish slim JRE 11 container image + # Only run this on repositories in the 'spinnaker' org, not on forks. + if: startsWith(github.repository, 'spinnaker/') + uses: docker/build-push-action@v4 + with: + context: . + file: Dockerfile.java11.slim + platforms: linux/amd64,linux/arm64 + push: true + tags: | + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.release_info.outputs.RELEASE_VERSION }}-java11-unvalidated" + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.release_info.outputs.RELEASE_VERSION }}-java11-unvalidated-slim" + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.release_info.outputs.RELEASE_VERSION }}-${{ steps.build_variables.outputs.VERSION }}-java11-unvalidated-slim" + - name: Build and publish ubuntu JRE 11 container image + # Only run this on repositories in the 'spinnaker' org, not on forks. + if: startsWith(github.repository, 'spinnaker/') + uses: docker/build-push-action@v4 + with: + context: . + file: Dockerfile.java11.ubuntu + platforms: linux/amd64,linux/arm64 + push: true + tags: | + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.release_info.outputs.RELEASE_VERSION }}-java11-unvalidated-ubuntu" + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.release_info.outputs.RELEASE_VERSION }}-${{ steps.build_variables.outputs.VERSION }}-java11-unvalidated-ubuntu" - name: Create release if: steps.release_info.outputs.SKIP_RELEASE == 'false' uses: softprops/action-gh-release@v1 diff --git a/Dockerfile.java11.slim b/Dockerfile.java11.slim new file mode 100644 index 0000000000..6a412a1210 --- /dev/null +++ b/Dockerfile.java11.slim @@ -0,0 +1,9 @@ +FROM alpine:3.16 +LABEL maintainer="sig-platform@spinnaker.io" +RUN apk --no-cache add --update bash openjdk11-jre +RUN addgroup -S -g 10111 spinnaker +RUN adduser -S -G spinnaker -u 10111 spinnaker +COPY gate-web/build/install/gate /opt/gate +RUN mkdir -p /opt/gate/plugins && chown -R spinnaker:nogroup /opt/gate/plugins +USER spinnaker +CMD ["/opt/gate/bin/gate"] diff --git a/Dockerfile.java11.ubuntu b/Dockerfile.java11.ubuntu new file mode 100644 index 0000000000..2ad183bd54 --- /dev/null +++ b/Dockerfile.java11.ubuntu @@ -0,0 +1,8 @@ +FROM ubuntu:bionic +LABEL maintainer="sig-platform@spinnaker.io" +RUN apt-get update && apt-get -y install openjdk-11-jre-headless wget +RUN adduser --system --uid 10111 --group spinnaker +COPY gate-web/build/install/gate /opt/gate +RUN mkdir -p /opt/gate/plugins && chown -R spinnaker:nogroup /opt/gate/plugins +USER spinnaker +CMD ["/opt/gate/bin/gate"] diff --git a/Dockerfile.slim b/Dockerfile.slim index 6a412a1210..e376b3c3a9 100644 --- a/Dockerfile.slim +++ b/Dockerfile.slim @@ -1,6 +1,6 @@ FROM alpine:3.16 LABEL maintainer="sig-platform@spinnaker.io" -RUN apk --no-cache add --update bash openjdk11-jre +RUN apk --no-cache add --update bash openjdk17-jre RUN addgroup -S -g 10111 spinnaker RUN adduser -S -G spinnaker -u 10111 spinnaker COPY gate-web/build/install/gate /opt/gate diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 2ad183bd54..2a34dc8545 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,6 +1,6 @@ FROM ubuntu:bionic LABEL maintainer="sig-platform@spinnaker.io" -RUN apt-get update && apt-get -y install openjdk-11-jre-headless wget +RUN apt-get update && apt-get -y install openjdk-17-jre-headless wget RUN adduser --system --uid 10111 --group spinnaker COPY gate-web/build/install/gate /opt/gate RUN mkdir -p /opt/gate/plugins && chown -R spinnaker:nogroup /opt/gate/plugins diff --git a/build.gradle b/build.gradle index 438ed08f7b..08f3ce068c 100644 --- a/build.gradle +++ b/build.gradle @@ -53,6 +53,17 @@ allprojects { } } + tasks.withType(JavaCompile).configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(11) + } + } + tasks.withType(Test).configureEach { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(17) + } + } + tasks.withType(JavaExec) { if (System.getProperty('DEBUG', 'false') == 'true') { jvmArgs '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8184' diff --git a/gate-plugins/src/test/kotlin/com/netflix/spinnaker/gate/plugins/deck/DeckPluginCacheTest.kt b/gate-plugins/src/test/kotlin/com/netflix/spinnaker/gate/plugins/deck/DeckPluginCacheTest.kt index e5065da533..236c34ff45 100644 --- a/gate-plugins/src/test/kotlin/com/netflix/spinnaker/gate/plugins/deck/DeckPluginCacheTest.kt +++ b/gate-plugins/src/test/kotlin/com/netflix/spinnaker/gate/plugins/deck/DeckPluginCacheTest.kt @@ -28,7 +28,6 @@ import dev.minutest.junit.JUnit5Minutests import dev.minutest.rootContext import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic import io.mockk.verify import java.nio.file.Files import java.nio.file.Paths @@ -37,6 +36,7 @@ import strikt.api.expectThat import strikt.assertions.hasSize import strikt.assertions.isEmpty import strikt.assertions.isEqualTo +import java.nio.file.Path import java.util.Optional class DeckPluginCacheTest : JUnit5Minutests { @@ -44,6 +44,10 @@ class DeckPluginCacheTest : JUnit5Minutests { fun tests() = rootContext { fixture { Fixture() } + after { + reset(pluginsDir) + } + context("caching") { test("latest plugin releases with deck artifacts are added to cache") { every { updateManager.downloadPluginRelease(any(), any()) } returns Paths.get("/dev/null") @@ -96,13 +100,14 @@ class DeckPluginCacheTest : JUnit5Minutests { } private inner class Fixture { + val pluginsDir = Files.createTempDirectory("plugins") val updateManager: SpinnakerUpdateManager = mockk(relaxed = true) val pluginBundleExtractor: PluginBundleExtractor = mockk(relaxed = true) val pluginStatusProvider: SpringPluginStatusProvider = mockk(relaxed = true) val pluginInfoReleaseProvider: PluginInfoReleaseProvider = mockk(relaxed = true) val registry: Registry = NoopRegistry() val springStrictPluginLoaderStatusProvider: SpringStrictPluginLoaderStatusProvider = mockk(relaxed = true) - val subject = DeckPluginCache(updateManager, pluginBundleExtractor, pluginStatusProvider, pluginInfoReleaseProvider, registry, springStrictPluginLoaderStatusProvider, Optional.empty()) + val subject = DeckPluginCache(updateManager, pluginBundleExtractor, pluginStatusProvider, pluginInfoReleaseProvider, registry, springStrictPluginLoaderStatusProvider, Optional.of(pluginsDir.toString())) init { val plugins = listOf( @@ -146,9 +151,11 @@ class DeckPluginCacheTest : JUnit5Minutests { } temp } - mockkStatic(Files::class) - every { Files.createDirectories(any()) } returns Paths.get("/dev/null") - every { Files.move(any(), any(), any()) } returns Paths.get("/dev/null") + } + + fun reset(path: Path) { + path.toFile().deleteRecursively() + path.toFile().mkdir() } } } diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy index b2519c39a4..a1283d522a 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy @@ -41,6 +41,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur import retrofit.RetrofitError import retrofit.RestAdapter; import retrofit.client.OkClient +import retrofit.converter.JacksonConverter import retrofit.mime.TypedInput import spock.lang.Shared import spock.lang.Specification @@ -100,6 +101,7 @@ class FunctionalSpec extends Specification { api = new RestAdapter.Builder() .setEndpoint("http://localhost:${localPort}") .setClient(new OkClient()) + .setConverter(new JacksonConverter()) .setLogLevel(RestAdapter.LogLevel.FULL) .build() .create(Api) diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/WebhookControllerSpec.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/WebhookControllerSpec.groovy index 13ec85ba46..20a3847977 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/WebhookControllerSpec.groovy +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/WebhookControllerSpec.groovy @@ -30,6 +30,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.web.util.NestedServletException import retrofit.RestAdapter import retrofit.client.OkClient +import retrofit.converter.JacksonConverter import spock.lang.Specification import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post @@ -54,6 +55,7 @@ class WebhooksControllerSpec extends Specification { EchoService echoService = new RestAdapter.Builder() .setEndpoint("http://localhost:${localPort}") .setClient(new OkClient()) + .setConverter(new JacksonConverter()) .build() .create(EchoService) From 88fafffdc79a2fec2bae3491b48274f211bce3bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 09:29:18 +0000 Subject: [PATCH 115/182] chore(deps): bump actions/checkout from 3 to 4 (#1716) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout 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/build.yml | 2 +- .github/workflows/pr.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5db104b580..120c9669bd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: if: startsWith(github.repository, 'spinnaker/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up QEMU diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b199e1c487..cc91e008f4 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -10,7 +10,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up QEMU diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cee1cb4c43..cd6f88aedd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up QEMU From 5496ff7c2851f7d300a2d01a8da2c3528cf67a77 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 09:44:13 +0000 Subject: [PATCH 116/182] chore(deps): bump docker/build-push-action from 4 to 5 (#1717) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v4...v5) --- updated-dependencies: - dependency-name: docker/build-push-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> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 8 ++++---- .github/workflows/pr.yml | 8 ++++---- .github/workflows/release.yml | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 120c9669bd..73fdd9cbc6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,7 +51,7 @@ jobs: - name: Build and publish slim container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: Dockerfile.slim @@ -65,7 +65,7 @@ jobs: - name: Build and publish ubuntu container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: Dockerfile.ubuntu @@ -77,7 +77,7 @@ jobs: - name: Build and publish slim JRE 11 container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: Dockerfile.java11.slim @@ -91,7 +91,7 @@ jobs: - name: Build and publish ubuntu JRE 11 container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: Dockerfile.java11.ubuntu diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index cc91e008f4..df661a93b7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -34,7 +34,7 @@ jobs: ORG_GRADLE_PROJECT_version: ${{ steps.build_variables.outputs.VERSION }} run: ./gradlew build ${{ steps.build_variables.outputs.REPO }}-web:installDist - name: Build slim container image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: Dockerfile.slim @@ -45,7 +45,7 @@ jobs: "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:latest-slim" "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-slim" - name: Build ubuntu container image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: Dockerfile.ubuntu @@ -54,7 +54,7 @@ jobs: "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:latest-ubuntu" "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-ubuntu" - name: Build slim JRE 11 container image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: Dockerfile.java11.slim @@ -65,7 +65,7 @@ jobs: "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:latest-java11-slim" "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-java11-slim" - name: Build ubuntu JRE 11 container image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: Dockerfile.java11.ubuntu diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd6f88aedd..399d384df7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -88,7 +88,7 @@ jobs: - name: Build and publish slim container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: Dockerfile.slim @@ -101,7 +101,7 @@ jobs: - name: Build and publish ubuntu container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: Dockerfile.ubuntu @@ -113,7 +113,7 @@ jobs: - name: Build and publish slim JRE 11 container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: Dockerfile.java11.slim @@ -126,7 +126,7 @@ jobs: - name: Build and publish ubuntu JRE 11 container image # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: Dockerfile.java11.ubuntu From a36020044076217e84ff0d751271b20ec4ac1868 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 09:54:57 +0000 Subject: [PATCH 117/182] chore(deps): bump docker/setup-buildx-action from 2 to 3 (#1718) Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/setup-buildx-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> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/pr.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 73fdd9cbc6..d22daef845 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - uses: actions/setup-java@v3 with: java-version: | diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index df661a93b7..ecececb73c 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -16,7 +16,7 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - uses: actions/setup-java@v3 with: java-version: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 399d384df7..3b1403426d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - uses: actions/setup-java@v3 with: java-version: | From 0ab9907eb17c2180575effa6c6273293da9df037 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:06:07 +0000 Subject: [PATCH 118/182] chore(deps): bump docker/login-action from 2 to 3 (#1719) Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/login-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> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d22daef845..42002a2dda 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,7 +42,7 @@ jobs: - name: Login to GAR # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/login-action@v2 + uses: docker/login-action@v3 # use service account flow defined at: https://github.com/docker/login-action#service-account-based-authentication-1 with: registry: us-docker.pkg.dev diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b1403426d..ac02689707 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,7 +79,7 @@ jobs: - name: Login to GAR # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: docker/login-action@v2 + uses: docker/login-action@v3 # use service account flow defined at: https://github.com/docker/login-action#service-account-based-authentication-1 with: registry: us-docker.pkg.dev From f1ac512af286e676f087f2b9cb2373f19501d6b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:18:38 +0000 Subject: [PATCH 119/182] chore(deps): bump docker/setup-qemu-action from 2 to 3 (#1715) Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/setup-qemu-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> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/pr.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 42002a2dda..976f4c47c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: with: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - uses: actions/setup-java@v3 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ecececb73c..05001bac17 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -14,7 +14,7 @@ jobs: with: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - uses: actions/setup-java@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac02689707..b6ef41accb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: with: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - uses: actions/setup-java@v3 From a18163c14da936986126f377e5f9326fc8e76d09 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 2 Oct 2023 21:11:07 -0400 Subject: [PATCH 120/182] chore(dependencies): Autobump korkVersion (#1720) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 580003ebf9..cc187fd5a8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.191.0 +korkVersion=7.192.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From 77d22831030c6e3f74436496def3d80392410fb0 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 4 Oct 2023 10:06:55 -0400 Subject: [PATCH 121/182] chore(dependencies): Autobump korkVersion (#1722) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cc187fd5a8..5691f0ba90 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.192.0 +korkVersion=7.193.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From 5345fe30ed63fdfea53d2d99c4a57844961bea65 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 17 Oct 2023 16:06:22 -0400 Subject: [PATCH 122/182] chore(dependencies): Autobump korkVersion (#1725) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5691f0ba90..8f0eea9696 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.193.0 +korkVersion=7.194.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From 4412a029be154a59a0d9170e2625cde31c0fc3fe Mon Sep 17 00:00:00 2001 From: Sandesh <30489233+j-sandy@users.noreply.github.com> Date: Wed, 18 Oct 2023 04:25:10 +0530 Subject: [PATCH 123/182] refactor(test): add runtime jupiter engine and remove vintage engine with upgrade of groovy 3 (#1724) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 08f3ce068c..c5b8b7c294 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ allprojects { testImplementation "org.springframework.boot:spring-boot-starter-test" testImplementation "org.hamcrest:hamcrest-core" testRuntimeOnly "cglib:cglib-nodep" - testRuntimeOnly "org.junit.vintage:junit-vintage-engine" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" testRuntimeOnly "org.objenesis:objenesis" } From 87a5dcb61a28d432beac7cd3bae020746cd151d6 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 20 Oct 2023 13:45:11 -0400 Subject: [PATCH 124/182] chore(dependencies): Autobump korkVersion (#1726) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8f0eea9696..995464504e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.194.0 +korkVersion=7.195.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From f9414f86479078f14cc3aaba28709359c6f68bce Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Sun, 29 Oct 2023 20:10:25 -0400 Subject: [PATCH 125/182] chore(dependencies): Autobump korkVersion (#1728) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 995464504e..30f2652e6c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.195.0 +korkVersion=7.196.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From e79565522e3cb2598623556db8cd3861e6c045a6 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 2 Nov 2023 15:56:35 -0400 Subject: [PATCH 126/182] chore(dependencies): Autobump korkVersion (#1729) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 30f2652e6c..674092135c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.196.0 +korkVersion=7.197.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From f748e0343b170b12beee7b2eeff9ec7802f37484 Mon Sep 17 00:00:00 2001 From: Sandesh <30489233+j-sandy@users.noreply.github.com> Date: Mon, 6 Nov 2023 22:11:58 +0530 Subject: [PATCH 127/182] refactor(web): fix groovy compilation failure due to missing type conversions during upgrade to springboot 2.5.15 (#1730) While upgrading springboot 2.5.15, encounter below error in gate-web module: ``` startup failed: /gate/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy: 231: [Static type checking] - Cannot call com.netflix.spinnaker.kork.web.selector.SelectableService#(java.util.List ) with arguments [java.util.List ] @ line 231, column 9. new SelectableService(selectors + defaultSelector), dynamicConfigService, contextProvider) ^ /gate/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy: 322: [Static type checking] - Cannot find matching method java.lang.Object#configure(com.fasterxml.jackson.databind.SerializationFeature, boolean). Please check if the declared type is correct and if the method exists. @ line 322, column 7. objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); ^ /gate/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy: 324: [Static type checking] - Cannot find matching method com.netflix.spinnaker.kork.client.ServiceClientProvider#getService(java.lang.Class , com.netflix.spinnaker.config.DefaultServiceEndpoint, T). Please check if the declared type is correct and if the method exists. @ line 324, column 5. serviceClientProvider.getService(type, new DefaultServiceEndpoint(serviceName, endpoint.url), objectMapper) ^ 3 errors > Task :gate-web:compileGroovy FAILED ``` To fix this, explicitly mentioned the type of object reference as `ServiceSelector defaultSelector` and typecasted the object `objectMapperBuilder.build() as ObjectMapper`. --- .../com/netflix/spinnaker/gate/config/GateConfig.groovy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy index 4cda1529ae..233667d5ce 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy @@ -23,6 +23,7 @@ import com.netflix.spectator.api.Registry import com.netflix.spinnaker.config.DefaultServiceEndpoint import com.netflix.spinnaker.config.OkHttp3ClientConfiguration import com.netflix.spinnaker.config.PluginsAutoConfiguration +import com.netflix.spinnaker.config.ServiceEndpoint import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator import com.netflix.spinnaker.fiat.shared.FiatService @@ -216,7 +217,7 @@ class GateConfig extends RedisHttpSessionConfiguration { // priority: 2 // origin: deck - def defaultSelector = new DefaultServiceSelector( + ServiceSelector defaultSelector = new DefaultServiceSelector( defaultClouddriverService, 1, null) @@ -317,7 +318,7 @@ class GateConfig extends RedisHttpSessionConfiguration { } private T buildService(String serviceName, Class type, Endpoint endpoint) { - ObjectMapper objectMapper = objectMapperBuilder.build() + ObjectMapper objectMapper = objectMapperBuilder.build() as ObjectMapper if(serviceName.equals("echo")) { objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); } From 9524bfa08abf812bd0f3f2256bdaeb19796c55bb Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 10 Nov 2023 14:54:05 -0500 Subject: [PATCH 128/182] chore(dependencies): Autobump korkVersion (#1733) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 674092135c..be215cb60a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.197.0 +korkVersion=7.198.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From b955dddb8dcddfd0373304c5a2e031f01c828ce4 Mon Sep 17 00:00:00 2001 From: David Byron <82477955+dbyron-sf@users.noreply.github.com> Date: Fri, 10 Nov 2023 16:06:39 -0500 Subject: [PATCH 129/182] refactor(web/test): update RedisTestConfig (#1734) to use the hostname and JedisPool from EmbeddedRedis --- .../com/netflix/spinnaker/gate/config/RedisTestConfig.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/config/RedisTestConfig.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/config/RedisTestConfig.groovy index 679c5b5b1f..0e039b8ff0 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/config/RedisTestConfig.groovy +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/config/RedisTestConfig.groovy @@ -26,12 +26,12 @@ class RedisTestConfig { @Primary @SpringSessionRedisConnectionFactory JedisConnectionFactory jedisConnectionFactory(EmbeddedRedis embeddedRedis) { - return new JedisConnectionFactory(new RedisStandaloneConfiguration("127.0.0.1", embeddedRedis.port)) + return new JedisConnectionFactory(new RedisStandaloneConfiguration(embeddedRedis.host, embeddedRedis.port)) } @Bean @Primary JedisPool jedis(EmbeddedRedis embeddedRedis) { - return new JedisPool(new URI("redis://127.0.0.1:$embeddedRedis.port"), 5000) + return embeddedRedis.getPool(); } } From 22d3c68157b5e51a4e7a396aa5fa871143b62997 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 13 Nov 2023 13:31:24 -0500 Subject: [PATCH 130/182] chore(dependencies): Autobump korkVersion (#1735) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index be215cb60a..8196730572 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.198.0 +korkVersion=7.199.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From 27b44731a327a1342d7ae630c187327287f72f7f Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 13 Nov 2023 12:53:08 -0600 Subject: [PATCH 131/182] chore(upgrades): OS Upgrades (#1723) --- Dockerfile.compile | 2 +- Dockerfile.slim | 2 +- Dockerfile.ubuntu | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile.compile b/Dockerfile.compile index d31a686faa..3160976a1a 100644 --- a/Dockerfile.compile +++ b/Dockerfile.compile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.18 RUN apk add --update \ openjdk11 \ && rm -rf /var/cache/apk diff --git a/Dockerfile.slim b/Dockerfile.slim index e376b3c3a9..35ccbf224c 100644 --- a/Dockerfile.slim +++ b/Dockerfile.slim @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.18 LABEL maintainer="sig-platform@spinnaker.io" RUN apk --no-cache add --update bash openjdk17-jre RUN addgroup -S -g 10111 spinnaker diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 2a34dc8545..9efd13c052 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,4 +1,4 @@ -FROM ubuntu:bionic +FROM ubuntu:jammy LABEL maintainer="sig-platform@spinnaker.io" RUN apt-get update && apt-get -y install openjdk-17-jre-headless wget RUN adduser --system --uid 10111 --group spinnaker From d240baa323a998b33b0979038b44820b4d7f73e5 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 14 Nov 2023 16:32:23 -0500 Subject: [PATCH 132/182] chore(dependencies): Autobump korkVersion (#1736) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8196730572..ef10ab1b17 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.199.0 +korkVersion=7.200.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From 5f73a67b3de899a8d519d225f657b5d81e63c44a Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Wed, 15 Nov 2023 12:44:06 -0600 Subject: [PATCH 133/182] fix(core): RetrofitError thrown on login (#1737) This fixes a regression from when PermissionService was converted from Groovy to Java. Ideally, we'd remove the extra parameter from FiatService directly, but that requires patching Fiat first, something that'll be easier to do when we migrate to the monorepo. --- .../netflix/spinnaker/gate/services/PermissionService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/PermissionService.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/PermissionService.java index 7481359b03..91c32e9f65 100644 --- a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/PermissionService.java +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/PermissionService.java @@ -77,7 +77,9 @@ public void login(final String userId) { try { AuthenticatedRequest.allowAnonymous( () -> { - getFiatServiceForLogin().loginUser(userId, null); + // TODO(jvz): FiatService::loginUser should have only one parameter as Retrofit no + // longer requires this body parameter + getFiatServiceForLogin().loginUser(userId, ""); permissionEvaluator.invalidatePermission(userId); return null; }); From 73df737f583afcc9cd04bdf43b870879847a9af2 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 20 Nov 2023 12:52:30 -0500 Subject: [PATCH 134/182] chore(dependencies): Autobump korkVersion (#1739) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ef10ab1b17..0153551aae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.200.0 +korkVersion=7.201.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From d457399c0ef4f328fa901d03604bcb7a9338591e Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 20 Nov 2023 16:38:07 -0500 Subject: [PATCH 135/182] chore(dependencies): Autobump korkVersion (#1740) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0153551aae..8ebef01a6f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.201.0 +korkVersion=7.202.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From 23806cc6cddf6213d107b19e5ebfad12ceb751c5 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 21 Nov 2023 14:56:21 -0500 Subject: [PATCH 136/182] chore(dependencies): Autobump korkVersion (#1741) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8ebef01a6f..01e73eb76a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.202.0 +korkVersion=7.203.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From 6e487bc95ad7058e4ac2b4924c9f3dea26cf8bed Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 21 Nov 2023 19:05:19 -0500 Subject: [PATCH 137/182] chore(dependencies): Autobump korkVersion (#1742) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 01e73eb76a..1b536b4b17 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.203.0 +korkVersion=7.204.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From 626e3987e139656f1c8bd81e7e3745bf6c5a4fc7 Mon Sep 17 00:00:00 2001 From: Sandesh <30489233+j-sandy@users.noreply.github.com> Date: Wed, 22 Nov 2023 06:18:08 +0530 Subject: [PATCH 138/182] chore(dependency): upgrade kotlin to 1.5.32 in gate along with kork (#1738) --- gradle.properties | 2 +- gradle/kotlin-test.gradle | 2 +- gradle/kotlin.gradle | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 1b536b4b17..eda4a67d0b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.204.0 -kotlinVersion=1.4.0 +kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 targetJava11=true diff --git a/gradle/kotlin-test.gradle b/gradle/kotlin-test.gradle index 5ffe2c3cd8..5ce62286c1 100644 --- a/gradle/kotlin-test.gradle +++ b/gradle/kotlin-test.gradle @@ -32,7 +32,7 @@ dependencies { compileTestKotlin { kotlinOptions { - languageVersion = "1.4" + languageVersion = "1.5" jvmTarget = "11" } } diff --git a/gradle/kotlin.gradle b/gradle/kotlin.gradle index cbe600fd35..d857ea3d61 100644 --- a/gradle/kotlin.gradle +++ b/gradle/kotlin.gradle @@ -19,14 +19,14 @@ apply plugin: "kotlin-spring" compileKotlin { kotlinOptions { - languageVersion = "1.4" + languageVersion = "1.5" jvmTarget = "11" } } compileTestKotlin { kotlinOptions { - languageVersion = "1.4" + languageVersion = "1.5" jvmTarget = "11" } } From e7530c7154cd9d834843a2e40d26d44a9d2a9c20 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 28 Nov 2023 12:22:27 -0500 Subject: [PATCH 139/182] chore(dependencies): Autobump korkVersion (#1743) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index eda4a67d0b..cee1a76006 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.204.0 +korkVersion=7.205.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.31.0 From b83c9c459db0855610acaf8dc1daa7df3790d5ef Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 1 Dec 2023 04:57:48 -0500 Subject: [PATCH 140/182] chore(dependencies): Autobump spinnakerGradleVersion (#1745) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cee1a76006..43dc14a7c3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.205.0 kotlinVersion=1.5.32 org.gradle.parallel=true -spinnakerGradleVersion=8.31.0 +spinnakerGradleVersion=8.32.1 targetJava11=true # To enable a composite reference to a project, set the From 30684655bd80f3a5bc20193745a9d9b0456afa66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 23:43:47 +0000 Subject: [PATCH 141/182] chore(deps): bump google-github-actions/auth from 1 to 2 (#1746) Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 1 to 2. - [Release notes](https://github.com/google-github-actions/auth/releases) - [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md) - [Commits](https://github.com/google-github-actions/auth/compare/v1...v2) --- updated-dependencies: - dependency-name: google-github-actions/auth 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/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b6ef41accb..b215be7aa7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: - name: Login to Google Cloud # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: 'google-github-actions/auth@v1' + uses: 'google-github-actions/auth@v2' # use service account flow defined at: https://github.com/google-github-actions/upload-cloud-storage#authenticating-via-service-account-key-json with: credentials_json: '${{ secrets.GAR_JSON_KEY }}' From 54bf338cd146ab6e623b9553fc0b418da8144a1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 23:52:24 +0000 Subject: [PATCH 142/182] chore(deps): bump actions/setup-java from 3 to 4 (#1747) Bumps [actions/setup-java](https://github.com/actions/setup-java) from 3 to 4. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-java 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/build.yml | 2 +- .github/workflows/pr.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 976f4c47c7..0fbb1ca850 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: java-version: | 17 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 05001bac17..9a606799a1 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -17,7 +17,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: java-version: | 17 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b215be7aa7..7c10cf5fc2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: java-version: | 17 From db11a65fbf2ba26daae6c08e25f541d295dd9691 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 5 Dec 2023 16:36:15 -0500 Subject: [PATCH 143/182] chore(dependencies): Autobump korkVersion (#1748) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 43dc14a7c3..a06c5e954c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.42.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.205.0 +korkVersion=7.206.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From e01e687df7e3bc755193d8bbf423adb15a044194 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 12 Dec 2023 04:59:18 -0500 Subject: [PATCH 144/182] chore(dependencies): Autobump fiatVersion (#1749) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a06c5e954c..17394ffd4b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.42.0 +fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.206.0 kotlinVersion=1.5.32 From ba6c51bdcebc24a0cff975bcbdf98c1f7751bd5d Mon Sep 17 00:00:00 2001 From: dzhengg <53837320+dzhengg@users.noreply.github.com> Date: Thu, 14 Dec 2023 11:25:56 -0800 Subject: [PATCH 145/182] fix(requests): Properly handle optional request parameters (#1750) * test(web): Define the behavior of the /type/{accountType} endpoint * fix(request): Properly declare optional request parameters Optional is not the right way to declare that a particular request parameter is not required. There was a similar issue in the corresponding clouddriver endpoint that was fixed by https://github.com/spinnaker/clouddriver/pull/6067 --------- Co-authored-by: Daniel Zheng --- .../controllers/CredentialsController.groovy | 6 +-- .../CredentialsControllerSpec.groovy | 41 ++++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/CredentialsController.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/CredentialsController.groovy index 1b7093b79b..a0944f3a84 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/CredentialsController.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/CredentialsController.groovy @@ -97,11 +97,11 @@ class CredentialsController { @ApiParam(value = 'Value of the "@type" key for accounts to search for.', example = 'kubernetes') @PathVariable String accountType, @ApiParam('Maximum number of entries to return in results. Used for pagination.') - @RequestParam OptionalInt limit, + @RequestParam(required = false) Integer limit, @ApiParam('Account name to start account definition listing from. Used for pagination.') - @RequestParam Optional startingAccountName + @RequestParam(required = false) String startingAccountName ) { - clouddriverService.getAccountDefinitionsByType(accountType, limit.isPresent() ? limit.getAsInt() : null, startingAccountName.orElse(null)) + clouddriverService.getAccountDefinitionsByType(accountType, limit, startingAccountName) } @PostMapping diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/CredentialsControllerSpec.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/CredentialsControllerSpec.groovy index a072882437..2205bde4ff 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/CredentialsControllerSpec.groovy +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/CredentialsControllerSpec.groovy @@ -58,7 +58,11 @@ class CredentialsControllerSpec extends Specification { contentNegotiationManagerFactoryBean.addMediaType("json", MediaType.APPLICATION_JSON) contentNegotiationManagerFactoryBean.favorPathExtension = false mockMvc = MockMvcBuilders - .standaloneSetup(new CredentialsController(accountLookupService: accountLookupService, allowedAccountsSupport: allowedAccountsSupport)) + .standaloneSetup(new CredentialsController( + accountLookupService: accountLookupService, + allowedAccountsSupport: allowedAccountsSupport, + clouddriverService: clouddriverService + )) .setContentNegotiationManager(contentNegotiationManagerFactoryBean.build()) .build() } @@ -78,4 +82,39 @@ class CredentialsControllerSpec extends Specification { "test" || "test" "test.com" || "test.com" } + + @Unroll + void "listing credentials by type should succeed when optional arguments are not provided"() { + when: + MockHttpServletResponse response = mockMvc.perform(get("/credentials/type/${accountType}") + .accept(MediaType.APPLICATION_JSON)).andReturn().response + + then: + response.status == 200 + 1 * clouddriverService.getAccountDefinitionsByType(accountType, _, _) + + where: + accountType | _ + "type1" | _ + "type2" | _ + } + + @Unroll + void "listing credentials by type should succeed when optional arguments are provided"() { + when: + MockHttpServletResponse response = mockMvc.perform( + get("/credentials/type/${accountType}") + .param("limit", "${limit}") + .param("startingAccountName", startingAccountName) + .accept(MediaType.APPLICATION_JSON)).andReturn().response + + then: + response.status == 200 + 1 * clouddriverService.getAccountDefinitionsByType(accountType, limit, startingAccountName) + + where: + accountType | limit | startingAccountName + "type1" | 2 | "account1" + "type2" | 500 | "account2" + } } From 082e4d14a2c00e52b5c0f259180c070194b334f4 Mon Sep 17 00:00:00 2001 From: Jalander Ramagiri <97948659+rjalander@users.noreply.github.com> Date: Fri, 22 Dec 2023 19:17:25 +0000 Subject: [PATCH 146/182] fix(cdevents-webhooks) : Adding support for Artifact Constraints and Parameters using CDEvents data (#1732) * Adding Ce-Data to the request * addressing review comments to remove headers usage * update test with valid CDEvents data --- .../gate/services/internal/EchoService.java | 1 + .../gate/controllers/WebhookController.groovy | 8 ++++--- .../gate/services/WebhookService.groovy | 4 ++-- .../controllers/WebhookControllerSpec.groovy | 21 +++++++++++++++++-- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/EchoService.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/EchoService.java index a5092926ec..b52b9c15a3 100644 --- a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/EchoService.java +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/EchoService.java @@ -24,6 +24,7 @@ public interface EchoService { ResponseEntity webhooks( @Path("source") String source, @Body CloudEvent cdevent, + @Header("Ce-Data") String ceDataJsonString, @Header("Ce-Id") String cdId, @Header("Ce-Specversion") String cdSpecVersion, @Header("Ce-Type") String cdType, diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/WebhookController.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/WebhookController.groovy index 6a14096c7e..e752cb4480 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/WebhookController.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/WebhookController.groovy @@ -29,6 +29,8 @@ import org.springframework.web.bind.annotation.RequestMethod import org.springframework.web.bind.annotation.RestController import io.cloudevents.CloudEvent +import java.nio.charset.StandardCharsets + @RestController @RequestMapping("/webhooks") class WebhookController { @@ -54,10 +56,10 @@ class WebhookController { @ApiOperation(value = "Endpoint for posting webhooks to Spinnaker's CDEvents webhook service") @RequestMapping(value = "/cdevents/{source}", method = RequestMethod.POST) ResponseEntity webhooks(@PathVariable String source, - @RequestBody CloudEvent cdevent, - @RequestHeader HttpHeaders headers) + @RequestBody CloudEvent cdEvent) { - webhookService.webhooks(source, cdevent, headers) + String ceDataJsonString = new String(cdEvent.getData().toBytes(), StandardCharsets.UTF_8); + webhookService.webhooks(source, cdEvent, ceDataJsonString) } @ApiOperation(value = "Retrieve a list of preconfigured webhooks in Orca") diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/WebhookService.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/WebhookService.groovy index ce653961d0..81acca5293 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/WebhookService.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/WebhookService.groovy @@ -56,9 +56,9 @@ class WebhookService { }) } - ResponseEntity webhooks(String source, CloudEvent cdevent, HttpHeaders headers) { + ResponseEntity webhooks(String source, CloudEvent cdEvent, String ceDataJsonString) { return AuthenticatedRequest.allowAnonymous( { - echoService.webhooks(source, cdevent, headers.get("Ce-Id").get(0), headers.get("Ce-Specversion").get(0), headers.get("Ce-Type").get(0), headers.get("Ce-Source").get(0)) + echoService.webhooks(source, cdEvent, ceDataJsonString, cdEvent.getId(), cdEvent.getSpecVersion().V1.toString(), cdEvent.getType(), cdEvent.getSource().toString()) }) } diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/WebhookControllerSpec.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/WebhookControllerSpec.groovy index 20a3847977..f346de917c 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/WebhookControllerSpec.groovy +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/controllers/WebhookControllerSpec.groovy @@ -118,13 +118,30 @@ class WebhooksControllerSpec extends Specification { headers.add("Ce-Type", "dev.cdevents.artifact.packaged") headers.add("Ce-Source", "spinnaker.test.io") headers.add("Content-Type", "application/cloudevents+json") - String payload = "{\"id\": \"1234\", \"subject\": \"event\"}" + String cdEventData = "{\n" + + " \"context\": {\n" + + " \"version\": \"0.1.2\",\n" + + " \"id\": \"c046b63b-a340-4847-bc39-ee408ad666d9\",\n" + + " \"source\": \"http://dev.cdevents\",\n" + + " \"type\": \"dev.cdevents.artifact.published.0.1.0\",\n" + + " \"timestamp\": \"2023-11-28T15:33:03Z\"\n" + + " },\n" + + " \"subject\": {\n" + + " \"id\": \"pkg:oci/myapp@sha256%3A0b31b1c02ff458ad9b7b\",\n" + + " \"source\": \"/dev/artifact/source\",\n" + + " \"type\": \"artifact\",\n" + + " \"content\": {\n" + + " }\n" + + " },\n" + + " \"customData\": {},\n" + + " \"customDataContentType\": \"application/json\"\n" + + "}" Map cdEvent = [ specversion: "1.0", type: "dev.cdevents.artifact.packaged", source: "/spinnaker.test.io", id: "12345", - data: payload + data: cdEventData ] when: From 3767a3269a7a5fc7a014c113b5aa2179bf30328b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 02:47:18 +0000 Subject: [PATCH 147/182] chore(deps): bump google-github-actions/upload-cloud-storage from 1 to 2 (#1751) Bumps [google-github-actions/upload-cloud-storage](https://github.com/google-github-actions/upload-cloud-storage) from 1 to 2. - [Release notes](https://github.com/google-github-actions/upload-cloud-storage/releases) - [Changelog](https://github.com/google-github-actions/upload-cloud-storage/blob/main/CHANGELOG.md) - [Commits](https://github.com/google-github-actions/upload-cloud-storage/compare/v1...v2) --- updated-dependencies: - dependency-name: google-github-actions/upload-cloud-storage 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/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c10cf5fc2..51daa62af4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,7 +71,7 @@ jobs: # https://console.cloud.google.com/storage/browser/halconfig # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') - uses: 'google-github-actions/upload-cloud-storage@v1' + uses: 'google-github-actions/upload-cloud-storage@v2' with: path: 'halconfig/' destination: 'halconfig/${{ steps.build_variables.outputs.REPO }}/${{ steps.release_info.outputs.RELEASE_VERSION }}' From 20878643df44f4e46b68c0da46a05cdcdad9bf5a Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 2 Jan 2024 13:59:04 -0500 Subject: [PATCH 148/182] chore(dependencies): Autobump korkVersion (#1752) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 17394ffd4b..2baf08665a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.206.0 +korkVersion=7.207.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From e52dc7d8425d85e7fd78cdbc7a863a2bd328db3f Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 4 Jan 2024 15:38:48 -0500 Subject: [PATCH 149/182] chore(dependencies): Autobump korkVersion (#1753) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2baf08665a..1a8b10cf2f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.207.0 +korkVersion=7.208.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From 428e14346dd8c40dde6cc86df01d5c792eef3db9 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 5 Jan 2024 18:42:56 -0500 Subject: [PATCH 150/182] chore(dependencies): Autobump korkVersion (#1754) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 1a8b10cf2f..d3aa26fa96 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.208.0 +korkVersion=7.209.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From 4aae98d69fdf22f141ef88da7f9b4225705702be Mon Sep 17 00:00:00 2001 From: ovidiupopa07 <105648914+ovidiupopa07@users.noreply.github.com> Date: Thu, 1 Feb 2024 17:46:45 +0200 Subject: [PATCH 151/182] fix: Fix git trigger issue caused by a misconfig of the object mapper when creating the echo retrofit service (#1756) * fix: Fix git trigger issue caused by a misconfig of the object mapper when creating the echo retrofit service Because of the misconfiguration echo was failing with GitEventHandler : Github Digest mismatch! Pipeline NOT triggered Closes https://github.com/spinnaker/spinnaker/issues/6886 * fix: Fix git trigger issue caused by a misconfig of the object mapper when creating the echo retrofit service Because of the misconfiguration echo was failing with GitEventHandler : Github Digest mismatch! Pipeline NOT triggered Closes https://github.com/spinnaker/spinnaker/issues/6886 * test: Add test verifying that the payload being sent to Echo is not ordered alphabetically Closes spinnaker/spinnaker#6886 * fix: Apply spotless checks to the test * fix: Fix failing test --- .../spinnaker/gate/config/GateConfig.groovy | 4 +- .../gate/service/EchoServiceTest.java | 107 ++++++++++++++++++ .../resources/application-echo.properties | 44 +++++++ 3 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 gate-web/src/test/java/com/netflix/spinnaker/gate/service/EchoServiceTest.java create mode 100644 gate-web/src/test/resources/application-echo.properties diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy index 233667d5ce..e7914a8a2a 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy @@ -23,7 +23,6 @@ import com.netflix.spectator.api.Registry import com.netflix.spinnaker.config.DefaultServiceEndpoint import com.netflix.spinnaker.config.OkHttp3ClientConfiguration import com.netflix.spinnaker.config.PluginsAutoConfiguration -import com.netflix.spinnaker.config.ServiceEndpoint import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator import com.netflix.spinnaker.fiat.shared.FiatService @@ -320,7 +319,8 @@ class GateConfig extends RedisHttpSessionConfiguration { private T buildService(String serviceName, Class type, Endpoint endpoint) { ObjectMapper objectMapper = objectMapperBuilder.build() as ObjectMapper if(serviceName.equals("echo")) { - objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, false) } serviceClientProvider.getService(type, new DefaultServiceEndpoint(serviceName, endpoint.url), objectMapper) } diff --git a/gate-web/src/test/java/com/netflix/spinnaker/gate/service/EchoServiceTest.java b/gate-web/src/test/java/com/netflix/spinnaker/gate/service/EchoServiceTest.java new file mode 100644 index 0000000000..c69fc2a994 --- /dev/null +++ b/gate-web/src/test/java/com/netflix/spinnaker/gate/service/EchoServiceTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2024 Harness, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.netflix.spinnaker.gate.Main; +import com.netflix.spinnaker.gate.services.internal.EchoService; +import com.squareup.okhttp.mockwebserver.Dispatcher; +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; +import com.squareup.okhttp.mockwebserver.RecordedRequest; +import groovy.util.logging.Slf4j; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +@ActiveProfiles("echo") +@Slf4j +@DirtiesContext +@SpringBootTest(classes = {Main.class}) +@TestPropertySource("/application-echo.properties") +class EchoServiceTest { + + @Autowired EchoService echoService; + + private static MockWebServer echoServer; + private static MockWebServer clouddriverServer; + private static MockWebServer front50Server; + + @BeforeAll + static void setUp() throws IOException { + clouddriverServer = new MockWebServer(); + clouddriverServer.start(7002); + + Dispatcher clouddriverDispatcher = + new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + return new MockResponse().setResponseCode(200); + } + }; + clouddriverServer.setDispatcher(clouddriverDispatcher); + + front50Server = new MockWebServer(); + front50Server.start(8081); + Dispatcher front50Dispatcher = + new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + return new MockResponse().setResponseCode(200); + } + }; + front50Server.setDispatcher(front50Dispatcher); + + echoServer = new MockWebServer(); + echoServer.start(8089); + } + + @AfterAll + static void tearDown() throws IOException { + echoServer.shutdown(); + clouddriverServer.shutdown(); + front50Server.shutdown(); + } + + @Test + void shouldNotOrderTheKeysWhenCallingEcho() throws InterruptedException { + + echoServer.enqueue(new MockResponse().setResponseCode(200)); + Map body = new HashMap<>(); + body.put("ref", "refs/heads/main"); + body.put("before", "ca7376e4b730f1f2878760abaeaed6c039fc5414"); + body.put("after", "c2420ce6e341ef0042f2e12591bdbe9eec29a032"); + body.put("id", 105648914); + + echoService.webhooks("git", "github", body); + RecordedRequest recordedRequest = echoServer.takeRequest(2, TimeUnit.SECONDS); + String requestBody = recordedRequest.getBody().readUtf8(); + assertThat(requestBody) + .isEqualTo( + "{\"ref\":\"refs/heads/main\",\"before\":\"ca7376e4b730f1f2878760abaeaed6c039fc5414\",\"after\":\"c2420ce6e341ef0042f2e12591bdbe9eec29a032\",\"id\":105648914}"); + } +} diff --git a/gate-web/src/test/resources/application-echo.properties b/gate-web/src/test/resources/application-echo.properties new file mode 100644 index 0000000000..f1ee4857ea --- /dev/null +++ b/gate-web/src/test/resources/application-echo.properties @@ -0,0 +1,44 @@ +# +# Copyright 2024 Harness, Inc. +# +# 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. +# + +spring.application.name=gate +services.clouddriver.baseUrl=http://localhost:7002 +services.deck.baseUrl=http://localhost:9000 + +services.echo.enabled=true +services.echo.baseUrl=http://localhost:8089 + +services.fiat.enabled=false + +services.fiat.baseUrl=http://localhost:8082 + +services.front50.baseUrl=http://localhost:8081 + +services.igor.enabled=false + +services.kayenta.enabled=false + + +services.orca.baseUrl=http://localhost:8083 + +services.mine.enabled=false + +services.swabbie.enabled=false +services.keel.enabled=false +services.keel.baseUrl=http://localhost:8087 + +retrofit.enabled=true +healthCheckableServices=igor From 1747a62206e3e8ac1562c7e983779a02acdc2959 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 2 Feb 2024 14:03:01 -0500 Subject: [PATCH 152/182] chore(dependencies): Autobump korkVersion (#1761) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d3aa26fa96..2297ba510a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.209.0 +korkVersion=7.210.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From b42b304dfc6797142c72215b3e81e88d814e2567 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Sun, 4 Feb 2024 23:14:07 -0500 Subject: [PATCH 153/182] chore(dependencies): Autobump korkVersion (#1762) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2297ba510a..bf17b81a68 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.210.0 +korkVersion=7.211.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From b681e07482038adb0c8022094005cf13f69ee02c Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 7 Feb 2024 14:32:26 -0500 Subject: [PATCH 154/182] chore(dependencies): Autobump korkVersion (#1764) * chore(dependencies): Autobump korkVersion * chore(dependency): upgrade bouncycastle dependency to 1.77 --------- Co-authored-by: root Co-authored-by: kirangodishala --- gate-x509/gate-x509.gradle | 4 ++-- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gate-x509/gate-x509.gradle b/gate-x509/gate-x509.gradle index 73ceb39e2a..c013ebb8eb 100644 --- a/gate-x509/gate-x509.gradle +++ b/gate-x509/gate-x509.gradle @@ -1,13 +1,13 @@ dependencies { implementation project(':gate-core') - implementation "org.bouncycastle:bcprov-jdk15on" + implementation "org.bouncycastle:bcprov-jdk18on" implementation "io.spinnaker.kork:kork-core" implementation "io.spinnaker.kork:kork-security" implementation "com.netflix.spectator:spectator-api" implementation "com.github.ben-manes.caffeine:caffeine" implementation "io.spinnaker.fiat:fiat-api:$fiatVersion" implementation "io.spinnaker.fiat:fiat-core:$fiatVersion" - testImplementation "org.bouncycastle:bcpkix-jdk15on" + testImplementation "org.bouncycastle:bcpkix-jdk18on" } sourceSets { diff --git a/gradle.properties b/gradle.properties index bf17b81a68..9a07a4a92c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.211.0 +korkVersion=7.212.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From b5ef03587831963725cf169ca6b6cf010cc6d7e2 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Mon, 19 Feb 2024 15:12:24 -0500 Subject: [PATCH 155/182] chore(dependencies): Autobump korkVersion (#1767) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9a07a4a92c..df6deb174a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.212.0 +korkVersion=7.213.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From c1dad242e570024c18d63585fbcd171309f0f6ba Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 20 Feb 2024 02:02:15 -0500 Subject: [PATCH 156/182] chore(dependencies): Autobump korkVersion (#1768) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index df6deb174a..9784c675a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.213.0 +korkVersion=7.214.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From f9629ccdb6ae43b716e656a135007b24b593c811 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 22 Feb 2024 12:19:42 -0500 Subject: [PATCH 157/182] chore(dependencies): Autobump korkVersion (#1769) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9784c675a2..53084f4909 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.214.0 +korkVersion=7.215.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From bc9212b597584013456146ff3423dff987a6c130 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 23 Feb 2024 12:47:18 -0500 Subject: [PATCH 158/182] chore(dependencies): Autobump korkVersion (#1770) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 53084f4909..5c99ad2286 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.215.0 +korkVersion=7.216.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From 3401bdc64acc0a4b79c65bbf9f37925989da0f5a Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 27 Feb 2024 00:28:31 -0500 Subject: [PATCH 159/182] chore(dependencies): Autobump korkVersion (#1771) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5c99ad2286..90e55a2ccc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.216.0 +korkVersion=7.217.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From b7a1f83719e430f00a052c5251984f04e7cb58a8 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Sun, 3 Mar 2024 12:43:17 -0500 Subject: [PATCH 160/182] chore(dependencies): Autobump korkVersion (#1772) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 90e55a2ccc..0bccf86fa7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.217.0 +korkVersion=7.218.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From 7019150e16ca0a8bd0971075672495bf77070287 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 12 Mar 2024 12:21:15 -0400 Subject: [PATCH 161/182] chore(dependencies): Autobump korkVersion (#1775) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0bccf86fa7..41486fce34 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.218.0 +korkVersion=7.219.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From 64afe5dd47c321b793bf9bfe136e9e4957d6b591 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 12 Mar 2024 15:15:15 -0400 Subject: [PATCH 162/182] chore(dependencies): Autobump korkVersion (#1776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(dependencies): Autobump korkVersion * fix(test/core): refactor constructor by passing context to HttpSecurity instance updated with spring boot 2.6.x While upgrading spring boot 2.6.15 and spring cloud 2021.0.8, encounter below errors during execution of tests under gate-core modules: ``` Cannot invoke "org.springframework.context.ApplicationContext.getBeanNamesForType(java.lang.Class)" because "context" is null java.lang.NullPointerException: Cannot invoke "org.springframework.context.ApplicationContext.getBeanNamesForType(java.lang.Class)" because "context" is null at org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer.(ExpressionUrlAuthorizationConfigurer.java:109) at org.springframework.security.config.annotation.web.builders.HttpSecurity.authorizeRequests(HttpSecurity.java:1265) at com.netflix.spinnaker.gate.config.AuthConfig.configure(AuthConfig.java:76) at com.netflix.spinnaker.gate.config.AuthConfigTest.test webhooks are unauthenticated by default(AuthConfigTest.groovy:51) Cannot invoke "org.springframework.context.ApplicationContext.getBeanNamesForType(java.lang.Class)" because "context" is null java.lang.NullPointerException: Cannot invoke "org.springframework.context.ApplicationContext.getBeanNamesForType(java.lang.Class)" because "context" is null at org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer.(ExpressionUrlAuthorizationConfigurer.java:109) at org.springframework.security.config.annotation.web.builders.HttpSecurity.authorizeRequests(HttpSecurity.java:1265) at com.netflix.spinnaker.gate.config.AuthConfig.configure(AuthConfig.java:76) at com.netflix.spinnaker.gate.config.AuthConfigTest.test webhooks can be configured to be authenticated(AuthConfigTest.groovy:86) ``` As per the reference given [here](https://stackoverflow.com/questions/71322261/nullpointerexception-at-org-springframework-security-config-annotation-web-confi), the implementation of spring-security-config got changed from 5.5.x to 5.6.x and it must require the ApplicationContext object. So, in order to fix this issue added a map containing ApplicationContext.class and its object to the constructor. * fix(tests): fix enableRedisKeyspaceNotificationsInitializer bean creation issue during upgrade to spring boot 2.6.x While upgrading spring boot 2.6.15 and spring cloud 2021.0.8, encounter below errors during execution of tests under gate-api-tck and gate-plugins-test modules: ``` Failed to load ApplicationContext java.lang.IllegalStateException: Failed to load ApplicationContext at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:98) at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:124) at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:190) at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:132) at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:248) at com.netflix.spinnaker.gate.plugins.test.GatePluginsTest$tests$1$1$invoke$$inlined$serviceFixture$1.invoke(PluginsTck.kt:78) at com.netflix.spinnaker.gate.plugins.test.GatePluginsTest$tests$1$1$invoke$$inlined$serviceFixture$1.invoke(PluginsTck.kt:76) ... Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'enableRedisKeyspaceNotificationsInitializer' defined in class path resource [com/netflix/spinnaker/gate/config/GateConfig.class]: Invocation of init method failed; nested exception is org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1804) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) at app//org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) at app//org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at app//org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at app//org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:955) at app//org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:920) at app//org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) at app//org.springframework.boot.SpringApplication.refresh(SpringApplication.java:745) at app//org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:423) at app//org.springframework.boot.SpringApplication.run(SpringApplication.java:307) at app//org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:148) at app//org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:141) at app//org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:90) ... 171 more Caused by: org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool at app//org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:292) at app//org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:514) at app//org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration$EnableRedisKeyspaceNotificationsInitializer.afterPropertiesSet(RedisHttpSessionConfiguration.java:331) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800) ... 186 more Caused by: redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool at app//redis.clients.jedis.util.Pool.getResource(Pool.java:84) at app//redis.clients.jedis.JedisPool.getResource(JedisPool.java:370) at app//redis.clients.jedis.JedisPool.getResource(JedisPool.java:15) at app//org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:283) ... 190 more Caused by: redis.clients.jedis.exceptions.JedisConnectionException: Failed to create socket. at app//redis.clients.jedis.DefaultJedisSocketFactory.createSocket(DefaultJedisSocketFactory.java:110) at app//redis.clients.jedis.Connection.connect(Connection.java:226) at app//redis.clients.jedis.BinaryClient.connect(BinaryClient.java:140) at app//redis.clients.jedis.BinaryJedis.connect(BinaryJedis.java:310) at app//redis.clients.jedis.BinaryJedis.initializeFromClientConfig(BinaryJedis.java:88) at app//redis.clients.jedis.BinaryJedis.(BinaryJedis.java:293) at app//redis.clients.jedis.Jedis.(Jedis.java:169) at app//redis.clients.jedis.JedisFactory.makeObject(JedisFactory.java:177) at app//org.apache.commons.pool2.impl.GenericObjectPool.create(GenericObjectPool.java:571) at app//org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:298) at app//org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:223) at app//redis.clients.jedis.util.Pool.getResource(Pool.java:75) ... 193 more Caused by: java.net.ConnectException: Connection refused at java.base/sun.nio.ch.Net.pollConnect(Native Method) at java.base/sun.nio.ch.Net.pollConnectNow(Net.java:672) at java.base/sun.nio.ch.NioSocketImpl.timedFinishConnect(NioSocketImpl.java:547) at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:602) at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327) at java.base/java.net.Socket.connect(Socket.java:633) at redis.clients.jedis.DefaultJedisSocketFactory.createSocket(DefaultJedisSocketFactory.java:80) ... 204 more ``` In order to fix this issue add `@TestConfiguration` to create the required beans for initialization of `enableRedisKeyspaceNotificationsInitializer` bean. * refactor(web/test): remove circular reference from GateConfig class identified during upgrade to spring boot 2.6.x While upgrading spring boot 2.6.15 and spring cloud 2021.0.8, encounter below errors in gate-web module during gate-web:test task execution and 17 tests fail with similar error: ``` Failed to load ApplicationContext java.lang.IllegalStateException: Failed to load ApplicationContext at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:98) at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:124) at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:190) at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:132) at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:248) at org.spockframework.spring.SpringTestContextManager.prepareTestInstance(SpringTestContextManager.java:56) at org.spockframework.spring.SpringInterceptor.interceptInitializerMethod(SpringInterceptor.java:46) at org.spockframework.runtime.extension.AbstractMethodInterceptor.intercept(AbstractMethodInterceptor.java:24) at org.spockframework.runtime.extension.MethodInvocation.proceed(MethodInvocation.java:101) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$2(NodeTestTask.java:123) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:123) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:90) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) at org.spockframework.runtime.model.MethodInfo.invoke(MethodInfo.java:148) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75) at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:62) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24) at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94) at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193) at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129) at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100) at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60) at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65) at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'gateConfig': Unsatisfied dependency expressed through method 'setConfigureRedisAction' parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'gateConfig': Requested bean is currently in creation: Is there an unresolvable circular reference? at app//org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:768) at app//org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:720) at app//org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) at app//org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) at app//org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) at app//org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at app//org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at app//org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:955) at app//org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:920) at app//org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) at app//org.springframework.boot.SpringApplication.refresh(SpringApplication.java:745) at app//org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:423) at app//org.springframework.boot.SpringApplication.run(SpringApplication.java:307) at app//org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:148) at app//org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:141) at app//org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:90) ... 61 more Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'gateConfig': Requested bean is currently in creation: Is there an unresolvable circular reference? at app//org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:355) at app//org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:227) at app//org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at app//org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) at app//org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:410) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1352) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1195) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) at app//org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) at app//org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at app//org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at app//org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) at app//org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) at app//org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:760) ... 80 more ``` and ``` Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2024-01-19 16:03:05.279 ERROR 705260 --- [ Test worker] o.s.b.d.LoggingFailureAnalysisReporter : [] *************************** APPLICATION FAILED TO START *************************** Description: The dependencies of some of the beans in the application context form a cycle: ┌──->──┐ | gateConfig └──<-──┘ Action: Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true. 2024-01-19 16:03:05.283 ERROR 705260 --- [ Test worker] o.s.test.context.TestContextManager : [] Caught exception while allowing TestExecutionListener [org.springframework.test.context.web.ServletTestExecutionListener@e044b4a] to prepare test instance [com.netflix.spinnaker.gate.config.GateCorsAllowedOriginConfigSpec@7f8e4389] java.lang.IllegalStateException: Failed to load ApplicationContext ... ``` The root cause of circular reference identification is new feature "circular reference prohibition" introduced in [Spring Boot 2.6.x](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.6-Release-Notes#circular-references-prohibited-by-default). To fix this issue refactoring the GateConfig class with explicit setConfigureRedisAction() setter. --------- Co-authored-by: root Co-authored-by: j-sandy <30489233+j-sandy@users.noreply.github.com> --- gate-api-tck/gate-api-tck.gradle | 2 ++ .../spinnaker/gate/api/test/GateFixture.kt | 31 +++++++++++++++++++ .../gate/config/AuthConfigTest.groovy | 16 ++++++++-- gate-plugins-test/gate-plugins-test.gradle | 2 ++ .../gate/plugins/test/GatePluginsFixture.kt | 27 +++++++++++++++- .../spinnaker/gate/config/GateConfig.groovy | 5 +++ gradle.properties | 2 +- 7 files changed, 81 insertions(+), 4 deletions(-) diff --git a/gate-api-tck/gate-api-tck.gradle b/gate-api-tck/gate-api-tck.gradle index 35e8d220a0..7dd6a08c93 100644 --- a/gate-api-tck/gate-api-tck.gradle +++ b/gate-api-tck/gate-api-tck.gradle @@ -3,6 +3,8 @@ apply from: "${project.rootDir}/gradle/kotlin-test.gradle" dependencies { implementation(project(":gate-web")) + implementation("io.spinnaker.kork:kork-jedis-test") + implementation("org.springframework.session:spring-session-data-redis") api("org.springframework.boot:spring-boot-starter-test") api("dev.minutest:minutest") diff --git a/gate-api-tck/src/main/kotlin/com/netflix/spinnaker/gate/api/test/GateFixture.kt b/gate-api-tck/src/main/kotlin/com/netflix/spinnaker/gate/api/test/GateFixture.kt index bdbeff87d7..6a2b81eee9 100644 --- a/gate-api-tck/src/main/kotlin/com/netflix/spinnaker/gate/api/test/GateFixture.kt +++ b/gate-api-tck/src/main/kotlin/com/netflix/spinnaker/gate/api/test/GateFixture.kt @@ -20,10 +20,20 @@ import com.netflix.spinnaker.gate.Main import dev.minutest.TestContextBuilder import dev.minutest.TestDescriptor import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration import org.springframework.test.context.TestContextManager import org.springframework.test.context.TestPropertySource +import org.springframework.context.annotation.Bean +import com.netflix.spinnaker.kork.jedis.EmbeddedRedis +import org.springframework.context.annotation.Primary +import org.springframework.data.redis.connection.RedisStandaloneConfiguration +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory +import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory +import org.springframework.test.context.ContextConfiguration +import redis.clients.jedis.JedisPool @SpringBootTest(classes = [Main::class]) +@ContextConfiguration(classes = [GateFixtureConfiguration::class]) @TestPropertySource(properties = ["spring.config.location=classpath:gate-test-app.yml"]) abstract class GateFixture @@ -39,3 +49,24 @@ inline fun TestContextBuilder.gateFixture( } } } + +@TestConfiguration +internal open class GateFixtureConfiguration { + @Bean(destroyMethod = "destroy") + fun embeddedRedis(): EmbeddedRedis { + return EmbeddedRedis.embed().also { redis -> redis.jedis.connect() }.also { redis -> redis.jedis.ping() } + } + + @Bean + @Primary + @SpringSessionRedisConnectionFactory + fun jedisConnectionFactory(embeddedRedis: EmbeddedRedis): JedisConnectionFactory { + return JedisConnectionFactory(RedisStandaloneConfiguration(embeddedRedis.host, embeddedRedis.port)) + } + + @Bean + @Primary + fun jedis(embeddedRedis: EmbeddedRedis): JedisPool { + return embeddedRedis.getPool(); + } +} diff --git a/gate-core/src/test/groovy/com/netflix/spinnaker/gate/config/AuthConfigTest.groovy b/gate-core/src/test/groovy/com/netflix/spinnaker/gate/config/AuthConfigTest.groovy index 730a52e93b..bd230754d9 100644 --- a/gate-core/src/test/groovy/com/netflix/spinnaker/gate/config/AuthConfigTest.groovy +++ b/gate-core/src/test/groovy/com/netflix/spinnaker/gate/config/AuthConfigTest.groovy @@ -18,15 +18,20 @@ package com.netflix.spinnaker.gate.config import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator import com.netflix.spinnaker.fiat.shared.FiatStatus +import org.springframework.context.ApplicationContext import org.springframework.security.config.annotation.ObjectPostProcessor import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.web.util.matcher.AnyRequestMatcher +import org.springframework.context.support.GenericApplicationContext import spock.lang.Specification import java.util.stream.Collectors class AuthConfigTest extends Specification { + + private GenericApplicationContext context = new GenericApplicationContext() + @SuppressWarnings("GroovyAccessibility") def "test webhooks are unauthenticated by default"() { given: @@ -44,7 +49,7 @@ class AuthConfigTest extends Specification { def httpSecurity = new HttpSecurity( Mock(ObjectPostProcessor), Mock(AuthenticationManagerBuilder), - [:] + getSharedObjects() ) when: @@ -79,7 +84,7 @@ class AuthConfigTest extends Specification { def httpSecurity = new HttpSecurity( Mock(ObjectPostProcessor), Mock(AuthenticationManagerBuilder), - [:] + getSharedObjects() ) when: @@ -95,4 +100,11 @@ class AuthConfigTest extends Specification { .collect(Collectors.toList()) filtered.size() == 1 } + + private HashMap, Object> getSharedObjects(){ + HashMap map = new HashMap, Object>() + context.refresh() + map.put(ApplicationContext.class, context) + return map; + } } diff --git a/gate-plugins-test/gate-plugins-test.gradle b/gate-plugins-test/gate-plugins-test.gradle index 2c3bf31579..3c4f990212 100644 --- a/gate-plugins-test/gate-plugins-test.gradle +++ b/gate-plugins-test/gate-plugins-test.gradle @@ -9,6 +9,8 @@ dependencies { testImplementation("io.spinnaker.kork:kork-plugins") testImplementation("io.spinnaker.kork:kork-plugins-tck") + testImplementation("io.spinnaker.kork:kork-jedis-test") + testImplementation("org.springframework.session:spring-session-data-redis") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") } diff --git a/gate-plugins-test/src/test/kotlin/com/netflix/spinnaker/gate/plugins/test/GatePluginsFixture.kt b/gate-plugins-test/src/test/kotlin/com/netflix/spinnaker/gate/plugins/test/GatePluginsFixture.kt index af93403da2..a709c94aca 100644 --- a/gate-plugins-test/src/test/kotlin/com/netflix/spinnaker/gate/plugins/test/GatePluginsFixture.kt +++ b/gate-plugins-test/src/test/kotlin/com/netflix/spinnaker/gate/plugins/test/GatePluginsFixture.kt @@ -30,6 +30,13 @@ import org.springframework.boot.test.context.TestConfiguration import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.TestPropertySource import org.springframework.test.web.servlet.MockMvc +import org.springframework.context.annotation.Bean +import com.netflix.spinnaker.kork.jedis.EmbeddedRedis +import org.springframework.context.annotation.Primary +import org.springframework.data.redis.connection.RedisStandaloneConfiguration +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory +import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory +import redis.clients.jedis.JedisPool class GatePluginsFixture : PluginsTckFixture, GateTestService() { @@ -75,4 +82,22 @@ class GatePluginsFixture : PluginsTckFixture, GateTestService() { abstract class GateTestService @TestConfiguration -internal open class PluginTestConfiguration +internal open class PluginTestConfiguration { + @Bean(destroyMethod = "destroy") + fun embeddedRedis(): EmbeddedRedis { + return EmbeddedRedis.embed().also { redis -> redis.jedis.connect() }.also { redis -> redis.jedis.ping() } + } + + @Bean + @Primary + @SpringSessionRedisConnectionFactory + fun jedisConnectionFactory(embeddedRedis: EmbeddedRedis): JedisConnectionFactory { + return JedisConnectionFactory(RedisStandaloneConfiguration(embeddedRedis.host, embeddedRedis.port)) + } + + @Bean + @Primary + fun jedis(embeddedRedis: EmbeddedRedis): JedisPool { + return embeddedRedis.getPool(); + } +} diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy index e7914a8a2a..533238c39d 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy @@ -79,6 +79,7 @@ import static retrofit.Endpoints.newFixedEndpoint class GateConfig extends RedisHttpSessionConfiguration { private ServiceClientProvider serviceClientProvider + private ConfigureRedisAction configureRedisAction @Value('${server.session.timeout-in-seconds:3600}') void setSessionTimeout(int maxInactiveIntervalInSeconds) { @@ -90,6 +91,10 @@ class GateConfig extends RedisHttpSessionConfiguration { this.serviceClientProvider = serviceClientProvider } + void setConfigureRedisAction(ConfigureRedisAction configureRedisAction){ + this.configureRedisAction = configureRedisAction; + } + @Autowired GateConfig(@Value('${server.session.timeout-in-seconds:3600}') int maxInactiveIntervalInSeconds) { super.setMaxInactiveIntervalInSeconds(maxInactiveIntervalInSeconds) diff --git a/gradle.properties b/gradle.properties index 41486fce34..4710820e42 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.43.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.219.0 +korkVersion=7.220.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From c7c459242cc18e4d487c6afe803b923ba81a7ca9 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 12 Mar 2024 16:07:25 -0400 Subject: [PATCH 163/182] chore(dependencies): Autobump fiatVersion (#1777) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4710820e42..f29ed66b05 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.43.0 +fiatVersion=1.44.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.220.0 kotlinVersion=1.5.32 From d3d55ad5469c4f2f9c013e7bcc8d92c1ed6df957 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Thu, 21 Mar 2024 12:45:35 -0400 Subject: [PATCH 164/182] chore(dependencies): Autobump korkVersion (#1778) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f29ed66b05..2701719768 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.44.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.220.0 +korkVersion=7.221.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From d59518c05e5b612e0f6370448c1ad92b24edbdd1 Mon Sep 17 00:00:00 2001 From: Nemesis Osorio Date: Tue, 26 Mar 2024 17:44:29 -0600 Subject: [PATCH 165/182] fix(redis): circular dependencies (#1780) --- .../spinnaker/gate/config/GateConfig.groovy | 8 ----- .../gate/config/RedisConfigSecure.java | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RedisConfigSecure.java diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy index 533238c39d..52ec734673 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy @@ -28,7 +28,6 @@ import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator import com.netflix.spinnaker.fiat.shared.FiatService import com.netflix.spinnaker.fiat.shared.FiatStatus import com.netflix.spinnaker.filters.AuthenticatedRequestFilter -import com.netflix.spinnaker.gate.config.PostConnectionConfiguringJedisConnectionFactory.ConnectionPostProcessor import com.netflix.spinnaker.gate.converters.JsonHttpMessageConverter import com.netflix.spinnaker.gate.converters.YamlHttpMessageConverter import com.netflix.spinnaker.gate.filters.RequestLoggingFilter @@ -127,13 +126,6 @@ class GateConfig extends RedisHttpSessionConfiguration { return ConfigureRedisAction.NO_OP } - @Bean - @ConnectionPostProcessor - @ConditionalOnProperty("redis.configuration.secure") - ConfigureRedisAction connectionPostProcessorConfigureRedisAction() { - return ConfigureRedisAction.NO_OP - } - @Bean ExecutorService executorService() { Executors.newCachedThreadPool() diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RedisConfigSecure.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RedisConfigSecure.java new file mode 100644 index 0000000000..7aad82db1e --- /dev/null +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RedisConfigSecure.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 OpsMx, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.config; + +import com.netflix.spinnaker.gate.config.PostConnectionConfiguringJedisConnectionFactory.ConnectionPostProcessor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.data.redis.config.ConfigureRedisAction; + +@Configuration +@ConditionalOnProperty("redis.configuration.secure") +public class RedisConfigSecure { + + @Bean + @ConnectionPostProcessor + public ConfigureRedisAction connectionPostProcessorConfigureRedisAction() { + return ConfigureRedisAction.NO_OP; + } +} From 7b3edc436fd1f8db8c9212a1433461d2d4fc9af3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 23:45:29 +0000 Subject: [PATCH 166/182] chore(deps): bump softprops/action-gh-release from 1 to 2 (#1782) Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2) --- updated-dependencies: - dependency-name: softprops/action-gh-release 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/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51daa62af4..e565d9bb88 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -137,7 +137,7 @@ jobs: "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.release_info.outputs.RELEASE_VERSION }}-${{ steps.build_variables.outputs.VERSION }}-java11-unvalidated-ubuntu" - name: Create release if: steps.release_info.outputs.SKIP_RELEASE == 'false' - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: body: | ${{ steps.release_info.outputs.CHANGELOG }} From a438159b4afd942667cfb868af7e70cb8ae6feb0 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 2 Apr 2024 09:07:45 -0400 Subject: [PATCH 167/182] chore(dependencies): Autobump korkVersion (#1784) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2701719768..35b8f7e90e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.44.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.221.0 +korkVersion=7.222.0 kotlinVersion=1.5.32 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From f108416eae73fd37025e69dab4f498e6297c0233 Mon Sep 17 00:00:00 2001 From: Sandesh <30489233+j-sandy@users.noreply.github.com> Date: Tue, 2 Apr 2024 19:01:34 +0530 Subject: [PATCH 168/182] chore(dependency): upgrade kotlin to 1.6.21 in gate along with kork (#1783) --- gradle.properties | 2 +- gradle/kotlin-test.gradle | 2 +- gradle/kotlin.gradle | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 35b8f7e90e..990cf86fdd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ enablePublishing=false fiatVersion=1.44.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.222.0 -kotlinVersion=1.5.32 +kotlinVersion=1.6.21 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 targetJava11=true diff --git a/gradle/kotlin-test.gradle b/gradle/kotlin-test.gradle index 5ce62286c1..dbcbd0744c 100644 --- a/gradle/kotlin-test.gradle +++ b/gradle/kotlin-test.gradle @@ -32,7 +32,7 @@ dependencies { compileTestKotlin { kotlinOptions { - languageVersion = "1.5" + languageVersion = "1.6" jvmTarget = "11" } } diff --git a/gradle/kotlin.gradle b/gradle/kotlin.gradle index d857ea3d61..fb3bcd0a57 100644 --- a/gradle/kotlin.gradle +++ b/gradle/kotlin.gradle @@ -19,14 +19,14 @@ apply plugin: "kotlin-spring" compileKotlin { kotlinOptions { - languageVersion = "1.5" + languageVersion = "1.6" jvmTarget = "11" } } compileTestKotlin { kotlinOptions { - languageVersion = "1.5" + languageVersion = "1.6" jvmTarget = "11" } } From 3383e775e65a58f2499273fbf802ff5ae6efc081 Mon Sep 17 00:00:00 2001 From: Nemesis Osorio Date: Tue, 2 Apr 2024 10:26:17 -0600 Subject: [PATCH 169/182] refactor(redis): moving config to a new class (#1781) --- .../spinnaker/gate/config/GateConfig.groovy | 41 +------------- ...tionConfiguringJedisConnectionFactory.java | 0 .../gate/config/RedisActionConfig.java} | 19 +++++-- .../spinnaker/gate/config/RedisConfig.java | 37 +++++++++++++ .../gate/config/RedisConfigTest.java | 53 +++++++++++++++++++ 5 files changed, 106 insertions(+), 44 deletions(-) rename gate-web/src/main/{groovy => java}/com/netflix/spinnaker/gate/config/PostConnectionConfiguringJedisConnectionFactory.java (100%) rename gate-web/src/main/{groovy/com/netflix/spinnaker/gate/config/RedisConfigSecure.java => java/com/netflix/spinnaker/gate/config/RedisActionConfig.java} (64%) create mode 100644 gate-web/src/main/java/com/netflix/spinnaker/gate/config/RedisConfig.java create mode 100644 gate-web/src/test/java/com/netflix/spinnaker/gate/config/RedisConfigTest.java diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy index 52ec734673..e3a3bf41ba 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy @@ -59,11 +59,8 @@ import org.springframework.context.annotation.Primary import org.springframework.core.Ordered import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder -import org.springframework.session.data.redis.config.ConfigureRedisAction -import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration import org.springframework.util.CollectionUtils import org.springframework.web.client.RestTemplate -import redis.clients.jedis.JedisPool import retrofit.Endpoint import java.util.concurrent.ExecutorService @@ -75,57 +72,21 @@ import static retrofit.Endpoints.newFixedEndpoint @Configuration @Slf4j @Import([PluginsAutoConfiguration, DeckPluginConfiguration, PluginWebConfiguration]) -class GateConfig extends RedisHttpSessionConfiguration { +class GateConfig { private ServiceClientProvider serviceClientProvider - private ConfigureRedisAction configureRedisAction - - @Value('${server.session.timeout-in-seconds:3600}') - void setSessionTimeout(int maxInactiveIntervalInSeconds) { - super.setMaxInactiveIntervalInSeconds(maxInactiveIntervalInSeconds) - } @Autowired void setServiceClientProvider(ServiceClientProvider serviceClientProvider) { this.serviceClientProvider = serviceClientProvider } - void setConfigureRedisAction(ConfigureRedisAction configureRedisAction){ - this.configureRedisAction = configureRedisAction; - } - - @Autowired - GateConfig(@Value('${server.session.timeout-in-seconds:3600}') int maxInactiveIntervalInSeconds) { - super.setMaxInactiveIntervalInSeconds(maxInactiveIntervalInSeconds) - } - - /** - * This pool is used for the rate limit storage, as opposed to the JedisConnectionFactory, which - * is a separate pool used for Spring Boot's session management. - */ - @Bean - JedisPool jedis(@Value('${redis.connection:redis://localhost:6379}') String connection, - @Value('${redis.timeout:2000}') int timeout) { - return new JedisPool(new URI(connection), timeout) - } - @Bean @ConditionalOnMissingBean(RestTemplate) RestTemplate restTemplate() { new RestTemplate() } - /** - * Always disable the ConfigureRedisAction that Spring Boot uses internally. Instead we use one - * qualified with @ConnectionPostProcessor. See - * {@link PostConnectionConfiguringJedisConnectionFactory}. - * */ - @Bean - @Primary - ConfigureRedisAction springBootConfigureRedisAction() { - return ConfigureRedisAction.NO_OP - } - @Bean ExecutorService executorService() { Executors.newCachedThreadPool() diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/PostConnectionConfiguringJedisConnectionFactory.java b/gate-web/src/main/java/com/netflix/spinnaker/gate/config/PostConnectionConfiguringJedisConnectionFactory.java similarity index 100% rename from gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/PostConnectionConfiguringJedisConnectionFactory.java rename to gate-web/src/main/java/com/netflix/spinnaker/gate/config/PostConnectionConfiguringJedisConnectionFactory.java diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RedisConfigSecure.java b/gate-web/src/main/java/com/netflix/spinnaker/gate/config/RedisActionConfig.java similarity index 64% rename from gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RedisConfigSecure.java rename to gate-web/src/main/java/com/netflix/spinnaker/gate/config/RedisActionConfig.java index 7aad82db1e..2ff9781f54 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/RedisConfigSecure.java +++ b/gate-web/src/main/java/com/netflix/spinnaker/gate/config/RedisActionConfig.java @@ -16,18 +16,29 @@ package com.netflix.spinnaker.gate.config; -import com.netflix.spinnaker.gate.config.PostConnectionConfiguringJedisConnectionFactory.ConnectionPostProcessor; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.session.data.redis.config.ConfigureRedisAction; @Configuration -@ConditionalOnProperty("redis.configuration.secure") -public class RedisConfigSecure { +public class RedisActionConfig { + /** + * Always disable the ConfigureRedisAction that Spring Boot uses internally. Instead we use one + * qualified with @ConnectionPostProcessor. See {@link + * PostConnectionConfiguringJedisConnectionFactory}. + */ @Bean - @ConnectionPostProcessor + @Primary + public ConfigureRedisAction springBootConfigureRedisAction() { + return ConfigureRedisAction.NO_OP; + } + + @Bean + @ConditionalOnProperty("redis.configuration.secure") + @PostConnectionConfiguringJedisConnectionFactory.ConnectionPostProcessor public ConfigureRedisAction connectionPostProcessorConfigureRedisAction() { return ConfigureRedisAction.NO_OP; } diff --git a/gate-web/src/main/java/com/netflix/spinnaker/gate/config/RedisConfig.java b/gate-web/src/main/java/com/netflix/spinnaker/gate/config/RedisConfig.java new file mode 100644 index 0000000000..f2a703f55b --- /dev/null +++ b/gate-web/src/main/java/com/netflix/spinnaker/gate/config/RedisConfig.java @@ -0,0 +1,37 @@ +package com.netflix.spinnaker.gate.config; + +import java.net.URI; +import java.net.URISyntaxException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration; +import redis.clients.jedis.JedisPool; + +@Configuration +public class RedisConfig extends RedisHttpSessionConfiguration { + + @Value("${server.session.timeout-in-seconds:3600}") + public void setSessionTimeout(int maxInactiveIntervalInSeconds) { + super.setMaxInactiveIntervalInSeconds(maxInactiveIntervalInSeconds); + } + + @Autowired + public RedisConfig( + @Value("${server.session.timeout-in-seconds:3600}") int maxInactiveIntervalInSeconds) { + super.setMaxInactiveIntervalInSeconds(maxInactiveIntervalInSeconds); + } + + /** + * This pool is used for the rate limit storage, as opposed to the JedisConnectionFactory, which + * is a separate pool used for Spring Boot's session management. + */ + @Bean + public JedisPool jedis( + @Value("${redis.connection:redis://localhost:6379}") String connection, + @Value("${redis.timeout:2000}") int timeout) + throws URISyntaxException { + return new JedisPool(new URI(connection), timeout); + } +} diff --git a/gate-web/src/test/java/com/netflix/spinnaker/gate/config/RedisConfigTest.java b/gate-web/src/test/java/com/netflix/spinnaker/gate/config/RedisConfigTest.java new file mode 100644 index 0000000000..8c26f3b173 --- /dev/null +++ b/gate-web/src/test/java/com/netflix/spinnaker/gate/config/RedisConfigTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 OpsMx, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.gate.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.session.data.redis.config.ConfigureRedisAction; + +class RedisConfigTest { + + @Test + public void testCircularDependenciesException() { + ApplicationContextRunner applicationContextRunner = + new ApplicationContextRunner() + .withUserConfiguration(RedisConfig.class, RedisActionConfig.class) + .withBean(PostConnectionConfiguringJedisConnectionFactory.class); + assertDoesNotThrow( + () -> + applicationContextRunner.run( + ctx -> assertThat(ctx).hasSingleBean(ConfigureRedisAction.class))); + } + + @Test + public void testCircularDependenciesExceptionSecure() { + ApplicationContextRunner applicationContextRunner = + new ApplicationContextRunner() + .withUserConfiguration(RedisConfig.class, RedisActionConfig.class) + .withBean(PostConnectionConfiguringJedisConnectionFactory.class) + .withPropertyValues("redis.configuration.secure", "true"); + + assertDoesNotThrow( + () -> + applicationContextRunner.run( + ctx -> assertThat(ctx).getBeans(ConfigureRedisAction.class).hasSize(2))); + } +} From b519b9198491a5817f54b300900e5ffcc9380dac Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 2 Apr 2024 15:25:17 -0400 Subject: [PATCH 170/182] chore(dependencies): Autobump fiatVersion (#1785) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 990cf86fdd..c833dc85c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.44.0 +fiatVersion=1.45.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.222.0 kotlinVersion=1.6.21 From cc1dca421925c33b6f78d95a3d745ad959d9ab54 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 2 Apr 2024 16:29:13 -0400 Subject: [PATCH 171/182] chore(dependencies): Autobump korkVersion (#1786) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c833dc85c5..2a15cd206d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.45.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.222.0 +korkVersion=7.223.0 kotlinVersion=1.6.21 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From 8d7be108561d33ced05060b1ccef325cc391e1d2 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Sat, 6 Apr 2024 00:26:48 -0400 Subject: [PATCH 172/182] chore(dependencies): Autobump korkVersion (#1787) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2a15cd206d..917a3fb6ad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.45.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.223.0 +korkVersion=7.224.0 kotlinVersion=1.6.21 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From 74eb5648347e71ce421ee759834f9d4a779f3cd2 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 16 Apr 2024 18:30:44 -0400 Subject: [PATCH 173/182] chore(dependencies): Autobump fiatVersion (#1788) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 917a3fb6ad..300e99c2c7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.45.0 +fiatVersion=1.46.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.224.0 kotlinVersion=1.6.21 From 13382b92944dc12a095a7659e7ab33c94e300c0b Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 26 Apr 2024 20:44:25 -0400 Subject: [PATCH 174/182] chore(dependencies): Autobump korkVersion (#1791) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 300e99c2c7..bfeff51fe4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.46.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.224.0 +korkVersion=7.225.0 kotlinVersion=1.6.21 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From 8b9e8cc9cfd92e6cbea5fe70c861b5e54131c295 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 26 Apr 2024 22:24:25 -0400 Subject: [PATCH 175/182] chore(dependencies): Autobump fiatVersion (#1792) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index bfeff51fe4..d1524ba30e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.46.0 +fiatVersion=1.47.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.225.0 kotlinVersion=1.6.21 From c9339784fd79e185657f1c3932fdd9b8897451d3 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Sat, 27 Apr 2024 23:18:42 -0400 Subject: [PATCH 176/182] chore(dependencies): Autobump korkVersion (#1793) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d1524ba30e..1cf0712f4a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.47.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.225.0 +korkVersion=7.226.0 kotlinVersion=1.6.21 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From 4cf1b9d876103affc8738877c7fe3064a9c8c57b Mon Sep 17 00:00:00 2001 From: David Byron <82477955+dbyron-sf@users.noreply.github.com> Date: Wed, 1 May 2024 09:33:16 -0700 Subject: [PATCH 177/182] feat(build): add gate-integration module to exercise the just-built docker image (#1794) * feat(docker): add HEALTHCHECK to facilitate testing container startup * feat(build): add gate-integration module to exercise the just-built docker image * feat(gha): run integration test in pr builds multi-arch with --load doesn't work, so add a separate step using the local platform to make an image available for testing. see docker/buildx#59 --- .github/workflows/pr.yml | 15 +- Dockerfile.java11.slim | 3 +- Dockerfile.java11.ubuntu | 3 +- Dockerfile.slim | 3 +- Dockerfile.ubuntu | 3 +- gate-integration/gate-integration.gradle | 25 +++ .../gate/StandaloneContainerTest.java | 200 ++++++++++++++++++ .../src/test/resources/logback.xml | 36 ++++ settings.gradle | 1 + 9 files changed, 284 insertions(+), 5 deletions(-) create mode 100644 gate-integration/gate-integration.gradle create mode 100644 gate-integration/src/test/java/com/netflix/spinnaker/gate/StandaloneContainerTest.java create mode 100644 gate-integration/src/test/resources/logback.xml diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9a606799a1..6574b215e8 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -72,4 +72,17 @@ jobs: platforms: linux/amd64,linux/arm64 tags: | "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:latest-java11-ubuntu" - "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-java11-ubuntu" \ No newline at end of file + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-java11-ubuntu" + - name: Build local slim container image for testing + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile.slim + load: true + platforms: local + tags: | + "${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}" + - name: Test local slim container image + env: + FULL_DOCKER_IMAGE_NAME: "${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}" + run: ./gradlew ${{ steps.build_variables.outputs.REPO }}-integration:test diff --git a/Dockerfile.java11.slim b/Dockerfile.java11.slim index 6a412a1210..f2a0a94a41 100644 --- a/Dockerfile.java11.slim +++ b/Dockerfile.java11.slim @@ -1,9 +1,10 @@ FROM alpine:3.16 LABEL maintainer="sig-platform@spinnaker.io" -RUN apk --no-cache add --update bash openjdk11-jre +RUN apk --no-cache add --update bash curl openjdk11-jre RUN addgroup -S -g 10111 spinnaker RUN adduser -S -G spinnaker -u 10111 spinnaker COPY gate-web/build/install/gate /opt/gate RUN mkdir -p /opt/gate/plugins && chown -R spinnaker:nogroup /opt/gate/plugins USER spinnaker +HEALTHCHECK CMD curl http://localhost:8084/health | grep UP || exit 1 CMD ["/opt/gate/bin/gate"] diff --git a/Dockerfile.java11.ubuntu b/Dockerfile.java11.ubuntu index 2ad183bd54..1fb6b0cb38 100644 --- a/Dockerfile.java11.ubuntu +++ b/Dockerfile.java11.ubuntu @@ -1,8 +1,9 @@ FROM ubuntu:bionic LABEL maintainer="sig-platform@spinnaker.io" -RUN apt-get update && apt-get -y install openjdk-11-jre-headless wget +RUN apt-get update && apt-get -y install curl openjdk-11-jre-headless wget RUN adduser --system --uid 10111 --group spinnaker COPY gate-web/build/install/gate /opt/gate RUN mkdir -p /opt/gate/plugins && chown -R spinnaker:nogroup /opt/gate/plugins USER spinnaker +HEALTHCHECK CMD curl http://localhost:8084/health | grep UP || exit 1 CMD ["/opt/gate/bin/gate"] diff --git a/Dockerfile.slim b/Dockerfile.slim index 35ccbf224c..60f5e8215e 100644 --- a/Dockerfile.slim +++ b/Dockerfile.slim @@ -1,9 +1,10 @@ FROM alpine:3.18 LABEL maintainer="sig-platform@spinnaker.io" -RUN apk --no-cache add --update bash openjdk17-jre +RUN apk --no-cache add --update bash curl openjdk17-jre RUN addgroup -S -g 10111 spinnaker RUN adduser -S -G spinnaker -u 10111 spinnaker COPY gate-web/build/install/gate /opt/gate RUN mkdir -p /opt/gate/plugins && chown -R spinnaker:nogroup /opt/gate/plugins USER spinnaker +HEALTHCHECK CMD curl http://localhost:8084/health | grep UP || exit 1 CMD ["/opt/gate/bin/gate"] diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 9efd13c052..bedcf5213f 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,8 +1,9 @@ FROM ubuntu:jammy LABEL maintainer="sig-platform@spinnaker.io" -RUN apt-get update && apt-get -y install openjdk-17-jre-headless wget +RUN apt-get update && apt-get -y install curl openjdk-17-jre-headless wget RUN adduser --system --uid 10111 --group spinnaker COPY gate-web/build/install/gate /opt/gate RUN mkdir -p /opt/gate/plugins && chown -R spinnaker:nogroup /opt/gate/plugins USER spinnaker +HEALTHCHECK CMD curl http://localhost:8084/health | grep UP || exit 1 CMD ["/opt/gate/bin/gate"] diff --git a/gate-integration/gate-integration.gradle b/gate-integration/gate-integration.gradle new file mode 100644 index 0000000000..919149e21c --- /dev/null +++ b/gate-integration/gate-integration.gradle @@ -0,0 +1,25 @@ +dependencies { + testImplementation "com.fasterxml.jackson.core:jackson-databind" + testImplementation "com.github.tomakehurst:wiremock-jre8-standalone" + testImplementation "org.assertj:assertj-core" + testImplementation "org.junit.jupiter:junit-jupiter-api" + testImplementation "org.slf4j:slf4j-api" + testImplementation "org.testcontainers:testcontainers" + testImplementation "org.testcontainers:junit-jupiter" + testRuntimeOnly "ch.qos.logback:logback-classic" +} + +test.configure { + def fullDockerImageName = System.getenv('FULL_DOCKER_IMAGE_NAME') + onlyIf("there is a docker image to test") { + fullDockerImageName != null && fullDockerImageName.trim() != '' + } +} + +test { + // So stdout and stderr from the just-built container are available in CI + testLogging.showStandardStreams = true + + // Run the tests when the docker image changes + inputs.property 'fullDockerImageName', System.getenv('FULL_DOCKER_IMAGE_NAME') +} diff --git a/gate-integration/src/test/java/com/netflix/spinnaker/gate/StandaloneContainerTest.java b/gate-integration/src/test/java/com/netflix/spinnaker/gate/StandaloneContainerTest.java new file mode 100644 index 0000000000..1a91825de6 --- /dev/null +++ b/gate-integration/src/test/java/com/netflix/spinnaker/gate/StandaloneContainerTest.java @@ -0,0 +1,200 @@ +/* + * Copyright 2024 Salesforce, Inc. + * + * 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. + */ +package com.netflix.spinnaker.gate; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Map; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +@Testcontainers +class StandaloneContainerTest { + + private static final String REDIS_NETWORK_ALIAS = "redisHost"; + + private static final int REDIS_PORT = 6379; + + private static final Logger logger = LoggerFactory.getLogger(StandaloneContainerTest.class); + + private static final Network network = Network.newNetwork(); + + // gate caches application information from both clouddriver and front50, and + // account information from clouddriver. gate's health doesn't depend on this + // succeeding, but when it fails it spams the log with a log of noise. + @RegisterExtension + static final WireMockExtension wmClouddriver = + WireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build(); + + @RegisterExtension + static final WireMockExtension wmFront50 = + WireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build(); + + static int clouddriverPort; + static int front50Port; + + private static final GenericContainer redis = + new GenericContainer(DockerImageName.parse("library/redis:5-alpine")) + .withNetwork(network) + .withNetworkAliases(REDIS_NETWORK_ALIAS) + .withExposedPorts(REDIS_PORT); + + private static GenericContainer gateContainer; + + @BeforeAll + static void setupOnce() throws Exception { + front50Port = wmFront50.getRuntimeInfo().getHttpPort(); + logger.info("wiremock front50 http port: {} ", front50Port); + + clouddriverPort = wmClouddriver.getRuntimeInfo().getHttpPort(); + logger.info("wiremock clouddriver http port: {} ", clouddriverPort); + + // set up front50 stubs + wmFront50.stubFor( + WireMock.get(urlPathEqualTo("/v2/applications")) + .willReturn(aResponse().withStatus(200).withBody("[]"))); + + wmFront50.stubFor( + WireMock.get(urlPathEqualTo("/health")) + .willReturn(aResponse().withStatus(200).withBody("{}"))); + + // set up clouddriver stubs + wmClouddriver.stubFor( + WireMock.get(urlPathEqualTo("/applications")) + .willReturn(aResponse().withStatus(200).withBody("[]"))); + + wmClouddriver.stubFor( + WireMock.get(urlPathEqualTo("/credentials")) + .willReturn(aResponse().withStatus(200).withBody("[]"))); + + wmClouddriver.stubFor( + WireMock.get(urlPathEqualTo("/health")) + .willReturn(aResponse().withStatus(200).withBody("{}"))); + + String fullDockerImageName = System.getenv("FULL_DOCKER_IMAGE_NAME"); + + // Skip the tests if there's no docker image. This allows gradlew build to work. + assumeTrue(fullDockerImageName != null); + + // expose front50 to gate + org.testcontainers.Testcontainers.exposeHostPorts(front50Port); + + // expose clouddriver to gate + org.testcontainers.Testcontainers.exposeHostPorts(clouddriverPort); + + redis.start(); + + DockerImageName dockerImageName = DockerImageName.parse(fullDockerImageName); + + gateContainer = + new GenericContainer(dockerImageName) + .withNetwork(network) + .withExposedPorts(8084) + .dependsOn(redis) + .waitingFor(Wait.forHealthcheck()) + .withEnv("SPRING_APPLICATION_JSON", getSpringApplicationJson()); + + Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(logger); + gateContainer.start(); + gateContainer.followOutput(logConsumer); + } + + private static String getSpringApplicationJson() throws JsonProcessingException { + String redisUrl = "redis://" + REDIS_NETWORK_ALIAS + ":" + REDIS_PORT; + logger.info("redisUrl: '{}'", redisUrl); + Map properties = + Map.of( + "services.rosco.enabled", + "false", + "services.echo.enabled", + "false", + "services.orca.enabled", + "false", + "services.fiat.baseUrl", + "http://nowhere", + "redis.connection", + redisUrl, + "services.clouddriver.baseUrl", + "http://" + GenericContainer.INTERNAL_HOST_HOSTNAME + ":" + clouddriverPort, + "services.front50.baseUrl", + "http://" + GenericContainer.INTERNAL_HOST_HOSTNAME + ":" + front50Port); + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(properties); + } + + @AfterAll + static void cleanupOnce() { + if (gateContainer != null) { + gateContainer.stop(); + } + + if (redis != null) { + redis.stop(); + } + } + + @BeforeEach + void init(TestInfo testInfo) { + System.out.println("--------------- Test " + testInfo.getDisplayName()); + } + + @Test + void testHealthCheck() throws Exception { + // hit an arbitrary endpoint + HttpRequest request = + HttpRequest.newBuilder() + .uri( + new URI( + "http://" + + gateContainer.getHost() + + ":" + + gateContainer.getFirstMappedPort() + + "/health")) + .GET() + .build(); + + HttpClient client = HttpClient.newHttpClient(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + assertThat(response).isNotNull(); + logger.info("response: {}, {}", response.statusCode(), response.body()); + assertThat(response.statusCode()).isEqualTo(200); + } +} diff --git a/gate-integration/src/test/resources/logback.xml b/gate-integration/src/test/resources/logback.xml new file mode 100644 index 0000000000..6145d38780 --- /dev/null +++ b/gate-integration/src/test/resources/logback.xml @@ -0,0 +1,36 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle index debb60fc5a..497af00ca3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,6 +32,7 @@ include "gate-api", "gate-basic", "gate-bom", "gate-iap", + "gate-integration", "gate-ldap", "gate-oauth2", "gate-proxy", From 4809af309d421abc7b3cc9da06a9946076ddf4e7 Mon Sep 17 00:00:00 2001 From: David Byron <82477955+dbyron-sf@users.noreply.github.com> Date: Wed, 1 May 2024 11:00:25 -0700 Subject: [PATCH 178/182] feat(gha): run integration test in branch builds (#1795) --- .github/workflows/build.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0fbb1ca850..f77598c899 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,6 +39,19 @@ jobs: env: ORG_GRADLE_PROJECT_version: ${{ steps.build_variables.outputs.VERSION }} run: ./gradlew build --stacktrace ${{ steps.build_variables.outputs.REPO }}-web:installDist + - name: Build local slim container image for testing + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile.slim + load: true + platforms: local + tags: | + "${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-unvalidated" + - name: Test local slim container image + env: + FULL_DOCKER_IMAGE_NAME: "${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-unvalidated" + run: ./gradlew ${{ steps.build_variables.outputs.REPO }}-integration:test - name: Login to GAR # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/') @@ -98,4 +111,4 @@ jobs: push: true tags: | "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ github.ref_name }}-latest-java11-unvalidated-ubuntu" - "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-java11-unvalidated-ubuntu" \ No newline at end of file + "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-java11-unvalidated-ubuntu" From 0c5e4bce27a8299799bbfac01a85c98d2f0af767 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Wed, 8 May 2024 12:51:39 -0500 Subject: [PATCH 179/182] feat(saml): Update SAML to use Spring Security (#1744) This updates `gate-saml` to use the built-in SAML support in Spring Security which uses OpenSAML 4.x. Nearly all the previously supported properties for SAML are still supported, though a couple niche options no longer seem to be configurable. This also introduces `AuthenticationService`, a variation of `PermissionService` which can also return a user's granted authorities in one login call. It was also used for exception translation previously as retrofit exceptions are not serializable which would cause errors in Spring Security authentication failure error handlers, but the underlying exception being thrown has since been updated to avoid that problem. Co-authored-by: Jason --- .../gate/services/AuthenticationService.java | 100 ++++++++ gate-saml/gate-saml.gradle | 23 +- .../saml/DefaultUserIdentifierExtractor.java | 36 +++ .../saml/DefaultUserRolesExtractor.java | 82 +++++++ .../saml/ResponseAuthenticationConverter.java | 95 ++++++++ .../gate/security/saml/SAMLConfiguration.java | 107 ++++++++ .../gate/security/saml/SAMLSSOConfig.java | 125 ---------- .../security/saml/SAMLUserDetailsService.java | 228 ------------------ ...rties.java => SecuritySamlProperties.java} | 147 +++++------ .../saml/UserIdentifierExtractor.java | 25 ++ .../security/saml/UserRolesExtractor.java | 27 +++ .../gate/security/saml/package-info.java | 21 ++ .../SAMLSecurityConfigPropertiesSpec.groovy | 55 ----- gate-saml/src/test/resources/saml-client.jks | Bin 1300 -> 0 bytes 14 files changed, 583 insertions(+), 488 deletions(-) create mode 100644 gate-core/src/main/java/com/netflix/spinnaker/gate/services/AuthenticationService.java create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserIdentifierExtractor.java create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserRolesExtractor.java create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/ResponseAuthenticationConverter.java create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLConfiguration.java delete mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSSOConfig.java delete mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLUserDetailsService.java rename gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/{SAMLSecurityConfigProperties.java => SecuritySamlProperties.java} (50%) create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserIdentifierExtractor.java create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserRolesExtractor.java create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/package-info.java delete mode 100644 gate-saml/src/test/groovy/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigPropertiesSpec.groovy delete mode 100644 gate-saml/src/test/resources/saml-client.jks diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/AuthenticationService.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/AuthenticationService.java new file mode 100644 index 0000000000..14773db1b5 --- /dev/null +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/AuthenticationService.java @@ -0,0 +1,100 @@ +/* + * Copyright 2023 Apple, Inc. + * + * 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. + * + */ + +package com.netflix.spinnaker.gate.services; + +import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator; +import com.netflix.spinnaker.fiat.shared.FiatService; +import com.netflix.spinnaker.fiat.shared.FiatStatus; +import com.netflix.spinnaker.security.AuthenticatedRequest; +import io.micrometer.core.annotation.Counted; +import java.util.Collection; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +/** Facade for logging in an authenticated user and obtaining Fiat authorities. */ +@Log4j2 +@Service +@RequiredArgsConstructor +public class AuthenticationService { + private final FiatStatus fiatStatus; + private final FiatService fiatService; + private final FiatPermissionEvaluator permissionEvaluator; + + @Setter( + onParam_ = {@Qualifier("fiatLoginService")}, + onMethod_ = {@Autowired(required = false)}) + private FiatService fiatLoginService; + + private FiatService getFiatServiceForLogin() { + return fiatLoginService != null ? fiatLoginService : fiatService; + } + + @Counted("fiat.login") + public Collection login(String userid) { + if (!fiatStatus.isEnabled()) { + return Set.of(); + } + + return AuthenticatedRequest.allowAnonymous( + () -> { + getFiatServiceForLogin().loginUser(userid, ""); + return resolveAuthorities(userid); + }); + } + + @Counted("fiat.login") + public Collection loginWithRoles( + String userid, Collection roles) { + if (!fiatStatus.isEnabled()) { + return Set.of(); + } + + return AuthenticatedRequest.allowAnonymous( + () -> { + getFiatServiceForLogin().loginWithRoles(userid, roles); + return resolveAuthorities(userid); + }); + } + + @Counted("fiat.logout") + public void logout(String userid) { + if (!fiatStatus.isEnabled()) { + return; + } + + getFiatServiceForLogin().logoutUser(userid); + permissionEvaluator.invalidatePermission(userid); + } + + private Collection resolveAuthorities(String userid) { + permissionEvaluator.invalidatePermission(userid); + var permission = permissionEvaluator.getPermission(userid); + if (permission == null) { + throw new UsernameNotFoundException( + String.format("No user found in Fiat named '%s'", userid)); + } + return permission.toGrantedAuthorities(); + } +} diff --git a/gate-saml/gate-saml.gradle b/gate-saml/gate-saml.gradle index 18dda69573..311f2b668d 100644 --- a/gate-saml/gate-saml.gradle +++ b/gate-saml/gate-saml.gradle @@ -1,13 +1,20 @@ -dependencies{ +dependencies { + constraints { + implementation 'org.opensaml:opensaml-core:4.1.0' + implementation 'org.opensaml:opensaml-saml-api:4.1.0' + implementation 'org.opensaml:opensaml-saml-impl:4.1.0' + } + implementation project(':gate-core') - // RetrySupport is in kork-exceptions and not kork-core! + implementation 'io.spinnaker.kork:kork-core' + implementation 'io.spinnaker.kork:kork-crypto' + implementation 'io.spinnaker.kork:kork-exceptions' + implementation 'io.spinnaker.kork:kork-security' implementation "io.spinnaker.fiat:fiat-api:$fiatVersion" - implementation "io.spinnaker.kork:kork-exceptions" - implementation "io.spinnaker.kork:kork-security" - implementation 'org.springframework:spring-context' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.security:spring-security-saml2-service-provider' implementation 'org.springframework.session:spring-session-core' - implementation 'org.springframework.boot:spring-boot-autoconfigure' - implementation "org.springframework.security.extensions:spring-security-saml2-core" - implementation "org.springframework.security.extensions:spring-security-saml-dsl-core" + testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserIdentifierExtractor.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserIdentifierExtractor.java new file mode 100644 index 0000000000..65d3a994d8 --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserIdentifierExtractor.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Apple, Inc. + * + * 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. + * + */ + +package com.netflix.spinnaker.gate.security.saml; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; + +/** + * Default implementation for extracting the user id from an authenticated SAML user. This uses the + * settings in {@link SecuritySamlProperties.UserAttributeMapping#getUsername()} + */ +@RequiredArgsConstructor +public class DefaultUserIdentifierExtractor implements UserIdentifierExtractor { + private final SecuritySamlProperties properties; + + @Override + public String fromPrincipal(Saml2AuthenticatedPrincipal principal) { + String userid = principal.getFirstAttribute(properties.getUserAttributeMapping().getUsername()); + return userid != null ? userid : principal.getName(); + } +} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserRolesExtractor.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserRolesExtractor.java new file mode 100644 index 0000000000..15f947e7b7 --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserRolesExtractor.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 Apple, Inc. + * + * 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. + * + */ + +package com.netflix.spinnaker.gate.security.saml; + +import com.netflix.spinnaker.kork.exceptions.ConfigurationException; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import lombok.RequiredArgsConstructor; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; + +/** + * Default implementation for extracting roles from an authenticated SAML user. This uses the + * settings in {@link SecuritySamlProperties} related to roles. If role names appear to be + * distinguished names (i.e., they contain the substring {@code CN=}), then they will be parsed as + * DNs to extract the common name (CN) attribute. + */ +@RequiredArgsConstructor +public class DefaultUserRolesExtractor implements UserRolesExtractor { + private final SecuritySamlProperties properties; + + @Override + public Set getRoles(Saml2AuthenticatedPrincipal principal) { + var userAttributeMapping = properties.getUserAttributeMapping(); + List roles = principal.getAttribute(userAttributeMapping.getRoles()); + Stream roleStream = roles != null ? roles.stream() : Stream.empty(); + String delimiter = userAttributeMapping.getRolesDelimiter(); + roleStream = + delimiter != null + ? roleStream.flatMap(role -> Stream.of(role.split(delimiter))) + : roleStream; + roleStream = roleStream.map(DefaultUserRolesExtractor::parseRole); + if (properties.isForceLowercaseRoles()) { + roleStream = roleStream.map(role -> role.toLowerCase(Locale.ROOT)); + } + if (properties.isSortRoles()) { + roleStream = roleStream.sorted(); + } + return roleStream.collect(Collectors.toCollection(LinkedHashSet::new)); + } + + private static String parseRole(String role) { + if (!role.contains("CN=")) { + return role; + } + try { + return new LdapName(role) + .getRdns().stream() + .filter(rdn -> rdn.getType().equals("CN")) + .map(rdn -> (String) rdn.getValue()) + .findFirst() + .orElseThrow( + () -> + new ConfigurationException( + String.format( + "SAML role '%s' contains 'CN=' but cannot be parsed as a DN", role))); + } catch (InvalidNameException e) { + throw new ConfigurationException( + String.format("Unable to parse SAML role name '%s'", role), e); + } + } +} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/ResponseAuthenticationConverter.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/ResponseAuthenticationConverter.java new file mode 100644 index 0000000000..3887fd1428 --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/ResponseAuthenticationConverter.java @@ -0,0 +1,95 @@ +/* + * Copyright 2023 Apple, Inc. + * + * 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. + * + */ + +package com.netflix.spinnaker.gate.security.saml; + +import com.netflix.spinnaker.gate.services.AuthenticationService; +import com.netflix.spinnaker.security.User; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.util.CollectionUtils; + +/** Handles conversion of an authenticated SAML user into a Spinnaker user and populating Fiat. */ +@Log4j2 +@RequiredArgsConstructor +public class ResponseAuthenticationConverter + implements Converter { + private final SecuritySamlProperties properties; + private final ObjectFactory userIdentifierExtractorFactory; + private final ObjectFactory userRolesExtractorFactory; + private final ObjectFactory authenticationServiceFactory; + + @Override + public PreAuthenticatedAuthenticationToken convert(ResponseToken source) { + UserIdentifierExtractor userIdentifierExtractor = userIdentifierExtractorFactory.getObject(); + UserRolesExtractor userRolesExtractor = userRolesExtractorFactory.getObject(); + AuthenticationService loginService = authenticationServiceFactory.getObject(); + log.debug("Decoding SAML response: {}", source.getToken()); + + Saml2Authentication authentication = convertToken(source); + @SuppressWarnings("deprecation") + var user = new User(); + Saml2AuthenticatedPrincipal principal = + (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); + String principalName = principal.getName(); + var userAttributeMapping = properties.getUserAttributeMapping(); + String email = principal.getFirstAttribute(userAttributeMapping.getEmail()); + user.setEmail(email != null ? email : principalName); + String userid = userIdentifierExtractor.fromPrincipal(principal); + user.setUsername(userid); + user.setFirstName(principal.getFirstAttribute(userAttributeMapping.getFirstName())); + user.setLastName(principal.getFirstAttribute(userAttributeMapping.getLastName())); + + Set roles = userRolesExtractor.getRoles(principal); + user.setRoles(roles); + + if (!CollectionUtils.isEmpty(properties.getRequiredRoles())) { + var requiredRoles = Set.copyOf(properties.getRequiredRoles()); + // check for at least one common role in both sets + if (Collections.disjoint(roles, requiredRoles)) { + throw new BadCredentialsException( + String.format("User %s is not in any required role from %s", email, requiredRoles)); + } + } + + Collection authorities = loginService.loginWithRoles(userid, roles); + return new PreAuthenticatedAuthenticationToken(user, principal, authorities); + } + + private static final Converter DEFAULT_CONVERTER = + OpenSaml4AuthenticationProvider.createDefaultResponseAuthenticationConverter(); + + private static Saml2Authentication convertToken(ResponseToken token) { + Saml2Authentication authentication = DEFAULT_CONVERTER.convert(token); + if (authentication == null) { + throw new IllegalArgumentException("Response token could not be converted"); + } + return authentication; + } +} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLConfiguration.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLConfiguration.java new file mode 100644 index 0000000000..7f3f1493a8 --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLConfiguration.java @@ -0,0 +1,107 @@ +/* + * Copyright 2023 Apple, Inc. + * + * 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. + * + */ + +package com.netflix.spinnaker.gate.security.saml; + +import com.netflix.spinnaker.gate.config.AuthConfig; +import com.netflix.spinnaker.gate.security.SpinnakerAuthConfig; +import com.netflix.spinnaker.gate.services.AuthenticationService; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.session.DefaultCookieSerializerCustomizer; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.Customizer; +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.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; +import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(SecuritySamlProperties.class) +public class SAMLConfiguration { + + @EnableWebSecurity + @SpinnakerAuthConfig + @RequiredArgsConstructor + @ConditionalOnProperty("saml.enabled") + public static class WebSecurityConfig extends WebSecurityConfigurerAdapter { + private final SecuritySamlProperties properties; + private final AuthConfig authConfig; + private final ObjectProvider userIdentifierExtractorProvider; + private final ObjectProvider userRolesExtractorProvider; + private final ObjectFactory authenticationServiceFactory; + + /** Disables the same-site requirement for cookies as configured in other SSO modules. */ + @Bean + public static DefaultCookieSerializerCustomizer defaultCookieSerializerCustomizer() { + return cookieSerializer -> cookieSerializer.setSameSite(null); + } + + @Bean + public ResponseAuthenticationConverter responseAuthenticationConverter() { + return new ResponseAuthenticationConverter( + properties, + () -> + userIdentifierExtractorProvider.getIfAvailable( + () -> new DefaultUserIdentifierExtractor(properties)), + () -> + userRolesExtractorProvider.getIfAvailable( + () -> new DefaultUserRolesExtractor(properties)), + authenticationServiceFactory); + } + + @Bean + @SneakyThrows + public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() { + var builder = + RelyingPartyRegistrations.fromMetadataLocation(properties.getMetadataUrl()) + .registrationId(properties.getRegistrationId()) + .entityId(properties.getIssuerId()) + .assertionConsumerServiceLocation(properties.getAssertionConsumerServiceLocation()); + Saml2X509Credential decryptionCredential = properties.getDecryptionCredential(); + if (decryptionCredential != null) { + builder.decryptionX509Credentials(credentials -> credentials.add(decryptionCredential)); + } + RelyingPartyRegistration registration = builder.build(); + return new InMemoryRelyingPartyRegistrationRepository(registration); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + authConfig.configure(http); + var authenticationProvider = new OpenSaml4AuthenticationProvider(); + authenticationProvider.setResponseAuthenticationConverter(responseAuthenticationConverter()); + http.rememberMe(Customizer.withDefaults()) + .saml2Login( + saml -> + saml.authenticationManager(new ProviderManager(authenticationProvider)) + .loginProcessingUrl(properties.getLoginProcessingUrl()) + .relyingPartyRegistrationRepository(relyingPartyRegistrationRepository())); + } + } +} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSSOConfig.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSSOConfig.java deleted file mode 100644 index 18763f8f6a..0000000000 --- a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSSOConfig.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2014 Netflix, Inc. - * Copyright 2023 Apple, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.security.saml; - -import static org.springframework.security.extensions.saml2.config.SAMLConfigurer.saml; - -import com.netflix.spinnaker.gate.config.AuthConfig; -import com.netflix.spinnaker.gate.security.SpinnakerAuthConfig; -import java.net.InetAddress; -import javax.annotation.Nullable; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import org.opensaml.xml.security.BasicSecurityConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -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.saml.userdetails.SAMLUserDetailsService; -import org.springframework.security.saml.websso.WebSSOProfileConsumerImpl; -import org.springframework.session.web.http.DefaultCookieSerializer; -import org.springframework.util.StringUtils; - -/** - * Configures SAML2 authentication for Spinnaker. - * - * @see SAML - * 2.0 configuration docs - */ -@Log4j2 -@ConditionalOnExpression("${saml.enabled:false}") -@Configuration -@SpinnakerAuthConfig -@EnableWebSecurity -@EnableConfigurationProperties(SAMLSecurityConfigProperties.class) -@ComponentScan -@RequiredArgsConstructor -public class SAMLSSOConfig extends WebSecurityConfigurerAdapter { - private final DefaultCookieSerializer defaultCookieSerializer; - private final AuthConfig authConfig; - private final SAMLUserDetailsService samlUserDetailsService; - private final SAMLSecurityConfigProperties samlSecurityConfigProperties; - @Nullable private final ServerProperties serverProperties; - - @Override - protected void configure(HttpSecurity http) throws Exception { - // We need our session cookie to come across when we get redirected back from the IdP: - defaultCookieSerializer.setSameSite(null); - authConfig.configure(http); - - http.rememberMe() - .key("password") - .rememberMeCookieName("cookieName") - .rememberMeParameter("rememberMe"); - - var webSSOProfileConsumer = new WebSSOProfileConsumerImpl(); - webSSOProfileConsumer.setMaxAuthenticationAge( - samlSecurityConfigProperties.getMaxAuthenticationAge()); - - var hostname = samlSecurityConfigProperties.getRedirectHostname(); - if (!StringUtils.hasLength(hostname) && serverProperties != null) { - InetAddress address = serverProperties.getAddress(); - if (address != null) { - hostname = address.getHostName(); - } - } - - // @formatter:off - - saml() - .userDetailsService(samlUserDetailsService) - .identityProvider() - .metadataFilePath(samlSecurityConfigProperties.getMetadataUrl()) - .discoveryEnabled(false) - .and() - .webSSOProfileConsumer(webSSOProfileConsumer) - .serviceProvider() - .entityId(samlSecurityConfigProperties.getIssuerId()) - .protocol(samlSecurityConfigProperties.getRedirectProtocol()) - .hostname(hostname) - .basePath(samlSecurityConfigProperties.getRedirectBasePath()) - .keyStore() - .storeFilePath(samlSecurityConfigProperties.getKeyStore()) - .password(samlSecurityConfigProperties.getKeyStorePassword()) - .keyname(samlSecurityConfigProperties.getKeyStoreAliasName()) - .keyPassword(samlSecurityConfigProperties.getKeyStorePassword()) - .and() - .and() - .init(http); - - // @formatter:on - - // Need to be after SAMLConfigurer initializes the global SecurityConfiguration - var secConfig = org.opensaml.Configuration.getGlobalSecurityConfiguration(); - if (secConfig instanceof BasicSecurityConfiguration) { - var config = (BasicSecurityConfiguration) secConfig; - var digest = samlSecurityConfigProperties.signatureDigest(); - log.info("Using {} digest for signing SAML messages", digest); - config.registerSignatureAlgorithmURI("RSA", digest.getSignatureMethod()); - config.setSignatureReferenceDigestMethod(digest.getDigestMethod()); - } else { - log.warn( - "Unable to find global BasicSecurityConfiguration (found '{}'). Ignoring signatureDigest configuration value.", - secConfig); - } - } -} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLUserDetailsService.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLUserDetailsService.java deleted file mode 100644 index c187c3e5f9..0000000000 --- a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLUserDetailsService.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright 2014 Netflix, Inc. - * Copyright 2023 Apple, Inc. - * - * 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. - */ - -package com.netflix.spinnaker.gate.security.saml; - -import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties; -import com.netflix.spinnaker.gate.security.AllowedAccountsSupport; -import com.netflix.spinnaker.gate.services.PermissionService; -import com.netflix.spinnaker.kork.core.RetrySupport; -import com.netflix.spinnaker.kork.exceptions.ConfigurationException; -import com.netflix.spinnaker.security.User; -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tag; -import io.micrometer.core.instrument.Tags; -import java.time.Duration; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; -import lombok.extern.log4j.Log4j2; -import org.opensaml.saml2.core.Assertion; -import org.opensaml.saml2.core.Attribute; -import org.opensaml.saml2.core.AttributeStatement; -import org.opensaml.xml.schema.XSAny; -import org.opensaml.xml.schema.XSString; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.saml.SAMLCredential; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; - -@Log4j2 -@Service -@ConditionalOnProperty("saml.enabled") -public class SAMLUserDetailsService - implements org.springframework.security.saml.userdetails.SAMLUserDetailsService { - private static final String COUNTER_NAME = "fiat.login"; - private static final Tag TYPE = Tag.of("type", "saml"); - private static final Tag SUCCESS = Tag.of("success", "true"); - private static final Tag FAILURE = Tag.of("success", "false"); - private static final Tag NO_FALLBACK = Tag.of("fallback", "none"); - - private final PermissionService permissionService; - private final AllowedAccountsSupport allowedAccountsSupport; - private final FiatClientConfigurationProperties fiatClientConfigurationProperties; - private final SAMLSecurityConfigProperties samlSecurityConfigProperties; - private final Counter successes; - private final Counter failures; - private final RetrySupport retrySupport = new RetrySupport(); - - public SAMLUserDetailsService( - PermissionService permissionService, - AllowedAccountsSupport allowedAccountsSupport, - FiatClientConfigurationProperties fiatClientConfigurationProperties, - SAMLSecurityConfigProperties samlSecurityConfigProperties, - MeterRegistry meterRegistry) { - this.permissionService = permissionService; - this.allowedAccountsSupport = allowedAccountsSupport; - this.fiatClientConfigurationProperties = fiatClientConfigurationProperties; - this.samlSecurityConfigProperties = samlSecurityConfigProperties; - successes = meterRegistry.counter(COUNTER_NAME, Tags.of(TYPE, SUCCESS, NO_FALLBACK)); - failures = - meterRegistry.counter( - COUNTER_NAME, - Tags.of( - TYPE, - FAILURE, - Tag.of( - "fallback", - Boolean.toString(fiatClientConfigurationProperties.isLegacyFallback())))); - } - - @Override - public Object loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException { - var assertion = credential.getAuthenticationAssertion(); - var attributes = extractAttributes(assertion); - var userAttributeMapping = samlSecurityConfigProperties.getUserAttributeMapping(); - @SuppressWarnings("deprecation") - var user = new User(); - - var subjectNameId = assertion.getSubject().getNameID().getValue(); - var emailAttributeValue = - CollectionUtils.firstElement(attributes.get(userAttributeMapping.getEmail())); - var email = emailAttributeValue != null ? emailAttributeValue : subjectNameId; - user.setEmail(email); - var usernameAttributeValue = - CollectionUtils.firstElement(attributes.get(userAttributeMapping.getUsername())); - var username = usernameAttributeValue != null ? usernameAttributeValue : subjectNameId; - user.setUsername(username); - var roles = extractRoles(attributes); - user.setRoles(roles); - - if (!CollectionUtils.isEmpty(samlSecurityConfigProperties.getRequiredRoles())) { - var requiredRoles = Set.copyOf(samlSecurityConfigProperties.getRequiredRoles()); - // check for at least one common role in both sets - if (Collections.disjoint(roles, requiredRoles)) { - throw new BadCredentialsException( - String.format("User %s is not in any required role from %s", email, requiredRoles)); - } - } - - Supplier login = - () -> { - permissionService.loginWithRoles(username, roles); - return null; - }; - - try { - retrySupport.retry(login, 5, Duration.ofSeconds(2), false); - log.debug( - "Successful SAML authentication (user: {}, roleCount: {}, roles: {})", - username, - roles.size(), - roles); - successes.increment(); - } catch (Exception e) { - boolean legacyFallback = fiatClientConfigurationProperties.isLegacyFallback(); - log.debug( - "Unsuccessful SAML authentication (user: {}, roleCount: {}, roles: {}, legacyFallback: {})", - username, - roles.size(), - roles, - legacyFallback, - e); - failures.increment(); - - if (!legacyFallback) { - throw e; - } - } - - user.setFirstName( - CollectionUtils.firstElement(attributes.get(userAttributeMapping.getFirstName()))); - user.setLastName( - CollectionUtils.firstElement(attributes.get(userAttributeMapping.getLastName()))); - user.setAllowedAccounts(allowedAccountsSupport.filterAllowedAccounts(username, roles)); - - return user; - } - - private Set extractRoles(Map> attributes) { - var userAttributeMapping = samlSecurityConfigProperties.getUserAttributeMapping(); - var roleStream = - attributes.getOrDefault(userAttributeMapping.getRoles(), List.of()).stream() - .flatMap(roles -> Stream.of(roles.split(userAttributeMapping.getRolesDelimiter()))) - .map(SAMLUserDetailsService::parseRole); - if (samlSecurityConfigProperties.isForceLowercaseRoles()) { - roleStream = roleStream.map(String::toLowerCase); - } - if (samlSecurityConfigProperties.isSortRoles()) { - roleStream = roleStream.sorted(); - } - return roleStream.collect(Collectors.toCollection(LinkedHashSet::new)); - } - - private static Map> extractAttributes(Assertion assertion) { - return assertion.getAttributeStatements().stream() - .flatMap(SAMLUserDetailsService::streamAttributes) - .collect( - Collectors.groupingBy( - Attribute::getName, - Collectors.flatMapping( - SAMLUserDetailsService::streamAttributeValues, Collectors.toList()))); - } - - private static Stream streamAttributes(AttributeStatement statement) { - return statement.getAttributes().stream(); - } - - private static Stream streamAttributeValues(Attribute attribute) { - return attribute.getAttributeValues().stream() - .map( - object -> { - if (object instanceof XSString) { - return ((XSString) object).getValue(); - } - if (object instanceof XSAny) { - return ((XSAny) object).getTextContent(); - } - return null; - }) - .filter(Objects::nonNull); - } - - private static String parseRole(String role) { - if (!role.contains("CN=")) { - return role; - } - try { - return new LdapName(role) - .getRdns().stream() - .filter(rdn -> rdn.getType().equals("CN")) - .map(rdn -> (String) rdn.getValue()) - .findFirst() - .orElseThrow( - () -> - new ConfigurationException( - String.format( - "SAML role '%s' contains 'CN=' but cannot be parsed as a DN", role))); - } catch (InvalidNameException e) { - throw new ConfigurationException( - String.format("Unable to parse SAML role name '%s'", role), e); - } - } -} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigProperties.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SecuritySamlProperties.java similarity index 50% rename from gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigProperties.java rename to gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SecuritySamlProperties.java index 5644fb435a..288234b097 100644 --- a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigProperties.java +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SecuritySamlProperties.java @@ -1,5 +1,4 @@ /* - * Copyright 2014 Netflix, Inc. * Copyright 2023 Apple, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,88 +12,116 @@ * 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 com.netflix.spinnaker.gate.security.saml; +import com.netflix.spinnaker.kork.annotations.NullableByDefault; import com.netflix.spinnaker.kork.exceptions.ConfigurationException; import java.io.IOException; -import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import java.util.Enumeration; import java.util.List; -import java.util.Locale; -import java.util.Objects; import java.util.Set; import java.util.TreeSet; +import javax.annotation.Nonnull; import javax.annotation.PostConstruct; -import javax.xml.crypto.dsig.DigestMethod; -import javax.xml.crypto.dsig.SignatureMethod; +import javax.validation.constraints.NotEmpty; import lombok.Getter; -import lombok.RequiredArgsConstructor; import lombok.Setter; -import org.opensaml.xml.signature.SignatureConstants; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; @Getter @Setter +@Validated @ConfigurationProperties("saml") -public class SAMLSecurityConfigProperties { - private static final String FILE_SCHEME = "file:"; - - private String keyStore; +@NullableByDefault +public class SecuritySamlProperties { + private Path keyStore; private String keyStoreType = "PKCS12"; private String keyStorePassword; - private String keyStoreAliasName; + private String keyStoreAliasName = "mykey"; // default alias for keytool + + public Saml2X509Credential getDecryptionCredential() + throws IOException, GeneralSecurityException { + if (keyStore == null) { + return null; + } + if (keyStoreType == null) { + keyStoreType = "PKCS12"; + } + KeyStore store = KeyStore.getInstance(keyStoreType); + char[] password = keyStorePassword != null ? keyStorePassword.toCharArray() : new char[0]; + try (var stream = Files.newInputStream(keyStore)) { + store.load(stream, password); + } + String alias = keyStoreAliasName; + var certificate = (X509Certificate) store.getCertificate(alias); + var privateKey = (PrivateKey) store.getKey(alias, password); + return Saml2X509Credential.decryption(privateKey, certificate); + } - // SAML DSL uses a metadata URL instead of hard coding a certificate/issuerId/redirectBase into - // the config. + /** URL pointing to the SAML metadata to use. */ private String metadataUrl; - // The parts of this endpoint passed to/used by the SAML IdP. - private String redirectProtocol = "https"; - private String redirectHostname; - private String redirectBasePath = "/"; - // The application identifier given to the IdP for this app. - private String issuerId; + /** Registration id for this SAML provider. Used in SAML processing URLs. */ + @NotEmpty private String registrationId = "SSO"; + + /** + * The Relying Party's entity ID (sometimes called an issuer ID). The value may contain a number + * of placeholders. They are "baseUrl", "registrationId", "baseScheme", "baseHost", and + * "basePort". + */ + @NotEmpty private String issuerId = "{baseUrl}/saml2/metadata"; + + /** + * The path used for login processing. When combined with the base URL, this should form the + * assertion consumer service location. + */ + @NotEmpty private String loginProcessingUrl = "/saml/{registrationId}"; + + /** + * Returns the assertion consumer service location template to use for redirecting back from the + * identity provider. + */ + public String getAssertionConsumerServiceLocation() { + return "{baseUrl}" + loginProcessingUrl; + } + + /** Optional list of roles required for authentication to succeed. */ private List requiredRoles; + + /** Determines whether to sort the roles returned from the SAML provider. */ private boolean sortRoles = false; + + /** Toggles whether role names should be converted to lowercase. */ private boolean forceLowercaseRoles = true; - @NestedConfigurationProperty + @Nonnull @NestedConfigurationProperty private UserAttributeMapping userAttributeMapping = new UserAttributeMapping(); - private long maxAuthenticationAge = 7200; - - // SHA1 is the default registered in DefaultSecurityConfigurationBootstrap.populateSignatureParams - private String signatureDigest = "SHA1"; - - public SignatureDigest signatureDigest() { - return SignatureDigest.fromName(signatureDigest); - } - - /** - * Ensure that the keystore exists and can be accessed with the given keyStorePassword and - * keyStoreAliasName. Validates the configured signature/digest is supported. - */ @PostConstruct public void validate() throws IOException, GeneralSecurityException { if (StringUtils.hasLength(metadataUrl) && metadataUrl.startsWith("/")) { - metadataUrl = FILE_SCHEME + metadataUrl; + metadataUrl = "file:" + metadataUrl; } - if (StringUtils.hasLength(keyStore)) { - if (!keyStore.startsWith(FILE_SCHEME)) { - keyStore = FILE_SCHEME + keyStore; + if (keyStore != null) { + if (keyStoreType == null) { + keyStoreType = "PKCS12"; } - var path = Path.of(URI.create(keyStore)); var keystore = KeyStore.getInstance(keyStoreType); var password = keyStorePassword != null ? keyStorePassword.toCharArray() : new char[0]; - try (var stream = Files.newInputStream(path)) { + try (var stream = Files.newInputStream(keyStore)) { // will throw an exception if `keyStorePassword` is invalid or if the key store file is // invalid keystore.load(stream, password); @@ -109,12 +136,11 @@ public void validate() throws IOException, GeneralSecurityException { } } } - // Validate signature digest algorithm - Objects.requireNonNull(signatureDigest()); } + @Nonnull private static Set caseInsensitiveSetFromAliasEnumeration( - Enumeration enumeration) { + @Nonnull Enumeration enumeration) { Set set = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); while (enumeration.hasMoreElements()) { set.add(enumeration.nextElement()); @@ -124,36 +150,13 @@ private static Set caseInsensitiveSetFromAliasEnumeration( @Getter @Setter + @Validated public static class UserAttributeMapping { - private String firstName = "User.FirstName"; - private String lastName = "User.LastName"; - private String roles = "memberOf"; - private String rolesDelimiter = ";"; + @NotEmpty private String firstName = "User.FirstName"; + @NotEmpty private String lastName = "User.LastName"; + @NotEmpty private String roles = "memberOf"; + @NotEmpty private String rolesDelimiter = ";"; private String username; private String email; } - - // only RSA-based signatures explicitly supported here (baseline requirement for XML signatures) - @Getter - @RequiredArgsConstructor - public enum SignatureDigest { - @Deprecated - SHA1(SignatureMethod.RSA_SHA1, DigestMethod.SHA1), - SHA256(SignatureMethod.RSA_SHA256, DigestMethod.SHA256), - SHA384(SignatureMethod.RSA_SHA384, DigestMethod.SHA384), - SHA512(SignatureMethod.RSA_SHA512, DigestMethod.SHA512), - @Deprecated - RIPEMD160(SignatureConstants.ALGO_ID_SIGNATURE_RSA_RIPEMD160, DigestMethod.RIPEMD160), - @Deprecated - MD5( - SignatureConstants.ALGO_ID_SIGNATURE_NOT_RECOMMENDED_RSA_MD5, - SignatureConstants.ALGO_ID_DIGEST_NOT_RECOMMENDED_MD5), - ; - private final String signatureMethod; - private final String digestMethod; - - public static SignatureDigest fromName(String name) { - return valueOf(name.toUpperCase(Locale.ROOT)); - } - } } diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserIdentifierExtractor.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserIdentifierExtractor.java new file mode 100644 index 0000000000..c34f324675 --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserIdentifierExtractor.java @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Apple, Inc. + * + * 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. + * + */ + +package com.netflix.spinnaker.gate.security.saml; + +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; + +/** Strategy for extracting a userid from an authenticated SAML2 principal. */ +public interface UserIdentifierExtractor { + String fromPrincipal(Saml2AuthenticatedPrincipal principal); +} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserRolesExtractor.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserRolesExtractor.java new file mode 100644 index 0000000000..1eee3bef9c --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserRolesExtractor.java @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Apple, Inc. + * + * 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. + * + */ + +package com.netflix.spinnaker.gate.security.saml; + +import java.util.Set; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; + +/** Strategy for extracting and potentially filtering roles from a SAML assertion. */ +public interface UserRolesExtractor { + /** Returns the roles to assign the given principal. */ + Set getRoles(Saml2AuthenticatedPrincipal principal); +} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/package-info.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/package-info.java new file mode 100644 index 0000000000..4b8309fff5 --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Apple, Inc. + * + * 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. + * + */ + +@NonnullByDefault +package com.netflix.spinnaker.gate.security.saml; + +import com.netflix.spinnaker.kork.annotations.NonnullByDefault; diff --git a/gate-saml/src/test/groovy/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigPropertiesSpec.groovy b/gate-saml/src/test/groovy/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigPropertiesSpec.groovy deleted file mode 100644 index 7a5f7cfc53..0000000000 --- a/gate-saml/src/test/groovy/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigPropertiesSpec.groovy +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * 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. - */ - - -package com.netflix.spinnaker.gate.security.saml - -import spock.lang.Specification -import spock.lang.Unroll - -class SAMLSecurityConfigPropertiesSpec extends Specification { - @Unroll - def "should validate that the keystore exists and the password/alias are valid"() { - given: - def ssoConfig = new SAMLSecurityConfigProperties( - keyStore: keyStore.toString(), keyStorePassword: keyStorePassword, keyStoreAliasName: keyStoreAliasName - ) - - expect: - try { - ssoConfig.validate() - assert !expectsException - } catch (Exception ignored) { - assert expectsException - } - - try { - // ensure validation works if a keystore is not prefixed with "file:" - ssoConfig.keyStore = ssoConfig ? ssoConfig.keyStore.replaceAll("file:", "") : null - ssoConfig.validate() - assert !expectsException - } catch (Exception ignored) { - assert expectsException - } - - where: - keyStore | keyStorePassword | keyStoreAliasName || expectsException - this.class.getResource("/does-not-exist.jks") | null | null || true // keystore does not exist - this.class.getResource("/saml-client.jks") | "invalid" | "saml-client" || true // password is invalid - this.class.getResource("/saml-client.jks") | "123456" | "invalid" || true // alias is invalid - this.class.getResource("/saml-client.jks") | "123456" | "saml-client" || false - } -} diff --git a/gate-saml/src/test/resources/saml-client.jks b/gate-saml/src/test/resources/saml-client.jks deleted file mode 100644 index ea2a99098d129cc904a9f5603fb85332d95d3f72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1300 zcmezO_TO6u1_mY|W&~sI;>6q>-Q=9i)Vva)SR}*cmt{b@-=K-H+klUaOPh_6g;9%1 zkdcvvj9EL3`xSkbg3{DE!XLvclqv#Ts+PnfMy z+VJdgss8Cct6alh(Vml(Le)D@wI;=X6JAgtk+t(zs@c{z>%Hr5@BZKWM3Hg2wR6D> zGpXn+W>THq;*Nl$$Jry>AM+g4S{uAS=Ejrhn-cvZA0XC;JYNlm1~H{`}aA zWup8nzni~ZJ}P&Z$uvYbN^GLhx6;q+v=gsuz5k@dEIIe02jg)jQHJRW0sW%N+-EjT z+S#<|bqn{02$`=Sr+t68p!M2p+k2%KF{EtX?(!4EN!-Wp7Fkj12GF zbtTgOSs}YXW5X6tu|nk+J{n@e+s|_v^_*Dt&_=APJEW#pQ0Mms18+4Vf3;7civNrxv`Z2dSj8>#QeyB65H2ZMJ+UXW4&tPu_LTz|zAlN7AOqTTWl2#ZkDgP|qXX9w zFU8I0zgt{9#i^6oc=F7>^i0{MPCeGw)w4yM4=z?TkT;M8rdwG)7BLo)i>KyJn(5D9 zmt(fQRQ1J{qu0f+u0l!Y%=!!lI!q$2=k-nhI+-PDSo?Oq7SuXX?xF6$BvQIb?AGDW o|K9y{HDn8(;P6p#R^G~;jl6liG8eZw>w2Gmyrc1pv`F+F0I)*|)&Kwi From 070213f2068464b27f0e4aaa1dd2707949a78e53 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 10 May 2024 14:24:48 -0400 Subject: [PATCH 180/182] chore(dependencies): Autobump korkVersion (#1797) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 1cf0712f4a..61a98a6fbb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.47.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.226.0 +korkVersion=7.227.0 kotlinVersion=1.6.21 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From 487f0d32595bdcab1d3cfc33ceba70a1d0ad6434 Mon Sep 17 00:00:00 2001 From: David Byron <82477955+dbyron-sf@users.noreply.github.com> Date: Tue, 14 May 2024 08:47:04 -0700 Subject: [PATCH 181/182] fix(web/test): stop leaking system properties from FunctionalSpec (#1798) When they leak, other tests fail with errors like: MainSpec > startupTest() FAILED java.lang.IllegalStateException: Failed to load ApplicationContext at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:132) at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:124) at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:190) at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:132) at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:248) at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:138) at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$6(ClassBasedTestDescriptor.java:350) at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.executeAndMaskThrowable(ClassBasedTestDescriptor.java:355) at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$7(ClassBasedTestDescriptor.java:350) at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195) at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:177) at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655) at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484) at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474) at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:312) at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:735) at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:734) at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658) at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeTestInstancePostProcessors(ClassBasedTestDescriptor.java:349) at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$instantiateAndPostProcessTestInstance$4(ClassBasedTestDescriptor.java:270) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateAndPostProcessTestInstance(ClassBasedTestDescriptor.java:269) at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$2(ClassBasedTestDescriptor.java:259) at java.base/java.util.Optional.orElseGet(Optional.java:369) at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$3(ClassBasedTestDescriptor.java:258) at org.junit.jupiter.engine.execution.TestInstancesProvider.getTestInstances(TestInstancesProvider.java:31) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$prepare$0(TestMethodTestDescriptor.java:101) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:100) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:65) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$1(NodeTestTask.java:111) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:111) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:79) at java.base/java.util.ArrayList.forEach(ArrayList.java:1541) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84) at java.base/java.util.ArrayList.forEach(ArrayList.java:1541) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32) at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75) at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:62) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24) at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94) at com.sun.proxy.$Proxy2.stop(Unknown Source) at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193) at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129) at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100) at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60) at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65) at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'applicationController': Unsatisfied dependency expressed through field 'applicationService'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'applicationService' defined in file [/Users/dbyron/src/spinnaker/salesforce/gate/gate-web/build/classes/groovy/main/com/netflix/spinnaker/gate/services/ApplicationService.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'clouddriverServiceSelector' defined in class path resource [com/netflix/spinnaker/gate/config/GateConfig.class]: Unsatisfied dependency expressed through method 'clouddriverServiceSelector' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'clouddriverService' defined in class path resource [com/netflix/spinnaker/gate/config/GateConfig.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.netflix.spinnaker.gate.services.internal.ClouddriverService]: Factory method 'clouddriverService' threw exception; nested exception is com.netflix.spinnaker.kork.exceptions.SystemException: No service client provider found for url (http://localhost:7002) at app//org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:659) at app//org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:639) at app//org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) at app//org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) at app//org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) at app//org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at app//org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at app//org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:953) at app//org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) at app//org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) at app//org.springframework.boot.SpringApplication.refresh(SpringApplication.java:780) at app//org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:453) at app//org.springframework.boot.SpringApplication.run(SpringApplication.java:343) at app//org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:144) at app//org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99) at app//org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124) ... 85 more Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'applicationService' defined in file [/Users/dbyron/src/spinnaker/salesforce/gate/gate-web/build/classes/groovy/main/com/netflix/spinnaker/gate/services/ApplicationService.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'clouddriverServiceSelector' defined in class path resource [com/netflix/spinnaker/gate/config/GateConfig.class]: Unsatisfied dependency expressed through method 'clouddriverServiceSelector' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'clouddriverService' defined in class path resource [com/netflix/spinnaker/gate/config/GateConfig.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.netflix.spinnaker.gate.services.internal.ClouddriverService]: Factory method 'clouddriverService' threw exception; nested exception is com.netflix.spinnaker.kork.exceptions.SystemException: No service client provider found for url (http://localhost:7002) at app//org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800) at app//org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) at app//org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) at app//org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at app//org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at app//org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) at app//org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1389) at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1309) at app//org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:656) ... 104 more Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'clouddriverServiceSelector' defined in class path resource [com/netflix/spinnaker/gate/config/GateConfig.class]: Unsatisfied dependency expressed through method 'clouddriverServiceSelector' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'clouddriverService' defined in class path resource [com/netflix/spinnaker/gate/config/GateConfig.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.netflix.spinnaker.gate.services.internal.ClouddriverService]: Factory method 'clouddriverService' threw exception; nested exception is com.netflix.spinnaker.kork.exceptions.SystemException: No service client provider found for url (http://localhost:7002) at app//org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800) at app//org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:541) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1352) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1195) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) at app//org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) at app//org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at app//org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at app//org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) at app//org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1389) at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1309) at app//org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887) at app//org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) ... 117 more Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'clouddriverService' defined in class path resource [com/netflix/spinnaker/gate/config/GateConfig.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.netflix.spinnaker.gate.services.internal.ClouddriverService]: Factory method 'clouddriverService' threw exception; nested exception is com.netflix.spinnaker.kork.exceptions.SystemException: No service client provider found for url (http://localhost:7002) at app//org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:658) at app//org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:486) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1352) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1195) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) at app//org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) at app//org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at app//org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at app//org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) at app//org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1389) at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1309) at app//org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887) at app//org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) ... 131 more Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.netflix.spinnaker.gate.services.internal.ClouddriverService]: Factory method 'clouddriverService' threw exception; nested exception is com.netflix.spinnaker.kork.exceptions.SystemException: No service client provider found for url (http://localhost:7002) at app//org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185) at app//org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:653) ... 145 more Caused by: com.netflix.spinnaker.kork.exceptions.SystemException: No service client provider found for url (http://localhost:7002) at app//com.netflix.spinnaker.config.DefaultServiceClientProvider.lambda$findProvider$1(DefaultServiceClientProvider.java:64) at java.base@11.0.10/java.util.Optional.orElseThrow(Optional.java:408) at app//com.netflix.spinnaker.config.DefaultServiceClientProvider.findProvider(DefaultServiceClientProvider.java:61) at app//com.netflix.spinnaker.config.DefaultServiceClientProvider.getService(DefaultServiceClientProvider.java:53) at app//com.netflix.spinnaker.gate.config.GateConfig.buildService(GateConfig.groovy:365) at app//com.netflix.spinnaker.gate.config.GateConfig.buildService(GateConfig.groovy) at app//com.netflix.spinnaker.gate.config.GateConfig.createClient(GateConfig.groovy:335) at app//com.netflix.spinnaker.gate.config.GateConfig.createClient(GateConfig.groovy) at app//com.netflix.spinnaker.gate.config.GateConfig.clouddriverService(GateConfig.groovy:217) at app//com.netflix.spinnaker.gate.config.GateConfig$$EnhancerBySpringCGLIB$$12dec0b7.CGLIB$clouddriverService$36() at app//com.netflix.spinnaker.gate.config.GateConfig$$EnhancerBySpringCGLIB$$12dec0b7$$FastClassBySpringCGLIB$$c2c329d3.invoke() at app//org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244) at app//org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331) at app//com.netflix.spinnaker.gate.config.GateConfig$$EnhancerBySpringCGLIB$$12dec0b7.clouddriverService() at java.base@11.0.10/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base@11.0.10/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base@11.0.10/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base@11.0.10/java.lang.reflect.Method.invoke(Method.java:566) at app//org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ... 146 more --- .../netflix/spinnaker/gate/FunctionalSpec.groovy | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy index a1283d522a..dbb29e04f8 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy @@ -72,6 +72,21 @@ class FunctionalSpec extends Specification { ConfigurableApplicationContext ctx + static Properties origProperties; + + void setupSpec() { + origProperties = System.getProperties(); + Properties copy = new Properties(); + copy.putAll(origProperties); + System.setProperties(copy); + } + + def cleanupSpec() { + if (origProperties != null) { + System.setProperties(origProperties) + } + } + void setup() { applicationService = Mock(ApplicationService) orcaServiceSelector = Mock(OrcaServiceSelector) From 317cbb02ed9c872833e217290a483cb4fdff86fd Mon Sep 17 00:00:00 2001 From: christosarvanitis Date: Thu, 23 May 2024 15:56:48 +0300 Subject: [PATCH 182/182] refactor(taskService): Adding a scheduled executor to retrieve the task status --- .../spinnaker/gate/services/TaskService.java | 54 ++++++++++++++----- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/TaskService.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/TaskService.java index 8b09b5bd8f..07c22f7b08 100644 --- a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/TaskService.java +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/TaskService.java @@ -21,6 +21,11 @@ import com.netflix.spinnaker.gate.services.internal.OrcaServiceSelector; import com.netflix.spinnaker.security.AuthenticatedRequest; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import javax.annotation.PreDestroy; import lombok.Data; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +42,8 @@ public class TaskService { private ClouddriverServiceSelector clouddriverServiceSelector; private TaskServiceProperties taskServiceProperties; + private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + @Autowired public TaskService( OrcaServiceSelector orcaServiceSelector, @@ -47,6 +54,11 @@ public TaskService( this.taskServiceProperties = taskServiceProperties; } + @PreDestroy + protected void shutdown() { + scheduler.shutdown(); + } + public Map create(Map body) { if (body.containsKey("application")) { AuthenticatedRequest.setApplication(body.get("application").toString()); @@ -111,20 +123,38 @@ public Map createAndWaitForCompletion(Map body, int maxPolls, int intervalMs) { LinkedHashMap map = new LinkedHashMap(1); map.put("id", taskId); Map task = map; - for (int i = 0; i < maxPolls; i++) { - try { - Thread.sleep(intervalMs); - } catch (InterruptedException ignored) { - } - task = getTask(taskId); - if (new ArrayList<>(Arrays.asList("SUCCEEDED", "TERMINAL")) - .contains((String) task.get("status"))) { - return task; - } - } + CompletableFuture> pollerTask = new CompletableFuture<>(); + Runnable poller = + new Runnable() { + private int polls = 0; + + @Override + public void run() { + if (polls >= maxPolls) { + pollerTask.complete(task); + return; + } + polls++; + Map currentTask = getTask(taskId); + if (Arrays.asList("SUCCEEDED", "TERMINAL").contains(currentTask.get("status"))) { + pollerTask.complete(currentTask); + } else if (polls >= maxPolls) { + pollerTask.complete(currentTask); + } + } + }; + + scheduler.scheduleAtFixedRate( + poller, 0, intervalMs, java.util.concurrent.TimeUnit.MILLISECONDS); - return task; + try { + return pollerTask.get(); + } catch (InterruptedException | ExecutionException e) { + log.error("Error while waiting for task completion", e); + Thread.currentThread().interrupt(); + return task; + } } public Map createAndWaitForCompletion(Map body, int maxPolls) {