Skip to content

Commit 8e930f4

Browse files
Marco Bergencesmarvin
Marco Bergen
authored andcommittedMar 13, 2025·
Merge branch 'release/v1.7.7-1'
2 parents 1e8ba81 + 3e179bf commit 8e930f4

28 files changed

+437
-89
lines changed
 

‎CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [v1.7.7-1] - 2025-03-13
11+
### Changed
12+
- [#240] Change error pages to show the new design
13+
- [#240] Remove ticket query param if invalid instead of showing an error page
14+
- Update makefiles to 9.6.0
15+
- Update ces-build-lib to 4.1.0
16+
- Update dogu-build-lib to 3.1.0
17+
1018
## [v1.7.6-2] - 2025-02-12
1119
### Changed
1220
- [#238] Add missing keys to dogu.json

‎Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ RUN set -x \
1818

1919
FROM registry.cloudogu.com/official/java:21.0.5-1
2020
LABEL NAME="official/smeagol" \
21-
VERSION="1.7.6-2" \
21+
VERSION="1.7.7-1" \
2222
maintainer="hello@cloudogu.com"
2323

2424
ENV SERVICE_TAGS=webapp \

‎Jenkinsfile

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!groovy
2-
@Library(['github.com/cloudogu/ces-build-lib@2.2.1', 'github.com/cloudogu/dogu-build-lib@v2.3.1'])
2+
@Library(['github.com/cloudogu/ces-build-lib@4.1.0', 'github.com/cloudogu/dogu-build-lib@v3.1.0'])
33
import com.cloudogu.ces.cesbuildlib.*
44
import com.cloudogu.ces.dogubuildlib.*
55

@@ -15,7 +15,6 @@ GitHub github = new GitHub(this, git)
1515
Changelog changelog = new Changelog(this)
1616

1717
EcoSystem ecoSystem = new EcoSystem(this, "gcloud-ces-operations-internal-packer", "jenkins-gcloud-ces-operations-internal")
18-
Trivy trivy = new Trivy(this, ecoSystem)
1918

2019
parallel(
2120
"source code": {
@@ -112,6 +111,8 @@ parallel(
112111
booleanParam(defaultValue: true, description: 'Enables cypress to take screenshots of failing integration tests.', name: 'EnableScreenshotRecording'),
113112
booleanParam(defaultValue: false, description: 'Test dogu upgrade from latest release or optionally from defined version below', name: 'TestDoguUpgrade'),
114113
string(defaultValue: '', description: 'Old Dogu version for the upgrade test (optional; e.g. 2.222.1-1)', name: 'OldDoguVersionForUpgradeTest'),
114+
choice(name: 'TrivySeverityLevels', choices: [TrivySeverityLevel.CRITICAL, TrivySeverityLevel.HIGH_AND_ABOVE, TrivySeverityLevel.MEDIUM_AND_ABOVE, TrivySeverityLevel.ALL], description: 'The levels to scan with trivy', defaultValue: TrivySeverityLevel.CRITICAL),
115+
choice(name: 'TrivyStrategy', choices: [TrivyScanStrategy.UNSTABLE, TrivyScanStrategy.FAIL, TrivyScanStrategy.IGNORE], description: 'Define whether the build should be unstable, fail or whether the error should be ignored if any vulnerability was found.', defaultValue: TrivyScanStrategy.UNSTABLE)
115116
])
116117
])
117118

@@ -143,9 +144,12 @@ parallel(
143144
}
144145

145146
stage('Trivy scan') {
146-
trivy.scanDogu("/dogu", TrivyScanFormat.HTML, TrivyScanLevel.CRITICAL, TrivyScanStrategy.UNSTABLE)
147-
trivy.scanDogu("/dogu", TrivyScanFormat.JSON, TrivyScanLevel.CRITICAL, TrivyScanStrategy.UNSTABLE)
148-
trivy.scanDogu("/dogu", TrivyScanFormat.PLAIN, TrivyScanLevel.CRITICAL, TrivyScanStrategy.UNSTABLE)
147+
ecoSystem.copyDoguImageToJenkinsWorker("/dogu")
148+
Trivy trivy = new Trivy(this)
149+
trivy.scanDogu(".", params.TrivySeverityLevels, params.TrivyStrategy)
150+
trivy.saveFormattedTrivyReport(TrivyScanFormat.TABLE)
151+
trivy.saveFormattedTrivyReport(TrivyScanFormat.JSON)
152+
trivy.saveFormattedTrivyReport(TrivyScanFormat.HTML)
149153
}
150154

151155
stage('Verify') {

‎Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
MAKEFILES_VERSION=9.5.0
1+
MAKEFILES_VERSION=9.6.0
22

33
.DEFAULT_GOAL:=dogu-release
44

‎build/make/k8s.mk

+3-3
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ K3S_LOCAL_REGISTRY_PORT?=30099
3636

3737
# The URL of the container-registry to use. Defaults to the registry of the local-cluster.
3838
# If RUNTIME_ENV is "remote" it is "registry.cloudogu.com/testing"
39-
CES_REGISTRY_HOST?="${K3S_CLUSTER_FQDN}:${K3S_LOCAL_REGISTRY_PORT}"
39+
CES_REGISTRY_HOST?=${K3S_CLUSTER_FQDN}:${K3S_LOCAL_REGISTRY_PORT}
4040
CES_REGISTRY_NAMESPACE ?=
4141
ifeq (${RUNTIME_ENV}, remote)
42-
CES_REGISTRY_HOST="registry.cloudogu.com"
43-
CES_REGISTRY_NAMESPACE="/testing"
42+
CES_REGISTRY_HOST=registry.cloudogu.com
43+
CES_REGISTRY_NAMESPACE=/testing
4444
endif
4545
$(info CES_REGISTRY_HOST=$(CES_REGISTRY_HOST))
4646

‎build/make/prerelease.mk

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33

44
.PHONY: prerelease_namespace
55
prerelease_namespace:
6-
build/make/stagex.sh prerelease_namespace
6+
build/make/prerelease.sh prerelease_namespace

‎build/make/prerelease.sh

100644100755
+10-2
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,29 @@ set -o pipefail
55

66
prerelease_namespace() {
77

8+
TIMESTAMP=$(date +"%Y%m%d%H%M%S")
9+
810
# Update version in dogu.json
911
if [ -f "dogu.json" ]; then
1012
echo "Updating name in dogu.json..."
1113
ORIG_NAME="$(jq -r ".Name" ./dogu.json)"
14+
ORIG_VERSION="$(jq -r ".Version" ./dogu.json)"
1215
PRERELEASE_NAME="prerelease_${ORIG_NAME}"
16+
PRERELEASE_VERSION="${ORIG_VERSION}${TIMESTAMP}"
1317
jq ".Name = \"${PRERELEASE_NAME}\"" dogu.json >dogu2.json && mv dogu2.json dogu.json
18+
jq ".Version = \"${PRERELEASE_VERSION}\"" dogu.json >dogu2.json && mv dogu2.json dogu.json
1419
jq ".Image = \"registry.cloudogu.com/${PRERELEASE_NAME}\"" dogu.json >dogu2.json && mv dogu2.json dogu.json
1520
fi
1621

1722
# Update version in Dockerfile
1823
if [ -f "Dockerfile" ]; then
1924
echo "Updating version in Dockerfile..."
20-
ORIG_NAME="$(grep -oP "^[ ]*NAME=\"([^\"]*)" Dockerfile | awk -F "\"" '{print $2}')"
25+
ORIG_NAME="$(grep -oP ".*[ ]*NAME=\"([^\"]*)" Dockerfile | awk -F "\"" '{print $2}')"
26+
ORIG_VERSION="$(grep -oP ".*[ ]*VERSION=\"([^\"]*)" Dockerfile | awk -F "\"" '{print $2}')"
2127
PRERELEASE_NAME="prerelease_$( echo -e "$ORIG_NAME" | sed 's/\//\\\//g' )"
22-
sed -i "s/\(^[ ]*NAME=\"\)\([^\"]*\)\(.*$\)/\1${PRERELEASE_NAME}\3/" Dockerfile
28+
PRERELEASE_VERSION="${ORIG_VERSION}${TIMESTAMP}"
29+
sed -i "s/\(.*[ ]*NAME=\"\)\([^\"]*\)\(.*$\)/\1${PRERELEASE_NAME}\3/" Dockerfile
30+
sed -i "s/\(.*[ ]*VERSION=\"\)\([^\"]*\)\(.*$\)/\1${PRERELEASE_VERSION}\3/" Dockerfile
2331
fi
2432

2533
}

