Skip to content

Commit c85509d

Browse files
committed
Merge branch 'release/1.1.1'
2 parents 67477c5 + 78160ff commit c85509d

24 files changed

+374
-46
lines changed

pom.xml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55

66
<groupId>eu.openanalytics</groupId>
77
<artifactId>containerproxy</artifactId>
8-
<version>1.1.0</version>
8+
<version>1.1.1</version>
99
<name>ContainerProxy</name>
1010
<packaging>jar</packaging>
1111

1212
<parent>
1313
<groupId>org.springframework.boot</groupId>
1414
<artifactId>spring-boot-starter-parent</artifactId>
15-
<version>3.2.2</version>
15+
<version>3.2.6</version>
1616
<relativePath/>
1717
</parent>
1818

@@ -21,7 +21,7 @@
2121
<java.version>17</java.version>
2222
<maven.compiler.source>17</maven.compiler.source>
2323
<maven.compiler.target>17</maven.compiler.target>
24-
<spring-boot.version>3.2.2</spring-boot.version>
24+
<spring-boot.version>3.2.6</spring-boot.version>
2525
<aws.java.sdk.version>2.23.12</aws.java.sdk.version>
2626
</properties>
2727

@@ -178,12 +178,17 @@
178178
<dependency>
179179
<groupId>commons-io</groupId>
180180
<artifactId>commons-io</artifactId>
181-
<version>2.15.1</version>
181+
<version>2.16.1</version>
182182
</dependency>
183183
<dependency>
184184
<groupId>org.springdoc</groupId>
185185
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
186-
<version>2.3.0</version>
186+
<version>2.5.0</version>
187+
</dependency>
188+
<dependency>
189+
<groupId>org.springdoc</groupId>
190+
<artifactId>springdoc-openapi-starter-common</artifactId>
191+
<version>2.5.0</version>
187192
</dependency>
188193
<dependency>
189194
<groupId>net.logstash.logback</groupId>
@@ -262,7 +267,7 @@
262267
<dependency>
263268
<groupId>org.postgresql</groupId>
264269
<artifactId>postgresql</artifactId>
265-
<version>42.7.1</version>
270+
<version>42.7.3</version>
266271
</dependency>
267272
<dependency>
268273
<groupId>com.mysql</groupId>
@@ -352,11 +357,6 @@
352357
<artifactId>lombok</artifactId>
353358
<scope>provided</scope>
354359
</dependency>
355-
<dependency>
356-
<groupId>org.springdoc</groupId>
357-
<artifactId>springdoc-openapi-starter-common</artifactId>
358-
<version>2.3.0</version>
359-
</dependency>
360360

361361
<dependency>
362362
<groupId>jakarta.mail</groupId>

src/main/java/eu/openanalytics/containerproxy/ContainerProxyApplication.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
package eu.openanalytics.containerproxy;
2222

