Add nullability annotations to module/spring-boot-cloudfoundry

See gh-46587
This commit is contained in:
Moritz Halbritter
2025-07-30 10:40:17 +02:00
parent 1b6aa9e036
commit 071f012d33
16 changed files with 93 additions and 43 deletions
@@ -16,6 +16,8 @@
package org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpStatus;
/**
@@ -28,11 +30,11 @@ public class CloudFoundryAuthorizationException extends RuntimeException {
private final Reason reason;
public CloudFoundryAuthorizationException(Reason reason, String message) {
public CloudFoundryAuthorizationException(Reason reason, @Nullable String message) {
this(reason, message, null);
}
public CloudFoundryAuthorizationException(Reason reason, String message, Throwable cause) {
public CloudFoundryAuthorizationException(Reason reason, @Nullable String message, @Nullable Throwable cause) {
super(message, cause);
this.reason = reason;
}
@@ -18,6 +18,8 @@ package org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.EndpointExposureOutcomeContributor;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter;
@@ -38,7 +40,7 @@ class CloudFoundryEndpointExposureOutcomeContributor implements EndpointExposure
private static final String PROPERTY = "management.endpoints.cloud-foundry.exposure";
private final IncludeExcludeEndpointFilter<?> filter;
private final @Nullable IncludeExcludeEndpointFilter<?> filter;
CloudFoundryEndpointExposureOutcomeContributor(Environment environment) {
this.filter = (!CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) ? null
@@ -46,7 +48,7 @@ class CloudFoundryEndpointExposureOutcomeContributor implements EndpointExposure
}
@Override
public ConditionOutcome getExposureOutcome(EndpointId endpointId, Set<EndpointExposure> exposures,
public @Nullable ConditionOutcome getExposureOutcome(EndpointId endpointId, Set<EndpointExposure> exposures,
Builder message) {
if (exposures.contains(EndpointExposure.WEB) && this.filter != null && this.filter.match(endpointId)) {
return ConditionOutcome.match(message.because("marked as exposed by a '" + PROPERTY + "' property"));
@@ -19,6 +19,8 @@ package org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint;
import java.util.Collection;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
@@ -60,7 +62,7 @@ public class CloudFoundryWebEndpointDiscoverer extends WebEndpointDiscoverer {
*/
public CloudFoundryWebEndpointDiscoverer(ApplicationContext applicationContext,
ParameterValueMapper parameterValueMapper, EndpointMediaTypes endpointMediaTypes,
List<PathMapper> endpointPathMappers, Collection<OperationInvokerAdvisor> invokerAdvisors,
@Nullable List<PathMapper> endpointPathMappers, Collection<OperationInvokerAdvisor> invokerAdvisors,
Collection<EndpointFilter<ExposableWebEndpoint>> endpointFilters,
Collection<OperationFilter<WebOperation>> operationFilters) {
super(applicationContext, parameterValueMapper, endpointMediaTypes, endpointPathMappers, null, invokerAdvisors,
@@ -89,7 +91,7 @@ public class CloudFoundryWebEndpointDiscoverer extends WebEndpointDiscoverer {
static class CloudFoundryWebEndpointDiscovererRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
hints.reflection()
.registerType(CloudFoundryEndpointFilter.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
@@ -16,6 +16,8 @@
package org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpStatus;
/**
@@ -28,13 +30,13 @@ public class SecurityResponse {
private final HttpStatus status;
private final String message;
private final @Nullable String message;
public SecurityResponse(HttpStatus status) {
this(status, null);
}
public SecurityResponse(HttpStatus status, String message) {
public SecurityResponse(HttpStatus status, @Nullable String message) {
this.status = status;
this.message = message;
}
@@ -43,7 +45,7 @@ public class SecurityResponse {
return this.status;
}
public String getMessage() {
public @Nullable String getMessage() {
return this.message;
}
@@ -17,4 +17,7 @@
/**
* Auto-configuration for Cloud Foundry endpoints.
*/
@NullMarked
package org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint;
import org.jspecify.annotations.NullMarked;
@@ -23,6 +23,8 @@ import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanPostProcessor;
@@ -128,12 +130,13 @@ public final class CloudFoundryReactiveActuatorAutoConfiguration {
private SecurityInterceptor getSecurityInterceptor(WebClient.Builder webClientBuilder, Environment environment) {
SecurityService cloudfoundrySecurityService = getCloudFoundrySecurityService(webClientBuilder, environment);
TokenValidator tokenValidator = new TokenValidator(cloudfoundrySecurityService);
TokenValidator tokenValidator = (cloudfoundrySecurityService != null)
? new TokenValidator(cloudfoundrySecurityService) : null;
return new SecurityInterceptor(tokenValidator, cloudfoundrySecurityService,
environment.getProperty("vcap.application.application_id"));
}
private SecurityService getCloudFoundrySecurityService(WebClient.Builder webClientBuilder,
private @Nullable SecurityService getCloudFoundrySecurityService(WebClient.Builder webClientBuilder,
Environment environment) {
String cloudControllerUrl = environment.getProperty("vcap.application.cf_api");
boolean skipSslValidation = environment.getProperty("management.cloudfoundry.skip-ssl-validation",
@@ -22,6 +22,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@@ -111,7 +112,7 @@ class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointH
});
}
private Map<String, Link> getAccessibleLinks(AccessLevel accessLevel, Map<String, Link> links) {
private Map<String, Link> getAccessibleLinks(@Nullable AccessLevel accessLevel, Map<String, Link> links) {
if (accessLevel == null) {
return new LinkedHashMap<>();
}
@@ -169,7 +170,7 @@ class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointH
private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
this.reflectiveRegistrar.registerRuntimeHints(hints, CloudFoundryLinksHandler.class);
this.bindingRegistrar.registerReflectionHints(hints.reflection(), Link.class);
}
@@ -20,6 +20,7 @@ import java.util.Locale;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;
import org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint.CloudFoundryAuthorizationException;
@@ -28,6 +29,7 @@ import org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint.Secu
import org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint.Token;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.server.ServerWebExchange;
@@ -41,16 +43,16 @@ class SecurityInterceptor {
private static final Log logger = LogFactory.getLog(SecurityInterceptor.class);
private final TokenValidator tokenValidator;
private final @Nullable TokenValidator tokenValidator;
private final SecurityService cloudFoundrySecurityService;
private final @Nullable SecurityService cloudFoundrySecurityService;
private final String applicationId;
private final @Nullable String applicationId;
private static final Mono<SecurityResponse> SUCCESS = Mono.just(SecurityResponse.success());
SecurityInterceptor(TokenValidator tokenValidator, SecurityService cloudFoundrySecurityService,
String applicationId) {
SecurityInterceptor(@Nullable TokenValidator tokenValidator, @Nullable SecurityService cloudFoundrySecurityService,
@Nullable String applicationId) {
this.tokenValidator = tokenValidator;
this.cloudFoundrySecurityService = cloudFoundrySecurityService;
this.applicationId = applicationId;
@@ -65,7 +67,7 @@ class SecurityInterceptor {
return Mono.error(new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
"Application id is not available"));
}
if (this.cloudFoundrySecurityService == null) {
if (this.cloudFoundrySecurityService == null || this.tokenValidator == null) {
return Mono.error(new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
"Cloud controller URL is not available"));
}
@@ -77,6 +79,9 @@ class SecurityInterceptor {
}
private Mono<Void> check(ServerWebExchange exchange, String id) {
Assert.state(this.tokenValidator != null, "'tokenValidator' must not be null");
Assert.state(this.cloudFoundrySecurityService != null, "'cloudFoundrySecurityService' must not be null");
Assert.state(this.applicationId != null, "'applicationId' must not be null");
try {
Token token = getToken(exchange.getRequest());
return this.tokenValidator.validate(token)
@@ -135,7 +135,9 @@ class SecurityService {
private Map<String, String> extractTokenKeys(Map<String, Object> response) {
Map<String, String> tokenKeys = new HashMap<>();
for (Object key : (List<?>) response.get("keys")) {
List<?> keys = (List<?>) response.get("keys");
Assert.state(keys != null, "'keys' must not be null");
for (Object key : keys) {
Map<?, ?> tokenKey = (Map<?, ?>) key;
tokenKeys.put((String) tokenKey.get("kid"), (String) tokenKey.get("value"));
}
@@ -17,4 +17,7 @@
/**
* Auto-configuration for Cloud Foundry WebFlux endpoints.
*/
@NullMarked
package org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint.reactive;
import org.jspecify.annotations.NullMarked;
@@ -22,6 +22,8 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
@@ -134,12 +136,13 @@ public final class CloudFoundryActuatorAutoConfiguration {
private SecurityInterceptor getSecurityInterceptor(RestTemplateBuilder restTemplateBuilder,
Environment environment) {
SecurityService cloudfoundrySecurityService = getCloudFoundrySecurityService(restTemplateBuilder, environment);
TokenValidator tokenValidator = new TokenValidator(cloudfoundrySecurityService);
TokenValidator tokenValidator = (cloudfoundrySecurityService != null)
? new TokenValidator(cloudfoundrySecurityService) : null;
return new SecurityInterceptor(tokenValidator, cloudfoundrySecurityService,
environment.getProperty("vcap.application.application_id"));
}
private SecurityService getCloudFoundrySecurityService(RestTemplateBuilder restTemplateBuilder,
private @Nullable SecurityService getCloudFoundrySecurityService(RestTemplateBuilder restTemplateBuilder,
Environment environment) {
String cloudControllerUrl = environment.getProperty("vcap.application.cf_api");
boolean skipSslValidation = environment.getProperty("management.cloudfoundry.skip-ssl-validation",
@@ -26,6 +26,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
import org.springframework.aot.hint.RuntimeHints;
@@ -172,7 +173,7 @@ class CloudFoundryWebEndpointServletHandlerMapping extends AbstractWebMvcEndpoin
private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
this.reflectiveRegistrar.registerRuntimeHints(hints, CloudFoundryLinksHandler.class);
this.bindingRegistrar.registerReflectionHints(hints.reflection(), Link.class);
}
@@ -21,6 +21,7 @@ import java.util.Locale;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.actuate.endpoint.EndpointId;
import org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint.AccessLevel;
@@ -30,6 +31,7 @@ import org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint.Secu
import org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint.Token;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsUtils;
@@ -42,22 +44,22 @@ class SecurityInterceptor {
private static final Log logger = LogFactory.getLog(SecurityInterceptor.class);
private final TokenValidator tokenValidator;
private final @Nullable TokenValidator tokenValidator;
private final SecurityService cloudFoundrySecurityService;
private final @Nullable SecurityService cloudFoundrySecurityService;
private final String applicationId;
private final @Nullable String applicationId;
private static final SecurityResponse SUCCESS = SecurityResponse.success();
SecurityInterceptor(TokenValidator tokenValidator, SecurityService cloudFoundrySecurityService,
String applicationId) {
SecurityInterceptor(@Nullable TokenValidator tokenValidator, @Nullable SecurityService cloudFoundrySecurityService,
@Nullable String applicationId) {
this.tokenValidator = tokenValidator;
this.cloudFoundrySecurityService = cloudFoundrySecurityService;
this.applicationId = applicationId;
}
SecurityResponse preHandle(HttpServletRequest request, EndpointId endpointId) {
SecurityResponse preHandle(HttpServletRequest request, @Nullable EndpointId endpointId) {
if (CorsUtils.isPreFlightRequest(request)) {
return SecurityResponse.success();
}
@@ -66,7 +68,7 @@ class SecurityInterceptor {
throw new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
"Application id is not available");
}
if (this.cloudFoundrySecurityService == null) {
if (this.cloudFoundrySecurityService == null || this.tokenValidator == null) {
throw new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
"Cloud controller URL is not available");
}
@@ -86,7 +88,10 @@ class SecurityInterceptor {
return SecurityResponse.success();
}
private void check(HttpServletRequest request, EndpointId endpointId) {
private void check(HttpServletRequest request, @Nullable EndpointId endpointId) {
Assert.state(this.cloudFoundrySecurityService != null, "'cloudFoundrySecurityService' must not be null");
Assert.state(this.applicationId != null, "'applicationId' must not be null");
Assert.state(this.tokenValidator != null, "'tokenValidator' must not be null");
Token token = getToken(request);
this.tokenValidator.validate(token);
AccessLevel accessLevel = this.cloudFoundrySecurityService.getAccessLevel(token.toString(), this.applicationId);
@@ -22,6 +22,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint.AccessLevel;
import org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint.CloudFoundryAuthorizationException;
import org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint.CloudFoundryAuthorizationException.Reason;
@@ -45,7 +47,7 @@ class SecurityService {
private final String cloudControllerUrl;
private String uaaUrl;
private @Nullable String uaaUrl;
SecurityService(RestTemplateBuilder restTemplateBuilder, String cloudControllerUrl, boolean skipSslValidation) {
Assert.notNull(restTemplateBuilder, "'restTemplateBuilder' must not be null");
@@ -69,7 +71,7 @@ class SecurityService {
URI uri = getPermissionsUri(applicationId);
RequestEntity<?> request = RequestEntity.get(uri).header("Authorization", "bearer " + token).build();
Map<?, ?> body = this.restTemplate.exchange(request, Map.class).getBody();
if (Boolean.TRUE.equals(body.get("read_sensitive_data"))) {
if (body != null && Boolean.TRUE.equals(body.get("read_sensitive_data"))) {
return AccessLevel.FULL;
}
return AccessLevel.RESTRICTED;
@@ -100,7 +102,9 @@ class SecurityService {
*/
Map<String, String> fetchTokenKeys() {
try {
return extractTokenKeys(this.restTemplate.getForObject(getUaaUrl() + "/token_keys", Map.class));
Map<?, ?> response = this.restTemplate.getForObject(getUaaUrl() + "/token_keys", Map.class);
Assert.state(response != null, "'response' must not be null");
return extractTokenKeys(response);
}
catch (HttpStatusCodeException ex) {
throw new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE, "UAA not reachable");
@@ -109,7 +113,9 @@ class SecurityService {
private Map<String, String> extractTokenKeys(Map<?, ?> response) {
Map<String, String> tokenKeys = new HashMap<>();
for (Object key : (List<?>) response.get("keys")) {
List<?> keys = (List<?>) response.get("keys");
Assert.state(keys != null, "'keys' must not be null");
for (Object key : keys) {
Map<?, ?> tokenKey = (Map<?, ?>) key;
tokenKeys.put((String) tokenKey.get("kid"), (String) tokenKey.get("value"));
}
@@ -124,7 +130,10 @@ class SecurityService {
if (this.uaaUrl == null) {
try {
Map<?, ?> response = this.restTemplate.getForObject(this.cloudControllerUrl + "/info", Map.class);
this.uaaUrl = (String) response.get("token_endpoint");
Assert.state(response != null, "'response' must not be null");
String tokenEndpoint = (String) response.get("token_endpoint");
Assert.state(tokenEndpoint != null, "'tokenEndpoint' must not be null");
this.uaaUrl = tokenEndpoint;
}
catch (HttpStatusCodeException ex) {
throw new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
@@ -27,9 +27,12 @@ import java.util.Base64;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint.CloudFoundryAuthorizationException;
import org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint.Token;
import org.springframework.util.Assert;
/**
* Validator used to ensure that a signed {@link Token} has not been tampered with.
@@ -40,7 +43,7 @@ class TokenValidator {
private final SecurityService securityService;
private Map<String, String> tokenKeys;
private @Nullable Map<String, String> tokenKeys;
TokenValidator(SecurityService cloudFoundrySecurityService) {
this.securityService = cloudFoundrySecurityService;
@@ -67,22 +70,23 @@ class TokenValidator {
private void validateKeyIdAndSignature(Token token) {
String keyId = token.getKeyId();
if (this.tokenKeys == null || !hasValidKeyId(keyId)) {
if (this.tokenKeys == null || !hasValidKeyId(this.tokenKeys, keyId)) {
this.tokenKeys = this.securityService.fetchTokenKeys();
if (!hasValidKeyId(keyId)) {
if (!hasValidKeyId(this.tokenKeys, keyId)) {
throw new CloudFoundryAuthorizationException(Reason.INVALID_KEY_ID,
"Key Id present in token header does not match");
}
}
if (!hasValidSignature(token, this.tokenKeys.get(keyId))) {
String key = this.tokenKeys.get(keyId);
Assert.state(key != null, "'key' must not be null");
if (!hasValidSignature(token, key)) {
throw new CloudFoundryAuthorizationException(Reason.INVALID_SIGNATURE,
"RSA Signature did not match content");
}
}
private boolean hasValidKeyId(String tokenKey) {
return this.tokenKeys.containsKey(tokenKey);
private boolean hasValidKeyId(Map<String, String> tokenKeys, String tokenKey) {
return tokenKeys.containsKey(tokenKey);
}
private boolean hasValidSignature(Token token, String key) {
@@ -17,4 +17,7 @@
/**
* Auto-configuration for Cloud Foundry Servlet endpoints.
*/
@NullMarked
package org.springframework.boot.cloudfoundry.actuate.autoconfigure.endpoint.servlet;
import org.jspecify.annotations.NullMarked;