‎build/make/release.mk

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
.PHONY: dogu-release
66
dogu-release: ## Start a dogu release
7-
build/make/release.sh dogu
7+
build/make/release.sh dogu "${FIXED_CVE_LIST}" $(DRY_RUN)
88

99
.PHONY: node-release
1010
node-release: ## Start a node package release

‎build/make/self-update.mk

+6-1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,9 @@ copy-new-files:
2424
.PHONY: update-build-libs
2525
update-build-libs:
2626
@echo "Check for newer Build-Lib versions"
27-
build/make/self-update.sh buildlibs
27+
build/make/self-update.sh buildlibs
28+
29+
.PHONY: set-dogu-version
30+
set-dogu-version:
31+
@echo "Set Version of Dogu without Release"
32+
build/make/self-update.sh versions

‎build/make/self-update.sh

+16-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ set -o errexit
33
set -o nounset
44
set -o pipefail
55

6+
7+
# shellcheck disable=SC1090
8+
source "$(pwd)/build/make/release_functions.sh"
9+
610
TYPE="${1}"
711

812
update_build_libs() {
@@ -34,12 +38,23 @@ get_highest_version() {
3438
# Patch Jenkinsfile
3539
update_jenkinsfile() {
3640
sed -i "s/ces-build-lib@[[:digit:]].[[:digit:]].[[:digit:]]/ces-build-lib@$(get_highest_version ces)/g" Jenkinsfile
37-
sed -i "s/dugu-build-lib@[[:digit:]].[[:digit:]].[[:digit:]]/dogu-build-lib@$(get_highest_version dogu)/g" Jenkinsfile
41+
sed -i "s/dogu-build-lib@v[[:digit:]].[[:digit:]].[[:digit:]]/dogu-build-lib@v$(get_highest_version dogu)/g" Jenkinsfile
42+
}
43+
44+
# Patch Dogu Version without Release
45+
set_dogu_version() {
46+
CURRENT_TOOL_VERSION=$(get_current_version_by_dogu_json)
47+
echo "$(tput setaf 1)ATTENTION: Make sure that the new version corresponds to the current software version$(tput sgr0)"
48+
NEW_RELEASE_VERSION="$(read_new_version)"
49+
validate_new_version "${NEW_RELEASE_VERSION}"
50+
update_versions "${NEW_RELEASE_VERSION}"
3851
}
3952

4053
# switch for script entrypoint
4154
if [[ "${TYPE}" == "buildlibs" ]];then
4255
update_build_libs
56+
elif [[ "${TYPE}" == "versions" ]];then
57+
set_dogu_version
4358
else
4459
echo "Unknown target ${TYPE}"
4560
fi

‎build/make/test-common.mk

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
GO_JUNIT_REPORT=$(UTILITY_BIN_PATH)/go-junit-report
2-
GO_JUNIT_REPORT_VERSION=v1.0.0
2+
GO_JUNIT_REPORT_VERSION=v2.1.0
33

44
$(GO_JUNIT_REPORT): $(UTILITY_BIN_PATH)
55
@echo "Download go-junit-report..."
6-
@$(call go-get-tool,$@,github.com/jstemmer/go-junit-report@$(GO_JUNIT_REPORT_VERSION))
6+
@$(call go-get-tool,$@,github.com/jstemmer/go-junit-report/v2@$(GO_JUNIT_REPORT_VERSION))

‎build/make/test-unit.mk

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
##@ Unit testing
22

33
UNIT_TEST_DIR=$(TARGET_DIR)/unit-tests
4+
XUNIT_JSON=$(UNIT_TEST_DIR)/report.json
45
XUNIT_XML=$(UNIT_TEST_DIR)/unit-tests.xml
56
UNIT_TEST_LOG=$(UNIT_TEST_DIR)/unit-tests.log
67
COVERAGE_REPORT=$(UNIT_TEST_DIR)/coverage.out
@@ -9,9 +10,9 @@ PRE_UNITTESTS?=
910
POST_UNITTESTS?=
1011

1112
.PHONY: unit-test
12-
unit-test: $(XUNIT_XML) ## Start unit tests
13+
unit-test: $(XUNIT_JSON) ## Start unit tests
1314

14-
$(XUNIT_XML): $(SRC) $(GO_JUNIT_REPORT)
15+
$(XUNIT_JSON): $(SRC) $(GO_JUNIT_REPORT)
1516
ifneq ($(strip $(PRE_UNITTESTS)),)
1617
@make $(PRE_UNITTESTS)
1718
endif
@@ -20,13 +21,15 @@ endif
2021
@echo 'mode: set' > ${COVERAGE_REPORT}
2122
@rm -f $(UNIT_TEST_LOG) || true
2223
@for PKG in $(PACKAGES) ; do \
23-
${GO_CALL} test -v $$PKG -coverprofile=${COVERAGE_REPORT}.tmp 2>&1 | tee $(UNIT_TEST_LOG).tmp ; \
24+
${GO_CALL} test -v $$PKG -coverprofile=${COVERAGE_REPORT}.tmp -json 2>&1 | tee $(UNIT_TEST_LOG).tmp ; \
2425
cat ${COVERAGE_REPORT}.tmp | tail +2 >> ${COVERAGE_REPORT} ; \
2526
rm -f ${COVERAGE_REPORT}.tmp ; \
2627
cat $(UNIT_TEST_LOG).tmp >> $(UNIT_TEST_LOG) ; \
2728
rm -f $(UNIT_TEST_LOG).tmp ; \
2829
done
29-
@cat $(UNIT_TEST_LOG) | $(GO_JUNIT_REPORT) > $@
30+
@cat $(UNIT_TEST_LOG) >> $@
31+
@cat $(UNIT_TEST_LOG) | $(GO_JUNIT_REPORT) -parser gojson > $(XUNIT_XML)
32+
3033
@if grep '^FAIL' $(UNIT_TEST_LOG); then \
3134
exit 1; \
3235
fi

‎docs/gui/release_notes_de.md

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ Technische Details zu einem Release finden Sie im zugehörigen [Changelog](https
66

77
## [Unreleased]
88

9+
## [v1.7.7-1] - 2025-03-13
10+
11+
- Das Design der Fehlerseiten wurde überarbeitet
12+
- Es wurde ein Fehler behoben, der aufgetreten ist, wenn ein ungültiges CAS-Service-Ticket verwendet wurde
13+
914
## [v1.7.6-2] - 2025-02-12
1015

1116
Wir haben nur technische Änderungen vorgenommen. Näheres finden Sie in den Changelogs.

‎docs/gui/release_notes_en.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ Technical details on a release can be found in the corresponding [Changelog](htt
66

77
## [Unreleased]
88

9+
## [v1.7.7-1] - 2025-03-13
10+
11+
- The design of the error pages has been revised
12+
- Fixed an error that occurred when an invalid CAS service ticket was used
13+
914
## [v1.7.6-2] - 2025-02-12
1015

1116
We have only made technical changes. You can find more details in the changelogs.
1217

13-
1418
## [v1.7.6-1] - 2025-01-10
1519
**The release fixes a critical security vulnerability ([CVE-2024-56337](https://github.com/advisories/GHSA-27hp-xhwr-wr2m)). An update is therefore recommended.**
1620

‎dogu.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"Name": "official/smeagol",
3-
"Version": "1.7.6-2",
3+
"Version": "1.7.7-1",
44
"DisplayName": "Smeagol",
55
"Description": "Store your technical documentation with in your git repositories",
66
"Category": "Development Apps",

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "smeagol",
3-
"version": "1.7.6-2",
3+
"version": "1.7.7-1",
44
"private": true,
55
"license": "AGPL-3.0-only",
66
"dependencies": {

‎pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
<groupId>com.cloudogu.wiki</groupId>
1414
<artifactId>smeagol</artifactId>
15-
<version>1.7.6-2</version>
15+
<version>1.7.7-1</version>
1616
<name>smeagol</name>
1717
<packaging>war</packaging>
1818

‎resources/app/application.yml.tpl

+2
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ scm:
88
cas:
99
url: https://{{ .Env.Get "FQDN" }}/cas
1010
serviceUrl: https://{{ .Env.Get "FQDN" }}
11+
errors:
12+
url: https://{{ .Env.Get "FQDN" }}/errors/
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,55 @@
11
package com.cloudogu.smeagol;
22

3+
import com.cloudogu.smeagol.authc.infrastructure.NginxErrorPageNotFoundException;
34
import jakarta.servlet.RequestDispatcher;
45
import jakarta.servlet.http.HttpServletRequest;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.beans.factory.annotation.Value;
58
import org.springframework.boot.web.servlet.error.ErrorController;
69
import org.springframework.http.HttpStatus;
710
import org.springframework.stereotype.Controller;
811
import org.springframework.web.bind.annotation.*;
12+
import java.io.BufferedReader;
13+
import java.io.IOException;
14+
import java.io.InputStreamReader;
15+
import java.net.HttpURLConnection;
16+
import java.net.URI;
17+
import java.net.URISyntaxException;
18+
import java.net.URL;
919

1020

1121
@Controller
1222
public class CustomErrorController implements ErrorController {
1323

24+
public static class HttpClientHelper {
25+
public HttpURLConnection createConnection(String urlString) throws IOException, URISyntaxException {
26+
final URL url = new URI(urlString).toURL();
27+
return (HttpURLConnection) url.openConnection();
28+
}
29+
}
30+
31+
private final String errorsUrl;
32+
private final HttpClientHelper helper;
33+
34+
CustomErrorController() {
35+
this("");
36+
}
37+
38+
@Autowired
39+
public CustomErrorController(
40+
@Value("${errors.url}") String errorsUrl
41+
) {
42+
this(errorsUrl, new HttpClientHelper());
43+
}
44+
45+
CustomErrorController(
46+
String errorsUrl,
47+
HttpClientHelper helper
48+
) {
49+
this.errorsUrl = errorsUrl;
50+
this.helper = helper;
51+
}
52+
1453
/**
1554
* Mapping for errors which are not properly handled with in the application
1655
*/
@@ -30,48 +69,34 @@ public String handleError(HttpServletRequest request) {
3069
if (message != null) {
3170
errorMessage = message.toString();
3271
}
33-
return renderErrorTemplate(statusCode, reasonPhrase, errorMessage, request.getContextPath());
72+
return renderErrorTemplate(statusCode, reasonPhrase, errorMessage);
3473
}
3574
return "";
3675
}
3776

38-
public String getErrorPath() {
39-
return "/error";
40-
}
41-
4277
@SuppressWarnings("squid:S1192") // sonar issue not relevant for this template
43-
private String renderErrorTemplate(int statusCode, String reasonPhrase, String message, String contextPath) {
44-
return String.format("<html>\n" +
45-
"<head>\n" +
46-
" <meta charset='utf-8'>\n" +
47-
" <title>Error</title>\n" +
48-
" <meta name='viewport' content='width=device-width, initial-scale=1'>\n" +
49-
" <meta http-equiv='X-UA-Compatible' content='IE=edge' />\n" +
50-
" <link rel='stylesheet' href='%4$s/static/error/errors.css'>\n" +
51-
" <!-- favicons -->\n" +
52-
" <link rel='icon' type='image/png' href='%4$s/favicon-64px.png' sizes='64x64' />\n" +
53-
" <link rel='icon' type='image/png' href='%4$s/favicon-32px.png' sizes='32x32' />\n" +
54-
" <link rel='icon' type='image/png' href='%4$s/favicon-16px.png' sizes='16x16' />\n" +
55-
"</head>\n" +
56-
"<body>\n" +
57-
" <div class='logo'>\n" +
58-
" <img src='%4$s/static/logo-white.png'>\n" +
59-
" </div>\n" +
60-
" <div class='message'>\n" +
61-
" <div class='code'>\n" +
62-
" %1$s\n" +
63-
" </div>\n" +
64-
" <div class='description'>\n" +
65-
" %2$s\n" +
66-
" </div>\n" +
67-
" <div class='error'>\n" +
68-
" %3$s\n " +
69-
" </div>\n" +
70-
" </div>\n" +
71-
" <div class='background'>\n" +
72-
" <img src='%4$s/static/clockwork.png'>\n" +
73-
" </div>\n" +
74-
"</body>\n" +
75-
"</html>", statusCode, reasonPhrase, message, contextPath);
78+
private String renderErrorTemplate(int statusCode, String reasonPhrase, String message) {
79+
final String urlString = errorsUrl + statusCode + ".html";
80+
81+
try {
82+
final HttpURLConnection connection = this.helper.createConnection(urlString);
83+
connection.setRequestMethod("GET");
84+
connection.setConnectTimeout(5000);
85+
connection.setReadTimeout(5000);
86+
87+
final int responseCode = connection.getResponseCode();
88+
if (responseCode == 200) {
89+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
90+
final StringBuilder response = new StringBuilder();
91+
String line;
92+
while ((line = reader.readLine()) != null) {
93+
response.append(line).append("\n");
94+
}
95+
return response.toString();
96+
}
97+
} else throw new NginxErrorPageNotFoundException();
98+
} catch (IOException | URISyntaxException | NginxErrorPageNotFoundException e) {
99+
return "<html><body><h1>Error " + statusCode + "</h1><p>" + reasonPhrase + "</p><p>" + message + "</p></body></html>";
100+
}
76101
}
77102
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.cloudogu.smeagol.authc.infrastructure;
2+
3+
import com.cloudogu.smeagol.ScmHttpClient;
4+
import jakarta.servlet.*;
5+
import org.apereo.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
9+
import java.io.IOException;
10+
11+
public class CESCas30ProxyReceivingTicketValidationFilter implements Filter {
12+
private static final Logger LOG = LoggerFactory.getLogger(CESCas30ProxyReceivingTicketValidationFilter.class);
13+
private final Cas30ProxyReceivingTicketValidationFilter original;
14+
15+
public CESCas30ProxyReceivingTicketValidationFilter() {
16+
super();
17+
this.original = new CasFilterWithRedirect();
18+
}
19+
20+
@Override
21+
public void init(FilterConfig filterConfig) throws ServletException {
22+
this.original.init(filterConfig);
23+
this.original.setExceptionOnValidationFailure(false);
24+
}
25+
26+
@Override
27+
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
28+
try {
29+
this.original.doFilter(servletRequest, servletResponse, filterChain);
30+
}
31+
catch (TicketEmptyException e){
32+
// Ignore that exception. The exception is just thrown to prevent normal filter flow after the redirect is called
33+
LOG.debug("A user has used an invalid cas service ticket which was removed from url.");
34+
}
35+
}
36+
37+
@Override
38+
public void destroy() {
39+
this.original.destroy();
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.cloudogu.smeagol.authc.infrastructure;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import jakarta.servlet.http.HttpServletResponse;
5+
import org.apereo.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter;
6+
7+
import java.io.IOException;
8+
import java.util.Arrays;
9+
import java.util.stream.Collectors;
10+
11+
public class CasFilterWithRedirect extends Cas30ProxyReceivingTicketValidationFilter {
12+
public CasFilterWithRedirect() {
13+
super();
14+
}
15+
16+
@Override
17+
protected void onFailedValidation(HttpServletRequest request, HttpServletResponse response) {
18+
try {
19+
final String requestUrl = request.getRequestURL().toString();
20+
final String queryString = request.getQueryString();
21+
22+
String newQuery = "";
23+
if (queryString != null) {
24+
newQuery = Arrays.stream(queryString.split("&"))
25+
.filter(param -> !param.startsWith("ticket="))
26+
.collect(Collectors.joining("&"));
27+
}
28+
29+
final String newUrl = requestUrl + (newQuery.isEmpty() ? "" : "?" + newQuery);
30+
response.sendRedirect(newUrl);
31+
throw new TicketEmptyException();
32+
} catch (IOException e) {
33+
throw new RuntimeException(e);
34+
}
35+
}
36+
}

‎src/main/java/com/cloudogu/smeagol/authc/infrastructure/CasInfrastructureRegistration.java

+3-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import org.apereo.cas.client.session.SingleSignOutFilter;
88
import org.apereo.cas.client.session.SingleSignOutHttpSessionListener;
99
import org.apereo.cas.client.util.HttpServletRequestWrapperFilter;
10-
import org.apereo.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter;
1110
import org.slf4j.Logger;
1211
import org.slf4j.LoggerFactory;
1312
import org.springframework.beans.factory.annotation.Autowired;
@@ -28,7 +27,7 @@ public class CasInfrastructureRegistration {
2827

2928
private static final Logger LOG = LoggerFactory.getLogger(CasInfrastructureRegistration.class);
3029

31-
private Map<String,String> casSettings;
30+
private Map<String, String> casSettings;
3231

3332
@Autowired
3433
public CasInfrastructureRegistration(CasConfiguration configuration, Stage stage) {
@@ -69,7 +68,7 @@ public FilterRegistrationBean singleSignOutFilter() {
6968
*/
7069
@Bean
7170
public FilterRegistrationBean proxyReceivingTicketValidationFilter() {
72-
return casFilterRegistration(new Cas30ProxyReceivingTicketValidationFilter(), 1);
71+
return casFilterRegistration(new CESCas30ProxyReceivingTicketValidationFilter(), 1);
7372
}
7473

7574
/**
@@ -93,7 +92,7 @@ public FilterRegistrationBean requestWrapperFilter() {
9392
return casFilterRegistration(new HttpServletRequestWrapperFilter(), 3);
9493
}
9594

96-
private FilterRegistrationBean casFilterRegistration(Filter filter, int order){
95+
private FilterRegistrationBean casFilterRegistration(Filter filter, int order) {
9796
FilterRegistrationBean registration = new FilterRegistrationBean();
9897
registration.setInitParameters(casSettings);
9998
registration.setFilter(filter);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.cloudogu.smeagol.authc.infrastructure;
2+
3+
public class NginxErrorPageNotFoundException extends Exception{
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.cloudogu.smeagol.authc.infrastructure;
2+
3+
public class TicketEmptyException extends RuntimeException{
4+
}

‎src/main/resources/application.yml

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ ui:
1717
scm:
1818
url: https://192.168.56.2/scm
1919

20+
errors:
21+
url: https://192.168.56.2/errors/
2022

2123
cas:
2224
url: https://192.168.56.2/cas

‎src/test/java/com/cloudogu/smeagol/CustomErrorControllerTest.java

+65-21
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,21 @@
1111
import org.mockito.Spy;
1212
import org.mockito.junit.MockitoJUnitRunner;
1313
import org.springframework.http.HttpStatus;
14-
import static org.mockito.Mockito.doReturn;
14+
15+
import java.io.ByteArrayInputStream;
16+
import java.io.IOException;
17+
import java.net.HttpURLConnection;
18+
import java.net.URISyntaxException;
19+
20+
import static org.mockito.Mockito.*;
1521
import static org.mockito.MockitoAnnotations.openMocks;
1622

1723
@RunWith(MockitoJUnitRunner.class)
1824
public class CustomErrorControllerTest {
1925

26+
@Mock
27+
private CustomErrorController.HttpClientHelper httpClientHelper;
28+
2029
@Spy
2130
private CustomErrorController customErrorController;
2231

@@ -28,64 +37,99 @@ public class CustomErrorControllerTest {
2837
@Before
2938
public void init() {
3039
openMocks(this);
40+
customErrorController = new CustomErrorController("http://errors.example.com/", httpClientHelper);
3141
}
3242

3343
@Test
34-
public void testErrorPageHandling_BAD_REQUEST() {
35-
HttpStatus expectedError = HttpStatus.BAD_REQUEST;
44+
public void testErrorPageHandling_fetchSuccess_BAD_REQUEST() throws IOException, URISyntaxException {
45+
HttpURLConnection mockConnection = mock(HttpURLConnection.class);
46+
when(this.httpClientHelper.createConnection(anyString())).thenReturn(mockConnection);
47+
when(mockConnection.getResponseCode()).thenReturn(200);
48+
when(mockConnection.getInputStream()).thenReturn(new ByteArrayInputStream("Mocked Response".getBytes()));
3649

3750
doReturn(HttpStatus.BAD_REQUEST.value()).when(requestMock)
38-
.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
51+
.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
3952

4053
Object errorMessage = "SOMETHING THAT PRODUCED A 400 ERROR";
4154
doReturn(errorMessage).when(requestMock)
42-
.getAttribute(RequestDispatcher.ERROR_MESSAGE);
55+
.getAttribute(RequestDispatcher.ERROR_MESSAGE);
56+
57+
String responseTemplate = customErrorController.handleError(requestMock);
58+
59+
Assert.assertThat(responseTemplate, CoreMatchers.containsString("Mocked Response"));
60+
}
61+
62+
@Test
63+
public void testErrorPageHandling_fetchFail_BAD_REQUEST() throws IOException, URISyntaxException {
64+
HttpURLConnection mockConnection = mock(HttpURLConnection.class);
65+
when(this.httpClientHelper.createConnection(anyString())).thenReturn(mockConnection);
66+
when(mockConnection.getResponseCode()).thenReturn(404);
67+
68+
HttpStatus expectedError = HttpStatus.BAD_REQUEST;
69+
70+
doReturn(HttpStatus.BAD_REQUEST.value()).when(requestMock)
71+
.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
4372

44-
doReturn(mockedContextPath).when(requestMock)
45-
.getContextPath();
73+
Object errorMessage = "SOMETHING THAT PRODUCED A 400 ERROR";
74+
doReturn(errorMessage).when(requestMock)
75+
.getAttribute(RequestDispatcher.ERROR_MESSAGE);
4676

4777
String responseTemplate = customErrorController.handleError(requestMock);
4878

49-
//check for HTTP status code (400), reasonPhrase(Bad Gateway) and error message (errorMessage)
5079
Assert.assertThat(responseTemplate, CoreMatchers.containsString(Integer.toString(expectedError.value())));
5180
Assert.assertThat(responseTemplate, CoreMatchers.containsString(expectedError.getReasonPhrase()));
5281
Assert.assertThat(responseTemplate, CoreMatchers.containsString(errorMessage.toString()));
53-
//check for correct resource path (should be /static see ProductionDispatcher)
54-
Assert.assertThat(responseTemplate, CoreMatchers.containsString(mockedContextPath + "/static/clockwork.png"));
55-
Assert.assertThat(responseTemplate, CoreMatchers.containsString(mockedContextPath + "/static/logo-white.png"));
5682
}
5783

5884
@Test
59-
public void testErrorPageHandling_INTERNAL_SERVER_ERROR() {
85+
public void testErrorPageHandling_fetchSuccess_INTERNAL_SERVER_ERROR() throws IOException, URISyntaxException {
86+
HttpURLConnection mockConnection = mock(HttpURLConnection.class);
87+
when(this.httpClientHelper.createConnection(anyString())).thenReturn(mockConnection);
88+
when(mockConnection.getResponseCode()).thenReturn(200);
89+
when(mockConnection.getInputStream()).thenReturn(new ByteArrayInputStream("Mocked Response".getBytes()));
90+
6091
HttpStatus expectedError = HttpStatus.INTERNAL_SERVER_ERROR;
6192

6293
doReturn(HttpStatus.INTERNAL_SERVER_ERROR.value()).when(requestMock)
63-
.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
94+
.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
6495

6596
Object errorMessage = "SOMETHING THAT PRODUCED A 500 ERROR";
6697
doReturn(errorMessage).when(requestMock)
67-
.getAttribute(RequestDispatcher.ERROR_MESSAGE);
98+
.getAttribute(RequestDispatcher.ERROR_MESSAGE);
99+
100+
String responseTemplate = customErrorController.handleError(requestMock);
101+
102+
Assert.assertThat(responseTemplate, CoreMatchers.containsString("Mocked Response"));
103+
}
68104

69-
doReturn(mockedContextPath).when(requestMock)
70-
.getContextPath();
105+
@Test
106+
public void testErrorPageHandling_fetchFail_INTERNAL_SERVER_ERROR() throws IOException, URISyntaxException {
107+
HttpURLConnection mockConnection = mock(HttpURLConnection.class);
108+
when(this.httpClientHelper.createConnection(anyString())).thenReturn(mockConnection);
109+
when(mockConnection.getResponseCode()).thenReturn(404);
110+
111+
HttpStatus expectedError = HttpStatus.INTERNAL_SERVER_ERROR;
112+
113+
doReturn(HttpStatus.INTERNAL_SERVER_ERROR.value()).when(requestMock)
114+
.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
115+
116+
Object errorMessage = "SOMETHING THAT PRODUCED A 500 ERROR";
117+
doReturn(errorMessage).when(requestMock)
118+
.getAttribute(RequestDispatcher.ERROR_MESSAGE);
71119

72120
String responseTemplate = customErrorController.handleError(requestMock);
73121

74-
//check for HTTP status code (500), reasonPhrase(Internal Server Error) and error message (errorMessage)
75122
Assert.assertThat(responseTemplate, CoreMatchers.containsString(Integer.toString(expectedError.value())));
76123
Assert.assertThat(responseTemplate, CoreMatchers.containsString(expectedError.getReasonPhrase()));
77124
Assert.assertThat(responseTemplate, CoreMatchers.containsString(errorMessage.toString()));
78-
//check for correct resource path (should be /static see ProductionDispatcher)
79-
Assert.assertThat(responseTemplate, CoreMatchers.containsString(mockedContextPath + "/static/clockwork.png"));
80-
Assert.assertThat(responseTemplate, CoreMatchers.containsString(mockedContextPath + "/static/logo-white.png"));
81125
}
82126

83127
@Test
84128

85129
public void testErrorPageHandling_Status_Undefined() {
86130

87131
doReturn(null).when(requestMock)
88-
.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
132+
.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
89133

90134
String responseTemplate = customErrorController.handleError(requestMock);
91135

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.cloudogu.smeagol.authc.infrastructure;
2+
3+
import jakarta.servlet.*;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import jakarta.servlet.http.HttpServletResponse;
6+
import org.apereo.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter;
7+
import org.junit.Assert;
8+
import org.junit.Test;
9+
import org.mockito.ArgumentCaptor;
10+
import org.mockito.Mockito;
11+
12+
import java.io.IOException;
13+
import java.lang.reflect.Field;
14+
15+
public class CESCas30ProxyReceivingTicketValidationFilterTest {
16+
17+
@Test
18+
public void testInit() {
19+
CESCas30ProxyReceivingTicketValidationFilter filter = new CESCas30ProxyReceivingTicketValidationFilter();
20+
21+
FilterConfig mockFilterConfig = Mockito.mock(FilterConfig.class);
22+
Cas30ProxyReceivingTicketValidationFilter mockValidationFilter = Mockito.mock(Cas30ProxyReceivingTicketValidationFilter.class);
23+
24+
try {
25+
Field field = CESCas30ProxyReceivingTicketValidationFilter.class.getDeclaredField("original");
26+
field.setAccessible(true);
27+
field.set(filter, mockValidationFilter);
28+
29+
ArgumentCaptor<Boolean> captor = ArgumentCaptor.forClass(Boolean.class);
30+
31+
filter.init(mockFilterConfig);
32+
33+
Mockito.verify(mockValidationFilter).setExceptionOnValidationFailure(captor.capture());
34+
35+
Assert.assertFalse(captor.getValue());
36+
} catch (ServletException | NoSuchFieldException | IllegalAccessException e) {
37+
Assert.fail("Unexcpected ServletException: " + e.getMessage());
38+
}
39+
}
40+
41+
@Test
42+
public void testDoFilter() {
43+
CESCas30ProxyReceivingTicketValidationFilter filter = new CESCas30ProxyReceivingTicketValidationFilter();
44+
45+
Cas30ProxyReceivingTicketValidationFilter mockValidationFilter = Mockito.mock(Cas30ProxyReceivingTicketValidationFilter.class);
46+
47+
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
48+
HttpServletResponse mockResp = Mockito.mock(HttpServletResponse.class);
49+
FilterChain mockFilterChain = Mockito.mock(FilterChain.class);
50+
51+
try {
52+
Field field = CESCas30ProxyReceivingTicketValidationFilter.class.getDeclaredField("original");
53+
field.setAccessible(true);
54+
field.set(filter, mockValidationFilter);
55+
filter.doFilter(mockReq, mockResp, mockFilterChain);
56+
57+
// Test if parameters are passed to the super class
58+
Mockito.verify(mockValidationFilter).doFilter(mockReq, mockResp, mockFilterChain);
59+
60+
Mockito.doThrow(new TicketEmptyException()).when(mockValidationFilter).doFilter(mockReq, mockResp, mockFilterChain);
61+
62+
try {
63+
filter.doFilter(mockReq, mockResp, mockFilterChain);
64+
} catch (TicketEmptyException e) {
65+
Assert.fail("This exception should be swallowed by CESCas30ProxyReceivingTicketValidationFilter: " + e.getMessage());
66+
}
67+
} catch (ServletException | NoSuchFieldException | IllegalAccessException | IOException e) {
68+
Assert.fail("Unexcpected ServletException: " + e.getMessage());
69+
}
70+
}
71+
72+
@Test
73+
public void testDoDestroy() {
74+
CESCas30ProxyReceivingTicketValidationFilter filter = new CESCas30ProxyReceivingTicketValidationFilter();
75+
76+
Cas30ProxyReceivingTicketValidationFilter mockValidationFilter = Mockito.mock(Cas30ProxyReceivingTicketValidationFilter.class);
77+
78+
try {
79+
Field field = CESCas30ProxyReceivingTicketValidationFilter.class.getDeclaredField("original");
80+
field.setAccessible(true);
81+
field.set(filter, mockValidationFilter);
82+
83+
filter.destroy();
84+
85+
Mockito.verify(mockValidationFilter).destroy();
86+
87+
} catch (NoSuchFieldException | IllegalAccessException e) {
88+
Assert.fail("Unexcpected ServletException: " + e.getMessage());
89+
}
90+
}
91+
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.cloudogu.smeagol.authc.infrastructure;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import jakarta.servlet.http.HttpServletResponse;
5+
6+
import org.junit.Assert;
7+
import org.junit.Test;
8+
import org.mockito.ArgumentCaptor;
9+
import org.mockito.Mockito;
10+
11+
import java.io.IOException;
12+
13+
public class CasFilterWithRedirectTest {
14+
15+
@Test
16+
public void testOnFailedValidation() throws IOException {
17+
CasFilterWithRedirect filter = new CasFilterWithRedirect();
18+
19+
HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
20+
HttpServletResponse mockResp = Mockito.mock(HttpServletResponse.class);
21+
22+
Mockito.when(mockReq.getRequestURL()).thenReturn(new StringBuffer("https://invalidurl/validation/"));
23+
Mockito.when(mockReq.getQueryString()).thenReturn("hallo=welt&ticket=12345&lorem=ipsum");
24+
25+
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
26+
27+
try {
28+
filter.onFailedValidation(mockReq, mockResp);
29+
Assert.fail("Should have thrown exception");
30+
} catch (TicketEmptyException e) {
31+
Mockito.verify(mockResp).sendRedirect(captor.capture());
32+
Assert.assertEquals(captor.getValue(), "https://invalidurl/validation/?hallo=welt&lorem=ipsum");
33+
}
34+
35+
Mockito.doThrow((new IOException())).when(mockResp).sendRedirect(Mockito.any());
36+
try {
37+
filter.onFailedValidation(mockReq, mockResp);
38+
Assert.fail("Should have thrown exception");
39+
} catch (RuntimeException e) {
40+
// this exception should be normal
41+
}
42+
43+
44+
}
45+
46+
47+
}

0 commit comments

Comments
 (0)
Please sign in to comment.