2323
import com.fasterxml.jackson.datatype.jsr353.JSR353Module;
24+
import eu.openanalytics.containerproxy.backend.dispatcher.proxysharing.ProxySharingDispatcher;
25+
import eu.openanalytics.containerproxy.model.spec.ProxySpec;
2426
import eu.openanalytics.containerproxy.service.hearbeat.ActiveProxiesService;
2527
import eu.openanalytics.containerproxy.service.hearbeat.HeartbeatService;
2628
import eu.openanalytics.containerproxy.service.hearbeat.IHeartbeatProcessor;
29+
import eu.openanalytics.containerproxy.spec.IProxySpecProvider;
2730
import eu.openanalytics.containerproxy.util.LoggingConfigurer;
2831
import eu.openanalytics.containerproxy.util.ProxyMappingManager;
2932
import io.undertow.Handlers;
@@ -106,13 +109,19 @@ public class ContainerProxyApplication {
106109
private DefaultCookieSerializer defaultCookieSerializer;
107110
@Autowired(required = false)
108111
private SessionManagerFactory sessionManagerFactory;
112+
@Inject
113+
private IProxySpecProvider proxySpecProvider;
109114

110115
public static void main(String[] args) {
111116
SpringApplication app = new SpringApplication(ContainerProxyApplication.class);
112117

113118
app.addListeners(new LoggingConfigurer());
114119

115-
boolean hasExternalConfig = Files.exists(Paths.get(CONFIG_FILENAME));
120+
boolean hasExternalConfig = Files.exists(Paths.get(CONFIG_FILENAME))
121+
|| System.getProperty("spring.config.location") != null
122+
|| System.getenv("SPRING_CONFIG_LOCATION") != null
123+
|| Arrays.asList(args).contains("--spring.config.location");
124+
116125
if (!hasExternalConfig) {
117126
app.setAdditionalProfiles(CONFIG_DEMO_PROFILE);
118127
Logger log = LogManager.getLogger(ContainerProxyApplication.class);
@@ -243,6 +252,14 @@ public void init() {
243252
log.warn("WARNING: Invalid configuration detected: user sessions are stored in Redis and App Recovery is enabled. Instead of using App Recovery, change store-mode so that app sessions are stored in Redis!");
244253
}
245254
}
255+
if (environment.getProperty(PROPERTY_RECOVER_RUNNING_PROXIES, Boolean.class, false) ||
256+
environment.getProperty(PROPERTY_RECOVER_RUNNING_PROXIES_FROM_DIFFERENT_CONFIG, Boolean.class, false)) {
257+
for (ProxySpec proxySpec : proxySpecProvider.getSpecs()) {
258+
if (ProxySharingDispatcher.supportSpec(proxySpec)) {
259+
throw new IllegalStateException("Cannot use App Recovery together with container pre-initialization or sharing");
260+
}
261+
}
262+
}
246263

247264
boolean hideSpecDetails = environment.getProperty(PROP_API_SECURITY_HIDE_SPEC_DETAILS, Boolean.class, true);
248265
if (!hideSpecDetails) {

src/main/java/eu/openanalytics/containerproxy/RedisStoreConfiguration.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,6 @@ public RedisTemplate<String, OAuth2AuthorizedClient> oAuth2AuthorizedClientRedis
200200
return template;
201201
}
202202

203-
@Bean
204-
public RedisTemplate<String, String> unClaimSeatIdsTemplate(RedisConnectionFactory connectionFactory) {
205-
return createRedisTemplate(connectionFactory, String.class);
206-
}
207-
208203
@Bean
209204
public RedisTemplate<String, Seat> seatsTemplate(RedisConnectionFactory connectionFactory) {
210205
return createRedisTemplate(connectionFactory, Seat.class);

src/main/java/eu/openanalytics/containerproxy/api/ProxyStatusController.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import eu.openanalytics.containerproxy.service.AsyncProxyService;
3636
import eu.openanalytics.containerproxy.service.InvalidParametersException;
3737
import eu.openanalytics.containerproxy.service.ProxyService;
38+
import eu.openanalytics.containerproxy.service.UserService;
3839
import io.swagger.v3.oas.annotations.Operation;
3940
import io.swagger.v3.oas.annotations.Parameter;
4041
import io.swagger.v3.oas.annotations.media.Content;
@@ -66,6 +67,8 @@ public class ProxyStatusController {
6667
private ProxyService proxyService;
6768
@Inject
6869
private AsyncProxyService asyncProxyService;
70+
@Inject
71+
private UserService userService;
6972

7073
@Operation(
7174
summary = "Change the status of a proxy.", tags = "ContainerProxy",
@@ -74,7 +77,7 @@ public class ProxyStatusController {
7477
mediaType = "application/json",
7578
schema = @Schema(implementation = ChangeProxyStatusDto.class),
7679
examples = {
77-
@ExampleObject(name = "Stopping", description = "Stop a proxy.", value = "{\"status\": \"Stopping\"}"),
80+
@ExampleObject(name = "Stopping", description = "Stop a proxy (allowed by admins).", value = "{\"status\": \"Stopping\"}"),
7881
@ExampleObject(name = "Pausing", description = "Pause a proxy.", value = "{\"status\": \"Pausing\"}"),
7982
@ExampleObject(name = "Resuming", description = "Resume a proxy.", value = "{\"status\": \"Resuming\"}"),
8083
@ExampleObject(name = "Resuming with parameters", description = "Resume a proxy.", value = "{\"status\": \"Resuming\", \"parameters\":{\"resources\":\"2 CPU cores - 8G RAM\"," +
@@ -115,10 +118,14 @@ public class ProxyStatusController {
115118
@ResponseBody
116119
@RequestMapping(value = "/api/proxy/{proxyId}/status", method = RequestMethod.PUT)
117120
public ResponseEntity<ApiResponse<Void>> changeProxyStatus(@PathVariable String proxyId, @RequestBody ChangeProxyStatusDto changeProxyStateDto) {
118-
Proxy proxy = proxyService.getUserProxy(proxyId);
121+
Proxy proxy = proxyService.getProxy(proxyId);
119122
if (proxy == null) {
120123
return ApiResponse.failForbidden();
121124
}
125+
if (!userService.isOwner(proxy) && !(changeProxyStateDto.getStatus().equals("Stopping") && userService.isAdmin())) {
126+
// admin is allowed to stop app
127+
return ApiResponse.failForbidden();
128+
}
122129

123130
try {
124131
switch (changeProxyStateDto.getStatus()) {

src/main/java/eu/openanalytics/containerproxy/auth/impl/SAMLAuthenticationBackend.java

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import eu.openanalytics.containerproxy.auth.IAuthenticationBackend;
2424
import eu.openanalytics.containerproxy.auth.impl.saml.AuthenticationFailureHandler;
25+
import eu.openanalytics.containerproxy.auth.impl.saml.DisableSaml2LogoutRequestFilterFilter;
2526
import eu.openanalytics.containerproxy.auth.impl.saml.Saml2MetadataFilter;
2627
import eu.openanalytics.containerproxy.util.ContextPathHelper;
2728
import jakarta.servlet.http.HttpServletRequest;
@@ -40,13 +41,15 @@
4041
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
4142
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
4243
import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
44+
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
4345
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver;
4446
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
4547
import org.springframework.security.web.authentication.logout.LogoutFilter;
4648
import org.springframework.security.web.util.matcher.AndRequestMatcher;
4749
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
4850
import org.springframework.security.web.util.matcher.RequestMatcher;
4951
import org.springframework.stereotype.Component;
52+
import org.springframework.web.filter.CorsFilter;
5053

5154
import javax.inject.Inject;
5255

@@ -117,11 +120,12 @@ public void configureHttpSecurity(HttpSecurity http) throws Exception {
117120
.authenticationManager(new ProviderManager(samlAuthenticationProvider))
118121
.failureHandler(failureHandler)
119122
.successHandler(successHandler))
120-
.saml2Logout(saml -> saml
121-
.logoutUrl(SAML_LOGOUT_SERVICE_LOCATION_PATH)
122-
.logoutResponse(r -> r.logoutUrl(SAML_LOGOUT_SERVICE_RESPONSE_LOCATION_PATH))
123-
.logoutRequest(r -> r.logoutRequestResolver(saml2LogoutRequestResolver))
124-
.addObjectPostProcessor(
123+
.saml2Logout(saml -> {
124+
saml.logoutUrl(SAML_LOGOUT_SERVICE_LOCATION_PATH)
125+
.logoutResponse(r -> r.logoutUrl(SAML_LOGOUT_SERVICE_RESPONSE_LOCATION_PATH))
126+
.logoutRequest(r -> r.logoutRequestResolver(saml2LogoutRequestResolver));
127+
128+
saml.addObjectPostProcessor(
125129
new ObjectPostProcessor<LogoutFilter>() {
126130
@Override
127131
public <O extends LogoutFilter> O postProcess(O object) {
@@ -132,8 +136,21 @@ public <O extends LogoutFilter> O postProcess(O object) {
132136
return object;
133137
}
134138
}
135-
))
136-
.addFilterBefore(metadataFilter, Saml2WebSsoAuthenticationFilter.class);
139+
);
140+
141+
saml.addObjectPostProcessor(
142+
new ObjectPostProcessor<Saml2LogoutRequestFilter>() {
143+
@Override
144+
public <O extends Saml2LogoutRequestFilter> O postProcess(O object) {
145+
// override the name of the filter, so it can be used in DisableSaml2LogoutRequestFilterFilter
146+
// See #33066.
147+
object.setBeanName("Saml2LogoutRequestFilter");
148+
return object;
149+
}
150+
});
151+
})
152+
.addFilterBefore(metadataFilter, Saml2WebSsoAuthenticationFilter.class)
153+
.addFilterAfter(new DisableSaml2LogoutRequestFilterFilter(), CorsFilter.class);
137154
}
138155

139156
@Override

src/main/java/eu/openanalytics/containerproxy/auth/impl/oidc/OpenIDConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
3838
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
3939
import org.springframework.security.oauth2.core.AuthorizationGrantType;
40+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
4041
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
4142
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
4243

@@ -88,6 +89,7 @@ public ClientRegistrationRepository clientRegistrationRepository() {
8889
.clientId(environment.getProperty("proxy.openid.client-id"))
8990
.clientSecret(environment.getProperty("proxy.openid.client-secret"))
9091
.userInfoUri(environment.getProperty("proxy.openid.userinfo-url"))
92+
.clientAuthenticationMethod(environment.getProperty("proxy.openid.client-authentication-method", ClientAuthenticationMethod.class))
9193
.build();
9294

9395
return new InMemoryClientRegistrationRepository(Collections.singletonList(client));
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2024 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.auth.impl.saml;
22+
23+
import eu.openanalytics.containerproxy.auth.impl.SAMLAuthenticationBackend;
24+
import jakarta.servlet.FilterChain;
25+
import jakarta.servlet.ServletException;
26+
import jakarta.servlet.ServletRequest;
27+
import jakarta.servlet.ServletResponse;
28+
import jakarta.servlet.http.HttpServletRequest;
29+
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
30+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
31+
import org.springframework.security.web.util.matcher.OrRequestMatcher;
32+
import org.springframework.security.web.util.matcher.RequestMatcher;
33+
import org.springframework.web.filter.GenericFilterBean;
34+
35+
import java.io.IOException;
36+
37+
/**
38+
* Filter that disables the {@link Saml2LogoutRequestFilter} filter, except for the SAML single logout endpoint.
39+
* The SAML filter calls getParameter and thefore consumes the POST body.
40+
* The name of {@link Saml2LogoutRequestFilter must be fixed in order for this to work (see {@link SAMLAuthenticationBackend}
41+
* See #33066.
42+
*/
43+
public class DisableSaml2LogoutRequestFilterFilter extends GenericFilterBean {
44+
45+
private static final RequestMatcher REQUEST_MATCHER = new OrRequestMatcher(
46+
new AntPathRequestMatcher("/logout/saml2/slo"),
47+
new AntPathRequestMatcher("/logout/saml2/slo/*")
48+
);
49+
50+
@Override
51+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
52+
if (!REQUEST_MATCHER.matches((HttpServletRequest) request)) {
53+
// set the filtered as already being executed, this is the only way to disable the filter
54+
request.setAttribute("Saml2LogoutRequestFilter.FILTERED", true);
55+
}
56+
chain.doFilter(request, response);
57+
}
58+
59+
}

src/main/java/eu/openanalytics/containerproxy/backend/dispatcher/ProxyDispatcherService.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131
import org.springframework.stereotype.Service;
3232

3333
import javax.annotation.PostConstruct;
34+
import javax.annotation.PreDestroy;
35+
import java.util.ArrayList;
3436
import java.util.HashMap;
37+
import java.util.List;
3538
import java.util.Map;
3639

3740
@Service
@@ -42,6 +45,7 @@ public class ProxyDispatcherService {
4245
private final IProxySharingStoreFactory storeFactory;
4346
private final ConfigurableListableBeanFactory beanFactory;
4447
private final DefaultProxyDispatcher defaultProxyDispatcher;
48+
private final List<AutoCloseable> closeables = new ArrayList<>();
4549

4650
public ProxyDispatcherService(IProxySpecProvider proxySpecProvider,
4751
IProxySharingStoreFactory storeFactory,
@@ -62,6 +66,7 @@ public void init() {
6266

6367
ProxySharingScaler proxySharingScaler = createProxySharingScaler(seatStore, proxySpec, delegateProxyStore);
6468
createBean(proxySharingScaler, "proxySharingScaler_" + proxySpec.getId());
69+
closeables.add(proxySharingScaler);
6570

6671
ProxySharingDispatcher proxySharingDispatcher = new ProxySharingDispatcher(proxySpec, delegateProxyStore, seatStore);
6772
createBean(proxySharingDispatcher, "proxySharingDispatcher_" + proxySpec.getId());
@@ -87,4 +92,12 @@ private <T> void createBean(T bean, String beanName) {
8792
beanFactory.registerSingleton(beanName, initializedBean);
8893
}
8994

95+
@PreDestroy
96+
public void shutdown() throws Exception {
97+
// when using beanFactory.registerSingleton, @PreDestroy is not called
98+
for (AutoCloseable closeable : closeables) {
99+
closeable.close();
100+
}
101+
}
102+
90103
}

0 commit comments

Comments
 (0)