Skip to content

Commit 3105a38

Browse files
committed
Introduce EndpointID to enforce naming rules
Add an `EndpointID` class to enforce the naming rules that we support for actuator endpoints. We now ensure that all endpoint names contain only letters and numbers and must begin with a lower-case letter. Existing public classes and interfaces have been changes so that String based `endpointId` methods are deprecated and strongly typed versions are preferred instead. A few public classes that we're not expecting to be used directly have been changed without deprecated methods being introduced. See gh-14773
1 parent c5786c2 commit 3105a38

File tree

62 files changed

+832
-291
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+832
-291
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,19 @@ public enum AccessLevel {
4040

4141
public static final String REQUEST_ATTRIBUTE = "cloudFoundryAccessLevel";
4242

43-
private final List<String> endpointIds;
43+
private final List<String> ids;
4444

45-
AccessLevel(String... endpointIds) {
46-
this.endpointIds = Arrays.asList(endpointIds);
45+
AccessLevel(String... ids) {
46+
this.ids = Arrays.asList(ids);
4747
}
4848

4949
/**
50-
* Returns if the access level should allow access to the specified endpoint path.
51-
* @param endpointId the endpoint ID to check
50+
* Returns if the access level should allow access to the specified ID.
51+
* @param id the ID to check
5252
* @return {@code true} if access is allowed
5353
*/
54-
public boolean isAccessAllowed(String endpointId) {
55-
return this.endpointIds.isEmpty() || this.endpointIds.contains(endpointId);
54+
public boolean isAccessAllowed(String id) {
55+
return this.ids.isEmpty() || this.ids.contains(id);
5656
}
5757

5858
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundrySecurityInterceptor.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class CloudFoundrySecurityInterceptor {
5959
this.applicationId = applicationId;
6060
}
6161

62-
Mono<SecurityResponse> preHandle(ServerWebExchange exchange, String endpointId) {
62+
Mono<SecurityResponse> preHandle(ServerWebExchange exchange, String id) {
6363
ServerHttpRequest request = exchange.getRequest();
6464
if (CorsUtils.isPreFlightRequest(request)) {
6565
return SUCCESS;
@@ -72,21 +72,21 @@ Mono<SecurityResponse> preHandle(ServerWebExchange exchange, String endpointId)
7272
return Mono.error(new CloudFoundryAuthorizationException(
7373
Reason.SERVICE_UNAVAILABLE, "Cloud controller URL is not available"));
7474
}
75-
return check(exchange, endpointId).then(SUCCESS).doOnError(this::logError)
75+
return check(exchange, id).then(SUCCESS).doOnError(this::logError)
7676
.onErrorResume(this::getErrorResponse);
7777
}
7878

7979
private void logError(Throwable ex) {
8080
logger.error(ex.getMessage(), ex);
8181
}
8282

83-
private Mono<Void> check(ServerWebExchange exchange, String path) {
83+
private Mono<Void> check(ServerWebExchange exchange, String id) {
8484
try {
8585
Token token = getToken(exchange.getRequest());
8686
return this.tokenValidator.validate(token)
8787
.then(this.cloudFoundrySecurityService
8888
.getAccessLevel(token.toString(), this.applicationId))
89-
.filter((accessLevel) -> accessLevel.isAccessAllowed(path))
89+
.filter((accessLevel) -> accessLevel.isAccessAllowed(id))
9090
.switchIfEmpty(Mono.error(new CloudFoundryAuthorizationException(
9191
Reason.ACCESS_DENIED, "Access denied")))
9292
.doOnSuccess((accessLevel) -> exchange.getAttributes()

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
2929
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse;
30+
import org.springframework.boot.actuate.endpoint.EndpointId;
3031
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
3132
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
3233
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@@ -70,7 +71,7 @@ class CloudFoundryWebFluxEndpointHandlerMapping
7071
protected ReactiveWebOperation wrapReactiveWebOperation(ExposableWebEndpoint endpoint,
7172
WebOperation operation, ReactiveWebOperation reactiveWebOperation) {
7273
return new SecureReactiveWebOperation(reactiveWebOperation,
73-
this.securityInterceptor, endpoint.getId());
74+
this.securityInterceptor, endpoint.getEndpointId());
7475
}
7576

7677
@Override
@@ -113,10 +114,11 @@ private static class SecureReactiveWebOperation implements ReactiveWebOperation
113114

114115
private final CloudFoundrySecurityInterceptor securityInterceptor;
115116

116-
private final String endpointId;
117+
private final EndpointId endpointId;
117118

118119
SecureReactiveWebOperation(ReactiveWebOperation delegate,
119-
CloudFoundrySecurityInterceptor securityInterceptor, String endpointId) {
120+
CloudFoundrySecurityInterceptor securityInterceptor,
121+
EndpointId endpointId) {
120122
this.delegate = delegate;
121123
this.securityInterceptor = securityInterceptor;
122124
this.endpointId = endpointId;
@@ -125,7 +127,8 @@ private static class SecureReactiveWebOperation implements ReactiveWebOperation
125127
@Override
126128
public Mono<ResponseEntity<Object>> handle(ServerWebExchange exchange,
127129
Map<String, String> body) {
128-
return this.securityInterceptor.preHandle(exchange, this.endpointId)
130+
return this.securityInterceptor
131+
.preHandle(exchange, this.endpointId.toLowerCaseString())
129132
.flatMap((securityResponse) -> flatMapResponse(exchange, body,
130133
securityResponse));
131134
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptor.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason;
2929
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse;
3030
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.Token;
31+
import org.springframework.boot.actuate.endpoint.EndpointId;
3132
import org.springframework.http.HttpMethod;
3233
import org.springframework.http.HttpStatus;
3334
import org.springframework.util.StringUtils;
@@ -59,7 +60,7 @@ class CloudFoundrySecurityInterceptor {
5960
this.applicationId = applicationId;
6061
}
6162

62-
SecurityResponse preHandle(HttpServletRequest request, String endpointId) {
63+
SecurityResponse preHandle(HttpServletRequest request, EndpointId endpointId) {
6364
if (CorsUtils.isPreFlightRequest(request)) {
6465
return SecurityResponse.success();
6566
}
@@ -90,12 +91,14 @@ SecurityResponse preHandle(HttpServletRequest request, String endpointId) {
9091
return SecurityResponse.success();
9192
}
9293

93-
private void check(HttpServletRequest request, String endpointId) throws Exception {
94+
private void check(HttpServletRequest request, EndpointId endpointId)
95+
throws Exception {
9496
Token token = getToken(request);
9597
this.tokenValidator.validate(token);
9698
AccessLevel accessLevel = this.cloudFoundrySecurityService
9799
.getAccessLevel(token.toString(), this.applicationId);
98-
if (!accessLevel.isAccessAllowed(endpointId)) {
100+
if (!accessLevel.isAccessAllowed(
101+
(endpointId != null) ? endpointId.toLowerCaseString() : "")) {
99102
throw new CloudFoundryAuthorizationException(Reason.ACCESS_DENIED,
100103
"Access denied");
101104
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
2929
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse;
30+
import org.springframework.boot.actuate.endpoint.EndpointId;
3031
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
3132
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
3233
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@@ -68,15 +69,15 @@ class CloudFoundryWebEndpointServletHandlerMapping
6869
protected ServletWebOperation wrapServletWebOperation(ExposableWebEndpoint endpoint,
6970
WebOperation operation, ServletWebOperation servletWebOperation) {
7071
return new SecureServletWebOperation(servletWebOperation,
71-
this.securityInterceptor, endpoint.getId());
72+
this.securityInterceptor, endpoint.getEndpointId());
7273
}
7374

7475
@Override
7576
@ResponseBody
7677
protected Map<String, Map<String, Link>> links(HttpServletRequest request,
7778
HttpServletResponse response) {
7879
SecurityResponse securityResponse = this.securityInterceptor.preHandle(request,
79-
"");
80+
null);
8081
if (!securityResponse.getStatus().equals(HttpStatus.OK)) {
8182
sendFailureResponse(response, securityResponse);
8283
}
@@ -115,10 +116,11 @@ private static class SecureServletWebOperation implements ServletWebOperation {
115116

116117
private final CloudFoundrySecurityInterceptor securityInterceptor;
117118

118-
private final String endpointId;
119+
private final EndpointId endpointId;
119120

120121
SecureServletWebOperation(ServletWebOperation delegate,
121-
CloudFoundrySecurityInterceptor securityInterceptor, String endpointId) {
122+
CloudFoundrySecurityInterceptor securityInterceptor,
123+
EndpointId endpointId) {
122124
this.delegate = delegate;
123125
this.securityInterceptor = securityInterceptor;
124126
this.endpointId = endpointId;

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ private boolean isExcluded(ExposableEndpoint<?> endpoint) {
110110
}
111111

112112
private boolean contains(Set<String> items, ExposableEndpoint<?> endpoint) {
113-
return items.contains(endpoint.getId().toLowerCase(Locale.ENGLISH));
113+
return items.contains(endpoint.getEndpointId().toLowerCaseString());
114114
}
115115

116116
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/DefaultEndpointObjectNameFactory.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ public ObjectName getObjectName(ExposableJmxEndpoint endpoint)
5252
throws MalformedObjectNameException {
5353
StringBuilder builder = new StringBuilder(this.properties.getDomain());
5454
builder.append(":type=Endpoint");
55-
builder.append(",name=" + StringUtils.capitalize(endpoint.getId()));
55+
builder.append(
56+
",name=" + StringUtils.capitalize(endpoint.getEndpointId().toString()));
5657
String baseName = builder.toString();
5758
if (this.mBeanServer != null && hasMBean(baseName)) {
5859
builder.append(",context=" + this.contextId);

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/MappingWebEndpointPathMapper.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.Map;
2020

21+
import org.springframework.boot.actuate.endpoint.EndpointId;
2122
import org.springframework.boot.actuate.endpoint.web.PathMapper;
2223

2324
/**
@@ -35,8 +36,14 @@ class MappingWebEndpointPathMapper implements PathMapper {
3536
}
3637

3738
@Override
39+
@Deprecated
3840
public String getRootPath(String endpointId) {
39-
return this.pathMapping.getOrDefault(endpointId, endpointId);
41+
return getRootPath(EndpointId.of(endpointId));
42+
}
43+
44+
@Override
45+
public String getRootPath(EndpointId endpointId) {
46+
return this.pathMapping.getOrDefault(endpointId, endpointId.toLowerCaseString());
4047
}
4148

4249
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3333
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
34+
import org.springframework.boot.actuate.endpoint.EndpointId;
3435
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
3536
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
3637
import org.springframework.boot.security.reactive.ApplicationContextServerWebExchangeMatcher;
@@ -208,22 +209,25 @@ private Stream<String> streamPaths(List<Object> source,
208209
.map(pathMappedEndpoints::getPath);
209210
}
210211

211-
private String getEndpointId(Object source) {
212+
private EndpointId getEndpointId(Object source) {
213+
if (source instanceof EndpointId) {
214+
return (EndpointId) source;
215+
}
212216
if (source instanceof String) {
213-
return (String) source;
217+
return (EndpointId.of((String) source));
214218
}
215219
if (source instanceof Class) {
216220
return getEndpointId((Class<?>) source);
217221
}
218222
throw new IllegalStateException("Unsupported source " + source);
219223
}
220224

221-
private String getEndpointId(Class<?> source) {
225+
private EndpointId getEndpointId(Class<?> source) {
222226
Endpoint annotation = AnnotatedElementUtils.getMergedAnnotation(source,
223227
Endpoint.class);
224228
Assert.state(annotation != null,
225229
() -> "Class " + source + " is not annotated with @Endpoint");
226-
return annotation.id();
230+
return EndpointId.of(annotation.id());
227231
}
228232

229233
private List<ServerWebExchangeMatcher> getDelegateMatchers(Set<String> paths) {

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3333
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
34+
import org.springframework.boot.actuate.endpoint.EndpointId;
3435
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
3536
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
3637
import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider;
@@ -256,22 +257,25 @@ private Stream<String> streamPaths(List<Object> source,
256257
.map(pathMappedEndpoints::getPath);
257258
}
258259

259-
private String getEndpointId(Object source) {
260+
private EndpointId getEndpointId(Object source) {
261+
if (source instanceof EndpointId) {
262+
return (EndpointId) source;
263+
}
260264
if (source instanceof String) {
261-
return (String) source;
265+
return (EndpointId.of((String) source));
262266
}
263267
if (source instanceof Class) {
264268
return getEndpointId((Class<?>) source);
265269
}
266270
throw new IllegalStateException("Unsupported source " + source);
267271
}
268272

269-
private String getEndpointId(Class<?> source) {
273+
private EndpointId getEndpointId(Class<?> source) {
270274
Endpoint annotation = AnnotatedElementUtils.getMergedAnnotation(source,
271275
Endpoint.class);
272276
Assert.state(annotation != null,
273277
() -> "Class " + source + " is not annotated with @Endpoint");
274-
return annotation.id();
278+
return EndpointId.of(annotation.id());
275279
}
276280

277281
private List<RequestMatcher> getDelegateMatchers(

0 commit comments

Comments
 (0)